diff --git a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java index e4cc7e79..6ff628b9 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java @@ -243,7 +243,7 @@ public final class CoreMoneroConnectionsService { /** * Signals that both the daemon and wallet have synced. - * + * * TODO: separate daemon and wallet download/done listeners */ public void doneDownload() { @@ -268,15 +268,9 @@ public final class CoreMoneroConnectionsService { addConnection(connection); } - // restore last used connection - var currentConnection = connectionList.getCurrentConnectionUri(); - currentConnection.ifPresentOrElse(connectionManager::setConnection, () -> { - connectionManager.setConnection(DEFAULT_CONNECTIONS.get(0).getUri()); // default to localhost - }); - - // initialize daemon - daemon = new MoneroDaemonRpc(connectionManager.getConnection()); - updateDaemonInfo(); + // restore last used connection if present + var currentConnectionUri = connectionList.getCurrentConnectionUri(); + if (currentConnectionUri.isPresent()) connectionManager.setConnection(currentConnectionUri.get()); // restore configuration connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); @@ -288,11 +282,15 @@ public final class CoreMoneroConnectionsService { // run once if (!isInitialized) { - // initialize local monero node + // register connection change listener + connectionManager.addListener(this::onConnectionChanged); + + // register local node listener nodeService.addListener(new MoneroNodeServiceListener() { @Override public void onNodeStarted(MoneroDaemonRpc daemon) { log.info(getClass() + ".onNodeStarted() called"); + daemon.getRpcConnection().checkConnection(connectionManager.getTimeout()); setConnection(daemon.getRpcConnection()); } @@ -303,10 +301,10 @@ public final class CoreMoneroConnectionsService { } }); - // start local node if the last connection is local and not running - currentConnection.ifPresent(connection -> { + // start local node if last connection is local and offline + currentConnectionUri.ifPresent(uri -> { try { - if (nodeService.isMoneroNodeConnection(connection) && !nodeService.isMoneroNodeRunning()) { + if (CoreMoneroNodeService.isLocalHost(uri) && !nodeService.isMoneroNodeRunning()) { nodeService.startMoneroNode(); } } catch (Exception e) { @@ -314,13 +312,22 @@ public final class CoreMoneroConnectionsService { } }); - // register connection change listener - connectionManager.addListener(this::onConnectionChanged); - // poll daemon periodically startPollingDaemon(); isInitialized = true; } + + // if offline, connect to local node if available + if (!connectionManager.isConnected() && nodeService.isMoneroNodeRunning()) { + MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri()); + if (connection == null) connection = nodeService.getDaemon().getRpcConnection(); + connection.checkConnection(connectionManager.getTimeout()); + setConnection(connection); + } + + // set the daemon based on the connection + if (getConnection() != null) daemon = new MoneroDaemonRpc(connectionManager.getConnection()); + updateDaemonInfo(); } } diff --git a/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java b/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java index f53e078f..521d2b1c 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroNodeService.java @@ -36,17 +36,17 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; -import monero.common.MoneroRpcConnection; import monero.daemon.MoneroDaemonRpc; /** - * Manages a Monero node instance or connection to an instance. + * Start and stop or connect to a local Monero node. */ @Slf4j @Singleton public class CoreMoneroNodeService { - public static final String LOCAL_NODE_ADDRESS = "127.0.0.1"; // expected connection from local MoneroDaemonRpc + private static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node + private static final String LOCALHOST = "localhost"; private static final String MONERO_NETWORK_TYPE = Config.baseCurrencyNetwork().getNetwork().toLowerCase(); private static final String MONEROD_PATH = System.getProperty("user.dir") + File.separator + ".localnet" + File.separator + "monerod"; private static final String MONEROD_DATADIR = System.getProperty("user.dir") + File.separator + ".localnet" + File.separator + MONERO_NETWORK_TYPE; @@ -63,15 +63,11 @@ public class CoreMoneroNodeService { "--rpc-login", "superuser:abctesting123" // TODO: remove authentication ); - // local monero node owned by this process + // client to the local Monero node private MoneroDaemonRpc daemon; - // local monero node for detecting running node not owned by this process - private MoneroDaemonRpc defaultMoneroDaemon; - @Inject public CoreMoneroNodeService(Preferences preferences) { - this.daemon = null; this.preferences = preferences; int rpcPort = 18081; // mainnet if (Config.baseCurrencyNetwork().isTestnet()) { @@ -79,9 +75,15 @@ public class CoreMoneroNodeService { } else if (Config.baseCurrencyNetwork().isStagenet()) { rpcPort = 38081; } - // TODO: remove authentication - var defaultMoneroConnection = new MoneroRpcConnection("http://" + LOCAL_NODE_ADDRESS + ":" + rpcPort, "superuser", "abctesting123").setPriority(1); // localhost is first priority - defaultMoneroDaemon = new MoneroDaemonRpc(defaultMoneroConnection); + this.daemon = new MoneroDaemonRpc("http://" + LOOPBACK_HOST + ":" + rpcPort, "superuser", "abctesting123"); // TODO: remove authentication + } + + /** + * Returns whether the given URI is on local host. // TODO: move to utils + */ + public static boolean isLocalHost(String uri) throws URISyntaxException { + String host = new URI(uri).getHost(); + return host.equals(CoreMoneroNodeService.LOOPBACK_HOST) || host.equals(CoreMoneroNodeService.LOCALHOST); } public void addListener(MoneroNodeServiceListener listener) { @@ -93,18 +95,17 @@ public class CoreMoneroNodeService { } /** - * Returns whether a connection string URI is a local monero node. + * Returns the client of the local monero node. */ - public boolean isMoneroNodeConnection(String connection) throws URISyntaxException { - var uri = new URI(connection); - return CoreMoneroNodeService.LOCAL_NODE_ADDRESS.equals(uri.getHost()); + public MoneroDaemonRpc getDaemon() { + return daemon; } /** - * Returns whether the local monero node is running or local daemon connection is running + * Returns whether a local monero node is running. */ public boolean isMoneroNodeRunning() { - return daemon != null || defaultMoneroDaemon.isConnected(); + return daemon.isConnected(); } public MoneroNodeSettings getMoneroNodeSettings() { @@ -124,7 +125,7 @@ public class CoreMoneroNodeService { * Persists the settings to preferences if the node started successfully. */ public void startMoneroNode(MoneroNodeSettings settings) throws IOException { - if (isMoneroNodeRunning()) throw new IllegalStateException("Monero node already running"); + if (isMoneroNodeRunning()) throw new IllegalStateException("Local Monero node already running"); log.info("Starting local Monero node: " + settings); @@ -146,23 +147,19 @@ public class CoreMoneroNodeService { args.addAll(flags); } - daemon = new MoneroDaemonRpc(args); + daemon = new MoneroDaemonRpc(args); // start daemon as process and re-assign client preferences.setMoneroNodeSettings(settings); for (var listener : listeners) listener.onNodeStarted(daemon); } /** - * Stops the current local monero node if owned by this process. + * Stops the current local monero node if we own its process. * Does not remove the last MoneroNodeSettings. */ public void stopMoneroNode() { - if (!isMoneroNodeRunning()) throw new IllegalStateException("Monero node is not running"); - if (daemon != null) { - daemon.stopProcess(); - daemon = null; - for (var listener : listeners) listener.onNodeStopped(); - } else { - defaultMoneroDaemon.stopProcess(); // throws MoneroError - } + if (!isMoneroNodeRunning()) throw new IllegalStateException("Local Monero node is not running"); + if (daemon.getProcess() == null || !daemon.getProcess().isAlive()) throw new IllegalStateException("Cannot stop local Monero node because we don't own its process"); // TODO (woodser): remove isAlive() check after monero-java 0.5.4 which nullifies internal process + daemon.stopProcess(); + for (var listener : listeners) listener.onNodeStopped(); } } diff --git a/core/src/main/java/bisq/core/app/AppStartupState.java b/core/src/main/java/bisq/core/app/AppStartupState.java index 70527edb..70093ba2 100644 --- a/core/src/main/java/bisq/core/app/AppStartupState.java +++ b/core/src/main/java/bisq/core/app/AppStartupState.java @@ -79,7 +79,7 @@ public class AppStartupState { if (a && b && c) { walletAndNetworkReady.set(true); } - return a && b && c && d; + return a && d; // app fully initialized before daemon connection and wallet by default }); p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> { if (newValue) { diff --git a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java index 88cd358a..bf871c28 100644 --- a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java @@ -17,6 +17,7 @@ package bisq.core.app; +import bisq.common.UserThread; import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.locale.Res; import bisq.core.provider.price.PriceFeedService; @@ -109,7 +110,7 @@ public class P2PNetworkSetup { return result; }); p2PNetworkInfoBinding.subscribe((observable, oldValue, newValue) -> { - p2PNetworkInfo.set(newValue); + UserThread.execute(() -> p2PNetworkInfo.set(newValue)); }); bootstrapState.set(Res.get("mainView.bootstrapState.connectionToTorNetwork")); diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index d386581f..2eda9186 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -18,6 +18,7 @@ package bisq.core.btc; import bisq.common.UserThread; +import bisq.core.btc.listeners.XmrBalanceListener; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; @@ -41,7 +42,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; -import monero.wallet.model.MoneroWalletListener; import org.bitcoinj.core.Coin; @Slf4j @@ -80,18 +80,19 @@ public class Balances { } public void onAllServicesInitialized() { - openOfferManager.getObservableList().addListener((ListChangeListener) c -> updateBalance()); - tradeManager.getObservableList().addListener((ListChangeListener) change -> updateBalance()); - refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> updateBalance()); - xmrWalletService.getWallet().addListener(new MoneroWalletListener() { - @Override public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { updateBalance(); } - @Override public void onOutputReceived(MoneroOutputWallet output) { updateBalance(); } - @Override public void onOutputSpent(MoneroOutputWallet output) { updateBalance(); } + openOfferManager.getObservableList().addListener((ListChangeListener) c -> updatedBalances()); + tradeManager.getObservableList().addListener((ListChangeListener) change -> updatedBalances()); + refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> updatedBalances()); + xmrWalletService.addBalanceListener(new XmrBalanceListener() { + @Override + public void onBalanceChanged(BigInteger balance) { + updatedBalances(); + } }); - updateBalance(); + updatedBalances(); } - private void updateBalance() { + private void updatedBalances() { // Need to delay a bit to get the balances correct UserThread.execute(() -> { updateAvailableBalance(); @@ -105,19 +106,21 @@ public class Balances { // TODO (woodser): balances being set as Coin from BigInteger.longValue(), which can lose precision. should be in centineros for consistency with the rest of the application private void updateAvailableBalance() { - availableBalance.set(Coin.valueOf(xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact())); + availableBalance.set(Coin.valueOf(xmrWalletService.getWallet() == null ? 0 : xmrWalletService.getWallet().getUnlockedBalance(0).longValueExact())); } private void updateLockedBalance() { - BigInteger balance = xmrWalletService.getWallet().getBalance(0); - BigInteger unlockedBalance = xmrWalletService.getWallet().getUnlockedBalance(0); + BigInteger balance = xmrWalletService.getWallet() == null ? new BigInteger("0") : xmrWalletService.getWallet().getBalance(0); + BigInteger unlockedBalance = xmrWalletService.getWallet() == null ? new BigInteger("0") : xmrWalletService.getWallet().getUnlockedBalance(0); lockedBalance.set(Coin.valueOf(balance.subtract(unlockedBalance).longValueExact())); } private void updateReservedOfferBalance() { Coin sum = Coin.valueOf(0); - List frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); - for (MoneroOutputWallet frozenOutput : frozenOutputs) sum = sum.add(Coin.valueOf(frozenOutput.getAmount().longValueExact())); + if (xmrWalletService.getWallet() != null) { + List frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); + for (MoneroOutputWallet frozenOutput : frozenOutputs) sum = sum.add(Coin.valueOf(frozenOutput.getAmount().longValueExact())); + } reservedOfferBalance.set(sum); } diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index c9de90f7..7c65661e 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -76,7 +76,7 @@ public class XmrWalletService { protected final CopyOnWriteArraySet walletListeners = new CopyOnWriteArraySet<>(); private TradeManager tradeManager; - private MoneroWallet wallet; + private MoneroWalletRpc wallet; private Map multisigWallets; @Inject @@ -159,64 +159,6 @@ public class XmrWalletService { return new File(path + ".keys").exists(); } - public MoneroWalletRpc createWallet(MoneroWalletConfig config, Integer port) { - - // start monero-wallet-rpc instance - MoneroWalletRpc walletRpc = startWalletRpcInstance(port); - - // create wallet - try { - walletRpc.createWallet(config); - walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); - return walletRpc; - } catch (Exception e) { - e.printStackTrace(); - MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false); - throw e; - } - } - - public MoneroWalletRpc openWallet(MoneroWalletConfig config, Integer port) { - - // start monero-wallet-rpc instance - MoneroWalletRpc walletRpc = startWalletRpcInstance(port); - - // open wallet - try { - walletRpc.openWallet(config); - walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); - return walletRpc; - } catch (Exception e) { - e.printStackTrace(); - MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false); - throw e; - } - } - - private MoneroWalletRpc startWalletRpcInstance(Integer port) { - - // check if monero-wallet-rpc exists - if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH - + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system"); - - // get app's current daemon connection - MoneroRpcConnection connection = connectionsService.getConnection(); - - // start monero-wallet-rpc instance and return connected client - List cmd = new ArrayList<>(Arrays.asList( // modifiable list - MONERO_WALLET_RPC_PATH, "--" + MONERO_NETWORK_TYPE.toString().toLowerCase(), "--daemon-address", connection.getUri(), "--rpc-login", - MONERO_WALLET_RPC_USERNAME + ":" + getWalletPassword(), "--wallet-dir", walletDir.toString())); - if (connection.getUsername() != null) { - cmd.add("--daemon-login"); - cmd.add(connection.getUsername() + ":" + connection.getPassword()); - } - if (port != null && port > 0) { - cmd.add("--rpc-bind-port"); - cmd.add(Integer.toString(port)); - } - return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); - } - public void closeWallet(MoneroWallet walletRpc, boolean save) { log.info("{}.closeWallet({}, {})", getClass(), walletRpc.getPath(), save); MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save); @@ -290,30 +232,114 @@ public class XmrWalletService { // backup wallet files backupWallets(); - // initialize main wallet - MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); - wallet = MoneroUtils.walletExists(xmrWalletFile.getPath()) ? openWallet(walletConfig, rpcBindPort) : createWallet(walletConfig, rpcBindPort); - System.out.println("Monero wallet path: " + wallet.getPath()); - System.out.println("Monero wallet address: " + wallet.getPrimaryAddress()); - System.out.println("Monero wallet uri: " + ((MoneroWalletRpc) wallet).getRpcConnection().getUri()); - wallet.sync(); // blocking - connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both - wallet.save(); - System.out.println("Loaded wallet balance: " + wallet.getBalance(0)); - System.out.println("Loaded wallet unlocked balance: " + wallet.getUnlockedBalance(0)); - + // initialize main wallet if connected or previously created + tryInitMainWallet(); + // update wallet connections on change connectionsService.addListener(newConnection -> { setWalletDaemonConnections(newConnection); }); + } - // notify on balance changes - wallet.addListener(new MoneroWalletListener() { - @Override - public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { - notifyBalanceListeners(); + private void tryInitMainWallet() { + MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); + if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { + wallet = openWallet(walletConfig, rpcBindPort); + } else if (connectionsService.getConnection() != null && Boolean.TRUE.equals(connectionsService.getConnection().isConnected())) { + wallet = createWallet(walletConfig, rpcBindPort); // wallet requires connection to daemon to correctly set height + } + + // wallet is not initialized until connected to a daemon + if (wallet != null) { + try { + wallet.sync(); // blocking + connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both + wallet.save(); + } catch (Exception e) { + e.printStackTrace(); } - }); + + System.out.println("Monero wallet path: " + wallet.getPath()); + System.out.println("Monero wallet address: " + wallet.getPrimaryAddress()); + System.out.println("Monero wallet uri: " + wallet.getRpcConnection().getUri()); + System.out.println("Monero wallet height: " + wallet.getHeight()); + System.out.println("Monero wallet balance: " + wallet.getBalance(0)); + System.out.println("Monero wallet unlocked balance: " + wallet.getUnlockedBalance(0)); + + // notify on balance changes + wallet.addListener(new MoneroWalletListener() { + @Override + public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + notifyBalanceListeners(); + } + }); + } + } + + private MoneroWalletRpc createWallet(MoneroWalletConfig config, Integer port) { + + // start monero-wallet-rpc instance + MoneroWalletRpc walletRpc = startWalletRpcInstance(port); + + // must be connected to daemon + MoneroRpcConnection connection = connectionsService.getConnection(); + if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); + + // create wallet + try { + walletRpc.createWallet(config); + walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); + return walletRpc; + } catch (Exception e) { + e.printStackTrace(); + MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false); + throw e; + } + } + + private MoneroWalletRpc openWallet(MoneroWalletConfig config, Integer port) { + + // start monero-wallet-rpc instance + MoneroWalletRpc walletRpc = startWalletRpcInstance(port); + + // open wallet + try { + walletRpc.openWallet(config); + walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE); + return walletRpc; + } catch (Exception e) { + e.printStackTrace(); + MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false); + throw e; + } + } + + private MoneroWalletRpc startWalletRpcInstance(Integer port) { + + // check if monero-wallet-rpc exists + if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system"); + + // build command to start monero-wallet-rpc + List cmd = new ArrayList<>(Arrays.asList( // modifiable list + MONERO_WALLET_RPC_PATH, "--" + MONERO_NETWORK_TYPE.toString().toLowerCase(), "--rpc-login", + MONERO_WALLET_RPC_USERNAME + ":" + getWalletPassword(), "--wallet-dir", walletDir.toString())); + MoneroRpcConnection connection = connectionsService.getConnection(); + if (connection != null) { + cmd.add("--daemon-address"); + cmd.add(connection.getUri()); + if (connection.getUsername() != null) { + cmd.add("--daemon-login"); + cmd.add(connection.getUsername() + ":" + connection.getPassword()); + } + } + if (port != null && port > 0) { + cmd.add("--rpc-bind-port"); + cmd.add(Integer.toString(port)); + } + + // start monero-wallet-rpc instance and return connected client + return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } private void backupWallets() { @@ -324,6 +350,7 @@ public class XmrWalletService { private void setWalletDaemonConnections(MoneroRpcConnection connection) { log.info("Setting wallet daemon connections: " + (connection == null ? null : connection.getUri())); + if (wallet == null) tryInitMainWallet(); if (wallet != null) wallet.setDaemonConnection(connection); for (MoneroWallet multisigWallet : multisigWallets.values()) multisigWallet.setDaemonConnection(connection); } @@ -333,7 +360,7 @@ public class XmrWalletService { Coin balance; if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); else balance = getAvailableConfirmedBalance(); - UserThread.execute(new Runnable() { + UserThread.execute(new Runnable() { // TODO (woodser): don't execute on UserThread @Override public void run() { balanceListener.onBalanceChanged(BigInteger.valueOf(balance.value)); @@ -549,6 +576,7 @@ public class XmrWalletService { return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()); } + // TODO (woodser): update balance and other listening public void addBalanceListener(XmrBalanceListener listener) { balanceListeners.add(listener); }