Add API functions to initialize Haveno account (#216)
Co-authored-by: woodser@protonmail.com
This commit is contained in:
parent
dc4692d97a
commit
e3b9a9962b
29
Makefile
29
Makefile
@ -51,17 +51,6 @@ arbitrator-desktop:
|
||||
--apiPassword=apitest \
|
||||
--apiPort=9998
|
||||
|
||||
arbitrator-daemon:
|
||||
# Arbitrator and mediator need to be registerd in the UI before launching the daemon.
|
||||
./haveno-daemon \
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
--useLocalhostForP2P=true \
|
||||
--useDevPrivilegeKeys=true \
|
||||
--nodePort=4444 \
|
||||
--appName=haveno-XMR_STAGENET_arbitrator \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=9998
|
||||
|
||||
arbitrator-desktop2:
|
||||
# Arbitrator and mediator need to be registerd in the UI after launching it.
|
||||
./haveno-desktop \
|
||||
@ -73,6 +62,18 @@ arbitrator-desktop2:
|
||||
--apiPassword=apitest \
|
||||
--apiPort=10001
|
||||
|
||||
arbitrator-daemon:
|
||||
# Arbitrator and mediator need to be registerd in the UI before launching the daemon!
|
||||
./haveno-daemon \
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
--useLocalhostForP2P=true \
|
||||
--useDevPrivilegeKeys=true \
|
||||
--nodePort=4444 \
|
||||
--appName=haveno-XMR_STAGENET_arbitrator \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=9998 \
|
||||
--passwordRequired=false
|
||||
|
||||
alice-desktop:
|
||||
./haveno-desktop \
|
||||
--baseCurrencyNetwork=XMR_STAGENET \
|
||||
@ -93,7 +94,8 @@ alice-daemon:
|
||||
--appName=haveno-XMR_STAGENET_Alice \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=9999 \
|
||||
--walletRpcBindPort=38091
|
||||
--walletRpcBindPort=38091 \
|
||||
--passwordRequired=false
|
||||
|
||||
bob-desktop:
|
||||
./haveno-desktop \
|
||||
@ -115,7 +117,8 @@ bob-daemon:
|
||||
--appName=haveno-XMR_STAGENET_Bob \
|
||||
--apiPassword=apitest \
|
||||
--apiPort=10000 \
|
||||
--walletRpcBindPort=38092
|
||||
--walletRpcBindPort=38092 \
|
||||
--passwordRequired=false
|
||||
|
||||
monero-shared:
|
||||
./.localnet/monerod \
|
||||
|
@ -114,6 +114,7 @@ public class Config {
|
||||
public static final String BTC_MIN_TX_FEE = "btcMinTxFee";
|
||||
public static final String BTC_FEES_TS = "bitcoinFeesTs";
|
||||
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
||||
public static final String PASSWORD_REQUIRED = "passwordRequired";
|
||||
|
||||
// Default values for certain options
|
||||
public static final int UNSPECIFIED_PORT = -1;
|
||||
@ -190,6 +191,7 @@ public class Config {
|
||||
public final boolean preventPeriodicShutdownAtSeedNode;
|
||||
public final boolean republishMailboxEntries;
|
||||
public final boolean bypassMempoolValidation;
|
||||
public final boolean passwordRequired;
|
||||
|
||||
// Properties derived from options but not exposed as options themselves
|
||||
public final File torDir;
|
||||
@ -579,6 +581,13 @@ public class Config {
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
ArgumentAcceptingOptionSpec<Boolean> passwordRequiredOpt =
|
||||
parser.accepts(PASSWORD_REQUIRED,
|
||||
"Requires a password for creating a Haveno account")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(false);
|
||||
|
||||
try {
|
||||
CompositeOptionSet options = new CompositeOptionSet();
|
||||
|
||||
@ -686,6 +695,7 @@ public class Config {
|
||||
this.preventPeriodicShutdownAtSeedNode = options.valueOf(preventPeriodicShutdownAtSeedNodeOpt);
|
||||
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
|
||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||
this.passwordRequired = options.valueOf(passwordRequiredOpt);
|
||||
} catch (OptionException ex) {
|
||||
throw new ConfigException("problem parsing option '%s': %s",
|
||||
ex.options().get(0),
|
||||
|
@ -54,11 +54,13 @@ public class Encryption {
|
||||
public static final String ASYM_KEY_ALGO = "RSA";
|
||||
private static final String ASYM_CIPHER = "RSA/ECB/OAEPWithSHA-256AndMGF1PADDING";
|
||||
|
||||
private static final String SYM_KEY_ALGO = "AES";
|
||||
public static final String SYM_KEY_ALGO = "AES";
|
||||
private static final String SYM_CIPHER = "AES";
|
||||
|
||||
private static final String HMAC = "HmacSHA256";
|
||||
|
||||
public static final String HMAC_ERROR_MSG = "Hmac does not match.";
|
||||
|
||||
public static KeyPair generateKeyPair() {
|
||||
long ts = System.currentTimeMillis();
|
||||
try {
|
||||
@ -101,11 +103,6 @@ public class Encryption {
|
||||
return new SecretKeySpec(secretKeyBytes, 0, secretKeyBytes.length, SYM_KEY_ALGO);
|
||||
}
|
||||
|
||||
public static byte[] getSecretKeyBytes(SecretKey secretKey) {
|
||||
return secretKey.getEncoded();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Hmac
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -179,7 +176,7 @@ public class Encryption {
|
||||
if (verifyHmac(Hex.decode(payloadAsHex), Hex.decode(hmacAsHex), secretKey)) {
|
||||
return Hex.decode(payloadAsHex);
|
||||
} else {
|
||||
throw new CryptoException("Hmac does not match.");
|
||||
throw new CryptoException(HMAC_ERROR_MSG);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.common.crypto;
|
||||
|
||||
public class IncorrectPasswordException extends Exception {
|
||||
public IncorrectPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -26,27 +26,93 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public final class KeyRing {
|
||||
private final KeyPair signatureKeyPair;
|
||||
private final KeyPair encryptionKeyPair;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
|
||||
private final KeyStorage keyStorage;
|
||||
|
||||
private KeyPair signatureKeyPair;
|
||||
private KeyPair encryptionKeyPair;
|
||||
private PubKeyRing pubKeyRing;
|
||||
|
||||
/**
|
||||
* Creates the KeyRing. Unlocks if not encrypted. Does not generate keys.
|
||||
*
|
||||
* @param keyStorage Persisted storage
|
||||
*/
|
||||
@Inject
|
||||
public KeyRing(KeyStorage keyStorage) {
|
||||
this(keyStorage, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates KeyRing with a password. Attempts to generate keys if they don't exist.
|
||||
*
|
||||
* @param keyStorage Persisted storage
|
||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||
* @param generateKeys Generate new keys with password if not created yet.
|
||||
*/
|
||||
public KeyRing(KeyStorage keyStorage, String password, boolean generateKeys) {
|
||||
this.keyStorage = keyStorage;
|
||||
try {
|
||||
unlockKeys(password, generateKeys);
|
||||
} catch(IncorrectPasswordException ex) {
|
||||
// no action
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
boolean isUnlocked = this.signatureKeyPair != null
|
||||
&& this.encryptionKeyPair != null
|
||||
&& this.pubKeyRing != null;
|
||||
return isUnlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the keyring disabling access to the keys until unlock is called.
|
||||
* If the keys are never persisted then the keys are lost and will be regenerated.
|
||||
*/
|
||||
public void lockKeys() {
|
||||
signatureKeyPair = null;
|
||||
encryptionKeyPair = null;
|
||||
pubKeyRing = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the keyring with a given password if required. If the keyring is already
|
||||
* unlocked, do nothing.
|
||||
*
|
||||
* @param password Decrypts the or encrypts newly generated keys with the given password.
|
||||
* @return Whether KeyRing is unlocked
|
||||
*/
|
||||
public boolean unlockKeys(@Nullable String password, boolean generateKeys) throws IncorrectPasswordException {
|
||||
if (isUnlocked()) return true;
|
||||
if (keyStorage.allKeyFilesExist()) {
|
||||
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE);
|
||||
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION);
|
||||
} else {
|
||||
// First time we create key pairs
|
||||
signatureKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_SIGNATURE, password);
|
||||
encryptionKeyPair = keyStorage.loadKeyPair(KeyStorage.KeyEntry.MSG_ENCRYPTION, password);
|
||||
if (signatureKeyPair != null && encryptionKeyPair != null) pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||
} else if (generateKeys) {
|
||||
generateKeys(password);
|
||||
}
|
||||
return isUnlocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new set of keys if the current keyring is closed.
|
||||
*
|
||||
* @param password The password to unlock the keys or to generate new keys, nullable.
|
||||
*/
|
||||
public void generateKeys(String password) {
|
||||
if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
|
||||
signatureKeyPair = Sig.generateKeyPair();
|
||||
encryptionKeyPair = Encryption.generateKeyPair();
|
||||
keyStorage.saveKeyRing(this);
|
||||
}
|
||||
pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic());
|
||||
keyStorage.saveKeyRing(this, password);
|
||||
}
|
||||
|
||||
// Don't print keys for security reasons
|
||||
|
@ -20,11 +20,19 @@ package bisq.common.crypto;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.file.FileUtil;
|
||||
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@ -39,6 +47,9 @@ import java.security.spec.KeySpec;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@ -46,6 +57,7 @@ import java.io.IOException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -58,6 +70,11 @@ import static bisq.common.util.Preconditions.checkDir;
|
||||
@Singleton
|
||||
public class KeyStorage {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyStorage.class);
|
||||
private static final int SALT_LENGTH = 20;
|
||||
|
||||
private static final byte[] ENCRYPTED_FORMAT_MAGIC = "HVNENC".getBytes(StandardCharsets.UTF_8);
|
||||
private static final int ENCRYPTED_FORMAT_VERSION = 1;
|
||||
private static final int ENCRYPTED_FORMAT_LENGTH = 4*2; // version,salt
|
||||
|
||||
public enum KeyEntry {
|
||||
MSG_SIGNATURE("sig", Sig.KEY_ALGO),
|
||||
@ -104,7 +121,7 @@ public class KeyStorage {
|
||||
return new File(storageDir + "/" + keyEntry.getFileName() + ".key").exists();
|
||||
}
|
||||
|
||||
public KeyPair loadKeyPair(KeyEntry keyEntry) {
|
||||
public KeyPair loadKeyPair(KeyEntry keyEntry, String password) throws IncorrectPasswordException {
|
||||
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
|
||||
// long now = System.currentTimeMillis();
|
||||
try {
|
||||
@ -118,9 +135,53 @@ public class KeyStorage {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
fis.read(encodedPrivateKey);
|
||||
|
||||
// Read magic bytes
|
||||
byte[] magicBytes = Arrays.copyOfRange(encodedPrivateKey, 0, ENCRYPTED_FORMAT_MAGIC.length);
|
||||
boolean isEncryptedPassword = Arrays.compare(magicBytes, ENCRYPTED_FORMAT_MAGIC) == 0;
|
||||
if (isEncryptedPassword && password == null) {
|
||||
throw new IncorrectPasswordException("Cannot load encrypted keys, user must open account with password " + filePrivateKey);
|
||||
} else if (password != null && !isEncryptedPassword) {
|
||||
log.warn("Password not needed for unencrypted key " + filePrivateKey);
|
||||
}
|
||||
|
||||
// Decrypt using password
|
||||
if (password != null) {
|
||||
int position = ENCRYPTED_FORMAT_MAGIC.length;
|
||||
|
||||
// Read remaining header
|
||||
ByteBuffer buf = ByteBuffer.wrap(encodedPrivateKey, position, ENCRYPTED_FORMAT_LENGTH);
|
||||
position += ENCRYPTED_FORMAT_LENGTH;
|
||||
int version = buf.getInt();
|
||||
if (version != 1) throw new RuntimeException("Unable to parse encrypted keys");
|
||||
int saltLength = buf.getInt();
|
||||
|
||||
// Read salt
|
||||
byte[] salt = Arrays.copyOfRange(encodedPrivateKey, position, position + saltLength);
|
||||
position += saltLength;
|
||||
|
||||
// Payload key derived from password
|
||||
KeyCrypterScrypt crypter = ScryptUtil.getKeyCrypterScrypt(salt);
|
||||
KeyParameter pwKey = ScryptUtil.deriveKeyWithScrypt(crypter, password);
|
||||
SecretKey secretKey = new SecretKeySpec(pwKey.getKey(), Encryption.SYM_KEY_ALGO);
|
||||
byte[] encryptedPayload = Arrays.copyOfRange(encodedPrivateKey, position, encodedPrivateKey.length);
|
||||
|
||||
// Decrypt key, handling exceptions caused by an incorrect password key
|
||||
try {
|
||||
encodedPrivateKey = Encryption.decryptPayloadWithHmac(encryptedPayload, secretKey);
|
||||
} catch (CryptoException ce) {
|
||||
// Most of the time (probably of slightly less than 255/256, around 99.61%) a bad password
|
||||
// will result in BadPaddingException before HMAC check.
|
||||
// See https://stackoverflow.com/questions/8049872/given-final-block-not-properly-padded
|
||||
if (ce.getCause() instanceof BadPaddingException || ce.getMessage() == Encryption.HMAC_ERROR_MSG)
|
||||
throw new IncorrectPasswordException("Incorrect password");
|
||||
else
|
||||
throw ce;
|
||||
}
|
||||
}
|
||||
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
|
||||
privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
} catch (InvalidKeySpecException | IOException e) {
|
||||
} catch (InvalidKeySpecException | IOException | CryptoException e) {
|
||||
log.error("Could not load key " + keyEntry.toString(), e.getMessage());
|
||||
throw new RuntimeException("Could not load key " + keyEntry.toString(), e);
|
||||
}
|
||||
@ -150,20 +211,44 @@ public class KeyStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public void saveKeyRing(KeyRing keyRing) {
|
||||
savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName());
|
||||
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName());
|
||||
public void saveKeyRing(KeyRing keyRing, String password) {
|
||||
savePrivateKey(keyRing.getSignatureKeyPair().getPrivate(), KeyEntry.MSG_SIGNATURE.getFileName(), password);
|
||||
savePrivateKey(keyRing.getEncryptionKeyPair().getPrivate(), KeyEntry.MSG_ENCRYPTION.getFileName(), password);
|
||||
}
|
||||
|
||||
private void savePrivateKey(PrivateKey privateKey, String name) {
|
||||
private void savePrivateKey(PrivateKey privateKey, String name, String password) {
|
||||
if (!storageDir.exists())
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
storageDir.mkdir();
|
||||
storageDir.mkdirs();
|
||||
|
||||
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
|
||||
try (FileOutputStream fos = new FileOutputStream(storageDir + "/" + name + ".key")) {
|
||||
fos.write(pkcs8EncodedKeySpec.getEncoded());
|
||||
} catch (IOException e) {
|
||||
byte[] keyBytes = pkcs8EncodedKeySpec.getEncoded();
|
||||
// Encrypt
|
||||
if (password != null) {
|
||||
// Magic
|
||||
fos.write(ENCRYPTED_FORMAT_MAGIC);
|
||||
|
||||
// Version, salt length
|
||||
ByteBuffer header = ByteBuffer.allocate(ENCRYPTED_FORMAT_LENGTH);
|
||||
header.putInt(ENCRYPTED_FORMAT_VERSION);
|
||||
header.putInt(SALT_LENGTH);
|
||||
fos.write(header.array());
|
||||
|
||||
// Salt value
|
||||
byte[] salt = CryptoUtils.getRandomBytes(SALT_LENGTH);
|
||||
fos.write(salt);
|
||||
|
||||
// Generate secret from password key and salt
|
||||
KeyCrypterScrypt crypter = ScryptUtil.getKeyCrypterScrypt(salt);
|
||||
KeyParameter pwKey = ScryptUtil.deriveKeyWithScrypt(crypter, password);
|
||||
SecretKey secretKey = new SecretKeySpec(pwKey.getKey(), Encryption.SYM_KEY_ALGO);
|
||||
|
||||
// Encrypt payload
|
||||
keyBytes = Encryption.encryptPayloadWithHmac(keyBytes, secretKey);
|
||||
}
|
||||
fos.write(keyBytes);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not save key " + name, e);
|
||||
throw new RuntimeException("Could not save key " + name, e);
|
||||
}
|
||||
|
@ -3,17 +3,22 @@ package bisq.common.crypto;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
/**
|
||||
* Allows User's static PubKeyRing to be injected into constructors without having to
|
||||
* open the account yet. Once its opened, PubKeyRingProvider will return non-null PubKeyRing.
|
||||
* Originally used via bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class);
|
||||
*/
|
||||
public class PubKeyRingProvider implements Provider<PubKeyRing> {
|
||||
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final KeyRing keyRing;
|
||||
|
||||
@Inject
|
||||
public PubKeyRingProvider(KeyRing keyRing) {
|
||||
pubKeyRing = keyRing.getPubKeyRing();
|
||||
this.keyRing = keyRing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing get() {
|
||||
return pubKeyRing;
|
||||
return keyRing.getPubKeyRing();
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.crypto;
|
||||
package bisq.common.crypto;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.util.Utilities;
|
||||
@ -52,14 +52,25 @@ public class ScryptUtil {
|
||||
return new KeyCrypterScrypt(scryptParameters);
|
||||
}
|
||||
|
||||
public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) {
|
||||
Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> {
|
||||
public static KeyParameter deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password) {
|
||||
try {
|
||||
log.debug("Doing key derivation");
|
||||
long start = System.currentTimeMillis();
|
||||
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.debug("Key derivation took {} msec", duration);
|
||||
return aesKey;
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
log.error("Key derivation failed. " + t.getMessage());
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
public static void deriveKeyWithScrypt(KeyCrypterScrypt keyCrypterScrypt, String password, DeriveKeyResultHandler resultHandler) {
|
||||
Utilities.getThreadPoolExecutor("ScryptUtil:deriveKeyWithScrypt-%d", 1, 2, 5L).submit(() -> {
|
||||
try {
|
||||
KeyParameter aesKey = deriveKeyWithScrypt(keyCrypterScrypt, password);
|
||||
UserThread.execute(() -> {
|
||||
try {
|
||||
resultHandler.handleResult(aesKey);
|
@ -114,7 +114,6 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// We don't know from which thread we are called so we map to user thread
|
||||
UserThread.execute(() -> {
|
||||
if (doShutdown) {
|
||||
@ -382,6 +381,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initCalled.get()) {
|
||||
log.warn("requestPersistence() called before init. Ignoring request");
|
||||
return;
|
||||
}
|
||||
|
||||
persistenceRequested = true;
|
||||
|
||||
// If we have not initialized yet we postpone the start of the timer and call maybeStartTimerForPersistence at
|
||||
|
128
common/src/main/java/bisq/common/util/ZipUtils.java
Normal file
128
common/src/main/java/bisq/common/util/ZipUtils.java
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.common.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ZipUtils {
|
||||
|
||||
/**
|
||||
* Zips directory into the output stream. Empty directories are not included.
|
||||
*
|
||||
* @param dir The directory to create the zip from.
|
||||
* @param out The stream to write to.
|
||||
*/
|
||||
public static void zipDirToStream(File dir, OutputStream out, int bufferSize) throws Exception {
|
||||
|
||||
// Get all files in directory and subdirectories.
|
||||
ArrayList<String> fileList = new ArrayList<>();
|
||||
getFilesRecursive(dir, fileList);
|
||||
try (ZipOutputStream zos = new ZipOutputStream(out)) {
|
||||
for (String filePath : fileList) {
|
||||
log.info("Compressing: " + filePath);
|
||||
|
||||
// Creates a zip entry.
|
||||
String name = filePath.substring(dir.getAbsolutePath().length() + 1);
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(name);
|
||||
zos.putNextEntry(zipEntry);
|
||||
|
||||
// Read file content and write to zip output stream.
|
||||
try (FileInputStream fis = new FileInputStream(filePath)) {
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int length;
|
||||
while ((length = fis.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
// Close the zip entry.
|
||||
zos.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files list from the directory recursive to the subdirectory.
|
||||
*/
|
||||
public static void getFilesRecursive(File directory, List<String> fileList) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null && files.length > 0) {
|
||||
for (File file : files) {
|
||||
if (file.isFile()) {
|
||||
fileList.add(file.getAbsolutePath());
|
||||
} else {
|
||||
getFilesRecursive(file, fileList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips the zipStream into the specified directory, overwriting any files.
|
||||
* Existing files are preserved.
|
||||
*
|
||||
* @param dir The directory to write to.
|
||||
* @param inputStream The raw stream assumed to be in zip format.
|
||||
* @param bufferSize The buffer used to read from efficiently.
|
||||
*/
|
||||
public static void unzipToDir(File dir, InputStream inputStream, int bufferSize) throws Exception {
|
||||
try (ZipInputStream zipStream = new ZipInputStream(inputStream)) {
|
||||
ZipEntry entry;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int count;
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
File file = new File(dir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
file.mkdirs();
|
||||
} else {
|
||||
|
||||
// Make sure folder exists.
|
||||
file.getParentFile().mkdirs();
|
||||
|
||||
log.info("Unzipped file: " + file.getAbsolutePath());
|
||||
// Don't overwrite the current logs
|
||||
if ("bisq.log".equals(file.getName())) {
|
||||
file = new File(file.getParent() + "/" + "bisq.backup.log");
|
||||
log.info("Unzipped logfile to backup path: " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
try (FileOutputStream fileOutput = new FileOutputStream(file)) {
|
||||
while ((count = zipStream.read(buffer)) != -1) {
|
||||
fileOutput.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
14
core/src/main/java/bisq/core/api/AccountServiceListener.java
Normal file
14
core/src/main/java/bisq/core/api/AccountServiceListener.java
Normal file
@ -0,0 +1,14 @@
|
||||
package bisq.core.api;
|
||||
|
||||
/**
|
||||
* Default account listener (takes no action).
|
||||
*/
|
||||
public class AccountServiceListener {
|
||||
public void onAppInitialized() {}
|
||||
public void onAccountCreated() {}
|
||||
public void onAccountOpened() {}
|
||||
public void onAccountClosed() {}
|
||||
public void onAccountRestored(Runnable onShutDown) {}
|
||||
public void onAccountDeleted(Runnable onShutDown) {}
|
||||
public void onPasswordChanged(String oldPassword, String newPassword) {}
|
||||
}
|
@ -1,50 +1,163 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.api;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.KeyStorage;
|
||||
import bisq.common.file.FileUtil;
|
||||
import bisq.common.persistence.PersistenceManager;
|
||||
import bisq.common.util.ZipUtils;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @deprecated Should be replaced by actual implementation once it is available
|
||||
* Manages the account state. A created account must have a password which encrypts
|
||||
* all persistence in the PersistenceManager. As a result, opening the account requires
|
||||
* a correct password to be passed in to deserialize the account properties that are
|
||||
* persisted. It is possible to persist the objects without a password (legacy).
|
||||
*
|
||||
* Backup and restore flushes the persistence objects in the app folder and sends or
|
||||
* restores a zip stream.
|
||||
*/
|
||||
@Singleton
|
||||
@Deprecated
|
||||
@Slf4j
|
||||
public class CoreAccountService {
|
||||
|
||||
private final Config config;
|
||||
private final KeyStorage keyStorage;
|
||||
private final KeyRing keyRing;
|
||||
|
||||
private static final String DEFAULT_PASSWORD = "abctesting123";
|
||||
@Getter
|
||||
private String password;
|
||||
private List<AccountServiceListener> listeners = new ArrayList<AccountServiceListener>();
|
||||
|
||||
private String password = DEFAULT_PASSWORD;
|
||||
|
||||
private final List<PasswordChangeListener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
@Inject
|
||||
public CoreAccountService(Config config,
|
||||
KeyStorage keyStorage,
|
||||
KeyRing keyRing) {
|
||||
this.config = config;
|
||||
this.keyStorage = keyStorage;
|
||||
this.keyRing = keyRing;
|
||||
}
|
||||
|
||||
public void setPassword(String newPassword) {
|
||||
String oldPassword = password;
|
||||
password = newPassword;
|
||||
notifyListenerAboutPasswordChange(oldPassword, newPassword);
|
||||
}
|
||||
|
||||
public void addPasswordChangeListener(PasswordChangeListener listener) {
|
||||
Objects.requireNonNull(listener, "listener");
|
||||
public void addListener(AccountServiceListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyListenerAboutPasswordChange(String oldPassword, String newPassword) {
|
||||
for (PasswordChangeListener listener : listeners) {
|
||||
listener.onPasswordChange(oldPassword, newPassword);
|
||||
public boolean removeListener(AccountServiceListener listener) {
|
||||
return listeners.remove(listener);
|
||||
}
|
||||
|
||||
public boolean accountExists() {
|
||||
return keyStorage.allKeyFilesExist(); // public and private key pair indicate the existence of the account
|
||||
}
|
||||
|
||||
public boolean isAccountOpen() {
|
||||
return keyRing.isUnlocked() && accountExists();
|
||||
}
|
||||
|
||||
public void checkAccountOpen() {
|
||||
checkState(isAccountOpen(), "Account not open");
|
||||
}
|
||||
|
||||
public void createAccount(String password) {
|
||||
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
|
||||
keyRing.generateKeys(password);
|
||||
this.password = password;
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountCreated();
|
||||
}
|
||||
|
||||
public void openAccount(String password) throws IncorrectPasswordException {
|
||||
if (!accountExists()) throw new IllegalStateException("Cannot open account if account does not exist");
|
||||
if (keyRing.unlockKeys(password, false)) {
|
||||
this.password = password;
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountOpened();
|
||||
} else {
|
||||
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
|
||||
}
|
||||
}
|
||||
|
||||
public interface PasswordChangeListener {
|
||||
public void changePassword(String password) {
|
||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account");
|
||||
keyStorage.saveKeyRing(keyRing, password);
|
||||
String oldPassword = this.password;
|
||||
this.password = password;
|
||||
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, password);
|
||||
}
|
||||
|
||||
void onPasswordChange(String oldPassword, String newPassword);
|
||||
public void closeAccount() {
|
||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
|
||||
keyRing.lockKeys(); // closed account means the keys are locked
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
|
||||
}
|
||||
|
||||
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
|
||||
if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account");
|
||||
|
||||
// flush all known persistence objects to disk
|
||||
PersistenceManager.flushAllDataToDiskAtBackup(() -> {
|
||||
try {
|
||||
File dataDir = new File(config.appDataDir.getPath());
|
||||
PipedInputStream in = new PipedInputStream(bufferSize); // pipe the serialized account object to stream which will be read by the consumer
|
||||
PipedOutputStream out = new PipedOutputStream(in);
|
||||
log.info("Zipping directory " + dataDir);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
ZipUtils.zipDirToStream(dataDir, out, bufferSize);
|
||||
} catch (Exception ex) {
|
||||
error.accept(ex);
|
||||
}
|
||||
}).start();
|
||||
consume.accept(in);
|
||||
} catch (java.io.IOException err) {
|
||||
error.accept(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void restoreAccount(InputStream inputStream, int bufferSize, Runnable onShutdown) throws Exception {
|
||||
if (accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
|
||||
File dataDir = new File(config.appDataDir.getPath());
|
||||
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
|
||||
}
|
||||
|
||||
public void deleteAccount(Runnable onShutdown) {
|
||||
try {
|
||||
keyRing.lockKeys();
|
||||
for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown);
|
||||
File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown
|
||||
FileUtil.deleteDirectory(dataDir, null, false);
|
||||
} catch (Exception err) {
|
||||
throw new RuntimeException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import bisq.core.api.model.AddressBalanceInfo;
|
||||
import bisq.core.api.model.BalancesInfo;
|
||||
import bisq.core.api.model.MarketPriceInfo;
|
||||
import bisq.core.api.model.TxFeeRateInfo;
|
||||
import bisq.core.app.AppStartupState;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
@ -33,6 +34,7 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
@ -46,6 +48,8 @@ import javax.inject.Singleton;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -58,7 +62,6 @@ import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
@ -73,6 +76,8 @@ public class CoreApi {
|
||||
|
||||
@Getter
|
||||
private final Config config;
|
||||
private final AppStartupState appStartupState;
|
||||
private final CoreAccountService coreAccountService;
|
||||
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
||||
private final CoreHelpService coreHelpService;
|
||||
private final CoreOffersService coreOffersService;
|
||||
@ -86,6 +91,8 @@ public class CoreApi {
|
||||
|
||||
@Inject
|
||||
public CoreApi(Config config,
|
||||
AppStartupState appStartupState,
|
||||
CoreAccountService coreAccountService,
|
||||
CoreDisputeAgentsService coreDisputeAgentsService,
|
||||
CoreHelpService coreHelpService,
|
||||
CoreOffersService coreOffersService,
|
||||
@ -97,6 +104,8 @@ public class CoreApi {
|
||||
CoreNotificationService notificationService,
|
||||
CoreMoneroConnectionsService coreMoneroConnectionsService) {
|
||||
this.config = config;
|
||||
this.appStartupState = appStartupState;
|
||||
this.coreAccountService = coreAccountService;
|
||||
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
||||
this.coreHelpService = coreHelpService;
|
||||
this.coreOffersService = coreOffersService;
|
||||
@ -115,11 +124,197 @@ public class CoreApi {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dispute Agents
|
||||
// Help
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
|
||||
coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey);
|
||||
public String getMethodHelp(String methodName) {
|
||||
return coreHelpService.getMethodHelp(methodName);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Account Service
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public boolean accountExists() {
|
||||
return coreAccountService.accountExists();
|
||||
}
|
||||
|
||||
public boolean isAccountOpen() {
|
||||
return coreAccountService.isAccountOpen();
|
||||
}
|
||||
|
||||
public void createAccount(String password) {
|
||||
coreAccountService.createAccount(password);
|
||||
}
|
||||
|
||||
public void openAccount(String password) throws IncorrectPasswordException {
|
||||
coreAccountService.openAccount(password);
|
||||
}
|
||||
|
||||
public boolean isAppInitialized() {
|
||||
return appStartupState.isApplicationFullyInitialized();
|
||||
}
|
||||
|
||||
public void changePassword(String password) {
|
||||
coreAccountService.changePassword(password);
|
||||
}
|
||||
|
||||
public void closeAccount() {
|
||||
coreAccountService.closeAccount();
|
||||
}
|
||||
|
||||
public void deleteAccount(Runnable onShutdown) {
|
||||
coreAccountService.deleteAccount(onShutdown);
|
||||
}
|
||||
|
||||
public void backupAccount(int bufferSize, Consumer<InputStream> consume, Consumer<Exception> error) {
|
||||
coreAccountService.backupAccount(bufferSize, consume, error);
|
||||
}
|
||||
|
||||
public void restoreAccount(InputStream zipStream, int bufferSize, Runnable onShutdown) throws Exception {
|
||||
coreAccountService.restoreAccount(zipStream, bufferSize, onShutdown);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Wallets
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public BalancesInfo getBalances(String currencyCode) {
|
||||
return walletsService.getBalances(currencyCode);
|
||||
}
|
||||
|
||||
public String getNewDepositSubaddress() {
|
||||
return walletsService.getNewDepositSubaddress();
|
||||
}
|
||||
|
||||
public List<MoneroTxWallet> getXmrTxs() {
|
||||
return walletsService.getXmrTxs();
|
||||
}
|
||||
|
||||
public MoneroTxWallet createXmrTx(List<MoneroDestination> destinations) {
|
||||
return walletsService.createXmrTx(destinations);
|
||||
}
|
||||
|
||||
public String relayXmrTx(String metadata) {
|
||||
return walletsService.relayXmrTx(metadata);
|
||||
}
|
||||
|
||||
public long getAddressBalance(String addressString) {
|
||||
return walletsService.getAddressBalance(addressString);
|
||||
}
|
||||
|
||||
public AddressBalanceInfo getAddressBalanceInfo(String addressString) {
|
||||
return walletsService.getAddressBalanceInfo(addressString);
|
||||
}
|
||||
|
||||
public List<AddressBalanceInfo> getFundingAddresses() {
|
||||
return walletsService.getFundingAddresses();
|
||||
}
|
||||
|
||||
public void sendBtc(String address,
|
||||
String amount,
|
||||
String txFeeRate,
|
||||
String memo,
|
||||
FutureCallback<Transaction> callback) {
|
||||
walletsService.sendBtc(address, amount, txFeeRate, memo, callback);
|
||||
}
|
||||
|
||||
|
||||
public void getTxFeeRate(ResultHandler resultHandler) {
|
||||
walletsService.getTxFeeRate(resultHandler);
|
||||
}
|
||||
|
||||
public void setTxFeeRatePreference(long txFeeRate,
|
||||
ResultHandler resultHandler) {
|
||||
walletsService.setTxFeeRatePreference(txFeeRate, resultHandler);
|
||||
}
|
||||
|
||||
public void unsetTxFeeRatePreference(ResultHandler resultHandler) {
|
||||
walletsService.unsetTxFeeRatePreference(resultHandler);
|
||||
}
|
||||
|
||||
public TxFeeRateInfo getMostRecentTxFeeRateInfo() {
|
||||
return walletsService.getMostRecentTxFeeRateInfo();
|
||||
}
|
||||
|
||||
public Transaction getTransaction(String txId) {
|
||||
return walletsService.getTransaction(txId);
|
||||
}
|
||||
|
||||
public void setWalletPassword(String password, String newPassword) {
|
||||
walletsService.setWalletPassword(password, newPassword);
|
||||
}
|
||||
|
||||
public void lockWallet() {
|
||||
walletsService.lockWallet();
|
||||
}
|
||||
|
||||
public void unlockWallet(String password, long timeout) {
|
||||
walletsService.unlockWallet(password, timeout);
|
||||
}
|
||||
|
||||
public void removeWalletPassword(String password) {
|
||||
walletsService.removeWalletPassword(password);
|
||||
}
|
||||
|
||||
public List<TradeStatistics3> getTradeStatistics() {
|
||||
return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
|
||||
}
|
||||
|
||||
public int getNumConfirmationsForMostRecentTransaction(String addressString) {
|
||||
return walletsService.getNumConfirmationsForMostRecentTransaction(addressString);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -139,11 +334,11 @@ public class CoreApi {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Help
|
||||
// Dispute Agents
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public String getMethodHelp(String methodName) {
|
||||
return coreHelpService.getMethodHelp(methodName);
|
||||
public void registerDisputeAgent(String disputeAgentType, String registrationKey) {
|
||||
coreDisputeAgentsService.registerDisputeAgent(disputeAgentType, registrationKey);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -312,146 +507,4 @@ public class CoreApi {
|
||||
public String getTradeRole(String tradeId) {
|
||||
return coreTradesService.getTradeRole(tradeId);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Wallets
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public BalancesInfo getBalances(String currencyCode) {
|
||||
return walletsService.getBalances(currencyCode);
|
||||
}
|
||||
|
||||
public String getNewDepositSubaddress() {
|
||||
return walletsService.getNewDepositSubaddress();
|
||||
}
|
||||
|
||||
public List<MoneroTxWallet> getXmrTxs() {
|
||||
return walletsService.getXmrTxs();
|
||||
}
|
||||
|
||||
public MoneroTxWallet createXmrTx(List<MoneroDestination> destinations) {
|
||||
return walletsService.createXmrTx(destinations);
|
||||
}
|
||||
|
||||
public String relayXmrTx(String metadata) {
|
||||
return walletsService.relayXmrTx(metadata);
|
||||
}
|
||||
|
||||
public long getAddressBalance(String addressString) {
|
||||
return walletsService.getAddressBalance(addressString);
|
||||
}
|
||||
|
||||
public AddressBalanceInfo getAddressBalanceInfo(String addressString) {
|
||||
return walletsService.getAddressBalanceInfo(addressString);
|
||||
}
|
||||
|
||||
public List<AddressBalanceInfo> getFundingAddresses() {
|
||||
return walletsService.getFundingAddresses();
|
||||
}
|
||||
|
||||
public void sendBtc(String address,
|
||||
String amount,
|
||||
String txFeeRate,
|
||||
String memo,
|
||||
FutureCallback<Transaction> callback) {
|
||||
walletsService.sendBtc(address, amount, txFeeRate, memo, callback);
|
||||
}
|
||||
|
||||
|
||||
public void getTxFeeRate(ResultHandler resultHandler) {
|
||||
walletsService.getTxFeeRate(resultHandler);
|
||||
}
|
||||
|
||||
public void setTxFeeRatePreference(long txFeeRate,
|
||||
ResultHandler resultHandler) {
|
||||
walletsService.setTxFeeRatePreference(txFeeRate, resultHandler);
|
||||
}
|
||||
|
||||
public void unsetTxFeeRatePreference(ResultHandler resultHandler) {
|
||||
walletsService.unsetTxFeeRatePreference(resultHandler);
|
||||
}
|
||||
|
||||
public TxFeeRateInfo getMostRecentTxFeeRateInfo() {
|
||||
return walletsService.getMostRecentTxFeeRateInfo();
|
||||
}
|
||||
|
||||
public Transaction getTransaction(String txId) {
|
||||
return walletsService.getTransaction(txId);
|
||||
}
|
||||
|
||||
public void setWalletPassword(String password, String newPassword) {
|
||||
walletsService.setWalletPassword(password, newPassword);
|
||||
}
|
||||
|
||||
public void lockWallet() {
|
||||
walletsService.lockWallet();
|
||||
}
|
||||
|
||||
public void unlockWallet(String password, long timeout) {
|
||||
walletsService.unlockWallet(password, timeout);
|
||||
}
|
||||
|
||||
public void removeWalletPassword(String password) {
|
||||
walletsService.removeWalletPassword(password);
|
||||
}
|
||||
|
||||
public List<TradeStatistics3> getTradeStatistics() {
|
||||
return new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,38 @@
|
||||
package bisq.core.api;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.core.btc.model.EncryptedConnectionList;
|
||||
import bisq.core.btc.setup.DownloadListener;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroConnectionManager;
|
||||
import monero.common.MoneroConnectionManagerListener;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.MoneroDaemonRpc;
|
||||
import monero.daemon.model.MoneroPeer;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class CoreMoneroConnectionsService {
|
||||
public final 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
|
||||
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
||||
private static final long DAEMON_REFRESH_PERIOD_MS = 15000L; // check connection periodically in ms
|
||||
private static final long DAEMON_INFO_POLL_PERIOD_MS = 20000L; // collect daemon info periodically in ms
|
||||
|
||||
// TODO (woodser): support each network type, move to config, remove localhost authentication
|
||||
private static final List<MoneroRpcConnection> DEFAULT_CONNECTIONS = Arrays.asList(
|
||||
@ -24,21 +41,222 @@ public class CoreMoneroConnectionsService {
|
||||
);
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final CoreAccountService accountService;
|
||||
private final MoneroConnectionManager connectionManager;
|
||||
private final EncryptedConnectionList connectionList;
|
||||
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
|
||||
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
|
||||
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
||||
private final DownloadListener downloadListener = new DownloadListener();
|
||||
|
||||
private MoneroDaemon daemon;
|
||||
private boolean isInitialized = false;
|
||||
|
||||
@Inject
|
||||
public CoreMoneroConnectionsService(MoneroConnectionManager connectionManager,
|
||||
public CoreMoneroConnectionsService(WalletsSetup walletsSetup,
|
||||
CoreAccountService accountService,
|
||||
MoneroConnectionManager connectionManager,
|
||||
EncryptedConnectionList connectionList) {
|
||||
this.accountService = accountService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.connectionList = connectionList;
|
||||
|
||||
// initialize after account open and basic setup
|
||||
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
|
||||
|
||||
// initialize from connections read from disk
|
||||
initialize();
|
||||
|
||||
// listen for account to be opened or password changed
|
||||
accountService.addListener(new AccountServiceListener() {
|
||||
|
||||
@Override
|
||||
public void onAccountOpened() {
|
||||
try {
|
||||
log.info(getClass() + ".onAccountOpened() called");
|
||||
initialize();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@Override
|
||||
public void onPasswordChanged(String oldPassword, String newPassword) {
|
||||
log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword);
|
||||
connectionList.changePassword(oldPassword, newPassword);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------ CONNECTION MANAGEMENT -------------------------
|
||||
|
||||
public MoneroDaemon getDaemon() {
|
||||
accountService.checkAccountOpen();
|
||||
return this.daemon;
|
||||
}
|
||||
|
||||
public void addListener(MoneroConnectionManagerListener listener) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.addListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void addConnection(MoneroRpcConnection connection) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionList.addConnection(connection);
|
||||
connectionManager.addConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeConnection(String uri) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionList.removeConnection(uri);
|
||||
connectionManager.removeConnection(uri);
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getConnection() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
return connectionManager.getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
public List<MoneroRpcConnection> getConnections() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
return connectionManager.getConnections();
|
||||
}
|
||||
}
|
||||
|
||||
public void setConnection(String connectionUri) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.setConnection(connectionUri); // listener will update connection list
|
||||
}
|
||||
}
|
||||
|
||||
public void setConnection(MoneroRpcConnection connection) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.setConnection(connection); // listener will update connection list
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroRpcConnection checkConnection() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.checkConnection();
|
||||
return getConnection();
|
||||
}
|
||||
}
|
||||
|
||||
public List<MoneroRpcConnection> checkConnections() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.checkConnections();
|
||||
return getConnections();
|
||||
}
|
||||
}
|
||||
|
||||
public void startCheckingConnection(Long refreshPeriod) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.startCheckingConnection(refreshPeriod == null ? DAEMON_REFRESH_PERIOD_MS : refreshPeriod);
|
||||
connectionList.setRefreshPeriod(refreshPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopCheckingConnection() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.stopCheckingConnection();
|
||||
connectionList.setRefreshPeriod(-1L);
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableConnection() {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
return connectionManager.getBestAvailableConnection();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAutoSwitch(boolean autoSwitch) {
|
||||
synchronized (lock) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.setAutoSwitch(autoSwitch);
|
||||
connectionList.setAutoSwitch(autoSwitch);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- APP METHODS ------------------------------
|
||||
|
||||
public boolean isChainHeightSyncedWithinTolerance() {
|
||||
if (daemon == null) return false;
|
||||
Long targetHeight = daemon.getSyncInfo().getTargetHeight();
|
||||
if (targetHeight == 0) return true; // monero-daemon-rpc sync_info's target_height returns 0 when node is fully synced
|
||||
long currentHeight = chainHeight.get();
|
||||
if (Math.abs(targetHeight - currentHeight) <= 3) {
|
||||
return true;
|
||||
}
|
||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), targetHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty numPeersProperty() {
|
||||
return numPeers;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public boolean hasSufficientPeersForBroadcast() {
|
||||
return numPeers.get() >= getMinBroadcastConnections();
|
||||
}
|
||||
|
||||
public LongProperty chainHeightProperty() {
|
||||
return chainHeight;
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty downloadPercentageProperty() {
|
||||
return downloadListener.percentageProperty();
|
||||
}
|
||||
|
||||
public int getMinBroadcastConnections() {
|
||||
return MIN_BROADCAST_CONNECTIONS;
|
||||
}
|
||||
|
||||
public boolean isDownloadComplete() {
|
||||
return downloadPercentageProperty().get() == 1d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that both the daemon and wallet have synced.
|
||||
*
|
||||
* TODO: separate daemon and wallet download/done listeners
|
||||
*/
|
||||
public void doneDownload() {
|
||||
downloadListener.doneDownload();
|
||||
}
|
||||
|
||||
// ------------------------------- HELPERS --------------------------------
|
||||
|
||||
private void initialize() {
|
||||
synchronized (lock) {
|
||||
|
||||
// reset connection manager's connections and listeners
|
||||
connectionManager.reset();
|
||||
|
||||
// load connections
|
||||
connectionList.getConnections().forEach(connectionManager::addConnection);
|
||||
log.info("Read " + connectionList.getConnections().size() + " connections from disk");
|
||||
|
||||
// add default connections
|
||||
for (MoneroRpcConnection connection : DEFAULT_CONNECTIONS) {
|
||||
@ -51,23 +269,37 @@ public class CoreMoneroConnectionsService {
|
||||
connectionManager.setConnection(DEFAULT_CONNECTIONS.get(0).getUri()); // default to localhost
|
||||
});
|
||||
|
||||
// initialize daemon
|
||||
daemon = new MoneroDaemonRpc(connectionManager.getConnection());
|
||||
updateDaemonInfo();
|
||||
|
||||
// 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 if (refreshPeriod == 0) connectionManager.startCheckingConnection(DAEMON_REFRESH_PERIOD_MS);
|
||||
else checkConnection();
|
||||
|
||||
// run once
|
||||
if (!isInitialized) {
|
||||
|
||||
// register connection change listener
|
||||
connectionManager.addListener(this::onConnectionChanged);
|
||||
|
||||
// poll daemon periodically
|
||||
startPollingDaemon();
|
||||
isInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||
synchronized (lock) {
|
||||
if (currentConnection == null) {
|
||||
daemon = null;
|
||||
connectionList.setCurrentConnectionUri(null);
|
||||
} else {
|
||||
daemon = new MoneroDaemonRpc(connectionManager.getConnection());
|
||||
connectionList.removeConnection(currentConnection.getUri());
|
||||
connectionList.addConnection(currentConnection);
|
||||
connectionList.setCurrentConnectionUri(currentConnection.getUri());
|
||||
@ -75,88 +307,26 @@ public class CoreMoneroConnectionsService {
|
||||
}
|
||||
}
|
||||
|
||||
public void addConnectionListener(MoneroConnectionManagerListener listener) {
|
||||
synchronized (lock) {
|
||||
connectionManager.addListener(listener);
|
||||
private void startPollingDaemon() {
|
||||
UserThread.runPeriodically(() -> {
|
||||
updateDaemonInfo();
|
||||
}, DAEMON_INFO_POLL_PERIOD_MS / 1000l);
|
||||
}
|
||||
|
||||
private void updateDaemonInfo() {
|
||||
try {
|
||||
if (daemon == null) throw new RuntimeException("No daemon connection");
|
||||
peers.set(getOnlinePeers());
|
||||
numPeers.set(peers.get().size());
|
||||
chainHeight.set(daemon.getHeight());
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not update daemon info: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
private List<MoneroPeer> getOnlinePeers() {
|
||||
return daemon.getPeers().stream()
|
||||
.filter(peer -> peer.isOnline())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,13 @@ public class CoreNotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendAppInitializedNotification() {
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.APP_INITIALIZED)
|
||||
.setTimestamp(System.currentTimeMillis())
|
||||
.build());
|
||||
}
|
||||
|
||||
public void sendTradeNotification(Trade trade, String title, String message) {
|
||||
sendNotification(NotificationMessage.newBuilder()
|
||||
.setType(NotificationType.TRADE_UPDATE)
|
||||
|
@ -89,6 +89,7 @@ import monero.wallet.model.MoneroTxWallet;
|
||||
class CoreWalletsService {
|
||||
|
||||
private final AppStartupState appStartupState;
|
||||
private final CoreAccountService accountService;
|
||||
private final CoreContext coreContext;
|
||||
private final Balances balances;
|
||||
private final WalletsManager walletsManager;
|
||||
@ -110,6 +111,7 @@ class CoreWalletsService {
|
||||
@Inject
|
||||
public CoreWalletsService(AppStartupState appStartupState,
|
||||
CoreContext coreContext,
|
||||
CoreAccountService accountService,
|
||||
Balances balances,
|
||||
WalletsManager walletsManager,
|
||||
WalletsSetup walletsSetup,
|
||||
@ -120,6 +122,7 @@ class CoreWalletsService {
|
||||
Preferences preferences) {
|
||||
this.appStartupState = appStartupState;
|
||||
this.coreContext = coreContext;
|
||||
this.accountService = accountService;
|
||||
this.balances = balances;
|
||||
this.walletsManager = walletsManager;
|
||||
this.walletsSetup = walletsSetup;
|
||||
@ -141,6 +144,7 @@ class CoreWalletsService {
|
||||
}
|
||||
|
||||
BalancesInfo getBalances(String currencyCode) {
|
||||
accountService.checkAccountOpen();
|
||||
verifyWalletCurrencyCodeIsValid(currencyCode);
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
@ -158,14 +162,17 @@ class CoreWalletsService {
|
||||
}
|
||||
|
||||
String getNewDepositSubaddress() {
|
||||
accountService.checkAccountOpen();
|
||||
return xmrWalletService.getWallet().createSubaddress(0).getAddress();
|
||||
}
|
||||
|
||||
List<MoneroTxWallet> getXmrTxs(){
|
||||
List<MoneroTxWallet> getXmrTxs() {
|
||||
accountService.checkAccountOpen();
|
||||
return xmrWalletService.getWallet().getTxs();
|
||||
}
|
||||
|
||||
MoneroTxWallet createXmrTx(List<MoneroDestination> destinations) {
|
||||
accountService.checkAccountOpen();
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
try {
|
||||
@ -177,6 +184,7 @@ class CoreWalletsService {
|
||||
}
|
||||
|
||||
String relayXmrTx(String metadata) {
|
||||
accountService.checkAccountOpen();
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
try {
|
||||
|
@ -12,7 +12,7 @@ import lombok.Value;
|
||||
@Builder(toBuilder = true)
|
||||
public class EncryptedConnection implements PersistablePayload {
|
||||
|
||||
String uri;
|
||||
String url;
|
||||
String username;
|
||||
byte[] encryptedPassword;
|
||||
byte[] encryptionSalt;
|
||||
@ -21,7 +21,7 @@ public class EncryptedConnection implements PersistablePayload {
|
||||
@Override
|
||||
public protobuf.EncryptedConnection toProtoMessage() {
|
||||
return protobuf.EncryptedConnection.newBuilder()
|
||||
.setUri(uri)
|
||||
.setUrl(url)
|
||||
.setUsername(username)
|
||||
.setEncryptedPassword(ByteString.copyFrom(encryptedPassword))
|
||||
.setEncryptionSalt(ByteString.copyFrom(encryptionSalt))
|
||||
@ -31,7 +31,7 @@ public class EncryptedConnection implements PersistablePayload {
|
||||
|
||||
public static EncryptedConnection fromProto(protobuf.EncryptedConnection encryptedConnection) {
|
||||
return new EncryptedConnection(
|
||||
encryptedConnection.getUri(),
|
||||
encryptedConnection.getUrl(),
|
||||
encryptedConnection.getUsername(),
|
||||
encryptedConnection.getEncryptedPassword().toByteArray(),
|
||||
encryptedConnection.getEncryptionSalt().toByteArray(),
|
||||
|
@ -17,22 +17,18 @@
|
||||
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.api.CoreNotificationService;
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||
|
||||
/**
|
||||
* We often need to wait until network and wallet is ready or other combination of startup states.
|
||||
@ -53,7 +49,9 @@ public class AppStartupState {
|
||||
private final BooleanProperty hasSufficientPeersForBroadcast = new SimpleBooleanProperty();
|
||||
|
||||
@Inject
|
||||
public AppStartupState(WalletsSetup walletsSetup, P2PService p2PService) {
|
||||
public AppStartupState(CoreNotificationService notificationService,
|
||||
CoreMoneroConnectionsService connectionsService,
|
||||
P2PService p2PService) {
|
||||
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
@ -62,13 +60,13 @@ public class AppStartupState {
|
||||
}
|
||||
});
|
||||
|
||||
walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.isDownloadComplete())
|
||||
connectionsService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (connectionsService.isDownloadComplete())
|
||||
isBlockDownloadComplete.set(true);
|
||||
});
|
||||
|
||||
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast())
|
||||
connectionsService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (connectionsService.hasSufficientPeersForBroadcast())
|
||||
hasSufficientPeersForBroadcast.set(true);
|
||||
});
|
||||
|
||||
@ -77,6 +75,7 @@ public class AppStartupState {
|
||||
hasSufficientPeersForBroadcast,
|
||||
allDomainServicesInitialized,
|
||||
(a, b, c, d) -> {
|
||||
log.info("p2pNetworkAndWalletInitialized = {} = updatedDataReceived={} && isBlockDownloadComplete={} && hasSufficientPeersForBroadcast={} && allDomainServicesInitialized={}", (a && b && c && d), updatedDataReceived.get(), isBlockDownloadComplete.get(), hasSufficientPeersForBroadcast.get(), allDomainServicesInitialized.get());
|
||||
if (a && b && c) {
|
||||
walletAndNetworkReady.set(true);
|
||||
}
|
||||
@ -85,6 +84,7 @@ public class AppStartupState {
|
||||
p2pNetworkAndWalletInitialized.subscribe((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
applicationFullyInitialized.set(true);
|
||||
notificationService.sendAppInitializedNotification();
|
||||
log.info("Application fully initialized");
|
||||
}
|
||||
});
|
||||
|
@ -41,8 +41,6 @@ import bisq.network.p2p.seed.SeedNodeRepository;
|
||||
|
||||
import bisq.common.app.AppModule;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
import bisq.common.proto.network.NetworkProtoResolver;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
|
||||
@ -93,6 +91,5 @@ public class CoreModule extends AppModule {
|
||||
install(new FilterModule(config));
|
||||
install(new CorePresentationModule(config));
|
||||
install(new MoneroConnectionModule(config));
|
||||
bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class);
|
||||
}
|
||||
}
|
||||
|
@ -230,14 +230,14 @@ public class DomainInitialisation {
|
||||
triggerPriceService.onAllServicesInitialized();
|
||||
mempoolService.onAllServicesInitialized();
|
||||
|
||||
if (revolutAccountsUpdateHandler != null) {
|
||||
if (revolutAccountsUpdateHandler != null && user.getPaymentAccountsAsObservable() != null) {
|
||||
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()
|
||||
.filter(paymentAccount -> paymentAccount instanceof RevolutAccount)
|
||||
.map(paymentAccount -> (RevolutAccount) paymentAccount)
|
||||
.filter(RevolutAccount::userNameNotSet)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
if (amazonGiftCardAccountsUpdateHandler != null) {
|
||||
if (amazonGiftCardAccountsUpdateHandler != null && user.getPaymentAccountsAsObservable() != null) {
|
||||
amazonGiftCardAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()
|
||||
.filter(paymentAccount -> paymentAccount instanceof AmazonGiftCardAccount)
|
||||
.map(paymentAccount -> (AmazonGiftCardAccount) paymentAccount)
|
||||
|
@ -17,6 +17,8 @@
|
||||
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.api.AccountServiceListener;
|
||||
import bisq.core.api.CoreAccountService;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
@ -27,7 +29,6 @@ import bisq.core.setup.CoreSetup;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.trade.txproof.xmr.XmrTxProofService;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
@ -35,6 +36,7 @@ import bisq.common.app.AppModule;
|
||||
import bisq.common.config.HavenoHelpFormatter;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.config.ConfigException;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.persistence.PersistenceManager;
|
||||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
@ -45,7 +47,6 @@ import bisq.common.util.Utilities;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@ -64,11 +65,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
private final String appName;
|
||||
private final String version;
|
||||
|
||||
protected CoreAccountService accountService;
|
||||
protected Injector injector;
|
||||
protected AppModule module;
|
||||
protected Config config;
|
||||
private boolean isShutdownInProgress;
|
||||
private boolean hasDowngraded;
|
||||
private boolean isReadOnly;
|
||||
|
||||
public HavenoExecutable(String fullName, String scriptName, String appName, String version) {
|
||||
this.fullName = fullName;
|
||||
@ -136,17 +138,61 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
setupGuice();
|
||||
setupAvoidStandbyMode();
|
||||
|
||||
hasDowngraded = HavenoSetup.hasDowngraded();
|
||||
if (hasDowngraded) {
|
||||
// If user tried to downgrade we do not read the persisted data to avoid data corruption
|
||||
// We call startApplication to enable UI to show popup. We prevent in HavenoSetup to go further
|
||||
// in the process and require a shut down.
|
||||
startApplication();
|
||||
} else {
|
||||
isReadOnly = HavenoSetup.hasDowngraded();
|
||||
|
||||
// Account service should be available before attempting to login.
|
||||
accountService = injector.getInstance(CoreAccountService.class);
|
||||
|
||||
// Application needs to restart on delete and restore of account.
|
||||
accountService.addListener(new AccountServiceListener() {
|
||||
@Override public void onAccountDeleted(Runnable onShutdown) { shutDownNoPersist(onShutdown); }
|
||||
@Override public void onAccountRestored(Runnable onShutdown) { shutDownNoPersist(onShutdown); }
|
||||
});
|
||||
|
||||
// Attempt to login, subclasses should implement interactive login and or rpc login.
|
||||
if (!isReadOnly && loginAccount()) {
|
||||
readAllPersisted(this::startApplication);
|
||||
} else {
|
||||
log.warn("Running application in readonly mode");
|
||||
startApplication();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not persist when shutting down after account restore and restarts since
|
||||
* that causes the current persistables to overwrite the restored or deleted state.
|
||||
*/
|
||||
protected void shutDownNoPersist(Runnable onShutdown) {
|
||||
this.isReadOnly = true;
|
||||
gracefulShutDown(() -> {
|
||||
log.info("Shutdown without persisting");
|
||||
if (onShutdown != null) onShutdown.run();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to login. TODO: supply a password in config or args
|
||||
*
|
||||
* @return true if account is opened successfully.
|
||||
*/
|
||||
protected boolean loginAccount() {
|
||||
if (accountService.accountExists()) {
|
||||
log.info("Account already exists, attempting to open");
|
||||
try {
|
||||
accountService.openAccount(null);
|
||||
} catch (IncorrectPasswordException ipe) {
|
||||
log.info("Account password protected, password required");
|
||||
}
|
||||
} else if (!config.passwordRequired) {
|
||||
log.info("Creating Haveno account with null password");
|
||||
accountService.createAccount(null);
|
||||
}
|
||||
return accountService.isAccountOpen();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We continue with a series of synchronous execution tasks
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -198,9 +244,9 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
}
|
||||
|
||||
protected void runHavenoSetup() {
|
||||
HavenoSetup bisqSetup = injector.getInstance(HavenoSetup.class);
|
||||
bisqSetup.addHavenoSetupListener(this);
|
||||
bisqSetup.start();
|
||||
HavenoSetup havenoSetup = injector.getInstance(HavenoSetup.class);
|
||||
havenoSetup.addHavenoSetupListener(this);
|
||||
havenoSetup.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -233,7 +279,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||
injector.getInstance(XmrTxProofService.class).shutDown();
|
||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||
injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc?
|
||||
injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService
|
||||
log.info("OpenOfferManager shutdown started");
|
||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||
log.info("OpenOfferManager shutdown completed");
|
||||
@ -248,16 +294,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
log.info("P2PService shutdown completed");
|
||||
module.close(injector);
|
||||
if (!hasDowngraded) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||
log.info("Graceful shutdown completed. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
}
|
||||
completeShutdown(resultHandler, EXIT_SUCCESS);
|
||||
});
|
||||
});
|
||||
walletsSetup.shutDown();
|
||||
@ -267,31 +304,26 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
// Wait max 20 sec.
|
||||
UserThread.runAfter(() -> {
|
||||
log.warn("Graceful shut down not completed in 20 sec. We trigger our timeout handler.");
|
||||
if (!hasDowngraded) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||
log.info("Graceful shutdown resulted in a timeout. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
}
|
||||
|
||||
completeShutdown(resultHandler, EXIT_SUCCESS);
|
||||
}, 20);
|
||||
} catch (Throwable t) {
|
||||
log.error("App shutdown failed with exception {}", t.toString());
|
||||
t.printStackTrace();
|
||||
if (!hasDowngraded) {
|
||||
completeShutdown(resultHandler, EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
private void completeShutdown(ResultHandler resultHandler, int exitCode) {
|
||||
if (!isReadOnly) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||
log.info("Graceful shutdown resulted in an error. Exiting now.");
|
||||
log.info("Graceful shutdown flushed persistence. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
|
||||
UserThread.runAfter(() -> System.exit(exitCode), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
|
||||
}
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(exitCode), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class HavenoHeadlessApp implements HeadlessApp {
|
||||
@Getter
|
||||
private static Runnable shutDownHandler;
|
||||
@Setter
|
||||
public static Runnable onGracefulShutDownHandler;
|
||||
|
||||
@Setter
|
||||
protected Injector injector;
|
||||
@ -50,6 +52,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||
shutDownHandler = this::stop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startApplication() {
|
||||
try {
|
||||
bisqSetup = injector.getInstance(HavenoSetup.class);
|
||||
@ -103,13 +106,13 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||
UserThread.runAfter(() -> {
|
||||
gracefulShutDownHandler.gracefulShutDown(() -> {
|
||||
log.debug("App shutdown complete");
|
||||
if (onGracefulShutDownHandler != null) onGracefulShutDownHandler.run();
|
||||
});
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
shutDownRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UncaughtExceptionHandler implementation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -115,6 +115,7 @@ public class HavenoHeadlessAppMain extends HavenoExecutable {
|
||||
onApplicationStarted();
|
||||
}
|
||||
|
||||
// TODO: implement interactive console which allows user to input commands; login, logoff, exit
|
||||
private void keepRunning() {
|
||||
while (true) {
|
||||
try {
|
||||
|
@ -195,7 +195,7 @@ public class HavenoSetup {
|
||||
private boolean allBasicServicesInitialized;
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private MonadicBinding<Boolean> p2pNetworkAndWalletInitialized;
|
||||
private final List<HavenoSetupListener> bisqSetupListeners = new ArrayList<>();
|
||||
private final List<HavenoSetupListener> havenoSetupListeners = new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
public HavenoSetup(DomainInitialisation domainInitialisation,
|
||||
@ -274,7 +274,7 @@ public class HavenoSetup {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addHavenoSetupListener(HavenoSetupListener listener) {
|
||||
bisqSetupListeners.add(listener);
|
||||
havenoSetupListeners.add(listener);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@ -284,7 +284,7 @@ public class HavenoSetup {
|
||||
return;
|
||||
}
|
||||
|
||||
persistBisqVersion();
|
||||
persistHavenoVersion();
|
||||
maybeReSyncSPVChain();
|
||||
maybeShowTac(this::step2);
|
||||
}
|
||||
@ -303,7 +303,7 @@ public class HavenoSetup {
|
||||
private void step4() {
|
||||
initDomainServices();
|
||||
|
||||
bisqSetupListeners.forEach(HavenoSetupListener::onSetupComplete);
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onSetupComplete);
|
||||
|
||||
// We set that after calling the setupCompleteHandler to not trigger a popup from the dev dummy accounts
|
||||
// in MainViewModel
|
||||
@ -373,7 +373,7 @@ public class HavenoSetup {
|
||||
}, STARTUP_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
||||
|
||||
log.info("Init P2P network");
|
||||
bisqSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
|
||||
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
|
||||
|
||||
// We only init wallet service here if not using Tor for bitcoinj.
|
||||
@ -402,10 +402,10 @@ public class HavenoSetup {
|
||||
|
||||
private void initWallet() {
|
||||
log.info("Init wallet");
|
||||
bisqSetupListeners.forEach(HavenoSetupListener::onInitWallet);
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
|
||||
Runnable walletPasswordHandler = () -> {
|
||||
log.info("Wallet password required");
|
||||
bisqSetupListeners.forEach(HavenoSetupListener::onRequestWalletPassword);
|
||||
havenoSetupListeners.forEach(HavenoSetupListener::onRequestWalletPassword);
|
||||
if (p2pNetworkReady.get())
|
||||
p2PNetworkSetup.setSplashP2PNetworkAnimationVisible(true);
|
||||
|
||||
@ -581,7 +581,7 @@ public class HavenoSetup {
|
||||
return hasDowngraded;
|
||||
}
|
||||
|
||||
public static void persistBisqVersion() {
|
||||
public static void persistHavenoVersion() {
|
||||
File versionFile = getVersionFile();
|
||||
if (!versionFile.exists()) {
|
||||
try {
|
||||
@ -639,6 +639,7 @@ public class HavenoSetup {
|
||||
}
|
||||
|
||||
private void maybeShowSecurityRecommendation() {
|
||||
if (user.getPaymentAccountsAsObservable() == null) return;
|
||||
String key = "remindPasswordAndBackup";
|
||||
user.getPaymentAccountsAsObservable().addListener((SetChangeListener<PaymentAccount>) change -> {
|
||||
if (!walletsManager.areWalletsEncrypted() && !user.isPaymentAccountImport() && preferences.showAgain(key) && change.wasAdded() &&
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.user.Preferences;
|
||||
@ -51,7 +51,7 @@ import javax.annotation.Nullable;
|
||||
public class P2PNetworkSetup {
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final Preferences preferences;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
@ -75,12 +75,12 @@ public class P2PNetworkSetup {
|
||||
@Inject
|
||||
public P2PNetworkSetup(PriceFeedService priceFeedService,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
Preferences preferences) {
|
||||
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
@ -91,7 +91,7 @@ public class P2PNetworkSetup {
|
||||
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
|
||||
|
||||
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
|
||||
walletsSetup.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
connectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
(state, warning, numP2pPeers, numBtcPeers, hiddenService, dataReceived) -> {
|
||||
String result;
|
||||
int p2pPeers = (int) numP2pPeers;
|
||||
|
@ -18,6 +18,7 @@
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.api.CoreContext;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.exceptions.InvalidHostException;
|
||||
import bisq.core.btc.exceptions.RejectedTxException;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
@ -67,6 +68,7 @@ public class WalletAppSetup {
|
||||
private final CoreContext coreContext;
|
||||
private final WalletsManager walletsManager;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final FeeService feeService;
|
||||
private final Config config;
|
||||
private final Preferences preferences;
|
||||
@ -91,12 +93,14 @@ public class WalletAppSetup {
|
||||
public WalletAppSetup(CoreContext coreContext,
|
||||
WalletsManager walletsManager,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
FeeService feeService,
|
||||
Config config,
|
||||
Preferences preferences) {
|
||||
this.coreContext = coreContext;
|
||||
this.walletsManager = walletsManager;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.feeService = feeService;
|
||||
this.config = config;
|
||||
this.preferences = preferences;
|
||||
@ -115,8 +119,8 @@ public class WalletAppSetup {
|
||||
VersionMessage.BITCOINJ_VERSION, "2a80db4");
|
||||
|
||||
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
|
||||
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),
|
||||
walletsSetup.chainHeightProperty(),
|
||||
btcInfoBinding = EasyBind.combine(connectionService.downloadPercentageProperty(), // TODO (woodser): update to XMR
|
||||
connectionService.chainHeightProperty(),
|
||||
feeService.feeUpdateCounterProperty(),
|
||||
walletServiceException,
|
||||
(downloadPercentage, chainHeight, feeUpdate, exception) -> {
|
||||
@ -124,10 +128,8 @@ public class WalletAppSetup {
|
||||
if (exception == null) {
|
||||
double percentage = (double) downloadPercentage;
|
||||
btcSyncProgress.set(percentage);
|
||||
int bestChainHeight = walletsSetup.getChain() != null ?
|
||||
walletsSetup.getChain().getBestChainHeight() :
|
||||
0;
|
||||
String chainHeightAsString = bestChainHeight > 0 ?
|
||||
Long bestChainHeight = connectionService.getDaemon() == null ? null : connectionService.getDaemon().getInfo().getHeight();
|
||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ?
|
||||
String.valueOf(bestChainHeight) :
|
||||
"";
|
||||
if (percentage == 1) {
|
||||
|
@ -29,7 +29,7 @@ import bisq.core.proto.persistable.CorePersistenceProtoResolver;
|
||||
import bisq.core.trade.TradeModule;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.core.xmr.connection.MoneroConnectionModule;
|
||||
import bisq.network.crypto.EncryptionServiceModule;
|
||||
import bisq.network.p2p.P2PModule;
|
||||
import bisq.network.p2p.network.BridgeAddressProvider;
|
||||
@ -41,8 +41,6 @@ import bisq.common.app.AppModule;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.KeyStorage;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
import bisq.common.proto.network.NetworkProtoResolver;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
|
||||
@ -93,6 +91,6 @@ public class ModuleForAppWithP2p extends AppModule {
|
||||
install(new BitcoinModule(config));
|
||||
install(new AlertModule(config));
|
||||
install(new FilterModule(config));
|
||||
bind(PubKeyRing.class).toProvider(PubKeyRingProvider.class);
|
||||
install(new MoneroConnectionModule(config));
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,14 @@ package bisq.core.btc.model;
|
||||
|
||||
import bisq.common.crypto.CryptoException;
|
||||
import bisq.common.crypto.Encryption;
|
||||
import bisq.common.crypto.ScryptUtil;
|
||||
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 com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Message;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashMap;
|
||||
@ -22,8 +24,6 @@ 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;
|
||||
@ -60,7 +60,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
transient private PersistenceManager<EncryptedConnectionList> persistenceManager;
|
||||
|
||||
private final Map<String, EncryptedConnection> items = new HashMap<>();
|
||||
private @NonNull String currentConnectionUri = "";
|
||||
private @NonNull String currentConnectionUrl = "";
|
||||
private long refreshPeriod;
|
||||
private boolean autoSwitch;
|
||||
|
||||
@ -70,17 +70,16 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
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,
|
||||
@NonNull String currentConnectionUrl,
|
||||
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.items.putAll(items.stream().collect(Collectors.toMap(EncryptedConnection::getUrl, Function.identity())));
|
||||
this.currentConnectionUrl = currentConnectionUrl;
|
||||
this.refreshPeriod = refreshPeriod;
|
||||
this.autoSwitch = autoSwitch;
|
||||
}
|
||||
@ -93,9 +92,11 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
initializeEncryption(persistedEncryptedConnectionList.keyCrypterScrypt);
|
||||
items.clear();
|
||||
items.putAll(persistedEncryptedConnectionList.items);
|
||||
currentConnectionUri = persistedEncryptedConnectionList.currentConnectionUri;
|
||||
currentConnectionUrl = persistedEncryptedConnectionList.currentConnectionUrl;
|
||||
refreshPeriod = persistedEncryptedConnectionList.refreshPeriod;
|
||||
autoSwitch = persistedEncryptedConnectionList.autoSwitch;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
@ -104,6 +105,8 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
writeLock.lock();
|
||||
try {
|
||||
initializeEncryption(ScryptUtil.getKeyCrypterScrypt());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
@ -203,11 +206,11 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentConnectionUri(String currentConnectionUri) {
|
||||
public void setCurrentConnectionUri(String currentConnectionUrl) {
|
||||
boolean changed;
|
||||
writeLock.lock();
|
||||
try {
|
||||
changed = !this.currentConnectionUri.equals(this.currentConnectionUri = currentConnectionUri == null ? "" : currentConnectionUri);
|
||||
changed = !this.currentConnectionUrl.equals(this.currentConnectionUrl = currentConnectionUrl == null ? "" : currentConnectionUrl);
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
}
|
||||
@ -219,17 +222,54 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
public Optional<String> getCurrentConnectionUri() {
|
||||
readLock.lock();
|
||||
try {
|
||||
return Optional.of(currentConnectionUri).filter(s -> !s.isEmpty());
|
||||
return Optional.of(currentConnectionUrl).filter(s -> !s.isEmpty());
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestPersistence() {
|
||||
public void requestPersistence() {
|
||||
persistenceManager.requestPersistence();
|
||||
}
|
||||
|
||||
private void onPasswordChange(String oldPassword, String newPassword) {
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
List<protobuf.EncryptedConnection> connections;
|
||||
ByteString saltString;
|
||||
String currentConnectionUrl;
|
||||
boolean autoSwitchEnabled;
|
||||
long refreshPeriod;
|
||||
readLock.lock();
|
||||
try {
|
||||
connections = items.values().stream()
|
||||
.map(EncryptedConnection::toProtoMessage).collect(Collectors.toList());
|
||||
saltString = keyCrypterScrypt.getScryptParameters().getSalt();
|
||||
currentConnectionUrl = this.currentConnectionUrl;
|
||||
autoSwitchEnabled = this.autoSwitch;
|
||||
refreshPeriod = this.refreshPeriod;
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
return protobuf.PersistableEnvelope.newBuilder()
|
||||
.setEncryptedConnectionList(protobuf.EncryptedConnectionList.newBuilder()
|
||||
.setSalt(saltString)
|
||||
.addAllItems(connections)
|
||||
.setCurrentConnectionUrl(currentConnectionUrl)
|
||||
.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.getCurrentConnectionUrl(), proto.getRefreshPeriod(), proto.getAutoSwitch());
|
||||
}
|
||||
|
||||
// ----------------------------- HELPERS ----------------------------------
|
||||
|
||||
public void changePassword(String oldPassword, String newPassword) {
|
||||
writeLock.lock();
|
||||
try {
|
||||
SecretKey oldSecret = encryptionKey;
|
||||
@ -243,9 +283,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
}
|
||||
|
||||
private SecretKey toSecretKey(String password) {
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
if (password == null) return null;
|
||||
return Encryption.getSecretKeyFromBytes(keyCrypterScrypt.deriveKey(password).getKey());
|
||||
}
|
||||
|
||||
@ -265,6 +303,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
}
|
||||
|
||||
private static byte[] decrypt(byte[] encrypted, SecretKey secret) {
|
||||
if (secret == null) return encrypted; // no encryption
|
||||
try {
|
||||
return Encryption.decrypt(encrypted, secret);
|
||||
} catch (CryptoException e) {
|
||||
@ -273,6 +312,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
}
|
||||
|
||||
private static byte[] encrypt(byte[] unencrypted, SecretKey secretKey) {
|
||||
if (secretKey == null) return unencrypted; // no encryption
|
||||
try {
|
||||
return Encryption.encrypt(unencrypted, secretKey);
|
||||
} catch (CryptoException e) {
|
||||
@ -286,7 +326,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
byte[] passwordSalt = generateSalt(passwordBytes);
|
||||
byte[] encryptedPassword = encryptPassword(passwordBytes, passwordSalt);
|
||||
return EncryptedConnection.builder()
|
||||
.uri(connection.getUri())
|
||||
.url(connection.getUri())
|
||||
.username(connection.getUsername() == null ? "" : connection.getUsername())
|
||||
.encryptedPassword(encryptedPassword)
|
||||
.encryptionSalt(passwordSalt)
|
||||
@ -298,7 +338,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
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 moneroRpcConnection = new MoneroRpcConnection(connection.getUrl(), username, password);
|
||||
moneroRpcConnection.setPriority(connection.getPriority());
|
||||
return moneroRpcConnection;
|
||||
}
|
||||
@ -357,39 +397,4 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ import com.google.inject.Inject;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.stream.Collectors;
|
||||
@ -37,10 +35,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroOutputWallet;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
|
||||
/**
|
||||
* The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the
|
||||
* associated protobuf message.
|
||||
@ -48,7 +42,6 @@ import monero.wallet.model.MoneroWalletListener;
|
||||
@Slf4j
|
||||
public final class XmrAddressEntryList implements PersistableEnvelope, PersistedDataHost {
|
||||
transient private PersistenceManager<XmrAddressEntryList> persistenceManager;
|
||||
transient private MoneroWallet wallet;
|
||||
private final Set<XmrAddressEntry> entrySet = new CopyOnWriteArraySet<>();
|
||||
|
||||
@Inject
|
||||
@ -100,61 +93,6 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onWalletReady(MoneroWallet wallet) {
|
||||
this.wallet = wallet;
|
||||
|
||||
if (!entrySet.isEmpty()) {
|
||||
// Set<XmrAddressEntry> toBeRemoved = new HashSet<>();
|
||||
// entrySet.forEach(addressEntry -> {
|
||||
// DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash(
|
||||
// addressEntry.getPubKeyHash(),
|
||||
// Script.ScriptType.P2PKH);
|
||||
// if (keyFromPubHash != null) {
|
||||
// Address addressFromKey = LegacyAddress.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash);
|
||||
// // We want to ensure key and address matches in case we have address in entry available already
|
||||
// if (addressEntry.getAddress() == null || addressFromKey.equals(addressEntry.getAddress())) {
|
||||
// addressEntry.setDeterministicKey(keyFromPubHash);
|
||||
// } else {
|
||||
// log.error("We found an address entry without key but cannot apply the key as the address " +
|
||||
// "is not matching. " +
|
||||
// "We remove that entry as it seems it is not compatible with our wallet. " +
|
||||
// "addressFromKey={}, addressEntry.getAddress()={}",
|
||||
// addressFromKey, addressEntry.getAddress());
|
||||
// toBeRemoved.add(addressEntry);
|
||||
// }
|
||||
// } else {
|
||||
// log.error("Key from addressEntry {} not found in that wallet. We remove that entry. " +
|
||||
// "This is expected at restore from seeds.", addressEntry.toString());
|
||||
// toBeRemoved.add(addressEntry);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// toBeRemoved.forEach(entrySet::remove);
|
||||
}
|
||||
|
||||
// In case we restore from seed words and have balance we need to add the relevant addresses to our list.
|
||||
// IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to
|
||||
// incoming txs at blockchain sync to add the rest.
|
||||
if (wallet.getBalance().compareTo(new BigInteger("0")) > 0) {
|
||||
wallet.getAccounts().forEach(acct -> {
|
||||
log.info("Create XmrAddressEntry for IssuedReceiveAddress. address={}", acct.getPrimaryAddress());
|
||||
if (acct.getIndex() != 0) entrySet.add(new XmrAddressEntry(acct.getIndex(), acct.getPrimaryAddress(), XmrAddressEntry.Context.AVAILABLE));
|
||||
});
|
||||
}
|
||||
|
||||
// We add those listeners to get notified about potential new transactions and
|
||||
// add an address entry list in case it does not exist yet. This is mainly needed for restore from seed words
|
||||
// but can help as well in case the addressEntry list would miss an address where the wallet was received
|
||||
// funds (e.g. if the user sends funds to an address which has not been provided in the main UI - like from the
|
||||
// wallet details window).
|
||||
wallet.addListener(new MoneroWalletListener() {
|
||||
@Override public void onOutputReceived(MoneroOutputWallet output) { maybeAddNewAddressEntry(output); }
|
||||
@Override public void onOutputSpent(MoneroOutputWallet output) { maybeAddNewAddressEntry(output); }
|
||||
});
|
||||
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public ImmutableList<XmrAddressEntry> getAddressEntriesAsListImmutable() {
|
||||
return ImmutableList.copyOf(entrySet);
|
||||
}
|
||||
@ -202,25 +140,6 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted
|
||||
persistenceManager.requestPersistence();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO (woodser): this should be removed since only using account 0
|
||||
private void maybeAddNewAddressEntry(MoneroOutputWallet output) {
|
||||
if (output.getAccountIndex() == 0) return;
|
||||
String address = wallet.getAddress(output.getAccountIndex(), output.getSubaddressIndex());
|
||||
if (!isAddressInEntries(address)) addAddressEntry(new XmrAddressEntry(output.getAccountIndex(), address, XmrAddressEntry.Context.AVAILABLE));
|
||||
}
|
||||
|
||||
private boolean isAddressInEntries(String address) {
|
||||
for (XmrAddressEntry entry : entrySet) {
|
||||
if (entry.getAddressString().equals(address)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "XmrAddressEntryList{" +
|
||||
|
@ -8,14 +8,14 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
class DownloadListener {
|
||||
public class DownloadListener {
|
||||
private final DoubleProperty percentage = new SimpleDoubleProperty(-1);
|
||||
|
||||
protected void progress(double percentage, int blocksLeft, Date date) {
|
||||
public void progress(double percentage, int blocksLeft, Date date) {
|
||||
UserThread.execute(() -> this.percentage.set(percentage / 100d));
|
||||
}
|
||||
|
||||
protected void doneDownload() {
|
||||
public void doneDownload() {
|
||||
UserThread.execute(() -> this.percentage.set(1d));
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
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;
|
||||
@ -72,9 +71,6 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -89,15 +85,6 @@ 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;
|
||||
import monero.daemon.model.MoneroNetworkType;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.MoneroWalletRpc;
|
||||
import monero.wallet.model.MoneroWalletConfig;
|
||||
|
||||
/**
|
||||
* <p>Utility class that wraps the boilerplate needed to set up a new SPV bitcoinj app. Instantiate it with a directory
|
||||
* and file prefix, optionally configure a few things, then use startAsync and optionally awaitRunning. The object will
|
||||
@ -125,27 +112,13 @@ public class WalletConfig extends AbstractIdleService {
|
||||
|
||||
protected static final Logger log = LoggerFactory.getLogger(WalletConfig.class);
|
||||
|
||||
// Monero configuration
|
||||
// TODO: don't hard code configuration, inject into classes?
|
||||
private static final MoneroNetworkType MONERO_NETWORK_TYPE = MoneroNetworkType.STAGENET;
|
||||
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";
|
||||
private static final String MONERO_WALLET_RPC_USERNAME = "rpc_user";
|
||||
private static final String MONERO_WALLET_RPC_PASSWORD = "abc123";
|
||||
private static final long MONERO_WALLET_SYNC_RATE = 5000l;
|
||||
|
||||
protected final NetworkParameters params;
|
||||
protected final String filePrefix;
|
||||
protected final CoreMoneroConnectionsService moneroConnectionsManager;
|
||||
protected volatile BlockChain vChain;
|
||||
protected volatile SPVBlockStore vStore;
|
||||
protected volatile MoneroDaemonRpc vXmrDaemon;
|
||||
protected volatile MoneroWalletRpc vXmrWallet;
|
||||
protected volatile Wallet vBtcWallet;
|
||||
protected volatile PeerGroup vPeerGroup;
|
||||
|
||||
protected final int rpcBindPort;
|
||||
protected final File directory;
|
||||
protected volatile File vXmrWalletFile;
|
||||
protected volatile File vBtcWalletFile;
|
||||
@ -176,21 +149,17 @@ public class WalletConfig extends AbstractIdleService {
|
||||
*/
|
||||
public WalletConfig(NetworkParameters params,
|
||||
File directory,
|
||||
int rpcBindPort,
|
||||
CoreMoneroConnectionsService connectionsManager,
|
||||
String filePrefix) {
|
||||
this(new Context(params), directory, rpcBindPort, connectionsManager, filePrefix);
|
||||
this(new Context(params), directory, 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, CoreMoneroConnectionsService connectionsManager, String filePrefix) {
|
||||
private WalletConfig(Context context, File directory, String filePrefix) {
|
||||
this.context = context;
|
||||
this.params = checkNotNull(context.getParams());
|
||||
this.directory = checkDir(directory);
|
||||
this.rpcBindPort = rpcBindPort;
|
||||
this.moneroConnectionsManager = connectionsManager;
|
||||
this.filePrefix = checkNotNull(filePrefix);
|
||||
}
|
||||
|
||||
@ -293,85 +262,6 @@ public class WalletConfig extends AbstractIdleService {
|
||||
// Meant to be overridden by subclasses
|
||||
}
|
||||
|
||||
public boolean walletExists(String walletName) {
|
||||
String path = directory.toString() + File.separator + walletName;
|
||||
return new File(path + ".keys").exists();
|
||||
}
|
||||
|
||||
public MoneroWalletRpc createWallet(MoneroWalletConfig config, Integer port) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||
|
||||
// create wallet
|
||||
try {
|
||||
walletRpc.createWallet(config);
|
||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
WalletConfig.MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroWalletRpc openWallet(MoneroWalletConfig config, Integer port) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||
|
||||
// open wallet
|
||||
try {
|
||||
walletRpc.openWallet(config);
|
||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
WalletConfig.MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private MoneroWalletRpc startWalletRpcInstance(Integer port) {
|
||||
|
||||
// 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", 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));
|
||||
}
|
||||
return WalletConfig.MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||
}
|
||||
|
||||
public void closeWallet(MoneroWallet walletRpc, boolean save) {
|
||||
WalletConfig.MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save);
|
||||
}
|
||||
|
||||
public void deleteWallet(String walletName) {
|
||||
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
||||
String path = directory.toString() + File.separator + walletName;
|
||||
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
//WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup?
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startUp() throws Exception {
|
||||
// Runs in a separate thread.
|
||||
@ -380,31 +270,6 @@ public class WalletConfig extends AbstractIdleService {
|
||||
File chainFile = new File(directory, filePrefix + ".spvchain");
|
||||
boolean chainFileExists = chainFile.exists();
|
||||
|
||||
// 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";
|
||||
vXmrWalletFile = new File(directory, filePrefix + xmrPrefix);
|
||||
if (MoneroUtils.walletExists(vXmrWalletFile.getPath())) {
|
||||
vXmrWallet = openWallet(new MoneroWalletConfig().setPath(filePrefix + xmrPrefix).setPassword("abctesting123"), rpcBindPort);
|
||||
} else {
|
||||
vXmrWallet = createWallet(new MoneroWalletConfig().setPath(filePrefix + xmrPrefix).setPassword("abctesting123"), rpcBindPort);
|
||||
}
|
||||
System.out.println("Monero wallet path: " + vXmrWallet.getPath());
|
||||
System.out.println("Monero wallet address: " + vXmrWallet.getPrimaryAddress());
|
||||
System.out.println("Monero wallet uri: " + vXmrWallet.getRpcConnection().getUri());
|
||||
// vXmrWallet.rescanSpent();
|
||||
// vXmrWallet.rescanBlockchain();
|
||||
vXmrWallet.sync(); // blocking
|
||||
downloadListener.doneDownload();
|
||||
vXmrWallet.save();
|
||||
System.out.println("Loaded wallet balance: " + vXmrWallet.getBalance(0));
|
||||
System.out.println("Loaded wallet unlocked balance: " + vXmrWallet.getUnlockedBalance(0));
|
||||
|
||||
String btcPrefix = "_BTC";
|
||||
vBtcWalletFile = new File(directory, filePrefix + btcPrefix + ".wallet");
|
||||
boolean shouldReplayWallet = (vBtcWalletFile.exists() && !chainFileExists) || restoreFromSeed != null;
|
||||
@ -647,16 +512,6 @@ public class WalletConfig extends AbstractIdleService {
|
||||
return vBtcWallet;
|
||||
}
|
||||
|
||||
public MoneroDaemon getXmrDaemon() {
|
||||
checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete");
|
||||
return vXmrDaemon;
|
||||
}
|
||||
|
||||
public MoneroWallet getXmrWallet() {
|
||||
checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete");
|
||||
return vXmrWallet;
|
||||
}
|
||||
|
||||
public PeerGroup peerGroup() {
|
||||
checkState(state() == State.STARTING || state() == State.RUNNING, "Cannot call until startup is complete");
|
||||
return vPeerGroup;
|
||||
|
@ -17,12 +17,10 @@
|
||||
|
||||
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;
|
||||
import bisq.core.btc.model.AddressEntryList;
|
||||
import bisq.core.btc.model.XmrAddressEntryList;
|
||||
import bisq.core.btc.nodes.BtcNetworkConfig;
|
||||
import bisq.core.btc.nodes.BtcNodes;
|
||||
import bisq.core.btc.nodes.BtcNodes.BtcNode;
|
||||
@ -66,14 +64,11 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@ -100,9 +95,6 @@ import javax.annotation.Nullable;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroPeer;
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
// Setup wallets and use WalletConfig for BitcoinJ wiring.
|
||||
// Other like WalletConfig we are here always on the user thread. That is one reason why we do not
|
||||
@ -111,8 +103,7 @@ import monero.wallet.MoneroWallet;
|
||||
public class WalletsSetup {
|
||||
|
||||
public static final String PRE_SEGWIT_WALLET_BACKUP = "pre_segwit_haveno_BTC.wallet.backup";
|
||||
private static final int MIN_BROADCAST_CONNECTIONS = 2;
|
||||
private static final long DAEMON_POLL_INTERVAL_SECONDS = 20;
|
||||
private static final int MIN_BROADCAST_CONNECTIONS = 0;
|
||||
|
||||
@Getter
|
||||
public final BooleanProperty walletsSetupFailed = new SimpleBooleanProperty();
|
||||
@ -122,25 +113,20 @@ public class WalletsSetup {
|
||||
|
||||
private final RegTestHost regTestHost;
|
||||
private final AddressEntryList addressEntryList;
|
||||
private final XmrAddressEntryList xmrAddressEntryList;
|
||||
private final Preferences preferences;
|
||||
private final Socks5ProxyProvider socks5ProxyProvider;
|
||||
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;
|
||||
private final NetworkParameters params;
|
||||
private final File walletDir;
|
||||
private final int walletRpcBindPort;
|
||||
private final int socks5DiscoverMode;
|
||||
private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
|
||||
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
||||
private final ObjectProperty<List<MoneroPeer>> peers = new SimpleObjectProperty<>();
|
||||
private final DownloadListener downloadListener = new DownloadListener();
|
||||
private final List<Runnable> setupTaskHandlers = new ArrayList<>();
|
||||
private final List<Runnable> setupCompletedHandlers = new ArrayList<>();
|
||||
public final BooleanProperty shutDownComplete = new SimpleBooleanProperty();
|
||||
private final boolean useAllProvidedNodes;
|
||||
@ -153,36 +139,29 @@ public class WalletsSetup {
|
||||
@Inject
|
||||
public WalletsSetup(RegTestHost regTestHost,
|
||||
AddressEntryList addressEntryList,
|
||||
XmrAddressEntryList xmrAddressEntryList,
|
||||
Preferences preferences,
|
||||
Socks5ProxyProvider socks5ProxyProvider,
|
||||
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,
|
||||
@Named(Config.USE_ALL_PROVIDED_NODES) boolean useAllProvidedNodes,
|
||||
@Named(Config.NUM_CONNECTIONS_FOR_BTC) int numConnectionsForBtc,
|
||||
@Named(Config.SOCKS5_DISCOVER_MODE) String socks5DiscoverModeString) {
|
||||
this.regTestHost = regTestHost;
|
||||
this.addressEntryList = addressEntryList;
|
||||
this.xmrAddressEntryList = xmrAddressEntryList;
|
||||
this.preferences = preferences;
|
||||
this.socks5ProxyProvider = socks5ProxyProvider;
|
||||
this.config = config;
|
||||
this.localBitcoinNode = localBitcoinNode;
|
||||
this.btcNodes = btcNodes;
|
||||
this.moneroConnectionsManager = moneroConnectionsManager;
|
||||
this.numConnectionsForBtc = numConnectionsForBtc;
|
||||
this.useAllProvidedNodes = useAllProvidedNodes;
|
||||
this.userAgent = userAgent;
|
||||
this.socks5DiscoverMode = evaluateMode(socks5DiscoverModeString);
|
||||
this.walletDir = walletDir;
|
||||
this.walletRpcBindPort = walletRpcBindPort;
|
||||
|
||||
xmrWalletFileName = "haveno_" + config.baseCurrencyNetwork.getCurrencyCode();
|
||||
params = Config.baseCurrencyNetworkParameters();
|
||||
PeerGroup.setIgnoreHttpSeeds(true);
|
||||
}
|
||||
@ -206,31 +185,23 @@ 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, moneroConnectionsManager, "haveno") {
|
||||
walletConfig = new WalletConfig(params, walletDir, "haveno") {
|
||||
@Override
|
||||
protected void onSetupCompleted() {
|
||||
//We are here in the btcj thread Thread[ STARTING,5,main]
|
||||
super.onSetupCompleted();
|
||||
|
||||
final PeerGroup peerGroup = walletConfig.peerGroup();
|
||||
final BlockChain chain = walletConfig.chain();
|
||||
|
||||
// We don't want to get our node white list polluted with nodes from AddressMessage calls.
|
||||
if (preferences.getBitcoinNodes() != null && !preferences.getBitcoinNodes().isEmpty())
|
||||
peerGroup.setAddPeersFromAddressMessage(false);
|
||||
|
||||
UserThread.runPeriodically(() -> {
|
||||
updateDaemonInfo();
|
||||
}, DAEMON_POLL_INTERVAL_SECONDS);
|
||||
|
||||
// Need to be Threading.SAME_THREAD executor otherwise BitcoinJ will skip that listener
|
||||
peerGroup.addPreMessageReceivedEventListener(Threading.SAME_THREAD, (peer, message) -> {
|
||||
if (message instanceof RejectMessage) {
|
||||
@ -244,11 +215,12 @@ public class WalletsSetup {
|
||||
return message;
|
||||
});
|
||||
|
||||
// run external startup handlers
|
||||
setupTaskHandlers.forEach(Runnable::run);
|
||||
|
||||
// Map to user thread
|
||||
UserThread.execute(() -> {
|
||||
updateDaemonInfo();
|
||||
addressEntryList.onWalletReady(walletConfig.btcWallet());
|
||||
xmrAddressEntryList.onWalletReady(walletConfig.getXmrWallet());
|
||||
timeoutTimer.stop();
|
||||
setupCompletedHandlers.forEach(Runnable::run);
|
||||
});
|
||||
@ -256,23 +228,6 @@ public class WalletsSetup {
|
||||
// onSetupCompleted in walletAppKit is not the called on the last invocations, so we add a bit of delay
|
||||
UserThread.runAfter(resultHandler::handleResult, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
walletConfig.setSocks5Proxy(socks5Proxy);
|
||||
walletConfig.setConfig(config);
|
||||
@ -427,9 +382,7 @@ public class WalletsSetup {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void backupWallets() {
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFileName, 20);
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFileName + ".keys", 20);
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFileName + ".address.txt", 20);
|
||||
// TODO: remove?
|
||||
}
|
||||
|
||||
public void clearBackups() {
|
||||
@ -479,6 +432,10 @@ public class WalletsSetup {
|
||||
// Handlers
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addSetupTaskHandler(Runnable handler) {
|
||||
setupTaskHandlers.add(handler);
|
||||
}
|
||||
|
||||
public void addSetupCompletedHandler(Runnable handler) {
|
||||
setupCompletedHandlers.add(handler);
|
||||
}
|
||||
@ -492,14 +449,6 @@ public class WalletsSetup {
|
||||
return walletConfig.btcWallet();
|
||||
}
|
||||
|
||||
public MoneroDaemon getXmrDaemon() {
|
||||
return walletConfig.getXmrDaemon();
|
||||
}
|
||||
|
||||
public MoneroWallet getXmrWallet() {
|
||||
return walletConfig.getXmrWallet();
|
||||
}
|
||||
|
||||
public NetworkParameters getParams() {
|
||||
return params;
|
||||
}
|
||||
@ -521,10 +470,6 @@ public class WalletsSetup {
|
||||
return numPeers;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<List<MoneroPeer>> peerConnectionsProperty() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public LongProperty chainHeightProperty() {
|
||||
return chainHeight;
|
||||
}
|
||||
@ -538,14 +483,7 @@ public class WalletsSetup {
|
||||
}
|
||||
|
||||
public boolean isChainHeightSyncedWithinTolerance() {
|
||||
Long peersChainHeight = walletConfig.vXmrDaemon.getSyncInfo().getTargetHeight();
|
||||
if (peersChainHeight == 0) return true; // monero-daemon-rpc sync_info's target_height returns 0 when node is fully synced
|
||||
long bestChainHeight = chainHeight.get();
|
||||
if (Math.abs(peersChainHeight - bestChainHeight) <= 3) {
|
||||
return true;
|
||||
}
|
||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), peersChainHeight);
|
||||
return false;
|
||||
throw new RuntimeException("WalletsSetup.isChainHeightSyncedWithinTolerance() not implemented for BTC");
|
||||
}
|
||||
|
||||
public Set<Address> getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) {
|
||||
|
@ -20,15 +20,12 @@ package bisq.core.btc.wallet;
|
||||
import bisq.core.btc.exceptions.SigningException;
|
||||
import bisq.core.btc.exceptions.TransactionVerificationException;
|
||||
import bisq.core.btc.exceptions.WalletException;
|
||||
import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.btc.model.InputsAndChangeOutput;
|
||||
import bisq.core.btc.model.PreparedDepositTxAndMakerInputs;
|
||||
import bisq.core.btc.model.RawTransactionInput;
|
||||
import bisq.core.btc.setup.WalletConfig;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.ParsingUtils;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.util.Tuple2;
|
||||
@ -37,7 +34,6 @@ import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.SegwitAddress;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
@ -78,11 +74,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
public class TradeWalletService {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
|
||||
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
|
||||
@ -94,8 +85,6 @@ public class TradeWalletService {
|
||||
@Nullable
|
||||
private Wallet wallet;
|
||||
@Nullable
|
||||
private MoneroWallet xmrWallet;
|
||||
@Nullable
|
||||
private WalletConfig walletConfig;
|
||||
@Nullable
|
||||
private KeyParameter aesKey;
|
||||
@ -113,7 +102,6 @@ public class TradeWalletService {
|
||||
walletsSetup.addSetupCompletedHandler(() -> {
|
||||
walletConfig = walletsSetup.getWalletConfig();
|
||||
wallet = walletsSetup.getBtcWallet();
|
||||
xmrWallet = walletsSetup.getXmrWallet();
|
||||
});
|
||||
}
|
||||
|
||||
@ -132,25 +120,6 @@ public class TradeWalletService {
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Trade fee
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public MoneroTxWallet createXmrTradingFeeTx(
|
||||
String reservedForTradeAddress,
|
||||
Coin reservedFundsForOffer,
|
||||
Coin makerFee,
|
||||
Coin txFee,
|
||||
String feeReceiver,
|
||||
boolean broadcastTx) {
|
||||
return xmrWallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setDestinations(
|
||||
new MoneroDestination(feeReceiver, ParsingUtils.coinToAtomicUnits(makerFee)),
|
||||
new MoneroDestination(reservedForTradeAddress, ParsingUtils.coinToAtomicUnits(reservedFundsForOffer)))
|
||||
.setRelay(broadcastTx));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Deposit tx
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1061,16 +1030,6 @@ public class TradeWalletService {
|
||||
// Misc
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Returns the local existing wallet transaction with the given ID, or {@code null} if missing.
|
||||
*
|
||||
* @param txHash the transaction hash of the transaction we want to lookup
|
||||
*/
|
||||
public MoneroTxWallet getWalletTx(String txHash) {
|
||||
checkNotNull(xmrWallet);
|
||||
return xmrWallet.getTx(txHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local existing wallet transaction with the given ID, or {@code null} if missing.
|
||||
*
|
||||
|
@ -18,9 +18,8 @@
|
||||
package bisq.core.btc.wallet;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.crypto.ScryptUtil;
|
||||
import bisq.core.locale.Res;
|
||||
|
||||
import bisq.common.crypto.ScryptUtil;
|
||||
import bisq.common.handlers.ExceptionHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
|
@ -1,40 +1,43 @@
|
||||
package bisq.core.btc.wallet;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.core.btc.exceptions.AddressEntryException;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.file.FileUtil;
|
||||
import bisq.core.api.AccountServiceListener;
|
||||
import bisq.core.api.CoreAccountService;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.listeners.XmrBalanceListener;
|
||||
import bisq.core.btc.model.XmrAddressEntry;
|
||||
import bisq.core.btc.model.XmrAddressEntryList;
|
||||
import bisq.core.btc.setup.MoneroWalletRpcManager;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.util.ParsingUtils;
|
||||
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import com.google.common.util.concurrent.Service.State;
|
||||
import com.google.inject.name.Named;
|
||||
import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroNetworkType;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.MoneroWalletRpc;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroOutputWallet;
|
||||
import monero.wallet.model.MoneroSubaddress;
|
||||
@ -44,123 +47,401 @@ import monero.wallet.model.MoneroTxWallet;
|
||||
import monero.wallet.model.MoneroWalletConfig;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
import monero.wallet.model.MoneroWalletListenerI;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class XmrWalletService {
|
||||
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
|
||||
|
||||
private WalletsSetup walletsSetup;
|
||||
private final XmrAddressEntryList addressEntryList;
|
||||
// Monero configuration
|
||||
// TODO: don't hard code configuration, inject into classes?
|
||||
private static final MoneroNetworkType MONERO_NETWORK_TYPE = MoneroNetworkType.STAGENET;
|
||||
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";
|
||||
private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user";
|
||||
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
|
||||
private static final String MONERO_WALLET_NAME = "haveno_XMR";
|
||||
private static final long MONERO_WALLET_SYNC_RATE = 5000l;
|
||||
|
||||
private final CoreAccountService accountService;
|
||||
private final CoreMoneroConnectionsService connectionsService;
|
||||
private final XmrAddressEntryList xmrAddressEntryList;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final File walletDir;
|
||||
private final File xmrWalletFile;
|
||||
private final int rpcBindPort;
|
||||
protected final CopyOnWriteArraySet<XmrBalanceListener> balanceListeners = new CopyOnWriteArraySet<>();
|
||||
protected final CopyOnWriteArraySet<MoneroWalletListenerI> walletListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private TradeManager tradeManager;
|
||||
private MoneroWallet wallet;
|
||||
private Map<String, MoneroWallet> multisigWallets;
|
||||
|
||||
@Getter
|
||||
private MoneroWallet wallet;
|
||||
|
||||
@Inject
|
||||
XmrWalletService(WalletsSetup walletsSetup,
|
||||
XmrAddressEntryList addressEntryList) {
|
||||
XmrWalletService(CoreAccountService accountService,
|
||||
CoreMoneroConnectionsService connectionsService,
|
||||
WalletsSetup walletsSetup,
|
||||
XmrAddressEntryList xmrAddressEntryList,
|
||||
@Named(Config.WALLET_DIR) File walletDir,
|
||||
@Named(Config.WALLET_RPC_BIND_PORT) int rpcBindPort) {
|
||||
this.accountService = accountService;
|
||||
this.connectionsService = connectionsService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
|
||||
this.addressEntryList = addressEntryList;
|
||||
this.xmrAddressEntryList = xmrAddressEntryList;
|
||||
this.multisigWallets = new HashMap<String, MoneroWallet>();
|
||||
this.walletDir = walletDir;
|
||||
this.rpcBindPort = rpcBindPort;
|
||||
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
||||
|
||||
walletsSetup.addSetupCompletedHandler(() -> {
|
||||
wallet = walletsSetup.getXmrWallet();
|
||||
wallet.addListener(new MoneroWalletListener() {
|
||||
@Override
|
||||
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { }
|
||||
// initialize after account open and basic setup
|
||||
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
|
||||
|
||||
// initialize
|
||||
initialize();
|
||||
|
||||
// listen for account updates
|
||||
accountService.addListener(new AccountServiceListener() {
|
||||
|
||||
@Override
|
||||
public void onNewBlock(long height) { }
|
||||
public void onAccountCreated() {
|
||||
log.info(getClass() + ".accountService.onAccountCreated()");
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
|
||||
notifyBalanceListeners();
|
||||
public void onAccountOpened() {
|
||||
log.info(getClass() + ".accountService.onAccountOpened()");
|
||||
initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccountClosed() {
|
||||
log.info(getClass() + ".accountService.onAccountClosed()");
|
||||
closeAllWallets();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasswordChanged(String oldPassword, String newPassword) {
|
||||
log.info(getClass() + "accountservice.onPasswordChanged()");
|
||||
changeWalletPasswords(oldPassword, newPassword);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
walletsSetup.getMoneroConnectionsManager().addConnectionListener(newConnection -> {
|
||||
updateDaemonConnections(newConnection);
|
||||
});
|
||||
});
|
||||
// TODO (woodser): need trade manager to get trade ids to change all wallet passwords?
|
||||
public void setTradeManager(TradeManager tradeManager) {
|
||||
this.tradeManager = tradeManager;
|
||||
}
|
||||
|
||||
public MoneroWallet getWallet() {
|
||||
State state = walletsSetup.getWalletConfig().state();
|
||||
checkState(state == State.STARTING || state == State.RUNNING, "Cannot call until startup is complete");
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public MoneroDaemon getDaemon() {
|
||||
return walletsSetup.getXmrDaemon();
|
||||
return connectionsService.getDaemon();
|
||||
}
|
||||
|
||||
// TODO (woodser): wallet has single password which is passed here?
|
||||
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
|
||||
public CoreMoneroConnectionsService getConnectionsService() {
|
||||
return connectionsService;
|
||||
}
|
||||
|
||||
public String getWalletPassword() {
|
||||
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
|
||||
}
|
||||
|
||||
public boolean walletExists(String walletName) {
|
||||
String path = walletDir.toString() + File.separator + walletName;
|
||||
return new File(path + ".keys").exists();
|
||||
}
|
||||
|
||||
public MoneroWalletRpc createWallet(MoneroWalletConfig config, Integer port) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||
|
||||
// create wallet
|
||||
try {
|
||||
walletRpc.createWallet(config);
|
||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroWalletRpc openWallet(MoneroWalletConfig config, Integer port) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||
|
||||
// open wallet
|
||||
try {
|
||||
walletRpc.openWallet(config);
|
||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc, false);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private MoneroWalletRpc startWalletRpcInstance(Integer port) {
|
||||
|
||||
// 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 = connectionsService.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", connection.getUri(), "--rpc-login",
|
||||
MONERO_WALLET_RPC_USERNAME + ":" + getWalletPassword(), "--wallet-dir", walletDir.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));
|
||||
}
|
||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||
}
|
||||
|
||||
public void closeWallet(MoneroWallet walletRpc, boolean save) {
|
||||
log.info("{}.closeWallet({}, {})", getClass(), walletRpc.getPath(), save);
|
||||
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save);
|
||||
}
|
||||
|
||||
public void deleteWallet(String walletName) {
|
||||
log.info("{}.deleteWallet({})", getClass(), walletName);
|
||||
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
||||
String path = walletDir.toString() + File.separator + walletName;
|
||||
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
// WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup?
|
||||
}
|
||||
|
||||
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
|
||||
public synchronized MoneroWallet createMultisigWallet(String tradeId) {
|
||||
log.info("{}.createMultisigWallet({})", getClass(), tradeId);
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = "xmr_multisig_trade_" + tradeId;
|
||||
MoneroWallet multisigWallet = null;
|
||||
multisigWallet = walletsSetup.getWalletConfig().createWallet(new MoneroWalletConfig()
|
||||
.setPath(path)
|
||||
.setPassword("abctesting123"),
|
||||
null); // auto-assign port
|
||||
multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
multisigWallet.startSyncing(5000l);
|
||||
return multisigWallet;
|
||||
}
|
||||
|
||||
public synchronized MoneroWallet getMultisigWallet(String tradeId) {
|
||||
public MoneroWallet getMultisigWallet(String tradeId) { // TODO (woodser): synchronize per wallet id
|
||||
log.info("{}.getMultisigWallet({})", getClass(), tradeId);
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = "xmr_multisig_trade_" + tradeId;
|
||||
MoneroWallet multisigWallet = null;
|
||||
multisigWallet = walletsSetup.getWalletConfig().openWallet(new MoneroWalletConfig()
|
||||
.setPath(path)
|
||||
.setPassword("abctesting123"),
|
||||
null);
|
||||
if (!walletExists(path)) return null;
|
||||
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
multisigWallet.startSyncing(5000l); // TODO (woodser): use sync period from config. apps stall if too many multisig wallets and too short sync period
|
||||
return multisigWallet;
|
||||
}
|
||||
|
||||
public synchronized boolean deleteMultisigWallet(String tradeId) {
|
||||
log.info("{}.deleteMultisigWallet({})", getClass(), tradeId);
|
||||
String walletName = "xmr_multisig_trade_" + tradeId;
|
||||
if (!walletsSetup.getWalletConfig().walletExists(walletName)) return false;
|
||||
if (!walletExists(walletName)) return false;
|
||||
try {
|
||||
walletsSetup.getWalletConfig().closeWallet(getMultisigWallet(tradeId), false);
|
||||
closeWallet(getMultisigWallet(tradeId), false);
|
||||
} catch (Exception err) {
|
||||
// multisig wallet may not be open
|
||||
}
|
||||
walletsSetup.getWalletConfig().deleteWallet(walletName);
|
||||
deleteWallet(walletName);
|
||||
multisigWallets.remove(tradeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||
try {
|
||||
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
||||
printTxs("XmrWalletService.createTx", tx);
|
||||
return tx;
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
closeAllWallets();
|
||||
}
|
||||
|
||||
// ------------------------------ PRIVATE HELPERS -------------------------
|
||||
|
||||
private void initialize() {
|
||||
|
||||
// backup wallet files
|
||||
backupWallets();
|
||||
|
||||
// initialize main wallet
|
||||
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
||||
wallet = MoneroUtils.walletExists(xmrWalletFile.getPath()) ? openWallet(walletConfig, rpcBindPort) : createWallet(walletConfig, rpcBindPort);
|
||||
System.out.println("Monero wallet path: " + wallet.getPath());
|
||||
System.out.println("Monero wallet address: " + wallet.getPrimaryAddress());
|
||||
System.out.println("Monero wallet uri: " + ((MoneroWalletRpc) wallet).getRpcConnection().getUri());
|
||||
wallet.sync(); // blocking
|
||||
connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both
|
||||
wallet.save();
|
||||
System.out.println("Loaded wallet balance: " + wallet.getBalance(0));
|
||||
System.out.println("Loaded wallet unlocked balance: " + wallet.getUnlockedBalance(0));
|
||||
|
||||
// update wallet connections on change
|
||||
connectionsService.addListener(newConnection -> {
|
||||
setWalletDaemonConnections(newConnection);
|
||||
});
|
||||
|
||||
// notify on balance changes
|
||||
wallet.addListener(new MoneroWalletListener() {
|
||||
@Override
|
||||
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
|
||||
notifyBalanceListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void backupWallets() {
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName(), 20);
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName() + ".keys", 20);
|
||||
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName() + ".address.txt", 20);
|
||||
}
|
||||
|
||||
private void setWalletDaemonConnections(MoneroRpcConnection connection) {
|
||||
log.info("Setting wallet daemon connections: " + (connection == null ? null : connection.getUri()));
|
||||
if (wallet != null) wallet.setDaemonConnection(connection);
|
||||
for (MoneroWallet multisigWallet : multisigWallets.values()) multisigWallet.setDaemonConnection(connection);
|
||||
}
|
||||
|
||||
private void notifyBalanceListeners() {
|
||||
for (XmrBalanceListener balanceListener : balanceListeners) {
|
||||
Coin balance;
|
||||
if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex());
|
||||
else balance = getAvailableConfirmedBalance();
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
balanceListener.onBalanceChanged(BigInteger.valueOf(balance.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void changeWalletPasswords(String oldPassword, String newPassword) {
|
||||
List<String> tradeIds = tradeManager.getTrades().stream().map(Trade::getId).collect(Collectors.toList());
|
||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size()));
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
wallet.changePassword(oldPassword, newPassword);
|
||||
wallet.save();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
for (String tradeId : tradeIds) {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
|
||||
if (multisigWallet == null) return;
|
||||
multisigWallet.changePassword(oldPassword, newPassword);
|
||||
multisigWallet.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||
} catch (InterruptedException e) {
|
||||
try { pool.shutdownNow(); }
|
||||
catch (Exception e2) { }
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeAllWallets() {
|
||||
|
||||
// collect wallets to shutdown
|
||||
List<MoneroWallet> openWallets = new ArrayList<MoneroWallet>();
|
||||
if (wallet != null) openWallets.add(wallet);
|
||||
for (String multisigWalletKey : multisigWallets.keySet()) {
|
||||
openWallets.add(multisigWallets.get(multisigWalletKey));
|
||||
}
|
||||
|
||||
// done if no open wallets
|
||||
if (openWallets.isEmpty()) return;
|
||||
|
||||
// close all wallets in parallel
|
||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, openWallets.size()));
|
||||
for (MoneroWallet openWallet : openWallets) {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
closeWallet(openWallet, true);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||
} catch (InterruptedException e) {
|
||||
pool.shutdownNow();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// clear wallets
|
||||
wallet = null;
|
||||
multisigWallets.clear();
|
||||
}
|
||||
|
||||
// ----------------------------- LEGACY APP -------------------------------
|
||||
|
||||
public XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) {
|
||||
var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE);
|
||||
if (!available.isPresent())
|
||||
return null;
|
||||
return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
|
||||
if (!available.isPresent()) return null;
|
||||
return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
|
||||
}
|
||||
|
||||
public XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||
MoneroSubaddress subaddress = wallet.createSubaddress(0);
|
||||
XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null);
|
||||
addressEntryList.addAddressEntry(entry);
|
||||
xmrAddressEntryList.addAddressEntry(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> offerId.equals(e.getOfferId()))
|
||||
.filter(e -> context == e.getContext())
|
||||
.findAny();
|
||||
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
||||
if (addressEntry.isPresent()) {
|
||||
return addressEntry.get();
|
||||
} else {
|
||||
// We try to use available and not yet used entries
|
||||
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext())
|
||||
.filter(e -> isSubaddressUnused(e.getSubaddressIndex()))
|
||||
.findAny();
|
||||
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext())
|
||||
.filter(e -> isSubaddressUnused(e.getSubaddressIndex())).findAny();
|
||||
if (emptyAvailableAddressEntry.isPresent()) {
|
||||
return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
||||
return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
||||
} else {
|
||||
return getNewAddressEntry(offerId, context);
|
||||
}
|
||||
@ -168,24 +449,17 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public Optional<XmrAddressEntry> getAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> offerId.equals(e.getOfferId()))
|
||||
.filter(e -> context == e.getContext())
|
||||
.findAny();
|
||||
return getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
||||
}
|
||||
|
||||
public void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) {
|
||||
Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> offerId.equals(e.getOfferId()))
|
||||
.filter(e -> context == e.getContext())
|
||||
.findAny();
|
||||
Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
||||
addressEntryOptional.ifPresent(e -> {
|
||||
log.info("swap addressEntry with address {} and offerId {} from context {} to available",
|
||||
e.getAddressString(), e.getOfferId(), context);
|
||||
addressEntryList.swapToAvailable(e);
|
||||
log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context);
|
||||
xmrAddressEntryList.swapToAvailable(e);
|
||||
saveAddressEntryList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void resetAddressEntriesForOpenOffer(String offerId) {
|
||||
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
||||
@ -195,54 +469,50 @@ public class XmrWalletService {
|
||||
|
||||
public void resetAddressEntriesForPendingTrade(String offerId) {
|
||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.MULTI_SIG);
|
||||
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases where a user cannot send the funds
|
||||
// to an external wallet directly in the last step of the trade, but the funds are in the Bisq wallet anyway and
|
||||
// the dealing with the external wallet is pure UI thing. The user can move the funds to the wallet and then
|
||||
// send out the funds to the external wallet. As this cleanup is a rare situation and most users do not use
|
||||
// the feature to send out the funds we prefer that strategy (if we keep the address entry it might cause
|
||||
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases
|
||||
// where a user cannot send the funds
|
||||
// to an external wallet directly in the last step of the trade, but the funds
|
||||
// are in the Bisq wallet anyway and
|
||||
// the dealing with the external wallet is pure UI thing. The user can move the
|
||||
// funds to the wallet and then
|
||||
// send out the funds to the external wallet. As this cleanup is a rare
|
||||
// situation and most users do not use
|
||||
// the feature to send out the funds we prefer that strategy (if we keep the
|
||||
// address entry it might cause
|
||||
// complications in some edge cases after a SPV resync).
|
||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||
}
|
||||
|
||||
private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> address.equals(e.getAddressString()))
|
||||
.filter(e -> context == e.getContext())
|
||||
.findAny();
|
||||
return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny();
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAvailableAddressEntries() {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntriesForTrade() {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.MULTI_SIG == addressEntry.getContext() ||
|
||||
XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext())
|
||||
.filter(addressEntry -> XmrAddressEntry.Context.MULTI_SIG == addressEntry.getContext() || XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntries(XmrAddressEntry.Context context) {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(addressEntry -> context == addressEntry.getContext())
|
||||
.collect(Collectors.toList());
|
||||
return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getFundedAvailableAddressEntries() {
|
||||
return getAvailableAddressEntries().stream()
|
||||
.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive())
|
||||
.collect(Collectors.toList());
|
||||
return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<XmrAddressEntry> getAddressEntryListAsImmutableList() {
|
||||
return addressEntryList.getAddressEntriesAsListImmutable();
|
||||
return xmrAddressEntryList.getAddressEntriesAsListImmutable();
|
||||
}
|
||||
|
||||
public boolean isSubaddressUnused(int subaddressIndex) {
|
||||
return subaddressIndex != 0 && getBalanceForSubaddress(subaddressIndex).value == 0;
|
||||
//return !wallet.getSubaddress(accountIndex, 0).isUsed(); // TODO: isUsed() does not include unconfirmed funds
|
||||
// return !wallet.getSubaddress(accountIndex, 0).isUsed(); // TODO: isUsed()
|
||||
// does not include unconfirmed funds
|
||||
}
|
||||
|
||||
public Coin getBalanceForSubaddress(int subaddressIndex) {
|
||||
@ -264,7 +534,6 @@ public class XmrWalletService {
|
||||
return Coin.valueOf(balance.longValueExact());
|
||||
}
|
||||
|
||||
|
||||
public Coin getAvailableConfirmedBalance() {
|
||||
return wallet != null ? Coin.valueOf(wallet.getUnlockedBalance(0).longValueExact()) : Coin.ZERO;
|
||||
}
|
||||
@ -289,133 +558,24 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public void saveAddressEntryList() {
|
||||
addressEntryList.requestPersistence();
|
||||
xmrAddressEntryList.requestPersistence();
|
||||
}
|
||||
|
||||
public List<MoneroTxWallet> getTransactions(boolean includeDead) {
|
||||
return wallet.getTxs(new MoneroTxQuery().setIsFailed(includeDead ? null : false));
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
|
||||
// collect wallets to shutdown
|
||||
List<MoneroWallet> openWallets = new ArrayList<MoneroWallet>();
|
||||
if (wallet != null) openWallets.add(wallet);
|
||||
for (String multisigWalletKey : multisigWallets.keySet()) {
|
||||
openWallets.add(multisigWallets.get(multisigWalletKey));
|
||||
}
|
||||
|
||||
// create shutdown threads
|
||||
List<Thread> threads = new ArrayList<Thread>();
|
||||
for (MoneroWallet openWallet : openWallets) {
|
||||
threads.add(new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try { walletsSetup.getWalletConfig().closeWallet(openWallet, true); }
|
||||
catch (Exception e) {
|
||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// run shutdown threads in parallel
|
||||
for (Thread thread : threads) thread.start();
|
||||
|
||||
// wait for all threads
|
||||
for (Thread thread : threads) {
|
||||
try { thread.join(); }
|
||||
catch (InterruptedException e) { e.printStackTrace(); }
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Withdrawal Send
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public String sendFunds(int fromAccountIndex,
|
||||
String toAddress,
|
||||
Coin receiverAmount,
|
||||
@SuppressWarnings("SameParameterValue") XmrAddressEntry.Context context,
|
||||
FutureCallback<MoneroTxWallet> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
|
||||
try {
|
||||
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(fromAccountIndex)
|
||||
.setAddress(toAddress)
|
||||
.setAmount(ParsingUtils.coinToAtomicUnits(receiverAmount))
|
||||
.setRelay(true));
|
||||
callback.onSuccess(tx);
|
||||
printTxs("sendFunds", tx);
|
||||
return tx.getHash();
|
||||
} catch (Exception e) {
|
||||
callback.onFailure(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// public String sendFunds(String fromAddress, String toAddress, Coin receiverAmount, Coin fee, @Nullable KeyParameter aesKey, @SuppressWarnings("SameParameterValue") AddressEntry.Context context,
|
||||
// FutureCallback<Transaction> callback) throws AddressFormatException, AddressEntryException, InsufficientMoneyException {
|
||||
// SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context);
|
||||
// Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
||||
// Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
|
||||
//
|
||||
// printTx("sendFunds", sendResult.tx);
|
||||
// return sendResult.tx.getTxId().toString();
|
||||
// }
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Create Tx
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||
try {
|
||||
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setDestinations(destinations)
|
||||
.setRelay(false)
|
||||
.setCanSplit(false));
|
||||
printTxs("XmrWalletService.createTx", tx);
|
||||
return tx;
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Util
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
public static void printTxs(String tracePrefix, MoneroTxWallet... txs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (MoneroTxWallet tx : txs) sb.append('\n' + tx.toString());
|
||||
log.info("\n" + tracePrefix + ":" + sb.toString());
|
||||
}
|
||||
|
||||
private void notifyBalanceListeners() {
|
||||
for (XmrBalanceListener balanceListener : balanceListeners) {
|
||||
Coin balance;
|
||||
if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) {
|
||||
balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex());
|
||||
} else {
|
||||
balance = getAvailableConfirmedBalance();
|
||||
}
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
balanceListener.onBalanceChanged(BigInteger.valueOf(balance.value));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@ -432,7 +592,8 @@ public class XmrWalletService {
|
||||
@Override
|
||||
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onSyncProgress(height, startHeight, endHeight, percentDone, message);
|
||||
}
|
||||
});
|
||||
@ -441,7 +602,8 @@ public class XmrWalletService {
|
||||
@Override
|
||||
public void onNewBlock(long height) {
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onNewBlock(height);
|
||||
}
|
||||
});
|
||||
@ -450,7 +612,8 @@ public class XmrWalletService {
|
||||
@Override
|
||||
public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onBalancesChanged(newBalance, newUnlockedBalance);
|
||||
}
|
||||
});
|
||||
@ -459,7 +622,8 @@ public class XmrWalletService {
|
||||
@Override
|
||||
public void onOutputReceived(MoneroOutputWallet output) {
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onOutputReceived(output);
|
||||
}
|
||||
});
|
||||
@ -468,7 +632,8 @@ public class XmrWalletService {
|
||||
@Override
|
||||
public void onOutputSpent(MoneroOutputWallet output) {
|
||||
UserThread.execute(new Runnable() {
|
||||
@Override public void run() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.onOutputSpent(output);
|
||||
}
|
||||
});
|
||||
|
@ -24,8 +24,7 @@ import bisq.core.notifications.MobileNotificationService;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -41,15 +40,15 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class TradeEvents {
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
private final TradeManager tradeManager;
|
||||
private final MobileNotificationService mobileNotificationService;
|
||||
|
||||
@Inject
|
||||
public TradeEvents(TradeManager tradeManager, KeyRing keyRing, MobileNotificationService mobileNotificationService) {
|
||||
public TradeEvents(TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider, MobileNotificationService mobileNotificationService) {
|
||||
this.tradeManager = tradeManager;
|
||||
this.mobileNotificationService = mobileNotificationService;
|
||||
this.pubKeyRing = keyRing.getPubKeyRing();
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
}
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
@ -74,19 +73,19 @@ public class TradeEvents {
|
||||
case DEPOSIT_PUBLISHED:
|
||||
break;
|
||||
case DEPOSIT_CONFIRMED:
|
||||
if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
|
||||
break;
|
||||
case FIAT_SENT:
|
||||
// We only notify the seller
|
||||
if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getSellerPubKeyRing()))
|
||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getSellerPubKeyRing()))
|
||||
msg = Res.get("account.notifications.trade.message.msg.started", shortId);
|
||||
break;
|
||||
case FIAT_RECEIVED:
|
||||
break;
|
||||
case PAYOUT_PUBLISHED:
|
||||
// We only notify the buyer
|
||||
if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
msg = Res.get("account.notifications.trade.message.msg.completed", shortId);
|
||||
break;
|
||||
case WITHDRAWN:
|
||||
|
@ -39,7 +39,7 @@ import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
import bisq.common.util.Tuple2;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
@ -65,7 +65,7 @@ public class CreateOfferService {
|
||||
private final TxFeeEstimationService txFeeEstimationService;
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final P2PService p2PService;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
private final User user;
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
@ -81,7 +81,7 @@ public class CreateOfferService {
|
||||
TxFeeEstimationService txFeeEstimationService,
|
||||
PriceFeedService priceFeedService,
|
||||
P2PService p2PService,
|
||||
PubKeyRing pubKeyRing,
|
||||
PubKeyRingProvider pubKeyRingProvider,
|
||||
User user,
|
||||
BtcWalletService btcWalletService,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
@ -90,7 +90,7 @@ public class CreateOfferService {
|
||||
this.txFeeEstimationService = txFeeEstimationService;
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.p2PService = p2PService;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
this.user = user;
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
@ -197,7 +197,7 @@ public class CreateOfferService {
|
||||
OfferPayload offerPayload = new OfferPayload(offerId,
|
||||
creationTime,
|
||||
makerAddress,
|
||||
pubKeyRing,
|
||||
pubKeyRingProvider.get(),
|
||||
OfferPayload.Direction.valueOf(direction.name()),
|
||||
priceAsLong,
|
||||
marketPriceMarginParam,
|
||||
|
@ -63,7 +63,7 @@ public class OfferFilter {
|
||||
this.filterManager = filterManager;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
|
||||
if (user != null) {
|
||||
if (user != null && user.getPaymentAccountsAsObservable() != null) {
|
||||
// If our accounts have changed we reset our myInsufficientTradeLimitCache as it depends on account data
|
||||
user.getPaymentAccountsAsObservable().addListener((SetChangeListener<PaymentAccount>) c ->
|
||||
myInsufficientTradeLimitCache.clear());
|
||||
|
@ -18,6 +18,7 @@
|
||||
package bisq.core.offer;
|
||||
|
||||
import bisq.core.api.CoreContext;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
@ -110,6 +111,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
private final KeyRing keyRing;
|
||||
private final User user;
|
||||
private final P2PService p2PService;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final XmrWalletService xmrWalletService;
|
||||
private final TradeWalletService tradeWalletService;
|
||||
@ -144,6 +146,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
KeyRing keyRing,
|
||||
User user,
|
||||
P2PService p2PService,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
BtcWalletService btcWalletService,
|
||||
XmrWalletService xmrWalletService,
|
||||
TradeWalletService tradeWalletService,
|
||||
@ -163,6 +166,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
this.keyRing = keyRing;
|
||||
this.user = user;
|
||||
this.p2PService = p2PService;
|
||||
this.connectionService = connectionService;
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
@ -751,8 +755,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow trade start if BitcoinJ is not fully synced (bisq issue #4764)
|
||||
if (!btcWalletService.isChainHeightSyncedWithinTolerance()) {
|
||||
// Don't allow trade start if Monero node is not fully synced
|
||||
if (!connectionService.isChainHeightSyncedWithinTolerance()) {
|
||||
errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced.";
|
||||
log.info(errorMessage);
|
||||
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
@ -47,7 +47,7 @@ import javax.annotation.Nullable;
|
||||
@Slf4j
|
||||
public abstract class SupportManager {
|
||||
protected final P2PService p2PService;
|
||||
protected final WalletsSetup walletsSetup;
|
||||
protected final CoreMoneroConnectionsService connectionService;
|
||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
@ -59,12 +59,11 @@ public abstract class SupportManager {
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public SupportManager(P2PService p2PService, WalletsSetup walletsSetup) {
|
||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService) {
|
||||
this.p2PService = p2PService;
|
||||
this.connectionService = connectionService;
|
||||
mailboxMessageService = p2PService.getMailboxMessageService();
|
||||
|
||||
this.walletsSetup = walletsSetup;
|
||||
|
||||
// We get first the message handler called then the onBootstrapped
|
||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
|
||||
@ -293,8 +292,8 @@ public abstract class SupportManager {
|
||||
private boolean isReady() {
|
||||
return allServicesInitialized &&
|
||||
p2PService.isBootstrapped() &&
|
||||
walletsSetup.isDownloadComplete() &&
|
||||
walletsSetup.hasSufficientPeersForBroadcast();
|
||||
connectionService.isDownloadComplete() &&
|
||||
connectionService.hasSufficientPeersForBroadcast();
|
||||
}
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
@ -39,7 +39,6 @@ import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
@ -111,7 +110,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
public DisputeManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService xmrWalletService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
@ -119,7 +118,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
DisputeListService<T> disputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, walletsSetup);
|
||||
super(p2PService, connectionService);
|
||||
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
@ -252,13 +251,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
});
|
||||
|
||||
walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.isDownloadComplete())
|
||||
connectionService.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (connectionService.isDownloadComplete())
|
||||
tryApplyMessages();
|
||||
});
|
||||
|
||||
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast())
|
||||
connectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (connectionService.hasSufficientPeersForBroadcast())
|
||||
tryApplyMessages();
|
||||
});
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
@ -95,7 +95,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
public ArbitrationManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
@ -103,7 +103,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
ArbitrationDisputeListService arbitrationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
@ -365,19 +365,19 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
cleanupRetryMap(uid);
|
||||
|
||||
// update multisig wallet
|
||||
// TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||
if (multisigWallet != null) {
|
||||
multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()));
|
||||
|
||||
// parse payout tx
|
||||
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
||||
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
||||
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
||||
}
|
||||
|
||||
// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs());
|
||||
// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx");
|
||||
// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet());
|
||||
|
||||
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
||||
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
||||
|
||||
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
||||
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
||||
requestPersistence();
|
||||
@ -436,7 +436,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
UUID.randomUUID().toString(),
|
||||
SupportType.ARBITRATION,
|
||||
payoutTx.getTxSet().getMultisigTxHex());
|
||||
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), contract.getArbitratorNodeAddress(), dispute.getTradeId(), response.getUid());
|
||||
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
||||
p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(),
|
||||
senderPubKeyRing,
|
||||
response,
|
||||
@ -644,8 +644,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
numAttempts++;
|
||||
payoutTx = multisigWallet.createTx(txConfig);
|
||||
} catch (MoneroError e) {
|
||||
System.out.println(e.toString());
|
||||
System.out.println(e.getStackTrace());
|
||||
// exception expected // TODO: better way of estimating fee?
|
||||
}
|
||||
}
|
||||
@ -716,7 +714,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// TODO (woodser): VERIFY PAYOUT TX AMOUNTS WHICH CONSIDERS FEE IF LONG TRADE, EXACT AMOUNT IF SHORT TRADE
|
||||
|
||||
|
||||
// if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not payout amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
||||
|
||||
// verify seller destination amount is payout amount - 1/2 tx costs
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
@ -77,7 +77,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||
public MediationManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
@ -85,7 +85,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||
MediationDisputeListService mediationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support.dispute.refund;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
@ -71,7 +71,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||
public RefundManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
XmrWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
@ -80,7 +80,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||
RefundDisputeListService refundDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
||||
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.support.traderchat;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.SupportManager;
|
||||
import bisq.core.support.SupportType;
|
||||
@ -31,6 +31,7 @@ import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -46,7 +47,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Singleton
|
||||
public class TraderChatManager extends SupportManager {
|
||||
private final TradeManager tradeManager;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -55,12 +56,12 @@ public class TraderChatManager extends SupportManager {
|
||||
|
||||
@Inject
|
||||
public TraderChatManager(P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
TradeManager tradeManager,
|
||||
PubKeyRing pubKeyRing) {
|
||||
super(p2PService, walletsSetup);
|
||||
PubKeyRingProvider pubKeyRingProvider) {
|
||||
super(p2PService, connectionService);
|
||||
this.tradeManager = tradeManager;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
}
|
||||
|
||||
|
||||
@ -82,7 +83,7 @@ public class TraderChatManager extends SupportManager {
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
||||
if (trade.getContract() != null) {
|
||||
return trade.getContract().getPeersNodeAddress(pubKeyRing);
|
||||
return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -93,7 +94,7 @@ public class TraderChatManager extends SupportManager {
|
||||
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
|
||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
||||
if (trade.getContract() != null) {
|
||||
return trade.getContract().getPeersPubKeyRing(pubKeyRing);
|
||||
return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -139,11 +140,13 @@ public class TraderChatManager extends SupportManager {
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onAllServicesInitialized() {
|
||||
super.onAllServicesInitialized();
|
||||
tryApplyMessages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSupportMessage(SupportMessage message) {
|
||||
if (canProcessMessage(message)) {
|
||||
log.info("Received {} with tradeId {} and uid {}",
|
||||
|
@ -1108,7 +1108,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Legacy arbitration is not handled anymore as not used anymore.
|
||||
// check for closed disputed case
|
||||
if (disputeState == DisputeState.DISPUTE_CLOSED) return false;
|
||||
|
||||
// In mediation case we check for the mediationResultState. As there are multiple sub-states we use ordinal.
|
||||
if (disputeState == DisputeState.MEDIATION_CLOSED) {
|
||||
|
@ -118,10 +118,9 @@ import javax.annotation.Nullable;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
|
||||
public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradeManager.class);
|
||||
|
||||
@ -283,6 +282,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
|
||||
onTradesChanged();
|
||||
|
||||
xmrWalletService.setTradeManager(this);
|
||||
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
||||
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
||||
.forEach(addressEntry -> {
|
||||
@ -1014,7 +1014,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
}
|
||||
|
||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||
xmrWalletService.deleteMultisigWallet(trade.getId());
|
||||
xmrWalletService.deleteMultisigWallet(trade.getId()); // TODO (woodser): don't delete multisig wallet until payout tx unlocked?
|
||||
requestPersistence();
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.MakerTrade;
|
||||
import bisq.core.trade.TakerTrade;
|
||||
@ -68,6 +69,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
||||
InitMultisigRequest request = (InitMultisigRequest) processModel.getTradeMessage();
|
||||
checkNotNull(request);
|
||||
checkTradeId(processModel.getOfferId(), request);
|
||||
XmrWalletService xmrWalletService = processModel.getProvider().getXmrWalletService();
|
||||
|
||||
System.out.println("PROCESS MULTISIG MESSAGE");
|
||||
System.out.println(request);
|
||||
@ -98,18 +100,18 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
||||
boolean updateParticipants = false;
|
||||
if (processModel.getPreparedMultisigHex() == null) {
|
||||
System.out.println("Preparing multisig wallet!");
|
||||
multisigWallet = processModel.getProvider().getXmrWalletService().createMultisigWallet(trade.getId());
|
||||
multisigWallet = xmrWalletService.createMultisigWallet(trade.getId());
|
||||
processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig());
|
||||
updateParticipants = true;
|
||||
} else {
|
||||
multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId());
|
||||
multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||
}
|
||||
|
||||
// make multisig if applicable
|
||||
TradingPeer[] peers = getMultisigPeers();
|
||||
if (processModel.getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) {
|
||||
System.out.println("Making multisig wallet!");
|
||||
MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, "abctesting123"); // TODO (woodser): move this to config
|
||||
MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)?
|
||||
processModel.setMadeMultisigHex(result.getMultisigHex());
|
||||
updateParticipants = true;
|
||||
}
|
||||
@ -117,7 +119,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
||||
// exchange multisig keys if applicable
|
||||
if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) {
|
||||
System.out.println("Exchanging multisig wallet!");
|
||||
multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), "abctesting123"); // TODO (woodser): move this to config
|
||||
multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword());
|
||||
processModel.setMultisigSetupComplete(true);
|
||||
}
|
||||
|
||||
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.trade.protocol.tasks.taker;
|
||||
|
||||
import bisq.core.btc.model.XmrAddressEntry;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
// TODO (woodser): rename this to TakerCreateFeeTx or rename TakerPublishFeeTx to TakerPublishReserveTradeTx for consistency
|
||||
@Slf4j
|
||||
public class TakerCreateFeeTx extends TradeTask {
|
||||
@SuppressWarnings({ "unused" })
|
||||
public TakerCreateFeeTx(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||
String id = processModel.getOffer().getId();
|
||||
XmrAddressEntry reservedForTradeAddressEntry = walletService.getOrCreateAddressEntry(id, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
||||
TradeWalletService tradeWalletService = processModel.getTradeWalletService();
|
||||
String feeReceiver = "52FnB7ABUrKJzVQRpbMNrqDFWbcKLjFUq8Rgek7jZEuB6WE2ZggXaTf4FK6H8gQymvSrruHHrEuKhMN3qTMiBYzREKsmRKM"; // TODO (woodser): don't hardcode
|
||||
|
||||
// pay trade fee to reserve trade
|
||||
MoneroTxWallet tx = tradeWalletService.createXmrTradingFeeTx(
|
||||
reservedForTradeAddressEntry.getAddressString(),
|
||||
Coin.valueOf(processModel.getFundsNeededForTradeAsLong()),
|
||||
trade.getTakerFee(),
|
||||
trade.getTxFee(),
|
||||
feeReceiver,
|
||||
false);
|
||||
|
||||
trade.setTakerFeeTxId(tx.getHash());
|
||||
processModel.setTakeOfferFeeTx(tx);
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
|
||||
package bisq.core.trade.txproof.xmr;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
@ -76,7 +76,7 @@ public class XmrTxProofService implements AssetTxProofService {
|
||||
private final MediationManager mediationManager;
|
||||
private final RefundManager refundManager;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final Socks5ProxyProvider socks5ProxyProvider;
|
||||
private final Map<String, XmrTxProofRequestsPerTrade> servicesByTradeId = new HashMap<>();
|
||||
private AutoConfirmSettings autoConfirmSettings;
|
||||
@ -101,7 +101,7 @@ public class XmrTxProofService implements AssetTxProofService {
|
||||
MediationManager mediationManager,
|
||||
RefundManager refundManager,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
Socks5ProxyProvider socks5ProxyProvider) {
|
||||
this.filterManager = filterManager;
|
||||
this.preferences = preferences;
|
||||
@ -111,7 +111,7 @@ public class XmrTxProofService implements AssetTxProofService {
|
||||
this.mediationManager = mediationManager;
|
||||
this.refundManager = refundManager;
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.socks5ProxyProvider = socks5ProxyProvider;
|
||||
}
|
||||
|
||||
@ -289,32 +289,32 @@ public class XmrTxProofService implements AssetTxProofService {
|
||||
|
||||
private BooleanProperty isXmrBlockDownloadComplete() {
|
||||
BooleanProperty result = new SimpleBooleanProperty();
|
||||
if (walletsSetup.isDownloadComplete()) {
|
||||
if (connectionService.isDownloadComplete()) {
|
||||
result.set(true);
|
||||
} else {
|
||||
xmrBlockListener = (observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.isDownloadComplete()) {
|
||||
walletsSetup.downloadPercentageProperty().removeListener(xmrBlockListener);
|
||||
if (connectionService.isDownloadComplete()) {
|
||||
connectionService.downloadPercentageProperty().removeListener(xmrBlockListener);
|
||||
result.set(true);
|
||||
}
|
||||
};
|
||||
walletsSetup.downloadPercentageProperty().addListener(xmrBlockListener);
|
||||
connectionService.downloadPercentageProperty().addListener(xmrBlockListener);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BooleanProperty hasSufficientXmrPeers() {
|
||||
BooleanProperty result = new SimpleBooleanProperty();
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast()) {
|
||||
if (connectionService.hasSufficientPeersForBroadcast()) {
|
||||
result.set(true);
|
||||
} else {
|
||||
xmrPeersListener = (observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast()) {
|
||||
walletsSetup.numPeersProperty().removeListener(xmrPeersListener);
|
||||
if (connectionService.hasSufficientPeersForBroadcast()) {
|
||||
connectionService.numPeersProperty().removeListener(xmrPeersListener);
|
||||
result.set(true);
|
||||
}
|
||||
};
|
||||
walletsSetup.numPeersProperty().addListener(xmrPeersListener);
|
||||
connectionService.numPeersProperty().addListener(xmrPeersListener);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -399,6 +399,7 @@ public class User implements PersistedDataHost {
|
||||
return userPayload.getPaymentAccounts();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ObservableSet<PaymentAccount> getPaymentAccountsAsObservable() {
|
||||
return paymentAccountsAsObservable;
|
||||
}
|
||||
|
@ -162,8 +162,8 @@ public class AccountAgeWitnessServiceTest {
|
||||
|
||||
@Test
|
||||
public void testArbitratorSignWitness() {
|
||||
KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1));
|
||||
KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2));
|
||||
KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1), null, true);
|
||||
KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2), null, true);
|
||||
|
||||
// Setup dispute for arbitrator to sign both sides
|
||||
List<Dispute> disputes = new ArrayList<>();
|
||||
@ -278,9 +278,9 @@ public class AccountAgeWitnessServiceTest {
|
||||
public void testArbitratorSignDummyWitness() throws CryptoException {
|
||||
ECKey arbitratorKey = new ECKey();
|
||||
// Init 2 user accounts
|
||||
var user1KeyRing = new KeyRing(new KeyStorage(dir1));
|
||||
var user2KeyRing = new KeyRing(new KeyStorage(dir2));
|
||||
var user3KeyRing = new KeyRing(new KeyStorage(dir3));
|
||||
var user1KeyRing = new KeyRing(new KeyStorage(dir1), null, true);
|
||||
var user2KeyRing = new KeyRing(new KeyStorage(dir2), null, true);
|
||||
var user3KeyRing = new KeyRing(new KeyStorage(dir3), null, true);
|
||||
var pubKeyRing1 = user1KeyRing.getPubKeyRing();
|
||||
var pubKeyRing2 = user2KeyRing.getPubKeyRing();
|
||||
var pubKeyRing3 = user3KeyRing.getPubKeyRing();
|
||||
|
@ -49,7 +49,7 @@ public class EncryptionTest {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
dir.mkdir();
|
||||
KeyStorage keyStorage = new KeyStorage(dir);
|
||||
keyRing = new KeyRing(keyStorage);
|
||||
keyRing = new KeyRing(keyStorage, null, true);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -51,7 +51,7 @@ public class SigTest {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
dir.mkdir();
|
||||
KeyStorage keyStorage = new KeyStorage(dir);
|
||||
keyRing = new KeyRing(keyStorage);
|
||||
keyRing = new KeyRing(keyStorage, null, true);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -60,6 +60,7 @@ public class OpenOfferManagerTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
offerBookService,
|
||||
null,
|
||||
null,
|
||||
@ -106,6 +107,7 @@ public class OpenOfferManagerTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
offerBookService,
|
||||
null,
|
||||
null,
|
||||
@ -146,6 +148,7 @@ public class OpenOfferManagerTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
offerBookService,
|
||||
null,
|
||||
null,
|
||||
|
64
daemon/src/main/java/bisq/daemon/app/ConsoleInput.java
Normal file
64
daemon/src/main/java/bisq/daemon/app/ConsoleInput.java
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.app;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* A cancellable console input reader.
|
||||
* Derived from https://www.javaspecialists.eu/archive/Issue153-Timeout-on-Console-Input.html
|
||||
*/
|
||||
public class ConsoleInput {
|
||||
private final int tries;
|
||||
private final int timeout;
|
||||
private final TimeUnit unit;
|
||||
private Future<String> future;
|
||||
|
||||
public ConsoleInput(int tries, int timeout, TimeUnit unit) {
|
||||
this.tries = tries;
|
||||
this.timeout = timeout;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (future != null)
|
||||
future.cancel(true);
|
||||
}
|
||||
|
||||
public String readLine() throws InterruptedException {
|
||||
ExecutorService ex = Executors.newSingleThreadExecutor();
|
||||
String input = null;
|
||||
try {
|
||||
for (int i = 0; i < tries; i++) {
|
||||
future = ex.submit(new ConsoleInputReadTask());
|
||||
try {
|
||||
input = future.get(timeout, unit);
|
||||
break;
|
||||
} catch (ExecutionException e) {
|
||||
e.getCause().printStackTrace();
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
} finally {
|
||||
future = null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ex.shutdownNow();
|
||||
}
|
||||
return input;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.app;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ConsoleInputReadTask implements Callable<String> {
|
||||
public String call() throws IOException {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
|
||||
log.debug("ConsoleInputReadTask run() called.");
|
||||
String input;
|
||||
do {
|
||||
try {
|
||||
// wait until we have data to complete a readLine()
|
||||
while (!br.ready()) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
// readline will always block until an input exists.
|
||||
input = br.readLine();
|
||||
} catch (InterruptedException e) {
|
||||
log.debug("ConsoleInputReadTask() cancelled");
|
||||
return null;
|
||||
}
|
||||
} while ("".equals(input));
|
||||
return input;
|
||||
}
|
||||
}
|
@ -19,21 +19,24 @@ package bisq.daemon.app;
|
||||
|
||||
import bisq.core.app.HavenoHeadlessAppMain;
|
||||
import bisq.core.app.HavenoSetup;
|
||||
import bisq.core.api.AccountServiceListener;
|
||||
import bisq.core.app.CoreModule;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.AppModule;
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import java.io.Console;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import bisq.daemon.grpc.GrpcServer;
|
||||
|
||||
@Slf4j
|
||||
@ -61,7 +64,6 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
|
||||
@Override
|
||||
protected void launchApplication() {
|
||||
headlessApp = new HavenoDaemon();
|
||||
|
||||
UserThread.execute(this::onApplicationLaunched);
|
||||
}
|
||||
|
||||
@ -101,15 +103,116 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet
|
||||
@Override
|
||||
protected void onApplicationStarted() {
|
||||
super.onApplicationStarted();
|
||||
|
||||
grpcServer = injector.getInstance(GrpcServer.class);
|
||||
grpcServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gracefulShutDown(ResultHandler resultHandler) {
|
||||
super.gracefulShutDown(resultHandler);
|
||||
if (grpcServer != null) grpcServer.shutdown(); // could be null if application attempted to shutdown early
|
||||
}
|
||||
|
||||
grpcServer.shutdown();
|
||||
/**
|
||||
* Start the grpcServer to allow logging in remotely.
|
||||
*/
|
||||
@Override
|
||||
protected boolean loginAccount() {
|
||||
boolean opened = super.loginAccount();
|
||||
|
||||
// Start rpc server in case login is coming in from rpc
|
||||
grpcServer = injector.getInstance(GrpcServer.class);
|
||||
grpcServer.start();
|
||||
|
||||
if (!opened) {
|
||||
// Nonblocking, we need to stop if the login occurred through rpc.
|
||||
// TODO: add a mode to mask password
|
||||
ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
|
||||
Thread t = new Thread(() -> {
|
||||
interactiveLogin(reader);
|
||||
});
|
||||
t.start();
|
||||
|
||||
// Handle asynchronous account opens.
|
||||
// Will need to also close and reopen account.
|
||||
AccountServiceListener accountListener = new AccountServiceListener() {
|
||||
@Override public void onAccountCreated() { onLogin(); }
|
||||
@Override public void onAccountOpened() { onLogin(); }
|
||||
private void onLogin() {
|
||||
log.info("Logged in successfully");
|
||||
reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread
|
||||
}
|
||||
};
|
||||
accountService.addListener(accountListener);
|
||||
|
||||
try {
|
||||
// Wait until interactive login or rpc. Check one more time if account is open to close race condition.
|
||||
if (!accountService.isAccountOpen()) {
|
||||
log.info("Interactive login required");
|
||||
t.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
accountService.removeListener(accountListener);
|
||||
opened = accountService.isAccountOpen();
|
||||
}
|
||||
|
||||
return opened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks user for login. TODO: Implement in the desktop app.
|
||||
* @return True if user logged in interactively.
|
||||
*/
|
||||
protected boolean interactiveLogin(ConsoleInput reader) {
|
||||
Console console = System.console();
|
||||
if (console == null) {
|
||||
// The ConsoleInput class reads from system.in, can wait for input without a console.
|
||||
log.info("No console available, account must be opened through rpc");
|
||||
try {
|
||||
// If user logs in through rpc, the reader will be interrupted through the event.
|
||||
reader.readLine();
|
||||
} catch (InterruptedException | CancellationException ex) {
|
||||
log.info("Reader interrupted, continuing startup");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String openedOrCreated = "Account unlocked\n";
|
||||
boolean accountExists = accountService.accountExists();
|
||||
while (!accountService.isAccountOpen()) {
|
||||
try {
|
||||
if (accountExists) {
|
||||
try {
|
||||
// readPassword will not return until the user inputs something
|
||||
// which is not suitable if we are waiting for rpc call which
|
||||
// could login the account. Must be able to interrupt the read.
|
||||
//new String(console.readPassword("Password:"));
|
||||
System.out.printf("Password:\n");
|
||||
String password = reader.readLine();
|
||||
accountService.openAccount(password);
|
||||
} catch (IncorrectPasswordException ipe) {
|
||||
System.out.printf("Incorrect password\n");
|
||||
}
|
||||
} else {
|
||||
System.out.printf("Creating a new account\n");
|
||||
System.out.printf("Password:\n");
|
||||
String password = reader.readLine();
|
||||
System.out.printf("Confirm:\n");
|
||||
String passwordConfirm = reader.readLine();
|
||||
if (password.equals(passwordConfirm)) {
|
||||
accountService.createAccount(password);
|
||||
openedOrCreated = "Account created\n";
|
||||
} else {
|
||||
System.out.printf("Passwords did not match\n");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.debug(ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
System.out.printf(openedOrCreated);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
286
daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java
Normal file
286
daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java
Normal file
@ -0,0 +1,286 @@
|
||||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Haveno is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package bisq.daemon.grpc;
|
||||
|
||||
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
|
||||
import static bisq.proto.grpc.AccountGrpc.getAccountExistsMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getBackupAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getChangePasswordMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getCloseAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getCreateAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getDeleteAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getIsAccountOpenMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getOpenAccountMethod;
|
||||
import static bisq.proto.grpc.AccountGrpc.getRestoreAccountMethod;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import bisq.common.crypto.IncorrectPasswordException;
|
||||
import bisq.core.api.CoreApi;
|
||||
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
|
||||
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
|
||||
import bisq.proto.grpc.AccountExistsReply;
|
||||
import bisq.proto.grpc.AccountExistsRequest;
|
||||
import bisq.proto.grpc.AccountGrpc.AccountImplBase;
|
||||
import bisq.proto.grpc.BackupAccountReply;
|
||||
import bisq.proto.grpc.BackupAccountRequest;
|
||||
import bisq.proto.grpc.ChangePasswordReply;
|
||||
import bisq.proto.grpc.ChangePasswordRequest;
|
||||
import bisq.proto.grpc.CloseAccountReply;
|
||||
import bisq.proto.grpc.CloseAccountRequest;
|
||||
import bisq.proto.grpc.CreateAccountReply;
|
||||
import bisq.proto.grpc.CreateAccountRequest;
|
||||
import bisq.proto.grpc.DeleteAccountReply;
|
||||
import bisq.proto.grpc.DeleteAccountRequest;
|
||||
import bisq.proto.grpc.IsAccountOpenReply;
|
||||
import bisq.proto.grpc.IsAccountOpenRequest;
|
||||
import bisq.proto.grpc.IsAppInitializedReply;
|
||||
import bisq.proto.grpc.IsAppInitializedRequest;
|
||||
import bisq.proto.grpc.OpenAccountReply;
|
||||
import bisq.proto.grpc.OpenAccountRequest;
|
||||
import bisq.proto.grpc.RestoreAccountReply;
|
||||
import bisq.proto.grpc.RestoreAccountRequest;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@VisibleForTesting
|
||||
@Slf4j
|
||||
public class GrpcAccountService extends AccountImplBase {
|
||||
|
||||
private final CoreApi coreApi;
|
||||
private final GrpcExceptionHandler exceptionHandler;
|
||||
|
||||
private ByteArrayOutputStream restoreStream; // in memory stream for restoring account
|
||||
|
||||
@Inject
|
||||
public GrpcAccountService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||
this.coreApi = coreApi;
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accountExists(AccountExistsRequest req, StreamObserver<AccountExistsReply> responseObserver) {
|
||||
try {
|
||||
var reply = AccountExistsReply.newBuilder()
|
||||
.setAccountExists(coreApi.accountExists())
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isAccountOpen(IsAccountOpenRequest req, StreamObserver<IsAccountOpenReply> responseObserver) {
|
||||
try {
|
||||
var reply = IsAccountOpenReply.newBuilder()
|
||||
.setIsAccountOpen(coreApi.isAccountOpen())
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAccount(CreateAccountRequest req, StreamObserver<CreateAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.createAccount(req.getPassword());
|
||||
var reply = CreateAccountReply.newBuilder()
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openAccount(OpenAccountRequest req, StreamObserver<OpenAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.openAccount(req.getPassword());
|
||||
var reply = OpenAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
if (cause instanceof IncorrectPasswordException) cause = new IllegalStateException(cause);
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isAppInitialized(IsAppInitializedRequest req, StreamObserver<IsAppInitializedReply> responseObserver) {
|
||||
try {
|
||||
var reply = IsAppInitializedReply.newBuilder().setIsAppInitialized(coreApi.isAppInitialized()).build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassword(ChangePasswordRequest req, StreamObserver<ChangePasswordReply> responseObserver) {
|
||||
try {
|
||||
coreApi.changePassword(req.getPassword());
|
||||
var reply = ChangePasswordReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeAccount(CloseAccountRequest req, StreamObserver<CloseAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.closeAccount();
|
||||
var reply = CloseAccountReply.newBuilder()
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccount(DeleteAccountRequest req, StreamObserver<DeleteAccountReply> responseObserver) {
|
||||
try {
|
||||
coreApi.deleteAccount(() -> {
|
||||
var reply = DeleteAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backupAccount(BackupAccountRequest req, StreamObserver<BackupAccountReply> responseObserver) {
|
||||
|
||||
// Send in large chunks to reduce unnecessary overhead. Typical backup will not be more than a few MB.
|
||||
// From current testing it appears that client gRPC-web is slow in processing the bytes on download.
|
||||
try {
|
||||
int bufferSize = 1024 * 1024 * 8;
|
||||
coreApi.backupAccount(bufferSize, (stream) -> {
|
||||
try {
|
||||
log.info("Sending bytes in chunks of: " + bufferSize);
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int length;
|
||||
int total = 0;
|
||||
while ((length = stream.read(buffer, 0, bufferSize)) != -1) {
|
||||
total += length;
|
||||
var reply = BackupAccountReply.newBuilder()
|
||||
.setZipBytes(ByteString.copyFrom(buffer, 0, length))
|
||||
.build();
|
||||
responseObserver.onNext(reply);
|
||||
}
|
||||
log.info("Completed backup account total sent: " + total);
|
||||
stream.close();
|
||||
responseObserver.onCompleted();
|
||||
} catch (Exception ex) {
|
||||
exceptionHandler.handleException(log, ex, responseObserver);
|
||||
}
|
||||
}, (ex) -> exceptionHandler.handleException(log, ex, responseObserver));
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreAccount(RestoreAccountRequest req, StreamObserver<RestoreAccountReply> responseObserver) {
|
||||
try {
|
||||
// Fail fast since uploading and processing bytes takes resources.
|
||||
if (coreApi.accountExists()) throw new IllegalStateException("Cannot restore account if there is an existing account");
|
||||
|
||||
// If the entire zip is in memory, no need to write to disk.
|
||||
// Restore the account directly from the zip stream.
|
||||
if (!req.getHasMore() && req.getOffset() == 0) {
|
||||
var inputStream = req.getZipBytes().newInput();
|
||||
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} else {
|
||||
if (req.getOffset() == 0) {
|
||||
log.info("RestoreAccount starting new chunked zip");
|
||||
restoreStream = new ByteArrayOutputStream((int) req.getTotalLength());
|
||||
}
|
||||
if (restoreStream.size() != req.getOffset()) {
|
||||
log.warn("Stream offset doesn't match current position");
|
||||
IllegalStateException cause = new IllegalStateException("Stream offset doesn't match current position");
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
} else {
|
||||
log.info("RestoreAccount writing chunk size " + req.getZipBytes().size());
|
||||
req.getZipBytes().writeTo(restoreStream);
|
||||
}
|
||||
|
||||
if (!req.getHasMore()) {
|
||||
var inputStream = new ByteArrayInputStream(restoreStream.toByteArray());
|
||||
restoreStream.close();
|
||||
restoreStream = null;
|
||||
coreApi.restoreAccount(inputStream, 1024 * 64, () -> {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted(); // reply after shutdown
|
||||
});
|
||||
} else {
|
||||
var reply = RestoreAccountReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
}
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
}
|
||||
}
|
||||
|
||||
final ServerInterceptor[] interceptors() {
|
||||
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
|
||||
return rateMeteringInterceptor.map(serverInterceptor ->
|
||||
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
|
||||
}
|
||||
|
||||
final Optional<ServerInterceptor> rateMeteringInterceptor() {
|
||||
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||
new HashMap<>() {{
|
||||
put(getAccountExistsMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getBackupAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
|
||||
put(getChangePasswordMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getCloseAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getCreateAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getDeleteAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getIsAccountOpenMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getOpenAccountMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getRestoreAccountMethod().getFullMethodName(), new GrpcCallRateMeter(5, SECONDS));
|
||||
}}
|
||||
)));
|
||||
}
|
||||
}
|
@ -42,9 +42,9 @@ 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 bisq.proto.grpc.UrlConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -84,7 +84,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
public void removeConnection(RemoveConnectionRequest request,
|
||||
StreamObserver<RemoveConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.removeMoneroConnection(validateUri(request.getUri()));
|
||||
coreApi.removeMoneroConnection(validateUri(request.getUrl()));
|
||||
return RemoveConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
@ -93,7 +93,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
public void getConnection(GetConnectionRequest request,
|
||||
StreamObserver<GetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
UriConnection replyConnection = toUriConnection(coreApi.getMoneroConnection());
|
||||
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection());
|
||||
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
@ -107,8 +107,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
StreamObserver<GetConnectionsReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
List<MoneroRpcConnection> connections = coreApi.getMoneroConnections();
|
||||
List<UriConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
|
||||
List<UrlConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
|
||||
return GetConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
|
||||
});
|
||||
}
|
||||
@ -117,8 +117,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
public void setConnection(SetConnectionRequest request,
|
||||
StreamObserver<SetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
if (request.getUri() != null && !request.getUri().isEmpty())
|
||||
coreApi.setMoneroConnection(validateUri(request.getUri()));
|
||||
if (request.getUrl() != null && !request.getUrl().isEmpty())
|
||||
coreApi.setMoneroConnection(validateUri(request.getUrl()));
|
||||
else if (request.hasConnection())
|
||||
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
|
||||
@ -131,7 +131,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
StreamObserver<CheckConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
|
||||
UriConnection replyConnection = toUriConnection(connection);
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
@ -145,8 +145,8 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
StreamObserver<CheckConnectionsReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
List<MoneroRpcConnection> connections = coreApi.checkMoneroConnections();
|
||||
List<UriConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList());
|
||||
List<UrlConnection> replyConnections = connections.stream()
|
||||
.map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList());
|
||||
return CheckConnectionsReply.newBuilder().addAllConnections(replyConnections).build();
|
||||
});
|
||||
}
|
||||
@ -176,7 +176,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
|
||||
UriConnection replyConnection = toUriConnection(connection);
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
@ -211,43 +211,40 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase {
|
||||
}
|
||||
|
||||
|
||||
private static UriConnection toUriConnection(MoneroRpcConnection rpcConnection) {
|
||||
private static UrlConnection toUrlConnection(MoneroRpcConnection rpcConnection) {
|
||||
if (rpcConnection == null) return null;
|
||||
return UriConnection.newBuilder()
|
||||
.setUri(rpcConnection.getUri())
|
||||
return UrlConnection.newBuilder()
|
||||
.setUrl(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 UrlConnection.AuthenticationStatus toAuthenticationStatus(Boolean authenticated) {
|
||||
if (authenticated == null) return UrlConnection.AuthenticationStatus.NO_AUTHENTICATION;
|
||||
else if (authenticated) return UrlConnection.AuthenticationStatus.AUTHENTICATED;
|
||||
else return UrlConnection.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 UrlConnection.OnlineStatus toOnlineStatus(Boolean online) {
|
||||
if (online == null) return UrlConnection.OnlineStatus.UNKNOWN;
|
||||
else if (online) return UrlConnection.OnlineStatus.ONLINE;
|
||||
else return UrlConnection.OnlineStatus.OFFLINE;
|
||||
}
|
||||
|
||||
private static MoneroRpcConnection toMoneroRpcConnection(UriConnection uriConnection) throws URISyntaxException {
|
||||
private static MoneroRpcConnection toMoneroRpcConnection(UrlConnection uriConnection) throws MalformedURLException {
|
||||
if (uriConnection == null) return null;
|
||||
return new MoneroRpcConnection(
|
||||
validateUri(uriConnection.getUri()),
|
||||
validateUri(uriConnection.getUrl()),
|
||||
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 validateUri(String url) throws MalformedURLException {
|
||||
if (url.isEmpty()) throw new IllegalArgumentException("URL is required");
|
||||
return new URL(url).toString(); // validate and return
|
||||
}
|
||||
|
||||
private static String nullIfEmpty(String value) {
|
||||
|
@ -49,6 +49,7 @@ public class GrpcServer {
|
||||
public GrpcServer(CoreContext coreContext,
|
||||
Config config,
|
||||
PasswordAuthInterceptor passwordAuthInterceptor,
|
||||
GrpcAccountService accountService,
|
||||
GrpcDisputeAgentsService disputeAgentsService,
|
||||
GrpcHelpService helpService,
|
||||
GrpcOffersService offersService,
|
||||
@ -63,6 +64,7 @@ public class GrpcServer {
|
||||
GrpcMoneroConnectionsService moneroConnectionsService) {
|
||||
this.server = ServerBuilder.forPort(config.apiPort)
|
||||
.executor(UserThread.getExecutor())
|
||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
||||
.addService(interceptForward(helpService, helpService.interceptors()))
|
||||
.addService(interceptForward(offersService, offersService.interceptors()))
|
||||
|
@ -48,9 +48,14 @@ class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase {
|
||||
StreamObserver<StopReply> responseObserver) {
|
||||
try {
|
||||
log.info("Shutdown request received.");
|
||||
HavenoHeadlessApp.setOnGracefulShutDownHandler(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
var reply = StopReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
}
|
||||
});
|
||||
UserThread.runAfter(HavenoHeadlessApp.getShutDownHandler(), 500, MILLISECONDS);
|
||||
} catch (Throwable cause) {
|
||||
exceptionHandler.handleException(log, cause, responseObserver);
|
||||
|
@ -40,9 +40,9 @@ import bisq.desktop.util.GUIUtil;
|
||||
import bisq.core.account.sign.SignedWitnessService;
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.app.HavenoSetup;
|
||||
import bisq.core.btc.nodes.LocalBitcoinNode;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.locale.CryptoCurrency;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
@ -53,7 +53,6 @@ import bisq.core.payment.AliPayAccount;
|
||||
import bisq.core.payment.AmazonGiftCardAccount;
|
||||
import bisq.core.payment.CryptoCurrencyAccount;
|
||||
import bisq.core.payment.RevolutAccount;
|
||||
import bisq.core.payment.payload.AssetsAccountPayload;
|
||||
import bisq.core.presentation.BalancePresentation;
|
||||
import bisq.core.presentation.SupportTicketsPresentation;
|
||||
import bisq.core.presentation.TradePresentation;
|
||||
@ -109,7 +108,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener {
|
||||
private final HavenoSetup bisqSetup;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final User user;
|
||||
private final BalancePresentation balancePresentation;
|
||||
private final TradePresentation tradePresentation;
|
||||
@ -140,7 +139,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
private final DoubleProperty combinedSyncProgress = new SimpleDoubleProperty(-1);
|
||||
private final BooleanProperty isSplashScreenRemoved = new SimpleBooleanProperty();
|
||||
private final StringProperty footerVersionInfo = new SimpleStringProperty();
|
||||
private Timer checkNumberOfBtcPeersTimer;
|
||||
private Timer checkNumberOfXmrPeersTimer;
|
||||
private Timer checkNumberOfP2pNetworkPeersTimer;
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private MonadicBinding<Boolean> tradesAndUIReady;
|
||||
@ -153,7 +152,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
|
||||
@Inject
|
||||
public MainViewModel(HavenoSetup bisqSetup,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
BtcWalletService btcWalletService,
|
||||
User user,
|
||||
BalancePresentation balancePresentation,
|
||||
@ -178,7 +177,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
TorNetworkSettingsWindow torNetworkSettingsWindow,
|
||||
CorruptedStorageFileHandler corruptedStorageFileHandler) {
|
||||
this.bisqSetup = bisqSetup;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.user = user;
|
||||
this.balancePresentation = balancePresentation;
|
||||
this.tradePresentation = tradePresentation;
|
||||
@ -258,7 +257,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
});
|
||||
|
||||
setupP2PNumPeersWatcher();
|
||||
setupBtcNumPeersWatcher();
|
||||
setupXmrNumPeersWatcher();
|
||||
|
||||
marketPricePresentation.setup();
|
||||
accountPresentation.setup();
|
||||
@ -509,19 +508,19 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
});
|
||||
}
|
||||
|
||||
private void setupBtcNumPeersWatcher() {
|
||||
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
private void setupXmrNumPeersWatcher() {
|
||||
connectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
int numPeers = (int) newValue;
|
||||
if ((int) oldValue > 0 && numPeers == 0) {
|
||||
if (checkNumberOfBtcPeersTimer != null)
|
||||
checkNumberOfBtcPeersTimer.stop();
|
||||
if (checkNumberOfXmrPeersTimer != null)
|
||||
checkNumberOfXmrPeersTimer.stop();
|
||||
|
||||
checkNumberOfBtcPeersTimer = UserThread.runAfter(() -> {
|
||||
checkNumberOfXmrPeersTimer = UserThread.runAfter(() -> {
|
||||
// check again numPeers
|
||||
if (walletsSetup.numPeersProperty().get() == 0) {
|
||||
if (connectionService.numPeersProperty().get() == 0) {
|
||||
if (localBitcoinNode.shouldBeUsed())
|
||||
getWalletServiceErrorMsg().set(
|
||||
Res.get("mainView.networkWarning.localhostBitcoinLost",
|
||||
Res.get("mainView.networkWarning.localhostBitcoinLost", // TODO: update error message for XMR
|
||||
Res.getBaseCurrencyName().toLowerCase()));
|
||||
else
|
||||
getWalletServiceErrorMsg().set(
|
||||
@ -532,8 +531,8 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
}
|
||||
}, 5);
|
||||
} else if ((int) oldValue == 0 && numPeers > 0) {
|
||||
if (checkNumberOfBtcPeersTimer != null)
|
||||
checkNumberOfBtcPeersTimer.stop();
|
||||
if (checkNumberOfXmrPeersTimer != null)
|
||||
checkNumberOfXmrPeersTimer.stop();
|
||||
getWalletServiceErrorMsg().set(null);
|
||||
}
|
||||
});
|
||||
|
@ -33,9 +33,8 @@ import bisq.desktop.util.Layout;
|
||||
import bisq.desktop.util.validation.PasswordValidator;
|
||||
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
import bisq.core.crypto.ScryptUtil;
|
||||
import bisq.core.locale.Res;
|
||||
|
||||
import bisq.common.crypto.ScryptUtil;
|
||||
import bisq.common.util.Tuple4;
|
||||
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
|
@ -61,7 +61,6 @@ import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepo
|
||||
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerSendsInputsForDepositTxResponse;
|
||||
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs;
|
||||
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx;
|
||||
import bisq.core.trade.protocol.tasks.taker.TakerCreateFeeTx;
|
||||
import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse;
|
||||
import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx;
|
||||
import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment;
|
||||
@ -119,7 +118,6 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
FXCollections.observableArrayList(Arrays.asList(
|
||||
ApplyFilter.class,
|
||||
TakerVerifyMakerFeePayment.class,
|
||||
TakerCreateFeeTx.class, // TODO (woodser): rename to TakerCreateFeeTx
|
||||
SellerAsTakerCreatesDepositTxInputs.class,
|
||||
|
||||
TakerProcessesInputsForDepositTxResponse.class,
|
||||
@ -182,7 +180,6 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
FXCollections.observableArrayList(Arrays.asList(
|
||||
ApplyFilter.class,
|
||||
TakerVerifyMakerFeePayment.class,
|
||||
TakerCreateFeeTx.class,
|
||||
BuyerAsTakerCreatesDepositTxInputs.class,
|
||||
|
||||
TakerProcessesInputsForDepositTxResponse.class,
|
||||
|
@ -24,7 +24,7 @@ import bisq.core.support.dispute.refund.RefundManager;
|
||||
import bisq.core.trade.Tradable;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@ -35,17 +35,17 @@ public class TransactionAwareTradableFactory {
|
||||
private final ArbitrationManager arbitrationManager;
|
||||
private final RefundManager refundManager;
|
||||
private final XmrWalletService xmrWalletService;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
|
||||
@Inject
|
||||
TransactionAwareTradableFactory(ArbitrationManager arbitrationManager,
|
||||
RefundManager refundManager,
|
||||
XmrWalletService xmrWalletService,
|
||||
PubKeyRing pubKeyRing) {
|
||||
PubKeyRingProvider pubKeyRingProvider) {
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
this.refundManager = refundManager;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
}
|
||||
|
||||
TransactionAwareTradable create(Tradable delegate) {
|
||||
@ -56,7 +56,7 @@ public class TransactionAwareTradableFactory {
|
||||
arbitrationManager,
|
||||
refundManager,
|
||||
xmrWalletService,
|
||||
pubKeyRing);
|
||||
pubKeyRingProvider.get());
|
||||
} else {
|
||||
return new DummyTransactionAwareTradable(delegate);
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
|
||||
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
@ -103,7 +102,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
|
||||
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final Preferences preferences;
|
||||
private final TradeDetailsWindow tradeDetailsWindow;
|
||||
private final OfferDetailsWindow offerDetailsWindow;
|
||||
@ -120,14 +119,14 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
|
||||
@Inject
|
||||
private TransactionsView(BtcWalletService btcWalletService,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
Preferences preferences,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
OfferDetailsWindow offerDetailsWindow,
|
||||
DisplayedTransactionsFactory displayedTransactionsFactory) {
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.preferences = preferences;
|
||||
this.tradeDetailsWindow = tradeDetailsWindow;
|
||||
this.offerDetailsWindow = offerDetailsWindow;
|
||||
@ -537,7 +536,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
|
||||
}
|
||||
|
||||
private void revertTransaction(String txId, @Nullable Tradable tradable) {
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) {
|
||||
try {
|
||||
btcWalletService.doubleSpendTransaction(txId, () -> {
|
||||
if (tradable != null)
|
||||
|
@ -27,7 +27,7 @@ import bisq.desktop.util.DisplayUtils;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.locale.BankUtil;
|
||||
import bisq.core.locale.CountryUtil;
|
||||
import bisq.core.locale.CryptoCurrency;
|
||||
@ -97,8 +97,8 @@ class OfferBookViewModel extends ActivatableViewModel {
|
||||
private final User user;
|
||||
private final OfferBook offerBook;
|
||||
final Preferences preferences;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final P2PService p2PService;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
final PriceFeedService priceFeedService;
|
||||
private final ClosedTradableManager closedTradableManager;
|
||||
final AccountAgeWitnessService accountAgeWitnessService;
|
||||
@ -142,7 +142,7 @@ class OfferBookViewModel extends ActivatableViewModel {
|
||||
OpenOfferManager openOfferManager,
|
||||
OfferBook offerBook,
|
||||
Preferences preferences,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
P2PService p2PService,
|
||||
PriceFeedService priceFeedService,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
@ -157,7 +157,7 @@ class OfferBookViewModel extends ActivatableViewModel {
|
||||
this.user = user;
|
||||
this.offerBook = offerBook;
|
||||
this.preferences = preferences;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.p2PService = p2PService;
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
@ -561,7 +561,7 @@ class OfferBookViewModel extends ActivatableViewModel {
|
||||
|
||||
boolean canCreateOrTakeOffer() {
|
||||
return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) &&
|
||||
GUIUtil.isChainHeightSyncedWithinToleranceOrShowPopup(walletsSetup) &&
|
||||
GUIUtil.isChainHeightSyncedWithinToleranceOrShowPopup(connectionService) &&
|
||||
GUIUtil.isBootstrappedOrShowPopup(p2PService);
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,7 @@ import bisq.desktop.main.overlays.Overlay;
|
||||
import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
import bisq.desktop.util.Transitions;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
import bisq.core.locale.Res;
|
||||
@ -53,7 +52,7 @@ public final class BtcEmptyWalletWindow extends Overlay<BtcEmptyWalletWindow> {
|
||||
private final WalletPasswordWindow walletPasswordWindow;
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final CoinFormatter btcFormatter;
|
||||
|
||||
@ -65,7 +64,7 @@ public final class BtcEmptyWalletWindow extends Overlay<BtcEmptyWalletWindow> {
|
||||
public BtcEmptyWalletWindow(WalletPasswordWindow walletPasswordWindow,
|
||||
OpenOfferManager openOfferManager,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
BtcWalletService btcWalletService,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) {
|
||||
headLine(Res.get("emptyWalletWindow.headline", "BTC"));
|
||||
@ -73,13 +72,14 @@ public final class BtcEmptyWalletWindow extends Overlay<BtcEmptyWalletWindow> {
|
||||
type = Type.Instruction;
|
||||
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.btcFormatter = btcFormatter;
|
||||
this.walletPasswordWindow = walletPasswordWindow;
|
||||
this.openOfferManager = openOfferManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
createGridPane();
|
||||
addHeadLine();
|
||||
@ -143,7 +143,7 @@ public final class BtcEmptyWalletWindow extends Overlay<BtcEmptyWalletWindow> {
|
||||
}
|
||||
|
||||
private void doEmptyWallet(KeyParameter aesKey) {
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) {
|
||||
if (!openOfferManager.getObservableList().isEmpty()) {
|
||||
UserThread.runAfter(() ->
|
||||
new Popup().warning(Res.get("emptyWalletWindow.openOffers.warn"))
|
||||
|
@ -25,11 +25,10 @@ import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
import bisq.desktop.util.validation.LengthValidator;
|
||||
import bisq.desktop.util.validation.PercentageNumberValidator;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.exceptions.TransactionVerificationException;
|
||||
import bisq.core.btc.exceptions.TxBroadcastException;
|
||||
import bisq.core.btc.exceptions.WalletException;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.TxBroadcaster;
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
@ -92,7 +91,6 @@ import java.time.Instant;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -111,7 +109,7 @@ public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
|
||||
private final P2PService p2PService;
|
||||
private final MediationManager mediationManager;
|
||||
private final Preferences preferences;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
private final WalletsManager walletsManager;
|
||||
GridPane inputsGridPane;
|
||||
GridPane importTxGridPane;
|
||||
@ -150,17 +148,18 @@ public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
|
||||
P2PService p2PService,
|
||||
MediationManager mediationManager,
|
||||
Preferences preferences,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
WalletsManager walletsManager) {
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.p2PService = p2PService;
|
||||
this.mediationManager = mediationManager;
|
||||
this.preferences = preferences;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.walletsManager = walletsManager;
|
||||
type = Type.Attention;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (headLine == null)
|
||||
headLine = "Emergency MultiSig payout tool"; // We dont translate here as it is for dev only purpose
|
||||
@ -810,7 +809,7 @@ public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
|
||||
}
|
||||
};
|
||||
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) {
|
||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) {
|
||||
try {
|
||||
tradeWalletService.emergencyPublishPayoutTxFrom2of2MultiSig(
|
||||
txIdAndHex.second,
|
||||
|
@ -28,12 +28,12 @@ import bisq.desktop.util.Layout;
|
||||
import bisq.desktop.util.Transitions;
|
||||
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
import bisq.core.crypto.ScryptUtil;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.ScryptUtil;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
|
@ -29,7 +29,7 @@ import bisq.desktop.main.support.dispute.client.mediation.MediationClientView;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.Offer;
|
||||
@ -55,6 +55,7 @@ import bisq.core.user.Preferences;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.PubKeyRingProvider;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
@ -97,7 +98,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
public final ArbitrationManager arbitrationManager;
|
||||
public final MediationManager mediationManager;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final CoreMoneroConnectionsService connectionService;
|
||||
@Getter
|
||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
public final Navigation navigation;
|
||||
@ -120,7 +121,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
private ChangeListener<Trade.State> tradeStateChangeListener;
|
||||
private Trade selectedTrade;
|
||||
@Getter
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final PubKeyRingProvider pubKeyRingProvider;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, initialization
|
||||
@ -129,13 +130,13 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
@Inject
|
||||
public PendingTradesDataModel(TradeManager tradeManager,
|
||||
XmrWalletService xmrWalletService,
|
||||
PubKeyRing pubKeyRing,
|
||||
PubKeyRingProvider pubKeyRingProvider,
|
||||
ArbitrationManager arbitrationManager,
|
||||
MediationManager mediationManager,
|
||||
TraderChatManager traderChatManager,
|
||||
Preferences preferences,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
CoreMoneroConnectionsService connectionService,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
Navigation navigation,
|
||||
WalletPasswordWindow walletPasswordWindow,
|
||||
@ -143,13 +144,13 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
OfferUtil offerUtil) {
|
||||
this.tradeManager = tradeManager;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
this.mediationManager = mediationManager;
|
||||
this.traderChatManager = traderChatManager;
|
||||
this.preferences = preferences;
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.connectionService = connectionService;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.navigation = navigation;
|
||||
this.walletPasswordWindow = walletPasswordWindow;
|
||||
@ -513,11 +514,11 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); // TODO (woodser): no serialized txs in xmr
|
||||
Dispute dispute = new Dispute(new Date().getTime(),
|
||||
trade.getId(),
|
||||
pubKeyRing.hashCode(), // traderId
|
||||
pubKeyRingProvider.get().hashCode(), // trader id
|
||||
true,
|
||||
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
|
||||
isMaker,
|
||||
pubKeyRing,
|
||||
pubKeyRingProvider.get(),
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
@ -549,11 +550,11 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser)
|
||||
Dispute dispute = new Dispute(new Date().getTime(),
|
||||
trade.getId(),
|
||||
pubKeyRing.hashCode(), // traderId,
|
||||
pubKeyRingProvider.get().hashCode(), // trader id,
|
||||
true,
|
||||
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
|
||||
isMaker,
|
||||
pubKeyRing,
|
||||
pubKeyRingProvider.get(),
|
||||
trade.getDate().getTime(),
|
||||
trade.getMaxTradePeriodDate().getTime(),
|
||||
trade.getContract(),
|
||||
@ -595,7 +596,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
}
|
||||
|
||||
public boolean isReadyForTxBroadcast() {
|
||||
return GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup);
|
||||
return GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService);
|
||||
}
|
||||
|
||||
public boolean isBootstrappedOrShowPopup() {
|
||||
|
@ -398,7 +398,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
|
||||
return Res.get("portfolio.pending.failedTrade.missingContract");
|
||||
}
|
||||
|
||||
PubKeyRing myPubKeyRing = model.dataModel.getPubKeyRing();
|
||||
PubKeyRing myPubKeyRing = model.dataModel.getPubKeyRingProvider().get();
|
||||
boolean isMyRoleBuyer = contract.isMyRoleBuyer(myPubKeyRing);
|
||||
boolean isMyRoleMaker = contract.isMyRoleMaker(myPubKeyRing);
|
||||
|
||||
|
@ -675,7 +675,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
|
||||
DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get();
|
||||
Contract contract = checkNotNull(trade.getContract(), "contract must not be null");
|
||||
boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRing());
|
||||
boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRingProvider().get());
|
||||
String buyerPayoutAmount = model.btcFormatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount());
|
||||
String sellerPayoutAmount = model.btcFormatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount());
|
||||
String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount;
|
||||
|
@ -28,6 +28,7 @@ import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.btc.nodes.BtcNodes;
|
||||
import bisq.core.btc.nodes.LocalBitcoinNode;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
@ -38,7 +39,6 @@ import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.validation.RegexValidator;
|
||||
import bisq.core.util.validation.RegexValidatorFactory;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.network.Statistic;
|
||||
|
||||
@ -119,6 +119,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
private final ClockWatcher clockWatcher;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final P2PService p2PService;
|
||||
private final CoreMoneroConnectionsService connectionManager;
|
||||
|
||||
private final ObservableList<P2pNetworkListItem> p2pNetworkListItems = FXCollections.observableArrayList();
|
||||
private final SortedList<P2pNetworkListItem> p2pSortedList = new SortedList<>(p2pNetworkListItems);
|
||||
@ -139,6 +140,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
@Inject
|
||||
public NetworkSettingsView(WalletsSetup walletsSetup,
|
||||
P2PService p2PService,
|
||||
CoreMoneroConnectionsService connectionManager,
|
||||
Preferences preferences,
|
||||
BtcNodes btcNodes,
|
||||
FilterManager filterManager,
|
||||
@ -148,6 +150,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
super();
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.p2PService = p2PService;
|
||||
this.connectionManager = connectionManager;
|
||||
this.preferences = preferences;
|
||||
this.btcNodes = btcNodes;
|
||||
this.filterManager = filterManager;
|
||||
@ -291,10 +294,10 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
|
||||
reSyncSPVChainButton.setOnAction(event -> GUIUtil.reSyncSPVChain(preferences));
|
||||
|
||||
moneroPeersSubscription = EasyBind.subscribe(walletsSetup.peerConnectionsProperty(),
|
||||
moneroPeersSubscription = EasyBind.subscribe(connectionManager.peerConnectionsProperty(),
|
||||
this::updateMoneroPeersTable);
|
||||
|
||||
moneroBlockHeightSubscription = EasyBind.subscribe(walletsSetup.chainHeightProperty(),
|
||||
moneroBlockHeightSubscription = EasyBind.subscribe(connectionManager.chainHeightProperty(),
|
||||
this::updateChainHeightTextField);
|
||||
|
||||
nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(),
|
||||
|
@ -30,29 +30,24 @@ import bisq.desktop.main.overlays.popups.Popup;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitness;
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.api.CoreMoneroConnectionsService;
|
||||
import bisq.core.app.HavenoSetup;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.locale.Country;
|
||||
import bisq.core.locale.CountryUtil;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.locale.TradeCurrency;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.monetary.Volume;
|
||||
import bisq.core.offer.OfferRestrictions;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.payment.PaymentAccountList;
|
||||
import bisq.core.payment.payload.PaymentMethod;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.MarketPrice;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.trade.txproof.AssetTxProofResult;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
import bisq.core.util.coin.CoinUtil;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
@ -63,7 +58,6 @@ import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.persistence.PersistenceManager;
|
||||
import bisq.common.proto.persistable.PersistableEnvelope;
|
||||
import bisq.common.proto.persistable.PersistenceProtoResolver;
|
||||
import bisq.common.util.MathUtils;
|
||||
import bisq.common.util.Tuple2;
|
||||
import bisq.common.util.Tuple3;
|
||||
import bisq.common.util.Utilities;
|
||||
@ -72,7 +66,6 @@ import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.TransactionConfidence;
|
||||
import org.bitcoinj.uri.BitcoinURI;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
|
||||
import com.googlecode.jcsv.CSVStrategy;
|
||||
import com.googlecode.jcsv.writer.CSVEntryConverter;
|
||||
@ -757,17 +750,17 @@ public class GUIUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isReadyForTxBroadcastOrShowPopup(P2PService p2PService, WalletsSetup walletsSetup) {
|
||||
public static boolean isReadyForTxBroadcastOrShowPopup(P2PService p2PService, CoreMoneroConnectionsService connectionService) {
|
||||
if (!GUIUtil.isBootstrappedOrShowPopup(p2PService)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!walletsSetup.hasSufficientPeersForBroadcast()) {
|
||||
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToBtcNetwork", walletsSetup.getMinBroadcastConnections())).show();
|
||||
if (!connectionService.hasSufficientPeersForBroadcast()) {
|
||||
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToBtcNetwork", connectionService.getMinBroadcastConnections())).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!walletsSetup.isDownloadComplete()) {
|
||||
if (!connectionService.isDownloadComplete()) {
|
||||
new Popup().information(Res.get("popup.warning.downloadNotComplete")).show();
|
||||
return false;
|
||||
}
|
||||
@ -775,8 +768,8 @@ public class GUIUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(WalletsSetup walletsSetup) {
|
||||
if (!walletsSetup.isChainHeightSyncedWithinTolerance()) {
|
||||
public static boolean isChainHeightSyncedWithinToleranceOrShowPopup(CoreMoneroConnectionsService connectionService) {
|
||||
if (!connectionService.isChainHeightSyncedWithinTolerance()) {
|
||||
new Popup().information(Res.get("popup.warning.chainNotSynced")).show();
|
||||
return false;
|
||||
}
|
||||
|
@ -107,7 +107,6 @@ public class GuiceSetupTest {
|
||||
assertSingleton(TradeLimits.class);
|
||||
assertSingleton(KeyStorage.class);
|
||||
assertSingleton(KeyRing.class);
|
||||
assertSingleton(PubKeyRing.class);
|
||||
assertSingleton(User.class);
|
||||
assertSingleton(ClockWatcher.class);
|
||||
assertSingleton(Preferences.class);
|
||||
|
@ -20,7 +20,6 @@ package bisq.network.crypto;
|
||||
import bisq.common.crypto.CryptoException;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.KeyStorage;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.file.FileUtil;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
@ -45,7 +44,6 @@ public class EncryptionServiceTests {
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private PubKeyRing pubKeyRing;
|
||||
private KeyRing keyRing;
|
||||
private File dir;
|
||||
|
||||
@ -58,8 +56,7 @@ public class EncryptionServiceTests {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
dir.mkdir();
|
||||
KeyStorage keyStorage = new KeyStorage(dir);
|
||||
keyRing = new KeyRing(keyStorage);
|
||||
pubKeyRing = keyRing.getPubKeyRing();
|
||||
keyRing = new KeyRing(keyStorage, null, true);
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -61,7 +61,7 @@ public class AddDataMessageTest {
|
||||
dir1.delete();
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
dir1.mkdir();
|
||||
keyRing1 = new KeyRing(new KeyStorage(dir1));
|
||||
keyRing1 = new KeyRing(new KeyStorage(dir1), null, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -24,20 +24,134 @@ option java_package = "bisq.proto.grpc";
|
||||
option java_multiple_files = true;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DisputeAgents
|
||||
// Help
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service DisputeAgents {
|
||||
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
|
||||
service Help {
|
||||
rpc GetMethodHelp (GetMethodHelpRequest) returns (GetMethodHelpReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentRequest {
|
||||
string dispute_agent_type = 1;
|
||||
string registration_key = 2;
|
||||
message GetMethodHelpRequest {
|
||||
string method_name = 1;
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentReply {
|
||||
message GetMethodHelpReply {
|
||||
string method_help = 1;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Version
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service GetVersion {
|
||||
rpc GetVersion (GetVersionRequest) returns (GetVersionReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message GetVersionRequest {
|
||||
}
|
||||
|
||||
message GetVersionReply {
|
||||
string version = 1;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Account
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service Account {
|
||||
rpc AccountExists (AccountExistsRequest) returns (AccountExistsReply) {
|
||||
}
|
||||
rpc IsAccountOpen (IsAccountOpenRequest) returns (IsAccountOpenReply) {
|
||||
}
|
||||
rpc CreateAccount (CreateAccountRequest) returns (CreateAccountReply) {
|
||||
}
|
||||
rpc OpenAccount (OpenAccountRequest) returns (OpenAccountReply) {
|
||||
}
|
||||
rpc IsAppInitialized (IsAppInitializedRequest) returns (IsAppInitializedReply) {
|
||||
}
|
||||
rpc ChangePassword (ChangePasswordRequest) returns (ChangePasswordReply) {
|
||||
}
|
||||
rpc CloseAccount (CloseAccountRequest) returns (CloseAccountReply) {
|
||||
}
|
||||
rpc DeleteAccount (DeleteAccountRequest) returns (DeleteAccountReply) {
|
||||
}
|
||||
rpc BackupAccount (BackupAccountRequest) returns (stream BackupAccountReply) {
|
||||
}
|
||||
rpc RestoreAccount (RestoreAccountRequest) returns (RestoreAccountReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message AccountExistsRequest {
|
||||
}
|
||||
|
||||
message AccountExistsReply {
|
||||
bool account_exists = 1;
|
||||
}
|
||||
|
||||
message IsAccountOpenRequest {
|
||||
}
|
||||
|
||||
message IsAccountOpenReply {
|
||||
bool is_account_open = 1;
|
||||
}
|
||||
|
||||
message CreateAccountRequest {
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message CreateAccountReply {
|
||||
}
|
||||
|
||||
message OpenAccountRequest {
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message OpenAccountReply {
|
||||
}
|
||||
|
||||
message IsAppInitializedRequest {
|
||||
}
|
||||
|
||||
message IsAppInitializedReply {
|
||||
bool is_app_initialized = 1;
|
||||
}
|
||||
|
||||
message ChangePasswordRequest {
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message ChangePasswordReply {
|
||||
}
|
||||
|
||||
message CloseAccountRequest {
|
||||
}
|
||||
|
||||
message CloseAccountReply {
|
||||
}
|
||||
|
||||
message DeleteAccountRequest {
|
||||
}
|
||||
|
||||
message DeleteAccountReply {
|
||||
}
|
||||
|
||||
message BackupAccountRequest {
|
||||
}
|
||||
|
||||
message BackupAccountReply {
|
||||
bytes zip_bytes = 1;
|
||||
}
|
||||
|
||||
message RestoreAccountRequest {
|
||||
bytes zip_bytes = 1;
|
||||
uint64 offset = 2;
|
||||
uint64 total_length = 3;
|
||||
bool has_more = 4;
|
||||
}
|
||||
|
||||
message RestoreAccountReply {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -56,9 +170,10 @@ message RegisterNotificationListenerRequest {
|
||||
|
||||
message NotificationMessage {
|
||||
enum NotificationType {
|
||||
TRADE_UPDATE = 0;
|
||||
CHAT_MESSAGE = 1;
|
||||
KEEP_ALIVE = 2;
|
||||
APP_INITIALIZED = 0;
|
||||
KEEP_ALIVE = 1;
|
||||
TRADE_UPDATE = 2;
|
||||
CHAT_MESSAGE = 3;
|
||||
}
|
||||
|
||||
string id = 1;
|
||||
@ -78,20 +193,134 @@ message SendNotificationReply {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Help
|
||||
// MoneroConnections
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service Help {
|
||||
rpc GetMethodHelp (GetMethodHelpRequest) returns (GetMethodHelpReply) {
|
||||
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 GetMethodHelpRequest {
|
||||
string method_name = 1;
|
||||
message UrlConnection {
|
||||
enum OnlineStatus {
|
||||
UNKNOWN = 0;
|
||||
ONLINE = 1;
|
||||
OFFLINE = 2;
|
||||
}
|
||||
enum AuthenticationStatus {
|
||||
NO_AUTHENTICATION = 0;
|
||||
AUTHENTICATED = 1;
|
||||
NOT_AUTHENTICATED = 2;
|
||||
}
|
||||
|
||||
string url = 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 GetMethodHelpReply {
|
||||
string method_help = 1;
|
||||
message AddConnectionRequest {
|
||||
UrlConnection connection = 1;
|
||||
}
|
||||
|
||||
message AddConnectionReply {}
|
||||
|
||||
message RemoveConnectionRequest {
|
||||
string url = 1;
|
||||
}
|
||||
|
||||
message RemoveConnectionReply {}
|
||||
|
||||
message GetConnectionRequest {}
|
||||
|
||||
message GetConnectionReply {
|
||||
UrlConnection connection = 1;
|
||||
}
|
||||
|
||||
message GetConnectionsRequest {}
|
||||
|
||||
message GetConnectionsReply {
|
||||
repeated UrlConnection connections = 1;
|
||||
}
|
||||
|
||||
message SetConnectionRequest {
|
||||
string url = 1;
|
||||
UrlConnection connection = 2;
|
||||
}
|
||||
|
||||
message SetConnectionReply {}
|
||||
|
||||
message CheckConnectionRequest {}
|
||||
|
||||
message CheckConnectionReply {
|
||||
UrlConnection connection = 1;
|
||||
}
|
||||
|
||||
message CheckConnectionsRequest {}
|
||||
|
||||
message CheckConnectionsReply {
|
||||
repeated UrlConnection connections = 1;
|
||||
}
|
||||
|
||||
message StartCheckingConnectionsRequest {
|
||||
int32 refresh_period = 1; // milliseconds
|
||||
}
|
||||
|
||||
message StartCheckingConnectionsReply {}
|
||||
|
||||
message StopCheckingConnectionsRequest {}
|
||||
|
||||
message StopCheckingConnectionsReply {}
|
||||
|
||||
message GetBestAvailableConnectionRequest {}
|
||||
|
||||
message GetBestAvailableConnectionReply {
|
||||
UrlConnection connection = 1;
|
||||
}
|
||||
|
||||
message SetAutoSwitchRequest {
|
||||
bool auto_switch = 1;
|
||||
}
|
||||
|
||||
message SetAutoSwitchReply {}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DisputeAgents
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service DisputeAgents {
|
||||
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentRequest {
|
||||
string dispute_agent_type = 1;
|
||||
string registration_key = 2;
|
||||
}
|
||||
|
||||
message RegisterDisputeAgentReply {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -704,133 +933,3 @@ message AddressBalanceInfo {
|
||||
int64 num_confirmations = 3;
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
service GetVersion {
|
||||
rpc GetVersion (GetVersionRequest) returns (GetVersionReply) {
|
||||
}
|
||||
}
|
||||
|
||||
message GetVersionRequest {
|
||||
}
|
||||
|
||||
message GetVersionReply {
|
||||
string version = 1;
|
||||
}
|
||||
|
@ -1696,7 +1696,7 @@ message TradingPeer {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
message EncryptedConnection {
|
||||
string uri = 1;
|
||||
string url = 1;
|
||||
string username = 2;
|
||||
bytes encrypted_password = 3;
|
||||
bytes encryption_salt = 4;
|
||||
@ -1706,7 +1706,7 @@ message EncryptedConnection {
|
||||
message EncryptedConnectionList {
|
||||
bytes salt = 1;
|
||||
repeated EncryptedConnection items = 2;
|
||||
string current_connection_uri = 3;
|
||||
string current_connection_url = 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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user