From e3b9a9962b4000a48b157664de5f62c4e23fd1f1 Mon Sep 17 00:00:00 2001 From: duriancrepe <94508990+duriancrepe@users.noreply.github.com> Date: Wed, 9 Feb 2022 01:39:57 -0800 Subject: [PATCH] Add API functions to initialize Haveno account (#216) Co-authored-by: woodser@protonmail.com --- Makefile | 29 +- .../main/java/bisq/common/config/Config.java | 10 + .../java/bisq/common/crypto/Encryption.java | 11 +- .../crypto/IncorrectPasswordException.java | 23 + .../main/java/bisq/common/crypto/KeyRing.java | 88 +- .../java/bisq/common/crypto/KeyStorage.java | 103 +- .../common/crypto/PubKeyRingProvider.java | 11 +- .../java/bisq/common}/crypto/ScryptUtil.java | 23 +- .../persistence/PersistenceManager.java | 6 +- .../main/java/bisq/common/util/ZipUtils.java | 128 +++ .../bisq/core/api/AccountServiceListener.java | 14 + .../bisq/core/api/CoreAccountService.java | 179 +++- core/src/main/java/bisq/core/api/CoreApi.java | 351 ++++--- .../api/CoreMoneroConnectionsService.java | 348 +++++-- .../core/api/CoreNotificationService.java | 9 +- .../bisq/core/api/CoreWalletsService.java | 10 +- .../core/api/model/EncryptedConnection.java | 6 +- .../java/bisq/core/app/AppStartupState.java | 30 +- .../main/java/bisq/core/app/CoreModule.java | 3 - .../bisq/core/app/DomainInitialisation.java | 4 +- .../java/bisq/core/app/HavenoExecutable.java | 122 ++- .../java/bisq/core/app/HavenoHeadlessApp.java | 5 +- .../bisq/core/app/HavenoHeadlessAppMain.java | 1 + .../main/java/bisq/core/app/HavenoSetup.java | 17 +- .../java/bisq/core/app/P2PNetworkSetup.java | 10 +- .../java/bisq/core/app/WalletAppSetup.java | 14 +- .../core/app/misc/ModuleForAppWithP2p.java | 6 +- .../btc/model/EncryptedConnectionList.java | 113 +- .../core/btc/model/XmrAddressEntryList.java | 81 -- .../bisq/core/btc/setup/DownloadListener.java | 6 +- .../bisq/core/btc/setup/WalletConfig.java | 149 +-- .../bisq/core/btc/setup/WalletsSetup.java | 86 +- .../core/btc/wallet/TradeWalletService.java | 41 - .../bisq/core/btc/wallet/WalletsManager.java | 3 +- .../core/btc/wallet/XmrWalletService.java | 961 ++++++++++-------- .../notifications/alerts/TradeEvents.java | 15 +- .../bisq/core/offer/CreateOfferService.java | 12 +- .../java/bisq/core/offer/OfferFilter.java | 8 +- .../bisq/core/offer/OpenOfferManager.java | 8 +- .../bisq/core/support/SupportManager.java | 13 +- .../core/support/dispute/DisputeManager.java | 15 +- .../arbitration/ArbitrationManager.java | 25 +- .../dispute/mediation/MediationManager.java | 6 +- .../support/dispute/refund/RefundManager.java | 6 +- .../support/traderchat/TraderChatManager.java | 19 +- core/src/main/java/bisq/core/trade/Trade.java | 3 +- .../java/bisq/core/trade/TradeManager.java | 6 +- .../tasks/ProcessInitMultisigRequest.java | 10 +- .../tasks/taker/TakerCreateFeeTx.java | 71 -- .../trade/txproof/xmr/XmrTxProofService.java | 24 +- core/src/main/java/bisq/core/user/User.java | 1 + .../witness/AccountAgeWitnessServiceTest.java | 10 +- .../java/bisq/core/crypto/EncryptionTest.java | 2 +- .../test/java/bisq/core/crypto/SigTest.java | 2 +- .../bisq/core/offer/OpenOfferManagerTest.java | 3 + .../java/bisq/daemon/app/ConsoleInput.java | 64 ++ .../bisq/daemon/app/ConsoleInputReadTask.java | 45 + .../bisq/daemon/app/HavenoDaemonMain.java | 117 ++- .../bisq/daemon/grpc/GrpcAccountService.java | 286 ++++++ .../grpc/GrpcMoneroConnectionsService.java | 61 +- .../java/bisq/daemon/grpc/GrpcServer.java | 2 + .../bisq/daemon/grpc/GrpcShutdownService.java | 11 +- .../java/bisq/desktop/main/MainViewModel.java | 31 +- .../content/password/PasswordView.java | 3 +- .../bisq/desktop/main/debug/DebugView.java | 3 - .../TransactionAwareTradableFactory.java | 10 +- .../funds/transactions/TransactionsView.java | 11 +- .../offer/offerbook/OfferBookViewModel.java | 10 +- .../windows/BtcEmptyWalletWindow.java | 12 +- .../windows/ManualPayoutTxWindow.java | 13 +- .../windows/WalletPasswordWindow.java | 2 +- .../pendingtrades/PendingTradesDataModel.java | 25 +- .../pendingtrades/PendingTradesView.java | 4 +- .../pendingtrades/steps/TradeStepView.java | 8 +- .../settings/network/NetworkSettingsView.java | 9 +- .../main/java/bisq/desktop/util/GUIUtil.java | 21 +- .../java/bisq/desktop/GuiceSetupTest.java | 1 - .../crypto/EncryptionServiceTests.java | 5 +- .../storage/messages/AddDataMessageTest.java | 2 +- proto/src/main/proto/grpc.proto | 395 ++++--- proto/src/main/proto/pb.proto | 4 +- 81 files changed, 2755 insertions(+), 1660 deletions(-) create mode 100644 common/src/main/java/bisq/common/crypto/IncorrectPasswordException.java rename {core/src/main/java/bisq/core => common/src/main/java/bisq/common}/crypto/ScryptUtil.java (78%) create mode 100644 common/src/main/java/bisq/common/util/ZipUtils.java create mode 100644 core/src/main/java/bisq/core/api/AccountServiceListener.java delete mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerCreateFeeTx.java create mode 100644 daemon/src/main/java/bisq/daemon/app/ConsoleInput.java create mode 100644 daemon/src/main/java/bisq/daemon/app/ConsoleInputReadTask.java create mode 100644 daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java diff --git a/Makefile b/Makefile index 61cb3d1210..d4b0216285 100644 --- a/Makefile +++ b/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 \ diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index 72206e4673..015f38846e 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -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 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), diff --git a/common/src/main/java/bisq/common/crypto/Encryption.java b/common/src/main/java/bisq/common/crypto/Encryption.java index f2e6d29615..e6e0f58ffe 100644 --- a/common/src/main/java/bisq/common/crypto/Encryption.java +++ b/common/src/main/java/bisq/common/crypto/Encryption.java @@ -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); } } diff --git a/common/src/main/java/bisq/common/crypto/IncorrectPasswordException.java b/common/src/main/java/bisq/common/crypto/IncorrectPasswordException.java new file mode 100644 index 0000000000..78b4059964 --- /dev/null +++ b/common/src/main/java/bisq/common/crypto/IncorrectPasswordException.java @@ -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 . + */ +package bisq.common.crypto; + +public class IncorrectPasswordException extends Exception { + public IncorrectPasswordException(String message) { + super(message); + } +} diff --git a/common/src/main/java/bisq/common/crypto/KeyRing.java b/common/src/main/java/bisq/common/crypto/KeyRing.java index 30ecfc0497..49b89bbc24 100644 --- a/common/src/main/java/bisq/common/crypto/KeyRing.java +++ b/common/src/main/java/bisq/common/crypto/KeyRing.java @@ -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) { - 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 = Sig.generateKeyPair(); - encryptionKeyPair = Encryption.generateKeyPair(); - keyStorage.saveKeyRing(this); + 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, 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(); pubKeyRing = new PubKeyRing(signatureKeyPair.getPublic(), encryptionKeyPair.getPublic()); + keyStorage.saveKeyRing(this, password); } // Don't print keys for security reasons diff --git a/common/src/main/java/bisq/common/crypto/KeyStorage.java b/common/src/main/java/bisq/common/crypto/KeyStorage.java index 5e5608e13b..3435d81481 100644 --- a/common/src/main/java/bisq/common/crypto/KeyStorage.java +++ b/common/src/main/java/bisq/common/crypto/KeyStorage.java @@ -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); } diff --git a/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java b/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java index 534ea69a5b..5134b7cca2 100644 --- a/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java +++ b/common/src/main/java/bisq/common/crypto/PubKeyRingProvider.java @@ -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 { - 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(); } } diff --git a/core/src/main/java/bisq/core/crypto/ScryptUtil.java b/common/src/main/java/bisq/common/crypto/ScryptUtil.java similarity index 78% rename from core/src/main/java/bisq/core/crypto/ScryptUtil.java rename to common/src/main/java/bisq/common/crypto/ScryptUtil.java index 4e0f7ef515..469e8d5557 100644 --- a/core/src/main/java/bisq/core/crypto/ScryptUtil.java +++ b/common/src/main/java/bisq/common/crypto/ScryptUtil.java @@ -15,7 +15,7 @@ * along with Haveno. If not, see . */ -package bisq.core.crypto; +package bisq.common.crypto; import bisq.common.UserThread; import bisq.common.util.Utilities; @@ -51,15 +51,26 @@ public class ScryptUtil { .build(); return new KeyCrypterScrypt(scryptParameters); } + + 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 { - 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); + KeyParameter aesKey = deriveKeyWithScrypt(keyCrypterScrypt, password); UserThread.execute(() -> { try { resultHandler.handleResult(aesKey); diff --git a/common/src/main/java/bisq/common/persistence/PersistenceManager.java b/common/src/main/java/bisq/common/persistence/PersistenceManager.java index 1dea484a20..37f7fe4735 100644 --- a/common/src/main/java/bisq/common/persistence/PersistenceManager.java +++ b/common/src/main/java/bisq/common/persistence/PersistenceManager.java @@ -114,7 +114,6 @@ public class PersistenceManager { 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 { 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 diff --git a/common/src/main/java/bisq/common/util/ZipUtils.java b/common/src/main/java/bisq/common/util/ZipUtils.java new file mode 100644 index 0000000000..0a1299f597 --- /dev/null +++ b/common/src/main/java/bisq/common/util/ZipUtils.java @@ -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 . + */ +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 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 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(); + } + } + } + +} diff --git a/core/src/main/java/bisq/core/api/AccountServiceListener.java b/core/src/main/java/bisq/core/api/AccountServiceListener.java new file mode 100644 index 0000000000..e2c712686d --- /dev/null +++ b/core/src/main/java/bisq/core/api/AccountServiceListener.java @@ -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) {} +} diff --git a/core/src/main/java/bisq/core/api/CoreAccountService.java b/core/src/main/java/bisq/core/api/CoreAccountService.java index bb5187e867..63b8cf07c5 100644 --- a/core/src/main/java/bisq/core/api/CoreAccountService.java +++ b/core/src/main/java/bisq/core/api/CoreAccountService.java @@ -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 . + */ + 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 static final String DEFAULT_PASSWORD = "abctesting123"; - - private String password = DEFAULT_PASSWORD; - - private final List listeners = new CopyOnWriteArrayList<>(); - - - public String getPassword() { - return password; + + private final Config config; + private final KeyStorage keyStorage; + private final KeyRing keyRing; + + @Getter + private String password; + private List listeners = new ArrayList(); + + @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 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); + } + + 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 consume, Consumer error) { + if (!accountExists()) throw new IllegalStateException("Cannot backup non existing account"); - public interface PasswordChangeListener { - - void onPasswordChange(String oldPassword, String newPassword); - + // 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); + } } } diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 7c0ff3afc1..7a4b073e52 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -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 consume, Consumer 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 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 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 getXmrTxs() { + return walletsService.getXmrTxs(); + } + + public MoneroTxWallet createXmrTx(List 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 getFundingAddresses() { + return walletsService.getFundingAddresses(); + } + + public void sendBtc(String address, + String amount, + String txFeeRate, + String memo, + FutureCallback 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 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 getXmrTxs() { - return walletsService.getXmrTxs(); - } - - public MoneroTxWallet createXmrTx(List 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 getFundingAddresses() { - return walletsService.getFundingAddresses(); - } - - public void sendBtc(String address, - String amount, - String txFeeRate, - String memo, - FutureCallback 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 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 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 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); - } } diff --git a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java index bfc5fbea02..225fa83d33 100644 --- a/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/bisq/core/api/CoreMoneroConnectionsService.java @@ -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 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> 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); + } + } + + @Override + public void onPasswordChanged(String oldPassword, String newPassword) { + log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword); + connectionList.changePassword(oldPassword, newPassword); + } + }); + }); } - public void initialize() { + // ------------------------ 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 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 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> 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) { @@ -50,24 +268,38 @@ public class CoreMoneroConnectionsService { connectionList.getCurrentConnectionUri().ifPresentOrElse(connectionManager::setConnection, () -> { 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(); - // register connection change listener - connectionManager.addListener(this::onConnectionChanged); + // 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 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 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 getOnlinePeers() { + return daemon.getPeers().stream() + .filter(peer -> peer.isOnline()) + .collect(Collectors.toList()); } } diff --git a/core/src/main/java/bisq/core/api/CoreNotificationService.java b/core/src/main/java/bisq/core/api/CoreNotificationService.java index fd78663ce1..36f762321e 100644 --- a/core/src/main/java/bisq/core/api/CoreNotificationService.java +++ b/core/src/main/java/bisq/core/api/CoreNotificationService.java @@ -40,7 +40,14 @@ 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) diff --git a/core/src/main/java/bisq/core/api/CoreWalletsService.java b/core/src/main/java/bisq/core/api/CoreWalletsService.java index 347fa72bb6..c929e9666a 100644 --- a/core/src/main/java/bisq/core/api/CoreWalletsService.java +++ b/core/src/main/java/bisq/core/api/CoreWalletsService.java @@ -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 getXmrTxs(){ + List getXmrTxs() { + accountService.checkAccountOpen(); return xmrWalletService.getWallet().getTxs(); } MoneroTxWallet createXmrTx(List destinations) { + accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { @@ -177,6 +184,7 @@ class CoreWalletsService { } String relayXmrTx(String metadata) { + accountService.checkAccountOpen(); verifyWalletsAreAvailable(); verifyEncryptedWalletIsUnlocked(); try { diff --git a/core/src/main/java/bisq/core/api/model/EncryptedConnection.java b/core/src/main/java/bisq/core/api/model/EncryptedConnection.java index 96775064a9..41ff4717e3 100644 --- a/core/src/main/java/bisq/core/api/model/EncryptedConnection.java +++ b/core/src/main/java/bisq/core/api/model/EncryptedConnection.java @@ -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(), diff --git a/core/src/main/java/bisq/core/app/AppStartupState.java b/core/src/main/java/bisq/core/app/AppStartupState.java index 429a06ee71..70527edb45 100644 --- a/core/src/main/java/bisq/core/app/AppStartupState.java +++ b/core/src/main/java/bisq/core/app/AppStartupState.java @@ -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"); } }); diff --git a/core/src/main/java/bisq/core/app/CoreModule.java b/core/src/main/java/bisq/core/app/CoreModule.java index fa7b983cb1..d1fe435209 100644 --- a/core/src/main/java/bisq/core/app/CoreModule.java +++ b/core/src/main/java/bisq/core/app/CoreModule.java @@ -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); } } diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index af5ef95992..ad3f68c5ff 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -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) diff --git a/core/src/main/java/bisq/core/app/HavenoExecutable.java b/core/src/main/java/bisq/core/app/HavenoExecutable.java index c4449eece0..cab0126d33 100644 --- a/core/src/main/java/bisq/core/app/HavenoExecutable.java +++ b/core/src/main/java/bisq/core/app/HavenoExecutable.java @@ -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 { + // 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. + 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) { - // 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."); - resultHandler.handleResult(); - UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1); - }); - } else { - UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1); - } + 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 flushed persistence. Exiting now."); + resultHandler.handleResult(); + UserThread.runAfter(() -> System.exit(exitCode), 1); + }); + } else { + resultHandler.handleResult(); + UserThread.runAfter(() -> System.exit(exitCode), 1); } } diff --git a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java index 7ee974c567..832888a642 100644 --- a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java @@ -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 /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/app/HavenoHeadlessAppMain.java b/core/src/main/java/bisq/core/app/HavenoHeadlessAppMain.java index 87ac91f700..da6335ef2c 100644 --- a/core/src/main/java/bisq/core/app/HavenoHeadlessAppMain.java +++ b/core/src/main/java/bisq/core/app/HavenoHeadlessAppMain.java @@ -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 { diff --git a/core/src/main/java/bisq/core/app/HavenoSetup.java b/core/src/main/java/bisq/core/app/HavenoSetup.java index d3584a2231..c2ad3fc2d3 100644 --- a/core/src/main/java/bisq/core/app/HavenoSetup.java +++ b/core/src/main/java/bisq/core/app/HavenoSetup.java @@ -195,7 +195,7 @@ public class HavenoSetup { private boolean allBasicServicesInitialized; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding p2pNetworkAndWalletInitialized; - private final List bisqSetupListeners = new ArrayList<>(); + private final List 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) change -> { if (!walletsManager.areWalletsEncrypted() && !user.isPaymentAccountImport() && preferences.showAgain(key) && change.wasAdded() && diff --git a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java index 7aee7035ae..88cd358a10 100644 --- a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java @@ -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; diff --git a/core/src/main/java/bisq/core/app/WalletAppSetup.java b/core/src/main/java/bisq/core/app/WalletAppSetup.java index a0312029d4..c1962005c8 100644 --- a/core/src/main/java/bisq/core/app/WalletAppSetup.java +++ b/core/src/main/java/bisq/core/app/WalletAppSetup.java @@ -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 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) { diff --git a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java index dd90c28812..cbfd7a2fcf 100644 --- a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java @@ -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)); } } diff --git a/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java b/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java index 57668f2f90..15f323498d 100644 --- a/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java +++ b/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java @@ -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 persistenceManager; private final Map 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 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 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(); } + + @Override + public Message toProtoMessage() { + List 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(); + } - private void onPasswordChange(String oldPassword, String newPassword) { + public static EncryptedConnectionList fromProto(protobuf.EncryptedConnectionList proto) { + List 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 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 items = proto.getItemsList().stream() - .map(EncryptedConnection::fromProto) - .collect(Collectors.toList()); - return new EncryptedConnectionList(proto.getSalt().toByteArray(), items, proto.getCurrentConnectionUri(), proto.getRefreshPeriod(), proto.getAutoSwitch()); - } } diff --git a/core/src/main/java/bisq/core/btc/model/XmrAddressEntryList.java b/core/src/main/java/bisq/core/btc/model/XmrAddressEntryList.java index 248907f9b2..9f9fb238ab 100644 --- a/core/src/main/java/bisq/core/btc/model/XmrAddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/XmrAddressEntryList.java @@ -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 persistenceManager; - transient private MoneroWallet wallet; private final Set 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 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 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{" + diff --git a/core/src/main/java/bisq/core/btc/setup/DownloadListener.java b/core/src/main/java/bisq/core/btc/setup/DownloadListener.java index f4f665792e..2d2e2b13dd 100644 --- a/core/src/main/java/bisq/core/btc/setup/DownloadListener.java +++ b/core/src/main/java/bisq/core/btc/setup/DownloadListener.java @@ -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)); } diff --git a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java index 0066cff414..5995d19752 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletConfig.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletConfig.java @@ -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; - /** *

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 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; diff --git a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java index c309bad59c..6c9461041c 100644 --- a/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java +++ b/core/src/main/java/bisq/core/btc/setup/WalletsSetup.java @@ -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> peers = new SimpleObjectProperty<>(); private final DownloadListener downloadListener = new DownloadListener(); + private final List setupTaskHandlers = new ArrayList<>(); private final List 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 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> 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

getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) { diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index 2729c659ed..922c913d85 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -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. * diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java index ae2ea1393e..f6d73e1c55 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java @@ -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; diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index b1c3129da1..c9de90f70c 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -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,211 +47,478 @@ 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 static final Logger log = LoggerFactory.getLogger(XmrWalletService.class); - private WalletsSetup walletsSetup; - private final XmrAddressEntryList addressEntryList; - protected final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); - protected final CopyOnWriteArraySet walletListeners = new CopyOnWriteArraySet<>(); - private Map multisigWallets; + // 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; - @Getter - private MoneroWallet wallet; + 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 balanceListeners = new CopyOnWriteArraySet<>(); + protected final CopyOnWriteArraySet walletListeners = new CopyOnWriteArraySet<>(); - @Inject - XmrWalletService(WalletsSetup walletsSetup, - XmrAddressEntryList addressEntryList) { - this.walletsSetup = walletsSetup; + private TradeManager tradeManager; + private MoneroWallet wallet; + private Map multisigWallets; - this.addressEntryList = addressEntryList; - this.multisigWallets = new HashMap(); + @Inject + 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.xmrAddressEntryList = xmrAddressEntryList; + this.multisigWallets = new HashMap(); + 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 - @Override - public void onNewBlock(long height) { } + // initialize + initialize(); - @Override - public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { - notifyBalanceListeners(); - } + // listen for account updates + accountService.addListener(new AccountServiceListener() { + + @Override + public void onAccountCreated() { + log.info(getClass() + ".accountService.onAccountCreated()"); + initialize(); + } + + @Override + 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); + } + }); }); + } + + // 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 connectionsService.getDaemon(); + } + + public CoreMoneroConnectionsService getConnectionsService() { + return connectionsService; + } + + public String getWalletPassword() { + return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); + } - walletsSetup.getMoneroConnectionsManager().addConnectionListener(newConnection -> { - updateDaemonConnections(newConnection); - }); - }); - } + public boolean walletExists(String walletName) { + String path = walletDir.toString() + File.separator + walletName; + return new File(path + ".keys").exists(); + } - public MoneroDaemon getDaemon() { - return walletsSetup.getXmrDaemon(); - } + public MoneroWalletRpc createWallet(MoneroWalletConfig config, Integer port) { - // TODO (woodser): wallet has single password which is passed here? - // TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse + // start monero-wallet-rpc instance + MoneroWalletRpc walletRpc = startWalletRpcInstance(port); - public synchronized MoneroWallet createMultisigWallet(String 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 - multisigWallets.put(tradeId, multisigWallet); - multisigWallet.startSyncing(5000l); - return multisigWallet; - } - - public synchronized MoneroWallet getMultisigWallet(String 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); - 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) { - String walletName = "xmr_multisig_trade_" + tradeId; - if (!walletsSetup.getWalletConfig().walletExists(walletName)) return false; - try { - walletsSetup.getWalletConfig().closeWallet(getMultisigWallet(tradeId), false); - } catch (Exception err) { - // multisig wallet may not be open - } - walletsSetup.getWalletConfig().deleteWallet(walletName); - multisigWallets.remove(tradeId); - return true; - } - - 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); - } - - 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); - return entry; - } - - public XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) { - Optional 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 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); - } else { - return getNewAddressEntry(offerId, context); + // 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 Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream() - .filter(e -> offerId.equals(e.getOfferId())) - .filter(e -> context == e.getContext()) - .findAny(); - } + public MoneroWalletRpc openWallet(MoneroWalletConfig config, Integer port) { - public void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) { - Optional 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); - saveAddressEntryList(); - }); -} + // start monero-wallet-rpc instance + MoneroWalletRpc walletRpc = startWalletRpcInstance(port); - public void resetAddressEntriesForOpenOffer(String offerId) { - log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); - swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); - swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE); - } + // 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; + } + } - 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 - // complications in some edge cases after a SPV resync). - swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); - } + private MoneroWalletRpc startWalletRpcInstance(Integer port) { - private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream() - .filter(e -> address.equals(e.getAddressString())) - .filter(e -> context == e.getContext()) - .findAny(); - } + // 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"); - public List getAvailableAddressEntries() { - return getAddressEntryListAsImmutableList().stream() - .filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()) - .collect(Collectors.toList()); -} + // get app's current daemon connection + MoneroRpcConnection connection = connectionsService.getConnection(); - public List getAddressEntriesForTrade() { - return getAddressEntryListAsImmutableList().stream() - .filter(addressEntry -> XmrAddressEntry.Context.MULTI_SIG == addressEntry.getContext() || - XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) - .collect(Collectors.toList()); - } + // start monero-wallet-rpc instance and return connected client + List 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 List getAddressEntries(XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream() - .filter(addressEntry -> context == addressEntry.getContext()) - .collect(Collectors.toList()); - } + public void closeWallet(MoneroWallet walletRpc, boolean save) { + log.info("{}.closeWallet({}, {})", getClass(), walletRpc.getPath(), save); + MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save); + } - public List getFundedAvailableAddressEntries() { - return getAvailableAddressEntries().stream() - .filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()) - .collect(Collectors.toList()); - } + 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? + } - public List getAddressEntryListAsImmutableList() { - return addressEntryList.getAddressEntriesAsListImmutable(); - } + // 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 = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port + multisigWallets.put(tradeId, multisigWallet); + multisigWallet.startSyncing(5000l); + return multisigWallet; + } - 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 - } + 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; + 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 Coin getBalanceForSubaddress(int subaddressIndex) { + public synchronized boolean deleteMultisigWallet(String tradeId) { + log.info("{}.deleteMultisigWallet({})", getClass(), tradeId); + String walletName = "xmr_multisig_trade_" + tradeId; + if (!walletExists(walletName)) return false; + try { + closeWallet(getMultisigWallet(tradeId), false); + } catch (Exception err) { + // multisig wallet may not be open + } + deleteWallet(walletName); + multisigWallets.remove(tradeId); + return true; + } - // get subaddress balance - BigInteger balance = wallet.getBalance(0, subaddressIndex); + public MoneroTxWallet createTx(List 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 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 openWallets = new ArrayList(); + 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 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); + xmrAddressEntryList.addAddressEntry(entry); + return entry; + } + + public XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) { + Optional 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 emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()) + .filter(e -> isSubaddressUnused(e.getSubaddressIndex())).findAny(); + if (emptyAvailableAddressEntry.isPresent()) { + return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); + } else { + return getNewAddressEntry(offerId, context); + } + } + } + + public Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); + } + + public void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) { + Optional 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); + xmrAddressEntryList.swapToAvailable(e); + saveAddressEntryList(); + }); + } + + public void resetAddressEntriesForOpenOffer(String offerId) { + log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); + swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE); + } + + 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 + // complications in some edge cases after a SPV resync). + swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); + } + + private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny(); + } + + public List getAvailableAddressEntries() { + return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList()); + } + + public List getAddressEntriesForTrade() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> XmrAddressEntry.Context.MULTI_SIG == addressEntry.getContext() || XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntries(XmrAddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList()); + } + + public List getFundedAvailableAddressEntries() { + return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()).collect(Collectors.toList()); + } + + public List getAddressEntryListAsImmutableList() { + 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 + } + + public Coin getBalanceForSubaddress(int subaddressIndex) { + + // get subaddress balance + BigInteger balance = wallet.getBalance(0, subaddressIndex); // // balance from xmr wallet does not include unconfirmed funds, so add them // TODO: support lower in stack? // for (MoneroTxWallet unconfirmedTx : wallet.getTxs(new MoneroTxQuery().setIsConfirmed(false))) { @@ -259,219 +529,114 @@ public class XmrWalletService { // } // } - System.out.println("Returning balance for subaddress " + subaddressIndex + ": " + balance.longValueExact()); + System.out.println("Returning balance for subaddress " + subaddressIndex + ": " + balance.longValueExact()); - return Coin.valueOf(balance.longValueExact()); - } - - - public Coin getAvailableConfirmedBalance() { - return wallet != null ? Coin.valueOf(wallet.getUnlockedBalance(0).longValueExact()) : Coin.ZERO; - } - - public Coin getSavingWalletBalance() { - return wallet != null ? Coin.valueOf(wallet.getBalance(0).longValueExact()) : Coin.ZERO; - } - - public Stream getAddressEntriesForAvailableBalanceStream() { - Stream availableAndPayout = Stream.concat(getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream(), getFundedAvailableAddressEntries().stream()); - Stream available = Stream.concat(availableAndPayout, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream()); - return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()); - } - - public void addBalanceListener(XmrBalanceListener listener) { - balanceListeners.add(listener); - } - - public void removeBalanceListener(XmrBalanceListener listener) { - balanceListeners.remove(listener); - } - - public void saveAddressEntryList() { - addressEntryList.requestPersistence(); - } - - public List getTransactions(boolean includeDead) { - return wallet.getTxs(new MoneroTxQuery().setIsFailed(includeDead ? null : false)); - } - - public void shutDown() { - - // collect wallets to shutdown - List openWallets = new ArrayList(); - if (wallet != null) openWallets.add(wallet); - for (String multisigWalletKey : multisigWallets.keySet()) { - openWallets.add(multisigWallets.get(multisigWalletKey)); + return Coin.valueOf(balance.longValueExact()); } - // create shutdown threads - List threads = new ArrayList(); - for (MoneroWallet openWallet : openWallets) { - threads.add(new Thread(new Runnable() { + public Coin getAvailableConfirmedBalance() { + return wallet != null ? Coin.valueOf(wallet.getUnlockedBalance(0).longValueExact()) : Coin.ZERO; + } + + public Coin getSavingWalletBalance() { + return wallet != null ? Coin.valueOf(wallet.getBalance(0).longValueExact()) : Coin.ZERO; + } + + public Stream getAddressEntriesForAvailableBalanceStream() { + Stream availableAndPayout = Stream.concat(getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream(), getFundedAvailableAddressEntries().stream()); + Stream available = Stream.concat(availableAndPayout, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream()); + return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).isPositive()); + } + + public void addBalanceListener(XmrBalanceListener listener) { + balanceListeners.add(listener); + } + + public void removeBalanceListener(XmrBalanceListener listener) { + balanceListeners.remove(listener); + } + + public void saveAddressEntryList() { + xmrAddressEntryList.requestPersistence(); + } + + public List getTransactions(boolean includeDead) { + return wallet.getTxs(new MoneroTxQuery().setIsFailed(includeDead ? null : false)); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // 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()); + } + + /** + * Wraps a MoneroWalletListener to notify the Haveno application. + * + * TODO (woodser): this is no longer necessary since not syncing to thread? + */ + public class HavenoWalletListener extends MoneroWalletListener { + + private MoneroWalletListener listener; + + public HavenoWalletListener(MoneroWalletListener listener) { + this.listener = listener; + } + @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?"); - } + public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { + UserThread.execute(new Runnable() { + @Override + public void run() { + listener.onSyncProgress(height, startHeight, endHeight, percentDone, message); + } + }); } - })); - } - // run shutdown threads in parallel - for (Thread thread : threads) thread.start(); + @Override + public void onNewBlock(long height) { + UserThread.execute(new Runnable() { + @Override + public void run() { + listener.onNewBlock(height); + } + }); + } - // wait for all threads - for (Thread thread : threads) { - try { thread.join(); } - catch (InterruptedException e) { e.printStackTrace(); } - } - } + @Override + public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + UserThread.execute(new Runnable() { + @Override + public void run() { + listener.onBalancesChanged(newBalance, newUnlockedBalance); + } + }); + } - /////////////////////////////////////////////////////////////////////////////////////////// - // Withdrawal Send - /////////////////////////////////////////////////////////////////////////////////////////// + @Override + public void onOutputReceived(MoneroOutputWallet output) { + UserThread.execute(new Runnable() { + @Override + public void run() { + listener.onOutputReceived(output); + } + }); + } - public String sendFunds(int fromAccountIndex, - String toAddress, - Coin receiverAmount, - @SuppressWarnings("SameParameterValue") XmrAddressEntry.Context context, - FutureCallback 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 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 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; + @Override + public void onOutputSpent(MoneroOutputWallet output) { + UserThread.execute(new Runnable() { + @Override + public void run() { + listener.onOutputSpent(output); + } + }); } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // 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. - * - * TODO (woodser): this is no longer necessary since not syncing to thread? - */ - public class HavenoWalletListener extends MoneroWalletListener { - - private MoneroWalletListener listener; - - public HavenoWalletListener(MoneroWalletListener listener) { - this.listener = listener; - } - - @Override - public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { - UserThread.execute(new Runnable() { - @Override public void run() { - listener.onSyncProgress(height, startHeight, endHeight, percentDone, message); - } - }); - } - - @Override - public void onNewBlock(long height) { - UserThread.execute(new Runnable() { - @Override public void run() { - listener.onNewBlock(height); - } - }); - } - - @Override - public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { - UserThread.execute(new Runnable() { - @Override public void run() { - listener.onBalancesChanged(newBalance, newUnlockedBalance); - } - }); - } - - @Override - public void onOutputReceived(MoneroOutputWallet output) { - UserThread.execute(new Runnable() { - @Override public void run() { - listener.onOutputReceived(output); - } - }); - } - - @Override - public void onOutputSpent(MoneroOutputWallet output) { - UserThread.execute(new Runnable() { - @Override public void run() { - listener.onOutputSpent(output); - } - }); - } - } } diff --git a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java index f20332c017..9ffc893b77 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/TradeEvents.java @@ -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: diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 638fc0d7ba..8814379b96 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -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; @@ -190,14 +190,14 @@ public class CreateOfferService { paymentAccount, currencyCode, makerFeeAsCoin); - + // select signing arbitrator Mediator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, mediatorManager); // TODO (woodser): using mediator manager for arbitrators OfferPayload offerPayload = new OfferPayload(offerId, creationTime, makerAddress, - pubKeyRing, + pubKeyRingProvider.get(), OfferPayload.Direction.valueOf(direction.name()), priceAsLong, marketPriceMarginParam, diff --git a/core/src/main/java/bisq/core/offer/OfferFilter.java b/core/src/main/java/bisq/core/offer/OfferFilter.java index 51654cec94..57d5c44dc8 100644 --- a/core/src/main/java/bisq/core/offer/OfferFilter.java +++ b/core/src/main/java/bisq/core/offer/OfferFilter.java @@ -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) c -> myInsufficientTradeLimitCache.clear()); @@ -212,13 +212,13 @@ public class OfferFilter { myInsufficientTradeLimitCache.put(offerId, result); return result; } - + public boolean hasValidSignature(Offer offer) { - + // get arbitrator Mediator arbitrator = user.getAcceptedMediatorByAddress(offer.getOfferPayload().getArbitratorSigner()); if (arbitrator == null) return false; // invalid arbitrator - + // validate arbitrator signature return TradeUtils.isArbitratorSignatureValid(offer.getOfferPayload(), arbitrator); } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index d24d1afdde..e7231e6fc9 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -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); diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index 8b2c7ed61e..a869279132 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -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 delayMsgMap = new HashMap<>(); private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet 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(); } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 9e23e611ea..1d3f55a1ce 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -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> 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> extends Sup DisputeListService 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> 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(); }); diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 4863bef3b0..473f344462 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -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 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 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); } diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 395866cc40..966640950a 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -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 { 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 { 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); } diff --git a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java index 6f1bbcc363..898c5ad997 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -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 {}", diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 055577f9a2..eccd1f5c6f 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -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) { diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 7bca9ecc5c..1dc8fe4051 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -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) 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(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 02d914a872..88fb3ff67e 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -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); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerCreateFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerCreateFeeTx.java deleted file mode 100644 index 812630cf7f..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerCreateFeeTx.java +++ /dev/null @@ -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 . - */ - -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); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index 0cdd444055..7dc4447d8d 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -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 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; } diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index bb6e7376f5..e1f05fa5b5 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -399,6 +399,7 @@ public class User implements PersistedDataHost { return userPayload.getPaymentAccounts(); } + @Nullable public ObservableSet getPaymentAccountsAsObservable() { return paymentAccountsAsObservable; } diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 66292f9646..0b898e9d3d 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -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 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(); diff --git a/core/src/test/java/bisq/core/crypto/EncryptionTest.java b/core/src/test/java/bisq/core/crypto/EncryptionTest.java index 61adddbfc2..5ff236ffda 100644 --- a/core/src/test/java/bisq/core/crypto/EncryptionTest.java +++ b/core/src/test/java/bisq/core/crypto/EncryptionTest.java @@ -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 diff --git a/core/src/test/java/bisq/core/crypto/SigTest.java b/core/src/test/java/bisq/core/crypto/SigTest.java index c9727a5f60..d93a29c175 100644 --- a/core/src/test/java/bisq/core/crypto/SigTest.java +++ b/core/src/test/java/bisq/core/crypto/SigTest.java @@ -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 diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index fa9fb9d406..65d290824d 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -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, diff --git a/daemon/src/main/java/bisq/daemon/app/ConsoleInput.java b/daemon/src/main/java/bisq/daemon/app/ConsoleInput.java new file mode 100644 index 0000000000..18bca675c8 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/ConsoleInput.java @@ -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 . + */ +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 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; + } +} diff --git a/daemon/src/main/java/bisq/daemon/app/ConsoleInputReadTask.java b/daemon/src/main/java/bisq/daemon/app/ConsoleInputReadTask.java new file mode 100644 index 0000000000..4bfb2840b8 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/app/ConsoleInputReadTask.java @@ -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 . + */ +package bisq.daemon.app; + +import java.io.*; +import java.util.concurrent.Callable; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ConsoleInputReadTask implements Callable { + 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; + } +} diff --git a/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java b/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java index c9c34714ea..2e1185f5c4 100644 --- a/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java +++ b/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java @@ -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; } } diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java new file mode 100644 index 0000000000..6f2111762b --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcAccountService.java @@ -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 . + */ +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 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 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 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 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 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 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 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 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 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 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 rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional 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)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java index b8b42ea623..e3c6c085eb 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcMoneroConnectionsService.java @@ -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 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 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 responseObserver) { handleRequest(responseObserver, () -> { List connections = coreApi.getMoneroConnections(); - List replyConnections = connections.stream() - .map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList()); + List 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 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 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 responseObserver) { handleRequest(responseObserver, () -> { List connections = coreApi.checkMoneroConnections(); - List replyConnections = connections.stream() - .map(GrpcMoneroConnectionsService::toUriConnection).collect(Collectors.toList()); + List replyConnections = connections.stream() + .map(GrpcMoneroConnectionsService::toUrlConnection).collect(Collectors.toList()); return CheckConnectionsReply.newBuilder().addAllConnections(replyConnections).build(); }); } @@ -176,7 +176,7 @@ class GrpcMoneroConnectionsService extends MoneroConnectionsImplBase { StreamObserver 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) { diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index 0eac7f939c..3d91582398 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -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())) diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java index b8518b1ba1..2ed080629e 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcShutdownService.java @@ -48,9 +48,14 @@ class GrpcShutdownService extends ShutdownServerGrpc.ShutdownServerImplBase { StreamObserver responseObserver) { try { log.info("Shutdown request received."); - var reply = StopReply.newBuilder().build(); - responseObserver.onNext(reply); - responseObserver.onCompleted(); + 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); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index fa7dfaaa7e..bf23b17fd6 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -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 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); } }); diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java index d211b7659a..10e95270cc 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java @@ -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; diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index 3c03e4af8f..1b212dc09f 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -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 { 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 { FXCollections.observableArrayList(Arrays.asList( ApplyFilter.class, TakerVerifyMakerFeePayment.class, - TakerCreateFeeTx.class, BuyerAsTakerCreatesDepositTxInputs.class, TakerProcessesInputsForDepositTxResponse.class, diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java index 6405304a89..3f2998add7 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java @@ -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); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index 472c870cb0..2ee13f5f50 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -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 { 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 { @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 { } 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) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index 37035268c7..4c20923723 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -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); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java index 9d5c77b650..9b55d67b49 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java @@ -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 { 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 { 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 { 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 { } 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")) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index e088239ff8..daa812760e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -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 { 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 { 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 { } }; - if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) { try { tradeWalletService.emergencyPublishPayoutTxFrom2of2MultiSig( txIdAndHex.second, diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java index ca0b232ef1..e1d6bdba0e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java @@ -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; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index a7eed90232..c737c215a8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -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 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() { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 084892f429..d3f72fa189 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -398,7 +398,7 @@ public class PendingTradesView extends ActivatableViewAndModel { private final ClockWatcher clockWatcher; private final WalletsSetup walletsSetup; private final P2PService p2PService; + private final CoreMoneroConnectionsService connectionManager; private final ObservableList p2pNetworkListItems = FXCollections.observableArrayList(); private final SortedList p2pSortedList = new SortedList<>(p2pNetworkListItems); @@ -139,6 +140,7 @@ public class NetworkSettingsView extends ActivatableView { @Inject public NetworkSettingsView(WalletsSetup walletsSetup, P2PService p2PService, + CoreMoneroConnectionsService connectionManager, Preferences preferences, BtcNodes btcNodes, FilterManager filterManager, @@ -148,6 +150,7 @@ public class NetworkSettingsView extends ActivatableView { 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 { 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(), diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 1a096f6f7f..f8335d39fc 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -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; } diff --git a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java index 3d6548b147..82a2aadddc 100644 --- a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java +++ b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java @@ -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); diff --git a/p2p/src/test/java/bisq/network/crypto/EncryptionServiceTests.java b/p2p/src/test/java/bisq/network/crypto/EncryptionServiceTests.java index f4eba98288..bf05dc1de3 100644 --- a/p2p/src/test/java/bisq/network/crypto/EncryptionServiceTests.java +++ b/p2p/src/test/java/bisq/network/crypto/EncryptionServiceTests.java @@ -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 diff --git a/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java b/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java index 21e812078a..59850470a6 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java @@ -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 diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index ea6c28ba18..1b9f1148a9 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -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 { } /////////////////////////////////////////////////////////////////////////////////////////// @@ -703,134 +932,4 @@ message AddressBalanceInfo { int64 balance = 2; 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; -} +} \ No newline at end of file diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 474f5e2448..2d652f3370 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -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; }