Add API functions to start and stop local Monero node

This commit is contained in:
duriancrepe 2022-04-02 09:35:07 -07:00 committed by woodser
parent 00765d7b32
commit 9dfbb0d5a6
12 changed files with 546 additions and 31 deletions

View file

@ -36,6 +36,8 @@ import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Trade;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.crypto.IncorrectPasswordException;
@ -53,6 +55,7 @@ import javax.inject.Singleton;
import com.google.common.util.concurrent.FutureCallback;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@ -94,6 +97,7 @@ public class CoreApi {
private final TradeStatisticsManager tradeStatisticsManager;
private final CoreNotificationService notificationService;
private final CoreMoneroConnectionsService coreMoneroConnectionsService;
private final CoreMoneroNodeService coreMoneroNodeService;
@Inject
public CoreApi(Config config,
@ -109,7 +113,8 @@ public class CoreApi {
CoreWalletsService walletsService,
TradeStatisticsManager tradeStatisticsManager,
CoreNotificationService notificationService,
CoreMoneroConnectionsService coreMoneroConnectionsService) {
CoreMoneroConnectionsService coreMoneroConnectionsService,
CoreMoneroNodeService coreMoneroNodeService) {
this.config = config;
this.appStartupState = appStartupState;
this.coreAccountService = coreAccountService;
@ -124,6 +129,7 @@ public class CoreApi {
this.tradeStatisticsManager = tradeStatisticsManager;
this.notificationService = notificationService;
this.coreMoneroConnectionsService = coreMoneroConnectionsService;
this.coreMoneroNodeService = coreMoneroNodeService;
}
@SuppressWarnings("SameReturnValue")
@ -235,6 +241,26 @@ public class CoreApi {
coreMoneroConnectionsService.setAutoSwitch(autoSwitch);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Monero node
///////////////////////////////////////////////////////////////////////////////////////////
public boolean isMoneroNodeRunning() {
return coreMoneroNodeService.isMoneroNodeRunning();
}
public MoneroNodeSettings getMoneroNodeSettings() {
return coreMoneroNodeService.getMoneroNodeSettings();
}
public void startMoneroNode(MoneroNodeSettings settings) throws IOException {
coreMoneroNodeService.startMoneroNode(settings);
}
public void stopMoneroNode() {
coreMoneroNodeService.stopMoneroNode();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Wallets
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -18,6 +18,7 @@ import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroConnectionManager;
import monero.common.MoneroConnectionManagerListener;
@ -36,12 +37,13 @@ public final class CoreMoneroConnectionsService {
// TODO (woodser): support each network type, move to config, remove localhost authentication
private static final List<MoneroRpcConnection> DEFAULT_CONNECTIONS = Arrays.asList(
new MoneroRpcConnection("http://localhost:38081", "superuser", "abctesting123").setPriority(1), // localhost is first priority
new MoneroRpcConnection("http://127.0.0.1:38081", "superuser", "abctesting123").setPriority(1), // localhost is first priority, use loopback address to match url generated by local node service
new MoneroRpcConnection("http://haveno.exchange:38081", "", "").setPriority(2)
);
private final Object lock = new Object();
private final CoreAccountService accountService;
private final CoreMoneroNodeService nodeService;
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
@ -55,21 +57,23 @@ public final class CoreMoneroConnectionsService {
@Inject
public CoreMoneroConnectionsService(WalletsSetup walletsSetup,
CoreAccountService accountService,
CoreMoneroNodeService nodeService,
MoneroConnectionManager connectionManager,
EncryptedConnectionList connectionList) {
this.accountService = accountService;
this.nodeService = nodeService;
this.connectionManager = connectionManager;
this.connectionList = connectionList;
// initialize after account open and basic setup
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
// initialize from connections read from disk
initialize();
// listen for account to be opened or password changed
accountService.addListener(new AccountServiceListener() {
@Override
public void onAccountOpened() {
try {
@ -80,7 +84,7 @@ public final class CoreMoneroConnectionsService {
throw new RuntimeException(e);
}
}
@Override
public void onPasswordChanged(String oldPassword, String newPassword) {
log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword);
@ -91,12 +95,12 @@ public final class CoreMoneroConnectionsService {
}
// ------------------------ CONNECTION MANAGEMENT -------------------------
public MoneroDaemon getDaemon() {
accountService.checkAccountOpen();
return this.daemon;
}
public void addListener(MoneroConnectionManagerListener listener) {
synchronized (lock) {
accountService.checkAccountOpen();
@ -194,9 +198,9 @@ public final class CoreMoneroConnectionsService {
connectionList.setAutoSwitch(autoSwitch);
}
}
// ----------------------------- APP METHODS ------------------------------
public boolean isChainHeightSyncedWithinTolerance() {
if (daemon == null) return false;
Long targetHeight = daemon.getSyncInfo().getTargetHeight();
@ -208,7 +212,7 @@ public final class CoreMoneroConnectionsService {
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), targetHeight);
return false;
}
public ReadOnlyIntegerProperty numPeersProperty() {
return numPeers;
}
@ -216,7 +220,7 @@ public final class CoreMoneroConnectionsService {
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
return peers;
}
public boolean hasSufficientPeersForBroadcast() {
return numPeers.get() >= getMinBroadcastConnections();
}
@ -224,33 +228,33 @@ public final class CoreMoneroConnectionsService {
public LongProperty chainHeightProperty() {
return chainHeight;
}
public ReadOnlyDoubleProperty downloadPercentageProperty() {
return downloadListener.percentageProperty();
}
public int getMinBroadcastConnections() {
return MIN_BROADCAST_CONNECTIONS;
}
public boolean isDownloadComplete() {
return downloadPercentageProperty().get() == 1d;
}
/**
* Signals that both the daemon and wallet have synced.
*
*
* TODO: separate daemon and wallet download/done listeners
*/
public void doneDownload() {
downloadListener.doneDownload();
}
// ------------------------------- HELPERS --------------------------------
private void initialize() {
synchronized (lock) {
// reset connection manager's connections and listeners
connectionManager.reset();
@ -265,10 +269,11 @@ public final class CoreMoneroConnectionsService {
}
// restore last used connection
connectionList.getCurrentConnectionUri().ifPresentOrElse(connectionManager::setConnection, () -> {
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();
@ -283,6 +288,32 @@ public final class CoreMoneroConnectionsService {
// run once
if (!isInitialized) {
// initialize local monero node
nodeService.addListener(new MoneroNodeServiceListener() {
@Override
public void onNodeStarted(MoneroDaemonRpc daemon) {
log.info(getClass() + ".onNodeStarted() called");
setConnection(daemon.getRpcConnection());
}
@Override
public void onNodeStopped() {
log.info(getClass() + ".onNodeStopped() called");
checkConnection();
}
});
// start local node if the last connection is local and not running
currentConnection.ifPresent(connection -> {
try {
if (nodeService.isMoneroNodeConnection(connection) && !nodeService.isMoneroNodeRunning()) {
nodeService.startMoneroNode();
}
} catch (Exception e) {
log.warn("Unable to start local monero node: " + e.getMessage());
}
});
// register connection change listener
connectionManager.addListener(this::onConnectionChanged);
@ -292,7 +323,7 @@ public final class CoreMoneroConnectionsService {
}
}
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
synchronized (lock) {
if (currentConnection == null) {

View file

@ -0,0 +1,168 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api;
import bisq.core.user.Preferences;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.common.config.Config;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.net.URI;
import java.net.URISyntaxException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
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.
*/
@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 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;
private final Preferences preferences;
private final List<MoneroNodeServiceListener> listeners = new ArrayList<>();
// required arguments
private static final List<String> MONEROD_ARGS = Arrays.asList(
MONEROD_PATH,
"--" + MONERO_NETWORK_TYPE,
"--no-igd",
"--hide-my-port",
"--rpc-login", "superuser:abctesting123" // TODO: remove authentication
);
// local monero node owned by this process
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()) {
rpcPort = 28081;
} 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);
}
public void addListener(MoneroNodeServiceListener listener) {
listeners.add(listener);
}
public boolean removeListener(MoneroNodeServiceListener listener) {
return listeners.remove(listener);
}
/**
* Returns whether a connection string URI is a local monero node.
*/
public boolean isMoneroNodeConnection(String connection) throws URISyntaxException {
var uri = new URI(connection);
return CoreMoneroNodeService.LOCAL_NODE_ADDRESS.equals(uri.getHost());
}
/**
* Returns whether the local monero node is running or local daemon connection is running
*/
public boolean isMoneroNodeRunning() {
return daemon != null || defaultMoneroDaemon.isConnected();
}
public MoneroNodeSettings getMoneroNodeSettings() {
return preferences.getMoneroNodeSettings();
}
/**
* Starts a local monero node from settings.
*/
public void startMoneroNode() throws IOException {
var settings = preferences.getMoneroNodeSettings();
this.startMoneroNode(settings);
}
/**
* Starts a local monero node. Throws MoneroError if the node cannot be started.
* 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");
log.info("Starting local Monero node: " + settings);
var args = new ArrayList<>(MONEROD_ARGS);
var dataDir = settings.getBlockchainPath();
if (dataDir == null || dataDir.isEmpty()) {
dataDir = MONEROD_DATADIR;
}
args.add("--data-dir=" + dataDir);
var bootstrapUrl = settings.getBootstrapUrl();
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
args.add("--bootstrap-daemon-address=" + bootstrapUrl);
}
var flags = settings.getStartupFlags();
if (flags != null) {
args.addAll(flags);
}
daemon = new MoneroDaemonRpc(args);
preferences.setMoneroNodeSettings(settings);
for (var listener : listeners) listener.onNodeStarted(daemon);
}
/**
* Stops the current local monero node if owned by this 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
}
}
}

View file

@ -0,0 +1,24 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api;
import monero.daemon.MoneroDaemonRpc;
public class MoneroNodeServiceListener {
public void onNodeStarted(MoneroDaemonRpc daemon) {}
public void onNodeStopped() {}
}

View file

@ -30,6 +30,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil;
import bisq.core.provider.fee.FeeService;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.network.p2p.network.BridgeAddressProvider;
@ -166,7 +167,6 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
@Getter
private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode());
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -687,7 +687,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
prefPayload.setTakeOfferSelectedPaymentAccountId(value);
requestPersistence();
}
public void setIgnoreDustThreshold(int value) {
prefPayload.setIgnoreDustThreshold(value);
requestPersistence();
@ -708,6 +708,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setMoneroNodeSettings(MoneroNodeSettings settings) {
prefPayload.setMoneroNodeSettings(settings);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -957,5 +961,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
void setDenyApiTaker(boolean value);
void setNotifyOnPreRelease(boolean value);
void setMoneroNodeSettings(MoneroNodeSettings settings);
}
}

View file

@ -23,6 +23,7 @@ import bisq.core.locale.FiatCurrency;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.common.proto.ProtoUtil;
import bisq.common.proto.persistable.PersistableEnvelope;
@ -130,6 +131,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
private boolean denyApiTaker;
private boolean notifyOnPreRelease;
private MoneroNodeSettings moneroNodeSettings;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -201,8 +204,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode);
Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode);
Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode);
Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent(
account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage()));
Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent(account -> builder.setSelectedPaymentAccountForCreateOffer(account.toProtoMessage()));
Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses);
Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges);
Optional.ofNullable(referralId).ifPresent(builder::setReferralId);
@ -210,7 +212,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
Optional.ofNullable(rpcUser).ifPresent(builder::setRpcUser);
Optional.ofNullable(rpcPw).ifPresent(builder::setRpcPw);
Optional.ofNullable(takeOfferSelectedPaymentAccountId).ifPresent(builder::setTakeOfferSelectedPaymentAccountId);
Optional.ofNullable(moneroNodeSettings).ifPresent(settings -> builder.setMoneroNodeSettings(settings.toProtoMessage()));
return protobuf.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build();
}
@ -286,7 +288,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
proto.getHideNonAccountPaymentMethods(),
proto.getShowOffersMatchingMyAccounts(),
proto.getDenyApiTaker(),
proto.getNotifyOnPreRelease()
proto.getNotifyOnPreRelease(),
MoneroNodeSettings.fromProto(proto.getMoneroNodeSettings())
);
}
}

View file

@ -0,0 +1,50 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.xmr;
import bisq.common.proto.persistable.PersistableEnvelope;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Data
@AllArgsConstructor
public class MoneroNodeSettings implements PersistableEnvelope {
String blockchainPath;
String bootstrapUrl;
List<String> startupFlags;
public static MoneroNodeSettings fromProto(protobuf.MoneroNodeSettings proto) {
return new MoneroNodeSettings(
proto.getBlockchainPath(),
proto.getBootstrapUrl(),
proto.getStartupFlagsList());
}
@Override
public protobuf.MoneroNodeSettings toProtoMessage() {
return protobuf.MoneroNodeSettings.newBuilder()
.setBlockchainPath(blockchainPath)
.setBootstrapUrl(bootstrapUrl)
.addAllStartupFlags(startupFlags).build();
}
}