From 60dc4901e46205faf970fcbc50e7a265f1ac2c6d Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 5 Feb 2023 18:59:33 -0500 Subject: [PATCH] move trade wallet management functions from XmrWalletService to Trade persist security deposits in TradePeer set deposit tx and reserved key images when deposit tx created listen to account service in trade manager --- .../bisq/core/api/CoreDisputesService.java | 29 +- .../java/bisq/core/app/HavenoExecutable.java | 9 +- .../app/misc/ExecutableForAppWithP2p.java | 2 +- .../core/btc/wallet/XmrWalletService.java | 327 +++++++----------- .../core/support/dispute/DisputeManager.java | 2 +- .../java/bisq/core/trade/HavenoUtils.java | 4 + core/src/main/java/bisq/core/trade/Trade.java | 225 +++++++----- .../java/bisq/core/trade/TradeManager.java | 63 +++- .../bisq/core/trade/protocol/TradePeer.java | 3 + ...tratorSendInitTradeOrMultisigRequests.java | 7 +- .../tasks/BuyerPreparePaymentSentMessage.java | 4 +- .../tasks/MaybeSendSignContractRequest.java | 12 +- .../tasks/ProcessInitMultisigRequest.java | 4 +- .../tasks/SendDepositsConfirmedMessage.java | 6 +- .../pendingtrades/PendingTradesDataModel.java | 4 - proto/src/main/proto/pb.proto | 3 +- .../main/java/bisq/seednode/SeedNodeMain.java | 1 - 17 files changed, 370 insertions(+), 335 deletions(-) diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index e1f1a99f1e..e10ea648f9 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -58,7 +58,6 @@ public class CoreDisputesService { private final CoinFormatter formatter; private final KeyRing keyRing; private final TradeManager tradeManager; - private final XmrWalletService xmrWalletService; @Inject public CoreDisputesService(ArbitrationManager arbitrationManager, @@ -70,7 +69,6 @@ public class CoreDisputesService { this.formatter = formatter; this.keyRing = keyRing; this.tradeManager = tradeManager; - this.xmrWalletService = xmrWalletService; } public List getDisputes() { @@ -144,19 +142,19 @@ public class CoreDisputesService { // TODO: does not wait for success or error response public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { - try { - // get winning dispute - Dispute winningDispute; - Trade trade = tradeManager.getTrade(tradeId); - var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() - .filter(d -> tradeId.equals(d.getTradeId())) - .filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller())) - .findFirst(); - if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); - else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); + // get winning dispute + Dispute winningDispute; + Trade trade = tradeManager.getTrade(tradeId); + var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() + .filter(d -> tradeId.equals(d.getTradeId())) + .filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller())) + .findFirst(); + if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); + else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); - synchronized (trade) { + synchronized (trade) { + try { var closeDate = new Date(); var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); @@ -193,9 +191,10 @@ public class CoreDisputesService { }, (errMessage, err) -> { throw new IllegalStateException(errMessage, err); }); + } catch (Exception e) { + e.printStackTrace(); + throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage()); } - } catch (Exception e) { - throw new IllegalStateException(e); } } diff --git a/core/src/main/java/bisq/core/app/HavenoExecutable.java b/core/src/main/java/bisq/core/app/HavenoExecutable.java index 07959d04ee..659e7e221e 100644 --- a/core/src/main/java/bisq/core/app/HavenoExecutable.java +++ b/core/src/main/java/bisq/core/app/HavenoExecutable.java @@ -27,6 +27,7 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.setup.CorePersistedDataHost; import bisq.core.setup.CoreSetup; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.TradeManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.txproof.xmr.XmrTxProofService; @@ -50,7 +51,7 @@ import com.google.inject.Guice; import com.google.inject.Injector; import java.io.Console; - +import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -315,12 +316,14 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); - injector.getInstance(TradeManager.class).shutDown(); + log.info("TradeManager and XmrWalletService shutdown started"); + HavenoUtils.executeTasks(Arrays.asList( // shut down trade and main wallets at same time + () -> injector.getInstance(TradeManager.class).shutDown(), + () -> injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly))); log.info("OpenOfferManager shutdown started"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { log.info("OpenOfferManager shutdown completed"); - injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly); injector.getInstance(BtcWalletService.class).shutDown(); // We need to shutdown BitcoinJ before the P2PService as it uses Tor. diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java index d0e0a8c9ef..ca2d7832b9 100644 --- a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -86,6 +86,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { if (injector != null) { JsonFileManager.shutDownAllInstances(); injector.getInstance(ArbitratorManager.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(true); injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> { injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { module.close(injector); @@ -97,7 +98,6 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { }); }); injector.getInstance(WalletsSetup.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(true); injector.getInstance(BtcWalletService.class).shutDown(); })); // we wait max 5 sec. diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index 1b61cda04f..3a1e64410e 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -83,9 +83,8 @@ public class XmrWalletService { private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user"; private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_NAME = "haveno_XMR"; - private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_"; public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee - private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can abosrb miner fee up to percent + private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee private static final int NUM_MAX_BACKUP_WALLETS = 10; @@ -101,8 +100,6 @@ public class XmrWalletService { private TradeManager tradeManager; private MoneroWalletRpc wallet; - private Map multisigWallets; - private Map walletLocks = new HashMap(); private final Map> txCache = new HashMap>(); private boolean isShutDown = false; @@ -117,7 +114,6 @@ public class XmrWalletService { this.connectionsService = connectionsService; this.walletsSetup = walletsSetup; this.xmrAddressEntryList = xmrAddressEntryList; - this.multisigWallets = new HashMap(); this.walletDir = walletDir; this.rpcBindPort = rpcBindPort; this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME); @@ -133,20 +129,20 @@ public class XmrWalletService { @Override public void onAccountCreated() { - log.info(getClass() + ".accountService.onAccountCreated()"); + log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()"); initialize(); } @Override public void onAccountOpened() { - log.info(getClass() + ".accountService.onAccountOpened()"); + log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()"); initialize(); } @Override public void onAccountClosed() { - log.info(getClass() + ".accountService.onAccountClosed()"); - closeAllWallets(true); + log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()"); + closeMainWallet(true); } @Override @@ -203,91 +199,68 @@ public class XmrWalletService { return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); } - private synchronized void initWalletLock(String id) { - if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object()); + public boolean walletExists(String walletName) { + String path = walletDir.toString() + File.separator + walletName; + return new File(path + ".keys").exists(); } - public boolean multisigWalletExists(String tradeId) { - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); - } + public MoneroWalletRpc createWallet(String walletName) { + log.info("{}.createWallet({})", getClass().getSimpleName(), walletName); + if (isShutDown) throw new IllegalStateException("Cannot create wallet because shutting down"); + return createWallet(new MoneroWalletConfig() + .setPath(walletName) + .setPassword(getWalletPassword()), + null, + true); } - public MoneroWallet createMultisigWallet(String tradeId) { - log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port - multisigWallets.put(tradeId, multisigWallet); - return multisigWallet; - } + public MoneroWalletRpc openWallet(String walletName) { + log.info("{}.openWallet({})", getClass().getSimpleName(), walletName); + if (isShutDown) throw new IllegalStateException("Cannot open wallet because shutting down"); + return openWallet(new MoneroWalletConfig() + .setPath(walletName) + .setPassword(getWalletPassword()), + null); } - // TODO (woodser): provide progress notifications during open? - public MoneroWallet getMultisigWallet(String tradeId) { - if (isShutDown) throw new RuntimeException(getClass().getName() + " is shut down"); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); - String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId); - MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); - multisigWallets.put(tradeId, multisigWallet); - return multisigWallet; - } - } - - public void saveMultisigWallet(String tradeId) { - log.info("{}.saveMultisigWallet({})", getClass().getSimpleName(), tradeId); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - if (!walletExists(walletName)) { - log.warn("Multisig wallet for trade {} does not exist"); - return; - } - if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to save was not previously opened for trade " + tradeId); - saveWallet(multisigWallets.get(tradeId), true); - } - } - - private void saveWallet(MoneroWallet wallet, boolean backup) { + public void saveWallet(MoneroWallet wallet, boolean backup) { wallet.save(); if (backup) backupWallet(wallet.getPath()); } - public void closeMultisigWallet(String tradeId) { - log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); - MoneroWallet wallet = multisigWallets.remove(tradeId); - closeWallet(wallet, true); + public void closeWallet(MoneroWallet wallet, boolean save) { + log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save); + MoneroError err = null; + try { + String path = wallet.getPath(); + wallet.close(save); + if (save) backupWallet(path); + } catch (MoneroError e) { + err = e; } + MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet); + if (err != null) throw err; } - public boolean deleteMultisigWallet(String tradeId) { - log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - if (!walletExists(walletName)) return false; - if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); - deleteWallet(walletName); - return true; - } + public void deleteWallet(String walletName) { + log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); + if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); + String path = walletDir.toString() + File.separator + walletName; + if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); + if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); + if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); } - public void deleteMultisigWalletBackups(String tradeId) { - log.info("{}.deleteMultisigWalletBackups({})", getClass().getSimpleName(), tradeId); - initWalletLock(tradeId); - synchronized (walletLocks.get(tradeId)) { - String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; - deleteWalletBackups(walletName); - } + public void backupWallet(String walletName) { + FileUtil.rollingBackup(walletDir, walletName, NUM_MAX_BACKUP_WALLETS); + FileUtil.rollingBackup(walletDir, walletName + ".keys", NUM_MAX_BACKUP_WALLETS); + FileUtil.rollingBackup(walletDir, walletName + ".address.txt", NUM_MAX_BACKUP_WALLETS); + } + + public void deleteWalletBackups(String walletName) { + FileUtil.deleteRollingBackup(walletDir, walletName); + FileUtil.deleteRollingBackup(walletDir, walletName + ".keys"); + FileUtil.deleteRollingBackup(walletDir, walletName + ".address.txt"); } public MoneroTxWallet createTx(List destinations) { @@ -404,56 +377,58 @@ public class XmrWalletService { public void verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List keyImages) { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); - try { - - // verify tx not submitted to pool - MoneroTx tx = daemon.getTx(txHash); - if (tx != null) throw new RuntimeException("Tx is already submitted"); - - // submit tx to pool - MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? - if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); - tx = getTx(txHash); - - // verify key images - if (keyImages != null) { - Set txKeyImages = new HashSet(); - for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); - if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Tx inputs do not match claimed key images"); - } - - // verify unlock height - if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); - - // verify trade fee - String feeAddress = HavenoUtils.getTradeFeeAddress(); - MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); - if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); - if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); - - // verify miner fee - BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); - double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? - if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee()); - log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff); - - // verify sufficient security deposit - check = wallet.checkTxKey(txHash, txKey, address); - if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); - BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger(); - BigInteger actualSecurityDeposit = check.getReceivedAmount().subtract(sendAmount); - if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit); - - // verify deposit amount + miner fee within dust tolerance - BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); - BigInteger actualDepositAndFee = check.getReceivedAmount().add(tx.getFee()); - if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee); - } finally { + synchronized (daemon) { try { - daemon.flushTxPool(txHash); // flush tx from pool - } catch (MoneroRpcError err) { - System.out.println(daemon.getRpcConnection()); - throw err.getCode() == -32601 ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; + + // verify tx not submitted to pool + MoneroTx tx = daemon.getTx(txHash); + if (tx != null) throw new RuntimeException("Tx is already submitted"); + + // submit tx to pool + MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? + if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); + tx = getTx(txHash); + + // verify key images + if (keyImages != null) { + Set txKeyImages = new HashSet(); + for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); + if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Tx inputs do not match claimed key images"); + } + + // verify unlock height + if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); + + // verify trade fee + String feeAddress = HavenoUtils.getTradeFeeAddress(); + MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); + if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); + if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); + + // verify miner fee + BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); + double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? + if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee()); + log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff); + + // verify sufficient security deposit + check = wallet.checkTxKey(txHash, txKey, address); + if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); + BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger(); + BigInteger actualSecurityDeposit = check.getReceivedAmount().subtract(sendAmount); + if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit); + + // verify deposit amount + miner fee within dust tolerance + BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); + BigInteger actualDepositAndFee = check.getReceivedAmount().add(tx.getFee()); + if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee); + } finally { + try { + daemon.flushTxPool(txHash); // flush tx from pool + } catch (MoneroRpcError err) { + System.out.println(daemon.getRpcConnection()); + throw err.getCode() == -32601 ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; + } } } } @@ -528,9 +503,19 @@ public class XmrWalletService { } } + private void closeMainWallet(boolean save) { + try { + closeWallet(wallet, true); + wallet = null; + walletListeners.clear(); + } catch (Exception e) { + log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); + } + } + public void shutDown(boolean save) { this.isShutDown = true; - closeAllWallets(save); + closeMainWallet(save); } // ------------------------------ PRIVATE HELPERS ------------------------- @@ -544,11 +529,6 @@ public class XmrWalletService { connectionsService.addListener(newConnection -> setDaemonConnection(newConnection)); } - private boolean walletExists(String walletName) { - String path = walletDir.toString() + File.separator + walletName; - return new File(path + ".keys").exists(); - } - private void maybeInitMainWallet() { if (wallet != null) throw new RuntimeException("Main wallet is already initialized"); @@ -605,7 +585,7 @@ public class XmrWalletService { } else { walletRpc.setDaemonConnection(connection); } - log.info("Done creating wallet " + config.getPath()); + log.info("Done creating wallet " + walletRpc.getPath()); return walletRpc; } catch (Exception e) { e.printStackTrace(); @@ -624,6 +604,7 @@ public class XmrWalletService { log.info("Opening wallet " + config.getPath()); walletRpc.openWallet(config); walletRpc.setDaemonConnection(connectionsService.getConnection()); + log.info("Done opening wallet " + walletRpc.getPath()); return walletRpc; } catch (Exception e) { e.printStackTrace(); @@ -718,80 +699,18 @@ public class XmrWalletService { } }); - // create tasks to change multisig wallet passwords - List tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList()); - for (String tradeId : tradeIds) { + // create tasks to change trade wallet passwords + List trades = tradeManager.getAllTrades(); + for (Trade trade : trades) { tasks.add(() -> { - MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open - if (multisigWallet == null) return; - multisigWallet.changePassword(oldPassword, newPassword); - saveMultisigWallet(tradeId); + if (trade.walletExists()) { + trade.changeWalletPassword(oldPassword, newPassword); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open + } }); } // excute tasks in parallel - HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.size())); - } - - private void closeWallet(MoneroWallet walletRpc, boolean save) { - log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save); - MoneroError err = null; - try { - String path = walletRpc.getPath(); - walletRpc.close(save); - if (save) backupWallet(path); - } catch (MoneroError e) { - err = e; - } - MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc); - if (err != null) throw err; - } - - private void deleteWallet(String walletName) { - log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); - if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); - String path = walletDir.toString() + File.separator + walletName; - if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); - } - - private void closeAllWallets(boolean save) { - - // collect wallets to shutdown - List openWallets = new ArrayList(); - if (wallet != null) openWallets.add(wallet); - for (String multisigWalletKey : multisigWallets.keySet()) { - openWallets.add(multisigWallets.get(multisigWalletKey)); - } - - // close wallets in parallel - Set tasks = new HashSet(); - for (MoneroWallet wallet : openWallets) tasks.add(() -> { - try { - closeWallet(wallet, true); - } catch (Exception e) { - log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); - } - }); - HavenoUtils.executeTasks(tasks); - - // clear wallets - wallet = null; - multisigWallets.clear(); - walletListeners.clear(); - } - - private void backupWallet(String walletName) { - FileUtil.rollingBackup(walletDir, walletName, NUM_MAX_BACKUP_WALLETS); - FileUtil.rollingBackup(walletDir, walletName + ".keys", NUM_MAX_BACKUP_WALLETS); - FileUtil.rollingBackup(walletDir, walletName + ".address.txt", NUM_MAX_BACKUP_WALLETS); - } - - private void deleteWalletBackups(String walletName) { - FileUtil.deleteRollingBackup(walletDir, walletName); - FileUtil.deleteRollingBackup(walletDir, walletName + ".keys"); - FileUtil.deleteRollingBackup(walletDir, walletName + ".address.txt"); + HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size())); } // ----------------------------- LEGACY APP ------------------------------- diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 8c9efaca93..2faeeb5789 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -445,7 +445,7 @@ public abstract class DisputeManager> extends Sup PubKeyRing senderPubKeyRing = null; try { - // intialize + // initialize T disputeList = getDisputeList(); if (disputeList == null) { log.warn("disputes is null"); diff --git a/core/src/main/java/bisq/core/trade/HavenoUtils.java b/core/src/main/java/bisq/core/trade/HavenoUtils.java index 553d4c2ffe..aaecc59ee9 100644 --- a/core/src/main/java/bisq/core/trade/HavenoUtils.java +++ b/core/src/main/java/bisq/core/trade/HavenoUtils.java @@ -86,6 +86,10 @@ public class HavenoUtils { return atomicUnitsToXmr(centinerosToAtomicUnits(centineros)); } + public static Coin centinerosToCoin(long centineros) { + return atomicUnitsToCoin(centinerosToAtomicUnits(centineros)); + } + public static long atomicUnitsToCentineros(long atomicUnits) { // TODO: atomic units should be BigInteger; remove this? return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue(); } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 906b71ba83..97f8f1e63a 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -115,6 +115,10 @@ import monero.wallet.model.MoneroWalletListener; @Slf4j public abstract class Trade implements Tradable, Model { + private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_"; + private MoneroWallet wallet; // trade wallet + private Object walletLock = new Object(); + /////////////////////////////////////////////////////////////////////////////////////////// // Enums /////////////////////////////////////////////////////////////////////////////////////////// @@ -412,7 +416,6 @@ public abstract class Trade implements Tradable, Model { @Getter @Setter private long lockTime; - @Getter @Setter private long startTime; // added for haveno @Getter @@ -583,7 +586,7 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// - // API + // INITIALIZATION /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(ProcessModelServiceProvider serviceProvider) { @@ -680,11 +683,42 @@ public abstract class Trade implements Tradable, Model { return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); } + /////////////////////////////////////////////////////////////////////////////////////////// + // WALLET MANAGEMENT + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean walletExists() { + synchronized (walletLock) { + return xmrWalletService.walletExists(MONERO_TRADE_WALLET_PREFIX + getId()); + } + } + + public MoneroWallet createWallet() { + synchronized (walletLock) { + if (walletExists()) throw new RuntimeException("Cannot create trade wallet because it already exists"); + wallet = xmrWalletService.createWallet(getWalletName()); + return wallet; + } + } + + public MoneroWallet getWallet() { + synchronized (walletLock) { + if (wallet != null) return wallet; + if (!walletExists()) return null; + if (isInitialized) wallet = xmrWalletService.openWallet(getWalletName()); + return wallet; + } + } + + private String getWalletName() { + return MONERO_TRADE_WALLET_PREFIX + getId(); + } + public void checkWalletConnection() { CoreMoneroConnectionsService connectionService = xmrWalletService.getConnectionsService(); connectionService.checkConnection(); connectionService.verifyConnection(); - if (!getWallet().isConnectedToDaemon()) throw new RuntimeException("Wallet is not connected to a Monero node"); + if (!getWallet().isConnectedToDaemon()) throw new RuntimeException("Trade wallet is not connected to a Monero node"); } public boolean isWalletConnected() { @@ -696,6 +730,88 @@ public abstract class Trade implements Tradable, Model { } } + public void syncWallet() { + if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); + log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); + getWallet().sync(); + pollWallet(); + log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); + updateWalletRefreshPeriod(); + } + + private void trySyncWallet() { + try { + syncWallet(); + } catch (Exception e) { + if (isInitialized) { + log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + e.printStackTrace(); + } + } + } + + public void syncWalletNormallyForMs(long syncNormalDuration) { + syncNormalStartTime = System.currentTimeMillis(); + setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs()); + UserThread.runAfter(() -> { + if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod(); + }, syncNormalDuration); + } + + public void changeWalletPassword(String oldPassword, String newPassword) { + synchronized (walletLock) { + getWallet().changePassword(oldPassword, newPassword); + saveWallet(); + } + } + + public void saveWallet() { + synchronized (walletLock) { + if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId()); + xmrWalletService.saveWallet(wallet, true); + } + } + + private void closeWallet() { + synchronized (walletLock) { + if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId()); + if (wallet.getPath() == null) log.warn("HOW DID PATH BECOME NULL?"); + xmrWalletService.closeWallet(wallet, true); + wallet = null; + } + } + + public void deleteWallet() { + synchronized (walletLock) { + if (walletExists()) { + + // check if funds deposited but payout not unlocked + if (isDepositsPublished() && !isPayoutUnlocked()) { + log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId()); + return; + } + + // close and delete trade wallet + if (wallet != null) closeWallet(); + xmrWalletService.deleteWallet(getWalletName()); + + // delete trade wallet backups unless deposits requested and payouts not unlocked + if (isDepositRequested() && !isPayoutUnlocked()) { + log.warn("Refusing to delete backup wallet for {} {} in the small chance it becomes funded", getClass().getSimpleName(), getId()); + return; + } + xmrWalletService.deleteWalletBackups(getWalletName()); + } else { + log.warn("Multisig wallet to delete for trade {} does not exist", getId()); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTOCOL API + /////////////////////////////////////////////////////////////////////////////////////////// + /** * Create a contract based on the current state. * @@ -959,70 +1075,17 @@ public abstract class Trade implements Tradable, Model { } } - public MoneroWallet getWallet() { - return xmrWalletService.multisigWalletExists(getId()) ? xmrWalletService.getMultisigWallet(getId()) : null; - } - - public void syncWallet() { - if (getWallet() == null) throw new RuntimeException("Cannot sync multisig wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); - if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync multisig wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); - log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); - getWallet().sync(); - pollWallet(); - log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); - updateWalletRefreshPeriod(); - } - - private void trySyncWallet() { - try { - syncWallet(); - } catch (Exception e) { - if (isInitialized) log.warn("Error syncing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); - } - } - - public void syncWalletNormallyForMs(long syncNormalDuration) { - syncNormalStartTime = System.currentTimeMillis(); - setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs()); - UserThread.runAfter(() -> { - if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod(); - }, syncNormalDuration); - } - - public void saveWallet() { - xmrWalletService.saveMultisigWallet(getId()); - } - - public void deleteWallet() { - if (xmrWalletService.multisigWalletExists(getId())) { - - // delete trade wallet unless funded - if (isDepositsPublished() && !isPayoutUnlocked()) { - log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId()); - return; - } - xmrWalletService.deleteMultisigWallet(getId()); - - // delete trade wallet backups unless possibly funded - boolean possiblyFunded = isDepositRequested() && !isPayoutUnlocked(); - if (possiblyFunded) { - log.warn("Refusing to delete backup wallet for {} {} in the small chance it becomes funded", getClass().getSimpleName(), getId()); - return; - } - xmrWalletService.deleteMultisigWalletBackups(getId()); - } else { - log.warn("Multisig wallet to delete for trade {} does not exist", getId()); - } - } - public void shutDown() { - isInitialized = false; - if (txPollLooper != null) { - txPollLooper.stop(); - txPollLooper = null; + synchronized (walletLock) { + isInitialized = false; + if (wallet != null) closeWallet(); + if (txPollLooper != null) { + txPollLooper.stop(); + txPollLooper = null; + } + if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); + if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); } - if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); - if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1458,23 +1521,13 @@ public abstract class Trade implements Tradable, Model { } public Coin getBuyerSecurityDeposit() { - if (this.getBuyer().getDepositTxHash() == null) return null; - try { - MoneroTxWallet depositTx = getWallet().getTx(this.getBuyer().getDepositTxHash()); // TODO (monero-java): return null if tx id not found instead of throw exception - return HavenoUtils.atomicUnitsToCoin(depositTx.getIncomingAmount()); - } catch (Exception e) { - return null; - } + if (getBuyer().getDepositTxHash() == null) return null; + return HavenoUtils.centinerosToCoin(getBuyer().getSecurityDeposit()); } public Coin getSellerSecurityDeposit() { - if (this.getSeller().getDepositTxHash() == null) return null; - try { - MoneroTxWallet depositTx = getWallet().getTx(this.getSeller().getDepositTxHash()); // TODO (monero-java): return null if tx id not found instead of throw exception - return HavenoUtils.atomicUnitsToCoin(depositTx.getIncomingAmount()).subtract(getAmount()); - } catch (Exception e) { - return null; - } + if (getSeller().getDepositTxHash() == null) return null; + return HavenoUtils.centinerosToCoin(getSeller().getSecurityDeposit()); } @Nullable @@ -1603,11 +1656,23 @@ public abstract class Trade implements Tradable, Model { // check deposit txs if (!isDepositsUnlocked()) { if (txs.size() == 2) { - setStateDepositsPublished(); + + // update trader state boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); + // set security deposits + if (getBuyer().getSecurityDeposit() == 0) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(HavenoUtils.coinToAtomicUnits(getAmount())); + getBuyer().setSecurityDeposit(HavenoUtils.atomicUnitsToCentineros(buyerSecurityDeposit)); + getSeller().setSecurityDeposit(HavenoUtils.atomicUnitsToCentineros(sellerSecurityDeposit)); + } + + // set deposits published state + setStateDepositsPublished(); + // check if deposit txs confirmed if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 0ecfb5480d..9e4958bee1 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -17,6 +17,8 @@ package bisq.core.trade; +import bisq.core.api.AccountServiceListener; +import bisq.core.api.CoreAccountService; import bisq.core.api.CoreNotificationService; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.wallet.XmrWalletService; @@ -121,6 +123,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private final User user; @Getter private final KeyRing keyRing; + private final CoreAccountService accountService; private final XmrWalletService xmrWalletService; private final CoreNotificationService notificationService; private final OfferBookService offerBookService; @@ -158,6 +161,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Inject public TradeManager(User user, KeyRing keyRing, + CoreAccountService accountService, XmrWalletService xmrWalletService, CoreNotificationService notificationService, OfferBookService offerBookService, @@ -177,6 +181,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ReferralIdService referralIdService) { this.user = user; this.keyRing = keyRing; + this.accountService = accountService; this.xmrWalletService = xmrWalletService; this.notificationService = notificationService; this.offerBookService = offerBookService; @@ -250,6 +255,39 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// public void onAllServicesInitialized() { + + // initialize + initialize(); + + // listen for account updates + accountService.addListener(new AccountServiceListener() { + + @Override + public void onAccountCreated() { + log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()"); + initialize(); + } + + @Override + public void onAccountOpened() { + log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()"); + initialize(); + } + + @Override + public void onAccountClosed() { + log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()"); + closeAllTrades(); + } + + @Override + public void onPasswordChanged(String oldPassword, String newPassword) { + // handled in XmrWalletService + } + }); + } + + private void initialize() { if (p2PService.isBootstrapped()) { new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread } else { @@ -272,6 +310,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public void shutDown() { isShutDown = true; + closeAllTrades(); + } + + private void closeAllTrades() { // collect trades to shutdown Set trades = new HashSet(); @@ -341,11 +383,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void initPersistedTrades() { - // get all trades // TODO: getAllTrades() - List trades = new ArrayList(); - trades.addAll(tradableList.getList()); - trades.addAll(closedTradableManager.getClosedTrades()); - trades.addAll(failedTradesManager.getObservableList()); + // get all trades + List trades = getAllTrades(); // open trades in parallel since each may open a multisig wallet int threadPoolSize = 10; @@ -1037,6 +1076,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } + public List getAllTrades() { + List trades = new ArrayList(); + trades.addAll(tradableList.getList()); + trades.addAll(closedTradableManager.getClosedTrades()); + trades.addAll(failedTradesManager.getObservableList()); + return trades; + } + public List getOpenTrades() { synchronized (tradableList) { return ImmutableList.copyOf(getObservableList().stream() @@ -1085,7 +1132,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } // remove trade if wallet deleted - if (!xmrWalletService.multisigWalletExists(trade.getId())) { + if (!trade.walletExists()) { removeTrade(trade); return; } @@ -1093,7 +1140,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // remove trade and wallet unless deposit requested without nack if (!trade.isDepositRequested() || trade.isDepositFailed()) { removeTrade(trade); - if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet(); + if (trade.walletExists()) trade.deleteWallet(); } else { scheduleDeletionIfUnfunded(trade); } @@ -1115,7 +1162,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId()); removeTrade(trade); failedTradesManager.removeTrade(trade); - if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet(); + if (trade.walletExists()) trade.deleteWallet(); } else { log.warn("Refusing to delete {} {} after protocol timeout because its wallet might be funded", trade.getClass().getSimpleName(), trade.getId()); } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradePeer.java b/core/src/main/java/bisq/core/trade/protocol/TradePeer.java index 6654955e5a..c30b40d282 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradePeer.java @@ -127,6 +127,7 @@ public final class TradePeer implements PersistablePayload { private String depositTxHex; @Nullable private String depositTxKey; + private long securityDeposit; @Nullable private String updatedMultisigHex; @@ -168,6 +169,7 @@ public final class TradePeer implements PersistablePayload { Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); + Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); builder.setCurrentDate(currentDate); @@ -218,6 +220,7 @@ public final class TradePeer implements PersistablePayload { tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); + tradePeer.setSecurityDeposit(proto.getSecurityDeposit()); tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); return tradePeer; } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java index d22e0c3204..58b5ae27f0 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ArbitratorSendInitTradeOrMultisigRequests.java @@ -19,16 +19,11 @@ package bisq.core.trade.protocol.tasks; import bisq.common.app.Version; -import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; import bisq.core.trade.Trade; import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitTradeRequest; -import bisq.core.trade.protocol.TradeListener; -import bisq.network.p2p.AckMessage; -import bisq.network.p2p.NodeAddress; import bisq.network.p2p.SendDirectMessageListener; -import com.google.common.base.Charsets; import java.util.Date; import java.util.UUID; import lombok.extern.slf4j.Slf4j; @@ -123,7 +118,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask { } // create wallet for multisig - MoneroWallet multisigWallet = processModel.getXmrWalletService().createMultisigWallet(trade.getId()); + MoneroWallet multisigWallet = trade.createWallet(); // prepare multisig String preparedHex = multisigWallet.prepareMultisig(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index afd70a6908..237068a3c7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -17,7 +17,6 @@ package bisq.core.trade.protocol.tasks; -import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.Trade; import bisq.common.taskrunner.TaskRunner; @@ -67,8 +66,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { checkNotNull(trade.getOffer(), "offer must not be null"); // get multisig wallet - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); + MoneroWallet multisigWallet = trade.getWallet(); // import multisig hex List updatedMultisigHexes = new ArrayList(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 4a35bc2397..c0f088ea4d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -27,12 +27,16 @@ import bisq.core.trade.Trade; import bisq.core.trade.Trade.State; import bisq.core.trade.messages.SignContractRequest; import bisq.network.p2p.SendDirectMessageListener; + +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; import com.google.common.base.Charsets; import lombok.extern.slf4j.Slf4j; +import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; // TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest @@ -73,9 +77,15 @@ public class MaybeSendSignContractRequest extends TradeTask { // create deposit tx and freeze inputs MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade); + // collect reserved key images + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + // save process state - processModel.setDepositTxXmr(depositTx); // TODO: trade.getSelf().setDepositTx() + processModel.setDepositTxXmr(depositTx); // TODO: redundant with trade.getSelf().setDepositTx(), remove? + trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setReserveTxKeyImages(reservedKeyImages); trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade)); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index c34a53a257..f46e845f29 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -82,12 +82,12 @@ public class ProcessInitMultisigRequest extends TradeTask { boolean updateParticipants = false; if (trade.getSelf().getPreparedMultisigHex() == null) { log.info("Preparing multisig wallet for trade {}", trade.getId()); - multisigWallet = xmrWalletService.createMultisigWallet(trade.getId()); + multisigWallet = trade.createWallet(); trade.getSelf().setPreparedMultisigHex(multisigWallet.prepareMultisig()); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_PREPARED); updateParticipants = true; } else if (processModel.getMultisigAddress() == null) { - multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); + multisigWallet = trade.getWallet(); } // make multisig if applicable diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index 9cc57f3891..614fd61e65 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -17,7 +17,6 @@ package bisq.core.trade.protocol.tasks; -import bisq.core.btc.wallet.XmrWalletService; import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; import bisq.core.trade.messages.DepositsConfirmedMessage; @@ -27,7 +26,6 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; -import monero.wallet.MoneroWallet; /** * Send message on first confirmation to decrypt peer payment account and update multisig hex. @@ -62,9 +60,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas // export multisig hex once if (trade.getSelf().getUpdatedMultisigHex() == null) { - XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); - MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId); - trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); + trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); processModel.getTradeManager().requestPersistence(); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index fb9a6b8392..ee76a91e51 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -89,9 +89,6 @@ import javax.inject.Named; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import monero.daemon.model.MoneroTx; -import monero.wallet.MoneroWallet; - public class PendingTradesDataModel extends ActivatableDataModel { @Getter public final TradeManager tradeManager; @@ -466,7 +463,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { byte[] payoutTxSerialized = null; String payoutTxHashAsString = null; - MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); if (trade.getPayoutTxId() != null) { // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxHashAsString = payoutTx.getHashAsString(); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 21ef33df13..8bbbf9c118 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1749,7 +1749,8 @@ message TradePeer { string deposit_tx_hash = 1008; string deposit_tx_hex = 1009; string deposit_tx_key = 1010; - string updated_multisig_hex = 1011; + int64 security_deposit = 1011; + string updated_multisig_hex = 1012; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java index f675f8b5f3..3fc79fc11e 100644 --- a/seednode/src/main/java/bisq/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/bisq/seednode/SeedNodeMain.java @@ -33,7 +33,6 @@ import bisq.common.UserThread; import bisq.common.app.AppModule; import bisq.common.app.Capabilities; import bisq.common.app.Capability; -import bisq.common.app.DevEnv; import bisq.common.config.BaseCurrencyNetwork; import bisq.common.config.Config; import bisq.common.handlers.ResultHandler;