mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
Add monero connections manager
This commit is contained in:
parent
ed13047bf6
commit
a3586fdef8
18 changed files with 1230 additions and 33 deletions
50
core/src/main/java/bisq/core/api/CoreAccountService.java
Normal file
50
core/src/main/java/bisq/core/api/CoreAccountService.java
Normal 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);
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
Loading…
Reference in a new issue