From 7e3a47de4a590222659ad647493d0809ce6995ad Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 5 Apr 2025 17:29:31 -0400 Subject: [PATCH] prompt to start local node or fallback on startup --- Makefile | 11 +++ .../haveno/core/api/XmrConnectionService.java | 51 ++++++++++-- .../haveno/core/app/HavenoHeadlessApp.java | 2 +- .../java/haveno/core/app/HavenoSetup.java | 13 +-- .../resources/i18n/displayStrings.properties | 9 +- .../i18n/displayStrings_cs.properties | 2 +- .../haveno/desktop/main/MainViewModel.java | 82 +++++++++++++------ 7 files changed, 129 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index ad51f76809..8f32c3ed41 100644 --- a/Makefile +++ b/Makefile @@ -423,6 +423,17 @@ haveno-desktop-stagenet: --apiPort=3204 \ --useNativeXmrWallet=false \ +haveno-daemon-stagenet: + ./haveno-daemon$(APP_EXT) \ + --baseCurrencyNetwork=XMR_STAGENET \ + --useLocalhostForP2P=false \ + --useDevPrivilegeKeys=false \ + --nodePort=9999 \ + --appName=Haveno \ + --apiPassword=apitest \ + --apiPort=3204 \ + --useNativeXmrWallet=false \ + # Mainnet network monerod: diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index d686d64925..346298f58e 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -24,7 +24,6 @@ import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; -import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; @@ -73,6 +72,11 @@ public final class XmrConnectionService { private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor + public enum XmrConnectionError { + LOCAL, + CUSTOM + } + private final Object lock = new Object(); private final Object pollLock = new Object(); private final Object listenerLock = new Object(); @@ -90,7 +94,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final SimpleStringProperty connectionServiceFallbackHandler = new SimpleStringProperty(); + private final ObjectProperty connectionServiceError = new SimpleObjectProperty<>(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -119,7 +123,7 @@ 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 static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private boolean fallbackApplied; @Inject @@ -260,7 +264,14 @@ public final class XmrConnectionService { private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); - if (!fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) return null; // user needs to explicitly allow fallback after syncing local node + + // user needs to authorize fallback on startup after using locally synced node + if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { + log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); + return null; + } + + // get best connection Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections @@ -543,6 +554,11 @@ public final class XmrConnectionService { // update connection if (isConnected) { setConnection(connection.getUri()); + + // reset error connecting to local node + if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { + connectionServiceError.set(null); + } } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { MoneroRpcConnection bestConnection = getBestConnection(); if (bestConnection != null) setConnection(bestConnection); // switch to best connection @@ -632,6 +648,27 @@ public final class XmrConnectionService { return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); } + public void startLocalNode() { + + // cannot start local node as seed node + if (HavenoUtils.isSeedNode()) { + throw new RuntimeException("Cannot start local node on seed node"); + } + + // start local node if offline and used as last connection + if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { + try { + log.info("Starting local node"); + xmrLocalNode.start(); + } catch (Exception e) { + log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); + throw new RuntimeException(e); + } + } else { + throw new RuntimeException("Local node is not offline and used as last connection"); + } + } + private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted || !accountService.isAccountOpen()) return; if (currentConnection == null) { @@ -717,14 +754,14 @@ public final class XmrConnectionService { // invoke fallback handling on startup error boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); if (lastInfo == null && canFallback) { - if (connectionServiceFallbackHandler.get() == null || connectionServiceFallbackHandler.equals("") && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { + if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); if (lastUsedLocalSyncingNode()) { log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); - connectionServiceFallbackHandler.set(Res.get("connectionFallback.localNode")); + connectionServiceError.set(XmrConnectionError.LOCAL); } else { log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); - connectionServiceFallbackHandler.set(Res.get("connectionFallback.customNode")); + connectionServiceError.set(XmrConnectionError.CUSTOM); } } return; diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index fc6eb2d75c..84bdcc746a 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -75,7 +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.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: 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 4ac7b23512..19503fafd8 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; +import haveno.core.api.XmrConnectionService.XmrConnectionError; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; import haveno.core.offer.OpenOfferManager; @@ -158,7 +159,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionFallbackHandler; + private Consumer displayMoneroConnectionErrorHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -430,9 +431,9 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceFallbackHandler().addListener((observable, oldValue, newValue) -> { - if (displayMoneroConnectionFallbackHandler == null) return; - displayMoneroConnectionFallbackHandler.accept(newValue); + getConnectionServiceError().addListener((observable, oldValue, newValue) -> { + if (displayMoneroConnectionErrorHandler == null) return; + displayMoneroConnectionErrorHandler.accept(newValue); }); log.info("Init P2P network"); @@ -734,8 +735,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public StringProperty getConnectionServiceFallbackHandler() { - return xmrConnectionService.getConnectionServiceFallbackHandler(); + public ObjectProperty getConnectionServiceError() { + return xmrConnectionService.getConnectionServiceError(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3a6b666102..3a321d56e0 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2057,9 +2057,12 @@ 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.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? -connectionFallback.localNode=Error connecting to your last used local node.\n\nDo you want to use the next best available Monero node? +xmrConnectionError.headline=Monero connection error +xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. +xmrConnectionError.localNode.start=Start local node +xmrConnectionError.localNode.start.error=Error starting local node +xmrConnectionError.localNode.fallback=Use next best node torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index bc1841259c..f711402900 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -2056,7 +2056,7 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) walletPasswordWindow.headline=Pro odemknutí zadejte heslo -connectionFallback.headline=Chyba připojení +connectionFallback.headline=Chyba připojení k Moneru connectionFallback.customNode=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? torNetworkSettingWindow.header=Nastavení sítě Tor diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 8c36eaf179..4ee10d7846 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -53,6 +53,7 @@ import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.Navigation; +import haveno.desktop.app.HavenoApp; import haveno.desktop.common.model.ViewModel; import haveno.desktop.components.TxIdTextField; import haveno.desktop.main.account.AccountView; @@ -140,7 +141,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; + private Popup moneroConnectionErrorPopup; /////////////////////////////////////////////////////////////////////////////////////////// @@ -335,24 +336,59 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionFallbackHandler(fallbackMsg -> { - if (fallbackMsg != null && !fallbackMsg.isEmpty()) { - moneroConnectionFallbackPopup = new Popup() - .headLine(Res.get("connectionFallback.headline")) - .warning(fallbackMsg) - .closeButtonText(Res.get("shared.no")) - .actionButtonText(Res.get("shared.yes")) - .onAction(() -> { - havenoSetup.getConnectionServiceFallbackHandler().set(""); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); - }) - .onClose(() -> { - log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceFallbackHandler().set(""); - }); - moneroConnectionFallbackPopup.show(); - } else if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { - moneroConnectionFallbackPopup.hide(); + havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { + if (connectionError == null) { + if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); + } else { + switch (connectionError) { + case LOCAL: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.localNode")) + .actionButtonText(Res.get("xmrConnectionError.localNode.start")) + .onAction(() -> { + log.warn("User has chosen to start local node."); + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> { + try { + HavenoUtils.xmrConnectionService.startLocalNode(); + } catch (Exception e) { + log.error("Error starting local node: {}", e.getMessage(), e); + new Popup() + .headLine(Res.get("xmrConnectionError.localNode.start.error")) + .warning(e.getMessage()) + .closeButtonText(Res.get("shared.close")) + .onClose(() -> havenoSetup.getConnectionServiceError().set(null)) + .show(); + } + }).start(); + }) + .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) + .onSecondaryAction(() -> { + log.warn("User has chosen to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .closeButtonText(Res.get("shared.shutDown")) + .onClose(HavenoApp.getShutDownHandler()); + break; + case CUSTOM: + moneroConnectionErrorPopup = new Popup() + .headLine(Res.get("xmrConnectionError.headline")) + .warning(Res.get("xmrConnectionError.customNode")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceError().set(null); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceError().set(null); + }); + break; + } + moneroConnectionErrorPopup.show(); } }); @@ -360,10 +396,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener if (show) { torNetworkSettingsWindow.show(); - // bring connection fallback popup to front if displayed - if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { - moneroConnectionFallbackPopup.hide(); - moneroConnectionFallbackPopup.show(); + // bring connection error popup to front if displayed + if (moneroConnectionErrorPopup != null && moneroConnectionErrorPopup.isDisplayed()) { + moneroConnectionErrorPopup.hide(); + moneroConnectionErrorPopup.show(); } } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide();