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
This commit is contained in:
woodser 2022-04-02 14:18:11 -04:00
parent 9dfbb0d5a6
commit fdddc87477
6 changed files with 176 additions and 140 deletions

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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"));

View File

@ -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<OpenOffer>) c -> updateBalance());
tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updateBalance());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) 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<OpenOffer>) c -> updatedBalances());
tradeManager.getObservableList().addListener((ListChangeListener<Trade>) change -> updatedBalances());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) 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<MoneroOutputWallet> 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<MoneroOutputWallet> 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);
}

View File

@ -76,7 +76,7 @@ public class XmrWalletService {
protected final CopyOnWriteArraySet<MoneroWalletListenerI> walletListeners = new CopyOnWriteArraySet<>();
private TradeManager tradeManager;
private MoneroWallet wallet;
private MoneroWalletRpc wallet;
private Map<String, MoneroWallet> 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<String> 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<String> 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);
}