From 6dfa1841f8a992e63117ea2c29167d0e5fea7b7f Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 25 May 2024 12:47:38 -0400 Subject: [PATCH] force restart trade wallet on confirm payment if missing txs to fix #960 --- .../haveno/core/offer/OpenOfferManager.java | 34 +++++----- .../java/haveno/core/trade/HavenoUtils.java | 2 +- .../main/java/haveno/core/trade/Trade.java | 67 +++++++++---------- .../core/trade/protocol/ProcessModel.java | 2 +- .../core/xmr/wallet/XmrWalletService.java | 2 +- 5 files changed, 54 insertions(+), 53 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a283755a89..2b5b074034 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1068,24 +1068,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe MoneroTxWallet splitOutputTx = null; synchronized (XmrWalletService.WALLET_LOCK) { XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); - long startTime = System.currentTimeMillis(); - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); - splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() - .setAccountIndex(0) - .setAddress(entry.getAddressString()) - .setAmount(reserveAmount) - .setRelay(true) - .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); - break; - } catch (Exception e) { - log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); - if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + synchronized (HavenoUtils.getWalletFunctionLock()) { + long startTime = System.currentTimeMillis(); + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); + splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .setAddress(entry.getAddressString()) + .setAmount(reserveAmount) + .setRelay(true) + .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); + break; + } catch (Exception e) { + log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } } + log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); } - log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); } // set split tx diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index d9acf03d7e..ed2563c47e 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -79,7 +79,7 @@ public class HavenoUtils { // synchronize requests to the daemon private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) - private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create tx, import multisig hex) + private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs) private static Object DAEMON_LOCK = new Object(); public static Object getDaemonLock() { return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object(); diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9665bad45a..526e70e812 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -94,7 +94,6 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; -import monero.common.MoneroUtils; import monero.common.TaskLooper; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroKeyImage; @@ -964,6 +963,7 @@ public abstract class Trade implements Tradable, Model { private void forceCloseWallet() { if (wallet != null) { xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); + stopPolling(); wallet = null; } } @@ -1099,36 +1099,18 @@ public abstract class Trade implements Tradable, Model { // check if multisig import needed if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); - // gather info - String sellerPayoutAddress = this.getSeller().getPayoutAddressString(); - String buyerPayoutAddress = this.getBuyer().getPayoutAddressString(); - Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); - Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); - - // TODO: wallet query to get deposit txs can sometimes return null, maybe when disconnected? - if (wallet.getTx(getSeller().getDepositTxHash()) == null || wallet.getTx(getBuyer().getDepositTxHash()) == null) { - String warningMsg = "Issue detected with trade wallet " + getShortId() + ". Please send logs to Haveno developers and restart your application if you encounter further problems:"; - warningMsg += "\n\nSeller deposit tx id: " + getSeller().getDepositTxHash(); - warningMsg += "\nBuyer deposit tx id: " + getBuyer().getDepositTxHash(); - warningMsg += "\nSeller deposit tx is initialized: " + (getSeller().getDepositTx() != null); - warningMsg += "\nBuyer deposit tx is initialized: " + (getBuyer().getDepositTx() != null); - log.warn(warningMsg); - - // request with logging - int previousLogLevel = MoneroUtils.getLogLevel(); - MoneroUtils.setLogLevel(3); - log.warn("Requesting seller tx with logging"); - MoneroTxWallet fetchedTx = wallet.getTx(getSeller().getDepositTxHash()); - log.info("Seller tx: " + fetchedTx); - log.warn("Requesting buyer tx with logging"); - fetchedTx = wallet.getTx(getBuyer().getDepositTxHash()); - log.info("Buyer tx: " + fetchedTx); - MoneroUtils.setLogLevel(previousLogLevel); - - // set top level error message to notify user - HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg); + // TODO: wallet sometimes returns empty data, after disconnect? + List txs = wallet.getTxs(); // TODO: this fetches from pool + if (txs.isEmpty()) { + log.warn("Restarting wallet for {} {} because deposit txs are missing to create payout tx", getClass().getSimpleName(), getId()); + forceRestartTradeWallet(); } + // gather info + String sellerPayoutAddress = getSeller().getPayoutAddressString(); + String buyerPayoutAddress = getBuyer().getPayoutAddressString(); + Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); + Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); BigInteger tradeAmount = getAmount(); @@ -1163,7 +1145,7 @@ public abstract class Trade implements Tradable, Model { return createTx(txConfig); } catch (Exception e) { if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); - log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + log.warn("Failed to create dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1183,6 +1165,22 @@ public abstract class Trade implements Tradable, Model { public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); + // TODO: wallet sometimes returns empty data, after disconnect? detect this condition with failure tolerance + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + List txs = wallet.getTxs(); // TODO: this fetches from pool + if (txs.isEmpty()) { + log.warn("Restarting wallet for {} {} because deposit txs are missing to process payout tx", getClass().getSimpleName(), getId()); + forceRestartTradeWallet(); + } + break; + } catch (Exception e) { + log.warn("Failed get wallet txs, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + // gather relevant info MoneroWallet wallet = getWallet(); Contract contract = getContract(); @@ -2514,12 +2512,13 @@ public abstract class Trade implements Tradable, Model { } private void forceRestartTradeWallet() { - log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); if (isShutDownStarted || restartInProgress) return; + log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); restartInProgress = true; forceCloseWallet(); if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; + doPollWallet(); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); } @@ -2609,15 +2608,15 @@ public abstract class Trade implements Tradable, Model { // skip if arbitrator if (this instanceof ArbitratorTrade) return; - // freeze outputs until spent - xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()); - // close open offer or reset address entries if (this instanceof MakerTrade) { processModel.getOpenOfferManager().closeOpenOffer(getOffer()); } else { getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); } + + // freeze outputs until spent + ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages())); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index aca3211f9b..54aaa65d37 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -114,7 +114,7 @@ public class ProcessModel implements Model, PersistablePayload { private byte[] payoutTxSignature; @Nullable @Setter - private byte[] preparedDepositTx; + private byte[] preparedDepositTx; // TODO: remove this unused field @Setter private boolean useSavingsWallet; @Setter diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 63c48bd0c3..e6398f9ff9 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1735,9 +1735,9 @@ public class XmrWalletService { private void forceCloseMainWallet() { isClosingWallet = true; + forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); stopPolling(); stopSyncWithProgress(); - forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); wallet = null; }