diff --git a/.gitignore b/.gitignore index 970241189f..ac0d42742d 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/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart deleted file mode 100644 index 73bc101c49..0000000000 --- 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 7e4b5f58f0..399cdd0770 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, + required this.addressType, + 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,86 +47,134 @@ abstract class BaseBitcoinAddressRecord { bool get isUsed => _isUsed; - void setAsUsed() => _isUsed = true; + void setAsUsed() { + _isUsed = true; + _isHidden = true; + } + void setNewName(String label) => _name = label; int get hashCode => address.hashCode; - BitcoinAddressType type; + BitcoinAddressType addressType; String toJSON(); } 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, + required super.addressType, 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? ?? '', balance: decoded['balance'] as int? ?? 0, - type: decoded['type'] != null && decoded['type'] != '' + addressType: decoded['type'] != null && decoded['type'] != '' ? BitcoinAddressType.values .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, 'balance': balance, - 'type': type.toString(), + 'type': addressType.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.addressType == addressType && + other.derivationType == derivationType; + } + + @override + int get hashCode => + address.hashCode ^ + index.hashCode ^ + derivationInfo.hashCode ^ + scriptHash.hashCode ^ + addressType.hashCode ^ + derivationType.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { + final String derivationPath; + 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, + this.derivationPath = BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, - required this.silentPaymentTweak, - required super.network, - required super.type, - }) : super(); + super.addressType = 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 +182,72 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, + derivationPath: decoded['derivationPath'] as String, + 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, + 'derivationPath': derivationPath, + 'labelIndex': labelIndex, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': addressType.toString(), + '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.addressType = 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? ?? 1, + 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: (decoded['spendKey'] as String?) == null + ? ECPrivate.random() + : ECPrivate.fromHex(decoded['spendKey'] as String), + ); + } @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, + 'type': addressType.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 d5a42d984b..0000000000 --- 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 a02c51c69b..15ab4f6c13 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 address = generateP2WPKHAddress( - hd: hd, index: 0, network: BitcoinNetwork.mainnet); + final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub) + .childKey(Bip32KeyIndex(0)) + .childKey(Bip32KeyIndex(index)); + + final address = ECPublic.fromBip32( + hd.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 01e905fb0d..f6d769735b 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 d1f45a5452..26a4c2f626 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 LitecoinTransactionPriority extends ElectrumTransactionPriority { + const LitecoinTransactionPriority({required super.title, required super.raw}); + + @override + String get units => 'lit'; } -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 BitcoinCashTransactionPriority extends ElectrumTransactionPriority { + const BitcoinCashTransactionPriority({required super.title, required super.raw}); - static BitcoinCashTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return slow; - case 1: - return medium; - case 2: - return fast; + @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: $raw for BitcoinCashTransactionPriority deserialize'); + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } } @override - String get units => 'Satoshi'; + 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 - String toString() { - var label = ''; + Map toJson() { + return { + 'unimportant': unimportant, + 'normal': normal, + 'elevated': elevated, + 'priority': priority, + 'custom': custom, + }; + } - 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; + 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 ElectrumTransactionPriority.medium: + return medium; + case ElectrumTransactionPriority.fast: + return fast; + case ElectrumTransactionPriority.custom: + return custom; default: - break; + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } + } - return label; + @override + String labelWithRate(TransactionPriority priorityType, [int? rate]) { + return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; + } + + factory ElectrumTransactionPriorities.fromList(List list) { + if (list.length != 3) { + throw Exception( + 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); + } + + 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 3691a7a22a..618ce8f0f0 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,22 +1,36 @@ 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.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, - ); + factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) => + BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout); + + factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) { + final addressType = json['address_runtimetype'] as String?; + final addressRecord = json['address_record'].toString(); + + return BitcoinUnspent( + address ?? + (addressType == null + ? BitcoinAddressRecord.fromJSON(addressRecord) + : addressType.contains("SP") + ? BitcoinReceivedSPAddressRecord.fromJSON(addressRecord) + : BitcoinSilentPaymentAddressRecord.fromJSON(addressRecord)), + json['tx_hash'] as String, + int.parse(json['value'].toString()), + int.parse(json['tx_pos'].toString()), + ); + } Map toJson() { final json = { 'address_record': bitcoinAddressRecord.toJSON(), + 'address_runtimetype': bitcoinAddressRecord.runtimeType.toString(), 'tx_hash': hash, 'value': value, 'tx_pos': vout, @@ -25,43 +39,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 9088978459..653faddf45 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,11 +1,15 @@ +import 'dart:async'; import 'dart:convert'; -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,10 +17,14 @@ 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:flutter/foundation.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'; @@ -32,7 +40,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, - Uint8List? seedBytes, + List? seedBytes, String? mnemonic, String? xpub, String? addressPageType, @@ -45,6 +53,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, + required bool mempoolAPIEnabled, + super.hdWallets, + super.initialUnspentCoins, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -61,34 +72,22 @@ 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, - initialRegularAddressIndex: initialRegularAddressIndex, - 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 +106,63 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, + required bool mempoolAPIEnabled, }) async { - late Uint8List seedBytes; + List? seedBytes = null; + final Map hdWallets = {}; - switch (walletInfo.derivationInfo?.derivationType) { - case DerivationType.bip39: - seedBytes = await bip39.mnemonicToSeed( - mnemonic, - passphrase: passphrase ?? "", - ); - break; - case DerivationType.electrum: - default: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); - break; + if (walletInfo.isRecovery) { + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.description?.contains("SP") ?? false) { + continue; + } + + if (derivation.derivationType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + + break; + } 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; + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } + } else { + switch (walletInfo.derivationInfo?.derivationType) { + case DerivationType.bip39: + seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + break; + case DerivationType.electrum: + default: + seedBytes = await ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + break; + } } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -134,10 +175,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, seedBytes: seedBytes, + hdWallets: hdWallets, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, + mempoolAPIEnabled: mempoolAPIEnabled, + initialUnspentCoins: [], ); } @@ -148,6 +192,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,27 +234,51 @@ 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.description?.contains("SP") ?? false) { + continue; + } + + 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; + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; } } @@ -231,9 +300,74 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: hdWallets, + initialUnspentCoins: snp?.unspentCoins ?? [], ); } + Future getNodeIsElectrs() async { + if (node?.uri.host.contains("electrs") ?? false) { + return true; + } + + final version = await sendWorker(ElectrumWorkerGetVersionRequest()); + + if (version is List && version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + } else if (version is String && version.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + + node!.isElectrs = false; + node!.save(); + return node!.isElectrs!; + } + + Future getNodeSupportsSilentPayments() async { + // TODO: handle disconnection on check + // TODO: use cached values + if (node == null) { + return false; + } + + final isFulcrum = node!.uri.host.contains("fulcrum"); + if (isFulcrum) { + return false; + } + + // As of today (august 2024), only ElectrumRS supports silent payments + if (!(await getNodeIsElectrs())) { + return false; + } + + try { + final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String; + final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson( + json.decode(workerResponse) as Map, + ); + final supportsScanning = tweaksResponse.result == true; + + if (supportsScanning) { + node!.supportsSilentPayments = true; + node!.save(); + return node!.supportsSilentPayments!; + } + } catch (_) {} + + node!.supportsSilentPayments = false; + node!.save(); + return node!.supportsSilentPayments!; + } + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; @@ -261,9 +395,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 +408,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 +419,472 @@ 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 if (syncStatus is! SyncedSyncStatus) { + await sendWorker(ElectrumWorkerStopScanningRequest()); + await startSync(); + } + } + + @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!); + } + }); + + unspentCoins.addAll(updatedUnspentCoins); + + await super.updateAllUnspents(); + + final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; + + walletAddresses.silentPaymentAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); + walletAddresses.receivedSPAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); + + final silentPaymentWallet = walletAddresses.silentPaymentWallet; + + unspentCoins.forEach((unspent) { + if (unspent.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) { + _updateSilentAddressRecord(unspent); + + final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; + final silentPaymentAddress = SilentPaymentAddress( + version: silentPaymentWallet!.version, + B_scan: silentPaymentWallet.B_scan, + B_spend: receiveAddressRecord.labelHex != null + ? silentPaymentWallet.B_spend.tweakAdd( + BigintUtils.fromBytes( + BytesUtils.fromHexString(receiveAddressRecord.labelHex!), + ), + ) + : silentPaymentWallet.B_spend, + ); + + walletAddresses.silentPaymentAddresses.forEach((addressRecord) { + if (addressRecord.address == silentPaymentAddress.toAddress(network)) { + addressRecord.txCount += 1; + addressRecord.balance += unspent.value; + } + }); + walletAddresses.receivedSPAddresses.forEach((addressRecord) { + if (addressRecord.address == receiveAddressRecord.address) { + addressRecord.txCount += 1; + addressRecord.balance += unspent.value; + } + }); + } + }); + + await walletAddresses.updateAddressesInBox(); + } + + @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 walletAddresses = this.walletAddresses as BitcoinWalletAddresses; + walletAddresses.addReceivedSPAddresses( + [unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord], + ); + } + + @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; + final workerError = messageJson['error'] as String?; + + switch (workerMethod) { + case ElectrumRequestMethods.tweaksSubscribeMethod: + if (workerError != null) { + print(messageJson); + // _onConnectionStatusChange(ConnectionStatus.failed); + break; + } + + final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); + onTweaksSyncResponse(response.result); + break; + } + } + + @action + Future onTweaksSyncResponse(TweaksSyncResponse result) async { + if (result.transactions?.isNotEmpty == true) { + (walletAddresses as BitcoinWalletAddresses).silentPaymentAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); + (walletAddresses as BitcoinWalletAddresses).receivedSPAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); + + 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); + + transactionHistory.addOne(tx); + 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; + + if (newSyncStatus is SyncedSyncStatus) { + silentPaymentsScanningActive = false; + } + } + + final height = result.height; + if (height != null) { + await walletInfo.updateRestoreHeight(height); + } + } + + await save(); + } + + @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(); + + final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; + workerSendPort!.send( + ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData( + silentPaymentsWallets: walletAddresses.silentPaymentWallets, + network: network, + height: height, + chainTip: chainTip, + transactionHistoryIds: transactionHistory.transactions.keys.toList(), + labels: walletAddresses.labels, + labelIndexes: walletAddresses.silentPaymentAddresses + .where((addr) => + addr.addressType == 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!); + } + } + + @override + @action + void syncStatusReaction(SyncStatus syncStatus) { + switch (syncStatus.runtimeType) { + case SyncingSyncStatus: + return; + case SyncedTipSyncStatus: + silentPaymentsScanningActive = false; + + // 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 04a3cae361..2f2f87084a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,7 +1,7 @@ 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/bitcoin_address_record.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 +12,369 @@ 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); + List? initialSilentAddresses, + List? initialReceivedSPAddresses, + }) : silentPaymentAddresses = ObservableList.of( + (initialSilentAddresses ?? []).toSet(), + ), + receivedSPAddresses = ObservableList.of( + (initialReceivedSPAddresses ?? []).toSet(), + ), + super(walletInfo) { + silentPaymentWallet = SilentPaymentOwner.fromBip32(hdWallet); + silentPaymentWallets = [silentPaymentWallet!]; + } + + @observable + SilentPaymentOwner? silentPaymentWallet; + final ObservableList silentPaymentAddresses; + final ObservableList receivedSPAddresses; + + @observable + List silentPaymentWallets = []; + + @observable + String? activeSilentAddress; + + @override + Future init() async { + await generateInitialAddresses(addressType: SegwitAddresType.p2wpkh); + + if (!isHardwareWallet) { + await generateInitialAddresses(addressType: P2pkhAddressType.p2pkh); + await generateInitialAddresses(addressType: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(addressType: SegwitAddresType.p2tr); + await generateInitialAddresses(addressType: SegwitAddresType.p2wsh); + } + + if (silentPaymentAddresses.length == 0) { + Bip32Path? oldSpendPath; + Bip32Path? oldScanPath; + + for (final derivationInfo in walletInfo.derivations ?? []) { + if (derivationInfo.description?.contains("SP") ?? false) { + if (derivationInfo.description?.toLowerCase().contains("spend") ?? false) { + oldSpendPath = Bip32PathParser.parse(derivationInfo.derivationPath ?? ""); + } else if (derivationInfo.description?.toLowerCase().contains("scan") ?? false) { + oldScanPath = Bip32PathParser.parse(derivationInfo.derivationPath ?? ""); + } + } + } + + if (oldSpendPath != null && oldScanPath != null) { + final oldSpendPriv = hdWallet.derive(oldSpendPath).privateKey; + final oldScanPriv = hdWallet.derive(oldScanPath).privateKey; + + final oldSilentPaymentWallet = SilentPaymentOwner( + b_scan: ECPrivate(oldScanPriv), + b_spend: ECPrivate(oldSpendPriv), + B_scan: ECPublic.fromBip32(oldScanPriv.publicKey), + B_spend: ECPublic.fromBip32(oldSpendPriv.publicKey), + version: 0, + ); + silentPaymentWallets.add(oldSilentPaymentWallet); + + silentPaymentAddresses.addAll( + [ + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toString(), + labelIndex: 1, + name: "", + addressType: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + ), + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(oldSilentPaymentWallet.generateLabel(0)), + addressType: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + ), + ], + ); + } + + silentPaymentAddresses.addAll([ + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet.toString(), + labelIndex: 1, + name: "", + addressType: SilentPaymentsAddresType.p2sp, + ), + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), + addressType: SilentPaymentsAddresType.p2sp, + ), + ]); + } + + await updateAddressesInBox(); + } + + @override + @computed + String get address { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + if (activeSilentAddress != null) { + return activeSilentAddress!; + } + + return silentPaymentWallet.toString(); + } + + return super.address; + } + + @override + set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } + + if (addressPageType == SilentPaymentsAddresType.p2sp) { + late BitcoinSilentPaymentAddressRecord selected; + try { + selected = + silentPaymentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + } catch (_) { + selected = silentPaymentAddresses[0]; + } + + if (selected.labelHex != null) { + activeSilentAddress = + silentPaymentWallet!.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); + } else { + activeSilentAddress = silentPaymentWallet.toString(); + } + return; + } + + super.address = addr; + } + + @override + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + final hdWallet = hdWallets[derivationType]!; + + // if (OLD_DERIVATION_TYPES.contains(derivationType)) { + // 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'); + } + } + + @override + @action + BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + final currentSPLabelIndex = silentPaymentAddresses + .where((addressRecord) => addressRecord.addressType != SegwitAddresType.p2tr) + .length - + 1; + + final address = BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toLabeledSilentPaymentAddress(currentSPLabelIndex).toString(), + labelIndex: currentSPLabelIndex, + name: label, + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(currentSPLabelIndex)), + addressType: SilentPaymentsAddresType.p2sp, + ); + + silentPaymentAddresses.add(address); + Future.delayed(Duration.zero, () => updateAddressesByMatch()); + + return address; + } + + return super.generateNewAddress(label: label); + } @override - String getAddress( - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { - if (addressType == P2pkhAddressType.p2pkh) - return generateP2PKHAddress(hd: hd, index: index, network: network); + @action + void addBitcoinAddressTypes() { + super.addBitcoinAddressTypes(); - if (addressType == SegwitAddresType.p2tr) - return generateP2TRAddress(hd: hd, index: index, network: network); + silentPaymentAddresses.forEach((addressRecord) { + if (addressRecord.addressType != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { + return; + } - if (addressType == SegwitAddresType.p2wsh) - return generateP2WSHAddress(hd: hd, index: index, network: network); + if (addressRecord.address != address) { + addressesMap[addressRecord.address] = addressRecord.name.isEmpty + ? "Silent Payments" + ': ${addressRecord.address}' + : "Silent Payments - " + addressRecord.name + ': ${addressRecord.address}'; + } else { + addressesMap[address] = 'Active - Silent Payments' + ': $address'; + } + }); + } + + @override + @action + void updateAddress(String address, String label) { + super.updateAddress(address, label); + + BaseBitcoinAddressRecord? foundAddress; + silentPaymentAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); - if (addressType == P2shAddressType.p2wpkhInP2sh) - return generateP2SHAddress(hd: hd, index: index, network: network); + if (foundAddress != null) { + foundAddress!.setNewName(label); + + final index = + silentPaymentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); + silentPaymentAddresses.remove(foundAddress); + silentPaymentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); + } + } + + @override + @action + void updateAddressesByMatch() { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(silentPaymentAddresses); + return; + } + + super.updateAddressesByMatch(); + } + + @action + void addSilentAddresses(Iterable addresses) { + final addressesSet = this.silentPaymentAddresses.toSet(); + addressesSet.addAll(addresses); + this.silentPaymentAddresses.clear(); + this.silentPaymentAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + + @action + void addReceivedSPAddresses(Iterable addresses) { + final addressesSet = this.receivedSPAddresses.toSet(); + addressesSet.addAll(addresses); + this.receivedSPAddresses.clear(); + this.receivedSPAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + + @action + void deleteSilentPaymentAddress(String address) { + final addressRecord = silentPaymentAddresses.firstWhere((addressRecord) => + addressRecord.addressType == SilentPaymentsAddresType.p2sp && + addressRecord.address == address); + + silentPaymentAddresses.remove(addressRecord); + updateAddressesByMatch(); + } + + Map get labels { + final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); + final labels = {}; + for (int i = 0; i < silentPaymentAddresses.length; i++) { + final silentAddressRecord = silentPaymentAddresses[i]; + final silentPaymentTweak = silentAddressRecord.labelHex; + + if (silentPaymentTweak != null && + SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { + labels[G + .tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak))) + .toHex()] = silentPaymentTweak; + } + } + return labels; + } + + @override + @action + void updateHiddenAddresses() { + super.updateHiddenAddresses(); + this.hiddenAddresses.addAll(silentPaymentAddresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); + } - return generateP2WPKHAddress(hd: hd, index: index, network: network); + Map toJson() { + final json = super.toJson(); + json['silentPaymentAddresses'] = + silentPaymentAddresses.map((address) => address.toJSON()).toList(); + json['receivedSPAddresses'] = receivedSPAddresses.map((address) => address.toJSON()).toList(); + return json; } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index a1b1418b8d..bab72b6251 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 06f2082e43..b310c1db3c 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 df8e141195..356f8c6b1b 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, @@ -319,13 +311,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]); @@ -371,20 +388,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(); - - return [bottom, middle, top]; - } catch (_) { - 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(); + + // return [bottom, middle, top]; + // } catch (_) { + // return []; + // } + // } // https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe // example response: @@ -529,6 +546,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 ebd2f06aea..e55cca3f8c 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 { @@ -34,31 +34,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); + String get formattedAvailableBalance => + 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 b688f097ba..f5d11954a9 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 7a8b3b951f..b5e4bede55 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,18 +10,44 @@ 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, + this.isDateValidated, + }); final BtcTransaction originalTransaction; final List ins; final int? time; + final bool? isDateValidated; 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?, + isDateValidated: data['isDateValidated'] as bool?, + ); + } } class ElectrumTransactionInfo extends TransactionInfo { - List? unspents; + List? unspents; bool isReceivedSilentPayment; + int? time; ElectrumTransactionInfo( this.type, { @@ -37,6 +61,8 @@ class ElectrumTransactionInfo extends TransactionInfo { required bool isPending, bool isReplaced = false, required DateTime date, + required int? time, + bool? isDateValidated, required int confirmations, String? to, this.unspents, @@ -51,9 +77,11 @@ class ElectrumTransactionInfo extends TransactionInfo { this.fee = fee; this.direction = direction; this.date = date; + this.time = time; this.isPending = isPending; this.isReplaced = isReplaced; this.confirmations = confirmations; + this.isDateValidated = isDateValidated; this.to = to; this.additionalInfo = additionalInfo ?? {}; } @@ -64,9 +92,8 @@ class ElectrumTransactionInfo extends TransactionInfo { final id = obj['txid'] as String; final vins = obj['vin'] as List? ?? []; final vout = (obj['vout'] as List? ?? []); - final date = obj['time'] is int - ? DateTime.fromMillisecondsSinceEpoch((obj['time'] as int) * 1000) - : DateTime.now(); + final time = obj['time'] as int?; + final date = time != null ? DateTime.fromMillisecondsSinceEpoch(time * 1000) : DateTime.now(); final confirmations = obj['confirmations'] as int? ?? 0; var direction = TransactionDirection.incoming; var inputsAmount = 0; @@ -77,7 +104,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; @@ -87,7 +115,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) || @@ -98,21 +127,28 @@ class ElectrumTransactionInfo extends TransactionInfo { final fee = inputsAmount - totalOutAmount; - return ElectrumTransactionInfo(type, - id: id, - height: height, - isPending: false, - isReplaced: false, - fee: fee, - direction: direction, - amount: amount, - date: date, - confirmations: confirmations); + return ElectrumTransactionInfo( + type, + id: id, + height: height, + isPending: false, + isReplaced: false, + fee: fee, + direction: direction, + amount: amount, + date: date, + confirmations: confirmations, + time: time, + ); } factory ElectrumTransactionInfo.fromElectrumBundle( - ElectrumTransactionBundle bundle, WalletType type, BasedUtxoNetwork network, - {required Set addresses, int? height}) { + ElectrumTransactionBundle bundle, + WalletType type, + BasedUtxoNetwork network, { + required Set addresses, + int? height, + }) { final date = bundle.time != null ? DateTime.fromMillisecondsSinceEpoch(bundle.time! * 1000) : DateTime.now(); @@ -123,22 +159,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); @@ -174,18 +221,22 @@ class ElectrumTransactionInfo extends TransactionInfo { } final fee = inputAmount - totalOutAmount; - return ElectrumTransactionInfo(type, - id: bundle.originalTransaction.txId(), - height: height, - isPending: bundle.confirmations == 0, - isReplaced: false, - inputAddresses: inputAddresses, - outputAddresses: outputAddresses, - fee: fee, - direction: direction, - amount: amount, - date: date, - confirmations: bundle.confirmations); + return ElectrumTransactionInfo( + type, + id: bundle.originalTransaction.txId(), + height: height, + isPending: bundle.confirmations == 0, + isReplaced: false, + inputAddresses: inputAddresses, + outputAddresses: outputAddresses, + fee: fee, + direction: direction, + amount: amount, + date: date, + confirmations: bundle.confirmations, + time: bundle.time, + isDateValidated: bundle.isDateValidated, + ); } factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { @@ -210,10 +261,11 @@ 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, + time: data['time'] as int?, + isDateValidated: data['isDateValidated'] as bool?, additionalInfo: data['additionalInfo'] as Map?, ); } @@ -224,11 +276,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 @@ -238,19 +290,22 @@ class ElectrumTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); ElectrumTransactionInfo updated(ElectrumTransactionInfo info) { - return ElectrumTransactionInfo(info.type, - id: id, - height: info.height, - amount: info.amount, - fee: info.fee, - direction: direction, - date: date, - isPending: isPending, - isReplaced: isReplaced ?? false, - inputAddresses: inputAddresses, - outputAddresses: outputAddresses, - confirmations: info.confirmations, - additionalInfo: additionalInfo); + return ElectrumTransactionInfo( + info.type, + id: id, + height: info.height, + amount: info.amount, + fee: info.fee, + direction: direction, + date: date, + isPending: isPending, + isReplaced: isReplaced ?? false, + inputAddresses: inputAddresses, + outputAddresses: outputAddresses, + confirmations: info.confirmations, + additionalInfo: additionalInfo, + time: info.time, + ); } Map toJson() { @@ -260,6 +315,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['amount'] = amount; m['direction'] = direction.index; m['date'] = date.millisecondsSinceEpoch; + m['time'] = time; m['isPending'] = isPending; m['isReplaced'] = isReplaced; m['confirmations'] = confirmations; @@ -270,6 +326,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; m['additionalInfo'] = additionalInfo; + m['isDateValidated'] = isDateValidated; return m; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e58fad00f7..fb6fc92b10 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,26 +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/litecoin_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'; @@ -31,22 +29,16 @@ 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'; @@ -55,30 +47,46 @@ class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; 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, + List initialUnspentCoins = const [], + }) : hdWallets = hdWallets ?? + { + CWBitcoinDerivationType.bip39: getAccountHDWallet( + currency, + network, + seedBytes, + xpub, + walletInfo.derivationInfo, + ) + }, syncStatus = NotConnectedSyncStatus(), _password = password, - _feeRates = [], - _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, - unspentCoins = [], - _scripthashesUpdateSubject = {}, + unspentCoins = BitcoinUnspentCoins.of(initialUnspentCoins), + scripthashesListening = [], balance = ObservableMap.of(currency != null ? { currency: initialBalance ?? @@ -93,7 +101,6 @@ abstract class ElectrumWalletBase this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -101,40 +108,99 @@ abstract class ElectrumWalletBase encryptionFileUtils: encryptionFileUtils, ); - reaction((_) => syncStatus, _syncStatusReaction); + reaction((_) => syncStatus, syncStatusReaction); sharedPrefs.complete(SharedPreferences.getInstance()); } + // 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: 30)); + } 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?; + final responseId = messageJson['id'] as int?; + + if (responseId != null && _responseCompleters.containsKey(responseId)) { + _responseCompleters[responseId]!.complete(message); + _responseCompleters.remove(responseId); + } + + switch (workerMethod) { + case ElectrumWorkerMethods.connectionMethod: + if (workerError != null) { + _onConnectionStatusChange(ConnectionStatus.failed); + break; + } + + 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, - Uint8List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { + 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)); } - 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) { @@ -147,14 +213,13 @@ abstract class ElectrumWalletBase } bool? alwaysScan; + bool mempoolAPIEnabled; + bool _updatingHistories = false; - final Bip32Slip10Secp256k1 accountHD; + final Map hdWallets; + Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; final String? _mnemonic; - Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); - - Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); - final EncryptionFileUtils encryptionFileUtils; @override @@ -164,7 +229,7 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - late ElectrumClient electrumClient; + ApiProvider? apiProvider; Box unspentCoinsInfo; @override @@ -178,23 +243,23 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) + List get addressesSet => walletAddresses.allAddresses + .where((element) => element.addressType != 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; @@ -211,8 +276,6 @@ abstract class ElectrumWalletBase @override bool isTestnet; - bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; - @observable bool nodeSupportsSilentPayments = true; @observable @@ -222,369 +285,156 @@ abstract class ElectrumWalletBase 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(); + @observable + int? currentChainTip; - if (tip == walletInfo.restoreHeight) { - syncStatus = SyncedTipSyncStatus(tip); - return; - } + @override + BitcoinWalletKeys get keys => BitcoinWalletKeys( + wif: WifEncoder.encode(bip32.privateKey.raw, netVer: network.wifNetVer), + privateKey: bip32.privateKey.toHex(), + publicKey: bip32.publicKey.toHex(), + ); - if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: currentChainTip); - } - } else { - alwaysScan = false; + String _password; + BitcoinUnspentCoins unspentCoins; - _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + @observable + TransactionPriorities? feeRates; - if (electrumClient.isConnected) { - syncStatus = SyncedSyncStatus(); - } else { - syncStatus = NotConnectedSyncStatus(); + 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; } } - } - 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; + return feeRates![priority]; } - @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), - privateKey: hd.privateKey.toHex(), - publicKey: hd.publicKey.toHex(), - ); - - String _password; - List unspentCoins; - List _feeRates; - - // ignore: prefer_final_fields - Map?> _scripthashesUpdateSubject; + @observable + List scripthashesListening; - // ignore: prefer_final_fields - BehaviorSubject? _chainTipUpdateSubject; - bool _isTransactionUpdating; - Future? _isolate; + bool _chainTipListenerOn = false; + // TODO: improve this + int _syncedTimes = 0; 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, - )); - - await _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 (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) + await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); + + _syncedTimes = 0; - 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 next await updateTransactions(); - await updateAllUnspents(); + // INFO: THIRD: Get the full wallet's balance with all addresses considered await updateBalance(); - await updateFeeRates(); + // INFO: FOURTH: Finish getting unspent coins for all the addresses + await updateAllUnspents(); + + // INFO: FIFTH: Get the latest recommended fee rates and start update timer + 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 { + if (_syncedTimes == 3) { syncStatus = SyncedSyncStatus(); } + + await save(); } catch (e, stacktrace) { - print(stacktrace); print("startSync $e"); + print(stacktrace); syncStatus = FailedSyncStatus(); } } @action - Future updateFeeRates() async { - if (await checkIfMempoolAPIIsEnabled()) { - try { - final response = - await http.get(Uri.parse("http://mempool.cakewallet.com:8999/api/v1/fees/recommended")); - - 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]; - } + void callError(FlutterErrorDetails error) { + _onError?.call(error); } - 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!; + @action + Future updateFeeRates() async { + workerSendPort!.send( + ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: mempoolAPIEnabled).toJson(), + ); } - Future getNodeSupportsSilentPayments() async { - // As of today (august 2024), only ElectrumRS supports silent payments - if (!(await getNodeIsElectrs())) { - return false; - } - - if (node == null) { - return false; + @action + Future onFeesResponse(TransactionPriorities? result) async { + if (result != null) { + feeRates = result; } - - 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!; } + Node? node; + @action @override Future connectToNode({required Node node}) async { this.node = node; - if (syncStatus is ConnectingSyncStatus) return; - try { syncStatus = ConnectingSyncStatus(); - await _receiveStream?.cancel(); - await electrumClient.close(); + if (_workerIsolate != null) { + _workerIsolate!.kill(priority: Isolate.immediate); + _workerSubscription?.cancel(); + receivePort?.close(); + } + + receivePort = ReceivePort(); - electrumClient.onConnectionStatusChange = _onConnectionStatusChange; + _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + _workerSubscription = receivePort!.listen((message) { + print('Main: received message: $message'); + if (message is SendPort) { + workerSendPort = message; + workerSendPort!.send( + ElectrumWorkerConnectionRequest( + uri: node.uri, + useSSL: node.useSSL ?? false, + network: network, + ).toJson(), + ); + } else { + handleWorkerResponse(message); + } + }); } catch (e, stacktrace) { print(stacktrace); print("connectToNode $e"); @@ -596,7 +446,7 @@ abstract class ElectrumWalletBase bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; - UtxoDetails _createUTXOS({ + TxCreateUtxoDetails _createUTXOS({ required bool sendAll, required bool paysToSilentPayment, int credentialsAmount = 0, @@ -619,9 +469,9 @@ abstract class ElectrumWalletBase switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: - return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; case UnspentCoinType.any: return true; } @@ -629,7 +479,8 @@ abstract class ElectrumWalletBase final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); // sort the unconfirmed coins so that mweb coins are first: - availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1); + availableInputs + .sort((a, b) => a.bitcoinAddressRecord.addressType == SegwitAddresType.mweb ? -1 : 1); for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; @@ -637,7 +488,7 @@ abstract class ElectrumWalletBase if (paysToSilentPayment) { // Check inputs for shared secret derivation - if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { + if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) { throw BitcoinTransactionSilentPaymentsNotSupported(); } } @@ -649,21 +500,19 @@ abstract class ElectrumWalletBase 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)); @@ -678,14 +527,19 @@ abstract class ElectrumWalletBase pubKeyHex = privkey.getPublic().toHex(); } else { - pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); + pubKeyHex = walletAddresses.hdWallet + .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( @@ -693,7 +547,7 @@ abstract class ElectrumWalletBase txHash: utx.hash, value: BigInt.from(utx.value), vout: utx.vout, - scriptType: _getScriptType(address), + scriptType: BitcoinAddressUtils.getScriptType(address), isSilentPayment: isSilentPayment, ), ownerDetails: UtxoAddressDetails( @@ -716,7 +570,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoInputsException(); } - return UtxoDetails( + return TxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -745,11 +599,8 @@ abstract class ElectrumWalletBase int fee = await calcFee( utxos: utxoDetails.utxos, outputs: outputs, - network: network, memo: memo, feeRate: feeRate, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - vinOutpoints: utxoDetails.vinOutpoints, ); if (fee == 0) { @@ -851,11 +702,7 @@ abstract class ElectrumWalletBase 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); @@ -868,11 +715,8 @@ abstract class ElectrumWalletBase // 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(); @@ -955,33 +799,16 @@ abstract class ElectrumWalletBase 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( - utxos: utxos, - outputs: outputs, - network: network, - memo: memo, - ); - } else { - estimatedSize = BitcoinTransactionBuilder.estimateTransactionSize( + }) async => + feeRate * + 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 { @@ -1084,11 +911,10 @@ abstract class ElectrumWalletBase 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 @@ -1164,11 +990,10 @@ abstract class ElectrumWalletBase 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, @@ -1212,33 +1037,17 @@ abstract class ElectrumWalletBase 'mnemonic': _mnemonic, 'xpub': xpub, 'passphrase': passphrase ?? '', - 'account_index': walletAddresses.currentReceiveAddressIndexByType, - 'change_address_index': walletAddresses.currentChangeAddressIndexByType, - 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), + 'walletAddresses': walletAddresses.toJson(), 'address_page_type': walletInfo.addressPageType == null ? SegwitAddresType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, - 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), - 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), - 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), 'alwaysScan': alwaysScan, + 'unspents': unspentCoins.map((e) => e.toJson()).toList(), }); - 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)); @@ -1250,8 +1059,12 @@ abstract class ElectrumWalletBase 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; @@ -1297,7 +1110,7 @@ abstract class ElectrumWalletBase 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(); @@ -1334,18 +1147,17 @@ abstract class ElectrumWalletBase 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(); @@ -1353,93 +1165,70 @@ abstract class ElectrumWalletBase @action Future updateAllUnspents() async { - List updatedUnspentCoins = []; - - if (hasSilentPaymentsScanning) { - // 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; - }); + workerSendPort!.send( + ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), + ).toJson(), + ); + } - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); + @action + void updateCoin(BitcoinUnspent coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); - unspentCoins = updatedUnspentCoins; + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + addCoinInfo(coin); } - - await updateCoins(unspentCoins); - await _refreshUnspentCoinsInfo(); } - Future updateCoins(List newUnspentCoins) async { - if (newUnspentCoins.isEmpty) { - return; - } + @action + Future onUnspentResponse(Map> unspents) async { + final updatedUnspentCoins = []; - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); + await Future.wait(unspents.entries.map((entry) async { + final unspent = entry.value; + final scriptHash = entry.key; - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; + final addressRecord = walletAddresses.allAddresses.firstWhereOrNull( + (element) => element.scriptHash == scriptHash, + ); - 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.addAll(updatedUnspentCoins); + unspentCoins.forEach(updateCoin); + + await refreshUnspentCoinsInfo(); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } @action @@ -1454,13 +1243,13 @@ abstract class ElectrumWalletBase 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 = @@ -1485,7 +1274,124 @@ abstract class ElectrumWalletBase } } - 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 || _updatingHistories) { + _updatingHistories = false; + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } + + return; + } + + _updatingHistories = true; + + final addressesWithHistory = []; + BitcoinAddressType? lastDiscoveredType; + + for (final addressHistory in histories) { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + final addressRecord = addressHistory.addressRecord; + final isChange = addressRecord.isChange; + + final addressList = + (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( + (element) => + element.addressType == addressRecord.addressType && + element.derivationType == addressRecord.derivationType); + final totalAddresses = addressList.length; + + final gapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + addressesWithHistory.add(addressRecord); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } + + final hasUsedAddressesUnderGap = addressRecord.index >= totalAddresses - gapLimit; + + if (hasUsedAddressesUnderGap && lastDiscoveredType != addressRecord.addressType) { + lastDiscoveredType = addressRecord.addressType; + + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverNewAddresses( + isChange: isChange, + derivationType: addressRecord.derivationType, + addressType: addressRecord.addressType, + derivationInfo: BitcoinAddressUtils.getDerivationFromType( + addressRecord.addressType, + isElectrum: [ + CWBitcoinDerivationType.electrum, + CWBitcoinDerivationType.old_electrum, + ].contains(addressRecord.derivationType), + ), + ); + + final newAddressList = + (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( + (element) => + element.addressType == addressRecord.addressType && + element.derivationType == addressRecord.derivationType); + print( + "discovered ${newAddresses.length} new addresses, new total: ${newAddressList.length}"); + + if (newAddresses.isNotEmpty) { + // Update the transactions for the new discovered addresses + await updateTransactions(newAddresses); + } + } + } + } + + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } + + walletAddresses.updateHiddenAddresses(); + _updatingHistories = false; + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } + } Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { @@ -1502,11 +1408,12 @@ abstract class ElectrumWalletBase 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; @@ -1544,17 +1451,21 @@ abstract class ElectrumWalletBase 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); @@ -1564,7 +1475,7 @@ abstract class ElectrumWalletBase txHash: input.txId, value: outTransaction.amount, vout: vout, - scriptType: _getScriptType(btcAddress), + scriptType: BitcoinAddressUtils.getScriptType(btcAddress), ), ownerDetails: UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress), @@ -1582,7 +1493,7 @@ abstract class ElectrumWalletBase 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'); @@ -1590,7 +1501,7 @@ abstract class ElectrumWalletBase } } - 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()))); } @@ -1629,7 +1540,7 @@ abstract class ElectrumWalletBase } // 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))) @@ -1669,10 +1580,9 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - electrumClient: electrumClient, + sendWorker: sendWorker, amount: sendingAmount, fee: newFee, - network: network, hasChange: changeOutputs.isNotEmpty, feeRate: newFee.toString(), )..addListener((transaction) async { @@ -1691,437 +1601,116 @@ abstract class ElectrumWalletBase } } - 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!, + mempoolAPIEnabled: mempoolAPIEnabled, + ), + ) 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 - .where((type) => type != SegwitAddresType.mweb) - .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 {}; - } - } - - 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()); - - 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 - - if (this is LitecoinWallet) { - // if we have a peg out transaction with the same value - // that matches this received transaction, mark it as being from a peg out: - for (final tx2 in transactionHistory.transactions.values) { - final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); - // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other - if (tx2.additionalInfo["isPegOut"] == true && - tx2.amount == tx.amount && - heightDiff <= 5) { - tx.additionalInfo["fromPegOut"] = true; - } - } - } - 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 {}; - } + throw UnimplementedError(); } - Future updateTransactions() async { - print("updateTransactions() called!"); - try { - if (_isTransactionUpdating) { - return; - } - currentChainTip = await getUpdatedChainTip(); - - bool updated = false; - transactionHistory.transactions.values.forEach((tx) { - if ((tx.height ?? 0) > 0 && (currentChainTip ?? 0) > 0) { - var confirmations = currentChainTip! - tx.height! + 1; - if (confirmations < 0) { - // if our chain tip is outdated then it could lead to negative confirmations so this is just a failsafe: - confirmations = 0; - } - if (confirmations != tx.confirmations) { - updated = true; - tx.confirmations = confirmations; - transactionHistory.addOne(tx); - } - } - }); - - if (updated) { - await transactionHistory.save(); - } - - _isTransactionUpdating = true; - await fetchTransactions(); - walletAddresses.updateReceiveAddresses(); - _isTransactionUpdating = false; - } catch (e, stacktrace) { - print(stacktrace); - print(e); - _isTransactionUpdating = false; - } + @action + Future updateTransactions([List? addresses]) async { + workerSendPort!.send(ElectrumWorkerGetHistoryRequest( + addresses: walletAddresses.allAddresses.toList(), + 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, + ).toJson()); } - Future subscribeForUpdates() async { - final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => - !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && - address.type != SegwitAddresType.mweb, + @action + Future subscribeForUpdates([Iterable? unsubscribedScriptHashes]) async { + unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where( + (sh) => !scripthashesListening.contains(sh), ); - 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); + Map scripthashByAddress = {}; + walletAddresses.allAddresses.forEach((addressRecord) { + scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; + }); - await updateBalance(); + workerSendPort!.send( + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + ).toJson(), + ); - await _fetchAddressHistory(address, await getCurrentChainTip()); - } catch (e, s) { - print("sub error: $e"); - _onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); - } - }, onError: (e, s) { - print("sub_listen error: $e $s"); - }); - })); + scripthashesListening.addAll(scripthashByAddress.values); } - 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 + void onBalanceResponse(ElectrumBalance balanceResult) { 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; - } - }); - }); - - 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; - } - }); - } - }); - } - - final balances = await Future.wait(balanceFutures); + var totalConfirmed = balanceResult.confirmed; + var totalUnconfirmed = balanceResult.unconfirmed; - if (balances.isNotEmpty && balances.first['confirmed'] == null) { - // if we got null balance responses from the server, set our connection status to lost and return our last known balance: - print("got null balance responses from the server, setting connection status to lost"); - syncStatus = LostConnectionSyncStatus(); - return balance[currency] ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - } - - 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; - - 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, ); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } + @action Future updateBalance() async { - print("updateBalance() called!"); - balance[currency] = await fetchBalances(); - await save(); + workerSendPort!.send(ElectrumWorkerGetBalanceRequest( + scripthashes: walletAddresses.allScriptHashes, + ).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); } @@ -2137,7 +1726,7 @@ abstract class ElectrumWalletBase if (signature.endsWith('=')) { sigDecodedBytes = base64.decode(signature); } else { - sigDecodedBytes = hex.decode(signature); + sigDecodedBytes = BytesUtils.fromHexString(signature); } if (sigDecodedBytes.length != 64 && sigDecodedBytes.length != 65) { @@ -2147,7 +1736,7 @@ abstract class ElectrumWalletBase 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); @@ -2184,33 +1773,6 @@ abstract class ElectrumWalletBase 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) { @@ -2225,7 +1787,7 @@ abstract class ElectrumWalletBase case ConnectionStatus.disconnected: if (syncStatus is! NotConnectedSyncStatus && syncStatus is! ConnectingSyncStatus && - syncStatus is! SyncronizingSyncStatus) { + syncStatus is! SynchronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; @@ -2243,16 +1805,18 @@ abstract class ElectrumWalletBase } } - 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 NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { + if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) { // Needs to re-subscribe to all scripthashes when reconnected - _scripthashesUpdateSubject = {}; + scripthashesListening = []; + _chainTipListenerOn = false; + } + if (isDisconnectedStatus) { if (_isTryingToConnect) return; _isTryingToConnect = true; @@ -2260,21 +1824,13 @@ abstract class ElectrumWalletBase 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) { @@ -2292,14 +1848,15 @@ abstract class ElectrumWalletBase 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); @@ -2310,7 +1867,7 @@ abstract class ElectrumWalletBase 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:'); @@ -2326,255 +1883,6 @@ abstract class ElectrumWalletBase } } -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, @@ -2610,25 +1918,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; @@ -2639,7 +1929,7 @@ class UtxoDetails { final bool spendsSilentPayment; final bool spendsUnconfirmedTX; - UtxoDetails({ + TxCreateUtxoDetails({ required this.availableInputs, required this.unconfirmedCoins, required this.utxos, @@ -2651,3 +1941,41 @@ class UtxoDetails { required this.spendsUnconfirmedTX, }); } + +class BitcoinUnspentCoins extends ObservableSet { + BitcoinUnspentCoins() : super(); + + static BitcoinUnspentCoins of(Iterable unspentCoins) { + final coins = BitcoinUnspentCoins(); + coins.addAll(unspentCoins); + return coins; + } + + 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 c29579436a..64b49ba5b0 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -11,91 +11,39 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; - -const List BITCOIN_ADDRESS_TYPES = [ - SegwitAddresType.p2wpkh, - P2pkhAddressType.p2pkh, - SegwitAddresType.p2tr, - SegwitAddresType.p2wsh, - P2shAddressType.p2wpkhInP2sh, -]; +enum CWBitcoinDerivationType { old_electrum, electrum, old_bip39, bip39, mweb } -const List LITECOIN_ADDRESS_TYPES = [ - SegwitAddresType.p2wpkh, - SegwitAddresType.mweb, +const OLD_DERIVATION_TYPES = [ + CWBitcoinDerivationType.old_electrum, + CWBitcoinDerivationType.old_bip39 ]; -const List BITCOIN_CASH_ADDRESS_TYPES = [ - P2pkhAddressType.p2pkh, -]; +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; 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, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, - 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()), - currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, - currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, + 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()), _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), - silentAddresses = ObservableList.of( - (initialSilentAddresses ?? []).toSet()), - currentSilentAddressIndex = initialSilentAddressIndex, 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, - ); - - 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, - )); - } - } - updateAddressesByMatch(); } @@ -103,21 +51,19 @@ 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; - // TODO: add this variable in `bitcoin_wallet_addresses` and just add a cast in cw_bitcoin to use it - final ObservableList silentAddresses; // 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 bool isHardwareWallet; - @observable - SilentPaymentOwner? silentAddress; + final Map hdWallets; + Bip32Slip10Secp256k1 get hdWallet => + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + + final bool isHardwareWallet; @observable late BitcoinAddressType _addressPageType; @@ -125,23 +71,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed BitcoinAddressType get addressPageType => _addressPageType; - @observable - String? activeSilentAddress; + @computed + List get allAddresses => _allAddresses.toList(); @computed - List get allAddresses => _addresses; + Set get allScriptHashes => + _allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet(); + + BitcoinAddressRecord getFromAddresses(String address) { + return _allAddresses.firstWhere((element) => element.address == address); + } @override @computed String get address { - if (addressPageType == SilentPaymentsAddresType.p2sp) { - if (activeSilentAddress != null) { - return activeSilentAddress!; - } - - return silentAddress.toString(); - } - String receiveAddress; final typeMatchingReceiveAddresses = @@ -152,7 +95,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { receiveAddress = generateNewAddress().address; } else { final previousAddressMatchesType = - previousAddressRecord != null && previousAddressRecord!.type == addressPageType; + previousAddressRecord != null && previousAddressRecord!.addressType == addressPageType; if (previousAddressMatchesType && typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { @@ -170,60 +113,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { - if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { - return; - } - if (addressPageType == SilentPaymentsAddresType.p2sp) { - final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); - - if (selected.silentPaymentTweak != null && silentAddress != null) { - activeSilentAddress = - silentAddress!.toLabeledSilentPaymentAddress(selected.index).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); - - Map currentReceiveAddressIndexByType; - - int get currentReceiveAddressIndex => - currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0; - - void set currentReceiveAddressIndex(int index) => - currentReceiveAddressIndexByType[_addressPageType.toString()] = index; - - Map currentChangeAddressIndexByType; - - int get currentChangeAddressIndex => - currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0; - - void set currentChangeAddressIndex(int index) => - currentChangeAddressIndexByType[_addressPageType.toString()] = index; - - int currentSilentAddressIndex; + String get primaryAddress => _allAddresses.first.address; @observable BitcoinAddressRecord? previousAddressRecord; @computed int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { + if (!addressRecord.isChange) { return acc + 1; } return acc; @@ -231,7 +140,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,230 +149,199 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { if (walletInfo.type == WalletType.bitcoinCash) { - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(addressType: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await generateInitialAddresses(addressType: SegwitAddresType.p2wpkh); if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await _generateInitialAddresses(type: SegwitAddresType.mweb); + await generateInitialAddresses(addressType: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { - await _generateInitialAddresses(); + await generateInitialAddresses(addressType: 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(addressType: P2pkhAddressType.p2pkh); + await generateInitialAddresses(addressType: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(addressType: SegwitAddresType.p2tr); + await generateInitialAddresses(addressType: SegwitAddresType.p2wsh); } } updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); - _validateAddresses(); await updateAddressesInBox(); - - if (currentReceiveAddressIndex >= receiveAddresses.length) { - currentReceiveAddressIndex = 0; - } - - if (currentChangeAddressIndex >= changeAddresses.length) { - currentChangeAddressIndex = 0; - } } @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; - } - - updateChangeAddresses(); - final address = changeAddresses[currentChangeAddressIndex]; - currentChangeAddressIndex += 1; + final address = changeAddresses.firstWhere( + // TODO: feature to choose change type + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh), + ); return address; } - Map get labels { - final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); - final labels = {}; - for (int i = 0; i < silentAddresses.length; i++) { - final silentAddressRecord = silentAddresses[i]; - final silentPaymentTweak = silentAddressRecord.silentPaymentTweak; - - if (silentPaymentTweak != null && - SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { - labels[G - .tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak))) - .toHex()] = silentPaymentTweak; - } - } - return labels; - } - @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { - if (addressPageType == SilentPaymentsAddresType.p2sp && silentAddress != null) { - final currentSilentAddressIndex = silentAddresses - .where((addressRecord) => addressRecord.type != SegwitAddresType.p2tr) - .length - - 1; - - this.currentSilentAddressIndex = currentSilentAddressIndex; - - final address = BitcoinSilentPaymentAddressRecord( - silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(), - index: currentSilentAddressIndex, - isHidden: false, - name: label, - silentPaymentTweak: - BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), - network: network, - type: SilentPaymentsAddresType.p2sp, - ); - - silentAddresses.add(address); - 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, + addressType: addressPageType, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), + derivationType: CWBitcoinDerivationType.bip39, ); - _addresses.add(address); + _allAddresses.add(address); Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } + BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required int index, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) { + throw UnimplementedError(); + } + String getAddress({ + required CWBitcoinDerivationType derivationType, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, - }) => - ''; + 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() .last; if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH'; + addressesMap[lastP2wpkh.address] = 'P2WPKH' + ': ${lastP2wpkh.address}'; } else { - addressesMap[address] = 'Active - P2WPKH'; + addressesMap[address] = 'Active - P2WPKH' + ': $address'; } - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH'; + addressesMap[lastP2pkh.address] = 'P2PKH' + ': ${lastP2pkh.address}'; } else { - addressesMap[address] = 'Active - P2PKH'; + addressesMap[address] = 'Active - P2PKH' + ': $address'; } - final lastP2sh = _addresses.firstWhere((addressRecord) => + final lastP2sh = _allAddresses.firstWhere((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); if (lastP2sh.address != address) { - addressesMap[lastP2sh.address] = 'P2SH'; + addressesMap[lastP2sh.address] = 'P2SH' + ': ${lastP2sh.address}'; } else { - addressesMap[address] = 'Active - P2SH'; + addressesMap[address] = 'Active - P2SH' + ': $address'; } - final lastP2tr = _addresses.firstWhere( + final lastP2tr = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); if (lastP2tr.address != address) { - addressesMap[lastP2tr.address] = 'P2TR'; + addressesMap[lastP2tr.address] = 'P2TR' + ': ${lastP2tr.address}'; } else { - addressesMap[address] = 'Active - P2TR'; + addressesMap[address] = 'Active - P2TR' + ': $address'; } - final lastP2wsh = _addresses.firstWhere( + final lastP2wsh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); if (lastP2wsh.address != address) { - addressesMap[lastP2wsh.address] = 'P2WSH'; + addressesMap[lastP2wsh.address] = 'P2WSH' + ': ${lastP2wsh.address}'; } else { - addressesMap[address] = 'Active - P2WSH'; + addressesMap[address] = 'Active - P2WSH' + ': $address'; } - - silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { - return; - } - - if (addressRecord.address != address) { - addressesMap[addressRecord.address] = addressRecord.name.isEmpty - ? "Silent Payments" - : "Silent Payments - " + addressRecord.name; - } else { - addressesMap[address] = 'Active - Silent Payments'; - } - }); } + @action void addLitecoinAddressTypes() { - final lastP2wpkh = _addresses + final lastP2wpkh = _allAddresses .where((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() .last; if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH'; + addressesMap[lastP2wpkh.address] = 'P2WPKH' + ': ${lastP2wpkh.address}'; } else { - addressesMap[address] = 'Active - P2WPKH'; + addressesMap[address] = 'Active - P2WPKH' + ': $address'; } - final lastMweb = _addresses.firstWhere( + final lastMweb = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); if (lastMweb.address != address) { - addressesMap[lastMweb.address] = 'MWEB'; + addressesMap[lastMweb.address] = 'MWEB' + ': ${lastMweb.address}'; } else { - addressesMap[address] = 'Active - MWEB'; + addressesMap[address] = 'Active - MWEB' + ': $address'; } } + @action void addBitcoinCashAddressTypes() { - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH'; + addressesMap[lastP2pkh.address] = 'P2PKH' + ': $address'; } else { - addressesMap[address] = 'Active - P2PKH'; + addressesMap[address] = 'Active - P2PKH' + ': $address'; } } @override + @action Future updateAddressesInBox() async { try { addressesMap.clear(); - addressesMap[address] = 'Active'; + addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address'; allAddressesMap.clear(); - _addresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { allAddressesMap[addressRecord.address] = addressRecord.name; }); @@ -490,12 +368,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAddress(String address, String label) { BaseBitcoinAddressRecord? foundAddress; - _addresses.forEach((addressRecord) { - if (addressRecord.address == address) { - foundAddress = addressRecord; - } - }); - silentAddresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { if (addressRecord.address == address) { foundAddress = addressRecord; } @@ -508,131 +381,167 @@ 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 { - final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); - silentAddresses.remove(foundAddress); - silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); - } } } @action void updateAddressesByMatch() { - if (addressPageType == SilentPaymentsAddresType.p2sp) { - addressesByReceiveType.clear(); - addressesByReceiveType.addAll(silentAddresses); - return; - } - 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 - (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); + final newAddresses = _allAddresses.where((addressRecord) => + addressRecord.isChange && + (walletInfo.type != WalletType.bitcoin || + addressRecord.addressType == 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); - addAddresses(newAddresses); - - final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory)); - final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; + Future> discoverNewAddresses({ + required CWBitcoinDerivationType derivationType, + required bool isChange, + required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, + }) async { + final count = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + final startIndex = (isChange ? receiveAddresses : changeAddresses) + .where((addr) => addr.derivationType == derivationType && addr.addressType == addressType) + .length; + + final newAddresses = []; - if (isLastAddressUsed) { - discoverAddresses(addressList, isHidden, getAddressHistory, type: type); + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord( + await getAddressAsync( + derivationType: derivationType, + isChange: isChange, + index: i, + addressType: addressType, + derivationInfo: derivationInfo, + ), + index: i, + isChange: isChange, + isHidden: OLD_DERIVATION_TYPES.contains(derivationType), + addressType: addressType, + network: network, + derivationInfo: derivationInfo, + derivationType: derivationType, + ); + newAddresses.add(address); } - } - Future _generateInitialAddresses( - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - var countOfReceiveAddresses = 0; - var countOfHiddenAddresses = 0; + addAddresses(newAddresses); + return newAddresses; + } - _addresses.forEach((addr) { - if (addr.type == type) { - if (addr.isHidden) { - countOfHiddenAddresses += 1; - return; - } + @action + Future generateInitialAddresses({required BitcoinAddressType addressType}) async { + if (_allAddresses.where((addr) => addr.addressType == addressType).isNotEmpty) { + return; + } - countOfReceiveAddresses += 1; + for (final derivationType in hdWallets.keys) { + // p2wpkh has always had the right derivations, skip if creating old derivations + if (OLD_DERIVATION_TYPES.contains(derivationType) && addressType == SegwitAddresType.p2wpkh) { + continue; } - }); - if (countOfReceiveAddresses < defaultReceiveAddressesCount) { - final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfReceiveAddresses, isHidden: false, type: type); - addAddresses(newAddresses); - } + final isElectrum = derivationType == CWBitcoinDerivationType.electrum || + derivationType == CWBitcoinDerivationType.old_electrum; - if (countOfHiddenAddresses < defaultChangeAddressesCount) { - final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfHiddenAddresses, isHidden: true, type: type); - addAddresses(newAddresses); - } - } + final derivationInfos = walletInfo.derivations?.where( + (element) => element.scriptType == addressType.toString(), + ); - Future> _createNewAddresses(int count, - {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { - final list = []; + if (derivationInfos == null || derivationInfos.isEmpty) { + final bitcoinDerivationInfo = BitcoinDerivationInfo( + derivationType: isElectrum ? BitcoinDerivationType.electrum : BitcoinDerivationType.bip39, + derivationPath: walletInfo.derivationInfo!.derivationPath!, + scriptType: addressType, + ); + + await discoverNewAddresses( + derivationType: derivationType, + isChange: false, + addressType: addressType, + derivationInfo: bitcoinDerivationInfo, + ); + await discoverNewAddresses( + derivationType: derivationType, + isChange: true, + addressType: addressType, + derivationInfo: bitcoinDerivationInfo, + ); + continue; + } - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord( - await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), - index: i, - isHidden: isHidden, - type: type ?? addressPageType, - network: network, - ); - list.add(address); + for (final derivationInfo in derivationInfos) { + final bitcoinDerivationInfo = BitcoinDerivationInfo( + derivationType: isElectrum ? BitcoinDerivationType.electrum : BitcoinDerivationType.bip39, + derivationPath: derivationInfo.derivationPath!, + scriptType: addressType, + ); + + await discoverNewAddresses( + derivationType: derivationType, + isChange: false, + addressType: addressType, + derivationInfo: bitcoinDerivationInfo, + ); + await discoverNewAddresses( + derivationType: derivationType, + isChange: true, + addressType: addressType, + derivationInfo: bitcoinDerivationInfo, + ); + } } + } - 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 - void addSilentAddresses(Iterable addresses) { - final addressesSet = this.silentAddresses.toSet(); - addressesSet.addAll(addresses); - this.silentAddresses.clear(); - this.silentAddresses.addAll(addressesSet); - updateAddressesByMatch(); + void updateHiddenAddresses() { + this.hiddenAddresses.clear(); + this.hiddenAddresses.addAll(_allAddresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); } @action @@ -644,24 +553,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,19 +565,17 @@ 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 _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => + addr.addressType == type; - bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => - !addr.isHidden && !addr.isUsed && addr.type == type; - - @action - void deleteSilentPaymentAddress(String address) { - final addressRecord = silentAddresses.firstWhere((addressRecord) => - addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address); + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + return !addr.isChange && !addr.isUsed && addr.addressType == type; + } - silentAddresses.remove(addressRecord); - updateAddressesByMatch(); + Map toJson() { + return { + 'allAddresses': _allAddresses.map((address) => address.toJSON()).toList(), + 'addressPageType': addressPageType.toString(), + }; } } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 9907190891..829f10de39 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -1,12 +1,11 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_unspent.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 { @@ -25,6 +24,7 @@ class ElectrumWalletSnapshot { required this.silentAddressIndex, required this.mwebAddresses, required this.alwaysScan, + required this.unspentCoins, this.passphrase, this.derivationType, this.derivationPath, @@ -34,6 +34,7 @@ class ElectrumWalletSnapshot { final String password; final WalletType type; final String? addressPageType; + List unspentCoins; @deprecated String? mnemonic; @@ -68,7 +69,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 +81,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 +94,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 = { @@ -129,6 +130,12 @@ class ElectrumWalletSnapshot { silentAddressIndex: silentAddressIndex, mwebAddresses: mwebAddresses, alwaysScan: alwaysScan, + unspentCoins: (data['unspent_coins'] as List) + .map((e) => BitcoinUnspent.fromJSON( + null, + e as Map, + )) + .toList(), ); } } 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 0000000000..14bb0c4dbb --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -0,0 +1,913 @@ +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; + bool _isScanning = false; + bool _stopScanRequested = false; + + 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 ElectrumWorkerMethods.checkTweaksMethod: + await _handleCheckTweaks( + ElectrumWorkerCheckTweaksRequest.fromJson(messageJson), + ); + break; + case ElectrumWorkerMethods.stopScanningMethod: + await _handleStopScanning( + ElectrumWorkerStopScanningRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.tweaksSubscribeMethod: + if (_isScanning) { + _stopScanRequested = false; + } + + if (!_stopScanRequested) { + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); + } else { + _stopScanRequested = false; + _sendResponse( + ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(syncStatus: SyncedSyncStatus()), + ), + ); + } + + break; + case ElectrumRequestMethods.estimateFeeMethod: + await _handleGetFeeRates( + ElectrumWorkerGetFeesRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.versionMethod: + await _handleGetVersion( + ElectrumWorkerGetVersionRequest.fromJson(messageJson), + ); + break; + } + } catch (e) { + _sendError(ElectrumWorkerErrorResponse(error: e.toString())); + } + } + + Future _handleConnect(ElectrumWorkerConnectionRequest request) async { + _network = request.network; + + try { + _electrumClient = await ElectrumApiProvider.connect( + request.useSSL + ? ElectrumSSLService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + }, + ) + : ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + }, + ), + ); + } catch (e) { + _sendError(ElectrumWorkerConnectionError(error: e.toString())); + } + } + + Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { + final req = ElectrumHeaderSubscribe(); + + final stream = _electrumClient!.subscribe(req); + if (stream == null) { + _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); + return; + } + + stream.listen((event) { + _sendResponse( + ElectrumWorkerHeadersSubscribeResponse( + result: req.onResponse(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 req = ElectrumScriptHashSubscribe(scriptHash: scripthash); + + final stream = await _electrumClient!.subscribe(req); + + if (stream == 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 + stream.listen((status) async { + print("status: $status"); + + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: req.onResponse(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; + ElectrumTransactionInfo? tx; + + try { + // Exception thrown on null, handled on catch + 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 (_) {} + + // date is validated when the API responds with the same date at least twice + // since sometimes the mempool api returns the wrong date at first, and we update + if (tx?.isDateValidated != true) { + tx = ElectrumTransactionInfo.fromElectrumBundle( + await _getTransactionExpanded( + hash: txid, + currentChainTip: result.chainTip, + mempoolAPIEnabled: result.mempoolAPIEnabled, + getTime: true, + confirmations: tx?.confirmations, + date: tx?.date, + ), + 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, + ); + } + })); + } + })); + + _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, + ); + + _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); + } + + Future _getTransactionExpanded({ + required String hash, + required int currentChainTip, + required bool mempoolAPIEnabled, + bool getTime = false, + int? confirmations, + DateTime? date, + }) async { + int? time; + int? height; + bool? isDateValidated; + + final transactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); + + if (getTime) { + if (mempoolAPIEnabled) { + try { + // TODO: mempool api class + final txVerbose = await http + .get( + Uri.parse( + "https://mempool.cakewallet.com/api/v1/tx/$hash/status", + ), + ) + .timeout(const Duration(seconds: 5)); + + 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( + "https://mempool.cakewallet.com/api/v1/block-height/$height", + ), + ) + .timeout(const Duration(seconds: 5)); + + if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { + final blockResponse = await http + .get( + Uri.parse( + "https://mempool.cakewallet.com/api/v1/block/${blockHash.body}", + ), + ) + .timeout(const Duration(seconds: 5)); + + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + + if (date != null) { + final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); + isDateValidated = newDate == date; + } + } + } + } + } catch (_) {} + } + + if (confirmations == null && height != null) { + 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( + // TODO: _getTXHex + ElectrumGetTransactionHex(transactionHash: vin.txId), + ); + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } + + return ElectrumTransactionBundle( + original, + ins: ins, + time: time, + confirmations: confirmations ?? 0, + isDateValidated: isDateValidated, + ); + } + + 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 _handleCheckTweaks(ElectrumWorkerCheckTweaksRequest request) async { + final response = await _electrumClient!.request( + ElectrumTweaksSubscribe( + height: 0, + count: 1, + historicalMode: false, + ), + ); + + final supportsScanning = response != null; + _sendResponse( + ElectrumWorkerCheckTweaksResponse(result: supportsScanning, id: request.id), + ); + } + + Future _handleStopScanning(ElectrumWorkerStopScanningRequest request) async { + _stopScanRequested = true; + _sendResponse( + ElectrumWorkerStopScanningResponse(result: true, id: request.id), + ); + } + + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { + _isScanning = true; + final scanData = request.scanData; + + // TODO: confirmedSwitch use new connection + // final _electrumClient = await ElectrumApiProvider.connect( + // ElectrumTCPService.connect( + // Uri.parse("tcp://electrs.cakewallet.com:50001"), + // onConnectionStatusChange: (status) { + // _sendResponse( + // ElectrumWorkerConnectionResponse(status: status, id: request.id), + // ); + // }, + // ), + // ); + + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; + + final receivers = scanData.silentPaymentsWallets.map( + (wallet) { + return Receiver( + wallet.b_scan.toHex(), + wallet.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); + }, + ); + + // Initial status UI update, send how many blocks in total to scan + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: StartingScanSyncStatus(syncHeight), + ), + )); + + final req = ElectrumTweaksSubscribe( + height: syncHeight, + count: 1, + historicalMode: false, + ); + + final stream = await _electrumClient!.subscribe(req); + + void listenFn(Map event, ElectrumTweaksSubscribe req) { + final response = req.onResponse(event); + if (_stopScanRequested || response == null) { + _stopScanRequested = false; + _isScanning = false; + return; + } + + // success or error msg + final noData = response.message != null; + + if (noData) { + if (scanData.isSingleScan) { + return; + } + + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + + if (nextHeight <= scanData.chainTip) { + final nextStream = _electrumClient!.subscribe( + ElectrumTweaksSubscribe( + height: nextHeight, + count: 1, + historicalMode: false, + ), + ); + nextStream?.listen((event) => listenFn(event, req)); + } + + 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 = {}; + + receivers.forEach((receiver) { + final scanResult = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + + addToWallet.addAll(scanResult); + }); + // final addToWallet = scanOutputs( + // outputPubkeys.keys.toList(), + // tweak, + // receivers.last, + // ); + + 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, + // TODO: tx time mempool api + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + time: null, + 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.silentPaymentsWallets.first.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}), + )); + + return; + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + + syncHeight = tweakHeight; + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + _sendResponse( + ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: scanData.isSingleScan + ? SyncedSyncStatus() + : SyncedTipSyncStatus(scanData.chainTip), + ), + ), + ); + + stream?.close(); + return; + } + } + + stream?.listen((event) => listenFn(event, req)); + _isScanning = false; + } + + 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 0000000000..4d9c85a47b --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -0,0 +1,21 @@ +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 String checkTweaksMethod = "checkTweaks"; + static const String stopScanningMethod = "stopScanning"; + + static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); + static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); + static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); + static const ElectrumWorkerMethods checkTweaks = ElectrumWorkerMethods._(checkTweaksMethod); + static const ElectrumWorkerMethods stopScanning = ElectrumWorkerMethods._(stopScanningMethod); + + @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 0000000000..ea3c0b1994 --- /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 0000000000..f295fa24a5 --- /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/check_tweaks_method.dart b/cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart new file mode 100644 index 0000000000..a67279778f --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart @@ -0,0 +1,49 @@ +part of 'methods.dart'; + +class ElectrumWorkerCheckTweaksRequest implements ElectrumWorkerRequest { + ElectrumWorkerCheckTweaksRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumWorkerMethods.checkTweaks.method; + + @override + factory ElectrumWorkerCheckTweaksRequest.fromJson(Map json) { + return ElectrumWorkerCheckTweaksRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method, 'id': id}; + } +} + +class ElectrumWorkerCheckTweaksError extends ElectrumWorkerErrorResponse { + ElectrumWorkerCheckTweaksError({required super.error, super.id}) : super(); + + @override + final String method = ElectrumWorkerMethods.checkTweaks.method; +} + +class ElectrumWorkerCheckTweaksResponse extends ElectrumWorkerResponse { + ElectrumWorkerCheckTweaksResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumWorkerMethods.checkTweaks.method); + + @override + String resultJson(result) { + return result.toString(); + } + + @override + factory ElectrumWorkerCheckTweaksResponse.fromJson(Map json) { + return ElectrumWorkerCheckTweaksResponse( + result: json['result'] == "true", + 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 0000000000..4ff27665cc --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -0,0 +1,77 @@ +part of 'methods.dart'; + +class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { + ElectrumWorkerConnectionRequest({ + required this.uri, + required this.network, + required this.useSSL, + this.id, + }); + + final Uri uri; + final bool useSSL; + 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, + ), + useSSL: json['useSSL'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return { + 'method': method, + 'uri': uri.toString(), + 'network': network.toString(), + 'useSSL': useSSL, + }; + } +} + +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 0000000000..2fc5513675 --- /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 0000000000..1892e2cb75 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -0,0 +1,62 @@ +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: json['result'] == null + ? null + : 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 0000000000..021ed6899e --- /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 0000000000..1824a0686e --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -0,0 +1,71 @@ +part of 'methods.dart'; + +class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { + ElectrumWorkerTxExpandedRequest({ + required this.txHash, + required this.currentChainTip, + required this.mempoolAPIEnabled, + this.id, + }); + + final String txHash; + final int currentChainTip; + final bool mempoolAPIEnabled; + 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, + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return { + 'method': method, + 'txHash': txHash, + 'currentChainTip': currentChainTip, + 'mempoolAPIEnabled': mempoolAPIEnabled, + }; + } +} + +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 0000000000..de02f5d249 --- /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 0000000000..66d1b1a68c --- /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 0000000000..8f23d1d6a3 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -0,0 +1,24 @@ +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'; +part 'check_tweaks_method.dart'; +part 'stop_scanning.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 0000000000..31f9abe76d --- /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/stop_scanning.dart b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart new file mode 100644 index 0000000000..a84a171b57 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart @@ -0,0 +1,49 @@ +part of 'methods.dart'; + +class ElectrumWorkerStopScanningRequest implements ElectrumWorkerRequest { + ElectrumWorkerStopScanningRequest({this.id}); + + final int? id; + + @override + final String method = ElectrumWorkerMethods.stopScanning.method; + + @override + factory ElectrumWorkerStopScanningRequest.fromJson(Map json) { + return ElectrumWorkerStopScanningRequest(id: json['id'] as int?); + } + + @override + Map toJson() { + return {'method': method, 'id': id}; + } +} + +class ElectrumWorkerStopScanningError extends ElectrumWorkerErrorResponse { + ElectrumWorkerStopScanningError({required super.error, super.id}) : super(); + + @override + final String method = ElectrumWorkerMethods.stopScanning.method; +} + +class ElectrumWorkerStopScanningResponse extends ElectrumWorkerResponse { + ElectrumWorkerStopScanningResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumWorkerMethods.stopScanning.method); + + @override + String resultJson(result) { + return result.toString(); + } + + @override + factory ElectrumWorkerStopScanningResponse.fromJson(Map json) { + return ElectrumWorkerStopScanningResponse( + result: json['result'] as bool, + 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 0000000000..c51670cdcd --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -0,0 +1,157 @@ +part of 'methods.dart'; + +class ScanData { + final List silentPaymentsWallets; + final int height; + final BasedUtxoNetwork network; + final int chainTip; + final List transactionHistoryIds; + final Map labels; + final List labelIndexes; + final bool isSingleScan; + + ScanData({ + required this.silentPaymentsWallets, + 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( + silentPaymentsWallets: scanData.silentPaymentsWallets, + height: newHeight, + network: scanData.network, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } + + Map toJson() { + return { + 'silentAddress': silentPaymentsWallets.map((e) => e.toJson()).toList(), + 'height': height, + 'network': network.value, + 'chainTip': chainTip, + 'transactionHistoryIds': transactionHistoryIds, + 'labels': labels, + 'labelIndexes': labelIndexes, + 'isSingleScan': isSingleScan, + }; + } + + static ScanData fromJson(Map json) { + return ScanData( + silentPaymentsWallets: (json['silentAddress'] as List) + .map((e) => SilentPaymentOwner.fromJson(e as Map)) + .toList(), + 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 0000000000..0f3f814d37 --- /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 62840933c0..c53a8713d5 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 86228fc833..2fa17e35c1 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_core/node.dart'; @@ -22,8 +22,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'; @@ -72,6 +70,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int? initialMwebHeight, bool? alwaysScan, + required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, password: password, @@ -85,6 +84,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ) { if (seedBytes != null) { mwebHd = @@ -97,41 +97,25 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, - 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) { @@ -156,19 +140,21 @@ 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; switch (walletInfo.derivationInfo?.derivationType) { @@ -197,6 +183,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -206,6 +193,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); @@ -241,7 +229,7 @@ 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; @@ -279,6 +267,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -313,16 +302,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"); } @@ -350,8 +339,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 @@ -393,7 +381,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // if the confirmations haven't changed, skip updating: if (tx.confirmations == confirmations) continue; - // if an outgoing tx is now confirmed, delete the utxo from the box (delete the unspent coin): if (confirmations >= 2 && tx.direction == TransactionDirection.outgoing && @@ -469,13 +456,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); @@ -535,6 +516,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { direction: TransactionDirection.incoming, isPending: utxo.height == 0, date: date, + time: null, confirmations: confirmations, inputAddresses: [], outputAddresses: [utxo.outputId], @@ -598,8 +580,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 { @@ -653,7 +635,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future deleteSpentUtxos() async { print("deleteSpentUtxos() called!"); - final chainHeight = await electrumClient.getCurrentBlockChainTip(); + final chainHeight = currentChainTip; final status = await CwMweb.status(StatusRequest()); if (chainHeight == null || status.blockHeaderHeight != chainHeight) return; if (status.mwebUtxosHeight != chainHeight) return; // we aren't synced @@ -703,7 +685,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (spent.isEmpty) return; 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; @@ -738,6 +720,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { direction: TransactionDirection.outgoing, isPending: false, date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000), + time: null, confirmations: 1, inputAddresses: inputAddresses.toList(), outputAddresses: [], @@ -838,7 +821,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: @@ -846,17 +829,17 @@ 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 confirmed = balance.confirmed; + // int unconfirmed = balance.unconfirmed; int confirmedMweb = 0; int unconfirmedMweb = 0; try { @@ -923,24 +906,119 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } - return ElectrumBalance( - confirmed: confirmed, - unconfirmed: unconfirmed, - frozen: balance.frozen, - secondConfirmed: confirmedMweb, - secondUnconfirmed: unconfirmedMweb, - ); + // return ElectrumBalance( + // confirmed: confirmed, + // unconfirmed: unconfirmed, + // frozen: balance.frozen, + // secondConfirmed: confirmedMweb, + // secondUnconfirmed: unconfirmedMweb, + // ); } + // @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; } } @@ -952,25 +1030,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) { @@ -980,7 +1047,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(), + ) ]; } @@ -1026,14 +1095,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; @@ -1132,12 +1201,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(), @@ -1225,10 +1294,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( @@ -1370,7 +1446,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( diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 062c590baf..db5e95694f 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'; @@ -21,16 +19,13 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle 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, - super.initialChangeAddressIndex, }) : super(walletInfo) { for (int i = 0; i < mwebAddresses.length; i++) { mwebAddrs.add(mwebAddresses[i].address); @@ -50,7 +45,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override Future init() async { - if (!isHardwareWallet) await initMwebAddresses(); + if (!super.isHardwareWallet) await initMwebAddresses(); await super.init(); } @@ -101,12 +96,16 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with 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, + addressType: SegwitAddresType.mweb, + network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, + ), + ) .toList(); addMwebAddresses(addressRecords); print("set ${addressRecords.length} mweb addresses"); @@ -118,36 +117,56 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with 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: hdWallet, + 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 @override - Future getChangeAddress( - {List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress({ + List? inputs, + List? outputs, + bool isPegIn = false, + }) async { // use regular change address on peg in, otherwise use mweb for change address: if (!mwebEnabled || isPegIn) { @@ -193,8 +212,10 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with return BitcoinAddressRecord( mwebAddrs[0], index: 0, - type: SegwitAddresType.mweb, + addressType: SegwitAddresType.mweb, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, ); } @@ -205,7 +226,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with String get addressForExchange { // don't use mweb addresses for exchange refund address: final addresses = receiveAddresses - .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); + .where((element) => element.addressType == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index d519f4d0a5..935708da6d 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; @@ -139,6 +147,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await currentWallet.renameWalletFiles(newName); @@ -165,6 +174,7 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -190,6 +200,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 411c7de16a..2c0763305f 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 { @@ -141,19 +138,22 @@ class PendingBitcoinTransaction 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, - isReplaced: false, - confirmations: 0, - inputAddresses: _tx.inputs.map((input) => input.txId).toList(), - outputAddresses: outputAddresses, - fee: fee); - + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo( + type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + isReplaced: false, + confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, + fee: fee, + time: null, + ); + @override Future commitUR() { throw UnimplementedError(); diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart deleted file mode 100644 index a7435bed1f..0000000000 --- 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 c8a83f90cd..0188e0a548 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -87,17 +87,17 @@ packages: dependency: "direct overridden" description: path: "." - ref: cake-update-v9 - resolved-ref: "86969a14e337383e14965f5fb45a72a63e5009bc" - 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" + ref: cake-update-v3 + resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" url: "https://github.com/cake-tech/blockchain_utils" source: git version: "3.3.0" @@ -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 bff6104ac1..26035f4e38 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 d55914dcde..af39965b95 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,52 @@ 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 +97,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -99,6 +107,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 +150,21 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { return BitcoinAddressRecord( addr.address, index: addr.index, - isHidden: addr.isHidden, - type: P2pkhAddressType.p2pkh, + isChange: addr.isChange, + addressType: 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, - type: P2pkhAddressType.p2pkh, + isChange: addr.isChange, + addressType: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + derivationType: CWBitcoinDerivationType.bip39, ); } }).toList(), @@ -162,6 +175,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -193,13 +207,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 +223,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 fe0ebc8284..5526f96cec 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,20 +10,25 @@ 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, super.initialAddressPageType, }) : 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: hdWallet, + 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 d14dc582df..4005bd5cbf 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 05483ce545..340c1eb642 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,16 +74,17 @@ 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, ); @override Future commitUR() { diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9a5c4f14f8..9947b1b277 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/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 2b0b77a895..dcf9c66759 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -245,6 +245,9 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-11": 868345, + "2024-10": 863584, + "2024-09": 859317, "2024-08": 854889, "2024-07": 850182, "2024-06": 846005, @@ -270,7 +273,7 @@ const bitcoinDates = { Future getBitcoinHeightByDateAPI({required DateTime date}) async { final response = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", + "https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}", ), ); diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285d..98bc9d886a 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,61 @@ 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 'SyncedSyncStatus': + return SyncedSyncStatus(); + 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_info.dart b/cw_core/lib/transaction_info.dart index 3e34af75f7..5cafbdff85 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -9,6 +9,7 @@ abstract class TransactionInfo extends Object with Keyable { late TransactionDirection direction; late bool isPending; late DateTime date; + bool? isDateValidated; int? height; late int confirmations; String amountFormatted(); diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index c173f1ddda..35282f49e4 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 55c24bf379..ae69fadace 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 bd035e30a7..96e0e94dab 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -19,6 +19,8 @@ enum DerivationType { bip39, @HiveField(4) electrum, + @HiveField(5) + old, } @HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID) @@ -79,6 +81,7 @@ class WalletInfo extends HiveObject { this.yatLastUsedAddressRaw, this.showIntroCakePayCard, this.derivationInfo, + this.derivations, this.hardwareWalletType, this.parentAddress, ) : _yatLastUsedAddressController = StreamController.broadcast(); @@ -97,6 +100,7 @@ class WalletInfo extends HiveObject { String yatEid = '', String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, + List? derivations, HardwareWalletType? hardwareWalletType, String? parentAddress, }) { @@ -114,6 +118,7 @@ class WalletInfo extends HiveObject { yatLastUsedAddressRaw, showIntroCakePayCard, derivationInfo, + derivations, hardwareWalletType, parentAddress, ); @@ -189,15 +194,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 638cdc39d1..ff680f9e10 100644 --- a/cw_core/lib/wallet_keys_file.dart +++ b/cw_core/lib/wallet_keys_file.dart @@ -27,7 +27,10 @@ mixin WalletKeysFile? 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, ); @@ -138,13 +136,24 @@ class CWBitcoin extends Bitcoin { List getSubAddresses(Object wallet) { final electrumWallet = wallet as ElectrumWallet; return electrumWallet.walletAddresses.addressesByReceiveType - .map((BaseBitcoinAddressRecord addr) => ElectrumSubAddress( + .map( + (addr) => ElectrumSubAddress( id: addr.index, name: addr.name, address: addr.address, + derivationPath: (addr as BitcoinAddressRecord) + .derivationInfo + .derivationPath + .addElem( + Bip32KeyIndex(addr.isChange ? 1 : 0), + ) + .addElem(Bip32KeyIndex(addr.index)) + .toString(), txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange, + ), + ) .toList(); } @@ -167,12 +176,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 +193,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, @@ -210,9 +215,9 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.unspentCoins.where((element) { switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; + return element.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: - return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + return element.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; case UnspentCoinType.any: return true; } @@ -224,30 +229,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 +347,38 @@ class CWBitcoin extends Bitcoin { } @override - Future> getDerivationsFromMnemonic({ + List getOldDerivationInfos(List list) { + final oldList = []; + oldList.addAll(list); + + for (var derivationInfo in list) { + final isElectrum = derivationInfo.derivationType == DerivationType.electrum; + + oldList.add( + DerivationInfo( + derivationType: DerivationType.old, + derivationPath: isElectrum + ? derivationInfo.derivationPath + : BitcoinAddressUtils.getDerivationFromType( + SegwitAddresType.p2wpkh, + ).derivationPath.toString(), + scriptType: derivationInfo.scriptType, + ), + ); + } + + oldList.addAll(bitcoin!.getOldSPDerivationInfos()); + + return oldList; + } + + @override + Future> getDerivationInfosFromMnemonic({ 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); + final list = []; late BasedUtxoNetwork network; switch (node.type) { @@ -346,72 +391,54 @@ 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) { + for (final addressType in BITCOIN_ADDRESS_TYPES) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + scriptType: addressType.value, + ), + ); + } + } + + var bip39SeedBytes; + try { + bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } catch (e) { + print("bip39 seed error: $e"); + } + + if (bip39SeedBytes != null) { + for (final addressType in BITCOIN_ADDRESS_TYPES) { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: BitcoinAddressUtils.getDerivationFromType( + addressType, + ).derivationPath.toString(), + scriptType: addressType.value, + ), + ); + } + } return list; } @@ -443,7 +470,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 +485,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 +509,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 @@ -515,33 +547,53 @@ class CWBitcoin extends Bitcoin { } } + @override + List getOldSPDerivationInfos() { + return [ + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/352'/1'/0'/1'/0", + description: "Old SP Scan", + ), + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/352'/1'/0'/0'/0", + description: "Old SP Spend", + ), + ]; + } + @override List getSilentPaymentAddresses(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.silentAddresses - .where((addr) => addr.type != SegwitAddresType.p2tr) + final walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; + return walletAddresses.silentPaymentAddresses .map((addr) => ElectrumSubAddress( - id: addr.index, - name: addr.name, - address: addr.address, - txCount: addr.txCount, - balance: addr.balance, - isChange: addr.isHidden)) + id: addr.index, + name: addr.name, + address: addr.address, + derivationPath: Bip32PathParser.parse(addr.derivationPath) + .addElem(Bip32KeyIndex(addr.index)) + .toString(), + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isChange, + )) .toList(); } @override List getSilentPaymentReceivedAddresses(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.walletAddresses.silentAddresses - .where((addr) => addr.type == SegwitAddresType.p2tr) + final walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; + return walletAddresses.receivedSPAddresses .map((addr) => ElectrumSubAddress( - id: addr.index, - name: addr.name, - address: addr.address, - txCount: addr.txCount, - balance: addr.balance, - isChange: addr.isHidden)) + id: addr.index, + name: addr.name, + address: addr.address, + derivationPath: "", + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isChange, + )) .toList(); } @@ -564,7 +616,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 +626,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,20 +653,20 @@ 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(); } @override void deleteSilentPaymentAddress(Object wallet, String address) { - final bitcoinWallet = wallet as ElectrumWallet; - bitcoinWallet.walletAddresses.deleteSilentPaymentAddress(address); + final walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; + walletAddresses.deleteSilentPaymentAddress(address); } @override @@ -699,8 +757,8 @@ class CWBitcoin extends Bitcoin { String? getUnusedSegwitAddress(Object wallet) { try { final electrumWallet = wallet as ElectrumWallet; - final segwitAddress = electrumWallet.walletAddresses.allAddresses - .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + final segwitAddress = electrumWallet.walletAddresses.allAddresses.firstWhere( + (element) => !element.isUsed && element.addressType == SegwitAddresType.p2wpkh); return segwitAddress.address; } catch (_) { return null; diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index b744487034..0a91313490 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 46dd62c3a0..86a8943e47 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/di.dart b/lib/di.dart index facdec9128..19d3e8b8fc 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -377,14 +377,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)); @@ -407,62 +407,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, _) => @@ -797,7 +787,6 @@ Future setup({ ); getIt.registerFactoryParam((arguments, _) { - return WalletEditPage( pageArguments: WalletEditPageArguments( walletEditViewModel: getIt.get(param1: arguments.walletListViewModel), @@ -896,8 +885,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()); @@ -905,7 +895,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)); @@ -1062,6 +1053,7 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().silentPaymentsAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, ); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService( @@ -1069,16 +1061,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); @@ -1086,7 +1084,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: @@ -1125,40 +1124,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]))); - - 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, - ); - } - ); + (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( (newWalletTypeArguments, _) { @@ -1180,8 +1175,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/preferences_key.dart b/lib/entities/preferences_key.dart index 5ed7a7ed62..597e37c9ec 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -49,6 +49,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 51fab4dd1d..babd6b8980 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/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 10f9aef43b..c81bca4844 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 7e3c2b5553..bae9a972a3 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_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index beef7c7625..5a1267bb4a 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -7,23 +7,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; class AddressCell extends StatelessWidget { - AddressCell( - {required this.address, - required this.name, - required this.isCurrent, - required this.isPrimary, - required this.backgroundColor, - required this.textColor, - this.onTap, - this.onEdit, - this.onHide, - this.isHidden = false, - this.onDelete, - this.txCount, - this.balance, - this.isChange = false, - this.hasBalance = false, - this.hasReceived = false}); + AddressCell({ + required this.address, + required this.derivationPath, + required this.name, + required this.isCurrent, + required this.isPrimary, + required this.backgroundColor, + required this.textColor, + this.onTap, + this.onEdit, + this.onHide, + this.isHidden = false, + this.onDelete, + this.txCount, + this.balance, + this.isChange = false, + this.hasBalance = false, + this.hasReceived = false, + }); factory AddressCell.fromItem( WalletAddressListItem item, { @@ -39,24 +41,27 @@ class AddressCell extends StatelessWidget { Function()? onDelete, }) => AddressCell( - address: item.address, - name: item.name ?? '', - isCurrent: isCurrent, - isPrimary: item.isPrimary, - backgroundColor: backgroundColor, - textColor: textColor, - onTap: onTap, - onEdit: onEdit, - onHide: onHide, - isHidden: isHidden, - onDelete: onDelete, - txCount: item.txCount, - balance: item.balance, - isChange: item.isChange, - hasBalance: hasBalance, - hasReceived: hasReceived,); + address: item.address, + derivationPath: item.derivationPath, + name: item.name ?? '', + isCurrent: isCurrent, + isPrimary: item.isPrimary, + backgroundColor: backgroundColor, + textColor: textColor, + onTap: onTap, + onEdit: onEdit, + onHide: onHide, + isHidden: isHidden, + onDelete: onDelete, + txCount: item.txCount, + balance: item.balance, + isChange: item.isChange, + hasBalance: hasBalance, + hasReceived: hasReceived, + ); final String address; + final String derivationPath; final String name; final bool isCurrent; final bool isPrimary; @@ -102,7 +107,9 @@ class AddressCell extends StatelessWidget { child: Column( children: [ Row( - mainAxisAlignment: name.isNotEmpty ? MainAxisAlignment.spaceBetween : MainAxisAlignment.center, + mainAxisAlignment: name.isNotEmpty + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ Row( @@ -151,6 +158,26 @@ class AddressCell extends StatelessWidget { ), ], ), + if (derivationPath.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: AutoSizeText( + derivationPath, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isChange ? 10 : 14, + color: textColor, + ), + ), + ), + ], + ), + ), if (hasBalance || hasReceived) Padding( padding: const EdgeInsets.only(top: 8.0), diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 9f15018d02..004690b67a 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,19 +154,23 @@ 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 (item.isChange || item.isHidden) { + return; + } + if (widget.onSelect != null) { widget.onSelect!(item.address); return; @@ -176,9 +178,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 +194,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 +206,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 6215e26c35..9a9fa1152a 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 bc0ecece1c..ebf952a565 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/store/settings_store.dart b/lib/store/settings_store.dart index 1ecaf50cc9..4ff3782eac 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -116,6 +116,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, @@ -544,6 +545,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) => @@ -780,6 +786,9 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool silentPaymentsKeyRegistered; + @observable bool mwebAlwaysScan; @@ -959,6 +968,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; @@ -1234,6 +1245,7 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + silentPaymentsKeyRegistered: silentPaymentsKeyRegistered, mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, @@ -1402,6 +1414,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; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index cbdad85b88..2849b77ec8 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 69011aa746..757c115a99 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -132,7 +132,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth) return false; - if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.matic) + if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.matic) return false; return true; @@ -419,7 +419,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // // state = FailureState(errorMsg); // } else { - state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); + state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency)); // } } return null; @@ -495,18 +495,18 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor nano!.updateTransactions(wallet); } - if (pendingTransaction!.id.isNotEmpty) { - final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}'; _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: descriptionKey, - recipientAddress: address, - transactionNote: note)) + id: descriptionKey, + recipientAddress: address, + transactionNote: note, + )) : await transactionDescriptionBox.add(TransactionDescription( - id: descriptionKey, - transactionNote: note)); + id: descriptionKey, + transactionNote: note, + )); } state = TransactionCommitted(); 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 5d20230d27..d7350e07a5 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/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index 725b1ddbf2..4ae99d05de 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -4,6 +4,7 @@ class WalletAddressListItem extends ListItem { WalletAddressListItem({ required this.address, required this.isPrimary, + this.derivationPath = "", this.id, this.name, this.txCount, @@ -18,6 +19,7 @@ class WalletAddressListItem extends ListItem { final int? id; final bool isPrimary; final String address; + final String derivationPath; final String? name; final int? txCount; final String? balance; diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 3e399266a6..d263b2a11b 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -31,8 +31,7 @@ import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; -class WalletAddressListViewModel = WalletAddressListViewModelBase - with _$WalletAddressListViewModel; +class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel; abstract class PaymentURI { PaymentURI({required this.amount, required this.address}); @@ -205,8 +204,7 @@ class WowneroURI extends PaymentURI { } } -abstract class WalletAddressListViewModelBase - extends WalletChangeListenerViewModel with Store { +abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, required this.yatStore, @@ -227,8 +225,7 @@ abstract class WalletAddressListViewModelBase _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven] - .contains(wallet.type); + hasAccounts = [WalletType.monero, WalletType.wownero, WalletType.haven].contains(wallet.type); } static const String _cryptoNumberPattern = '0.00000000'; @@ -241,8 +238,7 @@ abstract class WalletAddressListViewModelBase double? _fiatRate; String _rawAmount = ''; - List get currencies => - [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; String get buttonTitle { if (isElectrumWallet) { @@ -268,8 +264,8 @@ abstract class WalletAddressListViewModelBase WalletType get type => wallet.type; @computed - WalletAddressListItem get address => WalletAddressListItem( - address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address => + WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); @computed PaymentURI get uri { @@ -313,10 +309,8 @@ abstract class WalletAddressListViewModelBase final addressList = ObservableList(); if (wallet.type == WalletType.monero) { - final primaryAddress = - monero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = monero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = monero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -332,10 +326,8 @@ abstract class WalletAddressListViewModelBase } if (wallet.type == WalletType.wownero) { - final primaryAddress = - wownero!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -348,10 +340,8 @@ abstract class WalletAddressListViewModelBase } if (wallet.type == WalletType.haven) { - final primaryAddress = - haven!.getSubaddressList(wallet).subaddresses.first; - final addressItems = - haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final primaryAddress = haven!.getSubaddressList(wallet).subaddresses.first; + final addressItems = haven!.getSubaddressList(wallet).subaddresses.map((subaddress) { final isPrimary = subaddress == primaryAddress; return WalletAddressListItem( @@ -365,14 +355,14 @@ abstract class WalletAddressListViewModelBase if (isElectrumWallet) { if (bitcoin!.hasSelectedSilentPayments(wallet)) { - final addressItems = - bitcoin!.getSilentPaymentAddresses(wallet).map((address) { + final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; return WalletAddressListItem( id: address.id, isPrimary: isPrimary, name: address.name, + derivationPath: address.derivationPath, address: address.address, txCount: address.txCount, balance: AmountConverter.amountIntToString( @@ -390,6 +380,7 @@ abstract class WalletAddressListViewModelBase isPrimary: false, name: address.name, address: address.address, + derivationPath: address.derivationPath, txCount: address.txCount, balance: AmountConverter.amountIntToString( walletTypeToCryptoCurrency(type), address.balance), @@ -407,6 +398,7 @@ abstract class WalletAddressListViewModelBase isPrimary: isPrimary, name: subaddress.name, address: subaddress.address, + derivationPath: subaddress.derivationPath, txCount: subaddress.txCount, balance: AmountConverter.amountIntToString( walletTypeToCryptoCurrency(type), subaddress.balance), @@ -417,8 +409,7 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.litecoin && addressItems.length >= 1000) { // find the index of the last item with a txCount > 0 final addressItemsList = addressItems.toList(); - int index = addressItemsList - .lastIndexWhere((item) => (item.txCount ?? 0) > 0); + int index = addressItemsList.lastIndexWhere((item) => (item.txCount ?? 0) > 0); if (index == -1) { index = 0; } @@ -432,22 +423,19 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.ethereum) { final primaryAddress = ethereum!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.polygon) { final primaryAddress = polygon!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } if (wallet.type == WalletType.nano) { @@ -461,21 +449,18 @@ abstract class WalletAddressListViewModelBase if (wallet.type == WalletType.tron) { final primaryAddress = tron!.getAddress(wallet); - addressList.add(WalletAddressListItem( - isPrimary: true, name: null, address: primaryAddress)); + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isHidden = wallet - .walletAddresses.hiddenAddresses + (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses .contains((addressList[i] as WalletAddressListItem).address); } for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; - (addressList[i] as WalletAddressListItem).isManual = wallet - .walletAddresses.manualAddresses + (addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses .contains((addressList[i] as WalletAddressListItem).address); } @@ -493,8 +478,7 @@ abstract class WalletAddressListViewModelBase Future toggleHideAddress(WalletAddressListItem item) async { if (item.isHidden) { - wallet.walletAddresses.hiddenAddresses - .removeWhere((element) => element == item.address); + wallet.walletAddresses.hiddenAddresses.removeWhere((element) => element == item.address); } else { wallet.walletAddresses.hiddenAddresses.add(item.address); } @@ -543,28 +527,22 @@ abstract class WalletAddressListViewModelBase ].contains(wallet.type); @computed - bool get isElectrumWallet => [ - WalletType.bitcoin, - WalletType.litecoin, - WalletType.bitcoinCash - ].contains(wallet.type); + bool get isElectrumWallet => + [WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type); @computed bool get isBalanceAvailable => isElectrumWallet; @computed - bool get isReceivedAvailable => - [WalletType.monero, WalletType.wownero].contains(wallet.type); + bool get isReceivedAvailable => [WalletType.monero, WalletType.wownero].contains(wallet.type); @computed bool get isSilentPayments => - wallet.type == WalletType.bitcoin && - bitcoin!.hasSelectedSilentPayments(wallet); + wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet); @computed bool get isAutoGenerateSubaddressEnabled => - _settingsStore.autoGenerateSubaddressStatus != - AutoGenerateSubaddressStatus.disabled && + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled && !isSilentPayments; @computed @@ -647,8 +625,7 @@ abstract class WalletAddressListViewModelBase @action void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); - final fiatRate = - _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); + final fiatRate = _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0); if (fiatRate <= 0.0) { dev.log("Invalid Fiat Rate $fiatRate"); diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 17a8d6d28e..d5f11ad5fa 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -8,7 +8,6 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -101,6 +100,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, ); @@ -192,7 +192,7 @@ abstract class WalletCreationVMBase with Store { Future> getDerivationInfoFromQRCredentials( RestoredWallet restoreWallet) async { - var list = []; + final list = []; final walletType = restoreWallet.type; var appStore = getIt.get(); var node = appStore.settingsStore.getCurrentNode(walletType); @@ -200,16 +200,11 @@ abstract class WalletCreationVMBase with Store { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( + return await bitcoin!.getDerivationInfosFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, node: node, passphrase: restoreWallet.passphrase, ); - - if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) - return []; - return derivationList; - case WalletType.nano: return nanoUtil!.getDerivationsFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index d37b69f746..4c17998f41 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -97,7 +97,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { switch (type) { case WalletType.monero: return monero!.createMoneroRestoreWalletFromSeedCredentials( - name: name, height: height, mnemonic: seed, password: password); + name: name, + height: height, + mnemonic: seed, + password: password, + ); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromSeedCredentials( @@ -105,8 +109,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, passphrase: passphrase, - derivationType: derivationInfo!.derivationType!, - derivationPath: derivationInfo.derivationPath!, + derivations: options["derivations"] as List?, ); case WalletType.haven: return haven!.createHavenRestoreWalletFromSeedCredentials( @@ -256,11 +259,16 @@ 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 list = await bitcoin!.getDerivationInfosFromMnemonic( mnemonic: mnemonic!, node: node, passphrase: passphrase, ); + + // is restoring? = add old used derivations + final oldList = bitcoin!.getOldDerivationInfos(list); + + return oldList; 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 fa70b5837d..5633d34464 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -135,8 +135,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 9ebab6b6f3..69c7f73c76 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -723,6 +723,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 91256938db..ca1b17ec61 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -723,6 +723,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 0fe38166c8..f5e86ecdbc 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -723,6 +723,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 212ce05f73..d33aa8cc1e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -724,6 +724,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 15e0c04b31..3669bd64d5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -723,6 +723,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 83c0a09f0e..abdf69b44a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -724,6 +724,7 @@ "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": "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", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 549ec52759..0661582f3d 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -723,6 +723,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 222a9cf2fc..76a73ca8e6 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -725,6 +725,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 1b2f3a1b35..ee0f77da78 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -725,6 +725,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 4e106fdf6f..9ce575ec10 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -723,6 +723,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 40142ca5be..2d3c336e33 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -723,6 +723,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 04336b76eb..5b1c1c993c 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -726,6 +726,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 ad15ab3a99..9daafca74f 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -725,6 +725,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 520a73ade5..31882a7fb4 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -724,6 +724,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 1d97488667..905b1fb8e0 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -724,6 +724,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 e6be67060f..52df239642 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -723,6 +723,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 82c3899d4f..0b9d1e7ad9 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -723,6 +723,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 ed54624bfa..4243df066a 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -723,6 +723,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 7b43b5b123..8699a106ab 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -377,7 +377,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": "Ativar Mweb", "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", @@ -725,6 +725,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", @@ -955,4 +956,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 2795e1ffcf..0f1974f29d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -724,6 +724,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 5968616464..96dcb91b41 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -723,6 +723,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 f4678e00d0..7c5ff15d30 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -723,6 +723,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 d8b3ae3cf9..53e288ae5c 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -723,6 +723,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 f27300c180..3bcdf74ff2 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -724,6 +724,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 00f336d722..2742e21bb9 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -725,6 +725,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 1291b505ef..a805809c7f 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -722,6 +722,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 c9275b018d..e4dc693635 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -724,6 +724,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 e508d3c2c0..90b67252cf 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -723,6 +723,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/tool/configure.dart b/tool/configure.dart index f0f79cfb1a..9254fe3115 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -76,7 +76,6 @@ Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ import 'dart:io' show Platform; -import 'dart:typed_data'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; import 'package:cake_wallet/view_model/send/output.dart'; @@ -99,23 +98,20 @@ import 'package:cw_core/get_height_by_date.dart'; import 'package:hive/hive.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; 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'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/bitcoin_receive_page_option.dart'; import 'package:cw_bitcoin/bitcoin_wallet.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; 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'; @@ -126,21 +122,40 @@ import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ - - class ElectrumSubAddress { +const List BITCOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + P2pkhAddressType.p2pkh, + SegwitAddresType.p2tr, + SegwitAddresType.p2wsh, + P2shAddressType.p2wpkhInP2sh, +]; + +const List LITECOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, +]; + +const List BITCOIN_CASH_ADDRESS_TYPES = [ + P2pkhAddressType.p2pkh, +]; + +class ElectrumSubAddress { ElectrumSubAddress({ required this.id, required this.name, required this.address, required this.txCount, required this.balance, - required this.isChange}); + required this.isChange, + required this.derivationPath, + }); final int id; final String name; final String address; final int txCount; final int balance; final bool isChange; + final String derivationPath; } abstract class Bitcoin { @@ -150,8 +165,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}); @@ -181,10 +195,22 @@ abstract class Bitcoin { String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); + List getOldSPDerivationInfos(); 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 +218,8 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPrioritySlow(); Future> compareDerivationMethods( {required String mnemonic, required Node node}); - Future> getDerivationsFromMnemonic( + List getOldDerivationInfos(List list); + Future> getDerivationInfosFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); @@ -216,6 +243,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}); @@ -1030,9 +1058,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'; @@ -1050,7 +1075,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});