diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 3f50f4aa..664daaca 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -43,6 +43,7 @@ import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -50,6 +51,7 @@ import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; @@ -89,6 +91,8 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter + private final BooleanProperty connectionServiceFallbackHandlerActive = new SimpleBooleanProperty(); + @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); private Socks5ProxyProvider socks5ProxyProvider; @@ -99,6 +103,7 @@ public final class XmrConnectionService { private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; + private Long lastFallbackInvocation; private Long lastLogPollErrorTimestamp; private long lastLogDaemonNotSyncedTimestamp; private Long syncStartHeight; @@ -115,6 +120,8 @@ public final class XmrConnectionService { private int numRequestsLastMinute; private long lastSwitchTimestamp; private Set excludedConnections = new HashSet<>(); + private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 60 * 1; // offer to fallback up to once every minute + private boolean fallbackApplied; @Inject public XmrConnectionService(P2PService p2PService, @@ -424,6 +431,19 @@ public final class XmrConnectionService { return numUpdates; } + public void fallbackToBestConnection() { + if (isShutDownStarted) return; + if (xmrNodes.getProvidedXmrNodes().isEmpty()) { + log.warn("Falling back to public nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); + } else { + log.warn("Falling back to provided nodes"); + preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + } + fallbackApplied = true; + initializeConnections(); + } + // ------------------------------- HELPERS -------------------------------- private void doneDownload() { @@ -533,7 +553,7 @@ public final class XmrConnectionService { } // restore connections - if ("".equals(config.xmrNode)) { + if (!isFixedConnection()) { // load previous or default connections if (coreContext.isApiUser()) { @@ -569,10 +589,7 @@ public final class XmrConnectionService { } // restore last connection - if (isFixedConnection()) { - if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size()); - connectionManager.setConnection(getConnections().get(0)); - } else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { + if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) { if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) { connectionManager.setConnection(connectionList.getCurrentConnectionUri().get()); } @@ -592,7 +609,7 @@ public final class XmrConnectionService { maybeStartLocalNode(); // update connection - if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { + if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { MoneroRpcConnection bestConnection = getBestAvailableConnection(); if (bestConnection != null) setConnection(bestConnection); } @@ -614,6 +631,7 @@ public final class XmrConnectionService { } // notify initial connection + lastRefreshPeriodMs = getRefreshPeriodMs(); onConnectionChanged(connectionManager.getConnection()); } @@ -716,16 +734,14 @@ public final class XmrConnectionService { // skip handling if shutting down if (isShutDownStarted) return; - // fallback to provided or public nodes if custom connection fails on startup - if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) { - if (xmrNodes.getProvidedXmrNodes().isEmpty()) { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); - } else { - log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage()); - preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); + // invoke fallback handling on startup error + boolean canFallback = isFixedConnection() || isCustomConnections(); + if (lastInfo == null && canFallback) { + if (!connectionServiceFallbackHandlerActive.get() && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + lastFallbackInvocation = System.currentTimeMillis(); + connectionServiceFallbackHandlerActive.set(true); } - initializeConnections(); return; } @@ -819,6 +835,10 @@ public final class XmrConnectionService { } private boolean isFixedConnection() { - return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + return !"".equals(config.xmrNode) && !fallbackApplied; + } + + private boolean isCustomConnections() { + return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; } } diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 7235efce..0cf18224 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,6 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp { log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); acceptedHandler.run(); }); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.info("onDisplayMoneroConnectionFallbackHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index d80ca807..a291da50 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -158,6 +158,9 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable + private Consumer displayMoneroConnectionFallbackHandler; + @Setter + @Nullable private Consumer displayTorNetworkSettingsHandler; @Setter @Nullable @@ -426,6 +429,12 @@ public class HavenoSetup { getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); + // listen for fallback handling + getConnectionServiceFallbackHandlerActive().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionFallbackHandler == null) return; + displayMoneroConnectionFallbackHandler.accept(newValue); + }); + log.info("Init P2P network"); havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork); p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler); @@ -725,6 +734,10 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } + public BooleanProperty getConnectionServiceFallbackHandlerActive() { + return xmrConnectionService.getConnectionServiceFallbackHandlerActive(); + } + public StringProperty getTopErrorMsg() { return topErrorMsg; } diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 1f7946ea..d17c7ba3 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -120,7 +120,7 @@ public class WalletAppSetup { @Nullable Runnable showPopupIfInvalidBtcConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { - log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion()); + log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); xmrInfoBinding = EasyBind.combine( 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 759dabd0..79e9248c 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1298,9 +1298,10 @@ public class XmrWalletService extends XmrWalletBase { } else { // force restart main wallet if connection changed while syncing - log.warn("Force restarting main wallet because connection changed while syncing"); - forceRestartMainWallet(); - return; + if (wallet != null) { + log.warn("Force restarting main wallet because connection changed while syncing"); + forceRestartMainWallet(); + } } }); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9d5de331..c1f98ce4 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2044,6 +2044,9 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) walletPasswordWindow.headline=Enter password to unlock +connectionFallback.headline=Connection error +connectionFallback.msg=Error connecting to your custom Monero node(s).\n\nDo you want to try the next best available Monero node? + torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges torNetworkSettingWindow.providedBridges=Connect with provided bridges diff --git a/desktop/src/main/java/haveno/desktop/main/MainView.java b/desktop/src/main/java/haveno/desktop/main/MainView.java index 7290f768..9bf5f378 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainView.java +++ b/desktop/src/main/java/haveno/desktop/main/MainView.java @@ -674,6 +674,7 @@ public class MainView extends InitializableView { } } else { xmrInfoLabel.setId("footer-pane"); + xmrInfoLabel.getStyleClass().remove("error-text"); if (xmrNetworkWarnMsgPopup != null) xmrNetworkWarnMsgPopup.hide(); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 4681431b..f120b794 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -140,6 +140,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @SuppressWarnings("FieldCanBeLocal") private MonadicBinding tradesAndUIReady; private final Queue> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); + private Popup moneroConnectionFallbackPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -334,9 +335,38 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); + havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> { + if (moneroConnectionFallbackPopup == null) { + moneroConnectionFallbackPopup = new Popup() + .headLine(Res.get("connectionFallback.headline")) + .warning(Res.get("connectionFallback.msg")) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); + }); + } + if (show) { + moneroConnectionFallbackPopup.show(); + } else if (moneroConnectionFallbackPopup.isDisplayed()) { + moneroConnectionFallbackPopup.hide(); + } + }); + havenoSetup.setDisplayTorNetworkSettingsHandler(show -> { if (show) { torNetworkSettingsWindow.show(); + + // bring connection fallback popup to front if displayed + if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { + moneroConnectionFallbackPopup.hide(); + moneroConnectionFallbackPopup.show(); + } } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); }