From 1b5c03bce84e5785c5f7bcdee24cab875f54ca70 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 4 Aug 2024 18:03:17 -0400 Subject: [PATCH] refactor syncing wallet with progress and switching connections --- .../haveno/core/api/CoreWalletsService.java | 2 +- .../haveno/core/api/XmrConnectionService.java | 14 +- .../java/haveno/core/app/WalletAppSetup.java | 58 ++--- .../haveno/core/offer/OpenOfferManager.java | 4 +- .../tasks/MakerReserveOfferFunds.java | 13 +- .../arbitration/ArbitrationManager.java | 4 +- .../java/haveno/core/trade/HavenoUtils.java | 12 + .../main/java/haveno/core/trade/Trade.java | 41 ++-- .../tasks/MaybeSendSignContractRequest.java | 5 +- .../tasks/TakerReserveTradeFunds.java | 5 +- .../core/xmr/wallet/XmrWalletService.java | 219 +++++++++++------- .../main/funds/withdrawal/WithdrawalView.java | 4 +- 12 files changed, 240 insertions(+), 141 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreWalletsService.java b/core/src/main/java/haveno/core/api/CoreWalletsService.java index 351ec7d1..68ec8c13 100644 --- a/core/src/main/java/haveno/core/api/CoreWalletsService.java +++ b/core/src/main/java/haveno/core/api/CoreWalletsService.java @@ -178,7 +178,7 @@ class CoreWalletsService { verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { - return xmrWalletService.getWallet().relayTx(metadata); + return xmrWalletService.relayTx(metadata); } catch (Exception ex) { log.error("", ex); throw new IllegalStateException(ex); diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 6f9b4bb3..d9f2e223 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -273,7 +273,11 @@ public final class XmrConnectionService { } public synchronized boolean requestSwitchToNextBestConnection() { - log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); + return requestSwitchToNextBestConnection(null); + } + + public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { + log.warn("Requesting switch to next best monerod, source monerod={}", sourceConnection == null ? getConnection() == null ? null : getConnection().getUri() : sourceConnection.getUri()); // skip if shut down started if (isShutDownStarted) { @@ -281,9 +285,15 @@ public final class XmrConnectionService { return false; } + // skip if connection is already switched + if (sourceConnection != null && sourceConnection != getConnection()) { + log.warn("Skipping switch to next best Monero connection because source connection is not current connection"); + return false; + } + // skip if connection is fixed if (isFixedConnection() || !connectionManager.getAutoSwitch()) { - log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); + log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); return false; } diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 7a1458ab..1f7946ea 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -132,35 +132,14 @@ public class WalletAppSetup { (numConnectionUpdates, walletDownloadPercentage, walletHeight, exception, errorMsg) -> { String result; if (exception == null && errorMsg == null) { - - // update wallet sync progress - double walletDownloadPercentageD = (double) walletDownloadPercentage; - xmrWalletSyncProgress.set(walletDownloadPercentageD); - Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight; - String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : ""; - if (walletDownloadPercentageD == 1) { - String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString); - String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable - result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); - getXmrSplashSyncIconId().set("image-connection-synced"); - downloadCompleteHandler.run(); - } else if (walletDownloadPercentageD > 0) { - String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD)); - result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); - getXmrSplashSyncIconId().set(""); // clear synced icon - } else { - - // update daemon sync progress - double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue(); + + // update daemon sync progress + double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue(); + Long bestChainHeight = xmrConnectionService.chainHeightProperty().get(); + String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : ""; + if (chainDownloadPercentageD < 1) { xmrDaemonSyncProgress.set(chainDownloadPercentageD); - Long bestChainHeight = xmrConnectionService.chainHeightProperty().get(); - String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : ""; - if (chainDownloadPercentageD == 1) { - String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString); - String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable - result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); - getXmrSplashSyncIconId().set("image-connection-synced"); - } else if (chainDownloadPercentageD > 0.0) { + if (chainDownloadPercentageD > 0.0) { String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD)); result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); } else { @@ -168,6 +147,29 @@ public class WalletAppSetup { Res.get("mainView.footer.xmrInfo.connectingTo"), getXmrDaemonNetworkAsString()); } + } else { + + // update wallet sync progress + double walletDownloadPercentageD = (double) walletDownloadPercentage; + xmrWalletSyncProgress.set(walletDownloadPercentageD); + Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight; + String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : ""; + if (walletDownloadPercentageD == 1) { + String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString); + String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable + result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); + getXmrSplashSyncIconId().set("image-connection-synced"); + downloadCompleteHandler.run(); + } else if (walletDownloadPercentageD >= 0) { + String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD)); + result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); + getXmrSplashSyncIconId().set(""); // clear synced icon + } else { + String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString); + String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable + result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); + getXmrSplashSyncIconId().set("image-connection-synced"); + } } } else { result = Res.get("mainView.footer.xmrInfo", diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 9c0626fa..537c5fe0 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -110,6 +110,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.Getter; +import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroIncomingTransfer; @@ -1089,6 +1090,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (HavenoUtils.getWalletFunctionLock()) { long startTime = System.currentTimeMillis(); for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() @@ -1101,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } catch (Exception e) { if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + xmrWalletService.handleWalletError(e, sourceConnection); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 200cad8c..305ff6e2 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -32,6 +32,7 @@ import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; @@ -82,14 +83,16 @@ public class MakerReserveOfferFunds extends Task { try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = model.getXmrWalletService().getConnectionService().getConnection(); try { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); + model.getXmrWalletService().handleWalletError(e, sourceConnection); + verifyPending(); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout - if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -129,7 +132,11 @@ public class MakerReserveOfferFunds extends Task { } } - public void verifyPending() { - if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); + private boolean isPending() { + return model.getOpenOffer().isPending(); + } + + private void verifyPending() { + if (!isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); } } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 4941fcf4..7bc74d72 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -78,6 +78,7 @@ import haveno.network.p2p.P2PService; import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.MessageListener; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; @@ -501,6 +502,7 @@ public final class ArbitrationManager extends DisputeManager txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed @@ -509,7 +511,7 @@ public final class ArbitrationManager extends DisputeManager requestSwitchToNextBestConnection(), getId()); + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); throw e; } } @@ -2561,11 +2567,12 @@ public abstract class Trade implements Tradable, Model { // rescan spent outputs to detect unconfirmed payout tx if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { + MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { wallet.rescanSpent(); } catch (Exception e) { log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); - ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); // do not block polling thread } } @@ -2610,12 +2617,11 @@ public abstract class Trade implements Tradable, Model { } } } catch (Exception e) { - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartTradeWallet(); + if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); else { boolean isWalletConnected = isWalletConnectedToDaemon(); if (wallet != null && !isShutDownStarted && isWalletConnected) { - log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); + log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection()); //e.printStackTrace(); } } @@ -2675,7 +2681,8 @@ public abstract class Trade implements Tradable, Model { log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); wallet.rescanBlockchain(); } catch (Exception e) { - if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while + log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while throw e; } finally { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index bcc75132..20c889f8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -31,6 +31,7 @@ import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @@ -100,12 +101,14 @@ public class MaybeSendSignContractRequest extends TradeTask { try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); } catch (Exception e) { log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 0f0dd1cb..69c72211 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -26,6 +26,7 @@ import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; import monero.wallet.model.MoneroTxWallet; import java.math.BigInteger; @@ -66,12 +67,14 @@ public class TakerReserveTradeFunds extends TradeTask { try { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); } catch (Exception e) { log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); + trade.getXmrWalletService().handleWalletError(e, sourceConnection); + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } 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 2866bcef..587ed33c 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -160,9 +160,11 @@ public class XmrWalletService { private boolean isClosingWallet; private boolean isShutDownStarted; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type + private boolean isSyncingWithProgress; private Long syncStartHeight; private TaskLooper syncProgressLooper; private CountDownLatch syncProgressLatch; + private Exception syncProgressError; private Timer syncProgressTimeout; private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 60; @@ -178,7 +180,9 @@ public class XmrWalletService { private List cachedSubaddresses; private List cachedOutputs; private List cachedTxs; - private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked + private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked + private String testReconnectMonerod1 = "http://node.community.rino.io:18081"; + private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081"; @SuppressWarnings("unused") @Inject @@ -473,6 +477,14 @@ public class XmrWalletService { } } + public String relayTx(String metadata) { + synchronized (WALLET_LOCK) { + String txId = wallet.relayTx(metadata); + requestSaveMainWallet(); + return txId; + } + } + public MoneroTxWallet createTx(List destinations) { MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); //printTxs("XmrWalletService.createTx", tx); @@ -1289,9 +1301,19 @@ public class XmrWalletService { else log.info(appliedMsg); // listen for connection changes - xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> { - onConnectionChanged(connection); - }, THREAD_ID)); + xmrConnectionService.addConnectionListener(connection -> { + if (wasWalletSynced && !isSyncingWithProgress) { + ThreadUtils.execute(() -> { + onConnectionChanged(connection); + }, THREAD_ID); + } else { + + // force restart main wallet if connection changed while syncing + log.warn("Force restarting main wallet because connection changed while syncing"); + forceRestartMainWallet(); + return; + } + }); // initialize main wallet when daemon synced walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); @@ -1340,6 +1362,7 @@ public class XmrWalletService { long date = localDateTime.toEpochSecond(ZoneOffset.UTC); user.setWalletCreationDate(date); } + walletHeight.set(wallet.getHeight()); isClosingWallet = false; } @@ -1348,6 +1371,7 @@ public class XmrWalletService { log.info("Monero wallet path={}", wallet.getPath()); // sync main wallet if applicable + // TODO: error handling and re-initialization is jenky, refactor if (sync && numAttempts > 0) { try { @@ -1360,7 +1384,16 @@ public class XmrWalletService { // sync main wallet log.info("Syncing main wallet"); long time = System.currentTimeMillis(); - syncWithProgress(); // blocking + MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); + try { + syncWithProgress(); // blocking + } catch (Exception e) { + log.warn("Error syncing wallet with progress on startup: " + e.getMessage()); + forceCloseMainWallet(); + requestSwitchToNextBestConnection(sourceConnection); + maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again + return; + } log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); // poll wallet @@ -1435,69 +1468,83 @@ public class XmrWalletService { } private void syncWithProgress() { + synchronized (WALLET_LOCK) { - // start sync progress timeout - resetSyncProgressTimeout(); + // set initial state + isSyncingWithProgress = true; + syncProgressError = null; + updateSyncProgress(walletHeight.get()); - // show sync progress - updateSyncProgress(wallet.getHeight()); + // test connection changing on startup before wallet synced + if (testReconnectOnStartup) { + UserThread.runAfter(() -> { + log.warn("Testing connection change on startup before wallet synced"); + if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2); + else xmrConnectionService.setConnection(testReconnectMonerod1); + }, 1); + testReconnectOnStartup = false; // only run once + } - // test connection changing on startup before wallet synced - if (runReconnectTestOnStartup) { - UserThread.runAfter(() -> { - log.warn("Testing connection change on startup before wallet synced"); - xmrConnectionService.setConnection("http://node.community.rino.io:18081"); // TODO: needs to be online - }, 1); - runReconnectTestOnStartup = false; // only run once - } - - // get sync notifications from native wallet - if (wallet instanceof MoneroWalletFull) { - if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test - wallet.sync(new MoneroWalletListener() { - @Override - public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { - updateSyncProgress(height); - } - }); - wasWalletSynced = true; - return; - } - - // poll wallet for progress - wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - syncProgressLatch = new CountDownLatch(1); - syncProgressLooper = new TaskLooper(() -> { - if (wallet == null) return; - long height = 0; - try { - height = wallet.getHeight(); // can get read timeout while syncing - } catch (Exception e) { - if (!isShutDownStarted) e.printStackTrace(); + // native wallet provides sync notifications + if (wallet instanceof MoneroWalletFull) { + if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test + wallet.sync(new MoneroWalletListener() { + @Override + public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { + updateSyncProgress(height); + } + }); + setWalletSyncedWithProgress(); return; } - if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); - else { - syncProgressLooper.stop(); - wasWalletSynced = true; + + // start polling wallet for progress + syncProgressLatch = new CountDownLatch(1); + syncProgressLooper = new TaskLooper(() -> { + if (wallet == null) return; + long height; + try { + height = wallet.getHeight(); // can get read timeout while syncing + } catch (Exception e) { + log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); + if (wallet != null && !isShutDownStarted) e.printStackTrace(); + + // stop polling and release latch + syncProgressError = e; + syncProgressLatch.countDown(); + return; + } updateSyncProgress(height); - syncProgressLatch.countDown(); - } - }); - syncProgressLooper.start(1000); - HavenoUtils.awaitLatch(syncProgressLatch); - wallet.stopSyncing(); - if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); + if (height >= xmrConnectionService.getTargetHeight()) { + setWalletSyncedWithProgress(); + syncProgressLatch.countDown(); + } + }); + wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); + syncProgressLooper.start(1000); + + // wait for sync to complete + HavenoUtils.awaitLatch(syncProgressLatch); + + // stop polling + syncProgressLooper.stop(); + syncProgressTimeout.stop(); + if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close + isSyncingWithProgress = false; + if (syncProgressError != null) throw new RuntimeException(syncProgressError); + } } private void updateSyncProgress(long height) { + resetSyncProgressTimeout(); UserThread.execute(() -> { + + // set wallet height walletHeight.set(height); - resetSyncProgressTimeout(); // new wallet reports height 1 before synced if (height == 1) { - downloadListener.progress(.0001, xmrConnectionService.getTargetHeight() - height, null); // >0% shows progress bar + downloadListener.progress(0, xmrConnectionService.getTargetHeight() - height, null); return; } @@ -1505,7 +1552,7 @@ public class XmrWalletService { long targetHeight = xmrConnectionService.getTargetHeight(); long blocksLeft = targetHeight - walletHeight.get(); if (syncStartHeight == null) syncStartHeight = walletHeight.get(); - double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, (double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress + double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight)); downloadListener.progress(percent, blocksLeft, null); }); } @@ -1513,15 +1560,18 @@ public class XmrWalletService { private synchronized void resetSyncProgressTimeout() { if (syncProgressTimeout != null) syncProgressTimeout.stop(); syncProgressTimeout = UserThread.runAfter(() -> { - if (isShutDownStarted || wasWalletSynced) return; - log.warn("Sync progress timeout called"); - forceCloseMainWallet(); - requestSwitchToNextBestConnection(); - maybeInitMainWallet(true); - resetSyncProgressTimeout(); + if (isShutDownStarted) return; + syncProgressError = new RuntimeException("Sync progress timeout called"); + syncProgressLatch.countDown(); }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); } + private void setWalletSyncedWithProgress() { + wasWalletSynced = true; + isSyncingWithProgress = false; + syncProgressTimeout.stop(); + } + private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { // must be connected to daemon @@ -1686,14 +1736,6 @@ public class XmrWalletService { String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); - // force restart main wallet if connection changed before synced - if (!wasWalletSynced) { - if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; - log.warn("Force restarting main wallet because connection changed before inital sync"); - forceRestartMainWallet(); - return; - } - // update connection if (wallet instanceof MoneroWalletRpc) { if (StringUtils.equals(oldProxyUri, newProxyUri)) { @@ -1702,7 +1744,7 @@ public class XmrWalletService { log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet closeMainWallet(true); doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS); - return; // wallet is re-initialized + return; // wallet re-initializes off thread } } else { wallet.setDaemonConnection(connection); @@ -1771,19 +1813,26 @@ public class XmrWalletService { private void forceCloseMainWallet() { stopPolling(); - if (wallet != null) { + if (wallet != null && !isClosingWallet) { isClosingWallet = true; forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); wallet = null; } } - private void forceRestartMainWallet() { + public void forceRestartMainWallet() { log.warn("Force restarting main wallet"); + if (isClosingWallet) return; forceCloseMainWallet(); maybeInitMainWallet(true); } + public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) { + if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); + getWallet(); // re-open wallet + } + private void startPolling() { synchronized (WALLET_LOCK) { if (isShutDownStarted || isPolling()) return; @@ -1849,17 +1898,10 @@ public class XmrWalletService { log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); return; } - - // switch to best connection if wallet is too far behind - if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { - log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); - if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); - } - // sync wallet if behind daemon if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations - syncMainWallet(); + syncWithProgress(); } } @@ -1868,13 +1910,17 @@ public class XmrWalletService { if (updateTxs) { synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations synchronized (HavenoUtils.getDaemonLock()) { + MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection(); try { cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); } catch (Exception e) { // fetch from pool can fail if (!isShutDownStarted) { - if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging + + // throttle error handling + if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); lastLogPollErrorTimestamp = System.currentTimeMillis(); + requestSwitchToNextBestConnection(sourceConnection); } } } @@ -1883,8 +1929,7 @@ public class XmrWalletService { } } catch (Exception e) { if (wallet == null || isShutDownStarted) return; - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartMainWallet(); + if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet(); else if (isWalletConnectedToDaemon()) { log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); //e.printStackTrace(); @@ -1927,8 +1972,12 @@ public class XmrWalletService { } } - public boolean requestSwitchToNextBestConnection() { - return xmrConnectionService.requestSwitchToNextBestConnection(); + private boolean requestSwitchToNextBestConnection() { + return requestSwitchToNextBestConnection(null); + } + + public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) { + return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection); } private void onNewBlock(long height) { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index cab5d715..55bd970e 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -65,6 +65,7 @@ import javafx.scene.control.Button; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import monero.common.MoneroRpcConnection; import monero.common.MoneroUtils; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; @@ -256,6 +257,7 @@ public class WithdrawalView extends ActivatableView { // create tx MoneroTxWallet tx = null; for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + MoneroRpcConnection sourceConnection = xmrWalletService.getConnectionService().getConnection(); try { log.info("Creating withdraw tx"); long startTime = System.currentTimeMillis(); @@ -270,7 +272,7 @@ public class WithdrawalView extends ActivatableView { if (isNotEnoughMoney(e.getMessage())) throw e; log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); + if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } }