From fdddc8747725e71cdd81429dc3c8b425cba3d08d Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 2 Apr 2022 14:18:11 -0400 Subject: [PATCH] app fully initialized before daemon connection or wallet by default wallet initializes when first connected to get correct height connect to local node if available and last connection offline use only one internal daemon in monero node service --- .../api/CoreMoneroConnectionsService.java | 41 ++-- .../bisq/core/api/CoreMoneroNodeService.java | 53 +++-- .../java/bisq/core/app/AppStartupState.java | 2 +- .../java/bisq/core/app/P2PNetworkSetup.java | 3 +- .../src/main/java/bisq/core/btc/Balances.java | 33 ++-- .../core/btc/wallet/XmrWalletService.java | 184 ++++++++++-------- 6 files changed, 176 insertions(+), 140 deletions(-) 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); }