mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
Add API functions to start and stop local Monero node
This commit is contained in:
parent
00765d7b32
commit
9dfbb0d5a6
12 changed files with 546 additions and 31 deletions
|
@ -172,7 +172,7 @@ public class KeyStorage {
|
|||
// Most of the time (probably of slightly less than 255/256, around 99.61%) a bad password
|
||||
// will result in BadPaddingException before HMAC check.
|
||||
// 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");
|
||||
else
|
||||
throw ce;
|
||||
|
|
|
@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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) {
|
||||
|
|
168
core/src/main/java/bisq/core/api/CoreMoneroNodeService.java
Normal file
168
core/src/main/java/bisq/core/api/CoreMoneroNodeService.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
50
core/src/main/java/bisq/core/xmr/MoneroNodeSettings.java
Normal file
50
core/src/main/java/bisq/core/xmr/MoneroNodeSettings.java
Normal 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();
|
||||
}
|
||||
}
|
156
daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroNodeService.java
Normal file
156
daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroNodeService.java
Normal 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));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -62,7 +62,8 @@ public class GrpcServer {
|
|||
GrpcTradesService tradesService,
|
||||
GrpcWalletsService walletsService,
|
||||
GrpcNotificationsService notificationsService,
|
||||
GrpcMoneroConnectionsService moneroConnectionsService) {
|
||||
GrpcMoneroConnectionsService moneroConnectionsService,
|
||||
GrpcMoneroNodeService moneroNodeService) {
|
||||
this.server = ServerBuilder.forPort(config.apiPort)
|
||||
.executor(UserThread.getExecutor())
|
||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||
|
@ -79,6 +80,7 @@ public class GrpcServer {
|
|||
.addService(interceptForward(walletsService, walletsService.interceptors()))
|
||||
.addService(interceptForward(notificationsService, notificationsService.interceptors()))
|
||||
.addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors()))
|
||||
.addService(interceptForward(moneroNodeService, moneroNodeService.interceptors()))
|
||||
.intercept(passwordAuthInterceptor)
|
||||
.build();
|
||||
coreContext.setApiUser(true);
|
||||
|
|
|
@ -382,6 +382,48 @@ message SetAutoSwitchRequest {
|
|||
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -1814,6 +1814,7 @@ message PreferencesPayload {
|
|||
bool show_offers_matching_my_accounts = 55;
|
||||
bool deny_api_taker = 56;
|
||||
bool notify_on_pre_release = 57;
|
||||
MoneroNodeSettings monero_node_settings = 58;
|
||||
}
|
||||
|
||||
message AutoConfirmSettings {
|
||||
|
@ -1824,6 +1825,12 @@ message AutoConfirmSettings {
|
|||
string currency_code = 5;
|
||||
}
|
||||
|
||||
message MoneroNodeSettings {
|
||||
string blockchain_path = 1;
|
||||
string bootstrap_url = 2;
|
||||
repeated string startup_flags = 3;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UserPayload
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
Loading…
Reference in a new issue