prompt to start local node or fallback on startup

This commit is contained in:
woodser 2025-04-05 17:29:31 -04:00 committed by GitHub
parent 9668dd2369
commit 7e3a47de4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 129 additions and 41 deletions

View file

@ -423,6 +423,17 @@ haveno-desktop-stagenet:
--apiPort=3204 \ --apiPort=3204 \
--useNativeXmrWallet=false \ --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 # Mainnet network
monerod: monerod:

View file

@ -24,7 +24,6 @@ import haveno.common.UserThread;
import haveno.common.app.DevEnv; import haveno.common.app.DevEnv;
import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.BaseCurrencyNetwork;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.xmr.model.EncryptedConnectionList; 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_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 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 lock = new Object();
private final Object pollLock = new Object(); private final Object pollLock = new Object();
private final Object listenerLock = 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 LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener(); private final DownloadListener downloadListener = new DownloadListener();
@Getter @Getter
private final SimpleStringProperty connectionServiceFallbackHandler = new SimpleStringProperty(); private final ObjectProperty<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>();
@Getter @Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0); private final LongProperty numUpdates = new SimpleLongProperty(0);
@ -119,7 +123,7 @@ public final class XmrConnectionService {
private int numRequestsLastMinute; private int numRequestsLastMinute;
private long lastSwitchTimestamp; private long lastSwitchTimestamp;
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>(); private Set<MoneroRpcConnection> 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; private boolean fallbackApplied;
@Inject @Inject
@ -260,7 +264,14 @@ public final class XmrConnectionService {
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) { private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
accountService.checkAccountOpen(); 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<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections); Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet); addLocalNodeIfIgnored(ignoredConnectionsSet);
MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections
@ -543,6 +554,11 @@ public final class XmrConnectionService {
// update connection // update connection
if (isConnected) { if (isConnected) {
setConnection(connection.getUri()); 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())) { } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestConnection(); MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); // switch to best connection 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(); 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) { private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return; if (isShutDownStarted || !accountService.isAccountOpen()) return;
if (currentConnection == null) { if (currentConnection == null) {
@ -717,14 +754,14 @@ public final class XmrConnectionService {
// invoke fallback handling on startup error // invoke fallback handling on startup error
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode();
if (lastInfo == null && canFallback) { 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(); lastFallbackInvocation = System.currentTimeMillis();
if (lastUsedLocalSyncingNode()) { if (lastUsedLocalSyncingNode()) {
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); 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 { } else {
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); 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; return;

View file

@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
acceptedHandler.run(); 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.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));

View file

@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload; import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext; import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrConnectionService;
import haveno.core.api.XmrConnectionService.XmrConnectionError;
import haveno.core.api.XmrLocalNode; import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
@ -158,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler; rejectedTxErrorMessageHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<String> displayMoneroConnectionFallbackHandler; private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler; private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -430,9 +431,9 @@ public class HavenoSetup {
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
// listen for fallback handling // listen for fallback handling
getConnectionServiceFallbackHandler().addListener((observable, oldValue, newValue) -> { getConnectionServiceError().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionFallbackHandler == null) return; if (displayMoneroConnectionErrorHandler == null) return;
displayMoneroConnectionFallbackHandler.accept(newValue); displayMoneroConnectionErrorHandler.accept(newValue);
}); });
log.info("Init P2P network"); log.info("Init P2P network");
@ -734,8 +735,8 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg(); return xmrConnectionService.getConnectionServiceErrorMsg();
} }
public StringProperty getConnectionServiceFallbackHandler() { public ObjectProperty<XmrConnectionError> getConnectionServiceError() {
return xmrConnectionService.getConnectionServiceFallbackHandler(); return xmrConnectionService.getConnectionServiceError();
} }
public StringProperty getTopErrorMsg() { public StringProperty getTopErrorMsg() {

View file

@ -2057,9 +2057,12 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Sum of all trade fees paid in
closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount) closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amount)
walletPasswordWindow.headline=Enter password to unlock walletPasswordWindow.headline=Enter password to unlock
connectionFallback.headline=Connection error xmrConnectionError.headline=Monero connection error
connectionFallback.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.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.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.header=Tor networks settings
torNetworkSettingWindow.noBridges=Don't use bridges torNetworkSettingWindow.noBridges=Don't use bridges

View file

@ -2056,7 +2056,7 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.title=Suma obchodních poplatků v
closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů) closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu obchodů)
walletPasswordWindow.headline=Pro odemknutí zadejte heslo 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? 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 torNetworkSettingWindow.header=Nastavení sítě Tor

View file

@ -53,6 +53,7 @@ import haveno.core.user.Preferences.UseTorForXmr;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.desktop.Navigation; import haveno.desktop.Navigation;
import haveno.desktop.app.HavenoApp;
import haveno.desktop.common.model.ViewModel; import haveno.desktop.common.model.ViewModel;
import haveno.desktop.components.TxIdTextField; import haveno.desktop.components.TxIdTextField;
import haveno.desktop.main.account.AccountView; import haveno.desktop.main.account.AccountView;
@ -140,7 +141,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
@SuppressWarnings("FieldCanBeLocal") @SuppressWarnings("FieldCanBeLocal")
private MonadicBinding<Boolean> tradesAndUIReady; private MonadicBinding<Boolean> tradesAndUIReady;
private final Queue<Overlay<?>> popupQueue = new PriorityQueue<>(Comparator.comparing(Overlay::getDisplayOrderPriority)); private final Queue<Overlay<?>> 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(); tacWindow.onAction(acceptedHandler::run).show();
}, 1)); }, 1));
havenoSetup.setDisplayMoneroConnectionFallbackHandler(fallbackMsg -> { havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> {
if (fallbackMsg != null && !fallbackMsg.isEmpty()) { if (connectionError == null) {
moneroConnectionFallbackPopup = new Popup() if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide();
.headLine(Res.get("connectionFallback.headline")) } else {
.warning(fallbackMsg) switch (connectionError) {
.closeButtonText(Res.get("shared.no")) case LOCAL:
.actionButtonText(Res.get("shared.yes")) moneroConnectionErrorPopup = new Popup()
.headLine(Res.get("xmrConnectionError.headline"))
.warning(Res.get("xmrConnectionError.localNode"))
.actionButtonText(Res.get("xmrConnectionError.localNode.start"))
.onAction(() -> { .onAction(() -> {
havenoSetup.getConnectionServiceFallbackHandler().set(""); 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(); 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(() -> { .onClose(() -> {
log.warn("User has declined to fallback to the next best available Monero node."); log.warn("User has declined to fallback to the next best available Monero node.");
havenoSetup.getConnectionServiceFallbackHandler().set(""); havenoSetup.getConnectionServiceError().set(null);
}); });
moneroConnectionFallbackPopup.show(); break;
} else if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { }
moneroConnectionFallbackPopup.hide(); moneroConnectionErrorPopup.show();
} }
}); });
@ -360,10 +396,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
if (show) { if (show) {
torNetworkSettingsWindow.show(); torNetworkSettingsWindow.show();
// bring connection fallback popup to front if displayed // bring connection error popup to front if displayed
if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { if (moneroConnectionErrorPopup != null && moneroConnectionErrorPopup.isDisplayed()) {
moneroConnectionFallbackPopup.hide(); moneroConnectionErrorPopup.hide();
moneroConnectionFallbackPopup.show(); moneroConnectionErrorPopup.show();
} }
} else if (torNetworkSettingsWindow.isDisplayed()) { } else if (torNetworkSettingsWindow.isDisplayed()) {
torNetworkSettingsWindow.hide(); torNetworkSettingsWindow.hide();