Add monero connections manager

This commit is contained in:
Fritz Lumnitz 2022-01-24 19:40:04 +01:00 committed by woodser
parent ed13047bf6
commit a3586fdef8
18 changed files with 1230 additions and 33 deletions

View file

@ -0,0 +1,50 @@
package bisq.core.api;
import javax.inject.Singleton;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @deprecated Should be replaced by actual implementation once it is available
*/
@Singleton
@Deprecated
public class CoreAccountService {
private static final String DEFAULT_PASSWORD = "abctesting123";
private String password = DEFAULT_PASSWORD;
private final List<PasswordChangeListener> listeners = new CopyOnWriteArrayList<>();
public String getPassword() {
return password;
}
public void setPassword(String newPassword) {
String oldPassword = password;
password = newPassword;
notifyListenerAboutPasswordChange(oldPassword, newPassword);
}
public void addPasswordChangeListener(PasswordChangeListener listener) {
Objects.requireNonNull(listener, "listener");
listeners.add(listener);
}
private void notifyListenerAboutPasswordChange(String oldPassword, String newPassword) {
for (PasswordChangeListener listener : listeners) {
listener.onPasswordChange(oldPassword, newPassword);
}
}
public interface PasswordChangeListener {
void onPasswordChange(String oldPassword, String newPassword);
}
}

View file

