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

@ -172,7 +172,7 @@ public class KeyStorage {
// Most of the time (probably of slightly less than 255/256, around 99.61%) a bad password // Most of the time (probably of slightly less than 255/256, around 99.61%) a bad password
// will result in BadPaddingException before HMAC check. // will result in BadPaddingException before HMAC check.
// See https://stackoverflow.com/questions/8049872/given-final-block-not-properly-padded // See https://stackoverflow.com/questions/8049872/given-final-block-not-properly-padded
if (ce.getCause() instanceof BadPaddingException || ce.getMessage() == Encryption.HMAC_ERROR_MSG) if (ce.getCause() instanceof BadPaddingException || Encryption.HMAC_ERROR_MSG.equals(ce.getMessage()))
throw new IncorrectPasswordException("Incorrect password"); throw new IncorrectPasswordException("Incorrect password");
else else
throw ce; throw ce;

View File

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

View File

@ -18,6 +18,7 @@ import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroConnectionManager; import monero.common.MoneroConnectionManager;
import monero.common.MoneroConnectionManagerListener; import monero.common.MoneroConnectionManagerListener;
@ -36,12 +37,13 @@ public final class CoreMoneroConnectionsService {
// TODO (woodser): support each network type, move to config, remove localhost authentication // TODO (woodser): support each network type, move to config, remove localhost authentication
private static final List<MoneroRpcConnection> DEFAULT_CONNECTIONS = Arrays.asList( 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) new MoneroRpcConnection("http://haveno.exchange:38081", "", "").setPriority(2)
); );
private final Object lock = new Object(); private final Object lock = new Object();
private final CoreAccountService accountService; private final CoreAccountService accountService;
private final CoreMoneroNodeService nodeService;
private final MoneroConnectionManager connectionManager; private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList; private final EncryptedConnectionList connectionList;
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>(); private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
@ -55,21 +57,23 @@ public final class CoreMoneroConnectionsService {
@Inject @Inject
public CoreMoneroConnectionsService(WalletsSetup walletsSetup, public CoreMoneroConnectionsService(WalletsSetup walletsSetup,
CoreAccountService accountService, CoreAccountService accountService,
CoreMoneroNodeService nodeService,
MoneroConnectionManager connectionManager, MoneroConnectionManager connectionManager,
EncryptedConnectionList connectionList) { EncryptedConnectionList connectionList) {
this.accountService = accountService; this.accountService = accountService;
this.nodeService = nodeService;
this.connectionManager = connectionManager; this.connectionManager = connectionManager;
this.connectionList = connectionList; this.connectionList = connectionList;
// initialize after account open and basic setup // initialize after account open and basic setup
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
// initialize from connections read from disk // initialize from connections read from disk
initialize(); initialize();
// listen for account to be opened or password changed // listen for account to be opened or password changed
accountService.addListener(new AccountServiceListener() { accountService.addListener(new AccountServiceListener() {
@Override @Override
public void onAccountOpened() { public void onAccountOpened() {
try { try {
@ -80,7 +84,7 @@ public final class CoreMoneroConnectionsService {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override @Override
public void onPasswordChanged(String oldPassword, String newPassword) { public void onPasswordChanged(String oldPassword, String newPassword) {
log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword); log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword);
@ -91,12 +95,12 @@ public final class CoreMoneroConnectionsService {
} }
// ------------------------ CONNECTION MANAGEMENT ------------------------- // ------------------------ CONNECTION MANAGEMENT -------------------------
public MoneroDaemon getDaemon() { public MoneroDaemon getDaemon() {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
return this.daemon; return this.daemon;
} }
public void addListener(MoneroConnectionManagerListener listener) { public void addListener(MoneroConnectionManagerListener listener) {
synchronized (lock) { synchronized (lock) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
@ -194,9 +198,9 @@ public final class CoreMoneroConnectionsService {
connectionList.setAutoSwitch(autoSwitch); connectionList.setAutoSwitch(autoSwitch);
} }
} }
// ----------------------------- APP METHODS ------------------------------ // ----------------------------- APP METHODS ------------------------------
public boolean isChainHeightSyncedWithinTolerance() { public boolean isChainHeightSyncedWithinTolerance() {
if (daemon == null) return false; if (daemon == null) return false;
Long targetHeight = daemon.getSyncInfo().getTargetHeight(); 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); log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), targetHeight);
return false; return false;
} }
public ReadOnlyIntegerProperty numPeersProperty() { public ReadOnlyIntegerProperty numPeersProperty() {
return numPeers; return numPeers;
} }
@ -216,7 +220,7 @@ public final class CoreMoneroConnectionsService {
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() { public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
return peers; return peers;
} }
public boolean hasSufficientPeersForBroadcast() { public boolean hasSufficientPeersForBroadcast() {
return numPeers.get() >= getMinBroadcastConnections(); return numPeers.get() >= getMinBroadcastConnections();
} }
@ -224,33 +228,33 @@ public final class CoreMoneroConnectionsService {
public LongProperty chainHeightProperty() { public LongProperty chainHeightProperty() {
return chainHeight; return chainHeight;
} }
public ReadOnlyDoubleProperty downloadPercentageProperty() { public ReadOnlyDoubleProperty downloadPercentageProperty() {
return downloadListener.percentageProperty(); return downloadListener.percentageProperty();
} }
public int getMinBroadcastConnections() { public int getMinBroadcastConnections() {
return MIN_BROADCAST_CONNECTIONS; return MIN_BROADCAST_CONNECTIONS;
} }
public boolean isDownloadComplete() { public boolean isDownloadComplete() {
return downloadPercentageProperty().get() == 1d; return downloadPercentageProperty().get() == 1d;
} }
/** /**
* Signals that both the daemon and wallet have synced. * Signals that both the daemon and wallet have synced.
* *
* TODO: separate daemon and wallet download/done listeners * TODO: separate daemon and wallet download/done listeners
*/ */
public void doneDownload() { public void doneDownload() {
downloadListener.doneDownload(); downloadListener.doneDownload();
} }
// ------------------------------- HELPERS -------------------------------- // ------------------------------- HELPERS --------------------------------
private void initialize() { private void initialize() {
synchronized (lock) { synchronized (lock) {
// reset connection manager's connections and listeners // reset connection manager's connections and listeners
connectionManager.reset(); connectionManager.reset();
@ -265,10 +269,11 @@ public final class CoreMoneroConnectionsService {
} }
// restore last used connection // 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 connectionManager.setConnection(DEFAULT_CONNECTIONS.get(0).getUri()); // default to localhost
}); });
// initialize daemon // initialize daemon
daemon = new MoneroDaemonRpc(connectionManager.getConnection()); daemon = new MoneroDaemonRpc(connectionManager.getConnection());
updateDaemonInfo(); updateDaemonInfo();
@ -283,6 +288,32 @@ public final class CoreMoneroConnectionsService {
// run once // run once
if (!isInitialized) { 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 // register connection change listener
connectionManager.addListener(this::onConnectionChanged); connectionManager.addListener(this::onConnectionChanged);
@ -292,7 +323,7 @@ public final class CoreMoneroConnectionsService {
} }
} }
} }
private void onConnectionChanged(MoneroRpcConnection currentConnection) { private void onConnectionChanged(MoneroRpcConnection currentConnection) {
synchronized (lock) { synchronized (lock) {
if (currentConnection == null) { 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.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.PaymentAccountUtil;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.network.p2p.network.BridgeAddressProvider; import bisq.network.p2p.network.BridgeAddressProvider;
@ -166,7 +167,6 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
@Getter @Getter
private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode()); private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode());
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -687,7 +687,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
prefPayload.setTakeOfferSelectedPaymentAccountId(value); prefPayload.setTakeOfferSelectedPaymentAccountId(value);
requestPersistence(); requestPersistence();
} }
public void setIgnoreDustThreshold(int value) { public void setIgnoreDustThreshold(int value) {
prefPayload.setIgnoreDustThreshold(value); prefPayload.setIgnoreDustThreshold(value);
requestPersistence(); requestPersistence();
@ -708,6 +708,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence(); requestPersistence();
} }
public void setMoneroNodeSettings(MoneroNodeSettings settings) {
prefPayload.setMoneroNodeSettings(settings);
requestPersistence();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getter // Getter
@ -957,5 +961,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
void setDenyApiTaker(boolean value); void setDenyApiTaker(boolean value);
void setNotifyOnPreRelease(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.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.proto.CoreProtoResolver; import bisq.core.proto.CoreProtoResolver;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import bisq.common.proto.persistable.PersistableEnvelope; import bisq.common.proto.persistable.PersistableEnvelope;
@ -130,6 +131,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
private boolean denyApiTaker; private boolean denyApiTaker;
private boolean notifyOnPreRelease; private boolean notifyOnPreRelease;
private MoneroNodeSettings moneroNodeSettings;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -201,8 +204,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode); Optional.ofNullable(tradeChartsScreenCurrencyCode).ifPresent(builder::setTradeChartsScreenCurrencyCode);
Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode); Optional.ofNullable(buyScreenCurrencyCode).ifPresent(builder::setBuyScreenCurrencyCode);
Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode); Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode);
Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent( Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent(account -> builder.setSelectedPaymentAccountForCreateOffer(account.toProtoMessage()));
account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage()));
Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses); Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses);
Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges); Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges);
Optional.ofNullable(referralId).ifPresent(builder::setReferralId); Optional.ofNullable(referralId).ifPresent(builder::setReferralId);
@ -210,7 +212,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
Optional.ofNullable(rpcUser).ifPresent(builder::setRpcUser); Optional.ofNullable(rpcUser).ifPresent(builder::setRpcUser);
Optional.ofNullable(rpcPw).ifPresent(builder::setRpcPw); Optional.ofNullable(rpcPw).ifPresent(builder::setRpcPw);
Optional.ofNullable(takeOfferSelectedPaymentAccountId).ifPresent(builder::setTakeOfferSelectedPaymentAccountId); Optional.ofNullable(takeOfferSelectedPaymentAccountId).ifPresent(builder::setTakeOfferSelectedPaymentAccountId);
Optional.ofNullable(moneroNodeSettings).ifPresent(settings -> builder.setMoneroNodeSettings(settings.toProtoMessage()));
return protobuf.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build(); return protobuf.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build();
} }
@ -286,7 +288,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
proto.getHideNonAccountPaymentMethods(), proto.getHideNonAccountPaymentMethods(),
proto.getShowOffersMatchingMyAccounts(), proto.getShowOffersMatchingMyAccounts(),
proto.getDenyApiTaker(), 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();
}
}

