diff --git a/.gitignore b/.gitignore index 970241189..ac0d42742 100644 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,7 @@ integration_test/playground.dart # Monero.dart (Monero_C) scripts/monero_c +scripts/android/app_env.fish # iOS generated framework bin ios/MoneroWallet.framework/MoneroWallet ios/WowneroWallet.framework/WowneroWallet diff --git a/PRIVACY.md b/PRIVACY.md index 76cfcc4d3..a5c8eddfb 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -5,7 +5,7 @@ Last modified: January 24, 2024 Introduction ============ - Cake Labs LLC ("Cake Labs", "Company", or "We") respect your privacy and are committed to protecting it through our compliance with this policy. + Cake Labs LLC ("Cake Labs", "Company", or "We") respects your privacy and are committed to protecting it through our compliance with this policy. This policy describes the types of information we may collect from you or that you may provide when you use the App (our "App") and our practices for collecting, using, maintaining, protecting, and disclosing that information. @@ -13,7 +13,7 @@ Introduction - On this App. - In email, text, and other electronic messages between you and this App. It does not apply to information collected by: - - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or + - Us offline or through any other means, including on any other App operated by the Company or any third party (including our affiliates and subsidiaries); or - Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App. Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates. diff --git a/README.md b/README.md index 1c28f92a2..078c4437e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ # Cake Wallet -Cake Wallet is an open source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. +[Cake Wallet](https://cakewallet.com) is an open-source, non-custodial, and private multi-currency crypto wallet for Android, iOS, macOS, and Linux. Cake Wallet includes support for several cryptocurrencies, including: * Monero (XMR) @@ -26,7 +26,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Ethereum (ETH) * Litecoin (LTC) * Bitcoin Cash (BCH) -* Polygon (MATIC) +* Polygon (Pol) * Solana (SOL) * Nano (XNO) * Haven (XHV) @@ -44,7 +44,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Create several wallets * Select your own custom nodes/servers * Address book -* Backup to external location or iCloud +* Backup to an external location or iCloud * Send to OpenAlias, Unstoppable Domains, Yats, and FIO Crypto Handles * Set desired network fee level * Store local transaction notes @@ -161,7 +161,7 @@ The only parts to be translated, if needed, are the values m and s after the var 4. Add the language to `lib/entities/language_service.dart` under both `supportedLocales` and `localeCountryCode`. Use the name of the language in the local language and in English in parentheses after for `supportedLocales`. Use the [ISO 3166-1 alpha-3 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) for `localeCountryCode`. You must choose one country, so choose the country with the most native speakers of this language or is otherwise best associated with this language. -5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with a 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make transparent. Or you can use another program like Photoshop. +5. Add a relevant flag to `assets/images/flags/XXXX.png`, replacing XXXX with the 3 letters localeCountryCode. The image must be 42x26 pixels with 3 pixels of transparent margin on all 4 sides. You can resize the flag with [paint.net](https://www.getpaint.net/) to 36x20 pixels, expand the canvas to 42x26 pixels with the flag anchored in the middle, and then manually delete the 3 pixels on each side to make it transparent. Or you can use another program like Photoshop. 6. Add the new language code to `tool/utils/translation/translation_constants.dart` diff --git a/SECURITY.md b/SECURITY.md index a1b489b76..e7c6baa02 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,4 +9,4 @@ If you need to report a vulnerability, please either: ## Supported Versions -As we don't maintain prevoius versions of the app, only the latest release for each platform is supported and any updates will bump the version number. +As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number. diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 98bbb1236..5b080e3ec 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,7 +14,8 @@ - + + diff --git a/build-guide-linux.md b/build-guide-linux.md index 50ecc76fe..55264fbfa 100644 --- a/build-guide-linux.md +++ b/build-guide-linux.md @@ -55,7 +55,7 @@ Need to install flutter. For this please check section [How to install flutter o ### 3. Verify Installations -Verify that the Flutter have been correctly installed on your system with the following command: +Verify that the Flutter has been correctly installed on your system with the following command: `$ flutter doctor` @@ -163,7 +163,7 @@ And then export bundle: `$ flatpak build-bundle export cake_wallet.flatpak com.cakewallet.CakeWallet` -Result file: `cake_wallet.flatpak` should be generated in current directory. +Result file: `cake_wallet.flatpak` should be generated in the current directory. For install generated flatpak file use: diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart deleted file mode 100644 index 73bc101c4..000000000 --- a/cw_bitcoin/lib/address_from_output.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; - -String addressFromOutputScript(Script script, BasedUtxoNetwork network) { - try { - switch (script.getAddressType()) { - case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wpkh: - return P2wpkhAddress.fromScriptPubkey(script: script).toAddress(network); - case P2shAddressType.p2pkhInP2sh: - return P2shAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2wsh: - return P2wshAddress.fromScriptPubkey(script: script).toAddress(network); - case SegwitAddresType.p2tr: - return P2trAddress.fromScriptPubkey(script: script).toAddress(network); - default: - } - } catch (_) {} - - return ''; -} diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 7e4b5f58f..d4dd8319f 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,34 +1,39 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( this.address, { required this.index, - this.isHidden = false, + bool isChange = false, int txCount = 0, int balance = 0, String name = '', bool isUsed = false, required this.type, - required this.network, + bool? isHidden, }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed; + _isUsed = isUsed, + _isHidden = isHidden ?? isChange, + _isChange = isChange; @override bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; final String address; - bool isHidden; + bool _isHidden; + bool get isHidden => _isHidden; + final bool _isChange; + bool get isChange => _isChange; final int index; int _txCount; int _balance; String _name; bool _isUsed; - BasedUtxoNetwork? network; int get txCount => _txCount; @@ -42,7 +47,12 @@ abstract class BaseBitcoinAddressRecord { bool get isUsed => _isUsed; - void setAsUsed() => _isUsed = true; + void setAsUsed() { + _isUsed = true; + // TODO: check is hidden flow on addr list + _isHidden = true; + } + void setNewName(String label) => _name = label; int get hashCode => address.hashCode; @@ -53,27 +63,43 @@ abstract class BaseBitcoinAddressRecord { } class BitcoinAddressRecord extends BaseBitcoinAddressRecord { + final BitcoinDerivationInfo derivationInfo; + final CWBitcoinDerivationType derivationType; + BitcoinAddressRecord( super.address, { required super.index, - super.isHidden = false, + required this.derivationInfo, + required this.derivationType, + super.isHidden, + super.isChange = false, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, required super.type, String? scriptHash, - required super.network, - }) : scriptHash = scriptHash ?? - (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); + BasedUtxoNetwork? network, + }) { + if (scriptHash == null && network == null) { + throw ArgumentError('either scriptHash or network must be provided'); + } - factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); + } + + factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, + derivationInfo: BitcoinDerivationInfo.fromJSON( + decoded['derivationInfo'] as Map, + ), + derivationType: CWBitcoinDerivationType.values[decoded['derivationType'] as int], isHidden: decoded['isHidden'] as bool? ?? false, + isChange: decoded['isChange'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, name: decoded['name'] as String? ?? '', @@ -83,23 +109,19 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { .firstWhere((type) => type.toString() == decoded['type'] as String) : SegwitAddresType.p2wpkh, scriptHash: decoded['scriptHash'] as String?, - network: network, ); } - String? scriptHash; - - String getScriptHash(BasedUtxoNetwork network) { - if (scriptHash != null) return scriptHash!; - scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); - return scriptHash!; - } + late String scriptHash; @override String toJSON() => json.encode({ 'address': address, 'index': index, + 'derivationInfo': derivationInfo.toJSON(), + 'derivationType': derivationType.index, 'isHidden': isHidden, + 'isChange': isChange, 'isUsed': isUsed, 'txCount': txCount, 'name': name, @@ -107,21 +129,51 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 'type': type.toString(), 'scriptHash': scriptHash, }); + + @override + operator ==(Object other) { + if (identical(this, other)) return true; + + return other is BitcoinAddressRecord && + other.address == address && + other.index == index && + other.derivationInfo == derivationInfo && + other.scriptHash == scriptHash && + other.type == type && + other.derivationType == derivationType; + } + + @override + int get hashCode => + address.hashCode ^ + index.hashCode ^ + derivationInfo.hashCode ^ + scriptHash.hashCode ^ + type.hashCode ^ + derivationType.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { + int get labelIndex => index; + final String? labelHex; + + static bool isChangeAddress(int labelIndex) => labelIndex == 0; + BitcoinSilentPaymentAddressRecord( super.address, { - required super.index, - super.isHidden = false, + required int labelIndex, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, - required this.silentPaymentTweak, - required super.network, - required super.type, - }) : super(); + super.type = SilentPaymentsAddresType.p2sp, + super.isHidden, + this.labelHex, + }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) { + if (labelIndex != 1 && labelHex == null) { + throw ArgumentError('label must be provided for silent address index != 1'); + } + } factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { @@ -129,36 +181,68 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, + labelIndex: decoded['labelIndex'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, name: decoded['name'] as String? ?? '', balance: decoded['balance'] as int? ?? 0, - network: (decoded['network'] as String?) == null - ? network - : BasedUtxoNetwork.fromName(decoded['network'] as String), - silentPaymentTweak: decoded['silent_payment_tweak'] as String?, - type: decoded['type'] != null && decoded['type'] != '' - ? BitcoinAddressType.values - .firstWhere((type) => type.toString() == decoded['type'] as String) - : SilentPaymentsAddresType.p2sp, + labelHex: decoded['labelHex'] as String?, ); } - final String? silentPaymentTweak; - @override String toJSON() => json.encode({ 'address': address, - 'index': index, - 'isHidden': isHidden, + 'labelIndex': labelIndex, 'isUsed': isUsed, 'txCount': txCount, 'name': name, 'balance': balance, 'type': type.toString(), - 'network': network?.value, - 'silent_payment_tweak': silentPaymentTweak, + 'labelHex': labelHex, + }); +} + +class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { + final ECPrivate spendKey; + + BitcoinReceivedSPAddressRecord( + super.address, { + required super.labelIndex, + super.txCount = 0, + super.balance = 0, + super.name = '', + super.isUsed = false, + required this.spendKey, + super.type = SegwitAddresType.p2tr, + super.labelHex, + }) : super(isHidden: true); + + factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + final decoded = json.decode(jsonSource) as Map; + + return BitcoinReceivedSPAddressRecord( + decoded['address'] as String, + labelIndex: decoded['index'] as int, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + labelHex: decoded['label'] as String?, + spendKey: ECPrivate.fromHex(decoded['spendKey'] as String), + ); + } + + @override + String toJSON() => json.encode({ + 'address': address, + 'labelIndex': labelIndex, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + 'labelHex': labelHex, + 'spend_key': spendKey.toString(), }); } diff --git a/cw_bitcoin/lib/bitcoin_amount_format.dart b/cw_bitcoin/lib/bitcoin_amount_format.dart deleted file mode 100644 index d5a42d984..000000000 --- a/cw_bitcoin/lib/bitcoin_amount_format.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:cw_core/crypto_amount_format.dart'; - -const bitcoinAmountLength = 8; -const bitcoinAmountDivider = 100000000; -final bitcoinAmountFormat = NumberFormat() - ..maximumFractionDigits = bitcoinAmountLength - ..minimumFractionDigits = 1; - -String bitcoinAmountToString({required int amount}) => bitcoinAmountFormat.format( - cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider)); - -double bitcoinAmountToDouble({required int amount}) => - cryptoAmountToDouble(amount: amount, divider: bitcoinAmountDivider); - -int stringDoubleToBitcoinAmount(String amount) { - int result = 0; - - try { - result = (double.parse(amount) * bitcoinAmountDivider).round(); - } catch (e) { - result = 0; - } - - return result; -} diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index a02c51c69..415ae0e98 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; @@ -12,8 +11,7 @@ class BitcoinHardwareWalletService { final LedgerConnection ledgerConnection; - Future> getAvailableAccounts( - {int index = 0, int limit = 5}) async { + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); @@ -23,13 +21,14 @@ class BitcoinHardwareWalletService { for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = - await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); - Bip32Slip10Secp256k1 hd = - Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); + final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); + final bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress( - hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final fullPath = Bip32PathParser.parse(derivationPath).addElem(Bip32KeyIndex(0)); + + final address = ECPublic.fromBip32(bip32.derive(fullPath).publicKey) + .toP2wpkhAddress() + .toAddress(BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index 01e905fb0..f6d769735 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,13 +1,17 @@ -import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coin_type.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.outputs, - {required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any}); + BitcoinTransactionCredentials( + this.outputs, { + required this.priority, + this.feeRate, + this.coinTypeToSpendFrom = UnspentCoinType.any, + }); final List outputs; - final BitcoinTransactionPriority? priority; + final TransactionPriority? priority; final int? feeRate; final UnspentCoinType coinTypeToSpendFrom; } diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index d1f45a545..26a4c2f62 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,49 +1,57 @@ import 'package:cw_core/transaction_priority.dart'; class BitcoinTransactionPriority extends TransactionPriority { - const BitcoinTransactionPriority({required String title, required int raw}) - : super(title: title, raw: raw); + const BitcoinTransactionPriority({required super.title, required super.raw}); - static const List all = [fast, medium, slow, custom]; - static const BitcoinTransactionPriority slow = - BitcoinTransactionPriority(title: 'Slow', raw: 0); - static const BitcoinTransactionPriority medium = - BitcoinTransactionPriority(title: 'Medium', raw: 1); - static const BitcoinTransactionPriority fast = - BitcoinTransactionPriority(title: 'Fast', raw: 2); +// Unimportant: the lowest possible, confirms when it confirms no matter how long it takes + static const BitcoinTransactionPriority unimportant = + BitcoinTransactionPriority(title: 'Unimportant', raw: 0); +// Normal: low fee, confirms in a reasonable time, normal because in most cases more than this is not needed, gets you in the next 2-3 blocks (about 1 hour) + static const BitcoinTransactionPriority normal = + BitcoinTransactionPriority(title: 'Normal', raw: 1); +// Elevated: medium fee, confirms soon, elevated because it's higher than normal, gets you in the next 1-2 blocks (about 30 mins) + static const BitcoinTransactionPriority elevated = + BitcoinTransactionPriority(title: 'Elevated', raw: 2); +// Priority: high fee, expected in the next block (about 10 mins). + static const BitcoinTransactionPriority priority = + BitcoinTransactionPriority(title: 'Priority', raw: 3); +// Custom: any fee, user defined static const BitcoinTransactionPriority custom = - BitcoinTransactionPriority(title: 'Custom', raw: 3); + BitcoinTransactionPriority(title: 'Custom', raw: 4); static BitcoinTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: - return slow; + return unimportant; case 1: - return medium; + return normal; case 2: - return fast; + return elevated; case 3: + return priority; + case 4: return custom; default: - throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for TransactionPriority deserialize'); } } - String get units => 'sat'; - @override String toString() { var label = ''; switch (this) { - case BitcoinTransactionPriority.slow: - label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; + case BitcoinTransactionPriority.unimportant: + label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinTransactionPriority.medium: - label = 'Medium'; // S.current.transaction_priority_medium; + case BitcoinTransactionPriority.normal: + label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; break; - case BitcoinTransactionPriority.fast: - label = 'Fast'; + case BitcoinTransactionPriority.elevated: + label = 'Elevated'; + break; // S.current.transaction_priority_fast; + case BitcoinTransactionPriority.priority: + label = 'Priority'; break; // S.current.transaction_priority_fast; case BitcoinTransactionPriority.custom: label = 'Custom'; @@ -61,19 +69,22 @@ class BitcoinTransactionPriority extends TransactionPriority { } } -class LitecoinTransactionPriority extends BitcoinTransactionPriority { - const LitecoinTransactionPriority({required String title, required int raw}) +class ElectrumTransactionPriority extends TransactionPriority { + const ElectrumTransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); - static const List all = [fast, medium, slow]; - static const LitecoinTransactionPriority slow = - LitecoinTransactionPriority(title: 'Slow', raw: 0); - static const LitecoinTransactionPriority medium = - LitecoinTransactionPriority(title: 'Medium', raw: 1); - static const LitecoinTransactionPriority fast = - LitecoinTransactionPriority(title: 'Fast', raw: 2); + static const List all = [fast, medium, slow, custom]; - static LitecoinTransactionPriority deserialize({required int raw}) { + static const ElectrumTransactionPriority slow = + ElectrumTransactionPriority(title: 'Slow', raw: 0); + static const ElectrumTransactionPriority medium = + ElectrumTransactionPriority(title: 'Medium', raw: 1); + static const ElectrumTransactionPriority fast = + ElectrumTransactionPriority(title: 'Fast', raw: 2); + static const ElectrumTransactionPriority custom = + ElectrumTransactionPriority(title: 'Custom', raw: 3); + + static ElectrumTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return slow; @@ -81,27 +92,31 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { return medium; case 2: return fast; + case 3: + return custom; default: - throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); } } - @override - String get units => 'Litoshi'; + String get units => 'sat'; @override String toString() { var label = ''; switch (this) { - case LitecoinTransactionPriority.slow: - label = 'Slow'; // S.current.transaction_priority_slow; + case ElectrumTransactionPriority.slow: + label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case LitecoinTransactionPriority.medium: + case ElectrumTransactionPriority.medium: label = 'Medium'; // S.current.transaction_priority_medium; break; - case LitecoinTransactionPriority.fast: - label = 'Fast'; // S.current.transaction_priority_fast; + case ElectrumTransactionPriority.fast: + label = 'Fast'; + break; // S.current.transaction_priority_fast; + case ElectrumTransactionPriority.custom: + label = 'Custom'; break; default: break; @@ -110,54 +125,171 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { return label; } + String labelWithRate(int rate, int? customRate) { + final rateValue = this == custom ? customRate ??= 0 : rate; + return '${toString()} ($rateValue ${units}/byte)'; + } } -class BitcoinCashTransactionPriority extends BitcoinTransactionPriority { - const BitcoinCashTransactionPriority({required String title, required int raw}) - : super(title: title, raw: raw); - static const List all = [fast, medium, slow]; - static const BitcoinCashTransactionPriority slow = - BitcoinCashTransactionPriority(title: 'Slow', raw: 0); - static const BitcoinCashTransactionPriority medium = - BitcoinCashTransactionPriority(title: 'Medium', raw: 1); - static const BitcoinCashTransactionPriority fast = - BitcoinCashTransactionPriority(title: 'Fast', raw: 2); +class LitecoinTransactionPriority extends ElectrumTransactionPriority { + const LitecoinTransactionPriority({required super.title, required super.raw}); - static BitcoinCashTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: + @override + String get units => 'lit'; +} + +class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { + const BitcoinCashTransactionPriority({required super.title, required super.raw}); + + @override + String get units => 'satoshi'; +} + +class BitcoinTransactionPriorities implements TransactionPriorities { + const BitcoinTransactionPriorities({ + required this.unimportant, + required this.normal, + required this.elevated, + required this.priority, + required this.custom, + }); + + final int unimportant; + final int normal; + final int elevated; + final int priority; + final int custom; + + @override + int operator [](TransactionPriority type) { + switch (type) { + case BitcoinTransactionPriority.unimportant: + return unimportant; + case BitcoinTransactionPriority.normal: + return normal; + case BitcoinTransactionPriority.elevated: + return elevated; + case BitcoinTransactionPriority.priority: + return priority; + case BitcoinTransactionPriority.custom: + return custom; + default: + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); + } + } + + @override + String labelWithRate(TransactionPriority priorityType, [int? rate]) { + late int rateValue; + + if (priorityType == BitcoinTransactionPriority.custom) { + if (rate == null) { + throw Exception('Rate must be provided for custom transaction priority'); + } + rateValue = rate; + } else { + rateValue = this[priorityType]; + } + + return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)'; + } + + @override + Map toJson() { + return { + 'unimportant': unimportant, + 'normal': normal, + 'elevated': elevated, + 'priority': priority, + 'custom': custom, + }; + } + + static BitcoinTransactionPriorities fromJson(Map json) { + return BitcoinTransactionPriorities( + unimportant: json['unimportant'] as int, + normal: json['normal'] as int, + elevated: json['elevated'] as int, + priority: json['priority'] as int, + custom: json['custom'] as int, + ); + } +} + +class ElectrumTransactionPriorities implements TransactionPriorities { + const ElectrumTransactionPriorities({ + required this.slow, + required this.medium, + required this.fast, + required this.custom, + }); + + final int slow; + final int medium; + final int fast; + final int custom; + + @override + int operator [](TransactionPriority type) { + switch (type) { + case ElectrumTransactionPriority.slow: return slow; - case 1: + case ElectrumTransactionPriority.medium: return medium; - case 2: + case ElectrumTransactionPriority.fast: return fast; + case ElectrumTransactionPriority.custom: + return custom; default: - throw Exception('Unexpected token: $raw for BitcoinCashTransactionPriority deserialize'); + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } } @override - String get units => 'Satoshi'; + String labelWithRate(TransactionPriority priorityType, [int? rate]) { + return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; + } - @override - String toString() { - var label = ''; - - switch (this) { - case BitcoinCashTransactionPriority.slow: - label = 'Slow'; // S.current.transaction_priority_slow; - break; - case BitcoinCashTransactionPriority.medium: - label = 'Medium'; // S.current.transaction_priority_medium; - break; - case BitcoinCashTransactionPriority.fast: - label = 'Fast'; // S.current.transaction_priority_fast; - break; - default: - break; + factory ElectrumTransactionPriorities.fromList(List list) { + if (list.length != 3) { + throw Exception( + 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); } - return label; + return ElectrumTransactionPriorities( + slow: list[0], + medium: list[1], + fast: list[2], + custom: 0, + ); + } + + @override + Map toJson() { + return { + 'slow': slow, + 'medium': medium, + 'fast': fast, + 'custom': custom, + }; + } + + static ElectrumTransactionPriorities fromJson(Map json) { + return ElectrumTransactionPriorities( + slow: json['slow'] as int, + medium: json['medium'] as int, + fast: json['fast'] as int, + custom: json['custom'] as int, + ); } } +TransactionPriorities deserializeTransactionPriorities(Map json) { + if (json.containsKey('unimportant')) { + return BitcoinTransactionPriorities.fromJson(json); + } else if (json.containsKey('slow')) { + return ElectrumTransactionPriorities.fromJson(json); + } else { + throw Exception('Unexpected token: $json for deserializeTransactionPriorities'); + } +} diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 3691a7a22..93d9c25d5 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,17 +1,21 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; class BitcoinUnspent extends Unspent { BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout) : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); + factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) => + BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout); + factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) => BitcoinUnspent( address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), json['tx_hash'] as String, - json['value'] as int, - json['tx_pos'] as int, + int.parse(json['value'].toString()), + int.parse(json['tx_pos'].toString()), ); Map toJson() { @@ -25,43 +29,13 @@ class BitcoinUnspent extends Unspent { } final BaseBitcoinAddressRecord bitcoinAddressRecord; -} - -class BitcoinSilentPaymentsUnspent extends BitcoinUnspent { - BitcoinSilentPaymentsUnspent( - BitcoinSilentPaymentAddressRecord addressRecord, - String hash, - int value, - int vout, { - required this.silentPaymentTweak, - required this.silentPaymentLabel, - }) : super(addressRecord, hash, value, vout); @override - factory BitcoinSilentPaymentsUnspent.fromJSON( - BitcoinSilentPaymentAddressRecord? address, Map json) => - BitcoinSilentPaymentsUnspent( - address ?? BitcoinSilentPaymentAddressRecord.fromJSON(json['address_record'].toString()), - json['tx_hash'] as String, - json['value'] as int, - json['tx_pos'] as int, - silentPaymentTweak: json['silent_payment_tweak'] as String?, - silentPaymentLabel: json['silent_payment_label'] as String?, - ); - - @override - Map toJson() { - final json = { - 'address_record': bitcoinAddressRecord.toJSON(), - 'tx_hash': hash, - 'value': value, - 'tx_pos': vout, - 'silent_payment_tweak': silentPaymentTweak, - 'silent_payment_label': silentPaymentLabel, - }; - return json; + bool operator ==(Object o) { + if (identical(this, o)) return true; + return o is BitcoinUnspent && hash == o.hash && vout == o.vout; } - String? silentPaymentTweak; - String? silentPaymentLabel; + @override + int get hashCode => Object.hash(hash, vout); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 908897845..748acdbbe 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,11 +1,16 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:isolate'; -import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:cw_bitcoin/psbt_transaction_builder.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; @@ -13,26 +18,33 @@ import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:sp_scanner/sp_scanner.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { + StreamSubscription? _receiveStream; + BitcoinWalletBase({ required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, - Uint8List? seedBytes, + List? seedBytes, String? mnemonic, String? xpub, String? addressPageType, @@ -45,6 +57,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, + required bool mempoolAPIEnabled, + super.hdWallets, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -61,16 +75,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, seedBytes: seedBytes, encryptionFileUtils: encryptionFileUtils, - currency: networkParam == BitcoinNetwork.testnet - ? CryptoCurrency.tbtc - : CryptoCurrency.btc, + currency: + networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ) { - // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) - // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) - // String derivationPath = walletInfo.derivationInfo!.derivationPath!; - // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; - // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); walletAddresses = BitcoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -78,17 +87,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = - this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } @@ -107,21 +112,38 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, + required bool mempoolAPIEnabled, }) async { - late Uint8List seedBytes; + late List seedBytes; + final Map hdWallets = {}; - switch (walletInfo.derivationInfo?.derivationType) { - case DerivationType.bip39: - seedBytes = await bip39.mnemonicToSeed( - mnemonic, - passphrase: passphrase ?? "", - ); + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); break; - case DerivationType.electrum: - default: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + } else { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v1 seed error: $e"); + } + } + break; + } } + + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -138,6 +160,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, + mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: hdWallets, ); } @@ -148,6 +172,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required String password, required EncryptionFileUtils encryptionFileUtils, required bool alwaysScan, + required bool mempoolAPIEnabled, }) async { final network = walletInfo.network != null ? BasedUtxoNetwork.fromName(walletInfo.network!) @@ -189,28 +214,43 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= - snp?.derivationPath ?? electrum_path; - walletInfo.derivationInfo!.derivationType ??= - snp?.derivationType ?? DerivationType.electrum; + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; - Uint8List? seedBytes = null; + List? seedBytes = null; + final Map hdWallets = {}; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; if (mnemonic != null) { - switch (walletInfo.derivationInfo!.derivationType) { - case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + break; - case DerivationType.bip39: - default: - seedBytes = await bip39.mnemonicToSeed( - mnemonic, - passphrase: passphrase ?? '', - ); + } else { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = + Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + print("electrum_v1 seed error: $e"); + } + } + break; + } } + + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; } return BitcoinWallet( @@ -231,9 +271,59 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: hdWallets, ); } + Future getNodeIsElectrs() async { + final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List; + + if (version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + } + + node!.isElectrs = false; + node!.save(); + return node!.isElectrs!; + } + + Future getNodeSupportsSilentPayments() async { + return true; + // As of today (august 2024), only ElectrumRS supports silent payments + // if (!(await getNodeIsElectrs())) { + // return false; + // } + + // if (node == null) { + // return false; + // } + + // try { + // final tweaksResponse = await electrumClient.getTweaks(height: 0); + + // if (tweaksResponse != null) { + // node!.supportsSilentPayments = true; + // node!.save(); + // return node!.supportsSilentPayments!; + // } + // } on RequestFailedTimeoutException catch (_) { + // node!.supportsSilentPayments = false; + // node!.save(); + // return node!.supportsSilentPayments!; + // } catch (_) {} + + // node!.supportsSilentPayments = false; + // node!.save(); + // return node!.supportsSilentPayments!; + } + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; @@ -261,9 +351,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final psbtReadyInputs = []; for (final utxo in utxos) { final rawTx = - await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); - final publicKeyAndDerivationPath = - publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; + (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -275,8 +364,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); } - final psbt = PSBTTransactionBuild( - inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); + final psbt = + PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF); final rawHex = await _bitcoinLedgerApp!.signPsbt(psbt: psbt.psbt); return BtcTransaction.fromRaw(BytesUtils.toHexString(rawHex)); @@ -286,20 +375,443 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future signMessage(String message, {String? address = null}) async { if (walletInfo.isHardwareWallet) { final addressEntry = address != null - ? walletAddresses.allAddresses - .firstWhere((element) => element.address == address) + ? walletAddresses.allAddresses.firstWhere((element) => element.address == address) : null; final index = addressEntry?.index ?? 0; - final isChange = addressEntry?.isHidden == true ? 1 : 0; + final isChange = addressEntry?.isChange == true ? 1 : 0; final accountPath = walletInfo.derivationInfo?.derivationPath; - final derivationPath = - accountPath != null ? "$accountPath/$isChange/$index" : null; + final derivationPath = accountPath != null ? "$accountPath/$isChange/$index" : null; - final signature = await _bitcoinLedgerApp!.signMessage( - message: ascii.encode(message), signDerivationPath: derivationPath); + final signature = await _bitcoinLedgerApp! + .signMessage(message: ascii.encode(message), signDerivationPath: derivationPath); return base64Encode(signature); } return super.signMessage(message, address: address); } + + @action + Future setSilentPaymentsScanning(bool active) async { + silentPaymentsScanningActive = active; + + if (active) { + syncStatus = AttemptingScanSyncStatus(); + + final tip = currentChainTip!; + + if (tip == walletInfo.restoreHeight) { + syncStatus = SyncedTipSyncStatus(tip); + return; + } + + if (tip > walletInfo.restoreHeight) { + _setListeners(walletInfo.restoreHeight); + } + } else { + alwaysScan = false; + + // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + + // if (rpc!.isConnected) { + // syncStatus = SyncedSyncStatus(); + // } else { + // syncStatus = NotConnectedSyncStatus(); + // } + } + } + + // @override + // @action + // Future updateAllUnspents() async { + // List updatedUnspentCoins = []; + + // // Update unspents stored from scanned silent payment transactions + // transactionHistory.transactions.values.forEach((tx) { + // if (tx.unspents != null) { + // updatedUnspentCoins.addAll(tx.unspents!); + // } + // }); + + // // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating + // walletAddresses.allAddresses + // .where((element) => element.type != SegwitAddresType.mweb) + // .forEach((addr) { + // if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + // }); + + // await Future.wait(walletAddresses.allAddresses + // .where((element) => element.type != SegwitAddresType.mweb) + // .map((address) async { + // updatedUnspentCoins.addAll(await fetchUnspent(address)); + // })); + + // unspentCoins.addAll(updatedUnspentCoins); + + // if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + // unspentCoins.forEach((coin) => addCoinInfo(coin)); + // return; + // } + + // await updateCoins(unspentCoins.toSet()); + // await refreshUnspentCoinsInfo(); + // } + + @override + void updateCoin(BitcoinUnspent coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + addCoinInfo(coin); + } + } + + @action + @override + Future startSync() async { + await _setInitialScanHeight(); + + await super.startSync(); + + if (alwaysScan == true) { + _setListeners(walletInfo.restoreHeight); + } + } + + @action + @override + Future rescan({required int height, bool? doSingleScan}) async { + silentPaymentsScanningActive = true; + _setListeners(height, doSingleScan: doSingleScan); + } + + // @action + // Future registerSilentPaymentsKey(bool register) async { + // silentPaymentsScanningActive = active; + + // if (active) { + // syncStatus = AttemptingScanSyncStatus(); + + // final tip = await getUpdatedChainTip(); + + // if (tip == walletInfo.restoreHeight) { + // syncStatus = SyncedTipSyncStatus(tip); + // return; + // } + + // if (tip > walletInfo.restoreHeight) { + // _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + // } + // } else { + // alwaysScan = false; + + // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + + // if (electrumClient.isConnected) { + // syncStatus = SyncedSyncStatus(); + // } else { + // syncStatus = NotConnectedSyncStatus(); + // } + // } + // } + + @action + Future registerSilentPaymentsKey() async { + // final registered = await electrumClient.tweaksRegister( + // secViewKey: walletAddresses.silentAddress!.b_scan.toHex(), + // pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(), + // labels: walletAddresses.silentAddresses + // .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + // .map((addr) => addr.labelIndex) + // .toList(), + // ); + + // print("registered: $registered"); + } + + @action + void _updateSilentAddressRecord(BitcoinUnspent unspent) { + final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; + final silentAddress = walletAddresses.silentAddress!; + final silentPaymentAddress = SilentPaymentAddress( + version: silentAddress.version, + B_scan: silentAddress.B_scan, + B_spend: receiveAddressRecord.labelHex != null + ? silentAddress.B_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)), + ) + : silentAddress.B_spend, + ); + + final addressRecord = walletAddresses.silentAddresses + .firstWhere((address) => address.address == silentPaymentAddress.toString()); + addressRecord.txCount += 1; + addressRecord.balance += unspent.value; + + walletAddresses.addSilentAddresses( + [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], + ); + } + + @override + @action + Future handleWorkerResponse(dynamic message) async { + super.handleWorkerResponse(message); + + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + final workerMethod = messageJson['method'] as String; + + switch (workerMethod) { + case ElectrumRequestMethods.tweaksSubscribeMethod: + final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); + onTweaksSyncResponse(response.result); + break; + } + } + + @action + Future onTweaksSyncResponse(TweaksSyncResponse result) async { + if (result.transactions?.isNotEmpty == true) { + for (final map in result.transactions!.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + final existingTxInfo = transactionHistory.transactions[txid]; + final txAlreadyExisted = existingTxInfo != null; + + // Updating tx after re-scanned + if (txAlreadyExisted) { + existingTxInfo.amount = tx.amount; + existingTxInfo.confirmations = tx.confirmations; + existingTxInfo.height = tx.height; + + final newUnspents = tx.unspents! + .where((unspent) => !(existingTxInfo.unspents?.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value) ?? + false)) + .toList(); + + if (newUnspents.isNotEmpty) { + newUnspents.forEach(_updateSilentAddressRecord); + + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + + final newAmount = newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + + if (existingTxInfo.direction == TransactionDirection.incoming) { + existingTxInfo.amount += newAmount; + } + + // Updates existing TX + transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; + } + } else { + // else: First time seeing this TX after scanning + tx.unspents!.forEach(_updateSilentAddressRecord); + + // Add new TX record + transactionHistory.addOne(tx); + // Update balance record + balance[currency]!.confirmed += tx.amount; + } + + await updateAllUnspents(); + } + } + } + + final newSyncStatus = result.syncStatus; + + if (newSyncStatus != null) { + if (newSyncStatus is UnsupportedSyncStatus) { + nodeSupportsSilentPayments = false; + } + + if (newSyncStatus is SyncingSyncStatus) { + syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); + } else { + syncStatus = newSyncStatus; + } + + await walletInfo.updateRestoreHeight(result.height!); + } + } + + @action + Future _setListeners(int height, {bool? doSingleScan}) async { + if (currentChainTip == null) { + throw Exception("currentChainTip is null"); + } + + final chainTip = currentChainTip!; + + if (chainTip == height) { + syncStatus = SyncedSyncStatus(); + return; + } + + syncStatus = AttemptingScanSyncStatus(); + + workerSendPort!.send( + ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData( + silentAddress: walletAddresses.silentAddress!, + network: network, + height: height, + chainTip: chainTip, + transactionHistoryIds: transactionHistory.transactions.keys.toList(), + labels: walletAddresses.labels, + labelIndexes: walletAddresses.silentAddresses + .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + .map((addr) => addr.labelIndex) + .toList(), + isSingleScan: doSingleScan ?? false, + ), + ).toJson(), + ); + } + + @override + @action + Future> fetchTransactions() async { + throw UnimplementedError(); + // try { + // final Map historiesWithDetails = {}; + + // await Future.wait( + // BITCOIN_ADDRESS_TYPES.map( + // (type) => fetchTransactionsForAddressType(historiesWithDetails, type), + // ), + // ); + + // transactionHistory.transactions.values.forEach((tx) async { + // final isPendingSilentPaymentUtxo = + // (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null; + + // if (isPendingSilentPaymentUtxo) { + // final info = await fetchTransactionInfo(hash: tx.id, height: tx.height); + + // if (info != null) { + // tx.confirmations = info.confirmations; + // tx.isPending = tx.confirmations == 0; + // transactionHistory.addOne(tx); + // await transactionHistory.save(); + // } + // } + // }); + + // return historiesWithDetails; + // } catch (e) { + // print("fetchTransactions $e"); + // return {}; + // } + } + + @override + @action + Future updateTransactions([List? addresses]) async { + super.updateTransactions(); + + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null && + tx.unspents!.isNotEmpty && + tx.height != null && + tx.height! > 0 && + (currentChainTip ?? 0) > 0) { + tx.confirmations = currentChainTip! - tx.height! + 1; + } + }); + } + + // @action + // Future fetchBalances() async { + // final balance = await super.fetchBalances(); + + // int totalFrozen = balance.frozen; + // int totalConfirmed = balance.confirmed; + + // // Add values from unspent coins that are not fetched by the address list + // // i.e. scanned silent payments + // transactionHistory.transactions.values.forEach((tx) { + // if (tx.unspents != null) { + // tx.unspents!.forEach((unspent) { + // if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + // if (unspent.isFrozen) totalFrozen += unspent.value; + // totalConfirmed += unspent.value; + // } + // }); + // } + // }); + + // return ElectrumBalance( + // confirmed: totalConfirmed, + // unconfirmed: balance.unconfirmed, + // frozen: totalFrozen, + // ); + // } + + @override + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + super.onHeadersResponse(response); + + _setInitialScanHeight(); + + // New headers received, start scanning + if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + _setListeners(walletInfo.restoreHeight); + } + } + + Future _setInitialScanHeight() async { + final validChainTip = currentChainTip != null && currentChainTip != 0; + if (validChainTip && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); + } + } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); + + @override + @action + void syncStatusReaction(SyncStatus syncStatus) { + switch (syncStatus.runtimeType) { + case SyncingSyncStatus: + return; + case SyncedTipSyncStatus: + // Message is shown on the UI for 3 seconds, then reverted to synced + Timer(Duration(seconds: 3), () { + if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); + }); + break; + default: + super.syncStatusReaction(syncStatus); + } + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 04a3cae36..c5419a6f0 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,7 +1,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -12,33 +11,101 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, required super.network, required super.isHardwareWallet, + required super.hdWallets, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, super.initialSilentAddresses, super.initialSilentAddressIndex = 0, - super.masterHd, }) : super(walletInfo); @override - String getAddress( - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { - if (addressType == P2pkhAddressType.p2pkh) - return generateP2PKHAddress(hd: hd, index: index, network: network); + Future init() async { + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); - if (addressType == SegwitAddresType.p2tr) - return generateP2TRAddress(hd: hd, index: index, network: network); + if (!isHardwareWallet) { + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(type: SegwitAddresType.p2tr); + await generateInitialAddresses(type: SegwitAddresType.p2wsh); + } - if (addressType == SegwitAddresType.p2wsh) - return generateP2WSHAddress(hd: hd, index: index, network: network); + await updateAddressesInBox(); + } - if (addressType == P2shAddressType.p2wpkhInP2sh) - return generateP2SHAddress(hd: hd, index: index, network: network); + @override + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + final hdWallet = hdWallets[derivationType]!; - return generateP2WPKHAddress(hd: hd, index: index, network: network); + if (derivationType == CWBitcoinDerivationType.old) { + final pub = hdWallet + .childKey(Bip32KeyIndex(isChange ? 1 : 0)) + .childKey(Bip32KeyIndex(index)) + .publicKey; + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return ECPublic.fromBip32(pub).toP2pkhAddress(); + case SegwitAddresType.p2tr: + return ECPublic.fromBip32(pub).toP2trAddress(); + case SegwitAddresType.p2wsh: + return ECPublic.fromBip32(pub).toP2wshAddress(); + case P2shAddressType.p2wpkhInP2sh: + return ECPublic.fromBip32(pub).toP2wpkhInP2sh(); + case SegwitAddresType.p2wpkh: + return ECPublic.fromBip32(pub).toP2wpkhAddress(); + default: + throw ArgumentError('Invalid address type'); + } + } + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromDerivation( + bip32: hdWallet, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); + case SegwitAddresType.p2tr: + return P2trAddress.fromDerivation( + bip32: hdWallet, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromDerivation( + bip32: hdWallet, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); + case P2shAddressType.p2wpkhInP2sh: + return P2shAddress.fromDerivation( + bip32: hdWallet, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + type: P2shAddressType.p2wpkhInP2sh, + ); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromDerivation( + bip32: hdWallet, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); + default: + throw ArgumentError('Invalid address type'); + } } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index a1b1418b8..bab72b625 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -7,8 +7,6 @@ class BitcoinNewWalletCredentials extends WalletCredentials { required String name, WalletInfo? walletInfo, String? password, - DerivationType? derivationType, - String? derivationPath, String? passphrase, this.mnemonic, String? parentAddress, @@ -28,19 +26,15 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { required String name, required String password, required this.mnemonic, + required super.derivations, WalletInfo? walletInfo, - required DerivationType derivationType, - required String derivationPath, String? passphrase, }) : super( - name: name, - password: password, - passphrase: passphrase, - walletInfo: walletInfo, - derivationInfo: DerivationInfo( - derivationType: derivationType, - derivationPath: derivationPath, - )); + name: name, + password: password, + passphrase: passphrase, + walletInfo: walletInfo, + ); final String mnemonic; } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 06f2082e4..b310c1db3 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; -import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -14,18 +13,24 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:collection/collection.dart'; -import 'package:bip39/bip39.dart' as bip39; class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + BitcoinWalletService( + this.walletInfoSource, + this.unspentCoinsInfoSource, + this.alwaysScan, + this.isDirect, + this.mempoolAPIEnabled, + ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; + final bool mempoolAPIEnabled; final bool isDirect; @override @@ -37,7 +42,7 @@ class BitcoinWalletService extends WalletService< credentials.walletInfo?.network = network.value; final String mnemonic; - switch ( credentials.walletInfo?.derivationInfo?.derivationType) { + switch (credentials.walletInfo?.derivationInfo?.derivationType) { case DerivationType.bip39: final strength = credentials.seedPhraseLength == 24 ? 256 : 128; @@ -57,6 +62,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); @@ -80,6 +86,7 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -93,6 +100,7 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -118,6 +126,7 @@ class BitcoinWalletService extends WalletService< walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -146,6 +155,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -160,10 +170,6 @@ class BitcoinWalletService extends WalletService< @override Future restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - if (!validateMnemonic(credentials.mnemonic) && !bip39.validateMnemonic(credentials.mnemonic)) { - throw BitcoinMnemonicIsIncorrectException(); - } - final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet; credentials.walletInfo?.network = network.value; @@ -175,8 +181,8 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); - await wallet.save(); await wallet.init(); return wallet; } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a18c038fa..34849def3 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -3,17 +3,9 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; -enum ConnectionStatus { connected, disconnected, connecting, failed } - -String jsonrpcparams(List params) { - final _params = params.map((val) => '"${val.toString()}"').join(','); - return '[$_params]'; -} - String jsonrpc( {required String method, required List params, @@ -317,13 +309,38 @@ class ElectrumClient { Future> getHeader({required int height}) async => await call(method: 'blockchain.block.get_header', params: [height]) as Map; - BehaviorSubject? tweaksSubscribe({required int height, required int count}) { - return subscribe( - id: 'blockchain.tweaks.subscribe', - method: 'blockchain.tweaks.subscribe', - params: [height, count, false], - ); - } + BehaviorSubject? tweaksSubscribe({required int height, required int count}) => + subscribe( + id: 'blockchain.tweaks.subscribe', + method: 'blockchain.tweaks.subscribe', + params: [height, count, true], + ); + + Future tweaksRegister({ + required String secViewKey, + required String pubSpendKey, + List labels = const [], + }) => + call( + method: 'blockchain.tweaks.register', + params: [secViewKey, pubSpendKey, labels], + ); + + Future tweaksErase({required String pubSpendKey}) => call( + method: 'blockchain.tweaks.erase', + params: [pubSpendKey], + ); + + BehaviorSubject? tweaksScan({required String pubSpendKey}) => subscribe( + id: 'blockchain.tweaks.scan', + method: 'blockchain.tweaks.scan', + params: [pubSpendKey], + ); + + Future tweaksGet({required String pubSpendKey}) => call( + method: 'blockchain.tweaks.get', + params: [pubSpendKey], + ); Future getTweaks({required int height}) async => await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]); @@ -369,20 +386,20 @@ class ElectrumClient { return []; }); - Future> feeRates({BasedUtxoNetwork? network}) async { - try { - final topDoubleString = await estimatefee(p: 1); - final middleDoubleString = await estimatefee(p: 5); - final bottomDoubleString = await estimatefee(p: 10); - final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); - final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); - final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); + // Future> feeRates({BasedUtxoNetwork? network}) async { + // try { + // final topDoubleString = await estimatefee(p: 1); + // final middleDoubleString = await estimatefee(p: 5); + // final bottomDoubleString = await estimatefee(p: 10); + // final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); + // final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); + // final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); - return [bottom, middle, top]; - } catch (_) { - return []; - } - } + // return [bottom, middle, top]; + // } catch (_) { + // return []; + // } + // } // https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe // example response: @@ -527,6 +544,7 @@ class ElectrumClient { _tasks[method]?.subject?.add(params.last); break; case 'blockchain.tweaks.subscribe': + case 'blockchain.tweaks.scan': final params = request['params'] as List; _tasks[_tasks.keys.first]?.subject?.add(params.last); break; diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 4e37f40b1..1cb26fe78 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { @@ -31,32 +31,35 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; - final int frozen; + int frozen; int secondConfirmed = 0; int secondUnconfirmed = 0; @override String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed - frozen); + BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed - frozen); @override - String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); + String get formattedAdditionalBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: unconfirmed); @override String get formattedUnAvailableBalance { - final frozenFormatted = bitcoinAmountToString(amount: frozen); + final frozenFormatted = BitcoinAmountUtils.bitcoinAmountToString(amount: frozen); return frozenFormatted == '0.0' ? '' : frozenFormatted; } @override - String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + String get formattedSecondAvailableBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: secondConfirmed); @override - String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + String get formattedSecondAdditionalBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed); @override String get formattedFullAvailableBalance => - bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); String toJSON() => json.encode({ 'confirmed': confirmed, diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index b688f097b..f5d11954a 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -4,11 +4,8 @@ import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cw_bitcoin/electrum_transaction_info.dart'; part 'electrum_transaction_history.g.dart'; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 1ab7799e3..f75120531 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,9 +1,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -12,17 +10,39 @@ import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; class ElectrumTransactionBundle { - ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, required this.confirmations, this.time}); + ElectrumTransactionBundle( + this.originalTransaction, { + required this.ins, + required this.confirmations, + this.time, + }); final BtcTransaction originalTransaction; final List ins; final int? time; final int confirmations; + + Map toJson() { + return { + 'originalTransaction': originalTransaction.toHex(), + 'ins': ins.map((e) => e.toHex()).toList(), + 'confirmations': confirmations, + 'time': time, + }; + } + + static ElectrumTransactionBundle fromJson(Map data) { + return ElectrumTransactionBundle( + BtcTransaction.fromRaw(data['originalTransaction'] as String), + ins: (data['ins'] as List).map((e) => BtcTransaction.fromRaw(e as String)).toList(), + confirmations: data['confirmations'] as int, + time: data['time'] as int?, + ); + } } class ElectrumTransactionInfo extends TransactionInfo { - List? unspents; + List? unspents; bool isReceivedSilentPayment; ElectrumTransactionInfo( @@ -75,7 +95,8 @@ class ElectrumTransactionInfo extends TransactionInfo { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; final outAddresses = (out['scriptPubKey']['addresses'] as List?)?.toSet(); - inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); + inputsAmount += + BitcoinAmountUtils.stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -85,7 +106,8 @@ class ElectrumTransactionInfo extends TransactionInfo { for (dynamic out in vout) { final outAddresses = out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString()); + final value = BitcoinAmountUtils.stringDoubleToBitcoinAmount( + (out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -121,22 +143,33 @@ class ElectrumTransactionInfo extends TransactionInfo { List inputAddresses = []; List outputAddresses = []; - for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { - final input = bundle.originalTransaction.inputs[i]; - final inputTransaction = bundle.ins[i]; - final outTransaction = inputTransaction.outputs[input.txIndex]; - inputAmount += outTransaction.amount.toInt(); - if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { - direction = TransactionDirection.outgoing; - inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network)); + try { + for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { + final input = bundle.originalTransaction.inputs[i]; + final inputTransaction = bundle.ins[i]; + final outTransaction = inputTransaction.outputs[input.txIndex]; + inputAmount += outTransaction.amount.toInt(); + if (addresses.contains( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) { + direction = TransactionDirection.outgoing; + inputAddresses.add( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network)); + } } + } catch (e) { + print(bundle.originalTransaction.txId()); + print("original: ${bundle.originalTransaction}"); + print("bundle.inputs: ${bundle.originalTransaction.inputs}"); + print("ins: ${bundle.ins}"); + rethrow; } final receivedAmounts = []; for (final out in bundle.originalTransaction.outputs) { totalOutAmount += out.amount.toInt(); - final addressExists = addresses.contains(addressFromOutputScript(out.scriptPubKey, network)); - final address = addressFromOutputScript(out.scriptPubKey, network); + final addressExists = addresses + .contains(BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network)); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); if (address.isNotEmpty) outputAddresses.add(address); @@ -208,8 +241,7 @@ class ElectrumTransactionInfo extends TransactionInfo { outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(), to: data['to'] as String?, unspents: unspents - .map((unspent) => - BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) + .map((unspent) => BitcoinUnspent.fromJSON(null, unspent as Map)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, ); @@ -221,11 +253,11 @@ class ElectrumTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; + '${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String? feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' + ? '${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e8b8ea239..fceaf5d12 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,25 +4,24 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; -import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/get_height_by_date.dart'; @@ -30,55 +29,64 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; import 'package:mobx/mobx.dart'; -import 'package:rxdart/subjects.dart'; -import 'package:sp_scanner/sp_scanner.dart'; -import 'package:hex/hex.dart'; -import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase< - ElectrumBalance, - ElectrumTransactionHistory, - ElectrumTransactionInfo> with Store, WalletKeysFile { +abstract class ElectrumWalletBase + extends WalletBase + with Store, WalletKeysFile { + ReceivePort? receivePort; + SendPort? workerSendPort; + StreamSubscription? _workerSubscription; + Isolate? _workerIsolate; + final Map _responseCompleters = {}; + final Map _errorCompleters = {}; + int _messageId = 0; + ElectrumWalletBase({ required String password, required WalletInfo walletInfo, required Box unspentCoinsInfo, required this.network, required this.encryptionFileUtils, + Map? hdWallets, String? xpub, String? mnemonic, - Uint8List? seedBytes, + List? seedBytes, this.passphrase, List? initialAddresses, - ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = getAccountHDWallet( - currency, network, seedBytes, xpub, walletInfo.derivationInfo), + required this.mempoolAPIEnabled, + }) : hdWallets = hdWallets ?? + { + CWBitcoinDerivationType.bip39: getAccountHDWallet( + currency, + network, + seedBytes, + xpub, + walletInfo.derivationInfo, + ) + }, syncStatus = NotConnectedSyncStatus(), _password = password, - _feeRates = [], - _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, - unspentCoins = [], - _scripthashesUpdateSubject = {}, + // TODO: inital unspent coins + unspentCoins = BitcoinUnspentCoins(), + scripthashesListening = [], balance = ObservableMap.of(currency != null ? { currency: initialBalance ?? @@ -93,7 +101,6 @@ abstract class ElectrumWalletBase extends WalletBase< this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -101,47 +108,99 @@ abstract class ElectrumWalletBase extends WalletBase< encryptionFileUtils: encryptionFileUtils, ); - reaction((_) => syncStatus, _syncStatusReaction); + reaction((_) => syncStatus, syncStatusReaction); sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet( - CryptoCurrency? currency, - BasedUtxoNetwork network, - Uint8List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo) { + // Sends a request to the worker and returns a future that completes when the worker responds + Future sendWorker(ElectrumWorkerRequest request) { + final messageId = ++_messageId; + + final completer = Completer(); + _responseCompleters[messageId] = completer; + + final json = request.toJson(); + json['id'] = messageId; + workerSendPort!.send(json); + + try { + return completer.future.timeout(Duration(seconds: 5)); + } catch (e) { + _errorCompleters.addAll({messageId: e}); + _responseCompleters.remove(messageId); + rethrow; + } + } + + @action + Future handleWorkerResponse(dynamic message) async { + print('Main: received message: $message'); + + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + + final workerMethod = messageJson['method'] as String; + final workerError = messageJson['error'] as String?; + + if (workerError != null) { + print('Worker error: $workerError'); + return; + } + + final responseId = messageJson['id'] as int?; + if (responseId != null && _responseCompleters.containsKey(responseId)) { + _responseCompleters[responseId]!.complete(message); + _responseCompleters.remove(responseId); + } + + switch (workerMethod) { + case ElectrumWorkerMethods.connectionMethod: + final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); + _onConnectionStatusChange(response.result); + break; + case ElectrumRequestMethods.headersSubscribeMethod: + final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); + await onHeadersResponse(response.result); + break; + case ElectrumRequestMethods.getBalanceMethod: + final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson); + onBalanceResponse(response.result); + break; + case ElectrumRequestMethods.getHistoryMethod: + final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson); + onHistoriesResponse(response.result); + break; + case ElectrumRequestMethods.listunspentMethod: + final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); + onUnspentResponse(response.result); + break; + case ElectrumRequestMethods.estimateFeeMethod: + final response = ElectrumWorkerGetFeesResponse.fromJson(messageJson); + onFeesResponse(response.result); + break; + } + } + + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, + List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); } if (seedBytes != null) { - switch (currency) { - case CryptoCurrency.btc: - case CryptoCurrency.ltc: - case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) - .derivePath(_hardenedDerivationPath( - derivationInfo?.derivationPath ?? electrum_path)) - as Bip32Slip10Secp256k1; - case CryptoCurrency.bch: - return bitcoinCashHDWallet(seedBytes); - default: - throw Exception("Unsupported currency"); - } + return Bip32Slip10Secp256k1.fromSeed(seedBytes); } - return Bip32Slip10Secp256k1.fromExtendedKey( - xpub!, getKeyNetVersion(network)); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } - static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") - as Bip32Slip10Secp256k1; - - static int estimatedTransactionSize(int inputsCount, int outputsCounts) => + int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { @@ -154,14 +213,12 @@ abstract class ElectrumWalletBase extends WalletBase< } bool? alwaysScan; + bool mempoolAPIEnabled; - final Bip32Slip10Secp256k1 accountHD; + final Map hdWallets; + Bip32Slip10Secp256k1 get bip32 => walletAddresses.bip32; final String? _mnemonic; - Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); - - Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); - final EncryptionFileUtils encryptionFileUtils; @override @@ -171,7 +228,7 @@ abstract class ElectrumWalletBase extends WalletBase< @observable bool isEnabledAutoGenerateSubaddress; - late ElectrumClient electrumClient; + ApiProvider? apiProvider; Box unspentCoinsInfo; @override @@ -185,23 +242,23 @@ abstract class ElectrumWalletBase extends WalletBase< @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses + List get addressesSet => walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .map((addr) => addr.address) - .toSet(); + .toList(); List get scriptHashes => walletAddresses.addressesByReceiveType .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) - .map((addr) => (addr as BitcoinAddressRecord).getScriptHash(network)) + .map((addr) => (addr as BitcoinAddressRecord).scriptHash) .toList(); List get publicScriptHashes => walletAddresses.allAddresses - .where((addr) => !addr.isHidden) + .where((addr) => !addr.isChange) .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) - .map((addr) => addr.getScriptHash(network)) + .map((addr) => addr.scriptHash) .toList(); - String get xpub => accountHD.publicKey.toExtended; + String get xpub => bip32.publicKey.toExtended; @override String? get seed => _mnemonic; @@ -218,8 +275,6 @@ abstract class ElectrumWalletBase extends WalletBase< @override bool isTestnet; - bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; - @observable bool nodeSupportsSilentPayments = true; @observable @@ -229,261 +284,91 @@ abstract class ElectrumWalletBase extends WalletBase< Completer sharedPrefs = Completer(); - Future checkIfMempoolAPIIsEnabled() async { - bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true; - return isMempoolAPIEnabled; - } - - @action - Future setSilentPaymentsScanning(bool active) async { - silentPaymentsScanningActive = active; - - if (active) { - syncStatus = AttemptingScanSyncStatus(); - - final tip = await getUpdatedChainTip(); - - if (tip == walletInfo.restoreHeight) { - syncStatus = SyncedTipSyncStatus(tip); - return; - } - - if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); - } - } else { - alwaysScan = false; - - _isolate?.then((value) => value.kill(priority: Isolate.immediate)); - - if (electrumClient.isConnected) { - syncStatus = SyncedSyncStatus(); - } else { - syncStatus = NotConnectedSyncStatus(); - } - } - } - - int? _currentChainTip; - - Future getCurrentChainTip() async { - if ((_currentChainTip ?? 0) > 0) { - return _currentChainTip!; - } - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; - - return _currentChainTip!; - } - - Future getUpdatedChainTip() async { - final newTip = await electrumClient.getCurrentBlockChainTip(); - if (newTip != null && newTip > (_currentChainTip ?? 0)) { - _currentChainTip = newTip; - } - return _currentChainTip ?? 0; - } + @observable + int? currentChainTip; @override BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), - privateKey: hd.privateKey.toHex(), - publicKey: hd.publicKey.toHex(), + wif: WifEncoder.encode(bip32.privateKey.raw, netVer: network.wifNetVer), + privateKey: bip32.privateKey.toHex(), + publicKey: bip32.publicKey.toHex(), ); String _password; - List unspentCoins; - List _feeRates; + BitcoinUnspentCoins unspentCoins; - // ignore: prefer_final_fields - Map?> _scripthashesUpdateSubject; + @observable + TransactionPriorities? feeRates; - // ignore: prefer_final_fields - BehaviorSubject? _chainTipUpdateSubject; - bool _isTransactionUpdating; - Future? _isolate; + int feeRate(TransactionPriority priority) { + if (priority is ElectrumTransactionPriority && feeRates is BitcoinTransactionPriorities) { + final rates = feeRates as BitcoinTransactionPriorities; + + switch (priority) { + case ElectrumTransactionPriority.slow: + return rates.normal; + case ElectrumTransactionPriority.medium: + return rates.elevated; + case ElectrumTransactionPriority.fast: + return rates.priority; + case ElectrumTransactionPriority.custom: + return rates.custom; + } + } + + return feeRates![priority]; + } + + @observable + List scripthashesListening; + + bool _chainTipListenerOn = false; + bool _isInitialSync = true; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; - StreamSubscription? _receiveStream; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; Future init() async { await walletAddresses.init(); await transactionHistory.init(); - await save(); _autoSaveTimer = Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save()); } - @action - Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { - if (this is! BitcoinWallet) return; - final chainTip = chainTipParam ?? await getUpdatedChainTip(); - - if (chainTip == height) { - syncStatus = SyncedSyncStatus(); - return; - } - - syncStatus = AttemptingScanSyncStatus(); - - if (_isolate != null) { - final runningIsolate = await _isolate!; - runningIsolate.kill(priority: Isolate.immediate); - } - - final receivePort = ReceivePort(); - _isolate = Isolate.spawn( - startRefresh, - ScanData( - sendPort: receivePort.sendPort, - silentAddress: walletAddresses.silentAddress!, - network: network, - height: height, - chainTip: chainTip, - electrumClient: ElectrumClient(), - transactionHistoryIds: transactionHistory.transactions.keys.toList(), - node: (await getNodeSupportsSilentPayments()) == true - ? ScanNode(node!.uri, node!.useSSL) - : null, - labels: walletAddresses.labels, - labelIndexes: walletAddresses.silentAddresses - .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) - .map((addr) => addr.index) - .toList(), - isSingleScan: doSingleScan ?? false, - )); - - _receiveStream?.cancel(); - _receiveStream = receivePort.listen((var message) async { - if (message is Map) { - for (final map in message.entries) { - final txid = map.key; - final tx = map.value; - - if (tx.unspents != null) { - final existingTxInfo = transactionHistory.transactions[txid]; - final txAlreadyExisted = existingTxInfo != null; - - // Updating tx after re-scanned - if (txAlreadyExisted) { - existingTxInfo.amount = tx.amount; - existingTxInfo.confirmations = tx.confirmations; - existingTxInfo.height = tx.height; - - final newUnspents = tx.unspents! - .where((unspent) => !(existingTxInfo.unspents?.any((element) => - element.hash.contains(unspent.hash) && - element.vout == unspent.vout && - element.value == unspent.value) ?? - false)) - .toList(); - - if (newUnspents.isNotEmpty) { - newUnspents.forEach(_updateSilentAddressRecord); - - existingTxInfo.unspents ??= []; - existingTxInfo.unspents!.addAll(newUnspents); - - final newAmount = newUnspents.length > 1 - ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) - : newUnspents[0].value; - - if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += newAmount; - } - - // Updates existing TX - transactionHistory.addOne(existingTxInfo); - // Update balance record - balance[currency]!.confirmed += newAmount; - } - } else { - // else: First time seeing this TX after scanning - tx.unspents!.forEach(_updateSilentAddressRecord); - - // Add new TX record - transactionHistory.addMany(message); - // Update balance record - balance[currency]!.confirmed += tx.amount; - } - - await updateAllUnspents(); - } - } - } - - if (message is SyncResponse) { - if (message.syncStatus is UnsupportedSyncStatus) { - nodeSupportsSilentPayments = false; - } - - if (message.syncStatus is SyncingSyncStatus) { - var status = message.syncStatus as SyncingSyncStatus; - syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); - } else { - syncStatus = message.syncStatus; - } - - await walletInfo.updateRestoreHeight(message.height); - } - }); - } - - void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { - final silentAddress = walletAddresses.silentAddress!; - final silentPaymentAddress = SilentPaymentAddress( - version: silentAddress.version, - B_scan: silentAddress.B_scan, - B_spend: unspent.silentPaymentLabel != null - ? silentAddress.B_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), - ) - : silentAddress.B_spend, - network: network, - ); - - final addressRecord = walletAddresses.silentAddresses - .firstWhereOrNull((address) => address.address == silentPaymentAddress.toString()); - addressRecord?.txCount += 1; - addressRecord?.balance += unspent.value; - - walletAddresses.addSilentAddresses( - [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], - ); - } - @action @override Future startSync() async { try { - if (syncStatus is SyncronizingSyncStatus) { + if (syncStatus is SynchronizingSyncStatus) { return; } - syncStatus = SyncronizingSyncStatus(); + syncStatus = SynchronizingSyncStatus(); - if (hasSilentPaymentsScanning) { - await _setInitialHeight(); - } + // INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero + await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); - await subscribeForUpdates(); + // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later. await updateTransactions(); - await updateAllUnspents(); + // INFO: THIRD: Start loading the TX history await updateBalance(); + + // INFO: FOURTH: Finish with unspents + await updateAllUnspents(); + await updateFeeRates(); _updateFeeRateTimer ??= - Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - if (alwaysScan == true) { - _setListeners(walletInfo.restoreHeight); - } else { - syncStatus = SyncedSyncStatus(); - } + _isInitialSync = false; + syncStatus = SyncedSyncStatus(); + + await save(); } catch (e, stacktrace) { print(stacktrace); print("startSync $e"); @@ -491,91 +376,25 @@ abstract class ElectrumWalletBase extends WalletBase< } } + @action + void callError(FlutterErrorDetails error) { + _onError?.call(error); + } + @action Future updateFeeRates() async { - if (await checkIfMempoolAPIIsEnabled()) { - try { - final response = - await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); + workerSendPort!.send( + ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: mempoolAPIEnabled).toJson(), + ); + } - final result = json.decode(response.body) as Map; - final slowFee = (result['economyFee'] as num?)?.toInt() ?? 0; - int mediumFee = (result['hourFee'] as num?)?.toInt() ?? 0; - int fastFee = (result['fastestFee'] as num?)?.toInt() ?? 0; - if (slowFee == mediumFee) { - mediumFee++; - } - while (fastFee <= mediumFee) { - fastFee++; - } - _feeRates = [slowFee, mediumFee, fastFee]; - return; - } catch (e) { - print(e); - } - } - - final feeRates = await electrumClient.feeRates(network: network); - if (feeRates != [0, 0, 0]) { - _feeRates = feeRates; - } else if (isTestnet) { - _feeRates = [1, 1, 1]; - } + @action + Future onFeesResponse(TransactionPriorities result) async { + feeRates = result; } Node? node; - Future getNodeIsElectrs() async { - if (node == null) { - return false; - } - - final version = await electrumClient.version(); - - if (version.isNotEmpty) { - final server = version[0]; - - if (server.toLowerCase().contains('electrs')) { - node!.isElectrs = true; - node!.save(); - return node!.isElectrs!; - } - } - - node!.isElectrs = false; - node!.save(); - return node!.isElectrs!; - } - - Future getNodeSupportsSilentPayments() async { - // As of today (august 2024), only ElectrumRS supports silent payments - if (!(await getNodeIsElectrs())) { - return false; - } - - if (node == null) { - return false; - } - - try { - final tweaksResponse = await electrumClient.getTweaks(height: 0); - - if (tweaksResponse != null) { - node!.supportsSilentPayments = true; - node!.save(); - return node!.supportsSilentPayments!; - } - } on RequestFailedTimeoutException catch (_) { - node!.supportsSilentPayments = false; - node!.save(); - return node!.supportsSilentPayments!; - } catch (_) {} - - node!.supportsSilentPayments = false; - node!.save(); - return node!.supportsSilentPayments!; - } - @action @override Future connectToNode({required Node node}) async { @@ -584,12 +403,29 @@ abstract class ElectrumWalletBase extends WalletBase< try { syncStatus = ConnectingSyncStatus(); - await _receiveStream?.cancel(); - await electrumClient.close(); + if (_workerIsolate != null) { + _workerIsolate!.kill(priority: Isolate.immediate); + _workerSubscription?.cancel(); + receivePort?.close(); + } - electrumClient.onConnectionStatusChange = _onConnectionStatusChange; + receivePort = ReceivePort(); - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); + + _workerSubscription = receivePort!.listen((message) { + if (message is SendPort) { + workerSendPort = message; + workerSendPort!.send( + ElectrumWorkerConnectionRequest( + uri: node.uri, + network: network, + ).toJson(), + ); + } else { + handleWorkerResponse(message); + } + }); } catch (e, stacktrace) { print(stacktrace); print("connectToNode $e"); @@ -601,7 +437,7 @@ abstract class ElectrumWalletBase extends WalletBase< bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; - UtxoDetails _createUTXOS({ + TxCreateUtxoDetails _createUTXOS({ required bool sendAll, required bool paysToSilentPayment, int credentialsAmount = 0, @@ -651,22 +487,19 @@ abstract class ElectrumWalletBase extends WalletBase< ECPrivate? privkey; bool? isSilentPayment = false; - final hd = utx.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd; - - if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { - final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; - privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( - BigintUtils.fromBytes( - BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), - ), - ); + if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) { + privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey; spendsSilentPayment = true; isSilentPayment = true; } else if (!isHardwareWallet) { - privkey = - generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord); + final path = addressRecord.derivationInfo.derivationPath + .addElem(Bip32KeyIndex( + BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange), + )) + .addElem(Bip32KeyIndex(addressRecord.index)); + + privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -681,14 +514,19 @@ abstract class ElectrumWalletBase extends WalletBase< pubKeyHex = privkey.getPublic().toHex(); } else { - pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); + pubKeyHex = walletAddresses.bip32 + .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)) + .publicKey + .toHex(); } - final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}" - "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" - "/${utx.bitcoinAddressRecord.index}"; - publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + if (utx.bitcoinAddressRecord is BitcoinAddressRecord) { + final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord) + .derivationInfo + .derivationPath + .toString(); + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + } utxos.add( UtxoWithAddress( @@ -696,7 +534,7 @@ abstract class ElectrumWalletBase extends WalletBase< txHash: utx.hash, value: BigInt.from(utx.value), vout: utx.vout, - scriptType: _getScriptType(address), + scriptType: BitcoinAddressUtils.getScriptType(address), isSilentPayment: isSilentPayment, ), ownerDetails: UtxoAddressDetails( @@ -719,7 +557,7 @@ abstract class ElectrumWalletBase extends WalletBase< throw BitcoinTransactionNoInputsException(); } - return UtxoDetails( + return TxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -748,11 +586,8 @@ abstract class ElectrumWalletBase extends WalletBase< int fee = await calcFee( utxos: utxoDetails.utxos, outputs: outputs, - network: network, memo: memo, feeRate: feeRate, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, ); if (fee == 0) { @@ -854,11 +689,7 @@ abstract class ElectrumWalletBase extends WalletBase< isChange: true, )); - // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets - final changeDerivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" - "/${changeAddress.isHidden ? "1" : "0"}" - "/${changeAddress.index}"; + final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); @@ -871,11 +702,8 @@ abstract class ElectrumWalletBase extends WalletBase< // Always take only not updated bitcoin outputs here so for every estimation // the SP outputs are re-generated to the proper taproot addresses outputs: temp, - network: network, memo: memo, feeRate: feeRate, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, ); updatedOutputs.clear(); @@ -951,6 +779,7 @@ abstract class ElectrumWalletBase extends WalletBase< memo: memo, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, spendsSilentPayment: utxoDetails.spendsSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, ); } } @@ -958,33 +787,16 @@ abstract class ElectrumWalletBase extends WalletBase< Future calcFee({ required List utxos, required List outputs, - required BasedUtxoNetwork network, String? memo, required int feeRate, - List? inputPrivKeyInfos, - List? vinOutpoints, - }) async { - int estimatedSize; - if (network is BitcoinCashNetwork) { - estimatedSize = ForkedTransactionBuilder.estimateTransactionSize( + }) async => + feeRate * + BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxos, outputs: outputs, network: network, memo: memo, ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - inputPrivKeyInfos: inputPrivKeyInfos, - vinOutpoints: vinOutpoints, - ); - } - - return feeAmountWithFeeRate(feeRate, 0, 0, size: estimatedSize); - } @override Future createTransaction(Object credentials) async { @@ -1087,11 +899,10 @@ abstract class ElectrumWalletBase extends WalletBase< return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), - network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot @@ -1167,11 +978,10 @@ abstract class ElectrumWalletBase extends WalletBase< return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), - network: network, hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, @@ -1196,8 +1006,7 @@ abstract class ElectrumWalletBase extends WalletBase< } } - void setLedgerConnection(ledger.LedgerConnection connection) => - throw UnimplementedError(); + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); Future buildHardwareWalletTransaction({ required List outputs, @@ -1231,18 +1040,6 @@ abstract class ElectrumWalletBase extends WalletBase< 'alwaysScan': alwaysScan, }); - int feeRate(TransactionPriority priority) { - try { - if (priority is BitcoinTransactionPriority) { - return _feeRates[priority.raw]; - } - - return 0; - } catch (_) { - return 0; - } - } - int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount, {int? size}) => feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); @@ -1254,8 +1051,12 @@ abstract class ElectrumWalletBase extends WalletBase< int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount, int? size}) { if (priority is BitcoinTransactionPriority) { - return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount, - outputsCount: outputsCount, size: size); + return calculateEstimatedFeeWithFeeRate( + feeRate(priority), + amount, + outputsCount: outputsCount, + size: size, + ); } return 0; @@ -1301,7 +1102,7 @@ abstract class ElectrumWalletBase extends WalletBase< Future save() async { if (!(await WalletKeysFile.hasKeysFile(walletInfo.name, walletInfo.type))) { await saveKeysFile(_password, encryptionFileUtils); - saveKeysFile(_password, encryptionFileUtils, true); + await saveKeysFile(_password, encryptionFileUtils, true); } final path = await makePath(); @@ -1338,18 +1139,17 @@ abstract class ElectrumWalletBase extends WalletBase< await transactionHistory.changePassword(password); } - @action @override - Future rescan({required int height, bool? doSingleScan}) async { - silentPaymentsScanningActive = true; - _setListeners(height, doSingleScan: doSingleScan); + Future rescan({required int height}) async { + throw UnimplementedError(); } @override Future close({required bool shouldCleanup}) async { try { - await _receiveStream?.cancel(); - await electrumClient.close(); + _workerIsolate!.kill(priority: Isolate.immediate); + await _workerSubscription?.cancel(); + receivePort?.close(); } catch (_) {} _autoSaveTimer?.cancel(); _updateFeeRateTimer?.cancel(); @@ -1357,93 +1157,70 @@ abstract class ElectrumWalletBase extends WalletBase< @action Future updateAllUnspents() async { - List updatedUnspentCoins = []; + final req = ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), + ); - if (hasSilentPaymentsScanning) { - // Update unspents stored from scanned silent payment transactions - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null) { - updatedUnspentCoins.addAll(tx.unspents!); - } - }); + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); } - - // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating - walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .forEach((addr) { - if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; - }); - - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); - - unspentCoins = updatedUnspentCoins; - - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; - } - - await updateCoins(unspentCoins); - await _refreshUnspentCoinsInfo(); } - Future updateCoins(List newUnspentCoins) async { - if (newUnspentCoins.isEmpty) { - return; - } + @action + void updateCoin(BitcoinUnspent coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + addCoinInfo(coin); + } + } + + @action + Future onUnspentResponse(Map> unspents) async { + final updatedUnspentCoins = []; + + await Future.wait(unspents.entries.map((entry) async { + final unspent = entry.value; + final scriptHash = entry.key; + + final addressRecord = walletAddresses.allAddresses.firstWhereOrNull( + (element) => element.scriptHash == scriptHash, ); - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - addCoinInfo(coin); + if (addressRecord == null) { + return null; } - }); - } - @action - Future updateUnspentsForAddress(BitcoinAddressRecord address) async { - final newUnspentCoins = await fetchUnspent(address); - await updateCoins(newUnspentCoins); - } - - @action - Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; - List updatedUnspentCoins = []; - - unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); - - await Future.wait(unspents.map((unspent) async { - try { - final coin = BitcoinUnspent.fromJSON(address, unspent); + await Future.wait(unspent.map((unspent) async { + final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson()); + coin.isChange = addressRecord.isChange; final tx = await fetchTransactionInfo(hash: coin.hash); - coin.isChange = address.isHidden; - coin.confirmations = tx?.confirmations; + if (tx != null) { + coin.confirmations = tx.confirmations; + } updatedUnspentCoins.add(coin); - } catch (_) {} + })); })); - return updatedUnspentCoins; + unspentCoins.clear(); + unspentCoins.addAll(updatedUnspentCoins); + unspentCoins.forEach(updateCoin); + + await refreshUnspentCoinsInfo(); } @action @@ -1458,13 +1235,13 @@ abstract class ElectrumWalletBase extends WalletBase< value: coin.value, vout: coin.vout, isChange: coin.isChange, - isSilentPayment: coin is BitcoinSilentPaymentsUnspent, + isSilentPayment: coin.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord, ); await unspentCoinsInfo.add(newInfo); } - Future _refreshUnspentCoinsInfo() async { + Future refreshUnspentCoinsInfo() async { try { final List keys = []; final currentWalletUnspentCoins = @@ -1489,7 +1266,91 @@ abstract class ElectrumWalletBase extends WalletBase< } } - int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + currentChainTip = response.height; + + bool updated = false; + transactionHistory.transactions.values.forEach((tx) { + if (tx.height != null && tx.height! > 0) { + final newConfirmations = currentChainTip! - tx.height! + 1; + + if (tx.confirmations != newConfirmations) { + tx.confirmations = newConfirmations; + tx.isPending = tx.confirmations == 0; + updated = true; + } + } + }); + + if (updated) { + await save(); + } + } + + @action + Future subscribeForHeaders() async { + if (_chainTipListenerOn) return; + + workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); + _chainTipListenerOn = true; + } + + @action + Future onHistoriesResponse(List histories) async { + if (histories.isEmpty) { + return; + } + + final firstAddress = histories.first; + final isChange = firstAddress.addressRecord.isChange; + final type = firstAddress.addressRecord.type; + + final totalAddresses = (isChange + ? walletAddresses.receiveAddresses.where((element) => element.type == type).length + : walletAddresses.changeAddresses.where((element) => element.type == type).length); + final gapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + bool hasUsedAddressesUnderGap = false; + final addressesWithHistory = []; + + for (final addressHistory in histories) { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + final address = addressHistory.addressRecord; + addressesWithHistory.add(address); + + hasUsedAddressesUnderGap = + address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } + } + } + + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } + + if (hasUsedAddressesUnderGap) { + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverAddresses( + isChange: isChange, + derivationType: firstAddress.addressRecord.derivationType, + type: type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), + ); + + if (newAddresses.isNotEmpty) { + // Update the transactions for the new discovered addresses + await updateTransactions(newAddresses); + } + } + } Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { @@ -1506,11 +1367,12 @@ abstract class ElectrumWalletBase extends WalletBase< final bundle = await getTransactionExpanded(hash: txId); final outputs = bundle.originalTransaction.outputs; - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( - (element) => element.address == addressFromOutputScript(output.scriptPubKey, network))); + final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any((element) => + element.address == + BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network))); var allInputsAmount = 0; @@ -1548,19 +1410,21 @@ abstract class ElectrumWalletBase extends WalletBase< final inputTransaction = bundle.ins[i]; final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; - final address = addressFromOutputScript(outTransaction.scriptPubKey, network); - allInputsAmount += outTransaction.amount.toInt(); + final address = + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network); + // allInputsAmount += outTransaction.amount.toInt(); final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); - final privkey = generateECPrivate( - hd: addressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: addressRecord.index, - network: network); + final path = addressRecord.derivationInfo.derivationPath + .addElem(Bip32KeyIndex( + BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange), + )) + .addElem(Bip32KeyIndex(addressRecord.index)); + + final privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); privateKeys.add(privkey); @@ -1570,7 +1434,7 @@ abstract class ElectrumWalletBase extends WalletBase< txHash: input.txId, value: outTransaction.amount, vout: vout, - scriptType: _getScriptType(btcAddress), + scriptType: BitcoinAddressUtils.getScriptType(btcAddress), ), ownerDetails: UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress), @@ -1588,7 +1452,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (index + 1 <= script.length) { try { final opReturnData = script[index + 1].toString(); - memo = utf8.decode(HEX.decode(opReturnData)); + memo = StringUtils.decode(BytesUtils.fromHexString(opReturnData)); continue; } catch (_) { throw Exception('Cannot decode OP_RETURN data'); @@ -1596,7 +1460,7 @@ abstract class ElectrumWalletBase extends WalletBase< } } - final address = addressFromOutputScript(out.scriptPubKey, network); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); final btcAddress = RegexUtils.addressTypeFromStr(address, network); outputs.add(BitcoinOutput(address: btcAddress, value: BigInt.from(out.amount.toInt()))); } @@ -1635,7 +1499,7 @@ abstract class ElectrumWalletBase extends WalletBase< } // Identify all change outputs - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); final List changeOutputs = outputs .where((output) => changeAddresses .any((element) => element.address == output.address.toAddress(network))) @@ -1675,10 +1539,9 @@ abstract class ElectrumWalletBase extends WalletBase< return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: sendingAmount, fee: newFee, - network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { @@ -1697,404 +1560,119 @@ abstract class ElectrumWalletBase extends WalletBase< } } - Future getTransactionExpanded( - {required String hash, int? height}) async { - String transactionHex; - int? time; - int? confirmations; - - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); - - if (verboseTransaction.isEmpty) { - transactionHex = await electrumClient.getTransactionHex(hash: hash); - - if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { - try { - final blockHash = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", - ), - ); - - if (blockHash.statusCode == 200 && - blockHash.body.isNotEmpty && - jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", - ), - ); - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); - } - } - } catch (_) {} - } - } else { - transactionHex = verboseTransaction['hex'] as String; - time = verboseTransaction['time'] as int?; - confirmations = verboseTransaction['confirmations'] as int?; - } - - if (height != null) { - if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) - .round(); - } - - if (confirmations == null) { - final tip = await getUpdatedChainTip(); - if (tip > 0 && height > 0) { - // Add one because the block itself is the first confirmation - confirmations = tip - height + 1; - } - } - } - - final original = BtcTransaction.fromRaw(transactionHex); - final ins = []; - - for (final vin in original.inputs) { - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId); - - final String inputTransactionHex; - - if (verboseTransaction.isEmpty) { - inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); - } else { - inputTransactionHex = verboseTransaction['hex'] as String; - } - - ins.add(BtcTransaction.fromRaw(inputTransactionHex)); - } - - return ElectrumTransactionBundle( - original, - ins: ins, - time: time, - confirmations: confirmations ?? 0, - ); + Future getTransactionExpanded({required String hash}) async { + return await sendWorker( + ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!)) + as ElectrumTransactionBundle; } - Future fetchTransactionInfo( - {required String hash, int? height, bool? retryOnFailure}) async { + Future fetchTransactionInfo({required String hash, int? height}) async { try { return ElectrumTransactionInfo.fromElectrumBundle( - await getTransactionExpanded(hash: hash, height: height), + await getTransactionExpanded(hash: hash), walletInfo.type, network, - addresses: addressesSet, + addresses: walletAddresses.allAddresses.map((e) => e.address).toSet(), height: height, ); - } catch (e) { - if (e is FormatException && retryOnFailure == true) { - await Future.delayed(const Duration(seconds: 2)); - return fetchTransactionInfo(hash: hash, height: height); - } + } catch (_) { return null; } } @override + @action Future> fetchTransactions() async { - try { - final Map historiesWithDetails = {}; - - if (type == WalletType.bitcoin) { - await Future.wait(BITCOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - } else if (type == WalletType.bitcoinCash) { - await Future.wait(BITCOIN_CASH_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - } else if (type == WalletType.litecoin) { - await Future.wait(LITECOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - } - - transactionHistory.transactions.values.forEach((tx) async { - final isPendingSilentPaymentUtxo = - (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null; - - if (isPendingSilentPaymentUtxo) { - final info = - await fetchTransactionInfo(hash: tx.id, height: tx.height, retryOnFailure: true); - - if (info != null) { - tx.confirmations = info.confirmations; - tx.isPending = tx.confirmations == 0; - transactionHistory.addOne(tx); - await transactionHistory.save(); - } - } - }); - - return historiesWithDetails; - } catch (e) { - print("fetchTransactions $e"); - return {}; - } + throw UnimplementedError(); } - Future fetchTransactionsForAddressType( - Map historiesWithDetails, - BitcoinAddressType type, - ) async { - final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); - final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); - final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); - walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); - await walletAddresses.saveAddressesInBox(); - await Future.wait(addressesByType.map((addressRecord) async { - final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); + @action + Future updateTransactions([List? addresses]) async { + addresses ??= walletAddresses.allAddresses.toList(); - if (history.isNotEmpty) { - addressRecord.txCount = history.length; - historiesWithDetails.addAll(history); - - final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; - final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= - matchedAddresses.length - - (addressRecord.isHidden - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - - if (isUsedAddressUnderGap) { - final prevLength = walletAddresses.allAddresses.length; - - // Discover new addresses for the same address type until the gap limit is respected - await walletAddresses.discoverAddresses( - matchedAddresses.toList(), - addressRecord.isHidden, - (address) async { - await subscribeForUpdates(); - return _fetchAddressHistory(address, await getCurrentChainTip()) - .then((history) => history.isNotEmpty ? address.address : null); - }, - type: type, - ); - - final newLength = walletAddresses.allAddresses.length; - - if (newLength > prevLength) { - await fetchTransactionsForAddressType(historiesWithDetails, type); - } - } - } - })); - } - - Future> _fetchAddressHistory( - BitcoinAddressRecord addressRecord, int? currentHeight) async { - String txid = ""; - - try { - final Map historiesWithDetails = {}; - - final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); - - if (history.isNotEmpty) { - addressRecord.setAsUsed(); - - await Future.wait(history.map((transaction) async { - txid = transaction['tx_hash'] as String; - final height = transaction['height'] as int; - final storedTx = transactionHistory.transactions[txid]; - - if (storedTx != null) { - if (height > 0) { - storedTx.height = height; - // the tx's block itself is the first confirmation so add 1 - if ((currentHeight ?? 0) > 0) { - storedTx.confirmations = currentHeight! - height + 1; - } - storedTx.isPending = storedTx.confirmations == 0; - } - - historiesWithDetails[txid] = storedTx; - } else { - final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true); - - if (tx != null) { - historiesWithDetails[txid] = tx; - - // Got a new transaction fetched, add it to the transaction history - // instead of waiting all to finish, and next time it will be faster - transactionHistory.addOne(tx); - await transactionHistory.save(); - } - } - - return Future.value(null); - })); - } - - return historiesWithDetails; - } catch (e, stacktrace) { - _onError?.call(FlutterErrorDetails( - exception: "$txid - $e", - stack: stacktrace, - library: this.runtimeType.toString(), - )); - return {}; - } - } - - Future updateTransactions() async { - print("updateTransactions() called!"); - try { - if (_isTransactionUpdating) { - return; - } - await getCurrentChainTip(); - - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null && - tx.unspents!.isNotEmpty && - tx.height != null && - tx.height! > 0 && - (_currentChainTip ?? 0) > 0) { - tx.confirmations = _currentChainTip! - tx.height! + 1; - } - }); - - _isTransactionUpdating = true; - await fetchTransactions(); - walletAddresses.updateReceiveAddresses(); - _isTransactionUpdating = false; - } catch (e, stacktrace) { - print(stacktrace); - print(e); - _isTransactionUpdating = false; - } - } - - Future subscribeForUpdates() async { - final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => - !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && - address.type != SegwitAddresType.mweb, + final req = ElectrumWorkerGetHistoryRequest( + addresses: addresses, + storedTxs: transactionHistory.transactions.values.toList(), + walletType: type, + // If we still don't have currentChainTip, txs will still be fetched but shown + // with confirmations as 0 but will be auto fixed on onHeadersResponse + chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), + network: network, + mempoolAPIEnabled: mempoolAPIEnabled, ); - await Future.wait(unsubscribedScriptHashes.map((address) async { - final sh = address.getScriptHash(network); - if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { - try { - await _scripthashesUpdateSubject[sh]?.close(); - } catch (e) { - print("failed to close: $e"); - } - } - try { - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); - } catch (e) { - print("failed scripthashUpdate: $e"); - } - _scripthashesUpdateSubject[sh]?.listen((event) async { - try { - await updateUnspentsForAddress(address); - - await updateBalance(); - - await _fetchAddressHistory(address, await getCurrentChainTip()); - } catch (e, s) { - print("sub error: $e"); - _onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); - } - }); - })); + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } - Future fetchBalances() async { - final addresses = walletAddresses.allAddresses - .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) - .toList(); - final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { - final addressRecord = addresses[i]; - final sh = addressRecord.getScriptHash(network); - final balanceFuture = electrumClient.getBalance(sh); - balanceFutures.add(balanceFuture); - } + @action + Future subscribeForUpdates([Iterable? unsubscribedScriptHashes]) async { + unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where( + (sh) => !scripthashesListening.contains(sh), + ); - var totalFrozen = 0; - var totalConfirmed = 0; - var totalUnconfirmed = 0; - - unspentCoinsInfo.values.forEach((info) { - unspentCoins.forEach((element) { - if (element.hash == info.hash && - element.vout == info.vout && - info.isFrozen && - element.bitcoinAddressRecord.address == info.address && - element.value == info.value) { - totalFrozen += element.value; - } - }); + Map scripthashByAddress = {}; + walletAddresses.allAddresses.forEach((addressRecord) { + scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; }); - if (hasSilentPaymentsScanning) { - // Add values from unspent coins that are not fetched by the address list - // i.e. scanned silent payments - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null) { - tx.unspents!.forEach((unspent) { - if (unspent.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { - if (unspent.isFrozen) totalFrozen += unspent.value; - totalConfirmed += unspent.value; - } - }); - } - }); - } + workerSendPort!.send( + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + ).toJson(), + ); - final balances = await Future.wait(balanceFutures); + scripthashesListening.addAll(scripthashByAddress.values); + } - for (var i = 0; i < balances.length; i++) { - final addressRecord = addresses[i]; - final balance = balances[i]; - final confirmed = balance['confirmed'] as int? ?? 0; - final unconfirmed = balance['unconfirmed'] as int? ?? 0; - totalConfirmed += confirmed; - totalUnconfirmed += unconfirmed; + @action + void onBalanceResponse(ElectrumBalance balanceResult) { + var totalFrozen = 0; + var totalConfirmed = balanceResult.confirmed; + var totalUnconfirmed = balanceResult.unconfirmed; - addressRecord.balance = confirmed + unconfirmed; - if (confirmed > 0 || unconfirmed > 0) { - addressRecord.setAsUsed(); + unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { + if (unspentCoinInfo.isFrozen) { + totalFrozen += unspentCoinInfo.value; } - } + }); - return ElectrumBalance( + balance[currency] = ElectrumBalance( confirmed: totalConfirmed, unconfirmed: totalUnconfirmed, frozen: totalFrozen, ); } + @action Future updateBalance() async { - print("updateBalance() called!"); - balance[currency] = await fetchBalances(); - await save(); + final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); + + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; - @override Future signMessage(String message, {String? address = null}) async { - final index = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index - : null; - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); - final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex()); + final record = walletAddresses.getFromAddresses(address!); - String messagePrefix = '\x18Bitcoin Signed Message:\n'; - final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix); + final path = Bip32PathParser.parse(walletInfo.derivationInfo!.derivationPath!) + .addElem( + Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(record.isChange)), + ) + .addElem(Bip32KeyIndex(record.index)); + + final priv = ECPrivate.fromHex(bip32.derive(path).privateKey.toHex()); + + final hexEncoded = priv.signMessage(StringUtils.encode(message)); final decodedSig = hex.decode(hexEncoded); return base64Encode(decodedSig); } @@ -2110,7 +1688,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (signature.endsWith('=')) { sigDecodedBytes = base64.decode(signature); } else { - sigDecodedBytes = hex.decode(signature); + sigDecodedBytes = BytesUtils.fromHexString(signature); } if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) { @@ -2120,7 +1698,7 @@ abstract class ElectrumWalletBase extends WalletBase< String messagePrefix = '\x18Bitcoin Signed Message:\n'; final messageHash = QuickCrypto.sha256Hash( - BitcoinSignerUtils.magicMessage(utf8.encode(message), messagePrefix)); + BitcoinSignerUtils.magicMessage(StringUtils.encode(message), messagePrefix)); List correctSignature = sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes); @@ -2157,36 +1735,8 @@ abstract class ElectrumWalletBase extends WalletBase< return false; } - Future _setInitialHeight() async { - if (_chainTipUpdateSubject != null) return; - - _currentChainTip = await getUpdatedChainTip(); - - if ((_currentChainTip == null || _currentChainTip! == 0) && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(_currentChainTip!); - } - - _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); - _chainTipUpdateSubject?.listen((e) async { - final event = e as Map; - final height = int.tryParse(event['height'].toString()); - - if (height != null) { - _currentChainTip = height; - - if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - _setListeners(walletInfo.restoreHeight); - } - } - }); - } - - static String _hardenedDerivationPath(String derivationPath) => - derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); - @action void _onConnectionStatusChange(ConnectionStatus status) { - switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2216,16 +1766,18 @@ abstract class ElectrumWalletBase extends WalletBase< } } - void _syncStatusReaction(SyncStatus syncStatus) async { - print("SYNC_STATUS_CHANGE: ${syncStatus}"); - if (syncStatus is SyncingSyncStatus) { - return; + @action + void syncStatusReaction(SyncStatus syncStatus) { + final isDisconnectedStatus = + syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus; + + if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) { + // Needs to re-subscribe to all scripthashes when reconnected + scripthashesListening = []; + _chainTipListenerOn = false; } - if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { - // Needs to re-subscribe to all scripthashes when reconnected - _scripthashesUpdateSubject = {}; - + if (isDisconnectedStatus) { if (_isTryingToConnect) return; _isTryingToConnect = true; @@ -2233,21 +1785,13 @@ abstract class ElectrumWalletBase extends WalletBase< Timer(Duration(seconds: 5), () { if (this.syncStatus is NotConnectedSyncStatus || this.syncStatus is LostConnectionSyncStatus) { - this.electrumClient.connectToUri( - node!.uri, - useSSL: node!.useSSL ?? false, - ); + if (node == null) return; + + connectToNode(node: this.node!); } _isTryingToConnect = false; }); } - - // Message is shown on the UI for 3 seconds, revert to synced - if (syncStatus is SyncedTipSyncStatus) { - Timer(Duration(seconds: 3), () { - if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); - }); - } } void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) { @@ -2265,14 +1809,15 @@ abstract class ElectrumWalletBase extends WalletBase< final inputTransaction = bundle.ins[i]; final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; - final address = addressFromOutputScript(outTransaction.scriptPubKey, network); + final address = + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network); if (address.isNotEmpty) inputAddresses.add(address); } for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) { final out = bundle.originalTransaction.outputs[i]; - final address = addressFromOutputScript(out.scriptPubKey, network); + final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network); if (address.isNotEmpty) outputAddresses.add(address); @@ -2283,7 +1828,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (index + 1 <= script.length) { try { final opReturnData = script[index + 1].toString(); - final decodedString = utf8.decode(HEX.decode(opReturnData)); + final decodedString = StringUtils.decode(BytesUtils.fromHexString(opReturnData)); outputAddresses.add('OP_RETURN:$decodedString'); } catch (_) { outputAddresses.add('OP_RETURN:'); @@ -2299,255 +1844,6 @@ abstract class ElectrumWalletBase extends WalletBase< } } -class ScanNode { - final Uri uri; - final bool? useSSL; - - ScanNode(this.uri, this.useSSL); -} - -class ScanData { - final SendPort sendPort; - final SilentPaymentOwner silentAddress; - final int height; - final ScanNode? node; - final BasedUtxoNetwork network; - final int chainTip; - final ElectrumClient electrumClient; - final List transactionHistoryIds; - final Map labels; - final List labelIndexes; - final bool isSingleScan; - - ScanData({ - required this.sendPort, - required this.silentAddress, - required this.height, - required this.node, - required this.network, - required this.chainTip, - required this.electrumClient, - required this.transactionHistoryIds, - required this.labels, - required this.labelIndexes, - required this.isSingleScan, - }); - - factory ScanData.fromHeight(ScanData scanData, int newHeight) { - return ScanData( - sendPort: scanData.sendPort, - silentAddress: scanData.silentAddress, - height: newHeight, - node: scanData.node, - network: scanData.network, - chainTip: scanData.chainTip, - transactionHistoryIds: scanData.transactionHistoryIds, - electrumClient: scanData.electrumClient, - labels: scanData.labels, - labelIndexes: scanData.labelIndexes, - isSingleScan: scanData.isSingleScan, - ); - } -} - -class SyncResponse { - final int height; - final SyncStatus syncStatus; - - SyncResponse(this.height, this.syncStatus); -} - -Future startRefresh(ScanData scanData) async { - int syncHeight = scanData.height; - int initialSyncHeight = syncHeight; - - BehaviorSubject? tweaksSubscription = null; - - final electrumClient = scanData.electrumClient; - await electrumClient.connectToUri( - scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), - useSSL: scanData.node?.useSSL ?? false, - ); - - int getCountPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } - - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } - - if (tweaksSubscription == null) { - final receiver = Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, - ); - - // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - - tweaksSubscription = await electrumClient.tweaksSubscribe( - height: syncHeight, - count: initialCount, - ); - - Future listenFn(t) async { - final tweaks = t as Map; - final msg = tweaks["message"]; - // success or error msg - final noData = msg != null; - - if (noData) { - // re-subscribe to continue receiving messages, starting from the next unscanned height - final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - - if (nextCount > 0) { - tweaksSubscription?.close(); - - final nextTweaksSubscription = electrumClient.tweaksSubscribe( - height: nextHeight, - count: nextCount, - ); - nextTweaksSubscription?.listen(listenFn); - } - - return; - } - - // Continuous status UI update, send how many blocks left to scan - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - - final blockHeight = tweaks.keys.first; - final tweakHeight = int.parse(blockHeight); - - try { - final blockTweaks = tweaks[blockHeight] as Map; - - for (var j = 0; j < blockTweaks.keys.length; j++) { - final txid = blockTweaks.keys.elementAt(j); - final details = blockTweaks[txid] as Map; - final outputPubkeys = (details["output_pubkeys"] as Map); - final tweak = details["tweak"].toString(); - - try { - // scanOutputs called from rust here - final addToWallet = scanOutputs( - outputPubkeys.values.toList(), - tweak, - receiver, - ); - - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } - - // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) - final txInfo = ElectrumTransactionInfo( - WalletType.bitcoin, - id: txid, - height: tweakHeight, - amount: 0, - fee: 0, - direction: TransactionDirection.incoming, - isPending: false, - isReplaced: false, - date: scanData.network == BitcoinNetwork.mainnet - ? getDateByBitcoinHeight(tweakHeight) - : DateTime.now(), - confirmations: scanData.chainTip - tweakHeight + 1, - unspents: [], - isReceivedSilentPayment: true, - ); - - addToWallet.forEach((label, value) { - (value as Map).forEach((output, tweak) { - final t_k = tweak.toString(); - - final receivingOutputAddress = ECPublic.fromHex(output) - .toTaprootAddress(tweak: false) - .toAddress(scanData.network); - - int? amount; - int? pos; - outputPubkeys.entries.firstWhere((k) { - final isMatchingOutput = k.value[0] == output; - if (isMatchingOutput) { - amount = int.parse(k.value[1].toString()); - pos = int.parse(k.key.toString()); - return true; - } - return false; - }); - - final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( - receivingOutputAddress, - index: 0, - isHidden: false, - isUsed: true, - network: scanData.network, - silentPaymentTweak: t_k, - type: SegwitAddresType.p2tr, - txCount: 1, - balance: amount!, - ); - - final unspent = BitcoinSilentPaymentsUnspent( - receivedAddressRecord, - txid, - amount!, - pos!, - silentPaymentTweak: t_k, - silentPaymentLabel: label == "None" ? null : label, - ); - - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); - }); - - scanData.sendPort.send({txInfo.id: txInfo}); - } catch (_) {} - } - } catch (_) {} - - syncHeight = tweakHeight; - - if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { - if (tweakHeight >= scanData.chainTip) - scanData.sendPort.send(SyncResponse( - syncHeight, - SyncedTipSyncStatus(scanData.chainTip), - )); - - if (scanData.isSingleScan) { - scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - } - - await tweaksSubscription!.close(); - await electrumClient.close(); - } - } - - tweaksSubscription?.listen(listenFn); - } - - if (tweaksSubscription == null) { - return scanData.sendPort.send( - SyncResponse(syncHeight, UnsupportedSyncStatus()), - ); - } -} - class EstimatedTxResult { EstimatedTxResult({ required this.utxos, @@ -2583,25 +1879,7 @@ class PublicKeyWithDerivationPath { final String publicKey; } -BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { - if (type is P2pkhAddress) { - return P2pkhAddressType.p2pkh; - } else if (type is P2shAddress) { - return P2shAddressType.p2wpkhInP2sh; - } else if (type is P2wshAddress) { - return SegwitAddresType.p2wsh; - } else if (type is P2trAddress) { - return SegwitAddresType.p2tr; - } else if (type is MwebAddress) { - return SegwitAddresType.mweb; - } else if (type is SilentPaymentsAddresType) { - return SilentPaymentsAddresType.p2sp; - } else { - return SegwitAddresType.p2wpkh; - } -} - -class UtxoDetails { +class TxCreateUtxoDetails { final List availableInputs; final List unconfirmedCoins; final List utxos; @@ -2612,7 +1890,7 @@ class UtxoDetails { final bool spendsSilentPayment; final bool spendsUnconfirmedTX; - UtxoDetails({ + TxCreateUtxoDetails({ required this.availableInputs, required this.unconfirmedCoins, required this.utxos, @@ -2624,3 +1902,35 @@ class UtxoDetails { required this.spendsUnconfirmedTX, }); } + +class BitcoinUnspentCoins extends ObservableList { + BitcoinUnspentCoins() : super(); + + List forInfo(Iterable unspentCoinsInfo) { + return unspentCoinsInfo.where((element) { + final info = this.firstWhereOrNull( + (info) => + element.hash == info.hash && + element.vout == info.vout && + element.address == info.bitcoinAddressRecord.address && + element.value == info.value, + ); + + return info != null; + }).toList(); + } + + List fromInfo(Iterable unspentCoinsInfo) { + return this.where((element) { + final info = unspentCoinsInfo.firstWhereOrNull( + (info) => + element.hash == info.hash && + element.vout == info.vout && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value, + ); + + return info != null; + }).toList(); + } +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 2686d12cc..789a0e491 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -11,6 +11,8 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; +enum CWBitcoinDerivationType { old, electrum, bip39, mweb } + class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; const List BITCOIN_ADDRESS_TYPES = [ @@ -33,8 +35,7 @@ const List BITCOIN_CASH_ADDRESS_TYPES = [ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { - required this.mainHd, - required this.sideHd, + required this.hdWallets, required this.network, required this.isHardwareWallet, List? initialAddresses, @@ -43,18 +44,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, List? initialMwebAddresses, - Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, - - }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), + }) : _allAddresses = ObservableList.of(initialAddresses ?? []), addressesByReceiveType = ObservableList.of(([]).toSet()), - receiveAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), + receiveAddresses = ObservableList.of( + (initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()), + // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + changeAddresses = ObservableList.of( + (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? @@ -67,33 +65,23 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { mwebAddresses = ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { - if (masterHd != null) { - silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()), - b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()), - network: network, - ); + // TODO: initial silent address, not every time + silentAddress = SilentPaymentOwner.fromBip32(bip32); - if (silentAddresses.length == 0) { - silentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentAddress.toString(), - index: 0, - isHidden: false, - name: "", - silentPaymentTweak: null, - network: network, - type: SilentPaymentsAddresType.p2sp, - )); - silentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentAddress!.toLabeledSilentPaymentAddress(0).toString(), - index: 0, - isHidden: true, - name: "", - silentPaymentTweak: BytesUtils.toHexString(silentAddress!.generateLabel(0)), - network: network, - type: SilentPaymentsAddresType.p2sp, - )); - } + if (silentAddresses.length == 0) { + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress.toString(), + labelIndex: 1, + name: "", + type: SilentPaymentsAddresType.p2sp, + )); + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress!.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + )); } updateAddressesByMatch(); @@ -103,7 +91,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; - final ObservableList _addresses; + final ObservableList _allAddresses; final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -112,8 +100,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; - final Bip32Slip10Secp256k1 mainHd; - final Bip32Slip10Secp256k1 sideHd; + + final Map hdWallets; + Bip32Slip10Secp256k1 get bip32 => + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + final bool isHardwareWallet; @observable @@ -129,7 +120,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { String? activeSilentAddress; @computed - List get allAddresses => _addresses; + List get allAddresses => _allAddresses.toList(); + + @computed + Set get allScriptHashes => + _allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet(); + + BitcoinAddressRecord getFromAddresses(String address) { + return _allAddresses.firstWhere((element) => element.address == address); + } @override @computed @@ -174,31 +173,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return; } if (addressPageType == SilentPaymentsAddresType.p2sp) { - final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + late BitcoinSilentPaymentAddressRecord selected; + try { + selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + } catch (_) { + selected = silentAddresses[0]; + } - if (selected.silentPaymentTweak != null && silentAddress != null) { + if (selected.labelHex != null && silentAddress != null) { activeSilentAddress = - silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString(); + silentAddress!.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); } else { activeSilentAddress = silentAddress!.toString(); } return; } try { - final addressRecord = _addresses.firstWhere( - (addressRecord) => addressRecord.address == addr, - ); + final addressRecord = _allAddresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); - previousAddressRecord = addressRecord; - receiveAddresses.remove(addressRecord); - receiveAddresses.insert(0, addressRecord); + previousAddressRecord = addressRecord; } catch (e) { print("ElectrumWalletAddressBase: set address ($addr): $e"); } } @override - String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType); + String get primaryAddress => _allAddresses.first.address; Map currentReceiveAddressIndexByType; @@ -223,7 +225,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { + if (!addressRecord.isChange) { return acc + 1; } return acc; @@ -231,7 +233,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { - if (addressRecord.isHidden) { + if (addressRecord.isChange) { return acc + 1; } return acc; @@ -240,26 +242,25 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { if (walletInfo.type == WalletType.bitcoinCash) { - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await _generateInitialAddresses(type: SegwitAddresType.mweb); + await generateInitialAddresses(type: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { - await _generateInitialAddresses(); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); if (!isHardwareWallet) { - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(type: SegwitAddresType.p2tr); + await generateInitialAddresses(type: SegwitAddresType.p2wsh); } } updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); - _validateAddresses(); await updateAddressesInBox(); if (currentReceiveAddressIndex >= receiveAddresses.length) { @@ -272,16 +273,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress({ + List? inputs, + List? outputs, + bool isPegIn = false, + }) async { updateChangeAddresses(); - if (changeAddresses.isEmpty) { - final newAddresses = await _createNewAddresses(gap, - startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, - isHidden: true); - addAddresses(newAddresses); - } - if (currentChangeAddressIndex >= changeAddresses.length) { currentChangeAddressIndex = 0; } @@ -297,7 +295,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final labels = {}; for (int i = 0; i < silentAddresses.length; i++) { final silentAddressRecord = silentAddresses[i]; - final silentPaymentTweak = silentAddressRecord.silentPaymentTweak; + final silentPaymentTweak = silentAddressRecord.labelHex; if (silentPaymentTweak != null && SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { @@ -321,53 +319,87 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final address = BitcoinSilentPaymentAddressRecord( silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(), - index: currentSilentAddressIndex, - isHidden: false, + labelIndex: currentSilentAddressIndex, name: label, - silentPaymentTweak: - BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), - network: network, + labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), type: SilentPaymentsAddresType.p2sp, ); silentAddresses.add(address); - updateAddressesByMatch(); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } final newAddressIndex = addressesByReceiveType.fold( - 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); + 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); + final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType); final address = BitcoinAddressRecord( - getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType), + getAddress( + derivationType: CWBitcoinDerivationType.bip39, + isChange: false, + index: newAddressIndex, + addressType: addressPageType, + derivationInfo: derivationInfo, + ), index: newAddressIndex, - isHidden: false, + isChange: false, name: label, type: addressPageType, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), + derivationType: CWBitcoinDerivationType.bip39, ); - _addresses.add(address); - updateAddressesByMatch(); + _allAddresses.add(address); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } - String getAddress({ + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, - }) => - ''; + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + throw UnimplementedError(); + } + + String getAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + return generateAddress( + derivationType: derivationType, + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ).toAddress(network); + } Future getAddressAsync({ + required CWBitcoinDerivationType derivationType, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) async => - getAddress(index: index, hd: hd, addressType: addressType); + getAddress( + derivationType: derivationType, + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ); + @action void addBitcoinAddressTypes() { - final lastP2wpkh = _addresses + final lastP2wpkh = _allAddresses .where((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() @@ -378,7 +410,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2WPKH'; } - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { addressesMap[lastP2pkh.address] = 'P2PKH'; @@ -386,7 +418,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2PKH'; } - final lastP2sh = _addresses.firstWhere((addressRecord) => + final lastP2sh = _allAddresses.firstWhere((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); if (lastP2sh.address != address) { addressesMap[lastP2sh.address] = 'P2SH'; @@ -394,7 +426,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2SH'; } - final lastP2tr = _addresses.firstWhere( + final lastP2tr = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); if (lastP2tr.address != address) { addressesMap[lastP2tr.address] = 'P2TR'; @@ -402,7 +434,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2TR'; } - final lastP2wsh = _addresses.firstWhere( + final lastP2wsh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); if (lastP2wsh.address != address) { addressesMap[lastP2wsh.address] = 'P2WSH'; @@ -411,7 +443,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { return; } @@ -425,8 +457,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }); } + @action void addLitecoinAddressTypes() { - final lastP2wpkh = _addresses + final lastP2wpkh = _allAddresses .where((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() @@ -437,7 +470,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2WPKH'; } - final lastMweb = _addresses.firstWhere( + final lastMweb = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); if (lastMweb.address != address) { addressesMap[lastMweb.address] = 'MWEB'; @@ -446,8 +479,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } } + @action void addBitcoinCashAddressTypes() { - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { addressesMap[lastP2pkh.address] = 'P2PKH'; @@ -457,13 +491,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @override + @action Future updateAddressesInBox() async { try { addressesMap.clear(); addressesMap[address] = 'Active'; allAddressesMap.clear(); - _addresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { allAddressesMap[addressRecord.address] = addressRecord.name; }); @@ -490,7 +525,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAddress(String address, String label) { BaseBitcoinAddressRecord? foundAddress; - _addresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { if (addressRecord.address == address) { foundAddress = addressRecord; } @@ -509,11 +544,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (foundAddress != null) { foundAddress!.setNewName(label); - if (foundAddress is BitcoinAddressRecord) { - final index = _addresses.indexOf(foundAddress); - _addresses.remove(foundAddress); - _addresses.insert(index, foundAddress as BitcoinAddressRecord); - } else { + if (foundAddress is! BitcoinAddressRecord) { final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); silentAddresses.remove(foundAddress); silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); @@ -530,86 +561,103 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } addressesByReceiveType.clear(); - addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); + addressesByReceiveType.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList()); } @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAddresses = - _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + final newAddresses = _allAddresses.where((addressRecord) => !addressRecord.isChange); receiveAddresses.addAll(newAddresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAddresses = _addresses.where((addressRecord) => - addressRecord.isHidden && - !addressRecord.isUsed && - // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + final newAddresses = _allAddresses.where((addressRecord) => + addressRecord.isChange && (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); changeAddresses.addAll(newAddresses); } @action - Future discoverAddresses(List addressList, bool isHidden, - Future Function(BitcoinAddressRecord) getAddressHistory, - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - final newAddresses = await _createNewAddresses(gap, - startIndex: addressList.length, isHidden: isHidden, type: type); + Future> discoverAddresses({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required BitcoinAddressType type, + required BitcoinDerivationInfo derivationInfo, + }) async { + final gap = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + final newAddresses = await _createNewAddresses( + derivationType: derivationType, + gap, + isChange: isChange, + type: type, + derivationInfo: derivationInfo, + ); addAddresses(newAddresses); - - final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory)); - final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; - - if (isLastAddressUsed) { - discoverAddresses(addressList, isHidden, getAddressHistory, type: type); - } + return newAddresses; } - Future _generateInitialAddresses( - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - var countOfReceiveAddresses = 0; - var countOfHiddenAddresses = 0; - - _addresses.forEach((addr) { - if (addr.type == type) { - if (addr.isHidden) { - countOfHiddenAddresses += 1; - return; - } - - countOfReceiveAddresses += 1; + @action + Future generateInitialAddresses({required BitcoinAddressType type}) async { + for (final derivationType in hdWallets.keys) { + if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) { + continue; } - }); - if (countOfReceiveAddresses < defaultReceiveAddressesCount) { - final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfReceiveAddresses, isHidden: false, type: type); - addAddresses(newAddresses); - } + final derivationInfo = BitcoinAddressUtils.getDerivationFromType( + type, + isElectrum: derivationType == CWBitcoinDerivationType.electrum, + ); - if (countOfHiddenAddresses < defaultChangeAddressesCount) { - final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfHiddenAddresses, isHidden: true, type: type); - addAddresses(newAddresses); + await discoverAddresses( + derivationType: derivationType, + isChange: false, + type: type, + derivationInfo: derivationInfo, + ); + await discoverAddresses( + derivationType: derivationType, + isChange: true, + type: type, + derivationInfo: derivationInfo, + ); } } - Future> _createNewAddresses(int count, - {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { + @action + Future> _createNewAddresses( + int count, { + required CWBitcoinDerivationType derivationType, + required BitcoinDerivationInfo derivationInfo, + bool isChange = false, + BitcoinAddressType? type, + }) async { final list = []; + final startIndex = (isChange ? receiveAddresses : changeAddresses) + .where((addr) => addr.derivationType == derivationType && addr.type == type) + .length; for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync( + derivationType: derivationType, + isChange: isChange, + index: i, + addressType: type ?? addressPageType, + derivationInfo: derivationInfo, + ), index: i, - isHidden: isHidden, + isChange: isChange, + isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh, type: type ?? addressPageType, network: network, + derivationInfo: derivationInfo, + derivationType: derivationType, ); list.add(address); } @@ -617,13 +665,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return list; } + @action + void updateAdresses(Iterable addresses) { + for (final address in addresses) { + final index = _allAddresses.indexWhere((element) => element.address == address.address); + _allAddresses.replaceRange(index, index + 1, [address]); + + updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); + } + } + @action void addAddresses(Iterable addresses) { - final addressesSet = this._addresses.toSet(); - addressesSet.addAll(addresses); - this._addresses.clear(); - this._addresses.addAll(addressesSet); + this._allAddresses.addAll(addresses); updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); + + this.hiddenAddresses.addAll(addresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); } @action @@ -644,24 +707,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } - void _validateAddresses() { - _addresses.forEach((element) async { - if (element.type == SegwitAddresType.mweb) { - // this would add a ton of startup lag for mweb addresses since we have 1000 of them - return; - } - if (!element.isHidden && - element.address != - await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { - element.isHidden = true; - } else if (element.isHidden && - element.address != - await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { - element.isHidden = false; - } - }); - } - @action Future setAddressType(BitcoinAddressType type) async { _addressPageType = type; @@ -674,12 +719,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; - bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; - bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => - !addr.isHidden && !addr.isUsed && addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + return !addr.isChange && !addr.isUsed && addr.type == type; + } @action void deleteSilentPaymentAddress(String address) { diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 990719089..959618dcf 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -3,10 +3,8 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/encryption_file_utils.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; class ElectrumWalletSnapshot { @@ -68,7 +66,7 @@ class ElectrumWalletSnapshot { final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) .toList(); final silentAddressesTmp = data['silent_addresses'] as List? ?? []; @@ -80,7 +78,7 @@ class ElectrumWalletSnapshot { final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; final mwebAddresses = mwebAddressTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) .toList(); final alwaysScan = data['alwaysScan'] as bool? ?? false; @@ -93,7 +91,7 @@ class ElectrumWalletSnapshot { final derivationType = DerivationType .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; - final derivationPath = data['derivationPath'] as String? ?? electrum_path; + final derivationPath = data['derivationPath'] as String? ?? ELECTRUM_PATH; try { regularAddressIndexByType = { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart new file mode 100644 index 000000000..67ded289d --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -0,0 +1,794 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:http/http.dart' as http; +import 'package:sp_scanner/sp_scanner.dart'; + +class ElectrumWorker { + final SendPort sendPort; + ElectrumApiProvider? _electrumClient; + BasedUtxoNetwork? _network; + + ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) + : _electrumClient = electrumClient; + + static void run(SendPort sendPort) { + final worker = ElectrumWorker._(sendPort); + final receivePort = ReceivePort(); + + sendPort.send(receivePort.sendPort); + + receivePort.listen(worker.handleMessage); + } + + void _sendResponse(ElectrumWorkerResponse response) { + sendPort.send(jsonEncode(response.toJson())); + } + + void _sendError(ElectrumWorkerErrorResponse response) { + sendPort.send(jsonEncode(response.toJson())); + } + + void handleMessage(dynamic message) async { + print("Worker: received message: $message"); + + try { + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + final workerMethod = messageJson['method'] as String; + + switch (workerMethod) { + case ElectrumWorkerMethods.connectionMethod: + await _handleConnect( + ElectrumWorkerConnectionRequest.fromJson(messageJson), + ); + break; + case ElectrumWorkerMethods.txHashMethod: + await _handleGetTxExpanded( + ElectrumWorkerTxExpandedRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.headersSubscribeMethod: + await _handleHeadersSubscribe( + ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.scripthashesSubscribeMethod: + await _handleScriphashesSubscribe( + ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.getBalanceMethod: + await _handleGetBalance( + ElectrumWorkerGetBalanceRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.getHistoryMethod: + await _handleGetHistory( + ElectrumWorkerGetHistoryRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.listunspentMethod: + await _handleListUnspent( + ElectrumWorkerListUnspentRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.broadcastMethod: + await _handleBroadcast( + ElectrumWorkerBroadcastRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.tweaksSubscribeMethod: + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.estimateFeeMethod: + await _handleGetFeeRates( + ElectrumWorkerGetFeesRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.versionMethod: + await _handleGetVersion( + ElectrumWorkerGetVersionRequest.fromJson(messageJson), + ); + break; + } + } catch (e, s) { + print(s); + _sendError(ElectrumWorkerErrorResponse(error: e.toString())); + } + } + + Future _handleConnect(ElectrumWorkerConnectionRequest request) async { + _network = request.network; + + _electrumClient = await ElectrumApiProvider.connect( + ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), + ); + } + + Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { + final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); + if (listener == null) { + _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); + return; + } + + listener((event) { + _sendResponse( + ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id), + ); + }); + } + + Future _handleScriphashesSubscribe( + ElectrumWorkerScripthashesSubscribeRequest request, + ) async { + await Future.wait(request.scripthashByAddress.entries.map((entry) async { + final address = entry.key; + final scripthash = entry.value; + + final listener = await _electrumClient!.subscribe( + ElectrumScriptHashSubscribe(scriptHash: scripthash), + ); + + if (listener == null) { + _sendError(ElectrumWorkerScripthashesSubscribeError(error: 'Failed to subscribe')); + return; + } + + // https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status + // The status of the script hash is the hash of the tx history, or null if the string is empty because there are no transactions + listener((status) async { + print("status: $status"); + + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: status}, + id: request.id, + )); + }); + })); + } + + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async { + final Map histories = {}; + final addresses = result.addresses; + + await Future.wait(addresses.map((addressRecord) async { + final history = await _electrumClient!.request(ElectrumScriptHashGetHistory( + scriptHash: addressRecord.scriptHash, + )); + + if (history.isNotEmpty) { + addressRecord.setAsUsed(); + addressRecord.txCount = history.length; + + await Future.wait(history.map((transaction) async { + final txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + late ElectrumTransactionInfo tx; + + try { + // Exception thrown on null + tx = result.storedTxs.firstWhere((tx) => tx.id == txid); + + if (height > 0) { + tx.height = height; + + // the tx's block itself is the first confirmation so add 1 + tx.confirmations = result.chainTip - height + 1; + tx.isPending = tx.confirmations == 0; + } + } catch (_) { + tx = ElectrumTransactionInfo.fromElectrumBundle( + await _getTransactionExpanded( + hash: txid, + currentChainTip: result.chainTip, + mempoolAPIEnabled: result.mempoolAPIEnabled, + ), + result.walletType, + result.network, + addresses: result.addresses.map((addr) => addr.address).toSet(), + height: height, + ); + } + + final addressHistories = histories[addressRecord.address]; + if (addressHistories != null) { + addressHistories.txs.add(tx); + } else { + histories[addressRecord.address] = AddressHistoriesResponse( + addressRecord: addressRecord, + txs: [tx], + walletType: result.walletType, + ); + } + + return Future.value(null); + })); + } + + return histories; + })); + + _sendResponse(ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: result.id, + )); + } + + // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { + // final balanceFutures = >>[]; + + // for (final scripthash in request.scripthashes) { + // final balanceFuture = _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scripthash), + // ); + // balanceFutures.add(balanceFuture); + // } + + // var totalConfirmed = 0; + // var totalUnconfirmed = 0; + + // final balances = await Future.wait(balanceFutures); + + // for (final balance in balances) { + // final confirmed = balance['confirmed'] as int? ?? 0; + // final unconfirmed = balance['unconfirmed'] as int? ?? 0; + // totalConfirmed += confirmed; + // totalUnconfirmed += unconfirmed; + // } + + // _sendResponse(ElectrumWorkerGetBalanceResponse( + // result: ElectrumBalance( + // confirmed: totalConfirmed, + // unconfirmed: totalUnconfirmed, + // frozen: 0, + // ), + // )); + // } + + Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { + final balanceFutures = >>[]; + + for (final scripthash in request.scripthashes) { + final balanceFuture = _electrumClient!.request( + ElectrumGetScriptHashBalance(scriptHash: scripthash), + ); + balanceFutures.add(balanceFuture); + } + + var totalConfirmed = 0; + var totalUnconfirmed = 0; + + final balances = await Future.wait(balanceFutures); + + for (final balance in balances) { + final confirmed = balance['confirmed'] as int? ?? 0; + final unconfirmed = balance['unconfirmed'] as int? ?? 0; + totalConfirmed += confirmed; + totalUnconfirmed += unconfirmed; + } + + _sendResponse( + ElectrumWorkerGetBalanceResponse( + result: ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: 0, + ), + id: request.id, + ), + ); + } + + Future _handleListUnspent(ElectrumWorkerListUnspentRequest request) async { + final unspents = >{}; + + await Future.wait(request.scripthashes.map((scriptHash) async { + final scriptHashUnspents = await _electrumClient!.request( + ElectrumScriptHashListUnspent(scriptHash: scriptHash), + ); + + if (scriptHashUnspents.isNotEmpty) { + unspents[scriptHash] = scriptHashUnspents; + } + })); + + _sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id)); + } + + Future _handleBroadcast(ElectrumWorkerBroadcastRequest request) async { + final txHash = await _electrumClient!.request( + ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw), + ); + + _sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id)); + } + + Future _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async { + final tx = await _getTransactionExpanded( + hash: request.txHash, + currentChainTip: request.currentChainTip, + mempoolAPIEnabled: false, + getConfirmations: false, + ); + + _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); + } + + Future _getTransactionExpanded({ + required String hash, + required int currentChainTip, + required bool mempoolAPIEnabled, + bool getConfirmations = true, + }) async { + int? time; + int? height; + int? confirmations; + + final transactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); + + if (getConfirmations) { + if (mempoolAPIEnabled) { + try { + final txVerbose = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", + ), + ); + + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + ), + ); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } + } catch (_) {} + } + + if (height != null) { + if (time == null && height > 0) { + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + } + + final tip = currentChainTip; + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; + } + } + } + + final original = BtcTransaction.fromRaw(transactionHex); + final ins = []; + + for (final vin in original.inputs) { + final inputTransactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: vin.txId), + ); + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } + + return ElectrumTransactionBundle( + original, + ins: ins, + time: time, + confirmations: confirmations ?? 0, + ); + } + + Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { + if (request.mempoolAPIEnabled) { + try { + final recommendedFees = await ApiProvider.fromMempool( + _network!, + baseUrl: "http://mempool.cakewallet.com:8999/api", + ).getRecommendedFeeRate(); + + final unimportantFee = recommendedFees.economyFee!.satoshis; + final normalFee = recommendedFees.low.satoshis; + int elevatedFee = recommendedFees.medium.satoshis; + int priorityFee = recommendedFees.high.satoshis; + + // Bitcoin only: adjust fee rates to avoid equal fee values + // elevated fee should be higher than normal fee + if (normalFee == elevatedFee) { + elevatedFee++; + } + // priority fee should be higher than elevated fee + while (priorityFee <= elevatedFee) { + priorityFee++; + } + // this guarantees that, even if all fees are low and equal, + // higher priority fee txs can be consumed when chain fees start surging + + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: BitcoinTransactionPriorities( + unimportant: unimportantFee, + normal: normalFee, + elevated: elevatedFee, + priority: priorityFee, + custom: unimportantFee, + ), + ), + ); + } catch (e) { + _sendError(ElectrumWorkerGetFeesError(error: e.toString())); + } + } else { + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: ElectrumTransactionPriorities.fromList( + await _electrumClient!.getFeeRates(), + ), + ), + ); + } + } + + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { + final scanData = request.scanData; + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; + + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); + + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: StartingScanSyncStatus(syncHeight), + ), + )); + + final listener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + + Future listenFn(ElectrumTweaksSubscribeResponse response) async { + // success or error msg + final noData = response.message != null; + + if (noData) { + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + final nextListener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + nextListener?.call(listenFn); + } + + return; + } + + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus), + )); + + final tweakHeight = response.block; + + try { + final blockTweaks = response.blockTweaks; + + for (final txid in blockTweaks.keys) { + final tweakData = blockTweaks[txid]; + final outputPubkeys = tweakData!.outputPubkeys; + final tweak = tweakData.tweak; + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + isReplaced: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + isReceivedSilentPayment: true, + ); + + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); + + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + final matchingOutput = outputPubkeys[output]!; + final amount = matchingOutput.amount; + final pos = matchingOutput.vout; + + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + receivingOutputAddress, + labelIndex: 1, // TODO: get actual index/label + isUsed: true, + spendKey: scanData.silentAddress.b_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), + ), + txCount: 1, + balance: amount, + ); + + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}), + )); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + + syncHeight = tweakHeight; + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: SyncedTipSyncStatus(scanData.chainTip), + ), + )); + + if (scanData.isSingleScan) { + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()), + )); + } + } + } + + listener?.call(listenFn); + } + + Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { + _sendResponse(ElectrumWorkerGetVersionResponse( + result: (await _electrumClient!.request( + ElectrumVersion( + clientName: "", + protocolVersion: ["1.4"], + ), + )), + id: request.id)); + } +} + +Future delegatedScan(ScanData scanData) async { + // int syncHeight = scanData.height; + // int initialSyncHeight = syncHeight; + + // BehaviorSubject? tweaksSubscription = null; + + // final electrumClient = scanData.electrumClient; + // await electrumClient.connectToUri( + // scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), + // useSSL: scanData.node?.useSSL ?? false, + // ); + + // if (tweaksSubscription == null) { + // scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); + + // tweaksSubscription = await electrumClient.tweaksScan( + // pubSpendKey: scanData.silentAddress.B_spend.toHex(), + // ); + + // Future listenFn(t) async { + // final tweaks = t as Map; + // final msg = tweaks["message"]; + + // // success or error msg + // final noData = msg != null; + // if (noData) { + // return; + // } + + // // Continuous status UI update, send how many blocks left to scan + // final syncingStatus = scanData.isSingleScan + // ? SyncingSyncStatus(1, 0) + // : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + // scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + // final blockHeight = tweaks.keys.first; + // final tweakHeight = int.parse(blockHeight); + + // try { + // final blockTweaks = tweaks[blockHeight] as Map; + + // for (var j = 0; j < blockTweaks.keys.length; j++) { + // final txid = blockTweaks.keys.elementAt(j); + // final details = blockTweaks[txid] as Map; + // final outputPubkeys = (details["output_pubkeys"] as Map); + // final spendingKey = details["spending_key"].toString(); + + // try { + // // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + // final txInfo = ElectrumTransactionInfo( + // WalletType.bitcoin, + // id: txid, + // height: tweakHeight, + // amount: 0, + // fee: 0, + // direction: TransactionDirection.incoming, + // isPending: false, + // isReplaced: false, + // date: scanData.network == BitcoinNetwork.mainnet + // ? getDateByBitcoinHeight(tweakHeight) + // : DateTime.now(), + // confirmations: scanData.chainTip - tweakHeight + 1, + // unspents: [], + // isReceivedSilentPayment: true, + // ); + + // outputPubkeys.forEach((pos, value) { + // final secKey = ECPrivate.fromHex(spendingKey); + // final receivingOutputAddress = + // secKey.getPublic().toTaprootAddress(tweak: false).toAddress(scanData.network); + + // late int amount; + // try { + // amount = int.parse(value[1].toString()); + // } catch (_) { + // return; + // } + + // final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + // receivingOutputAddress, + // labelIndex: 0, + // isUsed: true, + // spendKey: secKey, + // txCount: 1, + // balance: amount, + // ); + + // final unspent = BitcoinUnspent( + // receivedAddressRecord, + // txid, + // amount, + // int.parse(pos.toString()), + // ); + + // txInfo.unspents!.add(unspent); + // txInfo.amount += unspent.value; + // }); + + // scanData.sendPort.send({txInfo.id: txInfo}); + // } catch (_) {} + // } + // } catch (_) {} + + // syncHeight = tweakHeight; + + // if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + // if (tweakHeight >= scanData.chainTip) + // scanData.sendPort.send(SyncResponse( + // syncHeight, + // SyncedTipSyncStatus(scanData.chainTip), + // )); + + // if (scanData.isSingleScan) { + // scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + // } + + // await tweaksSubscription!.close(); + // await electrumClient.close(); + // } + // } + + // tweaksSubscription?.listen(listenFn); + // } + + // if (tweaksSubscription == null) { + // return scanData.sendPort.send( + // SyncResponse(syncHeight, UnsupportedSyncStatus()), + // ); + // } +} + +class ScanNode { + final Uri uri; + final bool? useSSL; + + ScanNode(this.uri, this.useSSL); +} diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart new file mode 100644 index 000000000..6bd4d296e --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -0,0 +1,17 @@ +class ElectrumWorkerMethods { + const ElectrumWorkerMethods._(this.method); + final String method; + + static const String connectionMethod = "connection"; + static const String unknownMethod = "unknown"; + static const String txHashMethod = "txHash"; + + static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); + static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); + static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); + + @override + String toString() { + return method; + } +} diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart new file mode 100644 index 000000000..ea3c0b199 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -0,0 +1,53 @@ +// import 'dart:convert'; + +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; + +abstract class ElectrumWorkerRequest { + abstract final String method; + abstract final int? id; + + Map toJson(); + ElectrumWorkerRequest.fromJson(Map json); +} + +class ElectrumWorkerResponse { + ElectrumWorkerResponse({ + required this.method, + required this.result, + this.error, + this.id, + }); + + final String method; + final RESULT result; + final String? error; + final int? id; + + RESPONSE resultJson(RESULT result) { + throw UnimplementedError(); + } + + factory ElectrumWorkerResponse.fromJson(Map json) { + throw UnimplementedError(); + } + + Map toJson() { + return {'method': method, 'result': resultJson(result), 'error': error, 'id': id}; + } +} + +class ElectrumWorkerErrorResponse { + ElectrumWorkerErrorResponse({required this.error, this.id}); + + String get method => ElectrumWorkerMethods.unknown.method; + final int? id; + final String error; + + factory ElectrumWorkerErrorResponse.fromJson(Map json) { + return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int); + } + + Map toJson() { + return {'method': method, 'error': error, 'id': id}; + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart new file mode 100644 index 000000000..f295fa24a --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart @@ -0,0 +1,56 @@ +part of 'methods.dart'; + +class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest { + ElectrumWorkerBroadcastRequest({required this.transactionRaw, this.id}); + + final String transactionRaw; + final int? id; + + @override + final String method = ElectrumRequestMethods.broadcast.method; + + @override + factory ElectrumWorkerBroadcastRequest.fromJson(Map json) { + return ElectrumWorkerBroadcastRequest( + transactionRaw: json['transactionRaw'] as String, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'transactionRaw': transactionRaw}; + } +} + +class ElectrumWorkerBroadcastError extends ElectrumWorkerErrorResponse { + ElectrumWorkerBroadcastError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.broadcast.method; +} + +class ElectrumWorkerBroadcastResponse extends ElectrumWorkerResponse { + ElectrumWorkerBroadcastResponse({ + required String txHash, + super.error, + super.id, + }) : super(result: txHash, method: ElectrumRequestMethods.broadcast.method); + + @override + String resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerBroadcastResponse.fromJson(Map json) { + return ElectrumWorkerBroadcastResponse( + txHash: json['result'] as String, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/connection.dart b/cw_bitcoin/lib/electrum_worker/methods/connection.dart new file mode 100644 index 000000000..2512c6cfd --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -0,0 +1,73 @@ +part of 'methods.dart'; + +class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { + ElectrumWorkerConnectionRequest({ + required this.uri, + required this.network, + this.id, + }); + + final Uri uri; + final BasedUtxoNetwork network; + final int? id; + + @override + final String method = ElectrumWorkerMethods.connect.method; + + @override + factory ElectrumWorkerConnectionRequest.fromJson(Map json) { + return ElectrumWorkerConnectionRequest( + uri: Uri.parse(json['uri'] as String), + network: BasedUtxoNetwork.values.firstWhere( + (e) => e.toString() == json['network'] as String, + ), + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return { + 'method': method, + 'uri': uri.toString(), + 'network': network.toString(), + }; + } +} + +class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse { + ElectrumWorkerConnectionError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumWorkerMethods.connect.method; +} + +class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse { + ElectrumWorkerConnectionResponse({ + required ConnectionStatus status, + super.error, + super.id, + }) : super( + result: status, + method: ElectrumWorkerMethods.connect.method, + ); + + @override + String resultJson(result) { + return result.toString(); + } + + @override + factory ElectrumWorkerConnectionResponse.fromJson(Map json) { + return ElectrumWorkerConnectionResponse( + status: ConnectionStatus.values.firstWhere( + (e) => e.toString() == json['result'] as String, + ), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart new file mode 100644 index 000000000..2fc551367 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -0,0 +1,61 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id}); + + final Set scripthashes; + final int? id; + + @override + final String method = ElectrumRequestMethods.getBalance.method; + + @override + factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { + return ElectrumWorkerGetBalanceRequest( + scripthashes: (json['scripthashes'] as List).toSet(), + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashes.toList()}; + } +} + +class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetBalanceError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.getBalance.method; +} + +class ElectrumWorkerGetBalanceResponse + extends ElectrumWorkerResponse?> { + ElectrumWorkerGetBalanceResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getBalance.method); + + @override + Map? resultJson(result) { + return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; + } + + @override + factory ElectrumWorkerGetBalanceResponse.fromJson(Map json) { + return ElectrumWorkerGetBalanceResponse( + result: ElectrumBalance( + confirmed: json['result']['confirmed'] as int, + unconfirmed: json['result']['unconfirmed'] as int, + frozen: 0, + ), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart new file mode 100644 index 000000000..be81e5346 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetFeesRequest({ + required this.mempoolAPIEnabled, + this.id, + }); + + final bool mempoolAPIEnabled; + final int? id; + + @override + final String method = ElectrumRequestMethods.estimateFee.method; + + @override + factory ElectrumWorkerGetFeesRequest.fromJson(Map json) { + return ElectrumWorkerGetFeesRequest( + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'mempoolAPIEnabled': mempoolAPIEnabled}; + } +} + +class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetFeesError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.estimateFee.method; +} + +class ElectrumWorkerGetFeesResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerGetFeesResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.estimateFee.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerGetFeesResponse.fromJson(Map json) { + return ElectrumWorkerGetFeesResponse( + result: deserializeTransactionPriorities(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart new file mode 100644 index 000000000..021ed6899 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -0,0 +1,120 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetHistoryRequest({ + required this.addresses, + required this.storedTxs, + required this.walletType, + required this.chainTip, + required this.network, + required this.mempoolAPIEnabled, + this.id, + }); + + final List addresses; + final List storedTxs; + final WalletType walletType; + final int chainTip; + final BasedUtxoNetwork network; + final bool mempoolAPIEnabled; + final int? id; + + @override + final String method = ElectrumRequestMethods.getHistory.method; + + @override + factory ElectrumWorkerGetHistoryRequest.fromJson(Map json) { + final walletType = WalletType.values[json['walletType'] as int]; + + return ElectrumWorkerGetHistoryRequest( + addresses: (json['addresses'] as List) + .map((e) => BitcoinAddressRecord.fromJSON(e as String)) + .toList(), + storedTxs: (json['storedTxIds'] as List) + .map((e) => ElectrumTransactionInfo.fromJson(e as Map, walletType)) + .toList(), + walletType: walletType, + chainTip: json['chainTip'] as int, + network: BasedUtxoNetwork.fromName(json['network'] as String), + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return { + 'method': method, + 'addresses': addresses.map((e) => e.toJSON()).toList(), + 'storedTxIds': storedTxs.map((e) => e.toJson()).toList(), + 'walletType': walletType.index, + 'chainTip': chainTip, + 'network': network.value, + 'mempoolAPIEnabled': mempoolAPIEnabled, + }; + } +} + +class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetHistoryError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.getHistory.method; +} + +class AddressHistoriesResponse { + final BitcoinAddressRecord addressRecord; + final List txs; + final WalletType walletType; + + AddressHistoriesResponse( + {required this.addressRecord, required this.txs, required this.walletType}); + + factory AddressHistoriesResponse.fromJson(Map json) { + final walletType = WalletType.values[json['walletType'] as int]; + + return AddressHistoriesResponse( + addressRecord: BitcoinAddressRecord.fromJSON(json['address'] as String), + txs: (json['txs'] as List) + .map((e) => ElectrumTransactionInfo.fromJson(e as Map, walletType)) + .toList(), + walletType: walletType, + ); + } + + Map toJson() { + return { + 'address': addressRecord.toJSON(), + 'txs': txs.map((e) => e.toJson()).toList(), + 'walletType': walletType.index, + }; + } +} + +class ElectrumWorkerGetHistoryResponse + extends ElectrumWorkerResponse, List>> { + ElectrumWorkerGetHistoryResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getHistory.method); + + @override + List> resultJson(result) { + return result.map((e) => e.toJson()).toList(); + } + + @override + factory ElectrumWorkerGetHistoryResponse.fromJson(Map json) { + return ElectrumWorkerGetHistoryResponse( + result: (json['result'] as List) + .map((e) => AddressHistoriesResponse.fromJson(e as Map)) + .toList(), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart new file mode 100644 index 000000000..a2dfcda17 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -0,0 +1,63 @@ +part of 'methods.dart'; + +class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { + ElectrumWorkerTxExpandedRequest({ + required this.txHash, + required this.currentChainTip, + this.id, + }); + + final String txHash; + final int currentChainTip; + final int? id; + + @override + final String method = ElectrumWorkerMethods.txHash.method; + + @override + factory ElectrumWorkerTxExpandedRequest.fromJson(Map json) { + return ElectrumWorkerTxExpandedRequest( + txHash: json['txHash'] as String, + currentChainTip: json['currentChainTip'] as int, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'txHash': txHash, 'currentChainTip': currentChainTip}; + } +} + +class ElectrumWorkerTxExpandedError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTxExpandedError({ + required String error, + super.id, + }) : super(error: error); + + @override + String get method => ElectrumWorkerMethods.txHash.method; +} + +class ElectrumWorkerTxExpandedResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTxExpandedResponse({ + required ElectrumTransactionBundle expandedTx, + super.error, + super.id, + }) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTxExpandedResponse.fromJson(Map json) { + return ElectrumWorkerTxExpandedResponse( + expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart new file mode 100644 index 000000000..de02f5d24 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart @@ -0,0 +1,54 @@ +part of 'methods.dart'; + +class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerHeadersSubscribeRequest({this.id}); + + @override + final String method = ElectrumRequestMethods.headersSubscribe.method; + final int? id; + + @override + factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerHeadersSubscribeRequest( + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method}; + } +} + +class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerHeadersSubscribeError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.headersSubscribe.method; +} + +class ElectrumWorkerHeadersSubscribeResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerHeadersSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.headersSubscribe.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerHeadersSubscribeResponse.fromJson(Map json) { + return ElectrumWorkerHeadersSubscribeResponse( + result: ElectrumHeaderResponse.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart new file mode 100644 index 000000000..66d1b1a68 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest { + ElectrumWorkerListUnspentRequest({required this.scripthashes, this.id}); + + final List scripthashes; + final int? id; + + @override + final String method = ElectrumRequestMethods.listunspent.method; + + @override + factory ElectrumWorkerListUnspentRequest.fromJson(Map json) { + return ElectrumWorkerListUnspentRequest( + scripthashes: json['scripthashes'] as List, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashes}; + } +} + +class ElectrumWorkerListUnspentError extends ElectrumWorkerErrorResponse { + ElectrumWorkerListUnspentError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.listunspent.method; +} + +class ElectrumWorkerListUnspentResponse + extends ElectrumWorkerResponse>, Map> { + ElectrumWorkerListUnspentResponse({ + required Map> utxos, + super.error, + super.id, + }) : super(result: utxos, method: ElectrumRequestMethods.listunspent.method); + + @override + Map resultJson(result) { + return result.map((key, value) => MapEntry(key, value.map((e) => e.toJson()).toList())); + } + + @override + factory ElectrumWorkerListUnspentResponse.fromJson(Map json) { + return ElectrumWorkerListUnspentResponse( + utxos: (json['result'] as Map).map( + (key, value) => MapEntry(key, + (value as List).map((e) => ElectrumUtxo.fromJson(e as Map)).toList()), + ), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart new file mode 100644 index 000000000..295522d39 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -0,0 +1,22 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_transaction_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; + +part 'connection.dart'; +part 'headers_subscribe.dart'; +part 'scripthashes_subscribe.dart'; +part 'get_balance.dart'; +part 'get_history.dart'; +part 'get_tx_expanded.dart'; +part 'broadcast.dart'; +part 'list_unspent.dart'; +part 'tweaks_subscribe.dart'; +part 'get_fees.dart'; +part 'version.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart new file mode 100644 index 000000000..31f9abe76 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerScripthashesSubscribeRequest({ + required this.scripthashByAddress, + this.id, + }); + + final Map scripthashByAddress; + final int? id; + + @override + final String method = ElectrumRequestMethods.scriptHashSubscribe.method; + + @override + factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: json['scripthashes'] as Map, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashByAddress}; + } +} + +class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerScripthashesSubscribeError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.scriptHashSubscribe.method; +} + +class ElectrumWorkerScripthashesSubscribeResponse + extends ElectrumWorkerResponse?, Map?> { + ElectrumWorkerScripthashesSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); + + @override + Map? resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerScripthashesSubscribeResponse.fromJson(Map json) { + return ElectrumWorkerScripthashesSubscribeResponse( + result: json['result'] as Map?, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart new file mode 100644 index 000000000..0a6f36dc9 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -0,0 +1,157 @@ +part of 'methods.dart'; + +class ScanData { + final SilentPaymentOwner silentAddress; + final int height; + final BasedUtxoNetwork network; + final int chainTip; + final List transactionHistoryIds; + final Map labels; + final List labelIndexes; + final bool isSingleScan; + + ScanData({ + required this.silentAddress, + required this.height, + required this.network, + required this.chainTip, + required this.transactionHistoryIds, + required this.labels, + required this.labelIndexes, + required this.isSingleScan, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + silentAddress: scanData.silentAddress, + height: newHeight, + network: scanData.network, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } + + Map toJson() { + return { + 'silentAddress': silentAddress.toJson(), + 'height': height, + 'network': network.value, + 'chainTip': chainTip, + 'transactionHistoryIds': transactionHistoryIds, + 'labels': labels, + 'labelIndexes': labelIndexes, + 'isSingleScan': isSingleScan, + }; + } + + static ScanData fromJson(Map json) { + return ScanData( + silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map), + height: json['height'] as int, + network: BasedUtxoNetwork.fromName(json['network'] as String), + chainTip: json['chainTip'] as int, + transactionHistoryIds: + (json['transactionHistoryIds'] as List).map((e) => e as String).toList(), + labels: json['labels'] as Map, + labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(), + isSingleScan: json['isSingleScan'] as bool, + ); + } +} + +class ElectrumWorkerTweaksSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerTweaksSubscribeRequest({ + required this.scanData, + this.id, + }); + + final ScanData scanData; + final int? id; + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; + + @override + factory ElectrumWorkerTweaksSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData.fromJson(json['scanData'] as Map), + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scanData': scanData.toJson()}; + } +} + +class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTweaksSubscribeError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; +} + +class TweaksSyncResponse { + int? height; + SyncStatus? syncStatus; + Map? transactions = {}; + + TweaksSyncResponse({this.height, this.syncStatus, this.transactions}); + + Map toJson() { + return { + 'height': height, + 'syncStatus': syncStatus == null ? null : syncStatusToJson(syncStatus!), + 'transactions': transactions?.map((key, value) => MapEntry(key, value.toJson())), + }; + } + + static TweaksSyncResponse fromJson(Map json) { + return TweaksSyncResponse( + height: json['height'] as int?, + syncStatus: json['syncStatus'] == null + ? null + : syncStatusFromJson(json['syncStatus'] as Map), + transactions: json['transactions'] == null + ? null + : (json['transactions'] as Map).map( + (key, value) => MapEntry( + key, + ElectrumTransactionInfo.fromJson( + value as Map, + WalletType.bitcoin, + )), + ), + ); + } +} + +class ElectrumWorkerTweaksSubscribeResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTweaksSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.tweaksSubscribe.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTweaksSubscribeResponse.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/version.dart b/cw_bitcoin/lib/electrum_worker/methods/version.dart new file mode 100644 index 000000000..0f3f814d3 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/version.dart @@ -0,0 +1,52 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetVersionRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetVersionRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumRequestMethods.version.method; + + @override + factory ElectrumWorkerGetVersionRequest.fromJson(Map json) { + return ElectrumWorkerGetVersionRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method}; + } +} + +class ElectrumWorkerGetVersionError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetVersionError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.version.method; +} + +class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse, List> { + ElectrumWorkerGetVersionResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.version.method); + + @override + List resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerGetVersionResponse.fromJson(Map json) { + return ElectrumWorkerGetVersionResponse( + result: json['result'] as List, + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart index 62840933c..c53a8713d 100644 --- a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/hardware/hardware_account_data.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:ledger_litecoin/ledger_litecoin.dart'; @@ -12,8 +11,7 @@ class LitecoinHardwareWalletService { final LedgerConnection ledgerConnection; - Future> getAvailableAccounts( - {int index = 0, int limit = 5}) async { + Future> getAvailableAccounts({int index = 0, int limit = 5}) async { final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); await litecoinLedgerApp.getVersion(); @@ -27,14 +25,18 @@ class LitecoinHardwareWalletService { final xpub = await litecoinLedgerApp.getXPubKey( accountsDerivationPath: derivationPath, xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); - final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) - .childKey(Bip32KeyIndex(0)); + final bip32 = + Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0)); - final address = generateP2WPKHAddress( - hd: hd, index: 0, network: LitecoinNetwork.mainnet); + final address = P2wpkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: BitcoinDerivationInfos.LITECOIN, + isChange: false, + index: 0, + ); accounts.add(HardwareAccountData( - address: address, + address: address.toAddress(LitecoinNetwork.mainnet), accountIndex: i, derivationPath: derivationPath, xpub: xpub, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1fb39c878..815890757 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +// import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -21,8 +21,6 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/utils.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -70,6 +68,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int? initialMwebHeight, bool? alwaysScan, + required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, password: password, @@ -83,10 +82,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ) { if (seedBytes != null) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - "m/1000'") as Bip32Slip10Secp256k1; + mwebHd = + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; } else { mwebHd = null; @@ -98,38 +98,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, initialMwebAddresses: initialMwebAddresses, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); reaction((_) => mwebSyncStatus, (status) async { if (mwebSyncStatus is FailedSyncStatus) { - // we failed to connect to mweb, check if we are connected to the litecoin node: - late int nodeHeight; - try { - nodeHeight = await electrumClient.getCurrentBlockChainTip() ?? 0; - } catch (_) { - nodeHeight = 0; - } - - if (nodeHeight == 0) { - // we aren't connected to the litecoin node, so the current electrum_wallet reactions will take care of this case for us - } else { - // we're connected to the litecoin node, but we failed to connect to mweb, try again after a few seconds: - await CwMweb.stop(); - await Future.delayed(const Duration(seconds: 5)); - startSync(); - } + await CwMweb.stop(); + await Future.delayed(const Duration(seconds: 5)); + startSync(); } else if (mwebSyncStatus is SyncingSyncStatus) { syncStatus = mwebSyncStatus; - } else if (mwebSyncStatus is SyncronizingSyncStatus) { - if (syncStatus is! SyncronizingSyncStatus) { + } else if (mwebSyncStatus is SynchronizingSyncStatus) { + if (syncStatus is! SynchronizingSyncStatus) { syncStatus = mwebSyncStatus; } } else if (mwebSyncStatus is SyncedSyncStatus) { @@ -154,20 +140,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; - static Future create( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required EncryptionFileUtils encryptionFileUtils, - String? passphrase, - String? addressPageType, - List? initialAddresses, - List? initialMwebAddresses, - ElectrumBalance? initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex}) async { + static Future create({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, + String? passphrase, + String? addressPageType, + List? initialAddresses, + List? initialMwebAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + required bool mempoolAPIEnabled, + }) async { late Uint8List seedBytes; + late BitcoinDerivationType derivationType; switch (walletInfo.derivationInfo?.derivationType) { case DerivationType.bip39: @@ -175,10 +164,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic, passphrase: passphrase ?? "", ); + derivationType = BitcoinDerivationType.bip39; break; case DerivationType.electrum: default: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + derivationType = BitcoinDerivationType.electrum; break; } return LitecoinWallet( @@ -195,6 +186,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -204,6 +196,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, required bool alwaysScan, + required bool mempoolAPIEnabled, required EncryptionFileUtils encryptionFileUtils, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); @@ -239,10 +232,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo ??= DerivationInfo(); // set the default if not present: - walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; + walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? ELECTRUM_PATH; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; + late BitcoinDerivationType derivationType; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; @@ -253,10 +247,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic, passphrase: passphrase ?? "", ); + derivationType = BitcoinDerivationType.bip39; break; case DerivationType.electrum: default: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + derivationType = BitcoinDerivationType.electrum; break; } } @@ -277,6 +273,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -301,16 +298,16 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } - if (mwebSyncStatus is SyncronizingSyncStatus) { + if (mwebSyncStatus is SynchronizingSyncStatus) { return; } print("STARTING SYNC - MWEB ENABLED: $mwebEnabled"); _syncTimer?.cancel(); try { - mwebSyncStatus = SyncronizingSyncStatus(); + mwebSyncStatus = SynchronizingSyncStatus(); try { - await subscribeForUpdates(); + await subscribeForUpdates([]); } catch (e) { print("failed to subcribe for updates: $e"); } @@ -338,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } - final nodeHeight = - await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + final nodeHeight = await currentChainTip ?? 0; if (nodeHeight == 0) { // we aren't connected to the ltc node yet @@ -430,13 +426,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @action @override - Future rescan({ - required int height, - int? chainTip, - ScanData? scanData, - bool? doSingleScan, - bool? usingElectrs, - }) async { + Future rescan({required int height}) async { _syncTimer?.cancel(); await walletInfo.updateRestoreHeight(height); @@ -559,8 +549,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } _utxoStream = responseStream.listen((Utxo sUtxo) async { // we're processing utxos, so our balance could still be innacurate: - if (mwebSyncStatus is! SyncronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { - mwebSyncStatus = SyncronizingSyncStatus(); + if (mwebSyncStatus is! SynchronizingSyncStatus && mwebSyncStatus is! SyncingSyncStatus) { + mwebSyncStatus = SynchronizingSyncStatus(); processingUtxos = true; _processingTimer?.cancel(); _processingTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { @@ -631,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } final status = await CwMweb.status(StatusRequest()); - final height = await electrumClient.getCurrentBlockChainTip(); + final height = await currentChainTip; if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced int amount = 0; @@ -766,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); // copy coin control attributes to mwebCoins: - await updateCoins(mwebUnspentCoins); + // await updateCoins(mwebUnspentCoins); // get regular ltc unspents (this resets unspentCoins): await super.updateAllUnspents(); // add the mwebCoins: @@ -774,94 +764,126 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future fetchBalances() async { - final balance = await super.fetchBalances(); - if (!mwebEnabled) { - return balance; - } + @action + Future> fetchTransactions() async { + throw UnimplementedError(); + // try { + // final Map historiesWithDetails = {}; - // update unspent balances: - await updateUnspent(); + // await Future.wait(LITECOIN_ADDRESS_TYPES + // .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - int confirmed = balance.confirmed; - int unconfirmed = balance.unconfirmed; - int confirmedMweb = 0; - int unconfirmedMweb = 0; - try { - mwebUtxosBox.values.forEach((utxo) { - if (utxo.height > 0) { - confirmedMweb += utxo.value.toInt(); - } else { - unconfirmedMweb += utxo.value.toInt(); - } - }); - if (unconfirmedMweb > 0) { - unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); - } - } catch (_) {} - - for (var addressRecord in walletAddresses.allAddresses) { - addressRecord.balance = 0; - addressRecord.txCount = 0; - } - - unspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - super.addCoinInfo(coin); - } - }); - - // update the txCount for each address using the tx history, since we can't rely on mwebd - // to have an accurate count, we should just keep it in sync with what we know from the tx history: - for (final tx in transactionHistory.transactions.values) { - // if (tx.isPending) continue; - if (tx.inputAddresses == null || tx.outputAddresses == null) { - continue; - } - final txAddresses = tx.inputAddresses! + tx.outputAddresses!; - for (final address in txAddresses) { - final addressRecord = walletAddresses.allAddresses - .firstWhereOrNull((addressRecord) => addressRecord.address == address); - if (addressRecord == null) { - continue; - } - addressRecord.txCount++; - } - } - - return ElectrumBalance( - confirmed: confirmed, - unconfirmed: unconfirmed, - frozen: balance.frozen, - secondConfirmed: confirmedMweb, - secondUnconfirmed: unconfirmedMweb, - ); + // return historiesWithDetails; + // } catch (e) { + // print("fetchTransactions $e"); + // return {}; + // } } + // @override + // @action + // Future subscribeForUpdates([ + // Iterable? unsubscribedScriptHashes, + // ]) async { + // final unsubscribedScriptHashes = walletAddresses.allAddresses.where( + // (address) => + // !scripthashesListening.contains(address.scriptHash) && + // address.type != SegwitAddresType.mweb, + // ); + + // return super.subscribeForUpdates(unsubscribedScriptHashes); + // } + + // @override + // Future fetchBalances() async { + // final balance = await super.fetchBalances(); + + // if (!mwebEnabled) { + // return balance; + // } + + // // update unspent balances: + // await updateUnspent(); + + // int confirmed = balance.confirmed; + // int unconfirmed = balance.unconfirmed; + // int confirmedMweb = 0; + // int unconfirmedMweb = 0; + // try { + // mwebUtxosBox.values.forEach((utxo) { + // if (utxo.height > 0) { + // confirmedMweb += utxo.value.toInt(); + // } else { + // unconfirmedMweb += utxo.value.toInt(); + // } + // }); + // if (unconfirmedMweb > 0) { + // unconfirmedMweb = -1 * (confirmedMweb - unconfirmedMweb); + // } + // } catch (_) {} + + // for (var addressRecord in walletAddresses.allAddresses) { + // addressRecord.balance = 0; + // addressRecord.txCount = 0; + // } + + // unspentCoins.forEach((coin) { + // final coinInfoList = unspentCoinsInfo.values.where( + // (element) => + // element.walletId.contains(id) && + // element.hash.contains(coin.hash) && + // element.vout == coin.vout, + // ); + + // if (coinInfoList.isNotEmpty) { + // final coinInfo = coinInfoList.first; + + // coin.isFrozen = coinInfo.isFrozen; + // coin.isSending = coinInfo.isSending; + // coin.note = coinInfo.note; + // if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + // coin.bitcoinAddressRecord.balance += coinInfo.value; + // } else { + // super.addCoinInfo(coin); + // } + // }); + + // // update the txCount for each address using the tx history, since we can't rely on mwebd + // // to have an accurate count, we should just keep it in sync with what we know from the tx history: + // for (final tx in transactionHistory.transactions.values) { + // // if (tx.isPending) continue; + // if (tx.inputAddresses == null || tx.outputAddresses == null) { + // continue; + // } + // final txAddresses = tx.inputAddresses! + tx.outputAddresses!; + // for (final address in txAddresses) { + // final addressRecord = walletAddresses.allAddresses + // .firstWhereOrNull((addressRecord) => addressRecord.address == address); + // if (addressRecord == null) { + // continue; + // } + // addressRecord.txCount++; + // } + // } + + // return ElectrumBalance( + // confirmed: confirmed, + // unconfirmed: unconfirmed, + // frozen: balance.frozen, + // secondConfirmed: confirmedMweb, + // secondUnconfirmed: unconfirmedMweb, + // ); + // } + @override int feeRate(TransactionPriority priority) { - if (priority is LitecoinTransactionPriority) { + if (priority is ElectrumTransactionPriority) { switch (priority) { - case LitecoinTransactionPriority.slow: + case ElectrumTransactionPriority.slow: return 1; - case LitecoinTransactionPriority.medium: + case ElectrumTransactionPriority.medium: return 2; - case LitecoinTransactionPriority.fast: + case ElectrumTransactionPriority.fast: return 3; } } @@ -873,25 +895,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future calcFee({ required List utxos, required List outputs, - required BasedUtxoNetwork network, String? memo, required int feeRate, - List? inputPrivKeyInfos, - List? vinOutpoints, }) async { final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); final paysToMweb = outputs .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); if (!spendsMweb && !paysToMweb) { - return await super.calcFee( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - feeRate: feeRate, - inputPrivKeyInfos: inputPrivKeyInfos, - vinOutpoints: vinOutpoints, - ); + return await super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate); } if (!mwebEnabled) { @@ -901,7 +912,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (outputs.length == 1 && outputs[0].toOutput.amount == BigInt.zero) { outputs = [ BitcoinScriptOutput( - script: outputs[0].toOutput.scriptPubKey, value: utxos.sumOfUtxosValue()) + script: outputs[0].toOutput.scriptPubKey, + value: utxos.sumOfUtxosValue(), + ) ]; } @@ -928,14 +941,14 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { var feeIncrease = posOutputSum - expectedPegin; if (expectedPegin > 0 && fee == BigInt.zero) { feeIncrease += await super.calcFee( - utxos: posUtxos, - outputs: tx.outputs - .map((output) => - BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) - .toList(), - network: network, - memo: memo, - feeRate: feeRate) + + utxos: posUtxos, + outputs: tx.outputs + .map((output) => + BitcoinScriptOutput(script: output.scriptPubKey, value: output.amount)) + .toList(), + memo: memo, + feeRate: feeRate, + ) + feeRate * 41; } return fee.toInt() + feeIncrease; @@ -949,8 +962,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: false)) + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false)) .address; return tx; } @@ -990,10 +1002,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool isPegIn = !hasMwebInput && hasMwebOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular)) - .address; + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1025,12 +1036,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { witnesses: tx2.inputs.asMap().entries.map((e) { final utxo = unspentCoins .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); - final key = generateECPrivate( - hd: utxo.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: utxo.bitcoinAddressRecord.index, - network: network); + final addressRecord = (utxo.bitcoinAddressRecord as BitcoinAddressRecord); + final path = addressRecord.derivationInfo.derivationPath + .addElem( + Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange))) + .addElem(Bip32KeyIndex(addressRecord.index)); + final key = ECPrivate.fromBip32(bip32: bip32.derive(path)); final digest = tx2.getTransactionSegwitDigit( txInIndex: e.key, script: key.getPublic().toP2pkhAddress().toScriptPubKey(), @@ -1113,10 +1124,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future signMessage(String message, {String? address = null}) async { - final index = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index - : null; - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); + Bip32Slip10Secp256k1 HD = bip32; + + final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address); + + if (record.isChange) { + HD = HD.childKey(Bip32KeyIndex(1)); + } else { + HD = HD.childKey(Bip32KeyIndex(0)); + } + + HD = HD.childKey(Bip32KeyIndex(record.index)); final priv = ECPrivate.fromHex(HD.privateKey.privKey.toHex()); final privateKey = ECDSAPrivateKey.fromBytes( @@ -1240,8 +1258,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override void setLedgerConnection(LedgerConnection connection) { _ledgerConnection = connection; - _litecoinLedgerApp = - LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -1258,7 +1276,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) async { final readyInputs = []; for (final utxo in utxos) { - final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final rawTx = + (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; readyInputs.add(LedgerTransaction( @@ -1277,19 +1296,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; } - final rawHex = await _litecoinLedgerApp!.createTransaction( - inputs: readyInputs, - outputs: outputs - .map((e) => TransactionOutput.fromBigInt( - (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) - .toList(), - changePath: changePath, - sigHashType: 0x01, - additionals: ["bech32"], - isSegWit: true, - useTrustedInputForSegwit: true - ); + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value, + Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true); return BtcTransaction.fromRaw(rawHex); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index c55f5fc76..f9871a937 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -6,8 +6,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_mweb/cw_mweb.dart'; @@ -16,19 +14,16 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, required super.network, required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, + required super.hdWallets, super.initialAddresses, super.initialMwebAddresses, super.initialRegularAddressIndex, @@ -46,14 +41,13 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses List mwebAddrs = []; bool generating = false; - List get scanSecret => - mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override Future init() async { - if (!isHardwareWallet) await initMwebAddresses(); + if (!super.isHardwareWallet) await initMwebAddresses(); await super.init(); } @@ -104,12 +98,16 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses List addressRecords = mwebAddrs .asMap() .entries - .map((e) => BitcoinAddressRecord( - e.value, - index: e.key, - type: SegwitAddresType.mweb, - network: network, - )) + .map( + (e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, + ), + ) .toList(); addMwebAddresses(addressRecords); print("set ${addressRecords.length} mweb addresses"); @@ -121,30 +119,47 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses await ensureMwebAddressUpToIndexExists(20); return; } - } - @override - String getAddress({ - required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, - }) { - if (addressType == SegwitAddresType.mweb) { - return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; + @override + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + if (addressType == SegwitAddresType.mweb) { + return MwebAddress.fromAddress(address: mwebAddrs[0], network: network); + } + + return P2wpkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); } - return generateP2WPKHAddress(hd: hd, index: index, network: network); } @override Future getAddressAsync({ + required CWBitcoinDerivationType derivationType, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) async { if (addressType == SegwitAddresType.mweb) { await ensureMwebAddressUpToIndexExists(index); } - return getAddress(index: index, hd: hd, addressType: addressType); + + return getAddress( + derivationType: derivationType, + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ); } @action @@ -198,6 +213,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses index: 0, type: SegwitAddresType.mweb, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7cc266f5b..d13dcc8a4 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -23,12 +23,18 @@ class LitecoinWalletService extends WalletService< BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { LitecoinWalletService( - this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + this.walletInfoSource, + this.unspentCoinsInfoSource, + this.alwaysScan, + this.isDirect, + this.mempoolAPIEnabled, + ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; final bool isDirect; + final bool mempoolAPIEnabled; @override WalletType getType() => WalletType.litecoin; @@ -55,6 +61,7 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -68,7 +75,6 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -80,6 +86,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); saveBackup(name); @@ -93,6 +100,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; @@ -135,6 +143,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await currentWallet.renameWalletFiles(newName); @@ -161,6 +170,7 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -186,6 +196,7 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 5ed84dbf4..a8088f642 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,10 +1,10 @@ +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:grpc/grpc.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -15,11 +15,10 @@ class PendingBitcoinTransaction with PendingTransaction { PendingBitcoinTransaction( this._tx, this.type, { - required this.electrumClient, + required this.sendWorker, required this.amount, required this.fee, required this.feeRate, - this.network, required this.hasChange, this.isSendAll = false, this.hasTaprootInputs = false, @@ -29,11 +28,10 @@ class PendingBitcoinTransaction with PendingTransaction { final WalletType type; final BtcTransaction _tx; - final ElectrumClient electrumClient; + Future Function(ElectrumWorkerRequest) sendWorker; final int amount; final int fee; final String feeRate; - final BasedUtxoNetwork? network; final bool isSendAll; final bool hasChange; final bool hasTaprootInputs; @@ -51,10 +49,10 @@ class PendingBitcoinTransaction with PendingTransaction { String get hex => hexOverride ?? _tx.serialize(); @override - String get amountFormatted => bitcoinAmountToString(amount: amount); + String get amountFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: amount); @override - String get feeFormatted => bitcoinAmountToString(amount: fee); + String get feeFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: fee); @override int? get outputCount => _tx.outputs.length; @@ -80,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction { Future _commit() async { int? callId; - final result = await electrumClient.broadcastTransaction( - transactionRaw: hex, network: network, idCallback: (id) => callId = id); + final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String; - if (result.isEmpty) { - if (callId != null) { - final error = electrumClient.getErrorMessage(callId!); + // if (result.isEmpty) { + // if (callId != null) { + // final error = sendWorker(getErrorMessage(callId!)); - if (error.contains("dust")) { - if (hasChange) { - throw BitcoinTransactionCommitFailedDustChange(); - } else if (!isSendAll) { - throw BitcoinTransactionCommitFailedDustOutput(); - } else { - throw BitcoinTransactionCommitFailedDustOutputSendAll(); - } - } + // if (error.contains("dust")) { + // if (hasChange) { + // throw BitcoinTransactionCommitFailedDustChange(); + // } else if (!isSendAll) { + // throw BitcoinTransactionCommitFailedDustOutput(); + // } else { + // throw BitcoinTransactionCommitFailedDustOutputSendAll(); + // } + // } - if (error.contains("bad-txns-vout-negative")) { - throw BitcoinTransactionCommitFailedVoutNegative(); - } + // if (error.contains("bad-txns-vout-negative")) { + // throw BitcoinTransactionCommitFailedVoutNegative(); + // } - if (error.contains("non-BIP68-final")) { - throw BitcoinTransactionCommitFailedBIP68Final(); - } + // if (error.contains("non-BIP68-final")) { + // throw BitcoinTransactionCommitFailedBIP68Final(); + // } - if (error.contains("min fee not met")) { - throw BitcoinTransactionCommitFailedLessThanMin(); - } + // if (error.contains("min fee not met")) { + // throw BitcoinTransactionCommitFailedLessThanMin(); + // } - throw BitcoinTransactionCommitFailed(errorMessage: error); - } + // throw BitcoinTransactionCommitFailed(errorMessage: error); + // } - throw BitcoinTransactionCommitFailed(); - } + // throw BitcoinTransactionCommitFailed(); + // } } Future _ltcCommit() async { diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart deleted file mode 100644 index a7435bed1..000000000 --- a/cw_bitcoin/lib/utils.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:blockchain_utils/blockchain_utils.dart'; - -ECPrivate generateECPrivate({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPrivate(hd.childKey(Bip32KeyIndex(index)).privateKey); - -String generateP2WPKHAddress({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toP2wpkhAddress() - .toAddress(network); - -String generateP2SHAddress({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toP2wpkhInP2sh() - .toAddress(network); - -String generateP2WSHAddress({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toP2wshAddress() - .toAddress(network); - -String generateP2PKHAddress({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toP2pkhAddress() - .toAddress(network); - -String generateP2TRAddress({ - required Bip32Slip10Secp256k1 hd, - required BasedUtxoNetwork network, - required int index, -}) => - ECPublic.fromBip32(hd.childKey(Bip32KeyIndex(index)).publicKey) - .toTaprootAddress() - .toAddress(network); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 0e4921a3a..76bbdcc25 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -87,18 +87,18 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v8 - resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 - url: "https://github.com/cake-tech/bitcoin_base" + ref: cake-update-v15 + resolved-ref: "49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f" + url: "https://github.com/cake-tech/bitcoin_base.git" source: git version: "4.7.0" blockchain_utils: dependency: "direct main" description: path: "." - ref: cake-update-v2 - resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" - url: "https://github.com/cake-tech/blockchain_utils" + ref: cake-update-v3 + resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" + url: "https://github.com/cake-tech/blockchain_utils.git" source: git version: "3.3.0" bluez: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" + sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" url: "https://pub.dev" source: hosted - version: "0.3.1+4" + version: "0.3.0+2" googleapis_auth: dependency: transitive description: @@ -471,10 +471,10 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.0" http2: dependency: transitive description: @@ -849,10 +849,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -918,9 +918,9 @@ packages: dependency: "direct main" description: path: "." - ref: "sp_v4.0.0" - resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 - url: "https://github.com/cake-tech/sp_scanner" + ref: cake-update-v3 + resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b" + url: "https://github.com/cake-tech/sp_scanner.git" source: git version: "0.0.1" stack_trace: @@ -1047,18 +1047,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.3" xdg_directories: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 7a1576a98..2510d172d 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -29,14 +29,14 @@ dependencies: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + ref: cake-update-v3 cw_mweb: path: ../cw_mweb grpc: ^3.2.4 sp_scanner: git: - url: https://github.com/cake-tech/sp_scanner - ref: sp_v4.0.0 + url: https://github.com/cake-tech/sp_scanner.git + ref: cake-update-v3 bech32: git: url: https://github.com/cake-tech/bech32.git @@ -63,8 +63,12 @@ dependency_overrides: protobuf: ^3.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index d55914dcd..0019d32c6 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -6,6 +6,7 @@ import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/encryption_file_utils.dart'; @@ -37,46 +38,54 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + required bool mempoolAPIEnabled, }) : super( - mnemonic: mnemonic, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, - network: BitcoinCashNetwork.mainnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - currency: CryptoCurrency.bch, - encryptionFileUtils: encryptionFileUtils, - passphrase: passphrase) { + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, + network: BitcoinCashNetwork.mainnet, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + seedBytes: seedBytes, + currency: CryptoCurrency.bch, + encryptionFileUtils: encryptionFileUtils, + passphrase: passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: {CWBitcoinDerivationType.bip39: bitcoinCashHDWallet(seedBytes)}, + ) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), network: network, initialAddressPageType: addressPageType, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } - static Future create( - {required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required EncryptionFileUtils encryptionFileUtils, - String? passphrase, - String? addressPageType, - List? initialAddresses, - ElectrumBalance? initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex}) async { + @override + BitcoinCashNetwork get network => BitcoinCashNetwork.mainnet; + + static Future create({ + required String mnemonic, + required String password, + required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required EncryptionFileUtils encryptionFileUtils, + String? passphrase, + String? addressPageType, + List? initialAddresses, + ElectrumBalance? initialBalance, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, + required bool mempoolAPIEnabled, + }) async { return BitcoinCashWallet( mnemonic: mnemonic, password: password, @@ -90,6 +99,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -99,6 +109,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required String password, required EncryptionFileUtils encryptionFileUtils, + required bool mempoolAPIEnabled, }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); @@ -141,17 +152,21 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { return BitcoinAddressRecord( addr.address, index: addr.index, - isHidden: addr.isHidden, + isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + derivationType: CWBitcoinDerivationType.bip39, ); } catch (_) { return BitcoinAddressRecord( AddressUtils.getCashAddrFormat(addr.address), index: addr.index, - isHidden: addr.isHidden, + isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + derivationType: CWBitcoinDerivationType.bip39, ); } }).toList(), @@ -162,6 +177,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -193,13 +209,13 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override int feeRate(TransactionPriority priority) { - if (priority is BitcoinCashTransactionPriority) { + if (priority is ElectrumTransactionPriority) { switch (priority) { - case BitcoinCashTransactionPriority.slow: + case ElectrumTransactionPriority.slow: return 1; - case BitcoinCashTransactionPriority.medium: + case ElectrumTransactionPriority.medium: return 5; - case BitcoinCashTransactionPriority.fast: + case ElectrumTransactionPriority.fast: return 10; } } @@ -209,17 +225,39 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override Future signMessage(String message, {String? address = null}) async { - int? index; - try { - index = address != null - ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index - : null; - } catch (_) {} - final HD = index == null ? hd : hd.childKey(Bip32KeyIndex(index)); + Bip32Slip10Secp256k1 HD = bip32; + + final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address); + + if (record.isChange) { + HD = HD.childKey(Bip32KeyIndex(1)); + } else { + HD = HD.childKey(Bip32KeyIndex(0)); + } + + HD = HD.childKey(Bip32KeyIndex(record.index)); final priv = ECPrivate.fromWif( WifEncoder.encode(HD.privateKey.raw, netVer: network.wifNetVer), netVersion: network.wifNetVer, ); return priv.signMessage(StringUtils.encode(message)); } + + @override + Future calcFee({ + required List utxos, + required List outputs, + String? memo, + required int feeRate, + }) async => + feeRate * + ForkedTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + ); + + static Bip32Slip10Secp256k1 bitcoinCashHDWallet(List seedBytes) => + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index fe0ebc828..09b603c6e 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -1,7 +1,5 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -12,10 +10,9 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinCashWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, required super.network, required super.isHardwareWallet, + required super.hdWallets, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, @@ -23,9 +20,17 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi }) : super(walletInfo); @override - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => - generateP2PKHAddress(hd: hd, index: index, network: network); + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) => + P2pkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart index d14dc582d..4005bd5cb 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -18,11 +18,17 @@ class BitcoinCashWalletService extends WalletService< BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> { - BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); + BitcoinCashWalletService( + this.walletInfoSource, + this.unspentCoinsInfoSource, + this.isDirect, + this.mempoolAPIEnabled, + ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool isDirect; + final bool mempoolAPIEnabled; @override WalletType getType() => WalletType.bitcoinCash; @@ -42,6 +48,7 @@ class BitcoinCashWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), passphrase: credentials.passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -61,6 +68,7 @@ class BitcoinCashWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); saveBackup(name); @@ -73,6 +81,7 @@ class BitcoinCashWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; @@ -92,11 +101,13 @@ class BitcoinCashWalletService extends WalletService< final currentWalletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; final currentWallet = await BitcoinCashWalletBase.open( - password: password, - name: currentName, - walletInfo: currentWalletInfo, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect)); + password: password, + name: currentName, + walletInfo: currentWalletInfo, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, + ); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -128,12 +139,13 @@ class BitcoinCashWalletService extends WalletService< } final wallet = await BitcoinCashWalletBase.create( - password: credentials.password!, - mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo!, - unspentCoinsInfo: unspentCoinsInfoSource, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), - passphrase: credentials.passphrase + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + unspentCoinsInfo: unspentCoinsInfoSource, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + passphrase: credentials.passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart index e1fa9d6e0..f1f50855a 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -2,7 +2,7 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -31,10 +31,10 @@ class PendingBitcoinCashTransaction with PendingTransaction { String get hex => _tx.toHex(); @override - String get amountFormatted => bitcoinAmountToString(amount: amount); + String get amountFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: amount); @override - String get feeFormatted => bitcoinAmountToString(amount: fee); + String get feeFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: fee); final List _listeners; @@ -74,15 +74,16 @@ class PendingBitcoinCashTransaction with PendingTransaction { void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, - id: id, - height: 0, - amount: amount, - direction: TransactionDirection.outgoing, - date: DateTime.now(), - isPending: true, - confirmations: 0, - fee: fee, - isReplaced: false, + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo( + type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0, + fee: fee, + isReplaced: false, ); } diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 14ff407e2..e3f7eb0ab 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + ref: cake-update-v3 dev_dependencies: flutter_test: @@ -41,8 +41,12 @@ dependency_overrides: watcher: ^1.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285..6b4a5da93 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -45,7 +45,7 @@ class SyncedTipSyncStatus extends SyncedSyncStatus { final int tip; } -class SyncronizingSyncStatus extends SyncStatus { +class SynchronizingSyncStatus extends SyncStatus { @override double progress() => 0.0; } @@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; } + +Map syncStatusToJson(SyncStatus? status) { + if (status == null) { + return {}; + } + return { + 'progress': status.progress(), + 'type': status.runtimeType.toString(), + 'data': status is SyncingSyncStatus + ? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc} + : status is SyncedTipSyncStatus + ? {'tip': status.tip} + : status is FailedSyncStatus + ? {'error': status.error} + : status is StartingScanSyncStatus + ? {'beginHeight': status.beginHeight} + : null + }; +} + +SyncStatus syncStatusFromJson(Map json) { + final type = json['type'] as String; + final data = json['data'] as Map?; + + switch (type) { + case 'StartingScanSyncStatus': + return StartingScanSyncStatus(data!['beginHeight'] as int); + case 'SyncingSyncStatus': + return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double); + case 'SyncedTipSyncStatus': + return SyncedTipSyncStatus(data!['tip'] as int); + case 'FailedSyncStatus': + return FailedSyncStatus(error: data!['error'] as String?); + case 'SynchronizingSyncStatus': + return SynchronizingSyncStatus(); + case 'NotConnectedSyncStatus': + return NotConnectedSyncStatus(); + case 'AttemptingSyncStatus': + return AttemptingSyncStatus(); + case 'AttemptingScanSyncStatus': + return AttemptingScanSyncStatus(); + case 'ConnectedSyncStatus': + return ConnectedSyncStatus(); + case 'ConnectingSyncStatus': + return ConnectingSyncStatus(); + case 'UnsupportedSyncStatus': + return UnsupportedSyncStatus(); + case 'TimedOutSyncStatus': + return TimedOutSyncStatus(); + case 'LostConnectionSyncStatus': + return LostConnectionSyncStatus(); + default: + throw Exception('Unknown sync status type: $type'); + } +} diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index c173f1ddd..35282f49e 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -1,6 +1,20 @@ import 'package:cw_core/enumerable_item.dart'; -abstract class TransactionPriority extends EnumerableItem - with Serializable { - const TransactionPriority({required String title, required int raw}) : super(title: title, raw: raw); +abstract class TransactionPriority extends EnumerableItem with Serializable { + const TransactionPriority({required super.title, required super.raw}); + + String get units => ''; + String toString() { + return title; + } +} + +abstract class TransactionPriorities { + const TransactionPriorities(); + int operator [](TransactionPriority type); + String labelWithRate(TransactionPriority type); + Map toJson(); + factory TransactionPriorities.fromJson(Map json) { + throw UnimplementedError(); + } } diff --git a/cw_core/lib/wallet_credentials.dart b/cw_core/lib/wallet_credentials.dart index 55c24bf37..ae69fadac 100644 --- a/cw_core/lib/wallet_credentials.dart +++ b/cw_core/lib/wallet_credentials.dart @@ -9,6 +9,7 @@ abstract class WalletCredentials { this.password, this.passphrase, this.derivationInfo, + this.derivations, this.hardwareWalletType, this.parentAddress, }) { @@ -25,5 +26,6 @@ abstract class WalletCredentials { String? passphrase; WalletInfo? walletInfo; DerivationInfo? derivationInfo; + List? derivations; HardwareWalletType? hardwareWalletType; } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index bd035e30a..ab674d9b4 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -79,6 +79,7 @@ class WalletInfo extends HiveObject { this.yatLastUsedAddressRaw, this.showIntroCakePayCard, this.derivationInfo, + this.derivations, this.hardwareWalletType, this.parentAddress, ) : _yatLastUsedAddressController = StreamController.broadcast(); @@ -97,6 +98,7 @@ class WalletInfo extends HiveObject { String yatEid = '', String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, + List? derivations, HardwareWalletType? hardwareWalletType, String? parentAddress, }) { @@ -114,6 +116,7 @@ class WalletInfo extends HiveObject { yatLastUsedAddressRaw, showIntroCakePayCard, derivationInfo, + derivations, hardwareWalletType, parentAddress, ); @@ -189,15 +192,15 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; - + @HiveField(23) List? hiddenAddresses; @HiveField(24) List? manualAddresses; - - + @HiveField(25) + List? derivations; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; diff --git a/cw_core/lib/wallet_keys_file.dart b/cw_core/lib/wallet_keys_file.dart index 638cdc39d..ff680f9e1 100644 --- a/cw_core/lib/wallet_keys_file.dart +++ b/cw_core/lib/wallet_keys_file.dart @@ -27,7 +27,10 @@ mixin WalletKeysFile address; + // @override @observable Account? account; diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux new file mode 120000 index 000000000..5dc8fb651 --- /dev/null +++ b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -0,0 +1 @@ +/home/rafael/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index e69fd7ca0..9da2217bb 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -17,12 +17,12 @@ dependencies: path: ../cw_evm on_chain: git: - url: https://github.com/cake-tech/On_chain - ref: cake-update-v2 + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v3 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + ref: cake-update-v3 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -34,6 +34,13 @@ dev_dependencies: build_runner: ^2.3.3 mobx_codegen: ^2.1.1 hive_generator: ^1.1.3 + +dependency_overrides: + blockchain_utils: + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 + flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 60364c289..d4bc6799b 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -5,17 +5,15 @@ class CWBitcoin extends Bitcoin { required String name, required String mnemonic, required String password, - required DerivationType derivationType, - required String derivationPath, + required List? derivations, String? passphrase, }) => BitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: mnemonic, password: password, - derivationType: derivationType, - derivationPath: derivationPath, passphrase: passphrase, + derivations: derivations, ); @override @@ -54,7 +52,7 @@ class CWBitcoin extends Bitcoin { name: name, hwAccountData: accountData, walletInfo: walletInfo); @override - TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; + TransactionPriority getMediumTransactionPriority() => ElectrumTransactionPriority.medium; @override List getWordList() => wordlist; @@ -72,18 +70,18 @@ class CWBitcoin extends Bitcoin { } @override - List getTransactionPriorities() => BitcoinTransactionPriority.all; + List getTransactionPriorities() => ElectrumTransactionPriority.all; @override - List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; + List getLitecoinTransactionPriorities() => ElectrumTransactionPriority.all; @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => - BitcoinTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override TransactionPriority deserializeLitecoinTransactionPriority(int raw) => - LitecoinTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override int getFeeRate(Object wallet, TransactionPriority priority) { @@ -113,7 +111,7 @@ class CWBitcoin extends Bitcoin { UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { final bitcoinFeeRate = - priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; + priority == ElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( outputs .map((out) => OutputInfo( @@ -127,7 +125,7 @@ class CWBitcoin extends Bitcoin { formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo)) .toList(), - priority: priority as BitcoinTransactionPriority, + priority: priority as ElectrumTransactionPriority, feeRate: bitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -144,7 +142,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -167,12 +165,7 @@ class CWBitcoin extends Bitcoin { final p2shAddr = sk.getPublic().toP2pkhInP2sh(); final estimatedTx = await electrumWallet.estimateSendAllTx( [BitcoinOutput(address: p2shAddr, value: BigInt.zero)], - getFeeRate( - wallet, - wallet.type == WalletType.litecoin - ? priority as LitecoinTransactionPriority - : priority as BitcoinTransactionPriority, - ), + getFeeRate(wallet, priority), ); return estimatedTx.amount; @@ -189,19 +182,20 @@ class CWBitcoin extends Bitcoin { @override String formatterBitcoinAmountToString({required int amount}) => - bitcoinAmountToString(amount: amount); + BitcoinAmountUtils.bitcoinAmountToString(amount: amount); @override double formatterBitcoinAmountToDouble({required int amount}) => - bitcoinAmountToDouble(amount: amount); + BitcoinAmountUtils.bitcoinAmountToDouble(amount: amount); @override - int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount); + int formatterStringDoubleToBitcoinAmount(String amount) => + BitcoinAmountUtils.stringDoubleToBitcoinAmount(amount); @override String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}) => - (priority as BitcoinTransactionPriority).labelWithRate(rate, customRate); + (priority as ElectrumTransactionPriority).labelWithRate(rate, customRate); @override List getUnspents(Object wallet, @@ -224,30 +218,52 @@ class CWBitcoin extends Bitcoin { await bitcoinWallet.updateAllUnspents(); } - WalletService createBitcoinWalletService(Box walletInfoSource, - Box unspentCoinSource, bool alwaysScan, bool isDirect) { - return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); + WalletService createBitcoinWalletService( + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ) { + return BitcoinWalletService( + walletInfoSource, + unspentCoinSource, + alwaysScan, + isDirect, + mempoolAPIEnabled, + ); } - WalletService createLitecoinWalletService(Box walletInfoSource, - Box unspentCoinSource, bool alwaysScan, bool isDirect) { - return LitecoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect); + WalletService createLitecoinWalletService( + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ) { + return LitecoinWalletService( + walletInfoSource, + unspentCoinSource, + alwaysScan, + isDirect, + mempoolAPIEnabled, + ); } @override - TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium; + TransactionPriority getBitcoinTransactionPriorityMedium() => ElectrumTransactionPriority.fast; @override - TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom; + TransactionPriority getBitcoinTransactionPriorityCustom() => ElectrumTransactionPriority.custom; @override - TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; + TransactionPriority getLitecoinTransactionPriorityMedium() => ElectrumTransactionPriority.medium; @override - TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow; + TransactionPriority getBitcoinTransactionPrioritySlow() => ElectrumTransactionPriority.medium; @override - TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; + TransactionPriority getLitecoinTransactionPrioritySlow() => ElectrumTransactionPriority.slow; @override Future setAddressType(Object wallet, dynamic option) async { @@ -320,20 +336,12 @@ class CWBitcoin extends Bitcoin { } @override - Future> getDerivationsFromMnemonic({ + Future> getDerivationsFromMnemonic({ required String mnemonic, required Node node, String? passphrase, }) async { - List list = []; - - List types = await compareDerivationMethods(mnemonic: mnemonic, node: node); - if (types.length == 1 && types.first == DerivationType.electrum) { - return [getElectrumDerivations()[DerivationType.electrum]!.first]; - } - - final electrumClient = ElectrumClient(); - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + List list = []; late BasedUtxoNetwork network; switch (node.type) { @@ -346,72 +354,34 @@ class CWBitcoin extends Bitcoin { break; } - for (DerivationType dType in electrum_derivations.keys) { - late Uint8List seedBytes; - if (dType == DerivationType.electrum) { - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); - } else if (dType == DerivationType.bip39) { - seedBytes = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? ''); - } + var electrumSeedBytes; + try { + electrumSeedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + } catch (e) { + print("electrum_v2 seed error: $e"); - for (DerivationInfo dInfo in electrum_derivations[dType]!) { + if (passphrase != null && passphrase.isEmpty) { try { - DerivationInfo dInfoCopy = DerivationInfo( - derivationType: dInfo.derivationType, - derivationPath: dInfo.derivationPath, - description: dInfo.description, - scriptType: dInfo.scriptType, - ); - - String balancePath = dInfoCopy.derivationPath!; - int derivationDepth = _countCharOccurrences(balancePath, '/'); - - // for BIP44 - if (derivationDepth == 3 || derivationDepth == 1) { - // we add "/0" so that we generate account 0 - balancePath += "/0"; - } - - final hd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(balancePath) - as Bip32Slip10Secp256k1; - - // derive address at index 0: - String? address; - switch (dInfoCopy.scriptType) { - case "p2wpkh": - address = generateP2WPKHAddress(hd: hd, network: network, index: 0); - break; - case "p2pkh": - address = generateP2PKHAddress(hd: hd, network: network, index: 0); - break; - case "p2wpkh-p2sh": - address = generateP2SHAddress(hd: hd, network: network, index: 0); - break; - case "p2tr": - address = generateP2TRAddress(hd: hd, network: network, index: 0); - break; - default: - continue; - } - - final sh = BitcoinAddressUtils.scriptHash(address, network: network); - final history = await electrumClient.getHistory(sh); - - final balance = await electrumClient.getBalance(sh); - dInfoCopy.balance = balance.entries.firstOrNull?.value.toString() ?? "0"; - dInfoCopy.address = address; - dInfoCopy.transactionsCount = history.length; - - list.add(dInfoCopy); - } catch (e, s) { - print("derivationInfoError: $e"); - print("derivationInfoStack: $s"); + // TODO: language pick + electrumSeedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + } catch (e) { + print("electrum_v1 seed error: $e"); } } } - // sort the list such that derivations with the most transactions are first: - list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + if (electrumSeedBytes != null) { + list.add(BitcoinDerivationInfos.ELECTRUM); + } + + var bip39SeedBytes; + try { + bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } catch (_) {} + + if (bip39SeedBytes != null) { + list.add(BitcoinDerivationInfos.BIP84); + } return list; } @@ -443,7 +413,7 @@ class CWBitcoin extends Bitcoin { @override int getTransactionVSize(Object wallet, String transactionHex) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.transactionVSize(transactionHex); + return BtcTransaction.fromRaw(transactionHex).getVSize(); } @override @@ -458,7 +428,7 @@ class CWBitcoin extends Bitcoin { {int? size}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.feeAmountForPriority( - priority as BitcoinTransactionPriority, inputsCount, outputsCount); + priority as ElectrumTransactionPriority, inputsCount, outputsCount); } @override @@ -482,8 +452,13 @@ class CWBitcoin extends Bitcoin { @override int getMaxCustomFeeRate(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 10).round(); + final electrumWallet = wallet as ElectrumWallet; + final feeRates = electrumWallet.feeRates; + final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities + ? ElectrumTransactionPriority.fast + : BitcoinTransactionPriority.priority; + + return (electrumWallet.feeRate(maxFee) * 10).round(); } @override @@ -526,7 +501,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -541,7 +516,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -564,7 +539,7 @@ class CWBitcoin extends Bitcoin { @override Future setScanningActive(Object wallet, bool active) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; bitcoinWallet.setSilentPaymentsScanning(active); } @@ -574,10 +549,16 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.isTestnet; } + @override + Future registerSilentPaymentsKey(Object wallet, bool active) async { + final bitcoinWallet = wallet as BitcoinWallet; + return await bitcoinWallet.registerSilentPaymentsKey(); + } + @override Future checkIfMempoolAPIIsEnabled(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; - return await bitcoinWallet.checkIfMempoolAPIIsEnabled(); + return await bitcoinWallet.mempoolAPIEnabled; } @override @@ -595,13 +576,13 @@ class CWBitcoin extends Bitcoin { @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan); } @override Future getNodeIsElectrsSPEnabled(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return bitcoinWallet.getNodeSupportsSilentPayments(); } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index b74448703..0a9131349 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -6,8 +6,17 @@ class CWBitcoinCash extends BitcoinCash { @override WalletService createBitcoinCashWalletService( - Box walletInfoSource, Box unspentCoinSource, bool isDirect) { - return BitcoinCashWalletService(walletInfoSource, unspentCoinSource, isDirect); + Box walletInfoSource, + Box unspentCoinSource, + bool isDirect, + bool mempoolAPIEnabled, + ) { + return BitcoinCashWalletService( + walletInfoSource, + unspentCoinSource, + isDirect, + mempoolAPIEnabled, + ); } @override @@ -30,21 +39,23 @@ class CWBitcoinCash extends BitcoinCash { @override WalletCredentials createBitcoinCashRestoreWalletFromSeedCredentials( - {required String name, required String mnemonic, required String password, String? passphrase}) => + {required String name, + required String mnemonic, + required String password, + String? passphrase}) => BitcoinCashRestoreWalletFromSeedCredentials( name: name, mnemonic: mnemonic, password: password, passphrase: passphrase); @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => - BitcoinCashTransactionPriority.deserialize(raw: raw); + ElectrumTransactionPriority.deserialize(raw: raw); @override - TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => ElectrumTransactionPriority.medium; @override - List getTransactionPriorities() => BitcoinCashTransactionPriority.all; + List getTransactionPriorities() => ElectrumTransactionPriority.all; @override - TransactionPriority getBitcoinCashTransactionPrioritySlow() => - BitcoinCashTransactionPriority.slow; + TransactionPriority getBitcoinCashTransactionPrioritySlow() => ElectrumTransactionPriority.slow; } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 46dd62c3a..86a8943e4 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -51,7 +51,7 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_timed_out; } - if (syncStatus is SyncronizingSyncStatus) { + if (syncStatus is SynchronizingSyncStatus) { return S.current.sync_status_syncronizing; } diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index adb516817..ba4785643 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -267,6 +267,8 @@ abstract class Web3WalletServiceBase with Store { final keyForWallet = getKeyForStoringTopicsForWallet(); + if (keyForWallet.isEmpty) return; + final currentTopicsForWallet = getPairingTopicsForWallet(keyForWallet); final filteredPairings = @@ -360,6 +362,10 @@ abstract class Web3WalletServiceBase with Store { String getKeyForStoringTopicsForWallet() { List chainKeys = walletKeyService.getKeysForChain(appStore.wallet!); + if (chainKeys.isEmpty) { + return ''; + } + final keyForPairingTopic = PreferencesKey.walletConnectPairingTopicsListForWallet(chainKeys.first.publicKey); @@ -386,6 +392,8 @@ abstract class Web3WalletServiceBase with Store { // Get key specific to the current wallet final key = getKeyForStoringTopicsForWallet(); + if (key.isEmpty) return; + // Get all pairing topics attached to this key final pairingTopicsForWallet = getPairingTopicsForWallet(key); diff --git a/lib/di.dart b/lib/di.dart index 13ffd839e..99c3c56b6 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -367,14 +367,14 @@ Future setup({ (WalletType type) => getIt.get(param1: type))); getIt.registerFactoryParam( - (newWalletArgs, _) => WalletNewVM( - getIt.get(), - getIt.get(param1:newWalletArgs.type), - _walletInfoSource, - getIt.get(param1: newWalletArgs.type), - getIt.get(), - newWalletArguments: newWalletArgs,)); - + (newWalletArgs, _) => WalletNewVM( + getIt.get(), + getIt.get(param1: newWalletArgs.type), + _walletInfoSource, + getIt.get(param1: newWalletArgs.type), + getIt.get(), + newWalletArguments: newWalletArgs, + )); getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); @@ -397,62 +397,52 @@ Future setup({ ); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_loadable'); getIt.registerFactory( - () => getIt.get( - param1: WalletUnlockArguments( - callback: (bool successful, _) { - if (successful) { - final authStore = getIt.get(); - authStore.allowed(); - }}), - param2: false, - instanceName: 'wallet_unlock_loadable'), - instanceName: 'wallet_password_login'); + () => getIt.get( + param1: WalletUnlockArguments(callback: (bool successful, _) { + if (successful) { + final authStore = getIt.get(); + authStore.allowed(); + } + }), + param2: false, + instanceName: 'wallet_unlock_loadable'), + instanceName: 'wallet_password_login'); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_verifiable'); getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockLoadableViewModel( - getIt.get(), - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockLoadableViewModel(getIt.get(), getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + getIt.registerFactoryParam( + (args, _) { + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockVerifiableViewModel( - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockVerifiableViewModel(getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); getIt.registerFactoryParam((WalletType type, _) => @@ -785,7 +775,6 @@ Future setup({ ); getIt.registerFactoryParam((arguments, _) { - return WalletEditPage( pageArguments: WalletEditPageArguments( walletEditViewModel: getIt.get(param1: arguments.walletListViewModel), @@ -884,8 +873,9 @@ Future setup({ getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); getIt.registerFactory(() { - return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!, - getIt.get());}); + return OtherSettingsViewModel( + getIt.get(), getIt.get().wallet!, getIt.get()); + }); getIt.registerFactory(() { return SecuritySettingsViewModel(getIt.get()); @@ -893,7 +883,8 @@ Future setup({ getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); + getIt.registerFactory( + () => SeedSettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1037,6 +1028,7 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().silentPaymentsAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, ); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService( @@ -1044,16 +1036,22 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().mwebAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashWalletService(_walletInfoSource, - _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return bitcoinCash!.createBitcoinCashWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, + ); case WalletType.nano: case WalletType.banano: - return nano!.createNanoWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return nano!.createNanoWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.polygon: return polygon!.createPolygonWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1061,7 +1059,8 @@ Future setup({ return solana!.createSolanaWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.tron: - return tron!.createTronWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return tron!.createTronWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: @@ -1100,40 +1099,36 @@ Future setup({ param1: derivations, ))); - getIt.registerFactoryParam, void>( - (params, _) { - final transactionInfo = params[0] as TransactionInfo; - final canReplaceByFee = params[1] as bool? ?? false; - final wallet = getIt.get().wallet!; + getIt.registerFactoryParam, void>((params, _) { + final transactionInfo = params[0] as TransactionInfo; + final canReplaceByFee = params[1] as bool? ?? false; + final wallet = getIt.get().wallet!; - return TransactionDetailsViewModel( - transactionInfo: transactionInfo, - transactionDescriptionBox: _transactionDescriptionBox, - wallet: wallet, - settingsStore: getIt.get(), - sendViewModel: getIt.get(), - canReplaceByFee: canReplaceByFee, - ); - } - ); + return TransactionDetailsViewModel( + transactionInfo: transactionInfo, + transactionDescriptionBox: _transactionDescriptionBox, + wallet: wallet, + settingsStore: getIt.get(), + sendViewModel: getIt.get(), + canReplaceByFee: canReplaceByFee, + ); + }); getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => TransactionDetailsPage( - transactionDetailsViewModel: getIt.get( - param1: [transactionInfo, false]))); + (TransactionInfo transactionInfo, _) => TransactionDetailsPage( + transactionDetailsViewModel: + getIt.get(param1: [transactionInfo, false]))); - getIt.registerFactoryParam, void>( - (params, _) { - final transactionInfo = params[0] as TransactionInfo; - final txHex = params[1] as String; - return RBFDetailsPage( - transactionDetailsViewModel: getIt.get( - param1: [transactionInfo, true], - ), - rawTransaction: txHex, - ); - } - ); + getIt.registerFactoryParam, void>((params, _) { + final transactionInfo = params[0] as TransactionInfo; + final txHex = params[1] as String; + return RBFDetailsPage( + transactionDetailsViewModel: getIt.get( + param1: [transactionInfo, true], + ), + rawTransaction: txHex, + ); + }); getIt.registerFactoryParam( (newWalletTypeArguments, _) { @@ -1155,8 +1150,7 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, - _transactionDescriptionBox, - getIt.get(), getIt.get())); + _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( getIt.get(), getIt.get(), getIt.get())); diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index a8e2829d8..1a031ee82 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -114,7 +114,7 @@ class FiatCurrency extends EnumerableItem with Serializable impl FiatCurrency.tur.raw: FiatCurrency.tur, }; - static FiatCurrency deserialize({required String raw}) => _all[raw]!; + static FiatCurrency deserialize({required String raw}) => _all[raw] ?? FiatCurrency.usd; @override bool operator ==(Object other) => other is FiatCurrency && other.raw == raw; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 4fbe358e5..b1aa33a10 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -48,6 +48,7 @@ class PreferencesKey { static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; + static const silentPaymentsKeyRegistered = 'silentPaymentsKeyRegistered'; static const mwebCardDisplay = 'mwebCardDisplay'; static const mwebEnabled = 'mwebEnabled'; static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; diff --git a/lib/main.dart b/lib/main.dart index 29b216b22..8c30dcf3b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,6 @@ import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/locales/locale.dart'; -import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/bootstrap.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/routes.dart'; diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index f8cbe9120..0fb629685 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_ import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; @@ -100,6 +99,11 @@ class _DesktopWalletSelectionDropDownState extends State element.isSelected, + orElse: () => dropDownItems.first, + ); + return DropdownButton( items: dropDownItems .map( @@ -115,7 +119,7 @@ class _DesktopWalletSelectionDropDownState extends State()!.backgroundColor, style: TextStyle(color: themeData.extension()!.titleColor), selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(), - value: dropDownItems.firstWhere((element) => element.isSelected), + value: selectedItem, underline: const SizedBox(), focusColor: Colors.transparent, borderRadius: BorderRadius.circular(15.0), diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 10f9aef43..c81bca484 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -72,24 +72,52 @@ class AddressPage extends BasePage { bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; - return MergeSemantics( - child: SizedBox( - height: isMobileView ? 37 : 45, - width: isMobileView ? 37 : 45, - child: ButtonTheme( - minWidth: double.minPositive, - child: Semantics( - label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, - child: TextButton( - style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: !isMobileView ? _closeButton : _backButton, + ), ), - onPressed: () => onClose(context), - child: !isMobileView ? _closeButton : _backButton, ), ), ), - ), + MergeSemantics( + child: SizedBox( + height: isMobileView ? 37 : 45, + width: isMobileView ? 37 : 45, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), + ), + onPressed: () => onClose(context), + child: Icon( + Icons.more_vert, + color: titleColor(context), + size: 16, + ), + ), + ), + ), + ), + ), + ], ); } @@ -150,13 +178,13 @@ class AddressPage extends BasePage { Expanded( child: Observer( builder: (_) => QRWidget( - formKey: _formKey, - addressListViewModel: addressListViewModel, - amountTextFieldFocusNode: _cryptoAmountFocus, - amountController: _amountController, - isLight: dashboardViewModel.settingsStore.currentTheme.type == - ThemeType.light, - ))), + formKey: _formKey, + addressListViewModel: addressListViewModel, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: dashboardViewModel.settingsStore.currentTheme.type == + ThemeType.light, + ))), SizedBox(height: 16), Observer(builder: (_) { if (addressListViewModel.hasAddressList) { diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 7e3c2b555..bae9a972a 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -1,27 +1,13 @@ -import 'package:cake_wallet/src/screens/nano_accounts/nano_account_list_page.dart'; import 'package:cake_wallet/src/screens/receive/widgets/address_list.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; -import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/share_util.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; -import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart'; -import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/src/screens/receive/widgets/qr_widget.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; @@ -116,13 +102,13 @@ class ReceivePage extends BasePage { Padding( padding: EdgeInsets.fromLTRB(24, 50, 24, 24), child: QRWidget( - addressListViewModel: addressListViewModel, - formKey: _formKey, - heroTag: _heroTag, - amountTextFieldFocusNode: _cryptoAmountFocus, - amountController: _amountController, - isLight: currentTheme.type == ThemeType.light, - ), + addressListViewModel: addressListViewModel, + formKey: _formKey, + heroTag: _heroTag, + amountTextFieldFocusNode: _cryptoAmountFocus, + amountController: _amountController, + isLight: currentTheme.type == ThemeType.light, + ), ), AddressList(addressListViewModel: addressListViewModel), Padding( diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 9f15018d0..0d5805e52 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,6 +1,3 @@ - -import 'dart:math'; - import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -37,7 +34,6 @@ class AddressList extends StatefulWidget { } class _AddressListState extends State { - bool showHiddenAddresses = false; void _toggleHiddenAddresses() { @@ -131,9 +127,10 @@ class _AddressListState extends State { showTrailingButton: widget.addressListViewModel.showAddManualAddresses, showSearchButton: true, onSearchCallback: updateItems, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { - updateItems(); // refresh the new address - }), + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), trailingIcon: Icon( Icons.add, size: 20, @@ -148,7 +145,8 @@ class _AddressListState extends State { cell = Container(); } else { cell = Observer(builder: (_) { - final isCurrent = item.address == widget.addressListViewModel.address.address && editable; + final isCurrent = + item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -156,17 +154,17 @@ class _AddressListState extends State { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; - return AddressCell.fromItem( item, isCurrent: isCurrent, hasBalance: widget.addressListViewModel.isBalanceAvailable, hasReceived: widget.addressListViewModel.isReceivedAvailable, - // hasReceived: - backgroundColor: (kDebugMode && item.isHidden) ? - Theme.of(context).colorScheme.error : - (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : - backgroundColor, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) + ? Theme.of(context).colorScheme.error + : (kDebugMode && item.isManual) + ? Theme.of(context).colorScheme.error.withBlue(255) + : backgroundColor, textColor: textColor, onTap: (_) { if (widget.onSelect != null) { @@ -176,9 +174,11 @@ class _AddressListState extends State { widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { - updateItems(); // refresh the new address - }) + ? () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item) + .then((value) { + updateItems(); // refresh the new address + }) : null, isHidden: item.isHidden, onHide: () => _hideAddress(item), @@ -190,8 +190,8 @@ class _AddressListState extends State { return index != 0 ? cell : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), + borderRadius: + BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)), child: cell, ); }, @@ -202,5 +202,4 @@ class _AddressListState extends State { await widget.addressListViewModel.toggleHideAddress(item); updateItems(); } - } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 6215e26c3..9a9fa1152 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -54,8 +54,10 @@ class WalletRestorePage extends BasePage { _validateOnChange(isPolyseed: isPolyseed); }, displayWalletPassword: walletRestoreViewModel.hasWalletPassword, - onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, - onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword)); + onPasswordChange: (String password) => + walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => + walletRestoreViewModel.repeatedWalletPassword = repeatedPassword)); break; case WalletRestoreMode.keys: _pages.add(WalletRestoreFromKeysFrom( @@ -69,8 +71,10 @@ class WalletRestorePage extends BasePage { }, displayPrivateKeyField: walletRestoreViewModel.hasRestoreFromPrivateKey, displayWalletPassword: walletRestoreViewModel.hasWalletPassword, - onPasswordChange: (String password) => walletRestoreViewModel.walletPassword = password, - onRepeatedPasswordChange: (String repeatedPassword) => walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, + onPasswordChange: (String password) => + walletRestoreViewModel.walletPassword = password, + onRepeatedPasswordChange: (String repeatedPassword) => + walletRestoreViewModel.repeatedWalletPassword = repeatedPassword, onHeightOrDateEntered: (value) => walletRestoreViewModel.isButtonEnabled = value)); break; default: @@ -105,6 +109,7 @@ class WalletRestorePage extends BasePage { // DerivationType derivationType = DerivationType.unknown; // String? derivationPath = null; DerivationInfo? derivationInfo; + List? derivations; @override Function(BuildContext)? get pushToNextWidget => (context) { @@ -342,6 +347,7 @@ class WalletRestorePage extends BasePage { } credentials['derivationInfo'] = this.derivationInfo; + credentials['derivations'] = this.derivations; credentials['walletType'] = walletRestoreViewModel.type; return credentials; } @@ -379,39 +385,43 @@ class WalletRestorePage extends BasePage { walletRestoreViewModel.state = IsExecutingState(); - DerivationInfo? dInfo; - // get info about the different derivations: List derivations = await walletRestoreViewModel.getDerivationInfo(_credentials()); - int derivationsWithHistory = 0; - int derivationWithHistoryIndex = 0; - for (int i = 0; i < derivations.length; i++) { - if (derivations[i].transactionsCount > 0) { - derivationsWithHistory++; - derivationWithHistoryIndex = i; + if (walletRestoreViewModel.type == WalletType.nano) { + DerivationInfo? dInfo; + + int derivationsWithHistory = 0; + int derivationWithHistoryIndex = 0; + for (int i = 0; i < derivations.length; i++) { + if (derivations[i].transactionsCount > 0) { + derivationsWithHistory++; + derivationWithHistoryIndex = i; + } } - } - if (derivationsWithHistory > 1) { - dInfo = await Navigator.of(context).pushNamed( - Routes.restoreWalletChooseDerivation, - arguments: derivations, - ) as DerivationInfo?; - } else if (derivationsWithHistory == 1) { - dInfo = derivations[derivationWithHistoryIndex]; - } else if (derivations.length == 1) { - // we only return 1 derivation if we're pretty sure we know which one to use: - dInfo = derivations.first; + if (derivationsWithHistory > 1) { + dInfo = await Navigator.of(context).pushNamed( + Routes.restoreWalletChooseDerivation, + arguments: derivations, + ) as DerivationInfo?; + } else if (derivationsWithHistory == 1) { + dInfo = derivations[derivationWithHistoryIndex]; + } else if (derivations.length == 1) { + // we only return 1 derivation if we're pretty sure we know which one to use: + dInfo = derivations.first; + } else { + // if we have multiple possible derivations, and none (or multiple) have histories + // we just default to the most common one: + dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); + } + + this.derivationInfo = dInfo; } else { - // if we have multiple possible derivations, and none (or multiple) have histories - // we just default to the most common one: - dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); + this.derivations = derivations; } - this.derivationInfo = dInfo; - await walletRestoreViewModel.create(options: _credentials()); seedSettingsViewModel.setPassphrase(null); } catch (e) { diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index bc0ecece1..ebf952a56 100644 --- a/lib/src/screens/settings/silent_payments_settings.dart +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -37,6 +37,13 @@ class SilentPaymentsSettingsPage extends BasePage { _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value); }, ), + SettingsSwitcherCell( + title: S.current.silent_payments_register_key, + value: _silentPaymentsSettingsViewModel.silentPaymentsKeyRegistered, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.registerSilentPaymentsKey(value); + }, + ), SettingsCellWithArrow( title: S.current.silent_payments_scanning, handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.rescan), diff --git a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart index 30b6af7e0..42bef5135 100644 --- a/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart +++ b/lib/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart @@ -52,7 +52,9 @@ class BottomSheetListenerState extends State { ); }, ); - item.completer.complete(value); + if (!item.completer.isCompleted) { + item.completer.complete(value); + } widget.bottomSheetService.resetCurrentSheet(); } } diff --git a/lib/src/widgets/cake_image_widget.dart b/lib/src/widgets/cake_image_widget.dart index ad02c48dd..2038030c0 100644 --- a/lib/src/widgets/cake_image_widget.dart +++ b/lib/src/widgets/cake_image_widget.dart @@ -25,6 +25,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, + errorBuilder: (_, __, ___) => Icon(Icons.error), ); } @@ -33,6 +34,7 @@ class CakeImageWidget extends StatelessWidget { imageUrl!, height: height, width: width, + placeholderBuilder: (_) => Icon(Icons.error), ); } diff --git a/lib/src/widgets/services_updates_widget.dart b/lib/src/widgets/services_updates_widget.dart index 1babd23d0..1787b7118 100644 --- a/lib/src/widgets/services_updates_widget.dart +++ b/lib/src/widgets/services_updates_widget.dart @@ -46,6 +46,7 @@ class _ServicesUpdatesWidgetState extends State { "assets/images/notification_icon.svg", color: Theme.of(context).extension()!.pageTitleTextColor, width: 30, + placeholderBuilder: (_) => Icon(Icons.error), ), ); } @@ -136,6 +137,7 @@ class _ServicesUpdatesWidgetState extends State { "assets/images/notification_icon.svg", color: Theme.of(context).extension()!.pageTitleTextColor, width: 30, + placeholderBuilder: (_) => Icon(Icons.error), ), if (state.hasData && state.data!.hasUpdates && !wasOpened) Container( diff --git a/lib/store/dashboard/transaction_filter_store.dart b/lib/store/dashboard/transaction_filter_store.dart index fb9ee14cd..f28f7e915 100644 --- a/lib/store/dashboard/transaction_filter_store.dart +++ b/lib/store/dashboard/transaction_filter_store.dart @@ -91,8 +91,9 @@ abstract class TransactionFilterStoreBase with Store { (displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) || (displayIncoming && item.transaction.direction == TransactionDirection.incoming && - !bitcoin!.txIsReceivedSilentPayment(item.transaction)) || - (displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction)); + !(bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false)) || + (displaySilentPayments && + (bitcoin?.txIsReceivedSilentPayment(item.transaction) ?? false)); } else if (item is AnonpayTransactionListItem) { allowed = displayIncoming; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 9f03c95c3..cd39318f4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -114,6 +114,7 @@ abstract class SettingsStoreBase with Store { required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, + required this.silentPaymentsKeyRegistered, required this.mwebAlwaysScan, required this.mwebCardDisplay, required this.mwebEnabled, @@ -344,8 +345,8 @@ abstract class SettingsStoreBase with Store { reaction( (_) => bitcoinSeedType, - (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( - PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + (BitcoinSeedType bitcoinSeedType) => + sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); reaction( (_) => nanoSeedType, @@ -428,8 +429,10 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); - reaction((_) => useMempoolFeeAPI, - (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction( + (_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => + _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -559,6 +562,11 @@ abstract class SettingsStoreBase with Store { (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + reaction( + (_) => silentPaymentsKeyRegistered, + (bool silentPaymentsKeyRegistered) => _sharedPreferences.setBool( + PreferencesKey.silentPaymentsKeyRegistered, silentPaymentsKeyRegistered)); + reaction( (_) => mwebAlwaysScan, (bool mwebAlwaysScan) => @@ -790,6 +798,9 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool silentPaymentsKeyRegistered; + @observable bool mwebAlwaysScan; @@ -959,6 +970,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; final silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + final silentPaymentsKeyRegistered = + sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false; final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; @@ -1230,6 +1243,7 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + silentPaymentsKeyRegistered: silentPaymentsKeyRegistered, mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, @@ -1396,6 +1410,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + silentPaymentsKeyRegistered = + sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false; mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; @@ -1658,7 +1674,8 @@ abstract class SettingsStoreBase with Store { deviceName = windowsInfo.productName; } catch (e) { print(e); - print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); + print( + 'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); deviceName = "Windows Device"; } } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 91797c45e..41ae91d41 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -62,6 +62,14 @@ class ExceptionHandler { await _addDeviceInfo(_file!); + // Check if a mail client is available + final bool canSend = await FlutterMailer.canSendMail(); + + if (Platform.isIOS && !canSend) { + debugPrint('Mail app is not available'); + return; + } + final MailOptions mailOptions = MailOptions( subject: 'Mobile App Issue', recipients: ['support@cakewallet.com'], diff --git a/lib/utils/image_utill.dart b/lib/utils/image_utill.dart index a138df23a..ef3775e4c 100644 --- a/lib/utils/image_utill.dart +++ b/lib/utils/image_utill.dart @@ -53,8 +53,18 @@ class ImageUtil { ); } else { return isSvg - ? SvgPicture.asset(imagePath, height: _height, width: _width) - : Image.asset(imagePath, height: _height, width: _width); + ? SvgPicture.asset( + imagePath, + height: _height, + width: _width, + placeholderBuilder: (_) => Icon(Icons.error), + ) + : Image.asset( + imagePath, + height: _height, + width: _width, + errorBuilder: (_, __, ___) => Icon(Icons.error), + ); } } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 53a8277ab..423babefc 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -90,11 +90,12 @@ abstract class DashboardViewModelBase with Store { value: () => transactionFilterStore.displayOutgoing, caption: S.current.outgoing, onChanged: transactionFilterStore.toggleOutgoing), - FilterItem( - value: () => transactionFilterStore.displaySilentPayments, - caption: S.current.silent_payments, - onChanged: transactionFilterStore.toggleSilentPayments, - ), + if (appStore.wallet!.type == WalletType.bitcoin) + FilterItem( + value: () => transactionFilterStore.displaySilentPayments, + caption: S.current.silent_payments, + onChanged: transactionFilterStore.toggleSilentPayments, + ), // FilterItem( // value: () => false, // caption: S.current.transactions_by_date, @@ -435,7 +436,10 @@ abstract class DashboardViewModelBase with Store { } @computed - bool get hasMweb => wallet.type == WalletType.litecoin && (Platform.isIOS || Platform.isAndroid) && !wallet.isHardwareWallet; + bool get hasMweb => + wallet.type == WalletType.litecoin && + (Platform.isIOS || Platform.isAndroid) && + !wallet.isHardwareWallet; @computed bool get showMwebCard => hasMweb && settingsStore.mwebCardDisplay && !mwebEnabled; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index cbdad85b8..2849b77ec 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -38,7 +38,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store wif = '', address = '', super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, - type: type, isRecovery: true); + type: type, isRecovery: true); @observable int height; @@ -113,21 +113,12 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store ); case WalletType.bitcoin: case WalletType.litecoin: - - final derivationInfoList = await getDerivationInfoFromQRCredentials(restoreWallet); - DerivationInfo derivationInfo; - if (derivationInfoList.isEmpty) { - derivationInfo = getDefaultCreateDerivation()!; - } else { - derivationInfo = derivationInfoList.first; - } return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, passphrase: restoreWallet.passphrase, - derivationType: derivationInfo.derivationType!, - derivationPath: derivationInfo.derivationPath!, + derivations: [], ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( @@ -144,8 +135,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store passphrase: restoreWallet.passphrase, ); case WalletType.nano: - final derivationInfo = - (await getDerivationInfoFromQRCredentials(restoreWallet)).first; + final derivationInfo = (await getDerivationInfoFromQRCredentials(restoreWallet)).first; return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', @@ -190,8 +180,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store } @override - Future processFromRestoredWallet(WalletCredentials credentials, - RestoredWallet restoreWallet) async { + Future processFromRestoredWallet( + WalletCredentials credentials, RestoredWallet restoreWallet) async { try { switch (restoreWallet.restoreMode) { case WalletRestoreMode.keys: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 147ec937d..6ed9249c9 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -400,7 +400,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs); if (outputs.length == updatedOutputs.length) { - outputs = ObservableList.of(updatedOutputs); + outputs.clear(); + outputs.addAll(updatedOutputs); } } diff --git a/lib/view_model/settings/silent_payments_settings_view_model.dart b/lib/view_model/settings/silent_payments_settings_view_model.dart index 5d20230d2..d7350e07a 100644 --- a/lib/view_model/settings/silent_payments_settings_view_model.dart +++ b/lib/view_model/settings/silent_payments_settings_view_model.dart @@ -20,6 +20,9 @@ abstract class SilentPaymentsSettingsViewModelBase with Store { @computed bool get silentPaymentsAlwaysScan => _settingsStore.silentPaymentsAlwaysScan; + @computed + bool get silentPaymentsKeyRegistered => _settingsStore.silentPaymentsKeyRegistered; + @action void setSilentPaymentsCardDisplay(bool value) { _settingsStore.silentPaymentsCardDisplay = value; @@ -30,4 +33,10 @@ abstract class SilentPaymentsSettingsViewModelBase with Store { _settingsStore.silentPaymentsAlwaysScan = value; if (value) bitcoin!.setScanningActive(_wallet, true); } + + @action + void registerSilentPaymentsKey(bool value) { + _settingsStore.silentPaymentsKeyRegistered = value; + bitcoin!.registerSilentPaymentsKey(_wallet, true); + } } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 1833a0f51..da6699ae6 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -83,18 +83,19 @@ abstract class TransactionDetailsViewModelBase with Store { break; } - if (showRecipientAddress && !isRecipientAddressShown) { - try { - final recipientAddress = transactionDescriptionBox.values - .firstWhere((val) => val.id == transactionInfo.txHash) - .recipientAddress; + final descriptionKey = + '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; + final description = transactionDescriptionBox.values.firstWhere( + (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, + orElse: () => TransactionDescription(id: descriptionKey)); - if (recipientAddress?.isNotEmpty ?? false) { - items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, value: recipientAddress!)); - } - } catch (_) { - // FIX-ME: Unhandled exception + if (showRecipientAddress && !isRecipientAddressShown) { + final recipientAddress = description.recipientAddress; + + if (recipientAddress?.isNotEmpty ?? false) { + items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: recipientAddress!)); } } @@ -110,12 +111,6 @@ abstract class TransactionDetailsViewModelBase with Store { } catch (e) {} })); - final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; - - final description = transactionDescriptionBox.values.firstWhere( - (val) => val.id == descriptionKey || val.id == transactionInfo.txHash, - orElse: () => TransactionDescription(id: descriptionKey)); - items.add(TextFieldListItem( title: S.current.note_tap_to_change, value: description.note, diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 17a8d6d28..95cf0256c 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -101,6 +101,7 @@ abstract class WalletCreationVMBase with Store { address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), + derivations: credentials.derivations, hardwareWalletType: credentials.hardwareWalletType, parentAddress: credentials.parentAddress, ); @@ -200,15 +201,36 @@ abstract class WalletCreationVMBase with Store { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( + final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, node: node, passphrase: restoreWallet.passphrase, ); - if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) - return []; - return derivationList; + List list = []; + for (var derivation in bitcoinDerivations) { + if (derivation.derivationType == DerivationType.electrum) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ), + ); + } else { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + ); + } + } + + return list; case WalletType.nano: return nanoUtil!.getDerivationsFromMnemonic( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index d37b69f74..bf1168f01 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -91,6 +91,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final height = options['height'] as int? ?? 0; name = options['name'] as String; DerivationInfo? derivationInfo = options["derivationInfo"] as DerivationInfo?; + List? derivations = options["derivations"] as List?; if (mode == WalletRestoreMode.seed) { final seed = options['seed'] as String; @@ -105,8 +106,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, passphrase: passphrase, - derivationType: derivationInfo!.derivationType!, - derivationPath: derivationInfo.derivationPath!, + derivations: derivations, ); case WalletType.haven: return haven!.createHavenRestoreWalletFromSeedCredentials( @@ -256,11 +256,36 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.litecoin: String? mnemonic = credentials['seed'] as String?; String? passphrase = credentials['passphrase'] as String?; - return bitcoin!.getDerivationsFromMnemonic( + final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( mnemonic: mnemonic!, node: node, passphrase: passphrase, ); + + List list = []; + for (var derivation in bitcoinDerivations) { + if (derivation.derivationType.toString().endsWith("electrum")) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ), + ); + } else { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + ); + } + } + + return list; case WalletType.nano: String? mnemonic = credentials['seed'] as String?; String? seedKey = credentials['private_key'] as String?; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 2abd8401a..72c6cd6bc 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -133,8 +133,8 @@ dependency_overrides: protobuf: ^3.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 ffi: 2.1.0 flutter_icons: diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 81fe3cc2c..abcb892e1 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "حدد المدفوعات الصامتة دائمًا المسح الضوئي", "silent_payments_disclaimer": "العناوين الجديدة ليست هويات جديدة. إنها إعادة استخدام هوية موجودة مع ملصق مختلف.", "silent_payments_display_card": "عرض بطاقة المدفوعات الصامتة", + "silent_payments_register_key": "سجل عرض مفتاح المسح الأسرع", "silent_payments_scan_from_date": "فحص من التاريخ", "silent_payments_scan_from_date_or_blockheight": "يرجى إدخال ارتفاع الكتلة الذي تريد بدء المسح الضوئي للمدفوعات الصامتة الواردة ، أو استخدام التاريخ بدلاً من ذلك. يمكنك اختيار ما إذا كانت المحفظة تواصل مسح كل كتلة ، أو تتحقق فقط من الارتفاع المحدد.", "silent_payments_scan_from_height": "فحص من ارتفاع الكتلة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 50db1610a..2060711c5 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Задайте мълчаливи плащания винаги сканиране", "silent_payments_disclaimer": "Новите адреси не са нови идентичности. Това е повторна употреба на съществуваща идентичност с различен етикет.", "silent_payments_display_card": "Показване на безшумни плащания карта", + "silent_payments_register_key": "Регистрирайте ключа за преглед на по -бързото сканиране", "silent_payments_scan_from_date": "Сканиране от дата", "silent_payments_scan_from_date_or_blockheight": "Моля, въведете височината на блока, която искате да започнете да сканирате за входящи безшумни плащания, или вместо това използвайте датата. Можете да изберете дали портфейлът продължава да сканира всеки блок или проверява само определената височина.", "silent_payments_scan_from_height": "Сканиране от височината на блока", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ddc91340b..f06fe6885 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Nastavit tiché platby vždy skenování", "silent_payments_disclaimer": "Nové adresy nejsou nové identity. Je to opětovné použití existující identity s jiným štítkem.", "silent_payments_display_card": "Zobrazit kartu Silent Payments", + "silent_payments_register_key": "Zobrazení zaregistrujte klíč pro rychlejší skenování", "silent_payments_scan_from_date": "Skenovat od data", "silent_payments_scan_from_date_or_blockheight": "Zadejte výšku bloku, kterou chcete začít skenovat, zda jsou přicházející tiché platby, nebo místo toho použijte datum. Můžete si vybrat, zda peněženka pokračuje v skenování každého bloku nebo zkontroluje pouze zadanou výšku.", "silent_payments_scan_from_height": "Skenování z výšky bloku", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2ec59f349..c094e838a 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Setzen Sie stille Zahlungen immer scannen", "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", "silent_payments_display_card": "Zeigen Sie stille Zahlungskarte", + "silent_payments_register_key": "Registrieren Sie die Ansichtsschlüssel für schnelleres Scannen", "silent_payments_scan_from_date": "Scan ab Datum", "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", "silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d6a0ee9af..b3c2f4720 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -712,6 +712,7 @@ "silent_payments_always_scan": "Set Silent Payments always scanning", "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", + "silent_payments_register_key": "Register view key for faster scanning", "silent_payments_scan_from_date": "Scan from date", "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a8ad7549a..ecc2356bb 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -1,5 +1,5 @@ { - "about_cake_pay": "Cake Pay le permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150 000 comerciantes en los Estados Unidos.", + "about_cake_pay": "Cake Pay te permite comprar fácilmente tarjetas de regalo con activos virtuales, gastables instantáneamente en más de 150,000 comerciantes en los Estados Unidos.", "account": "Cuenta", "accounts": "Cuentas", "accounts_subaddresses": "Cuentas y subdirecciones", @@ -21,13 +21,13 @@ "add_token_disclaimer_check": "He confirmado la dirección del contrato del token y la información utilizando una fuente confiable. Agregar información maliciosa o incorrecta puede resultar en una pérdida de fondos.", "add_token_warning": "No edite ni agregue tokens según las instrucciones de los estafadores.\n¡Confirme siempre las direcciones de los tokens con fuentes acreditadas!", "add_value": "Añadir valor", - "address": "DIRECCIÓN", + "address": "Dirección", "address_book": "Libreta de direcciones", "address_book_menu": "Libreta de direcciones", "address_detected": "Dirección detectada", "address_from_domain": "Esta dirección es de ${domain} en Unstoppable Domains", "address_from_yat": "Esta dirección es de ${emoji} en Yat", - "address_label": "Address label", + "address_label": "Etiqueta de dirección", "address_remove_contact": "Remover contacto", "address_remove_content": "¿Estás seguro de que quieres eliminar el contacto seleccionado?", "addresses": "Direcciones", @@ -37,12 +37,12 @@ "agree_and_continue": "Aceptar y continuar", "agree_to": "Al crear una cuenta, aceptas ", "alert_notice": "Aviso", - "all": "TODOS", + "all": "Todos", "all_trades": "Todos los oficios", "all_transactions": "Todas las transacciones", "alphabetical": "Alfabético", "already_have_account": "¿Ya tienes una cuenta?", - "always": "siempre", + "always": "Siempre", "amount": "Cantidad: ", "amount_is_below_minimum_limit": "Su saldo después de las tarifas sería menor que la cantidad mínima necesaria para el intercambio (${min})", "amount_is_estimate": "El monto recibido es un estimado", @@ -54,17 +54,17 @@ "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Pregunta cada vez", - "auth_store_ban_timeout": "prohibición de tiempo de espera", + "auth_store_ban_timeout": "Prohibición de tiempo de espera", "auth_store_banned_for": "Prohibido para ", "auth_store_banned_minutes": " minutos", "auth_store_incorrect_password": "Contraseña PIN", "authenticated": "Autenticados", "authentication": "Autenticación", - "auto_generate_addresses": "Auto Generar direcciones", + "auto_generate_addresses": "Auto-generar nuevas direcciones", "auto_generate_subaddresses": "Generar subdirecciones automáticamente", "automatic": "Automático", "available_balance": "Balance disponible", - "available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.", + "available_balance_description": "Tu saldo disponible es la cantidad de fondos que puedes gastar. Los fondos que se muestran aquí, se pueden gastar inmediatamente.", "avg_savings": "Ahorro promedio", "awaitDAppProcessing": "Espere a que la dApp termine de procesarse.", "awaiting_payment_confirmation": "Esperando confirmación de pago", @@ -78,8 +78,8 @@ "billing_address_info": "Si se le solicita una dirección de facturación, proporcione su dirección de envío", "biometric_auth_reason": "Escanee su huella digital para autenticar", "bitcoin_dark_theme": "Tema oscuro de Bitcoin", - "bitcoin_light_theme": "Tema de la luz de Bitcoin", - "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por su paciencia! Se le enviará un correo electrónico cuando se confirme el pago.", + "bitcoin_light_theme": "Tema claro de Bitcoin", + "bitcoin_payments_require_1_confirmation": "Los pagos de Bitcoin requieren 1 confirmación, que puede demorar 20 minutos o más. ¡Gracias por tu paciencia! Se te enviará un correo electrónico cuando se confirme el pago.", "block_remaining": "1 bloqueo restante", "Blocks_remaining": "${status} Bloques restantes", "bluetooth": "Bluetooth", @@ -93,26 +93,26 @@ "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", "cake_2fa_preset": "Pastel 2FA preestablecido", - "cake_dark_theme": "Tema oscuro del pastel", - "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", - "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", - "cake_pay_save_order": "La tarjeta debe enviarse a su correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", - "cake_pay_subtitle": "Compre tarjetas prepagas y tarjetas de regalo en todo el mundo", - "cake_pay_web_cards_subtitle": "Compre tarjetas de prepago y tarjetas de regalo en todo el mundo", + "cake_dark_theme": "Tema oscuro", + "cake_pay_account_note": "Regístrate con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", + "cake_pay_learn_more": "¡Compra y canjea tarjetas de regalo al instante en la aplicación!\nDesliza el dedo de izquierda a derecha para obtener más información.", + "cake_pay_save_order": "La tarjeta debe enviarse a tu correo electrónico dentro de 1 día hábil \n Guardar su ID de pedido:", + "cake_pay_subtitle": "Compra tarjetas prepagadas y tarjetas de regalo en todo el mundo", + "cake_pay_web_cards_subtitle": "Compra tarjetas de prepago y tarjetas de regalo en todo el mundo", "cake_pay_web_cards_title": "Tarjetas Web Cake Pay", "cake_wallet": "Cake Wallet", "cakepay_prepaid_card": "Tarjeta de Débito Prepago CakePay", - "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.", - "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítelo desde la configuración de la aplicación.", + "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulta tu Política de privacidad para obtener más detalles.", + "camera_permission_is_required": "Se requiere permiso de la cámara.\nHabilítalo desde la configuración de la aplicación.", "cancel": "Cancelar", "card_address": "Dirección:", "cardholder_agreement": "Acuerdo del titular de la tarjeta", "cards": "Cartas", "chains": "Cadenas", "change": "Cambio", - "change_backup_password_alert": "Sus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?", + "change_backup_password_alert": "Tus archivos de respaldo anteriores no estarán disponibles para importar con la nueva contraseña de respaldo. La nueva contraseña de respaldo se utilizará solo para los nuevos archivos de respaldo. ¿Está seguro de que desea cambiar la contraseña de respaldo?", "change_currency": "Cambiar moneda", - "change_current_node": "¿Está seguro de cambiar el nodo actual a ${node}?", + "change_current_node": "¿Estás seguro de cambiar el nodo actual a ${node}?", "change_current_node_title": "Cambiar el nodo actual", "change_exchange_provider": "Cambiar proveedor de intercambio", "change_language": "Cambiar idioma", @@ -125,38 +125,38 @@ "change_wallet_alert_title": "Cambiar billetera actual", "choose_account": "Elegir cuenta", "choose_address": "\n\nPor favor elija la dirección:", - "choose_card_value": "Elija un valor de tarjeta", - "choose_derivation": "Elija la derivación de la billetera", - "choose_from_available_options": "Elija entre las opciones disponibles:", + "choose_card_value": "Elige un valor de tarjeta", + "choose_derivation": "Elige la derivación de la billetera", + "choose_from_available_options": "Elige entre las opciones disponibles:", "choose_one": "Elige uno", - "choose_relay": "Por favor elija un relé para usar", - "choose_wallet_currency": "Por favor, elija la moneda de la billetera:", - "choose_wallet_group": "Elija el grupo de billetera", + "choose_relay": "Por favor elige un relay para usar", + "choose_wallet_currency": "Por favor, elige la moneda de la billetera:", + "choose_wallet_group": "Elige el grupo de billetera", "clear": "Claro", "clearnet_link": "enlace Clearnet", "close": "Cerca", "coin_control": "Control de monedas (opcional)", - "cold_or_recover_wallet": "Agregue una billetera fría o recupere una billetera de papel", + "cold_or_recover_wallet": "Agrega una billetera fría o recupera una billetera de papel", "color_theme": "Tema de color", "commit_transaction_amount_fee": "Confirmar transacción\nCantidad: ${amount}\nCuota: ${fee}", "confirm": "Confirmar", - "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Desea continuar?", - "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Desea continuar?", + "confirm_delete_template": "Esta acción eliminará esta plantilla. ¿Deseas continuar?", + "confirm_delete_wallet": "Esta acción eliminará esta billetera. ¿Deseas continuar?", "confirm_fee_deduction": "Confirmar la deducción de la tarifa", - "confirm_fee_deduction_content": "¿Acepta deducir la tarifa de la producción?", - "confirm_passphrase": "Confirmar la frase de pases", + "confirm_fee_deduction_content": "¿Aceptas deducir la tarifa de la producción?", + "confirm_passphrase": "Confirmar la contraseña", "confirm_sending": "Confirmar envío", - "confirm_silent_payments_switch_node": "Su nodo actual no admite pagos silenciosos \\ ncake billet cambiará a un nodo compatible, solo para escanear", + "confirm_silent_payments_switch_node": "Tu nodo actual no admite pagos silenciosos \\ nCake cambiará a un nodo compatible, solo para escanear", "confirmations": "Confirmaciones", "confirmed": "Saldo confirmado", "confirmed_tx": "Confirmado", "congratulations": "Felicidades!", "connect_an_existing_yat": "Conectar un Yat existente", "connect_yats": "Conectar Yats", - "connect_your_hardware_wallet": "Conecte su billetera de hardware con Bluetooth o USB", - "connect_your_hardware_wallet_ios": "Conecte su billetera de hardware con Bluetooth", + "connect_your_hardware_wallet": "Conecta tu billetera de hardware con Bluetooth o USB", + "connect_your_hardware_wallet_ios": "Conecta tu billetera de hardware con Bluetooth", "connection_sync": "Conexión y sincronización", - "connectWalletPrompt": "Conecte su billetera con WalletConnect para realizar transacciones", + "connectWalletPrompt": "Conecte tu billetera con WalletConnect para realizar transacciones", "contact": "Contacto", "contact_list_contacts": "Contactos", "contact_list_wallets": "Mis billeteras", @@ -187,7 +187,7 @@ "custom_drag": "Custom (mantenía y arrastre)", "custom_redeem_amount": "Cantidad de canje personalizada", "custom_value": "Valor personalizado", - "dark_theme": "Oscura", + "dark_theme": "Oscuro", "debit_card": "Tarjeta de Débito", "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", "decimal_places_error": "Demasiados lugares decimales", @@ -197,21 +197,21 @@ "delete": "Borrar", "delete_account": "Eliminar cuenta", "delete_wallet": "Eliminar billetera", - "delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?", - "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", + "delete_wallet_confirm_message": "¿Estás seguro de que deseas eliminar la billetera ${wallet_name}?", + "deleteConnectionConfirmationPrompt": "¿Estás seguro de que deseas eliminar la conexión a", "denominations": "Denominaciones", "derivationpath": "Ruta de derivación", "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", - "dfx_option_description": "Compre criptografía con EUR y CHF. Para clientes minoristas y corporativos en Europa", + "dfx_option_description": "Compre cripto con EUR y CHF. Para clientes minoristas y corporativos en Europa", "didnt_get_code": "¿No recibiste el código?", "digit_pin": "-dígito PIN", "digital_and_physical_card": " tarjeta de débito prepago digital y física", "disable": "Desactivar", "disable_bulletin": "Desactivar el boletín de estado del servicio", "disable_buy": "Desactivar acción de compra", - "disable_cake_2fa": "Desactivar pastel 2FA", + "disable_cake_2fa": "Desactivar 2FA", "disable_exchange": "Deshabilitar intercambio", "disable_fee_api_warning": "Al apagar esto, las tasas de tarifas pueden ser inexactas en algunos casos, por lo que puede terminar pagando en exceso o pagando menos las tarifas por sus transacciones", "disable_fiat": "Deshabilitar fiat", @@ -224,7 +224,7 @@ "displayable": "Visualizable", "do_not_have_enough_gas_asset": "No tienes suficiente ${currency} para realizar una transacción con las condiciones actuales de la red blockchain. Necesita más ${currency} para pagar las tarifas de la red blockchain, incluso si envía un activo diferente.", "do_not_send": "no enviar", - "do_not_share_warning_text": "No comparta estos con nadie más, incluido el soporte.\n\n¡Sus fondos pueden ser y serán robados!", + "do_not_share_warning_text": "No compartas estos con nadie más, incluido el soporte.\n\n¡Tus fondos pueden ser y serán robados!", "do_not_show_me": "no me muestres esto otra vez", "domain_looks_up": "Búsquedas de dominio", "donation_link_details": "Detalles del enlace de donación", @@ -238,21 +238,21 @@ "enable": "Permitir", "enable_mempool_api": "API de Mempool para tarifas y fechas precisas", "enable_replace_by_fee": "Habilitar reemplazar por tarea", - "enable_silent_payments_scanning": "Comience a escanear pagos silenciosos, hasta que se alcance la punta", + "enable_silent_payments_scanning": "Comienza a escanear pagos silenciosos, hasta que se alcance la altura actual", "enabled": "Activado", - "enter_amount": "Ingrese la cantidad", - "enter_backup_password": "Ingrese la contraseña de respaldo aquí", + "enter_amount": "Ingresa la cantidad", + "enter_backup_password": "Ingresa la contraseña de respaldo aquí", "enter_code": "Ingresar código", - "enter_seed_phrase": "Ingrese su frase de semillas", - "enter_totp_code": "Ingrese el código TOTP.", - "enter_wallet_password": "Ingrese la contraseña de la billetera", + "enter_seed_phrase": "Ingresa su frase de semillas", + "enter_totp_code": "Ingresa el código TOTP.", + "enter_wallet_password": "Ingresa la contraseña de la billetera", "enter_your_note": "Ingresa tu nota…", "enter_your_pin": "Introduce tu PIN", - "enter_your_pin_again": "Ingrese su PIN nuevamente", - "enterTokenID": "Ingrese el ID del token", - "enterWalletConnectURI": "Ingrese el URI de WalletConnect", + "enter_your_pin_again": "Ingresa su PIN nuevamente", + "enterTokenID": "Ingresa el ID del token", + "enterWalletConnectURI": "Ingresa el URI de WalletConnect", "error": "Error", - "error_dialog_content": "Vaya, tenemos un error.\n\nEnvíe el informe de bloqueo a nuestro equipo de soporte para mejorar la aplicación.", + "error_dialog_content": "Vaya, tenemos un error.\n\nEnvía el informe de error a nuestro equipo de soporte para mejorar la aplicación.", "error_text_account_name": "El nombre de la cuenta solo puede contener letras, números \ny debe tener entre 1 y 15 caracteres de longitud", "error_text_address": "La dirección de la billetera debe corresponder al tipo \nde criptomoneda", "error_text_amount": "La cantidad solo puede contener números", @@ -281,7 +281,7 @@ "etherscan_history": "historia de etherscan", "event": "Evento", "events": "Eventos", - "exchange": "Intercambio", + "exchange": "Intercambiar", "exchange_incorrect_current_wallet_for_xmr": "Si desea intercambiar XMR desde su billetera de pastel Monero Balance, primero cambie a su billetera Monero.", "exchange_new_template": "Nueva plantilla", "exchange_provider_unsupported": "¡${providerName} ya no es compatible!", @@ -304,14 +304,14 @@ "fee_rate": "Tarifa", "fetching": "Cargando", "fiat_api": "Fiat API", - "fiat_balance": "Equilibrio Fiat", + "fiat_balance": "Balance fiat", "field_required": "Este campo es obligatorio", - "fill_code": "Por favor complete el código de verificación proporcionado a su correo electrónico", + "fill_code": "Por favor completa el código de verificación proporcionado en tu correo electrónico", "filter_by": "Filtrado por", "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", "fixed_pair_not_supported": "Este par fijo no es compatible con los servicios de intercambio seleccionados", "fixed_rate": "Tipo de interés fijo", - "fixed_rate_alert": "Podrá ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", + "fixed_rate_alert": "Podrás ingresar la cantidad recibida cuando el modo de tarifa fija esté marcado. ¿Quieres cambiar al modo de tarifa fija?", "forgot_password": "Olvidé mi contraseña", "freeze": "Congelar", "frequently_asked_questions": "Preguntas frecuentes", @@ -333,7 +333,7 @@ "gross_balance": "Saldo bruto", "group_by_type": "Grupo por tipo", "haven_app": "Haven by Cake Wallet", - "haven_app_wallet_text": "Awesome wallet for Haven", + "haven_app_wallet_text": "Increíble billetera para Haven", "help": "ayuda", "hidden_balance": "Balance oculto", "hide_details": "Ocultar detalles", @@ -349,9 +349,9 @@ "incoming": "Entrante", "incorrect_seed": "El texto ingresado no es válido.", "inputs": "Entradas", - "insufficient_lamport_for_tx": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agregue más SOL a su billetera o reduzca la cantidad de sol que está enviando.", - "insufficient_lamports": "No tiene suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", - "insufficientFundsForRentError": "No tiene suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agregue más sol a su billetera o reduzca la cantidad de sol que está enviando", + "insufficient_lamport_for_tx": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Por favor, agrega más SOL a su billetera o reduce la cantidad de sol que está enviando.", + "insufficient_lamports": "No tienes suficiente SOL para cubrir la transacción y su tarifa de transacción. Necesita al menos ${solValueNeeded} sol. Por favor, agrega más sol a su billetera o reduzca la cantidad de sol que está enviando", + "insufficientFundsForRentError": "No tienes suficiente SOL para cubrir la tarifa de transacción y alquilar para la cuenta. Por favor, agrega más sol a su billetera o reduce la cantidad de sol que está enviando", "introducing_cake_pay": "¡Presentamos Cake Pay!", "invalid_input": "Entrada inválida", "invalid_password": "Contraseña invalida", @@ -359,12 +359,12 @@ "is_percentage": "es", "last_30_days": "Últimos 30 días", "learn_more": "Aprende más", - "ledger_connection_error": "No se pudo conectar con su libro mayor. Inténtalo de nuevo.", - "ledger_error_device_locked": "El libro mayor está bloqueado", + "ledger_connection_error": "No se pudo conectar con ledger. Inténtalo de nuevo.", + "ledger_error_device_locked": "Ledger está bloqueado", "ledger_error_tx_rejected_by_user": "Transacción rechazada en el dispositivo", - "ledger_error_wrong_app": "Por favor, asegúrese de abrir la aplicación correcta en su libro mayor.", - "ledger_please_enable_bluetooth": "Habilite Bluetooth para detectar su libro mayor", - "light_theme": "Ligera", + "ledger_error_wrong_app": "Por favor, asegúrate de abrir la aplicación correcta en su libro mayor.", + "ledger_please_enable_bluetooth": "Habilita tu Bluetooth para detectar tu ledger", + "light_theme": "Ligero", "litecoin_enable_mweb_sync": "Habilitar el escaneo mweb", "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Establecer mweb siempre escaneo", @@ -372,8 +372,8 @@ "litecoin_mweb_dismiss": "Despedir", "litecoin_mweb_display_card": "Mostrar tarjeta MWEB", "litecoin_mweb_enable_later": "Puede elegir habilitar MWEB nuevamente en la configuración de visualización.", - "litecoin_mweb_pegin": "Meter", - "litecoin_mweb_pegout": "Estirar la pata", + "litecoin_mweb_pegin": "Convertir", + "litecoin_mweb_pegout": "Recuperar", "litecoin_mweb_scanning": "Escaneo mweb", "litecoin_mweb_settings": "Configuración de MWEB", "litecoin_mweb_warning": "El uso de MWEB inicialmente descargará ~ 600 MB de datos, y puede tomar hasta 30 minutos según la velocidad de la red. Estos datos iniciales solo se descargarán una vez y estarán disponibles para todas las billeteras de Litecoin", @@ -401,9 +401,9 @@ "min_value": "Min: ${value} ${currency}", "minutes_to_pin_code": "${minute} minutos", "mm": "mm", - "modify_2fa": "Modificar torta 2FA", - "monero_com": "Monero.com by Cake Wallet", - "monero_com_wallet_text": "Awesome wallet for Monero", + "modify_2fa": "Modificar 2FA", + "monero_com": "Monero.com por Cake Wallet", + "monero_com_wallet_text": "Increíble billetera para Monero", "monero_dark_theme": "Tema oscuro de Monero", "monero_light_theme": "Tema ligero de Monero", "moonpay_alert_text": "El valor de la cantidad debe ser mayor o igual a ${minAmount} ${fiatCurrency}", @@ -412,12 +412,12 @@ "mweb_unconfirmed": "Mweb no confirmado", "name": "Nombre", "nano_current_rep": "Representante actual", - "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerde regresar al navegador después de que se complete su transacción!", + "nano_gpt_thanks_message": "¡Gracias por usar nanogpt! ¡Recuerda regresar al navegador después de que se complete su transacción!", "nano_pick_new_rep": "Elija un nuevo representante", "nanogpt_subtitle": "Todos los modelos más nuevos (GPT-4, Claude). \\nSin suscripción, pague con cripto.", "narrow": "Angosto", - "new_first_wallet_text": "Mantenga fácilmente su criptomoneda segura", - "new_node_testing": "Prueba de nuevos nodos", + "new_first_wallet_text": "Mantén fácilmente tu criptomoneda segura", + "new_node_testing": "Prueba nuevos nodos", "new_subaddress_create": "Crear", "new_subaddress_label_name": "Nombre de etiqueta", "new_subaddress_title": "Nueva direccion", @@ -426,10 +426,10 @@ "newConnection": "Nueva conexión", "no_cards_found": "No se encuentran cartas", "no_id_needed": "¡No se necesita identificación!", - "no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar", - "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar.", - "no_relays": "Sin relevos", - "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relé. Indique al destinatario que agregue retransmisiones a su registro Nostr.", + "no_id_required": "No se requiere identificación. Recarga y gaste en cualquier lugar", + "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elige un relay para usar.", + "no_relays": "Sin relays", + "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relay. Indica al destinatario que agregue retransmisiones a su registro Nostr.", "node_address": "Dirección de nodo", "node_connection_failed": "La conexión falló", "node_connection_successful": "La conexión fue exitosa", @@ -449,15 +449,15 @@ "offline": "fuera de línea", "ok": "OK", "old_fee": "Tarifa antigua", - "onion_link": "Enlace de cebolla", + "onion_link": "Enlace de cebolla (Tor)", "online": "En línea", - "onramper_option_description": "Compre rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.", + "onramper_option_description": "Compra rápidamente cripto con muchos métodos de pago. Disponible en la mayoría de los países. Los diferenciales y las tarifas varían.", "open_gift_card": "Abrir tarjeta de regalo", "optional_description": "Descripción opcional", "optional_email_hint": "Correo electrónico de notificación del beneficiario opcional", "optional_name": "Nombre del destinatario opcional", "optionally_order_card": "Opcionalmente pide una tarjeta física.", - "orbot_running_alert": "Asegúrese de que Orbot se esté ejecutando antes de conectarse a este nodo.", + "orbot_running_alert": "Asegúrate de que Orbot se esté ejecutando antes de conectarte a este nodo.", "order_by": "Ordenar", "order_id": "Identificación del pedido", "order_physical_card": "Pedir tarjeta física", @@ -466,7 +466,7 @@ "outdated_electrum_wallet_receive_warning": "Si esta billetera tiene una semilla de 12 palabras y se creó en Cake, NO deposite Bitcoin en esta billetera. Cualquier BTC transferido a esta billetera se puede perder. Cree una nueva billetera de 24 palabras (toque el menú en la parte superior derecha, seleccione Monederos, elija Crear nueva billetera, luego seleccione Bitcoin) e INMEDIATAMENTE mueva su BTC allí. Las nuevas carteras BTC (24 palabras) de Cake son seguras", "outgoing": "Saliente", "outputs": "Salidas", - "overwrite_amount": "Overwrite amount", + "overwrite_amount": "Sobreescribir monto", "pairingInvalidEvent": "Evento de emparejamiento no válido", "passphrase": "Passfrase (opcional)", "passphrases_doesnt_match": "Las frases de contrato no coinciden, intente nuevamente", @@ -482,18 +482,18 @@ "pin_is_incorrect": "PIN es incorrecto", "pin_number": "Número PIN", "placeholder_contacts": "Tus contactos se mostrarán aquí", - "placeholder_transactions": "Sus transacciones se mostrarán aquí", - "please_fill_totp": "Complete el código de 8 dígitos presente en su otro dispositivo", - "please_make_selection": "Seleccione a continuación para crear o recuperar su billetera.", - "please_reference_document": "Consulte los documentos a continuación para obtener más información.", - "please_select": "Por favor seleccione:", - "please_select_backup_file": "Seleccione el archivo de respaldo e ingrese la contraseña de respaldo.", + "placeholder_transactions": "Tus transacciones se mostrarán aquí", + "please_fill_totp": "Completa el código de 8 dígitos presente en su otro dispositivo", + "please_make_selection": "Selecciona a continuación para crear o recuperar su billetera.", + "please_reference_document": "Consulta los documentos a continuación para obtener más información.", + "please_select": "Por favor selecciona:", + "please_select_backup_file": "Selecciona el archivo de respaldo e ingrese la contraseña de respaldo.", "please_try_to_connect_to_another_node": "Intenta conectarte a otro nodo", - "please_wait": "Espere por favor", + "please_wait": "Espera por favor", "polygonscan_history": "Historial de PolygonScan", - "powered_by": "Energizado por ${title}", + "powered_by": "Posible gracias a ${title}", "pre_seed_button_text": "Entiendo. Muéstrame mi semilla", - "pre_seed_description": "En la página siguiente verá una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar su billetera en caso de pérdida o mal funcionamiento. Es SU responsabilidad escribirlo y guardarlo en un lugar seguro fuera de la aplicación Cake Wallet.", + "pre_seed_description": "En la página siguiente verás una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar tu billetera en caso de pérdida o mal funcionamiento. Es TU responsabilidad escribirla y guardarla en un lugar seguro fuera de la aplicación Cake Wallet.", "pre_seed_title": "IMPORTANTE", "prepaid_cards": "Tajetas prepagadas", "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla", @@ -502,17 +502,17 @@ "privacy_settings": "Configuración de privacidad", "private_key": "Clave privada", "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", - "proceed_on_device": "Continúe con su dispositivo", - "proceed_on_device_description": "Siga las instrucciones solicitadas en su billetera de hardware", + "proceed_on_device": "Continúa con tu dispositivo", + "proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", "purchase_gift_card": "Comprar tarjeta de regalo", "purple_dark_theme": "Tema morado oscuro", "qr_fullscreen": "Toque para abrir el código QR en pantalla completa", - "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "qr_payment_amount": "Este código QR contiene un monto de pago. ¿Quieres sobreescribirlo?", "quantity": "Cantidad", - "question_to_disable_2fa": "¿Está seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", + "question_to_disable_2fa": "¿Estás seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", "receivable_balance": "Saldo de cuentas por cobrar", "receive": "Recibir", "receive_amount": "Cantidad", @@ -529,12 +529,12 @@ "remaining": "restante", "remove": "Retirar", "remove_node": "Eliminar nodo", - "remove_node_message": "¿Está seguro de que desea eliminar el nodo seleccionado?", - "rename": "Rebautizar", + "remove_node_message": "¿Estás seguro de que desea eliminar el nodo seleccionado?", + "rename": "Renombrar", "rep_warning": "Advertencia representativa", - "rep_warning_sub": "Su representante no parece estar en buena posición. Toque aquí para seleccionar uno nuevo", - "repeat_wallet_password": "Repita la contraseña de billetera", - "repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repita la contraseña de la billetera nuevamente.", + "rep_warning_sub": "Tu representante no parece estar en buena posición. Toca aquí para seleccionar uno nuevo", + "repeat_wallet_password": "Repite la contraseña de billetera", + "repeated_password_is_incorrect": "La contraseña repetida es incorrecta. Repite la contraseña de la billetera nuevamente.", "require_for_adding_contacts": "Requerido para agregar contactos", "require_for_all_security_and_backup_settings": "Requerido para todas las configuraciones de seguridad y copia de seguridad", "require_for_assessing_wallet": "Requerido para acceder a la billetera", @@ -566,17 +566,17 @@ "restore_recover": "Recuperar", "restore_restore_wallet": "Recuperar Cartera", "restore_seed_keys_restore": "Restauración de semillas / llaves", - "restore_spend_key_private": "Spend clave (privado)", + "restore_spend_key_private": "Llave de gasto (privada)", "restore_title_from_backup": "Restaurar desde un archivo de respaldo", - "restore_title_from_hardware_wallet": "Restaurar desde la billetera de hardware", - "restore_title_from_keys": "De las claves", - "restore_title_from_seed": "De la semilla", - "restore_title_from_seed_keys": "Restaurar desde semilla/claves", - "restore_view_key_private": "View clave (privado)", + "restore_title_from_hardware_wallet": "Restaurar desde una cartera fría", + "restore_title_from_keys": "Usando las claves", + "restore_title_from_seed": "Usando la semilla", + "restore_title_from_seed_keys": "Restaurar usando semilla/claves", + "restore_view_key_private": "Llave de vista (privado)", "restore_wallet": "Restaurar billetera", "restore_wallet_name": "Nombre de la billetera", "restore_wallet_restore_description": "Restaurar billetera", - "robinhood_option_description": "Compre y transfiera instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.", + "robinhood_option_description": "Compra y transfiere instantáneamente utilizando su tarjeta de débito, cuenta bancaria o saldo de Robinhood. Solo EE. UU.", "router_no_route": "No hay ruta definida para ${name}", "save": "Salvar", "save_backup_password": "Asegúrese de haber guardado su contraseña de respaldo. No podrá importar sus archivos de respaldo sin él.", @@ -585,7 +585,7 @@ "saved_the_trade_id": "He salvado comercial ID", "scan_one_block": "Escanear un bloque", "scan_qr_code": "Escanear código QR", - "scan_qr_code_to_get_address": "Escanee el código QR para obtener la dirección", + "scan_qr_code_to_get_address": "Escanea el código QR para obtener la dirección", "scan_qr_on_device": "Escanea este código QR en otro dispositivo", "search": "Búsqueda", "search_add_token": "Buscar/Agregar token", @@ -612,25 +612,25 @@ "seed_language_german": "Alemán", "seed_language_italian": "Italiana/Italiano", "seed_language_japanese": "Japonés", - "seed_language_korean": "coreano", + "seed_language_korean": "Coreano", "seed_language_next": "Próximo", "seed_language_portuguese": "Portugués", "seed_language_russian": "Ruso", "seed_language_spanish": "Español", "seed_phrase_length": "Longitud de la frase inicial", - "seed_reminder": "Anótelos en caso de que pierda o borre su teléfono", + "seed_reminder": "Anótalos en caso de que pierdas o borres tu aplicación", "seed_share": "Compartir semillas", "seed_title": "Semilla", - "seedtype": "Type de semillas", - "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con Bip39 Seed Type.", - "seedtype_alert_title": "Alerta de type de semillas", - "seedtype_legacy": "Legado (25 palabras)", - "seedtype_polyseed": "Polieta (16 palabras)", + "seedtype": "Tipos de semillas", + "seedtype_alert_content": "Compartir semillas con otras billeteras solo es posible con semillas bip39 - un tipo específico de semilla.", + "seedtype_alert_title": "Alerta de tipo de semillas", + "seedtype_legacy": "Semilla clásica-legacy (25 palabras)", + "seedtype_polyseed": "Poli-semilla (16 palabras)", "seedtype_wownero": "Wownero (14 palabras)", "select_backup_file": "Seleccionar archivo de respaldo", - "select_buy_provider_notice": "Seleccione un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", - "select_destination": "Seleccione el destino del archivo de copia de seguridad.", - "select_sell_provider_notice": "Seleccione un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", + "select_buy_provider_notice": "Selecciona un proveedor de compra arriba. Puede omitir esta pantalla configurando su proveedor de compra predeterminado en la configuración de la aplicación.", + "select_destination": "Selecciona el destino del archivo de copia de seguridad.", + "select_sell_provider_notice": "Selecciona un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", "sell": "Vender", "sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin, Ethereum y Litecoin. Cree o cambie a su billetera Bitcoin, Ethereum o Litecoin.", "sell_monero_com_alert_content": "Aún no se admite la venta de Monero", @@ -676,16 +676,16 @@ "settings_only_transactions": "Solo transacciones", "settings_personal": "Personal", "settings_save_recipient_address": "Guardar dirección del destinatario", - "settings_support": "Apoyo", + "settings_support": "Ayuda", "settings_terms_and_conditions": "Términos y Condiciones", "settings_title": "Configuraciones", "settings_trades": "Comercia", "settings_transactions": "Transacciones", "settings_wallets": "Carteras", - "setup_2fa": "Configurar pastel 2FA", - "setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puede encontrar más información y aplicaciones compatibles en la guía.", + "setup_2fa": "Configurar 2FA", + "setup_2fa_text": "Cake 2FA funciona utilizando TOTP como segundo factor de autenticación.\n\nEl TOTP de Cake 2FA requiere SHA-512 y soporte de 8 dígitos; esto proporciona una mayor seguridad. Puedes encontrar más información y aplicaciones compatibles en la guía.", "setup_pin": "PIN de configuración", - "setup_successful": "Su PIN se ha configurado correctamente!", + "setup_successful": "Tu PIN se ha configurado correctamente!", "setup_totp_recommended": "Configurar TOTP", "setup_warning_2fa_text": "Deberá restaurar su billetera a partir de la semilla mnemotécnica.\n\nEl soporte de Cake no podrá ayudarlo si pierde el acceso a su 2FA o a sus semillas mnemotécnicas.\nCake 2FA es una segunda autenticación para ciertas acciones en la billetera. Antes de usar Cake 2FA, recomendamos leer la guía.NO es tan seguro como el almacenamiento en frío.\n\nSi pierde el acceso a su aplicación 2FA o a sus claves TOTP, perderá el acceso a esta billetera. ", "setup_your_debit_card": "Configura tu tarjeta de débito", @@ -704,23 +704,24 @@ "signature": "Firma", "signature_invalid_error": "La firma no es válida para el mensaje dado", "signTransaction": "Firmar transacción", - "signup_for_card_accept_terms": "Regístrese para obtener la tarjeta y acepte los términos.", + "signup_for_card_accept_terms": "Regístrate para obtener la tarjeta y acepte los términos.", "silent_payment": "Pago silencioso", "silent_payments": "Pagos silenciosos", "silent_payments_always_scan": "Establecer pagos silenciosos siempre escaneando", "silent_payments_disclaimer": "Las nuevas direcciones no son nuevas identidades. Es una reutilización de una identidad existente con una etiqueta diferente.", "silent_payments_display_card": "Mostrar tarjeta de pagos silenciosos", + "silent_payments_register_key": "Clave de vista de registro para escaneo más rápido", "silent_payments_scan_from_date": "Escanear desde la fecha", - "silent_payments_scan_from_date_or_blockheight": "Ingrese la altura del bloque que desea comenzar a escanear para pagos silenciosos entrantes, o use la fecha en su lugar. Puede elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.", - "silent_payments_scan_from_height": "Escanear desde la altura del bloque", - "silent_payments_scanned_tip": "Escaneado hasta la punta! (${tip})", + "silent_payments_scan_from_date_or_blockheight": "Ingresa la altura de bloque que desea comenzar a escanear para pagos silenciosos entrantes, o usa la fecha en su lugar. Puedes elegir si la billetera continúa escaneando cada bloque, o verifica solo la altura especificada.", + "silent_payments_scan_from_height": "Escanear desde la altura de bloque específico", + "silent_payments_scanned_tip": "Escaneado hasta la altura actual! (${tip})", "silent_payments_scanning": "Escaneo de pagos silenciosos", "silent_payments_settings": "Configuración de pagos silenciosos", "single_seed_wallets_group": "Billeteras de semillas individuales", "slidable": "deslizable", "sort_by": "Ordenar por", - "spend_key_private": "Spend clave (privado)", - "spend_key_public": "Spend clave (público)", + "spend_key_private": "Llave de gasto (privada)", + "spend_key_public": "Llave de gasto (pública)", "status": "Estado: ", "string_default": "Por defecto", "subaddress_title": "Lista de subdirecciones", @@ -729,14 +730,14 @@ "successful": "Exitoso", "support_description_guides": "Documentación y apoyo para problemas comunes", "support_description_live_chat": "¡GRATIS y RÁPIDO! Los representantes de apoyo capacitado están disponibles para ayudar", - "support_description_other_links": "Únase a nuestras comunidades o comuníquese con nosotros nuestros socios a través de otros métodos", + "support_description_other_links": "Únete a nuestras comunidades o comunícate con nosotros nuestros socios a través de otros métodos", "support_title_guides": "Guías de billetera para pastel", - "support_title_live_chat": "Soporte vital", + "support_title_live_chat": "Soporte en tiempo real", "support_title_other_links": "Otros enlaces de soporte", - "sweeping_wallet": "Billetera de barrido", + "sweeping_wallet": "Barrer billetera (gastar todos los fondos disponibles)", "sweeping_wallet_alert": "Esto no debería llevar mucho tiempo. NO DEJES ESTA PANTALLA O SE PUEDEN PERDER LOS FONDOS BARRIDOS", - "switchToETHWallet": "Cambie a una billetera Ethereum e inténtelo nuevamente.", - "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", + "switchToETHWallet": "Cambia a una billetera Ethereum e inténtelo nuevamente.", + "switchToEVMCompatibleWallet": "Cambia a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", "symbol": "Símbolo", "sync_all_wallets": "Sincronizar todas las billeteras", "sync_status_attempting_scan": "Intento de escaneo", @@ -759,7 +760,7 @@ "third_intro_content": "Los Yats también viven fuera de Cake Wallet. Cualquier dirección de billetera en la tierra se puede reemplazar con un Yat!", "third_intro_title": "Yat juega muy bien con otras", "thorchain_contract_address_not_supported": "Thorchain no admite enviar a una dirección de contrato", - "thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambie la dirección o seleccione un proveedor diferente.", + "thorchain_taproot_address_not_supported": "El proveedor de Thorchain no admite las direcciones de Taproot. Cambia la dirección o selecciona un proveedor diferente.", "time": "${minutes}m ${seconds}s", "tip": "Consejo:", "today": "Hoy", @@ -772,8 +773,8 @@ "tor_only": "solo Tor", "total": "Total", "total_saving": "Ahorro Total", - "totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Use una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.", - "totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerde guardar su semilla mnemotécnica en caso de que pierda el acceso a la billetera.", + "totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Usa una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.", + "totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerda guardar tu semilla mnemotécnica en caso de que pierdas el acceso a la billetera.", "totp_auth_url": "URL de autenticación TOTP", "totp_code": "Código TOTP", "totp_secret_code": "Código secreto TOTP", @@ -826,21 +827,21 @@ "transaction_priority_slow": "Lento", "transaction_sent": "Transacción enviada!", "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", - "transactions": "Actas", + "transactions": "Transacciones", "transactions_by_date": "Transacciones por fecha", "trongrid_history": "Historia trongrid", "trusted": "de confianza", "tx_commit_exception_no_dust_on_change": "La transacción se rechaza con esta cantidad. Con estas monedas puede enviar ${min} sin cambios o ${max} que devuelve el cambio.", - "tx_commit_failed": "La confirmación de transacción falló. Póngase en contacto con el soporte.", - "tx_commit_failed_no_peers": "La transacción no se transmitió, intente nuevamente en un segundo más o menos", + "tx_commit_failed": "La confirmación de transacción falló. Ponte en contacto con el soporte.", + "tx_commit_failed_no_peers": "La transacción no se transmitió, intenta nuevamente en un segundo más o menos", "tx_invalid_input": "Está utilizando el tipo de entrada incorrecto para este tipo de pago", "tx_no_dust_exception": "La transacción se rechaza enviando una cantidad demasiado pequeña. Intente aumentar la cantidad.", - "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Seleccione más bajo control de monedas", + "tx_not_enough_inputs_exception": "No hay suficientes entradas disponibles. Selecciona más bajo control de monedas", "tx_rejected_bip68_final": "La transacción tiene entradas no confirmadas y no ha podido reemplazar por tarifa.", - "tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intente enviar todo o reducir la cantidad.", - "tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumente la cantidad.", + "tx_rejected_dust_change": "Transacción rechazada por reglas de red, bajo cambio de cambio (polvo). Intenta enviar todo o reducir la cantidad.", + "tx_rejected_dust_output": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Aumenta la cantidad.", "tx_rejected_dust_output_send_all": "Transacción rechazada por reglas de red, baja cantidad de salida (polvo). Verifique el saldo de monedas seleccionadas bajo control de monedas.", - "tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifique el saldo de monedas bajo control de monedas.", + "tx_rejected_vout_negative": "No es suficiente saldo para pagar las tarifas de esta transacción. Verifica el saldo de monedas bajo control de monedas.", "tx_wrong_balance_exception": "No tiene suficiente ${currency} para enviar esta cantidad.", "tx_wrong_balance_with_amount_exception": "No tiene suficiente ${currency} para enviar la cantidad total de ${amount}", "tx_zero_fee_exception": "No se puede enviar transacciones con 0 tarifa. Intente aumentar la tasa o verificar su conexión para las últimas estimaciones.", @@ -849,7 +850,7 @@ "unconfirmed": "Saldo no confirmado", "understand": "Entiendo", "unlock": "desbloquear", - "unmatched_currencies": "La moneda de su billetera actual no coincide con la del QR escaneado", + "unmatched_currencies": "La moneda de tu billetera actual no coincide con la del QR escaneado", "unspent_change": "Cambiar", "unspent_coins_details_title": "Detalles de monedas no gastadas", "unspent_coins_title": "Monedas no gastadas", @@ -858,11 +859,11 @@ "upto": "hasta ${value}", "usb": "USB", "use": "Utilizar a ", - "use_card_info_three": "Utilice la tarjeta digital en línea o con métodos de pago sin contacto.", + "use_card_info_three": "Utiliza la tarjeta digital en línea o con métodos de pago sin contacto.", "use_card_info_two": "Los fondos se convierten a USD cuando se mantienen en la cuenta prepaga, no en monedas digitales.", - "use_ssl": "Utilice SSL", + "use_ssl": "Utiliza SSL", "use_suggested": "Usar sugerido", - "use_testnet": "Use TestNet", + "use_testnet": "Usar TestNet", "value": "Valor", "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", @@ -871,17 +872,17 @@ "verify_with_2fa": "Verificar con Cake 2FA", "version": "Versión ${currentVersion}", "view_all": "Ver todo", - "view_in_block_explorer": "View in Block Explorer", - "view_key_private": "View clave (privado)", - "view_key_public": "View clave (público)", - "view_transaction_on": "View Transaction on ", + "view_in_block_explorer": "Ver en explorador de bloques", + "view_key_private": "Llave de vista (privada)", + "view_key_public": "Llave de vista (pública)", + "view_transaction_on": "Ver transacción en ", "voting_weight": "Peso de votación", - "waitFewSecondForTxUpdate": "Espere unos segundos para que la transacción se refleje en el historial de transacciones.", + "waitFewSecondForTxUpdate": "Espera unos segundos para que la transacción se refleje en el historial de transacciones.", "wallet_group": "Grupo de billetera", - "wallet_group_description_four": "para crear una billetera con una semilla completamente nueva.", + "wallet_group_description_four": "Para crear una billetera con una semilla completamente nueva.", "wallet_group_description_one": "En la billetera de pastel, puedes crear un", "wallet_group_description_three": "Para ver las billeteras disponibles y/o la pantalla de grupos de billeteras. O elegir", - "wallet_group_description_two": "seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puede seleccionar", + "wallet_group_description_two": "Seleccionando una billetera existente para compartir una semilla con. Cada grupo de billetera puede contener una sola billetera de cada tipo de moneda. \n\n puedes seleccionar", "wallet_group_empty_state_text_one": "Parece que no tienes ningún grupo de billetera compatible !\n\n toque", "wallet_group_empty_state_text_two": "a continuación para hacer uno nuevo.", "wallet_keys": "Billetera semilla/claves", @@ -889,7 +890,7 @@ "wallet_list_edit_group_name": "Editar nombre de grupo", "wallet_list_edit_wallet": "Editar billetera", "wallet_list_failed_to_load": "No se pudo cargar ${wallet_name} la billetera. ${error}", - "wallet_list_failed_to_remove": "Error al elimina ${wallet_name} billetera. ${error}", + "wallet_list_failed_to_remove": "Error al eliminar ${wallet_name} billetera. ${error}", "wallet_list_load_wallet": "Billetera de carga", "wallet_list_loading_wallet": "Billetera ${wallet_name} de carga", "wallet_list_removing_wallet": "Retirar ${wallet_name} billetera", @@ -898,8 +899,8 @@ "wallet_list_wallet_name": "Nombre de la billetera", "wallet_menu": "Menú de billetera", "wallet_name": "Nombre de la billetera", - "wallet_name_exists": "Wallet con ese nombre ya ha existido", - "wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no debe estar vacía", + "wallet_name_exists": "Cartera con ese nombre ya existe, escoge otro", + "wallet_password_is_empty": "La contraseña de billetera está vacía. La contraseña de la billetera no puede estar vacía", "wallet_recovery_height": "Altura de recuperación", "wallet_restoration_store_incorrect_seed_length": "Longitud de semilla incorrecta", "wallet_seed": "Semilla de billetera", @@ -913,30 +914,30 @@ "what_is_silent_payments": "¿Qué son los pagos silenciosos?", "widgets_address": "Dirección", "widgets_or": "o", - "widgets_restore_from_blockheight": "Restaurar desde blockheight", + "widgets_restore_from_blockheight": "Restaurar desde altura de bloque", "widgets_restore_from_date": "Restaurar desde fecha", "widgets_seed": "Semilla", "wouoldLikeToConnect": "quisiera conectar", - "write_down_backup_password": "Escriba su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.", - "xlm_extra_info": "No olvide especificar el ID de nota al enviar la transacción XLM para el intercambio", + "write_down_backup_password": "Escribe su contraseña de respaldo, que se utiliza para la importación de sus archivos de respaldo.", + "xlm_extra_info": "No olvides especificar el ID de nota al enviar la transacción XLM para el intercambio", "xmr_available_balance": "Saldo disponible", "xmr_full_balance": "Balance total", "xmr_hidden": "Oculto", "xmr_to_error": "Error de XMR.TO", "xmr_to_error_description": "Monto invalido. Límite máximo de 8 dígitos después del punto decimal", - "xrp_extra_info": "No olvide especificar la etiqueta de destino al enviar la transacción XRP para el intercambio", + "xrp_extra_info": "No olvides especificar la etiqueta de destino al enviar la transacción XRP para el intercambio", "yat": "Yat", "yat_address": "Dirección de Yat", "yat_alert_content": "Los usuarios de Cake Wallet ahora pueden enviar y recibir todas sus monedas favoritas con un nombre de usuario único basado en emoji.", - "yat_alert_title": "Envíe y reciba criptomonedas más fácilmente con Yat", + "yat_alert_title": "Envía y recibe criptomonedas más fácilmente con Yat", "yat_error": "Error de Yat", "yat_error_content": "No hay direcciones vinculadas con este Yat. Prueba con otro Yat", - "yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administre Yats en cualquier momento en la pantalla de configuración", - "yat_popup_title": "La dirección de su billetera se puede emojificar.", + "yat_popup_content": "Ahora puede enviar y recibir criptografía en Cake Wallet con su Yat, un nombre de usuario corto basado en emoji. Administra Yats en cualquier momento en la pantalla de configuración", + "yat_popup_title": "La dirección de tu billetera se puede emojificar.", "yesterday": "Ayer", - "you_now_have_debit_card": "Ahora tiene una tarjeta de débito", + "you_now_have_debit_card": "Ahora tienes una tarjeta de débito", "you_pay": "Tú pagas", "you_will_get": "Convertir a", "you_will_send": "Convertir de", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index be5b48dd8..a68eecc40 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Définir les paiements silencieux toujours à la scanne", "silent_payments_disclaimer": "Les nouvelles adresses ne sont pas de nouvelles identités. Il s'agit d'une réutilisation d'une identité existante avec une étiquette différente.", "silent_payments_display_card": "Afficher la carte de paiement silencieuse", + "silent_payments_register_key": "Enregistrez la touche Afficher pour une analyse plus rapide", "silent_payments_scan_from_date": "Analyser à partir de la date", "silent_payments_scan_from_date_or_blockheight": "Veuillez saisir la hauteur du bloc que vous souhaitez commencer à scanner pour les paiements silencieux entrants, ou utilisez la date à la place. Vous pouvez choisir si le portefeuille continue de numériser chaque bloc ou ne vérifie que la hauteur spécifiée.", "silent_payments_scan_from_height": "Scan à partir de la hauteur du bloc", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4deb0df1d..809db1727 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Saita biya na shiru koyaushe", "silent_payments_disclaimer": "Sabbin adiresoshin ba sabon tsari bane. Wannan shine sake amfani da asalin asalin tare da wata alama daban.", "silent_payments_display_card": "Nuna katin silent", + "silent_payments_register_key": "Yi rijista mabuɗin don bincika sauri", "silent_payments_scan_from_date": "Scan daga kwanan wata", "silent_payments_scan_from_date_or_blockheight": "Da fatan za a shigar da toshe wurin da kake son fara bincika don biyan silins mai shigowa, ko, yi amfani da kwanan wata. Zaka iya zabar idan walat ɗin ya ci gaba da bincika kowane toshe, ko duba tsinkaye da aka ƙayyade.", "silent_payments_scan_from_height": "Scan daga tsayin daka", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 5161250fc..84c1afd6d 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "मूक भुगतान हमेशा स्कैनिंग सेट करें", "silent_payments_disclaimer": "नए पते नई पहचान नहीं हैं। यह एक अलग लेबल के साथ एक मौजूदा पहचान का पुन: उपयोग है।", "silent_payments_display_card": "मूक भुगतान कार्ड दिखाएं", + "silent_payments_register_key": "तेजी से स्कैनिंग के लिए रजिस्टर व्यू कुंजी", "silent_payments_scan_from_date": "तिथि से स्कैन करना", "silent_payments_scan_from_date_or_blockheight": "कृपया उस ब्लॉक ऊंचाई दर्ज करें जिसे आप आने वाले मूक भुगतान के लिए स्कैन करना शुरू करना चाहते हैं, या, इसके बजाय तारीख का उपयोग करें। आप चुन सकते हैं कि क्या वॉलेट हर ब्लॉक को स्कैन करना जारी रखता है, या केवल निर्दिष्ट ऊंचाई की जांच करता है।", "silent_payments_scan_from_height": "ब्लॉक ऊंचाई से स्कैन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8ef92aaf0..6c08955a8 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Postavite tiho plaćanje uvijek skeniranje", "silent_payments_disclaimer": "Nove adrese nisu novi identiteti. To je ponovna upotreba postojećeg identiteta s drugom oznakom.", "silent_payments_display_card": "Prikaži karticu tihih plaćanja", + "silent_payments_register_key": "Registrirajte ključ za brže skeniranje", "silent_payments_scan_from_date": "Skeniranje iz datuma", "silent_payments_scan_from_date_or_blockheight": "Unesite visinu bloka koju želite započeti skeniranje za dolazna tiha plaćanja ili umjesto toga upotrijebite datum. Možete odabrati da li novčanik nastavlja skenirati svaki blok ili provjerava samo navedenu visinu.", "silent_payments_scan_from_height": "Skeniranje s visine bloka", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 40ed1e116..f3f29721e 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -701,6 +701,7 @@ "silent_payments_always_scan": "Միացնել Լուռ Վճարումներ մշտական սկանավորումը", "silent_payments_disclaimer": "Նոր հասցեները նոր ինքնություն չեն։ Դա այլ պիտակով գոյություն ունեցող ինքնության վերագործածում է", "silent_payments_display_card": "Ցուցադրել Լուռ Վճարումներ քարտը", + "silent_payments_register_key": "Գրանցեք Դիտել ստեղնը `ավելի արագ սկանավորման համար", "silent_payments_scan_from_date": "Սկանավորել ամսաթվից", "silent_payments_scan_from_date_or_blockheight": "Խնդրում ենք մուտքագրել բլոկի բարձրությունը, որտեղից դուք ցանկանում եք սկսել սկանավորել մուտքային Լուռ Վճարումները կամ տեղափոխել ամսաթվի փոխարեն։ Դուք կարող եք ընտրել, արդյոք դրամապանակը շարունակելու է սկանավորել ամեն բլոկ կամ ստուգել միայն սահմանված բարձրությունը", "silent_payments_scan_from_height": "Բլոկի բարձրությունից սկանավորել", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5f93082ec..1aa071753 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -712,6 +712,7 @@ "silent_payments_always_scan": "Tetapkan pembayaran diam selalu pemindaian", "silent_payments_disclaimer": "Alamat baru bukanlah identitas baru. Ini adalah penggunaan kembali identitas yang ada dengan label yang berbeda.", "silent_payments_display_card": "Tunjukkan kartu pembayaran diam", + "silent_payments_register_key": "Daftar Kunci Lihat untuk pemindaian yang lebih cepat", "silent_payments_scan_from_date": "Pindai dari tanggal", "silent_payments_scan_from_date_or_blockheight": "Harap masukkan ketinggian blok yang ingin Anda mulai pemindaian untuk pembayaran diam yang masuk, atau, gunakan tanggal sebagai gantinya. Anda dapat memilih jika dompet terus memindai setiap blok, atau memeriksa hanya ketinggian yang ditentukan.", "silent_payments_scan_from_height": "Pindai dari Tinggi Blok", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 08ae928af..13133c297 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Impostare i pagamenti silenziosi che scansionano sempre", "silent_payments_disclaimer": "I nuovi indirizzi non sono nuove identità. È un riutilizzo di un'identità esistente con un'etichetta diversa.", "silent_payments_display_card": "Mostra la carta di pagamenti silenziosi", + "silent_payments_register_key": "Registra la chiave di visualizzazione per una scansione più veloce", "silent_payments_scan_from_date": "Scansionare dalla data", "silent_payments_scan_from_date_or_blockheight": "Inserisci l'altezza del blocco che si desidera iniziare la scansione per i pagamenti silenziosi in arrivo o, utilizza invece la data. Puoi scegliere se il portafoglio continua a scansionare ogni blocco o controlla solo l'altezza specificata.", "silent_payments_scan_from_height": "Scansione dall'altezza del blocco", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d70eca31b..331057e23 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "サイレント決済を常にスキャンします", "silent_payments_disclaimer": "新しいアドレスは新しいアイデンティティではありません。これは、異なるラベルを持つ既存のアイデンティティの再利用です。", "silent_payments_display_card": "サイレントペイメントカードを表示します", + "silent_payments_register_key": "登録キーを登録して、より速いスキャンを行います", "silent_payments_scan_from_date": "日付からスキャンします", "silent_payments_scan_from_date_or_blockheight": "着信のサイレント決済のためにスキャンを開始するブロックの高さを入力するか、代わりに日付を使用してください。ウォレットがすべてのブロックをスキャンし続けるか、指定された高さのみをチェックするかどうかを選択できます。", "silent_payments_scan_from_height": "ブロックの高さからスキャンします", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 133ca1838..542998ebe 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "무음금을 항상 스캔합니다", "silent_payments_disclaimer": "새로운 주소는 새로운 정체성이 아닙니다. 다른 레이블로 기존 신원을 재사용하는 것입니다.", "silent_payments_display_card": "사일런트 지불 카드 표시", + "silent_payments_register_key": "더 빠른 스캔을 위해보기 키 등록 키", "silent_payments_scan_from_date": "날짜부터 스캔하십시오", "silent_payments_scan_from_date_or_blockheight": "들어오는 사일런트 결제를 위해 스캔을 시작하려는 블록 높이를 입력하거나 대신 날짜를 사용하십시오. 지갑이 모든 블록을 계속 스캔하는지 여부를 선택하거나 지정된 높이 만 확인할 수 있습니다.", "silent_payments_scan_from_height": "블록 높이에서 스캔하십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1727f0d71..b64615a56 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "အမြဲတမ်း scanning အမြဲ scanning", "silent_payments_disclaimer": "လိပ်စာအသစ်များသည်အထောက်အထားအသစ်များမဟုတ်ပါ။ ၎င်းသည်ကွဲပြားခြားနားသောတံဆိပ်ဖြင့်ရှိပြီးသားဝိသေသလက်ခဏာကိုပြန်လည်အသုံးပြုခြင်းဖြစ်သည်။", "silent_payments_display_card": "အသံတိတ်ငွေပေးချေမှုကဒ်ကိုပြပါ", + "silent_payments_register_key": "ပိုမိုမြန်ဆန်သောစကင်ဖတ်စစ်ဆေးရန်အတွက်ကြည့်ပါ", "silent_payments_scan_from_date": "ရက်စွဲမှစကင်ဖတ်ပါ", "silent_payments_scan_from_date_or_blockheight": "ကျေးဇူးပြု. သင်ဝင်လာသောအသံတိတ်ငွေပေးချေမှုအတွက်သင်စကင်ဖတ်စစ်ဆေးလိုသည့်အမြင့်ကိုဖြည့်ပါ။ သို့မဟုတ်နေ့စွဲကိုသုံးပါ။ Wallet သည်လုပ်ကွက်တိုင်းကိုဆက်လက်စကင်ဖတ်စစ်ဆေးပါကသို့မဟုတ်သတ်မှတ်ထားသောအမြင့်ကိုသာစစ်ဆေးပါကသင်ရွေးချယ်နိုင်သည်။", "silent_payments_scan_from_height": "ပိတ်ပင်တားဆီးမှုအမြင့်ကနေ scan", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3f2df531b..d732ac410 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Stel stille betalingen in het scannen", "silent_payments_disclaimer": "Nieuwe adressen zijn geen nieuwe identiteiten. Het is een hergebruik van een bestaande identiteit met een ander label.", "silent_payments_display_card": "Toon stille betalingskaart", + "silent_payments_register_key": "Registerweergave Key voor sneller scannen", "silent_payments_scan_from_date": "Scan vanaf datum", "silent_payments_scan_from_date_or_blockheight": "Voer de blokhoogte in die u wilt beginnen met scannen op inkomende stille betalingen, of gebruik in plaats daarvan de datum. U kunt kiezen of de portemonnee elk blok blijft scannen of alleen de opgegeven hoogte controleert.", "silent_payments_scan_from_height": "Scan van blokhoogte", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 91b265144..5c82dcdc7 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Ustaw ciche płatności zawsze skanowanie", "silent_payments_disclaimer": "Nowe adresy nie są nową tożsamością. Jest to ponowne wykorzystanie istniejącej tożsamości z inną etykietą.", "silent_payments_display_card": "Pokaż kartę Silent Payments", + "silent_payments_register_key": "Zarejestruj się Wyświetl Klucz do szybszego skanowania", "silent_payments_scan_from_date": "Skanuj z daty", "silent_payments_scan_from_date_or_blockheight": "Wprowadź wysokość bloku, którą chcesz rozpocząć skanowanie w poszukiwaniu cichej płatności lub zamiast tego skorzystaj z daty. Możesz wybrać, czy portfel kontynuuje skanowanie każdego bloku, lub sprawdza tylko określoną wysokość.", "silent_payments_scan_from_height": "Skanuj z wysokości bloku", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 524dbcace..4f222c2ed 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -369,7 +369,7 @@ "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", - "litecoin_mweb_dismiss": "Liberar", + "litecoin_mweb_dismiss": "Ocultar", "litecoin_mweb_display_card": "Mostre o cartão MWEB", "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", "litecoin_mweb_pegin": "Peg in", @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Defina pagamentos silenciosos sempre escaneando", "silent_payments_disclaimer": "Novos endereços não são novas identidades. É uma reutilização de uma identidade existente com um rótulo diferente.", "silent_payments_display_card": "Mostrar cartão de pagamento silencioso", + "silent_payments_register_key": "Chave de exibição de registro para digitalização mais rápida", "silent_payments_scan_from_date": "Escanear a partir da data", "silent_payments_scan_from_date_or_blockheight": "Por favor, insira a altura do bloco que deseja iniciar o escaneamento para obter pagamentos silenciosos ou use a data. Você pode escolher se a carteira continua escaneando cada bloco ou verifica apenas a altura especificada.", "silent_payments_scan_from_height": "Escanear a partir da altura do bloco", @@ -941,4 +942,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} \ No newline at end of file +} diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1a8c2447f..f172d4390 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Установить молчаливые платежи всегда сканирование", "silent_payments_disclaimer": "Новые адреса не являются новыми личностями. Это повторное использование существующей идентичности с другой этикеткой.", "silent_payments_display_card": "Показать бесшумную платежную карту", + "silent_payments_register_key": "Зарегистрируйте ключ просмотра для более быстрого сканирования", "silent_payments_scan_from_date": "Сканирование с даты", "silent_payments_scan_from_date_or_blockheight": "Пожалуйста, введите высоту блока, которую вы хотите начать сканирование для входящих молчаливых платежей, или вместо этого используйте дату. Вы можете выбрать, продолжает ли кошелек сканировать каждый блок или проверять только указанную высоту.", "silent_payments_scan_from_height": "Сканирование с высоты блока", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 213f74530..a93411085 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "ตั้งค่าการชำระเงินแบบเงียบเสมอ", "silent_payments_disclaimer": "ที่อยู่ใหม่ไม่ใช่ตัวตนใหม่ มันเป็นการใช้ซ้ำของตัวตนที่มีอยู่ด้วยฉลากที่แตกต่างกัน", "silent_payments_display_card": "แสดงบัตร Silent Payments", + "silent_payments_register_key": "ลงทะเบียนคีย์มุมมองสำหรับการสแกนที่เร็วขึ้น", "silent_payments_scan_from_date": "สแกนตั้งแต่วันที่", "silent_payments_scan_from_date_or_blockheight": "โปรดป้อนความสูงของบล็อกที่คุณต้องการเริ่มการสแกนสำหรับการชำระเงินแบบเงียบ ๆ หรือใช้วันที่แทน คุณสามารถเลือกได้ว่ากระเป๋าเงินยังคงสแกนทุกบล็อกหรือตรวจสอบความสูงที่ระบุเท่านั้น", "silent_payments_scan_from_height": "สแกนจากความสูงของบล็อก", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 0ca8ee665..b8c4af9d2 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag-scan", "silent_payments_disclaimer": "Ang mga bagong address ay hindi mga bagong pagkakakilanlan. Ito ay isang muling paggamit ng isang umiiral na pagkakakilanlan na may ibang label.", "silent_payments_display_card": "Ipakita ang Silent Payment Card", + "silent_payments_register_key": "Magrehistro ng View Key para sa mas mabilis na pag -scan", "silent_payments_scan_from_date": "I-scan mula sa petsa", "silent_payments_scan_from_date_or_blockheight": "Mangyaring ipasok ang block height na gusto mong simulan ang pag-scan para sa papasok na tahimik na pagbabayad, o, gamitin ang petsa sa halip. Maaari kang pumili kung ang wallet ay patuloy na pag-scan sa bawat bloke, o suriin lamang ang tinukoy na taas.", "silent_payments_scan_from_height": "I-scan mula sa block height", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b23f64d60..2b44b6306 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Sessiz ödemeleri her zaman tarama ayarlayın", "silent_payments_disclaimer": "Yeni adresler yeni kimlikler değildir. Farklı bir etikete sahip mevcut bir kimliğin yeniden kullanımıdır.", "silent_payments_display_card": "Sessiz Ödeme Kartı Göster", + "silent_payments_register_key": "Daha hızlı tarama için tuşunu kaydet", "silent_payments_scan_from_date": "Tarihten tarama", "silent_payments_scan_from_date_or_blockheight": "Lütfen gelen sessiz ödemeler için taramaya başlamak istediğiniz blok yüksekliğini girin veya bunun yerine tarihi kullanın. Cüzdanın her bloğu taramaya devam edip etmediğini veya yalnızca belirtilen yüksekliği kontrol edip etmediğini seçebilirsiniz.", "silent_payments_scan_from_height": "Blok yüksekliğinden tarama", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 79dc0543f..d5a82293e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Встановити мовчазні платежі завжди сканувати", "silent_payments_disclaimer": "Нові адреси - це не нові ідентичності. Це повторне використання існуючої ідентичності з іншою етикеткою.", "silent_payments_display_card": "Покажіть безшумну карту платежів", + "silent_payments_register_key": "Зареєструйтесь ключ для більш швидкого сканування", "silent_payments_scan_from_date": "Сканувати з дати", "silent_payments_scan_from_date_or_blockheight": "Введіть висоту блоку, яку ви хочете почати сканувати для вхідних мовчазних платежів, або скористайтеся датою замість цього. Ви можете вибрати, якщо гаманець продовжує сканувати кожен блок, або перевіряє лише вказану висоту.", "silent_payments_scan_from_height": "Сканування від висоти блоку", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0a136d140..84a8bb355 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "خاموش ادائیگی ہمیشہ اسکیننگ کریں", "silent_payments_disclaimer": "نئے پتے نئی شناخت نہیں ہیں۔ یہ ایک مختلف لیبل کے ساتھ موجودہ شناخت کا دوبارہ استعمال ہے۔", "silent_payments_display_card": "خاموش ادائیگی کارڈ دکھائیں", + "silent_payments_register_key": "تیزی سے اسکیننگ کے لئے کلید کو رجسٹر کریں", "silent_payments_scan_from_date": "تاریخ سے اسکین کریں", "silent_payments_scan_from_date_or_blockheight": "براہ کرم بلاک اونچائی میں داخل ہوں جس سے آپ آنے والی خاموش ادائیگیوں کے لئے اسکیننگ شروع کرنا چاہتے ہیں ، یا اس کے بجائے تاریخ کا استعمال کریں۔ آپ یہ منتخب کرسکتے ہیں کہ اگر پرس ہر بلاک کو اسکیننگ جاری رکھے ہوئے ہے ، یا صرف مخصوص اونچائی کی جانچ پڑتال کرتا ہے۔", "silent_payments_scan_from_height": "بلاک اونچائی سے اسکین کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 8d28d48a2..6b6f0dd50 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -702,6 +702,7 @@ "silent_payments_always_scan": "Đặt Thanh toán im lặng luôn quét", "silent_payments_disclaimer": "Địa chỉ mới không phải là danh tính mới. Đây là việc tái sử dụng một danh tính hiện có với nhãn khác.", "silent_payments_display_card": "Hiển thị thẻ Thanh toán im lặng", + "silent_payments_register_key": "Đăng ký khóa xem để quét nhanh hơn", "silent_payments_scan_from_date": "Quét từ ngày", "silent_payments_scan_from_date_or_blockheight": "Vui lòng nhập chiều cao khối bạn muốn bắt đầu quét cho các thanh toán im lặng đến, hoặc, sử dụng ngày thay thế. Bạn có thể chọn nếu ví tiếp tục quét mỗi khối, hoặc chỉ kiểm tra chiều cao đã chỉ định.", "silent_payments_scan_from_height": "Quét từ chiều cao khối", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 14270120c..c45acfe7f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Ṣeto awọn sisanwo ipalọlọ nigbagbogbo n ṣatunṣe", "silent_payments_disclaimer": "Awọn adirẹsi tuntun kii ṣe awọn idanimọ tuntun. O jẹ yiyan ti idanimọ ti o wa pẹlu aami oriṣiriṣi.", "silent_payments_display_card": "Ṣafihan kaadi isanwo ti o dakẹ", + "silent_payments_register_key": "Forukọsilẹ Wo bọtini Window fun Cranding yiyara", "silent_payments_scan_from_date": "Scan lati ọjọ", "silent_payments_scan_from_date_or_blockheight": "Jọwọ tẹ giga idibo ti o fẹ bẹrẹ ọlọjẹ fun awọn sisanwo ipalọlọ, tabi, lo ọjọ dipo. O le yan ti apamọwọ naa tẹsiwaju nṣapẹẹrẹ gbogbo bulọọki, tabi ṣayẹwo nikan giga ti o sọ tẹlẹ.", "silent_payments_scan_from_height": "Scan lati Iga Iga", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 65047b4fe..cee24ba1b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "设置无声付款总是扫描", "silent_payments_disclaimer": "新地址不是新的身份。这是重复使用具有不同标签的现有身份。", "silent_payments_display_card": "显示无声支付卡", + "silent_payments_register_key": "注册查看密钥以进行更快的扫描", "silent_payments_scan_from_date": "从日期开始扫描", "silent_payments_scan_from_date_or_blockheight": "请输入您要开始扫描输入静音付款的块高度,或者使用日期。您可以选择钱包是否继续扫描每个块,或仅检查指定的高度。", "silent_payments_scan_from_height": "从块高度扫描", diff --git a/scripts/android/app_env.fish b/scripts/android/app_env.fish new file mode 100644 index 000000000..c290a3593 --- /dev/null +++ b/scripts/android/app_env.fish @@ -0,0 +1,78 @@ +#!/usr/bin/env fish + +set APP_ANDROID_NAME "" +set APP_ANDROID_VERSION "" +set APP_ANDROID_BUILD_VERSION "" +set APP_ANDROID_ID "" +set APP_ANDROID_PACKAGE "" +set APP_ANDROID_SCHEME "" + +set MONERO_COM "monero.com" +set CAKEWALLET cakewallet +set HAVEN haven + +set -l TYPES $MONERO_COM $CAKEWALLET $HAVEN +set APP_ANDROID_TYPE $argv[1] + +set MONERO_COM_NAME "Monero.com" +set MONERO_COM_VERSION "1.17.0" +set MONERO_COM_BUILD_NUMBER 103 +set MONERO_COM_BUNDLE_ID "com.monero.app" +set MONERO_COM_PACKAGE "com.monero.app" +set MONERO_COM_SCHEME "monero.com" + +set CAKEWALLET_NAME "Cake Wallet" +set CAKEWALLET_VERSION "4.20.0" +set CAKEWALLET_BUILD_NUMBER 232 +set CAKEWALLET_BUNDLE_ID "com.cakewallet.cake_wallet" +set CAKEWALLET_PACKAGE "com.cakewallet.cake_wallet" +set CAKEWALLET_SCHEME cakewallet + +set HAVEN_NAME Haven +set HAVEN_VERSION "1.0.0" +set HAVEN_BUILD_NUMBER 1 +set HAVEN_BUNDLE_ID "com.cakewallet.haven" +set HAVEN_PACKAGE "com.cakewallet.haven" + +if not contains $APP_ANDROID_TYPE $TYPES + echo "Wrong app type." + return 1 + exit 1 +end + +switch $APP_ANDROID_TYPE + case $MONERO_COM + set APP_ANDROID_NAME $MONERO_COM_NAME + set APP_ANDROID_VERSION $MONERO_COM_VERSION + set APP_ANDROID_BUILD_NUMBER $MONERO_COM_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $MONERO_COM_BUNDLE_ID + set APP_ANDROID_PACKAGE $MONERO_COM_PACKAGE + set APP_ANDROID_SCHEME $MONERO_COM_SCHEME + + case $CAKEWALLET + set APP_ANDROID_NAME $CAKEWALLET_NAME + set APP_ANDROID_VERSION $CAKEWALLET_VERSION + set APP_ANDROID_BUILD_NUMBER $CAKEWALLET_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $CAKEWALLET_BUNDLE_ID + set APP_ANDROID_PACKAGE $CAKEWALLET_PACKAGE + set APP_ANDROID_SCHEME $CAKEWALLET_SCHEME + + case $HAVEN + set APP_ANDROID_NAME $HAVEN_NAME + set APP_ANDROID_VERSION $HAVEN_VERSION + set APP_ANDROID_BUILD_NUMBER $HAVEN_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $HAVEN_BUNDLE_ID + set APP_ANDROID_PACKAGE $HAVEN_PACKAGE + +end + +export APP_ANDROID_TYPE +export APP_ANDROID_NAME +export APP_ANDROID_VERSION +export APP_ANDROID_BUILD_NUMBER +export APP_ANDROID_BUNDLE_ID +export APP_ANDROID_PACKAGE +export APP_ANDROID_SCHEME +export APP_ANDROID_BUNDLE_ID +export APP_ANDROID_PACKAGE +export APP_ANDROID_SCHEME diff --git a/tool/configure.dart b/tool/configure.dart index 97541c2fa..d159bffe1 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -102,7 +102,6 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; const bitcoinCWHeaders = """ -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -115,7 +114,6 @@ import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; @@ -150,8 +148,7 @@ abstract class Bitcoin { required String name, required String mnemonic, required String password, - required DerivationType derivationType, - required String derivationPath, + required List? derivations, String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); @@ -183,8 +180,19 @@ abstract class Bitcoin { List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ); + WalletService createLitecoinWalletService( + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -192,7 +200,7 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPrioritySlow(); Future> compareDerivationMethods( {required String mnemonic, required Node node}); - Future> getDerivationsFromMnemonic( + Future> getDerivationsFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); @@ -216,6 +224,7 @@ abstract class Bitcoin { int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, {int? outputsCount, int? size}); int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); + Future registerSilentPaymentsKey(Object wallet, bool active); Future checkIfMempoolAPIIsEnabled(Object wallet); Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); int getLitecoinHeightByDate({required DateTime date}); @@ -1016,9 +1025,6 @@ abstract class Polygon { Future generateBitcoinCash(bool hasImplementation) async { final outputFile = File(bitcoinCashOutputPath); const bitcoinCashCommonHeaders = """ -import 'dart:typed_data'; - -import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -1036,7 +1042,11 @@ abstract class BitcoinCash { String getCashAddrFormat(String address); WalletService createBitcoinCashWalletService( - Box walletInfoSource, Box unspentCoinSource, bool isDirect); + Box walletInfoSource, + Box unspentCoinSource, + bool isDirect, + bool mempoolAPIEnabled, + ); WalletCredentials createBitcoinCashNewWalletCredentials( {required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic, String? parentAddress});