diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index e1f1a99f..e10ea648 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 07959d04..659e7e22 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 d0e0a8c9..ca2d7832 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 1b61cda0..3a1e6441 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 8c9efaca..2faeeb57 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 553d4c2f..aaecc59e 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 906b71ba..97f8f1e6 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 0ecfb548..9e4958be 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 6654955e..c30b40d2 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 d22e0c32..58b5ae27 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 afd70a69..237068a3 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 4a35bc23..c0f088ea 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 c34a53a2..f46e845f 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 9cc57f38..614fd61e 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 fb9a6b83..ee76a91e 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 21ef33df..8bbbf9c1 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 f675f8b5..3fc79fc1 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;