View File

@ -0,0 +1,156 @@
/*
* 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.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.xmr.MoneroNodeSettings;
import bisq.proto.grpc.GetMoneroNodeSettingsReply;
import bisq.proto.grpc.GetMoneroNodeSettingsRequest;
import bisq.proto.grpc.IsMoneroNodeRunningReply;
import bisq.proto.grpc.IsMoneroNodeRunningRequest;
import bisq.proto.grpc.MoneroNodeGrpc.MoneroNodeImplBase;
import bisq.proto.grpc.StartMoneroNodeReply;
import bisq.proto.grpc.StartMoneroNodeRequest;
import bisq.proto.grpc.StopMoneroNodeReply;
import bisq.proto.grpc.StopMoneroNodeRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.MoneroNodeGrpc.getStartMoneroNodeMethod;
import static bisq.proto.grpc.MoneroNodeGrpc.getStopMoneroNodeMethod;
import static bisq.proto.grpc.MoneroNodeGrpc.getIsMoneroNodeRunningMethod;
import static bisq.proto.grpc.MoneroNodeGrpc.getGetMoneroNodeSettingsMethod;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
import monero.common.MoneroError;
@Slf4j
public class GrpcMoneroNodeService extends MoneroNodeImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcMoneroNodeService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void isMoneroNodeRunning(IsMoneroNodeRunningRequest request,
StreamObserver<IsMoneroNodeRunningReply> responseObserver) {
try {
var reply = IsMoneroNodeRunningReply.newBuilder()
.setIsRunning(coreApi.isMoneroNodeRunning())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void getMoneroNodeSettings(GetMoneroNodeSettingsRequest request,
StreamObserver<GetMoneroNodeSettingsReply> responseObserver) {
try {
var settings = coreApi.getMoneroNodeSettings();
var builder = GetMoneroNodeSettingsReply.newBuilder();
if (settings != null) {
builder.setSettings(settings.toProtoMessage());
}
var reply = builder.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void startMoneroNode(StartMoneroNodeRequest request,
StreamObserver<StartMoneroNodeReply> responseObserver) {
try {
var settings = request.getSettings();
coreApi.startMoneroNode(MoneroNodeSettings.fromProto(settings));
var reply = StartMoneroNodeReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (MoneroError me) {
handleMoneroError(me, responseObserver);
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void stopMoneroNode(StopMoneroNodeRequest request,
StreamObserver<StopMoneroNodeReply> responseObserver) {
try {
coreApi.stopMoneroNode();
var reply = StopMoneroNodeReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (MoneroError me) {
handleMoneroError(me, responseObserver);
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
private void handleMoneroError(MoneroError me, StreamObserver<?> responseObserver) {
// MoneroError is caused by the node startup failing, don't treat as unknown server error
// by wrapping with a handled exception type.
var headerLengthLimit = 8192; // MoneroErrors may print the entire monerod help text which causes a header overflow in grpc
if (me.getMessage().length() > headerLengthLimit) {
exceptionHandler.handleException(log, new IllegalStateException(me.getMessage().substring(0, headerLengthLimit - 1)), responseObserver);
} else {
exceptionHandler.handleException(log, new IllegalStateException(me), responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
private Optional<ServerInterceptor> rateMeteringInterceptor() {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
int allowedCallsPerTimeWindow = 10;
put(getIsMoneroNodeRunningMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetMoneroNodeSettingsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStartMoneroNodeMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStopMoneroNodeMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
}}
)));
}
}

View File

@ -62,7 +62,8 @@ public class GrpcServer {
GrpcTradesService tradesService, GrpcTradesService tradesService,
GrpcWalletsService walletsService, GrpcWalletsService walletsService,
GrpcNotificationsService notificationsService, GrpcNotificationsService notificationsService,
GrpcMoneroConnectionsService moneroConnectionsService) { GrpcMoneroConnectionsService moneroConnectionsService,
GrpcMoneroNodeService moneroNodeService) {
this.server = ServerBuilder.forPort(config.apiPort) this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor()) .executor(UserThread.getExecutor())
.addService(interceptForward(accountService, accountService.interceptors())) .addService(interceptForward(accountService, accountService.interceptors()))
@ -79,6 +80,7 @@ public class GrpcServer {
.addService(interceptForward(walletsService, walletsService.interceptors())) .addService(interceptForward(walletsService, walletsService.interceptors()))
.addService(interceptForward(notificationsService, notificationsService.interceptors())) .addService(interceptForward(notificationsService, notificationsService.interceptors()))
.addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors())) .addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors()))
.addService(interceptForward(moneroNodeService, moneroNodeService.interceptors()))
.intercept(passwordAuthInterceptor) .intercept(passwordAuthInterceptor)
.build(); .build();
coreContext.setApiUser(true); coreContext.setApiUser(true);

View File

@ -382,6 +382,48 @@ message SetAutoSwitchRequest {
message SetAutoSwitchReply {} message SetAutoSwitchReply {}
///////////////////////////////////////////////////////////////////////////////////////////
// MoneroNode
///////////////////////////////////////////////////////////////////////////////////////////
service MoneroNode {
rpc IsMoneroNodeRunning (IsMoneroNodeRunningRequest) returns (IsMoneroNodeRunningReply) {
}
rpc GetMoneroNodeSettings (GetMoneroNodeSettingsRequest) returns (GetMoneroNodeSettingsReply) {
}
rpc StartMoneroNode (StartMoneroNodeRequest) returns (StartMoneroNodeReply) {
}
rpc StopMoneroNode (StopMoneroNodeRequest) returns (StopMoneroNodeReply) {
}
}
message IsMoneroNodeRunningRequest {
}
message IsMoneroNodeRunningReply {
bool is_running = 1;
}
message GetMoneroNodeSettingsRequest {
}
message GetMoneroNodeSettingsReply {
MoneroNodeSettings settings = 1; // pb.proto
}
message StartMoneroNodeRequest {
MoneroNodeSettings settings = 1;
}
message StartMoneroNodeReply {
}
message StopMoneroNodeRequest {
}
message StopMoneroNodeReply {
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Offers // Offers
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1814,6 +1814,7 @@ message PreferencesPayload {
bool show_offers_matching_my_accounts = 55; bool show_offers_matching_my_accounts = 55;
bool deny_api_taker = 56; bool deny_api_taker = 56;
bool notify_on_pre_release = 57; bool notify_on_pre_release = 57;
MoneroNodeSettings monero_node_settings = 58;
} }
message AutoConfirmSettings { message AutoConfirmSettings {
@ -1824,6 +1825,12 @@ message AutoConfirmSettings {
string currency_code = 5; string currency_code = 5;
} }
message MoneroNodeSettings {
string blockchain_path = 1;
string bootstrap_url = 2;
repeated string startup_flags = 3;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// UserPayload // UserPayload
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////