@ -59,6 +59,7 @@ import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroTxWallet;
@ -81,6 +82,7 @@ public class CoreApi {
private final CoreWalletsService walletsService;
private final TradeStatisticsManager tradeStatisticsManager;
private final CoreNotificationService notificationService;
private final CoreMoneroConnectionsService coreMoneroConnectionsService;
@Inject
public CoreApi(Config config,
@ -92,7 +94,8 @@ public class CoreApi {
CoreTradesService coreTradesService,
CoreWalletsService walletsService,
TradeStatisticsManager tradeStatisticsManager,
CoreNotificationService notificationService) {
CoreNotificationService notificationService,
CoreMoneroConnectionsService coreMoneroConnectionsService) {
this.config = config;
this.coreDisputeAgentsService = coreDisputeAgentsService;
this.coreHelpService = coreHelpService;
@ -103,6 +106,7 @@ public class CoreApi {
this.walletsService = walletsService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.notificationService = notificationService;
this.coreMoneroConnectionsService = coreMoneroConnectionsService;
}
@SuppressWarnings("SameReturnValue")
@ -398,4 +402,56 @@ public class CoreApi {
public int getNumConfirmationsForMostRecentTransaction(String addressString) {
return walletsService.getNumConfirmationsForMostRecentTransaction(addressString);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Monero Connections
///////////////////////////////////////////////////////////////////////////////////////////
public void addMoneroConnection(MoneroRpcConnection connection) {
coreMoneroConnectionsService.addConnection(connection);
}
public void removeMoneroConnection(String connectionUri) {
coreMoneroConnectionsService.removeConnection(connectionUri);
}
public MoneroRpcConnection getMoneroConnection() {
return coreMoneroConnectionsService.getConnection();
}
public List<MoneroRpcConnection> getMoneroConnections() {
return coreMoneroConnectionsService.getConnections();
}
public void setMoneroConnection(String connectionUri) {
coreMoneroConnectionsService.setConnection(connectionUri);
}
public void setMoneroConnection(MoneroRpcConnection connection) {
coreMoneroConnectionsService.setConnection(connection);
}
public MoneroRpcConnection checkMoneroConnection() {
return coreMoneroConnectionsService.checkConnection();
}
public List<MoneroRpcConnection> checkMoneroConnections() {
return coreMoneroConnectionsService.checkConnections();
}
public void startCheckingMoneroConnection(Long refreshPeriod) {
coreMoneroConnectionsService.startCheckingConnection(refreshPeriod);
}
public void stopCheckingMoneroConnection() {
coreMoneroConnectionsService.stopCheckingConnection();
}
public MoneroRpcConnection getBestAvailableMoneroConnection() {
return coreMoneroConnectionsService.getBestAvailableConnection();
}
public void setMoneroConnectionAutoSwitch(boolean autoSwitch) {
coreMoneroConnectionsService.setAutoSwitch(autoSwitch);
}
}

View file

@ -0,0 +1,162 @@
package bisq.core.api;
import bisq.core.btc.model.EncryptedConnectionList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroConnectionManager;
import monero.common.MoneroConnectionManagerListener;
import monero.common.MoneroRpcConnection;
@Slf4j
@Singleton
public class CoreMoneroConnectionsService {
// TODO: this connection manager should update app status, don't poll in WalletsSetup every 30 seconds
private static final long DEFAULT_REFRESH_PERIOD = 15_000L; // check the connection every 15 seconds per default
// 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://haveno.exchange:38081", "", "").setPriority(2)
);
private final Object lock = new Object();
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
@Inject
public CoreMoneroConnectionsService(MoneroConnectionManager connectionManager,
EncryptedConnectionList connectionList) {
this.connectionManager = connectionManager;
this.connectionList = connectionList;
}
public void initialize() {
synchronized (lock) {
// load connections
connectionList.getConnections().forEach(connectionManager::addConnection);
// add default connections
for (MoneroRpcConnection connection : DEFAULT_CONNECTIONS) {
if (connectionList.hasConnection(connection.getUri())) continue;
addConnection(connection);
}
// restore last used connection
connectionList.getCurrentConnectionUri().ifPresentOrElse(connectionManager::setConnection, () -> {
connectionManager.setConnection(DEFAULT_CONNECTIONS.get(0).getUri()); // default to localhost
});
// restore configuration
connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
long refreshPeriod = connectionList.getRefreshPeriod();
if (refreshPeriod > 0) connectionManager.startCheckingConnection(refreshPeriod);
else if (refreshPeriod == 0) connectionManager.startCheckingConnection(DEFAULT_REFRESH_PERIOD);
else checkConnection();
// register connection change listener
connectionManager.addListener(this::onConnectionChanged);
}
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
synchronized (lock) {
if (currentConnection == null) {
connectionList.setCurrentConnectionUri(null);
} else {
connectionList.removeConnection(currentConnection.getUri());
connectionList.addConnection(currentConnection);
connectionList.setCurrentConnectionUri(currentConnection.getUri());
}
}
}
public void addConnectionListener(MoneroConnectionManagerListener listener) {
synchronized (lock) {
connectionManager.addListener(listener);
}
}
public void addConnection(MoneroRpcConnection connection) {
synchronized (lock) {
connectionList.addConnection(connection);
connectionManager.addConnection(connection);
}
}
public void removeConnection(String uri) {
synchronized (lock) {
connectionList.removeConnection(uri);
connectionManager.removeConnection(uri);
}
}
public MoneroRpcConnection getConnection() {
synchronized (lock) {
return connectionManager.getConnection();
}
}
public List<MoneroRpcConnection> getConnections() {
synchronized (lock) {
return connectionManager.getConnections();
}
}
public void setConnection(String connectionUri) {
synchronized (lock) {
connectionManager.setConnection(connectionUri); // listener will update connection list
}
}
public void setConnection(MoneroRpcConnection connection) {
synchronized (lock) {
connectionManager.setConnection(connection); // listener will update connection list
}
}
public MoneroRpcConnection checkConnection() {
synchronized (lock) {
connectionManager.checkConnection();
return getConnection();
}
}
public List<MoneroRpcConnection> checkConnections() {
synchronized (lock) {
connectionManager.checkConnections();
return getConnections();
}
}
public void startCheckingConnection(Long refreshPeriod) {
synchronized (lock) {
connectionManager.startCheckingConnection(refreshPeriod == null ? DEFAULT_REFRESH_PERIOD : refreshPeriod);
connectionList.setRefreshPeriod(refreshPeriod);
}
}
public void stopCheckingConnection() {
synchronized (lock) {
connectionManager.stopCheckingConnection();
connectionList.setRefreshPeriod(-1L);
}
}
public MoneroRpcConnection getBestAvailableConnection() {
synchronized (lock) {
return connectionManager.getBestAvailableConnection();
}
}
public void setAutoSwitch(boolean autoSwitch) {
synchronized (lock) {
connectionManager.setAutoSwitch(autoSwitch);
connectionList.setAutoSwitch(autoSwitch);
}
}
}

View file

@ -0,0 +1,40 @@
package bisq.core.api.model;
import bisq.common.proto.persistable.PersistablePayload;
import com.google.protobuf.ByteString;
import lombok.Builder;
import lombok.Value;
@Value
@Builder(toBuilder = true)
public class EncryptedConnection implements PersistablePayload {
String uri;
String username;
byte[] encryptedPassword;
byte[] encryptionSalt;
int priority;
@Override
public protobuf.EncryptedConnection toProtoMessage() {
return protobuf.EncryptedConnection.newBuilder()
.setUri(uri)
.setUsername(username)
.setEncryptedPassword(ByteString.copyFrom(encryptedPassword))
.setEncryptionSalt(ByteString.copyFrom(encryptionSalt))
.setPriority(priority)
.build();
}
public static EncryptedConnection fromProto(protobuf.EncryptedConnection encryptedConnection) {
return new EncryptedConnection(
encryptedConnection.getUri(),
encryptedConnection.getUsername(),
encryptedConnection.getEncryptedPassword().toByteArray(),
encryptedConnection.getEncryptionSalt().toByteArray(),
encryptedConnection.getPriority());
}
}

View file

@ -31,6 +31,7 @@ import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.ImmutableCoinFormatter;
import bisq.core.xmr.connection.MoneroConnectionModule;
import bisq.network.crypto.EncryptionServiceModule;
import bisq.network.p2p.P2PModule;
@ -91,6 +92,7 @@ public class CoreModule extends AppModule {
install(new AlertModule(config));
install(new FilterModule(config));
install(new CorePresentationModule(config));
install(new MoneroConnectionModule(config));
bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class);
}
}

View file

@ -18,6 +18,7 @@
package bisq.core.btc;
import bisq.core.btc.model.AddressEntryList;
import bisq.core.btc.model.EncryptedConnectionList;
import bisq.core.btc.model.XmrAddressEntryList;
import bisq.core.btc.nodes.BtcNodes;
import bisq.core.btc.setup.RegTestHost;
@ -83,6 +84,7 @@ public class BitcoinModule extends AppModule {
bind(AddressEntryList.class).in(Singleton.class);
bind(XmrAddressEntryList.class).in(Singleton.class);
bind(EncryptedConnectionList.class).in(Singleton.class);
bind(WalletsSetup.class).in(Singleton.class);
bind(XmrWalletService.class).in(Singleton.class);
bind(BtcWalletService.class).in(Singleton.class);

View file

@ -0,0 +1,395 @@
package bisq.core.btc.model;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Encryption;
import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.persistable.PersistableEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.core.api.CoreAccountService;
import bisq.core.api.model.EncryptedConnection;
import bisq.core.crypto.ScryptUtil;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.crypto.SecretKey;
import javax.inject.Inject;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import lombok.NonNull;
import monero.common.MoneroRpcConnection;
import org.bitcoinj.crypto.KeyCrypterScrypt;
/**
* Store for {@link EncryptedConnection}s.
* <p>
* Passwords are encrypted when stored onto disk, using the account password.
* If a connection has no password, this is "hidden" by using some random value as fake password.
*
* @implNote The password encryption mechanism is handled as follows.
* A random salt is generated and stored for each connection. If the connection has no password,
* the salt is used as prefix and some random data is attached as fake password. If the connection has a password,
* the salt is used as suffix to the actual password. When the password gets decrypted, it is checked whether the
* salt is a prefix of the decrypted value. If it is a prefix, the connection has no password.
* Otherwise, it is removed (from the end) and the remaining value is the actual password.
*/
public class EncryptedConnectionList implements PersistableEnvelope, PersistedDataHost {
private static final int MIN_FAKE_PASSWORD_LENGTH = 5;
private static final int MAX_FAKE_PASSWORD_LENGTH = 32;
private static final int SALT_LENGTH = 16;
transient private final ReadWriteLock lock = new ReentrantReadWriteLock();
transient private final Lock readLock = lock.readLock();
transient private final Lock writeLock = lock.writeLock();
transient private final SecureRandom random = new SecureRandom();
transient private KeyCrypterScrypt keyCrypterScrypt;
transient private SecretKey encryptionKey;
transient private CoreAccountService accountService;
transient private PersistenceManager<EncryptedConnectionList> persistenceManager;
private final Map<String, EncryptedConnection> items = new HashMap<>();
private @NonNull String currentConnectionUri = "";
private long refreshPeriod;
private boolean autoSwitch;
@Inject
public EncryptedConnectionList(PersistenceManager<EncryptedConnectionList> persistenceManager,
CoreAccountService accountService) {
this.accountService = accountService;
this.persistenceManager = persistenceManager;
this.persistenceManager.initialize(this, "EncryptedConnectionList", PersistenceManager.Source.PRIVATE);
this.accountService.addPasswordChangeListener(this::onPasswordChange);
}
private EncryptedConnectionList(byte[] salt,
List<EncryptedConnection> items,
@NonNull String currentConnectionUri,
long refreshPeriod,
boolean autoSwitch) {
this.keyCrypterScrypt = ScryptUtil.getKeyCrypterScrypt(salt);
this.items.putAll(items.stream().collect(Collectors.toMap(EncryptedConnection::getUri, Function.identity())));
this.currentConnectionUri = currentConnectionUri;
this.refreshPeriod = refreshPeriod;
this.autoSwitch = autoSwitch;
}
@Override
public void readPersisted(Runnable completeHandler) {
persistenceManager.readPersisted(persistedEncryptedConnectionList -> {
writeLock.lock();
try {
initializeEncryption(persistedEncryptedConnectionList.keyCrypterScrypt);
items.clear();
items.putAll(persistedEncryptedConnectionList.items);
currentConnectionUri = persistedEncryptedConnectionList.currentConnectionUri;
refreshPeriod = persistedEncryptedConnectionList.refreshPeriod;
autoSwitch = persistedEncryptedConnectionList.autoSwitch;
} finally {
writeLock.unlock();
}
completeHandler.run();
}, () -> {
writeLock.lock();
try {
initializeEncryption(ScryptUtil.getKeyCrypterScrypt());
} finally {
writeLock.unlock();
}
completeHandler.run();
});
}
private void initializeEncryption(KeyCrypterScrypt keyCrypterScrypt) {
this.keyCrypterScrypt = keyCrypterScrypt;
encryptionKey = toSecretKey(accountService.getPassword());
}
public List<MoneroRpcConnection> getConnections() {
readLock.lock();
try {
return items.values().stream().map(this::toMoneroRpcConnection).collect(Collectors.toList());
} finally {
readLock.unlock();
}
}
public boolean hasConnection(String connection) {
readLock.lock();
try {
return items.containsKey(connection);
} finally {
readLock.unlock();
}
}
public void addConnection(MoneroRpcConnection connection) {
EncryptedConnection currentValue;
writeLock.lock();
try {
EncryptedConnection encryptedConnection = toEncryptedConnection(connection);
currentValue = items.putIfAbsent(connection.getUri(), encryptedConnection);
} finally {
writeLock.unlock();
}
if (currentValue != null) {
throw new IllegalStateException(String.format("There exists already an connection for \"%s\"", connection.getUri()));
}
requestPersistence();
}
public void removeConnection(String connection) {
writeLock.lock();
try {
items.remove(connection);
} finally {
writeLock.unlock();
}
requestPersistence();
}
public void setAutoSwitch(boolean autoSwitch) {
boolean changed;
writeLock.lock();
try {
changed = this.autoSwitch != (this.autoSwitch = autoSwitch);
} finally {
writeLock.unlock();
}
if (changed) {
requestPersistence();
}
}
public boolean getAutoSwitch() {
readLock.lock();
try {
return autoSwitch;
} finally {
readLock.unlock();
}
}
public void setRefreshPeriod(Long refreshPeriod) {
boolean changed;
writeLock.lock();
try {
changed = this.refreshPeriod != (this.refreshPeriod = refreshPeriod == null ? 0L : refreshPeriod);
} finally {
writeLock.unlock();
}
if (changed) {
requestPersistence();
}
}
public long getRefreshPeriod() {
readLock.lock();
try {
return refreshPeriod;
} finally {
readLock.unlock();
}
}
public void setCurrentConnectionUri(String currentConnectionUri) {
boolean changed;
writeLock.lock();
try {
changed = !this.currentConnectionUri.equals(this.currentConnectionUri = currentConnectionUri == null ? "" : currentConnectionUri);
} finally {
writeLock.unlock();
}
if (changed) {
requestPersistence();
}
}
public Optional<String> getCurrentConnectionUri() {
readLock.lock();
try {
return Optional.of(currentConnectionUri).filter(s -> !s.isEmpty());
} finally {
readLock.unlock();
}
}
private void requestPersistence() {
persistenceManager.requestPersistence();
}
private void onPasswordChange(String oldPassword, String newPassword) {
writeLock.lock();
try {
SecretKey oldSecret = encryptionKey;
assert Objects.equals(oldSecret, toSecretKey(oldPassword)) : "Old secret does not match old password";
encryptionKey = toSecretKey(newPassword);
items.replaceAll((key, connection) -> reEncrypt(connection, oldSecret, encryptionKey));
} finally {
writeLock.unlock();
}
requestPersistence();
}
private SecretKey toSecretKey(String password) {
if (password == null) {
return null;
}
return Encryption.getSecretKeyFromBytes(keyCrypterScrypt.deriveKey(password).getKey());
}
private static EncryptedConnection reEncrypt(EncryptedConnection connection,
SecretKey oldSecret, SecretKey newSecret) {
return connection.toBuilder()
.encryptedPassword(reEncrypt(connection.getEncryptedPassword(), oldSecret, newSecret))
.build();
}
private static byte[] reEncrypt(byte[] value,
SecretKey oldSecret, SecretKey newSecret) {
// was previously not encrypted if null
byte[] decrypted = oldSecret == null ? value : decrypt(value, oldSecret);
// should not be encrypted if null
return newSecret == null ? decrypted : encrypt(decrypted, newSecret);
}
private static byte[] decrypt(byte[] encrypted, SecretKey secret) {
try {
return Encryption.decrypt(encrypted, secret);
} catch (CryptoException e) {
throw new IllegalArgumentException("Illegal old password", e);
}
}
private static byte[] encrypt(byte[] unencrypted, SecretKey secretKey) {
try {
return Encryption.encrypt(unencrypted, secretKey);
} catch (CryptoException e) {
throw new RuntimeException("Could not encrypt data with the provided secret", e);
}
}
private EncryptedConnection toEncryptedConnection(MoneroRpcConnection connection) {
String password = connection.getPassword();
byte[] passwordBytes = password == null ? null : password.getBytes(StandardCharsets.UTF_8);
byte[] passwordSalt = generateSalt(passwordBytes);
byte[] encryptedPassword = encryptPassword(passwordBytes, passwordSalt);
return EncryptedConnection.builder()
.uri(connection.getUri())
.username(connection.getUsername() == null ? "" : connection.getUsername())
.encryptedPassword(encryptedPassword)
.encryptionSalt(passwordSalt)
.priority(connection.getPriority())
.build();
}
private MoneroRpcConnection toMoneroRpcConnection(EncryptedConnection connection) {
byte[] decryptedPasswordBytes = decryptPassword(connection.getEncryptedPassword(), connection.getEncryptionSalt());
String password = decryptedPasswordBytes == null ? null : new String(decryptedPasswordBytes, StandardCharsets.UTF_8);
String username = connection.getUsername().isEmpty() ? null : connection.getUsername();
MoneroRpcConnection moneroRpcConnection = new MoneroRpcConnection(connection.getUri(), username, password);
moneroRpcConnection.setPriority(connection.getPriority());
return moneroRpcConnection;
}
private byte[] encryptPassword(byte[] password, byte[] salt) {
byte[] saltedPassword;
if (password == null) {
// no password given, so use salt as prefix and add some random data, which disguises itself as password
int fakePasswordLength = random.nextInt(MAX_FAKE_PASSWORD_LENGTH - MIN_FAKE_PASSWORD_LENGTH + 1)
+ MIN_FAKE_PASSWORD_LENGTH;
byte[] fakePassword = new byte[fakePasswordLength];
random.nextBytes(fakePassword);
saltedPassword = new byte[salt.length + fakePasswordLength];
System.arraycopy(salt, 0, saltedPassword, 0, salt.length);
System.arraycopy(fakePassword, 0, saltedPassword, salt.length, fakePassword.length);
} else {
// password given, so append salt to end
saltedPassword = new byte[password.length + salt.length];
System.arraycopy(password, 0, saltedPassword, 0, password.length);
System.arraycopy(salt, 0, saltedPassword, password.length, salt.length);
}
return encrypt(saltedPassword, encryptionKey);
}
private byte[] decryptPassword(byte[] encryptedSaltedPassword, byte[] salt) {
byte[] decryptedSaltedPassword = decrypt(encryptedSaltedPassword, encryptionKey);
if (arrayStartsWith(decryptedSaltedPassword, salt)) {
// salt is prefix, so no actual password set
return null;
} else {
// remove salt suffix, the rest is the actual password
byte[] decryptedPassword = new byte[decryptedSaltedPassword.length - salt.length];
System.arraycopy(decryptedSaltedPassword, 0, decryptedPassword, 0, decryptedPassword.length);
return decryptedPassword;
}
}
private byte[] generateSalt(byte[] password) {
byte[] salt = new byte[SALT_LENGTH];
// Generate salt, that is guaranteed to be no prefix of the password
do {
random.nextBytes(salt);
} while (password != null && arrayStartsWith(password, salt));
return salt;
}
private static boolean arrayStartsWith(byte[] container, byte[] prefix) {
if (container.length < prefix.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (container[i] != prefix[i]) {
return false;
}
}
return true;
}
@Override
public Message toProtoMessage() {
List<protobuf.EncryptedConnection> connections;
ByteString saltString;
String currentConnectionUri;
boolean autoSwitchEnabled;
long refreshPeriod;
readLock.lock();
try {
connections = items.values().stream()
.map(EncryptedConnection::toProtoMessage).collect(Collectors.toList());
saltString = keyCrypterScrypt.getScryptParameters().getSalt();
currentConnectionUri = this.currentConnectionUri;
autoSwitchEnabled = this.autoSwitch;
refreshPeriod = this.refreshPeriod;
} finally {
readLock.unlock();
}
return protobuf.PersistableEnvelope.newBuilder()
.setEncryptedConnectionList(protobuf.EncryptedConnectionList.newBuilder()
.setSalt(saltString)
.addAllItems(connections)
.setCurrentConnectionUri(currentConnectionUri)
.setRefreshPeriod(refreshPeriod)
.setAutoSwitch(autoSwitchEnabled))
.build();
}
public static EncryptedConnectionList fromProto(protobuf.EncryptedConnectionList proto) {
List<EncryptedConnection> items = proto.getItemsList().stream()
.map(EncryptedConnection::fromProto)
.collect(Collectors.toList());
return new EncryptedConnectionList(proto.getSalt().toByteArray(), items, proto.getCurrentConnectionUri(), proto.getRefreshPeriod(), proto.getAutoSwitch());
}
}

View file

@ -17,6 +17,7 @@
package bisq.core.btc.setup;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.btc.nodes.ProxySocketFactory;
import bisq.core.btc.wallet.HavenoRiskAnalysis;
@ -88,6 +89,7 @@ import static bisq.common.util.Preconditions.checkDir;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils;
import monero.daemon.MoneroDaemon;
import monero.daemon.MoneroDaemonRpc;
@ -126,9 +128,6 @@ public class WalletConfig extends AbstractIdleService {
// Monero configuration
// TODO: don't hard code configuration, inject into classes?
private static final MoneroNetworkType MONERO_NETWORK_TYPE = MoneroNetworkType.STAGENET;
private static final String MONERO_DAEMON_URI = "http://localhost:38081";
private static final String MONERO_DAEMON_USERNAME = "superuser";
private static final String MONERO_DAEMON_PASSWORD = "abctesting123";
private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager();
private static final String MONERO_WALLET_RPC_DIR = System.getProperty("user.dir") + File.separator + ".localnet"; // .localnet contains monero-wallet-rpc and wallet files
private static final String MONERO_WALLET_RPC_PATH = MONERO_WALLET_RPC_DIR + File.separator + "monero-wallet-rpc";
@ -138,9 +137,10 @@ public class WalletConfig extends AbstractIdleService {
protected final NetworkParameters params;
protected final String filePrefix;
protected final CoreMoneroConnectionsService moneroConnectionsManager;
protected volatile BlockChain vChain;
protected volatile SPVBlockStore vStore;
protected volatile MoneroDaemon vXmrDaemon;
protected volatile MoneroDaemonRpc vXmrDaemon;
protected volatile MoneroWalletRpc vXmrWallet;
protected volatile Wallet vBtcWallet;
protected volatile PeerGroup vPeerGroup;
@ -177,18 +177,20 @@ public class WalletConfig extends AbstractIdleService {
public WalletConfig(NetworkParameters params,
File directory,
int rpcBindPort,
CoreMoneroConnectionsService connectionsManager,
String filePrefix) {
this(new Context(params), directory, rpcBindPort, filePrefix);
this(new Context(params), directory, rpcBindPort, connectionsManager, filePrefix);
}
/**
* Creates a new WalletConfig, with the given {@link Context}. Files will be stored in the given directory.
*/
private WalletConfig(Context context, File directory, int rpcBindPort, String filePrefix) {
private WalletConfig(Context context, File directory, int rpcBindPort, CoreMoneroConnectionsService connectionsManager, String filePrefix) {
this.context = context;
this.params = checkNotNull(context.getParams());
this.directory = checkDir(directory);
this.rpcBindPort = rpcBindPort;
this.moneroConnectionsManager = connectionsManager;
this.filePrefix = checkNotNull(filePrefix);
}
@ -335,15 +337,21 @@ public class WalletConfig extends AbstractIdleService {
// check if monero-wallet-rpc exists
if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH + "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system");
// get app's current daemon connection
MoneroRpcConnection connection = moneroConnectionsManager.getConnection();
// start monero-wallet-rpc instance and return connected client
List<String> cmd = new ArrayList<>(Arrays.asList( // modifiable list
MONERO_WALLET_RPC_PATH,
"--" + MONERO_NETWORK_TYPE.toString().toLowerCase(),
"--daemon-address", MONERO_DAEMON_URI,
"--daemon-login", MONERO_DAEMON_USERNAME + ":" + MONERO_DAEMON_PASSWORD,
"--daemon-address", connection.getUri(),
"--rpc-login", MONERO_WALLET_RPC_USERNAME + ":" + MONERO_WALLET_RPC_PASSWORD,
"--wallet-dir", directory.toString()
));
if (connection.getUsername() != null) {
cmd.add("--daemon-login");
cmd.add(connection.getUsername() + ":" + connection.getPassword());
}
if (port != null && port > 0) {
cmd.add("--rpc-bind-port");
cmd.add(Integer.toString(port));
@ -372,8 +380,11 @@ public class WalletConfig extends AbstractIdleService {
File chainFile = new File(directory, filePrefix + ".spvchain");
boolean chainFileExists = chainFile.exists();
// XMR daemon
vXmrDaemon = new MoneroDaemonRpc(MONERO_DAEMON_URI, MONERO_DAEMON_USERNAME, MONERO_DAEMON_PASSWORD);
// set XMR daemon and listen for updates
vXmrDaemon = new MoneroDaemonRpc(moneroConnectionsManager.getConnection());
moneroConnectionsManager.addConnectionListener(newConnection -> {
vXmrDaemon = newConnection == null ? null : new MoneroDaemonRpc(newConnection);
});
// XMR wallet
String xmrPrefix = "_XMR";

View file

@ -17,6 +17,7 @@
package bisq.core.btc.setup;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.exceptions.InvalidHostException;
import bisq.core.btc.exceptions.RejectedTxException;
import bisq.core.btc.model.AddressEntry;
@ -127,6 +128,8 @@ public class WalletsSetup {
private final Config config;
private final LocalBitcoinNode localBitcoinNode;
private final BtcNodes btcNodes;
@Getter
private final CoreMoneroConnectionsService moneroConnectionsManager;
private final String xmrWalletFileName;
private final int numConnectionsForBtc;
private final String userAgent;
@ -156,6 +159,7 @@ public class WalletsSetup {
Config config,
LocalBitcoinNode localBitcoinNode,
BtcNodes btcNodes,
CoreMoneroConnectionsService moneroConnectionsManager,
@Named(Config.USER_AGENT) String userAgent,
@Named(Config.WALLET_DIR) File walletDir,
@Named(Config.WALLET_RPC_BIND_PORT) int walletRpcBindPort,
@ -170,6 +174,7 @@ public class WalletsSetup {
this.config = config;
this.localBitcoinNode = localBitcoinNode;
this.btcNodes = btcNodes;
this.moneroConnectionsManager = moneroConnectionsManager;
this.numConnectionsForBtc = numConnectionsForBtc;
this.useAllProvidedNodes = useAllProvidedNodes;
this.userAgent = userAgent;
@ -201,12 +206,15 @@ public class WalletsSetup {
exceptionHandler.handleException(new TimeoutException("Wallet did not initialize in " +
STARTUP_TIMEOUT + " seconds.")), STARTUP_TIMEOUT);
// initialize Monero connection manager
moneroConnectionsManager.initialize();
backupWallets();
final Socks5Proxy socks5Proxy = preferences.getUseTorForBitcoinJ() ? socks5ProxyProvider.getSocks5Proxy() : null;
log.info("Socks5Proxy for bitcoinj: socks5Proxy=" + socks5Proxy);
walletConfig = new WalletConfig(params, walletDir, walletRpcBindPort, "haveno") {
walletConfig = new WalletConfig(params, walletDir, walletRpcBindPort, moneroConnectionsManager, "haveno") {
@Override
protected void onSetupCompleted() {
//We are here in the btcj thread Thread[ STARTING,5,main]
@ -220,9 +228,7 @@ public class WalletsSetup {
peerGroup.setAddPeersFromAddressMessage(false);
UserThread.runPeriodically(() -> {
peers.set(getPeerConnections());
numPeers.set(peers.get().size());
chainHeight.set(vXmrDaemon.getHeight());
updateDaemonInfo();
}, DAEMON_POLL_INTERVAL_SECONDS);
// Need to be Threading.SAME_THREAD executor otherwise BitcoinJ will skip that listener
@ -240,9 +246,7 @@ public class WalletsSetup {
// Map to user thread
UserThread.execute(() -> {
peers.set(getPeerConnections());
numPeers.set(peers.get().size());
chainHeight.set(vXmrDaemon.getHeight());
updateDaemonInfo();
addressEntryList.onWalletReady(walletConfig.btcWallet());
xmrAddressEntryList.onWalletReady(walletConfig.getXmrWallet());
timeoutTimer.stop();
@ -253,7 +257,18 @@ public class WalletsSetup {
UserThread.runAfter(resultHandler::handleResult, 100, TimeUnit.MILLISECONDS);
}
private List<MoneroPeer> getPeerConnections() {
private void updateDaemonInfo() {
try {
if (vXmrDaemon == null) throw new RuntimeException("No daemon connection");
peers.set(getOnlinePeers());
numPeers.set(peers.get().size());
chainHeight.set(vXmrDaemon.getHeight());
} catch (Exception e) {
log.warn("Could not update daemon info: " + e.getMessage());
}
}
private List<MoneroPeer> getOnlinePeers() {
return vXmrDaemon.getPeers().stream()
.filter(peer -> peer.isOnline())
.collect(Collectors.toList());

View file

@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.MoneroDaemon;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
@ -53,8 +54,6 @@ public class XmrWalletService {
protected final CopyOnWriteArraySet<MoneroWalletListenerI> walletListeners = new CopyOnWriteArraySet<>();
private Map<String, MoneroWallet> multisigWallets;
@Getter
private MoneroDaemon daemon;
@Getter
private MoneroWallet wallet;
@ -67,7 +66,6 @@ public class XmrWalletService {
this.multisigWallets = new HashMap<String, MoneroWallet>();
walletsSetup.addSetupCompletedHandler(() -> {
daemon = walletsSetup.getXmrDaemon();
wallet = walletsSetup.getXmrWallet();
wallet.addListener(new MoneroWalletListener() {
@Override
@ -81,7 +79,15 @@ public class XmrWalletService {
notifyBalanceListeners();
}
});
walletsSetup.getMoneroConnectionsManager().addConnectionListener(newConnection -> {
updateDaemonConnections(newConnection);
});
});
}
public MoneroDaemon getDaemon() {
return walletsSetup.getXmrDaemon();
}
// TODO (woodser): wallet has single password which is passed here?
@ -404,6 +410,12 @@ public class XmrWalletService {
}
}
private void updateDaemonConnections(MoneroRpcConnection connection) {
log.info("Setting wallet daemon connections: " + (connection == null ? null : connection.getUri()));
walletsSetup.getXmrWallet().setDaemonConnection(connection);
for (MoneroWallet multisigWallet : multisigWallets.values()) multisigWallet.setDaemonConnection(connection);
}
/**
* Wraps a MoneroWalletListener to notify the Haveno application.
*

View file

@ -39,11 +39,15 @@ public class ScryptUtil {
}
public static KeyCrypterScrypt getKeyCrypterScrypt() {
return getKeyCrypterScrypt(KeyCrypterScrypt.randomSalt());
}
public static KeyCrypterScrypt getKeyCrypterScrypt(byte[] salt) {
Protos.ScryptParameters scryptParameters = Protos.ScryptParameters.newBuilder()
.setP(6)
.setR(8)
.setN(32768)
.setSalt(ByteString.copyFrom(KeyCrypterScrypt.randomSalt()))
.setSalt(ByteString.copyFrom(salt))
.build();
return new KeyCrypterScrypt(scryptParameters);
}

View file

@ -20,6 +20,7 @@ package bisq.core.proto.persistable;
import bisq.core.account.sign.SignedWitnessStore;
import bisq.core.account.witness.AccountAgeWitnessStore;
import bisq.core.btc.model.AddressEntryList;
import bisq.core.btc.model.EncryptedConnectionList;
import bisq.core.btc.model.XmrAddressEntryList;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.XmrWalletService;
@ -34,7 +35,6 @@ import bisq.core.trade.statistics.TradeStatistics2Store;
import bisq.core.trade.statistics.TradeStatistics3Store;
import bisq.core.user.PreferencesPayload;
import bisq.core.user.UserPayload;
import bisq.network.p2p.mailbox.IgnoredMailboxMap;
import bisq.network.p2p.mailbox.MailboxMessageList;
import bisq.network.p2p.peers.peerexchange.PeerList;
@ -85,6 +85,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
return AddressEntryList.fromProto(proto.getAddressEntryList());
case XMR_ADDRESS_ENTRY_LIST:
return XmrAddressEntryList.fromProto(proto.getXmrAddressEntryList());
case ENCRYPTED_CONNECTION_LIST:
return EncryptedConnectionList.fromProto(proto.getEncryptedConnectionList());
case TRADABLE_LIST:
return TradableList.fromProto(proto.getTradableList(), this, xmrWalletService.get());
case ARBITRATION_DISPUTE_LIST:

View file

@ -18,6 +18,7 @@
package bisq.core.setup;
import bisq.core.btc.model.AddressEntryList;
import bisq.core.btc.model.EncryptedConnectionList;
import bisq.core.btc.model.XmrAddressEntryList;
import bisq.core.offer.OpenOfferManager;
import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService;
@ -28,14 +29,12 @@ import bisq.core.trade.closed.ClosedTradableManager;
import bisq.core.trade.failed.FailedTradesManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.network.p2p.mailbox.IgnoredMailboxService;
import bisq.network.p2p.mailbox.MailboxMessageService;
import bisq.network.p2p.peers.PeerManager;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.persistence.RemovedPayloadsService;
import bisq.common.config.Config;
import bisq.common.proto.persistable.PersistedDataHost;
import com.google.inject.Injector;
@ -55,6 +54,7 @@ public class CorePersistedDataHost {
persistedDataHosts.add(injector.getInstance(User.class));
persistedDataHosts.add(injector.getInstance(AddressEntryList.class));
persistedDataHosts.add(injector.getInstance(XmrAddressEntryList.class));
persistedDataHosts.add(injector.getInstance(EncryptedConnectionList.class));
persistedDataHosts.add(injector.getInstance(OpenOfferManager.class));
persistedDataHosts.add(injector.getInstance(TradeManager.class));
persistedDataHosts.add(injector.getInstance(ClosedTradableManager.class));

View file

@ -0,0 +1,23 @@
package bisq.core.xmr.connection;
import bisq.common.app.AppModule;
import bisq.common.config.Config;
import bisq.core.api.CoreMoneroConnectionsService;
import bisq.core.btc.model.EncryptedConnectionList;
import com.google.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MoneroConnectionModule extends AppModule {
public MoneroConnectionModule(Config config) {
super(config);
}
@Override
protected final void configure() {
bind(EncryptedConnectionList.class).in(Singleton.class);
bind(CoreMoneroConnectionsService.class).in(Singleton.class);
}
}

View file

@ -0,0 +1,286 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
import bisq.proto.grpc.AddConnectionReply;
import bisq.proto.grpc.AddConnectionRequest;
import bisq.proto.grpc.CheckConnectionReply;
import bisq.proto.grpc.CheckConnectionRequest;
import bisq.proto.grpc.CheckConnectionsReply;
import bisq.proto.grpc.CheckConnectionsRequest;
import bisq.proto.grpc.GetBestAvailableConnectionReply;
import bisq.proto.grpc.GetBestAvailableConnectionRequest;
import bisq.proto.grpc.GetConnectionReply;
import bisq.proto.grpc.GetConnectionRequest;
import bisq.proto.grpc.GetConnectionsReply;
import bisq.proto.grpc.GetConnectionsRequest;
import bisq.proto.grpc.RemoveConnectionReply;
import bisq.proto.grpc.RemoveConnectionRequest;
import bisq.proto.grpc.SetAutoSwitchReply;
import bisq.proto.grpc.SetAutoSwitchRequest;
import bisq.proto.grpc.SetConnectionReply;
import bisq.proto.grpc.SetConnectionRequest;
import bisq.proto.grpc.StartCheckingConnectionsReply;
import bisq.proto.grpc.StartCheckingConnectionsRequest;
import bisq.proto.grpc.StopCheckingConnectionsReply;
import bisq.proto.grpc.StopCheckingConnectionsRequest;
import bisq.proto.grpc.UriConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.MoneroConnectionsGrpc.*;
import static java.util.concurrent.TimeUnit.SECONDS;
@Slf4j
class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcMoneroConnectionsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void addConnection(AddConnectionRequest request,
StreamObserver<AddConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.addMoneroConnection(toMoneroRpcConnection(request.getConnection()));
return AddConnectionReply.newBuilder().build();
});
}
@Override
public void removeConnection(RemoveConnectionRequest request,
StreamObserver<RemoveConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.removeMoneroConnection(validateUri(request.getUri()));
return RemoveConnectionReply.newBuilder().build();
});
}
@Override
public void getConnection(GetConnectionRequest request,
StreamObserver<GetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
UriConnection replyConnection = toUriConnection(coreApi.getMoneroConnection());
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
}
return builder.build();
});
}
@Override
public void getConnections(GetConnectionsRequest request,
StreamObserver<GetConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
List<MoneroRpcConnection> connections = coreApi.getMoneroConnections();
List<UriConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
return GetConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
});
}
@Override
public void setConnection(SetConnectionRequest request,
StreamObserver<SetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
if (request.getUri() != null && !request.getUri().isEmpty())
coreApi.setMoneroConnection(validateUri(request.getUri()));
else if (request.hasConnection())
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
return SetConnectionReply.newBuilder().build();
});
}
@Override
public void checkConnection(CheckConnectionRequest request,
StreamObserver<CheckConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
UriConnection replyConnection = toUriConnection(connection);
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
}
return builder.build();
});
}
@Override
public void checkConnections(CheckConnectionsRequest request,
StreamObserver<CheckConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
List<MoneroRpcConnection> connections = coreApi.checkMoneroConnections();
List<UriConnection> replyConnections = connections.stream()
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
return CheckConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
});
}
@Override
public void startCheckingConnections(StartCheckingConnectionsRequest request,
StreamObserver<StartCheckingConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
int refreshMillis = request.getRefreshPeriod();
Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis;
coreApi.startCheckingMoneroConnection(refreshPeriod);
return StartCheckingConnectionsReply.newBuilder().build();
});
}
@Override
public void stopCheckingConnections(StopCheckingConnectionsRequest request,
StreamObserver<StopCheckingConnectionsReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.stopCheckingMoneroConnection();
return StopCheckingConnectionsReply.newBuilder().build();
});
}
@Override
public void getBestAvailableConnection(GetBestAvailableConnectionRequest request,
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
UriConnection replyConnection = toUriConnection(connection);
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
}
return builder.build();
});
}
@Override
public void setAutoSwitch(SetAutoSwitchRequest request,
StreamObserver<SetAutoSwitchReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.setMoneroConnectionAutoSwitch(request.getAutoSwitch());
return SetAutoSwitchReply.newBuilder().build();
});
}
private <_Reply> void handleRequest(StreamObserver<_Reply> responseObserver,
RpcRequestHandler<_Reply> handler) {
try {
_Reply reply = handler.handleRequest();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@FunctionalInterface
private interface RpcRequestHandler<_Reply> {
_Reply handleRequest() throws Exception;
}
private static UriConnection toUriConnection(MoneroRpcConnection rpcConnection) {
if (rpcConnection == null) return null;
return UriConnection.newBuilder()
.setUri(rpcConnection.getUri())
.setPriority(rpcConnection.getPriority())
.setOnlineStatus(toOnlineStatus(rpcConnection.isOnline()))
.setAuthenticationStatus(toAuthenticationStatus(rpcConnection.isAuthenticated()))
.build();
}
private static UriConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
if (authenticated == null) return UriConnection.AuthenticationStatus.NO_AUTHENTICATION;
else if (authenticated) return UriConnection.AuthenticationStatus.AUTHENTICATED;
else return UriConnection.AuthenticationStatus.NOT_AUTHENTICATED;
}
private static UriConnection.OnlineStatus toOnlineStatus(Boolean online) {
if (online == null) return UriConnection.OnlineStatus.UNKNOWN;
else if (online) return UriConnection.OnlineStatus.ONLINE;
else return UriConnection.OnlineStatus.OFFLINE;
}
private static MoneroRpcConnection toMoneroRpcConnection(UriConnection uriConnection) throws URISyntaxException {
if (uriConnection == null) return null;
return new MoneroRpcConnection(
validateUri(uriConnection.getUri()),
nullIfEmpty(uriConnection.getUsername()),
nullIfEmpty(uriConnection.getPassword()))
.setPriority(uriConnection.getPriority());
}
private static String validateUri(String uri) throws URISyntaxException {
if (uri.isEmpty()) {
throw new IllegalArgumentException("URI is required");
}
// Create new URI for validation, internally String is used again
return new URI(uri).toString();
}
private static String nullIfEmpty(String value) {
if (value == null || value.isEmpty()) {
return null;
}
return value;
}
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(getAddConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getRemoveConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStartCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStopCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
}}
)));
}
}

View file

@ -59,7 +59,8 @@ public class GrpcServer {
GrpcGetTradeStatisticsService tradeStatisticsService,
GrpcTradesService tradesService,
GrpcWalletsService walletsService,
GrpcNotificationsService notificationsService) {
GrpcNotificationsService notificationsService,
GrpcMoneroConnectionsService moneroConnectionsService) {
this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor())
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
@ -73,6 +74,7 @@ public class GrpcServer {
.addService(interceptForward(versionService, versionService.interceptors()))
.addService(interceptForward(walletsService, walletsService.interceptors()))
.addService(interceptForward(notificationsService, notificationsService.interceptors()))
.addService(interceptForward(moneroConnectionsService, moneroConnectionsService.interceptors()))
.intercept(passwordAuthInterceptor)
.build();
coreContext.setApiUser(true);

View file

@ -705,6 +705,120 @@ message AddressBalanceInfo {
bool is_address_unused = 4;
}
///////////////////////////////////////////////////////////////////////////////////////////
// MoneroConnections
///////////////////////////////////////////////////////////////////////////////////////////
service MoneroConnections {
rpc AddConnection (AddConnectionRequest) returns (AddConnectionReply) {
}
rpc RemoveConnection(RemoveConnectionRequest) returns (RemoveConnectionReply) {
}
rpc GetConnection(GetConnectionRequest) returns (GetConnectionReply) {
}
rpc GetConnections(GetConnectionsRequest) returns (GetConnectionsReply) {
}
rpc SetConnection(SetConnectionRequest) returns (SetConnectionReply) {
}
rpc CheckConnection(CheckConnectionRequest) returns (CheckConnectionReply) {
}
rpc CheckConnections(CheckConnectionsRequest) returns (CheckConnectionsReply) {
}
rpc StartCheckingConnections(StartCheckingConnectionsRequest) returns (StartCheckingConnectionsReply) {
}
rpc StopCheckingConnections(StopCheckingConnectionsRequest) returns (StopCheckingConnectionsReply) {
}
rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) {
}
rpc SetAutoSwitch(SetAutoSwitchRequest) returns (SetAutoSwitchReply) {
}
}
message UriConnection {
enum OnlineStatus {
UNKNOWN = 0;
ONLINE = 1;
OFFLINE = 2;
}
enum AuthenticationStatus {
NO_AUTHENTICATION = 0;
AUTHENTICATED = 1;
NOT_AUTHENTICATED = 2;
}
string uri = 1;
string username = 2; // request only
string password = 3; // request only
int32 priority = 4;
OnlineStatus online_status = 5; // reply only
AuthenticationStatus authentication_status = 6; // reply only
}
message AddConnectionRequest {
UriConnection connection = 1;
}
message AddConnectionReply {}
message RemoveConnectionRequest {
string uri = 1;
}
message RemoveConnectionReply {}
message GetConnectionRequest {}
message GetConnectionReply {
UriConnection connection = 1;
}
message GetConnectionsRequest {}
message GetConnectionsReply {
repeated UriConnection connections = 1;
}
message SetConnectionRequest {
string uri = 1;
UriConnection connection = 2;
}
message SetConnectionReply {}
message CheckConnectionRequest {}
message CheckConnectionReply {
UriConnection connection = 1;
}
message CheckConnectionsRequest {}
message CheckConnectionsReply {
repeated UriConnection connections = 1;
}
message StartCheckingConnectionsRequest {
int32 refresh_period = 1; // milliseconds
}
message StartCheckingConnectionsReply {}
message StopCheckingConnectionsRequest {}
message StopCheckingConnectionsReply {}
message GetBestAvailableConnectionRequest {}
message GetBestAvailableConnectionReply {
UriConnection connection = 1;
}
message SetAutoSwitchRequest {
bool auto_switch = 1;
}
message SetAutoSwitchReply {}
///////////////////////////////////////////////////////////////////////////////////////////
// Version
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -1321,6 +1321,7 @@ message PersistableEnvelope {
XmrAddressEntryList xmr_address_entry_list = 1001;
SignedOfferList signed_offer_list = 1002;
EncryptedConnectionList encrypted_connection_list = 1003;
}
}
@ -1690,6 +1691,26 @@ message TradingPeer {
string deposit_tx_key = 1010;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Connections
///////////////////////////////////////////////////////////////////////////////////////////
message EncryptedConnection {
string uri = 1;
string username = 2;
bytes encrypted_password = 3;
bytes encryption_salt = 4;
int32 priority = 5;
}
message EncryptedConnectionList {
bytes salt = 1;
repeated EncryptedConnection items = 2;
string current_connection_uri = 3;
int64 refresh_period = 4; // negative: no automated refresh is activated, zero: automated refresh with default period, positive: automated refresh with configured period (value)
bool auto_switch = 5;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Dispute
///////////////////////////////////////////////////////////////////////////////////////////