From e47846bc7d7d4a2e4c13266a4c615cea7ae23a2a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 4 Oct 2024 11:49:49 -0300 Subject: [PATCH 01/64] feat: begin delegated scan, big refactors --- cw_bitcoin/lib/bitcoin_address_record.dart | 113 ++++-- .../lib/bitcoin_hardware_wallet_service.dart | 13 +- cw_bitcoin/lib/bitcoin_unspent.dart | 39 -- cw_bitcoin/lib/bitcoin_wallet.dart | 47 +-- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 45 ++- cw_bitcoin/lib/electrum.dart | 40 +- cw_bitcoin/lib/electrum_transaction_info.dart | 5 +- cw_bitcoin/lib/electrum_wallet.dart | 354 ++++++++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 173 +++++---- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 5 +- cw_bitcoin/lib/litecoin_wallet.dart | 30 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 32 +- cw_bitcoin/lib/utils.dart | 54 --- .../lib/src/bitcoin_cash_wallet.dart | 25 +- .../src/bitcoin_cash_wallet_addresses.dart | 16 +- lib/bitcoin/cw_bitcoin.dart | 19 +- lib/entities/preferences_key.dart | 1 + .../screens/receive/widgets/address_list.dart | 39 +- .../settings/silent_payments_settings.dart | 7 + lib/store/settings_store.dart | 27 +- .../silent_payments_settings_view_model.dart | 6 + res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 2 +- 50 files changed, 648 insertions(+), 472 deletions(-) delete mode 100644 cw_bitcoin/lib/utils.dart diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 7e4b5f58f0..2c3abad0ff 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -6,13 +6,12 @@ abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( this.address, { required this.index, - this.isHidden = false, + this.isChange = false, int txCount = 0, int balance = 0, String name = '', bool isUsed = false, required this.type, - required this.network, }) : _txCount = txCount, _balance = balance, _name = name, @@ -22,13 +21,12 @@ abstract class BaseBitcoinAddressRecord { bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; final String address; - bool isHidden; + bool isChange; final int index; int _txCount; int _balance; String _name; bool _isUsed; - BasedUtxoNetwork? network; int get txCount => _txCount; @@ -56,24 +54,29 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { BitcoinAddressRecord( super.address, { required super.index, - super.isHidden = false, + super.isChange = false, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, required super.type, String? scriptHash, - required super.network, - }) : scriptHash = scriptHash ?? - (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); + BasedUtxoNetwork? network, + }) { + if (scriptHash == null && network == null) { + throw ArgumentError('either scriptHash or network must be provided'); + } - factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); + } + + factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, + isChange: decoded['isChange'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, name: decoded['name'] as String? ?? '', @@ -83,23 +86,16 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { .firstWhere((type) => type.toString() == decoded['type'] as String) : SegwitAddresType.p2wpkh, scriptHash: decoded['scriptHash'] as String?, - network: network, ); } - String? scriptHash; - - String getScriptHash(BasedUtxoNetwork network) { - if (scriptHash != null) return scriptHash!; - scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); - return scriptHash!; - } + late String scriptHash; @override String toJSON() => json.encode({ 'address': address, 'index': index, - 'isHidden': isHidden, + 'isChange': isChange, 'isUsed': isUsed, 'txCount': txCount, 'name': name, @@ -110,18 +106,23 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { + int get labelIndex => index; + final String? labelHex; + BitcoinSilentPaymentAddressRecord( super.address, { - required super.index, - super.isHidden = false, + required int labelIndex, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, - required this.silentPaymentTweak, - required super.network, - required super.type, - }) : super(); + super.type = SilentPaymentsAddresType.p2sp, + this.labelHex, + }) : super(index: labelIndex, isChange: labelIndex == 0) { + if (labelIndex != 1 && labelHex == null) { + throw ArgumentError('label must be provided for silent address index > 0'); + } + } factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { @@ -129,36 +130,68 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, - index: decoded['index'] as int, - isHidden: decoded['isHidden'] as bool? ?? false, + labelIndex: decoded['labelIndex'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, name: decoded['name'] as String? ?? '', balance: decoded['balance'] as int? ?? 0, - network: (decoded['network'] as String?) == null - ? network - : BasedUtxoNetwork.fromName(decoded['network'] as String), - silentPaymentTweak: decoded['silent_payment_tweak'] as String?, - type: decoded['type'] != null && decoded['type'] != '' - ? BitcoinAddressType.values - .firstWhere((type) => type.toString() == decoded['type'] as String) - : SilentPaymentsAddresType.p2sp, + labelHex: decoded['labelHex'] as String?, ); } - final String? silentPaymentTweak; + @override + String toJSON() => json.encode({ + 'address': address, + 'labelIndex': labelIndex, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.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.type = SegwitAddresType.p2tr, + super.labelHex, + }); + + factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + final decoded = json.decode(jsonSource) as Map; + + return BitcoinReceivedSPAddressRecord( + decoded['address'] as String, + labelIndex: decoded['index'] as int, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + labelHex: decoded['label'] as String?, + spendKey: ECPrivate.fromHex(decoded['spendKey'] as String), + ); + } @override String toJSON() => json.encode({ 'address': address, - 'index': index, - 'isHidden': isHidden, + 'labelIndex': labelIndex, 'isUsed': isUsed, 'txCount': txCount, 'name': name, 'balance': balance, 'type': type.toString(), - 'network': network?.value, - 'silent_payment_tweak': silentPaymentTweak, + 'labelHex': labelHex, + 'spend_key': spendKey.toString(), }); } diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index a02c51c69b..582147e3d2 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,16 +21,13 @@ class BitcoinHardwareWalletService { for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; - final xpub = - await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); + 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); - accounts.add(HardwareAccountData( - address: address, + address: P2wpkhAddress.fromBip32(bip32: hd, account: i, index: 0) + .toAddress(BitcoinNetwork.mainnet), accountIndex: i, derivationPath: derivationPath, masterFingerprint: masterFp, diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 3691a7a22a..d3421980ae 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -26,42 +26,3 @@ 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; - } - - String? silentPaymentTweak; - String? silentPaymentLabel; -} diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 9088978459..029b6f2417 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -61,16 +61,10 @@ 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, ) { - // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here) - // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here) - // String derivationPath = walletInfo.derivationInfo!.derivationPath!; - // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1"; - // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType); walletAddresses = BitcoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -78,17 +72,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), + bip32: bip32, network: networkParam ?? network, - masterHd: - seedBytes != null ? Bip32Slip10Secp256k1.fromSeed(seedBytes) : null, - isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { - this.walletAddresses.isEnabledAutoGenerateSubaddress = - this.isEnabledAutoGenerateSubaddress; + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); } @@ -189,10 +178,8 @@ 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; final mnemonic = keysData.mnemonic; @@ -260,10 +247,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()]!; + final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash); + final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( utxo: utxo.utxo, @@ -275,8 +260,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,17 +271,15 @@ 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); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 04a3cae361..a6f047fa13 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,7 +1,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package: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,8 +11,7 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, + required super.bip32, required super.network, required super.isHardwareWallet, super.initialAddresses, @@ -21,24 +19,33 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialChangeAddressIndex, super.initialSilentAddresses, super.initialSilentAddressIndex = 0, - super.masterHd, }) : super(walletInfo); @override - String getAddress( - {required int index, required Bip32Slip10Secp256k1 hd, BitcoinAddressType? addressType}) { - if (addressType == P2pkhAddressType.p2pkh) - return generateP2PKHAddress(hd: hd, index: index, network: network); - - if (addressType == SegwitAddresType.p2tr) - return generateP2TRAddress(hd: hd, index: index, network: network); - - if (addressType == SegwitAddresType.p2wsh) - return generateP2WSHAddress(hd: hd, index: index, network: network); - - if (addressType == P2shAddressType.p2wpkhInP2sh) - return generateP2SHAddress(hd: hd, index: index, network: network); - - return generateP2WPKHAddress(hd: hd, index: index, network: network); + BitcoinBaseAddress generateAddress({ + required int account, + required int index, + required Bip32Slip10Secp256k1 hd, + required BitcoinAddressType addressType, + }) { + switch (addressType) { + case P2pkhAddressType.p2pkh: + return P2pkhAddress.fromBip32(account: account, bip32: hd, index: index); + case SegwitAddresType.p2tr: + return P2trAddress.fromBip32(account: account, bip32: hd, index: index); + case SegwitAddresType.p2wsh: + return P2wshAddress.fromBip32(account: account, bip32: hd, index: index); + case P2shAddressType.p2wpkhInP2sh: + return P2shAddress.fromBip32( + account: account, + bip32: hd, + index: index, + type: P2shAddressType.p2wpkhInP2sh, + ); + case SegwitAddresType.p2wpkh: + return P2wpkhAddress.fromBip32(account: account, bip32: hd, index: index); + default: + throw ArgumentError('Invalid address type'); + } } } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index a18c038fa5..0a963bd6f2 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -317,13 +317,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, false], + ); + + Future tweaksRegister({ + required String secViewKey, + required String pubSpendKey, + List labels = const [], + }) => + call( + method: 'blockchain.tweaks.subscribe', + 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]); @@ -527,6 +552,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_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 1ab7799e3e..f5857437c0 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -22,7 +22,7 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { - List? unspents; + List? unspents; bool isReceivedSilentPayment; ElectrumTransactionInfo( @@ -208,8 +208,7 @@ class ElectrumTransactionInfo extends TransactionInfo { outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(), to: data['to'] as String?, unspents: unspents - .map((unspent) => - BitcoinSilentPaymentsUnspent.fromJSON(null, unspent as Map)) + .map((unspent) => BitcoinUnspent.fromJSON(null, unspent as Map)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, ); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index c05095cf17..ee4e7d7bb0 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/bitcoin_wallet.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -23,7 +22,6 @@ 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'; @@ -52,10 +50,9 @@ part 'electrum_wallet.g.dart'; class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; -abstract class ElectrumWalletBase extends WalletBase< - ElectrumBalance, - ElectrumTransactionHistory, - ElectrumTransactionInfo> with Store, WalletKeysFile { +abstract class ElectrumWalletBase + extends WalletBase + with Store, WalletKeysFile { ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -71,8 +68,7 @@ abstract class ElectrumWalletBase extends WalletBase< ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - }) : accountHD = getAccountHDWallet( - currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : bip32 = getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, _feeRates = [], @@ -107,12 +103,8 @@ abstract class ElectrumWalletBase extends WalletBase< sharedPrefs.complete(SharedPreferences.getInstance()); } - static Bip32Slip10Secp256k1 getAccountHDWallet( - CryptoCurrency? currency, - BasedUtxoNetwork network, - Uint8List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, + Uint8List? 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"); @@ -123,10 +115,7 @@ abstract class ElectrumWalletBase extends WalletBase< case CryptoCurrency.btc: case CryptoCurrency.ltc: case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)) - .derivePath(_hardenedDerivationPath( - derivationInfo?.derivationPath ?? electrum_path)) - as Bip32Slip10Secp256k1; + return Bip32Slip10Secp256k1.fromSeed(seedBytes); case CryptoCurrency.bch: return bitcoinCashHDWallet(seedBytes); default: @@ -134,13 +123,11 @@ abstract class ElectrumWalletBase extends WalletBase< } } - return Bip32Slip10Secp256k1.fromExtendedKey( - xpub!, getKeyNetVersion(network)); + return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") - as Bip32Slip10Secp256k1; + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; static int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; @@ -156,13 +143,9 @@ abstract class ElectrumWalletBase extends WalletBase< bool? alwaysScan; - final Bip32Slip10Secp256k1 accountHD; + final Bip32Slip10Secp256k1 bip32; final String? _mnemonic; - Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); - - Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); - final EncryptionFileUtils encryptionFileUtils; @override @@ -193,16 +176,16 @@ abstract class ElectrumWalletBase extends WalletBase< 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; @@ -235,6 +218,36 @@ abstract class ElectrumWalletBase extends WalletBase< return isMempoolAPIEnabled; } + // @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 setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; @@ -286,9 +299,9 @@ abstract class ElectrumWalletBase extends WalletBase< @override BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), - privateKey: hd.privateKey.toHex(), - publicKey: hd.publicKey.toHex(), + wif: WifEncoder.encode(bip32.privateKey.raw, netVer: network.wifNetVer), + privateKey: bip32.privateKey.toHex(), + publicKey: bip32.publicKey.toHex(), ); String _password; @@ -337,7 +350,7 @@ abstract class ElectrumWalletBase extends WalletBase< final receivePort = ReceivePort(); _isolate = Isolate.spawn( - startRefresh, + delegatedScan, ScanData( sendPort: receivePort.sendPort, silentAddress: walletAddresses.silentAddress!, @@ -351,8 +364,8 @@ abstract class ElectrumWalletBase extends WalletBase< : null, labels: walletAddresses.labels, labelIndexes: walletAddresses.silentAddresses - .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) - .map((addr) => addr.index) + .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, )); @@ -433,17 +446,17 @@ abstract class ElectrumWalletBase extends WalletBase< }); } - void _updateSilentAddressRecord(BitcoinSilentPaymentsUnspent unspent) { + void _updateSilentAddressRecord(BitcoinUnspent unspent) { + final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; final silentAddress = walletAddresses.silentAddress!; final silentPaymentAddress = SilentPaymentAddress( version: silentAddress.version, B_scan: silentAddress.B_scan, - B_spend: unspent.silentPaymentLabel != null + B_spend: receiveAddressRecord.labelHex != null ? silentAddress.B_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(unspent.silentPaymentLabel!)), + BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)), ) : silentAddress.B_spend, - network: network, ); final addressRecord = walletAddresses.silentAddresses @@ -652,22 +665,16 @@ abstract class ElectrumWalletBase extends WalletBase< ECPrivate? privkey; bool? isSilentPayment = false; - final hd = utx.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd; - - if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { - final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; - privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( - BigintUtils.fromBytes( - BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), - ), - ); + if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) { + privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey; spendsSilentPayment = true; isSilentPayment = true; } else if (!isHardwareWallet) { - privkey = - generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + privkey = ECPrivate.fromBip32( + bip32: walletAddresses.bip32, + account: utx.bitcoinAddressRecord.isChange ? 1 : 0, + index: utx.bitcoinAddressRecord.index, + ); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -682,12 +689,15 @@ abstract class ElectrumWalletBase extends WalletBase< pubKeyHex = privkey.getPublic().toHex(); } else { - pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); + pubKeyHex = walletAddresses.bip32 + .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)) + .publicKey + .toHex(); } final derivationPath = "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}" - "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.isChange ? "1" : "0"}" "/${utx.bitcoinAddressRecord.index}"; publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); @@ -1233,8 +1243,7 @@ abstract class ElectrumWalletBase extends WalletBase< } } - void setLedgerConnection(ledger.LedgerConnection connection) => - throw UnimplementedError(); + void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); Future buildHardwareWalletTransaction({ required List outputs, @@ -1467,13 +1476,13 @@ abstract class ElectrumWalletBase extends WalletBase< List> unspents = []; List updatedUnspentCoins = []; - unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + unspents = await electrumClient.getListUnspent(address.scriptHash); await Future.wait(unspents.map((unspent) async { try { final coin = BitcoinUnspent.fromJSON(address, unspent); final tx = await fetchTransactionInfo(hash: coin.hash); - coin.isChange = address.isHidden; + coin.isChange = address.isChange; coin.confirmations = tx?.confirmations; updatedUnspentCoins.add(coin); @@ -1495,7 +1504,7 @@ abstract class ElectrumWalletBase extends WalletBase< value: coin.value, vout: coin.vout, isChange: coin.isChange, - isSilentPayment: coin is BitcoinSilentPaymentsUnspent, + isSilentPayment: coin.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord, ); await unspentCoinsInfo.add(newInfo); @@ -1543,7 +1552,7 @@ abstract class ElectrumWalletBase extends WalletBase< final bundle = await getTransactionExpanded(hash: txId); final outputs = bundle.originalTransaction.outputs; - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); // look for a change address in the outputs final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any( @@ -1592,12 +1601,11 @@ abstract class ElectrumWalletBase extends WalletBase< 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 privkey = ECPrivate.fromBip32( + bip32: walletAddresses.bip32, + account: addressRecord.isChange ? 1 : 0, + index: addressRecord.index, + ); privateKeys.add(privkey); @@ -1672,7 +1680,7 @@ abstract class ElectrumWalletBase extends WalletBase< } // Identify all change outputs - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden); + final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); final List changeOutputs = outputs .where((output) => changeAddresses .any((element) => element.address == output.address.toAddress(network))) @@ -1777,8 +1785,7 @@ abstract class ElectrumWalletBase extends WalletBase< if (height != null) { if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000) - .round(); + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } if (confirmations == null) { @@ -1879,8 +1886,8 @@ abstract class ElectrumWalletBase extends WalletBase< 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); + final hiddenAddresses = addressesByType.where((addr) => addr.isChange == true); + final receiveAddresses = addressesByType.where((addr) => addr.isChange == false); walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); await walletAddresses.saveAddressesInBox(); await Future.wait(addressesByType.map((addressRecord) async { @@ -1890,10 +1897,10 @@ abstract class ElectrumWalletBase extends WalletBase< addressRecord.txCount = history.length; historiesWithDetails.addAll(history); - final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + final matchedAddresses = addressRecord.isChange ? hiddenAddresses : receiveAddresses; final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= matchedAddresses.length - - (addressRecord.isHidden + (addressRecord.isChange ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); @@ -1903,7 +1910,7 @@ abstract class ElectrumWalletBase extends WalletBase< // Discover new addresses for the same address type until the gap limit is respected await walletAddresses.discoverAddresses( matchedAddresses.toList(), - addressRecord.isHidden, + addressRecord.isChange, (address) async { await subscribeForUpdates(); return _fetchAddressHistory(address, await getCurrentChainTip()) @@ -1929,7 +1936,7 @@ abstract class ElectrumWalletBase extends WalletBase< try { final Map historiesWithDetails = {}; - final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); + final history = await electrumClient.getHistory(addressRecord.scriptHash); if (history.isNotEmpty) { addressRecord.setAsUsed(); @@ -2010,12 +2017,12 @@ abstract class ElectrumWalletBase extends WalletBase< Future subscribeForUpdates() async { final unsubscribedScriptHashes = walletAddresses.allAddresses.where( (address) => - !_scripthashesUpdateSubject.containsKey(address.getScriptHash(network)) && + !_scripthashesUpdateSubject.containsKey(address.scriptHash) && address.type != SegwitAddresType.mweb, ); await Future.wait(unsubscribedScriptHashes.map((address) async { - final sh = address.getScriptHash(network); + final sh = address.scriptHash; if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { try { await _scripthashesUpdateSubject[sh]?.close(); @@ -2054,7 +2061,7 @@ abstract class ElectrumWalletBase extends WalletBase< final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = addressRecord.getScriptHash(network); + final sh = addressRecord.scriptHash; final balanceFuture = electrumClient.getBalance(sh); balanceFutures.add(balanceFuture); } @@ -2095,15 +2102,17 @@ abstract class ElectrumWalletBase extends WalletBase< 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(); - } + try { + 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(); + } + } catch (_) {} } return ElectrumBalance( @@ -2124,10 +2133,17 @@ abstract class ElectrumWalletBase extends WalletBase< @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()); String messagePrefix = '\x18Bitcoin Signed Message:\n'; @@ -2223,7 +2239,6 @@ abstract class ElectrumWalletBase extends WalletBase< @action void _onConnectionStatusChange(ConnectionStatus status) { - switch (status) { case ConnectionStatus.connected: if (syncStatus is NotConnectedSyncStatus || @@ -2396,6 +2411,137 @@ class SyncResponse { SyncResponse(this.height, this.syncStatus); } +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()), + ); + } +} + Future startRefresh(ScanData scanData) async { int syncHeight = scanData.height; int initialSyncHeight = syncHeight; @@ -2528,26 +2674,18 @@ Future startRefresh(ScanData scanData) async { return false; }); - final receivedAddressRecord = BitcoinSilentPaymentAddressRecord( + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( receivingOutputAddress, - index: 0, - isHidden: false, + labelIndex: 0, isUsed: true, - network: scanData.network, - silentPaymentTweak: t_k, - type: SegwitAddresType.p2tr, + spendKey: scanData.silentAddress.b_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), + ), txCount: 1, balance: amount!, ); - final unspent = BitcoinSilentPaymentsUnspent( - receivedAddressRecord, - txid, - amount!, - pos!, - silentPaymentTweak: t_k, - silentPaymentLabel: label == "None" ? null : label, - ); + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount!, pos!); txInfo.unspents!.add(unspent); txInfo.amount += unspent.value; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index c29579436a..68de355bd3 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -33,8 +33,7 @@ const List BITCOIN_CASH_ADDRESS_TYPES = [ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { - required this.mainHd, - required this.sideHd, + required this.bip32, required this.network, required this.isHardwareWallet, List? initialAddresses, @@ -43,17 +42,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, List? initialMwebAddresses, - Bip32Slip10Secp256k1? masterHd, BitcoinAddressType? initialAddressPageType, - }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), addressesByReceiveType = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) + .where((addressRecord) => !addressRecord.isChange && !addressRecord.isUsed) .toSet()), changeAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) + .where((addressRecord) => addressRecord.isChange && !addressRecord.isUsed) .toSet()), currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, @@ -67,33 +64,25 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { mwebAddresses = ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { - if (masterHd != null) { - silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(masterHd.derivePath(SCAN_PATH).privateKey.toHex()), - b_spend: ECPrivate.fromHex(masterHd.derivePath(SPEND_PATH).privateKey.toHex()), - network: network, - ); - - 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, - )); - } + silentAddress = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate.fromHex(bip32.derive(SCAN_PATH).privateKey.toHex()), + b_spend: ECPrivate.fromHex(bip32.derive(SPEND_PATH).privateKey.toHex()), + network: network, + ); + if (silentAddresses.length == 0) { + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress.toString(), + labelIndex: 1, + name: "", + type: SilentPaymentsAddresType.p2sp, + )); + silentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentAddress!.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + )); } updateAddressesByMatch(); @@ -112,9 +101,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; - final Bip32Slip10Secp256k1 mainHd; - final Bip32Slip10Secp256k1 sideHd; - final bool isHardwareWallet; + final Bip32Slip10Secp256k1 bip32; @observable SilentPaymentOwner? silentAddress; @@ -174,31 +161,37 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return; } if (addressPageType == SilentPaymentsAddresType.p2sp) { - final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + late BitcoinSilentPaymentAddressRecord selected; + try { + selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + } catch (_) { + selected = silentAddresses[0]; + } - if (selected.silentPaymentTweak != null && silentAddress != null) { + if (selected.labelHex != null && silentAddress != null) { activeSilentAddress = - silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString(); + silentAddress!.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); } else { activeSilentAddress = silentAddress!.toString(); } return; } try { - final addressRecord = _addresses.firstWhere( - (addressRecord) => addressRecord.address == addr, - ); + final addressRecord = _addresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); - previousAddressRecord = addressRecord; - receiveAddresses.remove(addressRecord); - receiveAddresses.insert(0, addressRecord); + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); } catch (e) { print("ElectrumWalletAddressBase: set address ($addr): $e"); } } @override - String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType); + String get primaryAddress => + getAddress(account: 0, index: 0, hd: bip32, addressType: addressPageType); Map currentReceiveAddressIndexByType; @@ -223,7 +216,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { + if (!addressRecord.isChange) { return acc + 1; } return acc; @@ -231,7 +224,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; @@ -272,7 +265,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress( + {List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); if (changeAddresses.isEmpty) { @@ -297,7 +291,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final labels = {}; for (int i = 0; i < silentAddresses.length; i++) { final silentAddressRecord = silentAddresses[i]; - final silentPaymentTweak = silentAddressRecord.silentPaymentTweak; + final silentPaymentTweak = silentAddressRecord.labelHex; if (silentPaymentTweak != null && SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { @@ -321,12 +315,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final address = BitcoinSilentPaymentAddressRecord( silentAddress!.toLabeledSilentPaymentAddress(currentSilentAddressIndex).toString(), - index: currentSilentAddressIndex, - isHidden: false, + labelIndex: currentSilentAddressIndex, name: label, - silentPaymentTweak: - BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), - network: network, + labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), type: SilentPaymentsAddresType.p2sp, ); @@ -337,12 +328,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } 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 address = BitcoinAddressRecord( - getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType), + getAddress(account: 0, index: newAddressIndex, hd: bip32, addressType: addressPageType), index: newAddressIndex, - isHidden: false, + isChange: false, name: label, type: addressPageType, network: network, @@ -352,19 +343,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } + BitcoinBaseAddress generateAddress({ + required int account, + required int index, + required Bip32Slip10Secp256k1 hd, + required BitcoinAddressType addressType, + }) { + throw UnimplementedError(); + } + String getAddress({ + required int account, required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, - }) => - ''; + required BitcoinAddressType addressType, + }) { + return generateAddress(account: account, index: index, hd: hd, addressType: addressType) + .toAddress(network); + } Future getAddressAsync({ + required int account, required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, + required BitcoinAddressType addressType, }) async => - getAddress(index: index, hd: hd, addressType: addressType); + getAddress(account: account, index: index, hd: hd, addressType: addressType); void addBitcoinAddressTypes() { final lastP2wpkh = _addresses @@ -411,7 +415,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } silentAddresses.forEach((addressRecord) { - if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isHidden) { + if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { return; } @@ -537,7 +541,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); final newAddresses = - _addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + _addresses.where((addressRecord) => !addressRecord.isChange && !addressRecord.isUsed); receiveAddresses.addAll(newAddresses); } @@ -545,7 +549,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); final newAddresses = _addresses.where((addressRecord) => - addressRecord.isHidden && + addressRecord.isChange && !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)); @@ -575,7 +579,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { _addresses.forEach((addr) { if (addr.type == type) { - if (addr.isHidden) { + if (addr.isChange) { countOfHiddenAddresses += 1; return; } @@ -605,9 +609,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( - await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + await getAddressAsync( + account: _getAccount(isHidden), + index: i, + hd: bip32, + addressType: type ?? addressPageType), index: i, - isHidden: isHidden, + isChange: isHidden, type: type ?? addressPageType, network: network, ); @@ -650,14 +658,24 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { // this would add a ton of startup lag for mweb addresses since we have 1000 of them return; } - if (!element.isHidden && + if (!element.isChange && element.address != - await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { - element.isHidden = true; - } else if (element.isHidden && + await getAddressAsync( + account: 0, + index: element.index, + hd: bip32, + addressType: element.type, + )) { + element.isChange = true; + } else if (element.isChange && element.address != - await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { - element.isHidden = false; + await getAddressAsync( + account: 1, + index: element.index, + hd: bip32, + addressType: element.type, + )) { + element.isChange = false; } }); } @@ -674,12 +692,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; - + int _getAccount(bool isHidden) => isHidden ? 1 : 0; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => - !addr.isHidden && !addr.isUsed && addr.type == type; + !addr.isChange && !addr.isUsed && addr.type == type; @action void deleteSilentPaymentAddress(String address) { diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 9907190891..f7c2e1a28e 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -6,7 +6,6 @@ import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/utils/file.dart'; import 'package:cw_core/wallet_type.dart'; class ElectrumWalletSnapshot { @@ -68,7 +67,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 +79,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; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1fb39c8783..1ca89e00a7 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -21,7 +21,6 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -98,8 +97,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, initialMwebAddresses: initialMwebAddresses, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), + bip32: bip32, network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, @@ -1025,12 +1023,11 @@ 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 key = ECPrivate.fromBip32( + bip32: walletAddresses.bip32, + account: utxo.bitcoinAddressRecord.isChange ? 1 : 0, + index: utxo.bitcoinAddressRecord.index, + ); final digest = tx2.getTransactionSegwitDigit( txInIndex: e.key, script: key.getPublic().toP2pkhAddress().toScriptPubKey(), @@ -1113,10 +1110,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( diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index c55f5fc762..b6e7b44288 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -7,7 +7,6 @@ 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'; @@ -23,8 +22,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, + required super.bip32, required super.network, required super.isHardwareWallet, required this.mwebHd, @@ -121,30 +119,34 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses await ensureMwebAddressUpToIndexExists(20); return; } - } - @override - String getAddress({ - required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, - }) { - if (addressType == SegwitAddresType.mweb) { - return hd == sideHd ? mwebAddrs[0] : mwebAddrs[index + 1]; + @override + BitcoinBaseAddress generateAddress({ + required int account, + required int index, + required Bip32Slip10Secp256k1 hd, + required BitcoinAddressType addressType, + }) { + if (addressType == SegwitAddresType.mweb) { + return MwebAddress.fromAddress(address: mwebAddrs[0], network: network); + } + + return P2wpkhAddress.fromBip32(account: account, bip32: hd, index: index); } - return generateP2WPKHAddress(hd: hd, index: index, network: network); } @override Future getAddressAsync({ + required int account, required int index, required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType, + required BitcoinAddressType addressType, }) async { if (addressType == SegwitAddresType.mweb) { await ensureMwebAddressUpToIndexExists(index); } - return getAddress(index: index, hd: hd, addressType: addressType); + + return getAddress(account: account, index: index, hd: hd, addressType: addressType); } @action 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_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index d55914dcde..768c3fb4bd 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -54,8 +54,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, - mainHd: hd, - sideHd: accountHD.childKey(Bip32KeyIndex(1)), + bip32: bip32, network: network, initialAddressPageType: addressPageType, isHardwareWallet: walletInfo.isHardwareWallet, @@ -141,7 +140,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { return BitcoinAddressRecord( addr.address, index: addr.index, - isHidden: addr.isHidden, + isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, ); @@ -149,7 +148,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { return BitcoinAddressRecord( AddressUtils.getCashAddrFormat(addr.address), index: addr.index, - isHidden: addr.isHidden, + isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, ); @@ -209,13 +208,17 @@ 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, 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..ae195bf6b6 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,6 @@ 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,8 +11,7 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinCashWalletAddressesBase( WalletInfo walletInfo, { - required super.mainHd, - required super.sideHd, + required super.bip32, required super.network, required super.isHardwareWallet, super.initialAddresses, @@ -23,9 +21,11 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi }) : super(walletInfo); @override - String getAddress( - {required int index, - required Bip32Slip10Secp256k1 hd, - BitcoinAddressType? addressType}) => - generateP2PKHAddress(hd: hd, index: index, network: network); + BitcoinBaseAddress generateAddress({ + required int account, + required int index, + required Bip32Slip10Secp256k1 hd, + required BitcoinAddressType addressType, + }) => + P2pkhAddress.fromBip32(account: account, bip32: hd, index: index); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 60364c2896..cc7e97cd98 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -144,7 +144,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -379,16 +379,16 @@ class CWBitcoin extends Bitcoin { String? address; switch (dInfoCopy.scriptType) { case "p2wpkh": - address = generateP2WPKHAddress(hd: hd, network: network, index: 0); + address = P2wpkhAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); break; case "p2pkh": - address = generateP2PKHAddress(hd: hd, network: network, index: 0); + address = P2pkhAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); break; case "p2wpkh-p2sh": - address = generateP2SHAddress(hd: hd, network: network, index: 0); + address = P2shAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); break; case "p2tr": - address = generateP2TRAddress(hd: hd, network: network, index: 0); + address = P2trAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); break; default: continue; @@ -526,7 +526,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -541,7 +541,7 @@ class CWBitcoin extends Bitcoin { address: addr.address, txCount: addr.txCount, balance: addr.balance, - isChange: addr.isHidden)) + isChange: addr.isChange)) .toList(); } @@ -574,6 +574,11 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.isTestnet; } + @override + Future registerSilentPaymentsKey(Object wallet, bool active) async { + return; + } + @override Future checkIfMempoolAPIIsEnabled(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 4fbe358e57..b1aa33a10f 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -48,6 +48,7 @@ class PreferencesKey { static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; + static const silentPaymentsKeyRegistered = 'silentPaymentsKeyRegistered'; static const mwebCardDisplay = 'mwebCardDisplay'; static const mwebEnabled = 'mwebEnabled'; static const hasEnabledMwebBefore = 'hasEnabledMwebBefore'; diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 9f15018d02..0d5805e526 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -1,6 +1,3 @@ - -import 'dart:math'; - import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -37,7 +34,6 @@ class AddressList extends StatefulWidget { } class _AddressListState extends State { - bool showHiddenAddresses = false; void _toggleHiddenAddresses() { @@ -131,9 +127,10 @@ class _AddressListState extends State { showTrailingButton: widget.addressListViewModel.showAddManualAddresses, showSearchButton: true, onSearchCallback: updateItems, - trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { - updateItems(); // refresh the new address - }), + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) { + updateItems(); // refresh the new address + }), trailingIcon: Icon( Icons.add, size: 20, @@ -148,7 +145,8 @@ class _AddressListState extends State { cell = Container(); } else { cell = Observer(builder: (_) { - final isCurrent = item.address == widget.addressListViewModel.address.address && editable; + final isCurrent = + item.address == widget.addressListViewModel.address.address && editable; final backgroundColor = isCurrent ? Theme.of(context).extension()!.currentTileBackgroundColor : Theme.of(context).extension()!.tilesBackgroundColor; @@ -156,17 +154,17 @@ class _AddressListState extends State { ? Theme.of(context).extension()!.currentTileTextColor : Theme.of(context).extension()!.tilesTextColor; - return AddressCell.fromItem( item, isCurrent: isCurrent, hasBalance: widget.addressListViewModel.isBalanceAvailable, hasReceived: widget.addressListViewModel.isReceivedAvailable, - // hasReceived: - backgroundColor: (kDebugMode && item.isHidden) ? - Theme.of(context).colorScheme.error : - (kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) : - backgroundColor, + // hasReceived: + backgroundColor: (kDebugMode && item.isHidden) + ? Theme.of(context).colorScheme.error + : (kDebugMode && item.isManual) + ? Theme.of(context).colorScheme.error.withBlue(255) + : backgroundColor, textColor: textColor, onTap: (_) { if (widget.onSelect != null) { @@ -176,9 +174,11 @@ class _AddressListState extends State { widget.addressListViewModel.setAddress(item); }, onEdit: editable - ? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) { - updateItems(); // refresh the new address - }) + ? () => Navigator.of(context) + .pushNamed(Routes.newSubaddress, arguments: item) + .then((value) { + updateItems(); // refresh the new address + }) : null, isHidden: item.isHidden, onHide: () => _hideAddress(item), @@ -190,8 +190,8 @@ class _AddressListState extends State { return index != 0 ? cell : ClipRRect( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(30), topRight: Radius.circular(30)), + borderRadius: + BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)), child: cell, ); }, @@ -202,5 +202,4 @@ class _AddressListState extends State { await widget.addressListViewModel.toggleHideAddress(item); updateItems(); } - } diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index bc0ecece1c..d2a4f3600d 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.silentPaymentsAlwaysScan, + onValueChange: (_, bool value) { + _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(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 9f03c95c30..cd39318f4c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -114,6 +114,7 @@ abstract class SettingsStoreBase with Store { required this.customBitcoinFeeRate, required this.silentPaymentsCardDisplay, required this.silentPaymentsAlwaysScan, + required this.silentPaymentsKeyRegistered, required this.mwebAlwaysScan, required this.mwebCardDisplay, required this.mwebEnabled, @@ -344,8 +345,8 @@ abstract class SettingsStoreBase with Store { reaction( (_) => bitcoinSeedType, - (BitcoinSeedType bitcoinSeedType) => sharedPreferences.setInt( - PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); + (BitcoinSeedType bitcoinSeedType) => + sharedPreferences.setInt(PreferencesKey.bitcoinSeedType, bitcoinSeedType.raw)); reaction( (_) => nanoSeedType, @@ -428,8 +429,10 @@ abstract class SettingsStoreBase with Store { reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); - reaction((_) => useMempoolFeeAPI, - (bool useMempoolFeeAPI) => _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); + reaction( + (_) => useMempoolFeeAPI, + (bool useMempoolFeeAPI) => + _sharedPreferences.setBool(PreferencesKey.useMempoolFeeAPI, useMempoolFeeAPI)); reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -559,6 +562,11 @@ abstract class SettingsStoreBase with Store { (bool silentPaymentsAlwaysScan) => _sharedPreferences.setBool( PreferencesKey.silentPaymentsAlwaysScan, silentPaymentsAlwaysScan)); + reaction( + (_) => silentPaymentsKeyRegistered, + (bool silentPaymentsKeyRegistered) => _sharedPreferences.setBool( + PreferencesKey.silentPaymentsKeyRegistered, silentPaymentsKeyRegistered)); + reaction( (_) => mwebAlwaysScan, (bool mwebAlwaysScan) => @@ -790,6 +798,9 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; + @observable + bool silentPaymentsKeyRegistered; + @observable bool mwebAlwaysScan; @@ -959,6 +970,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; final silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + final silentPaymentsKeyRegistered = + sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false; final mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; final mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; final mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; @@ -1230,6 +1243,7 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, + silentPaymentsKeyRegistered: silentPaymentsKeyRegistered, mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, @@ -1396,6 +1410,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true; silentPaymentsAlwaysScan = sharedPreferences.getBool(PreferencesKey.silentPaymentsAlwaysScan) ?? false; + silentPaymentsKeyRegistered = + sharedPreferences.getBool(PreferencesKey.silentPaymentsKeyRegistered) ?? false; mwebAlwaysScan = sharedPreferences.getBool(PreferencesKey.mwebAlwaysScan) ?? false; mwebCardDisplay = sharedPreferences.getBool(PreferencesKey.mwebCardDisplay) ?? true; mwebEnabled = sharedPreferences.getBool(PreferencesKey.mwebEnabled) ?? false; @@ -1658,7 +1674,8 @@ abstract class SettingsStoreBase with Store { deviceName = windowsInfo.productName; } catch (e) { print(e); - print('likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); + print( + 'likely digitalProductId is null wait till https://github.com/fluttercommunity/plus_plugins/pull/3188 is merged'); deviceName = "Windows Device"; } } diff --git a/lib/view_model/settings/silent_payments_settings_view_model.dart b/lib/view_model/settings/silent_payments_settings_view_model.dart index 5d20230d27..37c2f64867 100644 --- a/lib/view_model/settings/silent_payments_settings_view_model.dart +++ b/lib/view_model/settings/silent_payments_settings_view_model.dart @@ -30,4 +30,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/res/values/strings_ar.arb b/res/values/strings_ar.arb index 81fe3cc2c6..abcb892e1b 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "حدد المدفوعات الصامتة دائمًا المسح الضوئي", "silent_payments_disclaimer": "العناوين الجديدة ليست هويات جديدة. إنها إعادة استخدام هوية موجودة مع ملصق مختلف.", "silent_payments_display_card": "عرض بطاقة المدفوعات الصامتة", + "silent_payments_register_key": "سجل عرض مفتاح المسح الأسرع", "silent_payments_scan_from_date": "فحص من التاريخ", "silent_payments_scan_from_date_or_blockheight": "يرجى إدخال ارتفاع الكتلة الذي تريد بدء المسح الضوئي للمدفوعات الصامتة الواردة ، أو استخدام التاريخ بدلاً من ذلك. يمكنك اختيار ما إذا كانت المحفظة تواصل مسح كل كتلة ، أو تتحقق فقط من الارتفاع المحدد.", "silent_payments_scan_from_height": "فحص من ارتفاع الكتلة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 50db1610aa..2060711c50 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Задайте мълчаливи плащания винаги сканиране", "silent_payments_disclaimer": "Новите адреси не са нови идентичности. Това е повторна употреба на съществуваща идентичност с различен етикет.", "silent_payments_display_card": "Показване на безшумни плащания карта", + "silent_payments_register_key": "Регистрирайте ключа за преглед на по -бързото сканиране", "silent_payments_scan_from_date": "Сканиране от дата", "silent_payments_scan_from_date_or_blockheight": "Моля, въведете височината на блока, която искате да започнете да сканирате за входящи безшумни плащания, или вместо това използвайте датата. Можете да изберете дали портфейлът продължава да сканира всеки блок или проверява само определената височина.", "silent_payments_scan_from_height": "Сканиране от височината на блока", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index ddc91340b8..f06fe68850 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Nastavit tiché platby vždy skenování", "silent_payments_disclaimer": "Nové adresy nejsou nové identity. Je to opětovné použití existující identity s jiným štítkem.", "silent_payments_display_card": "Zobrazit kartu Silent Payments", + "silent_payments_register_key": "Zobrazení zaregistrujte klíč pro rychlejší skenování", "silent_payments_scan_from_date": "Skenovat od data", "silent_payments_scan_from_date_or_blockheight": "Zadejte výšku bloku, kterou chcete začít skenovat, zda jsou přicházející tiché platby, nebo místo toho použijte datum. Můžete si vybrat, zda peněženka pokračuje v skenování každého bloku nebo zkontroluje pouze zadanou výšku.", "silent_payments_scan_from_height": "Skenování z výšky bloku", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2ec59f3491..c094e838af 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Setzen Sie stille Zahlungen immer scannen", "silent_payments_disclaimer": "Neue Adressen sind keine neuen Identitäten. Es ist eine Wiederverwendung einer bestehenden Identität mit einem anderen Etikett.", "silent_payments_display_card": "Zeigen Sie stille Zahlungskarte", + "silent_payments_register_key": "Registrieren Sie die Ansichtsschlüssel für schnelleres Scannen", "silent_payments_scan_from_date": "Scan ab Datum", "silent_payments_scan_from_date_or_blockheight": "Bitte geben Sie die Blockhöhe ein, die Sie für eingehende stille Zahlungen scannen möchten, oder verwenden Sie stattdessen das Datum. Sie können wählen, ob die Wallet jeden Block scannt oder nur die angegebene Höhe überprüft.", "silent_payments_scan_from_height": "Scan aus der Blockhöhe scannen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index d6a0ee9afa..b3c2f4720d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -712,6 +712,7 @@ "silent_payments_always_scan": "Set Silent Payments always scanning", "silent_payments_disclaimer": "New addresses are not new identities. It is a re-use of an existing identity with a different label.", "silent_payments_display_card": "Show Silent Payments card", + "silent_payments_register_key": "Register view key for faster scanning", "silent_payments_scan_from_date": "Scan from date", "silent_payments_scan_from_date_or_blockheight": "Please enter the block height you want to start scanning for incoming Silent Payments or use the date instead. You can choose if the wallet continues scanning every block, or checks only the specified height.", "silent_payments_scan_from_height": "Scan from block height", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 25c9f95c1a..ecc2356bb7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -710,6 +710,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 be5b48dd87..a68eecc401 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Définir les paiements silencieux toujours à la scanne", "silent_payments_disclaimer": "Les nouvelles adresses ne sont pas de nouvelles identités. Il s'agit d'une réutilisation d'une identité existante avec une étiquette différente.", "silent_payments_display_card": "Afficher la carte de paiement silencieuse", + "silent_payments_register_key": "Enregistrez la touche Afficher pour une analyse plus rapide", "silent_payments_scan_from_date": "Analyser à partir de la date", "silent_payments_scan_from_date_or_blockheight": "Veuillez saisir la hauteur du bloc que vous souhaitez commencer à scanner pour les paiements silencieux entrants, ou utilisez la date à la place. Vous pouvez choisir si le portefeuille continue de numériser chaque bloc ou ne vérifie que la hauteur spécifiée.", "silent_payments_scan_from_height": "Scan à partir de la hauteur du bloc", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 4deb0df1d6..809db17277 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Saita biya na shiru koyaushe", "silent_payments_disclaimer": "Sabbin adiresoshin ba sabon tsari bane. Wannan shine sake amfani da asalin asalin tare da wata alama daban.", "silent_payments_display_card": "Nuna katin silent", + "silent_payments_register_key": "Yi rijista mabuɗin don bincika sauri", "silent_payments_scan_from_date": "Scan daga kwanan wata", "silent_payments_scan_from_date_or_blockheight": "Da fatan za a shigar da toshe wurin da kake son fara bincika don biyan silins mai shigowa, ko, yi amfani da kwanan wata. Zaka iya zabar idan walat ɗin ya ci gaba da bincika kowane toshe, ko duba tsinkaye da aka ƙayyade.", "silent_payments_scan_from_height": "Scan daga tsayin daka", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 5161250fcc..84c1afd6d1 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "मूक भुगतान हमेशा स्कैनिंग सेट करें", "silent_payments_disclaimer": "नए पते नई पहचान नहीं हैं। यह एक अलग लेबल के साथ एक मौजूदा पहचान का पुन: उपयोग है।", "silent_payments_display_card": "मूक भुगतान कार्ड दिखाएं", + "silent_payments_register_key": "तेजी से स्कैनिंग के लिए रजिस्टर व्यू कुंजी", "silent_payments_scan_from_date": "तिथि से स्कैन करना", "silent_payments_scan_from_date_or_blockheight": "कृपया उस ब्लॉक ऊंचाई दर्ज करें जिसे आप आने वाले मूक भुगतान के लिए स्कैन करना शुरू करना चाहते हैं, या, इसके बजाय तारीख का उपयोग करें। आप चुन सकते हैं कि क्या वॉलेट हर ब्लॉक को स्कैन करना जारी रखता है, या केवल निर्दिष्ट ऊंचाई की जांच करता है।", "silent_payments_scan_from_height": "ब्लॉक ऊंचाई से स्कैन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 8ef92aaf0f..6c08955a89 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Postavite tiho plaćanje uvijek skeniranje", "silent_payments_disclaimer": "Nove adrese nisu novi identiteti. To je ponovna upotreba postojećeg identiteta s drugom oznakom.", "silent_payments_display_card": "Prikaži karticu tihih plaćanja", + "silent_payments_register_key": "Registrirajte ključ za brže skeniranje", "silent_payments_scan_from_date": "Skeniranje iz datuma", "silent_payments_scan_from_date_or_blockheight": "Unesite visinu bloka koju želite započeti skeniranje za dolazna tiha plaćanja ili umjesto toga upotrijebite datum. Možete odabrati da li novčanik nastavlja skenirati svaki blok ili provjerava samo navedenu visinu.", "silent_payments_scan_from_height": "Skeniranje s visine bloka", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 40ed1e1164..f3f29721e4 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -701,6 +701,7 @@ "silent_payments_always_scan": "Միացնել Լուռ Վճարումներ մշտական սկանավորումը", "silent_payments_disclaimer": "Նոր հասցեները նոր ինքնություն չեն։ Դա այլ պիտակով գոյություն ունեցող ինքնության վերագործածում է", "silent_payments_display_card": "Ցուցադրել Լուռ Վճարումներ քարտը", + "silent_payments_register_key": "Գրանցեք Դիտել ստեղնը `ավելի արագ սկանավորման համար", "silent_payments_scan_from_date": "Սկանավորել ամսաթվից", "silent_payments_scan_from_date_or_blockheight": "Խնդրում ենք մուտքագրել բլոկի բարձրությունը, որտեղից դուք ցանկանում եք սկսել սկանավորել մուտքային Լուռ Վճարումները կամ տեղափոխել ամսաթվի փոխարեն։ Դուք կարող եք ընտրել, արդյոք դրամապանակը շարունակելու է սկանավորել ամեն բլոկ կամ ստուգել միայն սահմանված բարձրությունը", "silent_payments_scan_from_height": "Բլոկի բարձրությունից սկանավորել", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5f93082ec7..1aa0717533 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -712,6 +712,7 @@ "silent_payments_always_scan": "Tetapkan pembayaran diam selalu pemindaian", "silent_payments_disclaimer": "Alamat baru bukanlah identitas baru. Ini adalah penggunaan kembali identitas yang ada dengan label yang berbeda.", "silent_payments_display_card": "Tunjukkan kartu pembayaran diam", + "silent_payments_register_key": "Daftar Kunci Lihat untuk pemindaian yang lebih cepat", "silent_payments_scan_from_date": "Pindai dari tanggal", "silent_payments_scan_from_date_or_blockheight": "Harap masukkan ketinggian blok yang ingin Anda mulai pemindaian untuk pembayaran diam yang masuk, atau, gunakan tanggal sebagai gantinya. Anda dapat memilih jika dompet terus memindai setiap blok, atau memeriksa hanya ketinggian yang ditentukan.", "silent_payments_scan_from_height": "Pindai dari Tinggi Blok", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 08ae928afb..13133c2970 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Impostare i pagamenti silenziosi che scansionano sempre", "silent_payments_disclaimer": "I nuovi indirizzi non sono nuove identità. È un riutilizzo di un'identità esistente con un'etichetta diversa.", "silent_payments_display_card": "Mostra la carta di pagamenti silenziosi", + "silent_payments_register_key": "Registra la chiave di visualizzazione per una scansione più veloce", "silent_payments_scan_from_date": "Scansionare dalla data", "silent_payments_scan_from_date_or_blockheight": "Inserisci l'altezza del blocco che si desidera iniziare la scansione per i pagamenti silenziosi in arrivo o, utilizza invece la data. Puoi scegliere se il portafoglio continua a scansionare ogni blocco o controlla solo l'altezza specificata.", "silent_payments_scan_from_height": "Scansione dall'altezza del blocco", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index d70eca31be..331057e236 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "サイレント決済を常にスキャンします", "silent_payments_disclaimer": "新しいアドレスは新しいアイデンティティではありません。これは、異なるラベルを持つ既存のアイデンティティの再利用です。", "silent_payments_display_card": "サイレントペイメントカードを表示します", + "silent_payments_register_key": "登録キーを登録して、より速いスキャンを行います", "silent_payments_scan_from_date": "日付からスキャンします", "silent_payments_scan_from_date_or_blockheight": "着信のサイレント決済のためにスキャンを開始するブロックの高さを入力するか、代わりに日付を使用してください。ウォレットがすべてのブロックをスキャンし続けるか、指定された高さのみをチェックするかどうかを選択できます。", "silent_payments_scan_from_height": "ブロックの高さからスキャンします", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 133ca1838e..542998ebe1 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "무음금을 항상 스캔합니다", "silent_payments_disclaimer": "새로운 주소는 새로운 정체성이 아닙니다. 다른 레이블로 기존 신원을 재사용하는 것입니다.", "silent_payments_display_card": "사일런트 지불 카드 표시", + "silent_payments_register_key": "더 빠른 스캔을 위해보기 키 등록 키", "silent_payments_scan_from_date": "날짜부터 스캔하십시오", "silent_payments_scan_from_date_or_blockheight": "들어오는 사일런트 결제를 위해 스캔을 시작하려는 블록 높이를 입력하거나 대신 날짜를 사용하십시오. 지갑이 모든 블록을 계속 스캔하는지 여부를 선택하거나 지정된 높이 만 확인할 수 있습니다.", "silent_payments_scan_from_height": "블록 높이에서 스캔하십시오", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 1727f0d719..b64615a560 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "အမြဲတမ်း scanning အမြဲ scanning", "silent_payments_disclaimer": "လိပ်စာအသစ်များသည်အထောက်အထားအသစ်များမဟုတ်ပါ။ ၎င်းသည်ကွဲပြားခြားနားသောတံဆိပ်ဖြင့်ရှိပြီးသားဝိသေသလက်ခဏာကိုပြန်လည်အသုံးပြုခြင်းဖြစ်သည်။", "silent_payments_display_card": "အသံတိတ်ငွေပေးချေမှုကဒ်ကိုပြပါ", + "silent_payments_register_key": "ပိုမိုမြန်ဆန်သောစကင်ဖတ်စစ်ဆေးရန်အတွက်ကြည့်ပါ", "silent_payments_scan_from_date": "ရက်စွဲမှစကင်ဖတ်ပါ", "silent_payments_scan_from_date_or_blockheight": "ကျေးဇူးပြု. သင်ဝင်လာသောအသံတိတ်ငွေပေးချေမှုအတွက်သင်စကင်ဖတ်စစ်ဆေးလိုသည့်အမြင့်ကိုဖြည့်ပါ။ သို့မဟုတ်နေ့စွဲကိုသုံးပါ။ Wallet သည်လုပ်ကွက်တိုင်းကိုဆက်လက်စကင်ဖတ်စစ်ဆေးပါကသို့မဟုတ်သတ်မှတ်ထားသောအမြင့်ကိုသာစစ်ဆေးပါကသင်ရွေးချယ်နိုင်သည်။", "silent_payments_scan_from_height": "ပိတ်ပင်တားဆီးမှုအမြင့်ကနေ scan", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 3f2df531bf..d732ac410e 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Stel stille betalingen in het scannen", "silent_payments_disclaimer": "Nieuwe adressen zijn geen nieuwe identiteiten. Het is een hergebruik van een bestaande identiteit met een ander label.", "silent_payments_display_card": "Toon stille betalingskaart", + "silent_payments_register_key": "Registerweergave Key voor sneller scannen", "silent_payments_scan_from_date": "Scan vanaf datum", "silent_payments_scan_from_date_or_blockheight": "Voer de blokhoogte in die u wilt beginnen met scannen op inkomende stille betalingen, of gebruik in plaats daarvan de datum. U kunt kiezen of de portemonnee elk blok blijft scannen of alleen de opgegeven hoogte controleert.", "silent_payments_scan_from_height": "Scan van blokhoogte", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 91b2651442..5c82dcdc73 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Ustaw ciche płatności zawsze skanowanie", "silent_payments_disclaimer": "Nowe adresy nie są nową tożsamością. Jest to ponowne wykorzystanie istniejącej tożsamości z inną etykietą.", "silent_payments_display_card": "Pokaż kartę Silent Payments", + "silent_payments_register_key": "Zarejestruj się Wyświetl Klucz do szybszego skanowania", "silent_payments_scan_from_date": "Skanuj z daty", "silent_payments_scan_from_date_or_blockheight": "Wprowadź wysokość bloku, którą chcesz rozpocząć skanowanie w poszukiwaniu cichej płatności lub zamiast tego skorzystaj z daty. Możesz wybrać, czy portfel kontynuuje skanowanie każdego bloku, lub sprawdza tylko określoną wysokość.", "silent_payments_scan_from_height": "Skanuj z wysokości bloku", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 524dbcace9..3d6b0c8bbd 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "Defina pagamentos silenciosos sempre escaneando", "silent_payments_disclaimer": "Novos endereços não são novas identidades. É uma reutilização de uma identidade existente com um rótulo diferente.", "silent_payments_display_card": "Mostrar cartão de pagamento silencioso", + "silent_payments_register_key": "Chave de exibição de registro para digitalização mais rápida", "silent_payments_scan_from_date": "Escanear a partir da data", "silent_payments_scan_from_date_or_blockheight": "Por favor, insira a altura do bloco que deseja iniciar o escaneamento para obter pagamentos silenciosos ou use a data. Você pode escolher se a carteira continua escaneando cada bloco ou verifica apenas a altura especificada.", "silent_payments_scan_from_height": "Escanear a partir da altura do bloco", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 1a8c2447f9..f172d43900 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Установить молчаливые платежи всегда сканирование", "silent_payments_disclaimer": "Новые адреса не являются новыми личностями. Это повторное использование существующей идентичности с другой этикеткой.", "silent_payments_display_card": "Показать бесшумную платежную карту", + "silent_payments_register_key": "Зарегистрируйте ключ просмотра для более быстрого сканирования", "silent_payments_scan_from_date": "Сканирование с даты", "silent_payments_scan_from_date_or_blockheight": "Пожалуйста, введите высоту блока, которую вы хотите начать сканирование для входящих молчаливых платежей, или вместо этого используйте дату. Вы можете выбрать, продолжает ли кошелек сканировать каждый блок или проверять только указанную высоту.", "silent_payments_scan_from_height": "Сканирование с высоты блока", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 213f745302..a934110853 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "ตั้งค่าการชำระเงินแบบเงียบเสมอ", "silent_payments_disclaimer": "ที่อยู่ใหม่ไม่ใช่ตัวตนใหม่ มันเป็นการใช้ซ้ำของตัวตนที่มีอยู่ด้วยฉลากที่แตกต่างกัน", "silent_payments_display_card": "แสดงบัตร Silent Payments", + "silent_payments_register_key": "ลงทะเบียนคีย์มุมมองสำหรับการสแกนที่เร็วขึ้น", "silent_payments_scan_from_date": "สแกนตั้งแต่วันที่", "silent_payments_scan_from_date_or_blockheight": "โปรดป้อนความสูงของบล็อกที่คุณต้องการเริ่มการสแกนสำหรับการชำระเงินแบบเงียบ ๆ หรือใช้วันที่แทน คุณสามารถเลือกได้ว่ากระเป๋าเงินยังคงสแกนทุกบล็อกหรือตรวจสอบความสูงที่ระบุเท่านั้น", "silent_payments_scan_from_height": "สแกนจากความสูงของบล็อก", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 0ca8ee6653..b8c4af9d21 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Itakda ang mga tahimik na pagbabayad na laging nag-scan", "silent_payments_disclaimer": "Ang mga bagong address ay hindi mga bagong pagkakakilanlan. Ito ay isang muling paggamit ng isang umiiral na pagkakakilanlan na may ibang label.", "silent_payments_display_card": "Ipakita ang Silent Payment Card", + "silent_payments_register_key": "Magrehistro ng View Key para sa mas mabilis na pag -scan", "silent_payments_scan_from_date": "I-scan mula sa petsa", "silent_payments_scan_from_date_or_blockheight": "Mangyaring ipasok ang block height na gusto mong simulan ang pag-scan para sa papasok na tahimik na pagbabayad, o, gamitin ang petsa sa halip. Maaari kang pumili kung ang wallet ay patuloy na pag-scan sa bawat bloke, o suriin lamang ang tinukoy na taas.", "silent_payments_scan_from_height": "I-scan mula sa block height", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b23f64d604..2b44b63063 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "Sessiz ödemeleri her zaman tarama ayarlayın", "silent_payments_disclaimer": "Yeni adresler yeni kimlikler değildir. Farklı bir etikete sahip mevcut bir kimliğin yeniden kullanımıdır.", "silent_payments_display_card": "Sessiz Ödeme Kartı Göster", + "silent_payments_register_key": "Daha hızlı tarama için tuşunu kaydet", "silent_payments_scan_from_date": "Tarihten tarama", "silent_payments_scan_from_date_or_blockheight": "Lütfen gelen sessiz ödemeler için taramaya başlamak istediğiniz blok yüksekliğini girin veya bunun yerine tarihi kullanın. Cüzdanın her bloğu taramaya devam edip etmediğini veya yalnızca belirtilen yüksekliği kontrol edip etmediğini seçebilirsiniz.", "silent_payments_scan_from_height": "Blok yüksekliğinden tarama", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 79dc0543f5..d5a82293e1 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Встановити мовчазні платежі завжди сканувати", "silent_payments_disclaimer": "Нові адреси - це не нові ідентичності. Це повторне використання існуючої ідентичності з іншою етикеткою.", "silent_payments_display_card": "Покажіть безшумну карту платежів", + "silent_payments_register_key": "Зареєструйтесь ключ для більш швидкого сканування", "silent_payments_scan_from_date": "Сканувати з дати", "silent_payments_scan_from_date_or_blockheight": "Введіть висоту блоку, яку ви хочете почати сканувати для вхідних мовчазних платежів, або скористайтеся датою замість цього. Ви можете вибрати, якщо гаманець продовжує сканувати кожен блок, або перевіряє лише вказану висоту.", "silent_payments_scan_from_height": "Сканування від висоти блоку", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 0a136d1400..84a8bb355b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -711,6 +711,7 @@ "silent_payments_always_scan": "خاموش ادائیگی ہمیشہ اسکیننگ کریں", "silent_payments_disclaimer": "نئے پتے نئی شناخت نہیں ہیں۔ یہ ایک مختلف لیبل کے ساتھ موجودہ شناخت کا دوبارہ استعمال ہے۔", "silent_payments_display_card": "خاموش ادائیگی کارڈ دکھائیں", + "silent_payments_register_key": "تیزی سے اسکیننگ کے لئے کلید کو رجسٹر کریں", "silent_payments_scan_from_date": "تاریخ سے اسکین کریں", "silent_payments_scan_from_date_or_blockheight": "براہ کرم بلاک اونچائی میں داخل ہوں جس سے آپ آنے والی خاموش ادائیگیوں کے لئے اسکیننگ شروع کرنا چاہتے ہیں ، یا اس کے بجائے تاریخ کا استعمال کریں۔ آپ یہ منتخب کرسکتے ہیں کہ اگر پرس ہر بلاک کو اسکیننگ جاری رکھے ہوئے ہے ، یا صرف مخصوص اونچائی کی جانچ پڑتال کرتا ہے۔", "silent_payments_scan_from_height": "بلاک اونچائی سے اسکین کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 8d28d48a2e..6b6f0dd508 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -702,6 +702,7 @@ "silent_payments_always_scan": "Đặt Thanh toán im lặng luôn quét", "silent_payments_disclaimer": "Địa chỉ mới không phải là danh tính mới. Đây là việc tái sử dụng một danh tính hiện có với nhãn khác.", "silent_payments_display_card": "Hiển thị thẻ Thanh toán im lặng", + "silent_payments_register_key": "Đăng ký khóa xem để quét nhanh hơn", "silent_payments_scan_from_date": "Quét từ ngày", "silent_payments_scan_from_date_or_blockheight": "Vui lòng nhập chiều cao khối bạn muốn bắt đầu quét cho các thanh toán im lặng đến, hoặc, sử dụng ngày thay thế. Bạn có thể chọn nếu ví tiếp tục quét mỗi khối, hoặc chỉ kiểm tra chiều cao đã chỉ định.", "silent_payments_scan_from_height": "Quét từ chiều cao khối", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 14270120c9..c45acfe7f5 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -710,6 +710,7 @@ "silent_payments_always_scan": "Ṣeto awọn sisanwo ipalọlọ nigbagbogbo n ṣatunṣe", "silent_payments_disclaimer": "Awọn adirẹsi tuntun kii ṣe awọn idanimọ tuntun. O jẹ yiyan ti idanimọ ti o wa pẹlu aami oriṣiriṣi.", "silent_payments_display_card": "Ṣafihan kaadi isanwo ti o dakẹ", + "silent_payments_register_key": "Forukọsilẹ Wo bọtini Window fun Cranding yiyara", "silent_payments_scan_from_date": "Scan lati ọjọ", "silent_payments_scan_from_date_or_blockheight": "Jọwọ tẹ giga idibo ti o fẹ bẹrẹ ọlọjẹ fun awọn sisanwo ipalọlọ, tabi, lo ọjọ dipo. O le yan ti apamọwọ naa tẹsiwaju nṣapẹẹrẹ gbogbo bulọọki, tabi ṣayẹwo nikan giga ti o sọ tẹlẹ.", "silent_payments_scan_from_height": "Scan lati Iga Iga", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 65047b4fe0..cee24ba1bd 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -709,6 +709,7 @@ "silent_payments_always_scan": "设置无声付款总是扫描", "silent_payments_disclaimer": "新地址不是新的身份。这是重复使用具有不同标签的现有身份。", "silent_payments_display_card": "显示无声支付卡", + "silent_payments_register_key": "注册查看密钥以进行更快的扫描", "silent_payments_scan_from_date": "从日期开始扫描", "silent_payments_scan_from_date_or_blockheight": "请输入您要开始扫描输入静音付款的块高度,或者使用日期。您可以选择钱包是否继续扫描每个块,或仅检查指定的高度。", "silent_payments_scan_from_height": "从块高度扫描", diff --git a/tool/configure.dart b/tool/configure.dart index 97541c2fa6..16370e977f 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -102,7 +102,6 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:bip39/bip39.dart' as bip39; """; const bitcoinCWHeaders = """ -import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -216,6 +215,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}); From 7339b7876fb9af3795bcbaf7939772b998316d7f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 28 Oct 2024 12:16:23 -0300 Subject: [PATCH 02/64] refactor: init --- cw_bitcoin/lib/bitcoin_amount_format.dart | 26 - .../lib/bitcoin_transaction_credentials.dart | 12 +- .../lib/bitcoin_transaction_priority.dart | 289 +++- cw_bitcoin/lib/bitcoin_unspent.dart | 4 + cw_bitcoin/lib/bitcoin_wallet.dart | 749 +++++++++- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 7 +- cw_bitcoin/lib/bitcoin_wallet_service.dart | 21 +- cw_bitcoin/lib/electrum.dart | 40 +- cw_bitcoin/lib/electrum_balance.dart | 19 +- cw_bitcoin/lib/electrum_transaction_info.dart | 35 +- cw_bitcoin/lib/electrum_wallet.dart | 1206 ++++------------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 26 +- cw_bitcoin/lib/litecoin_wallet.dart | 119 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 14 +- .../lib/pending_bitcoin_transaction.dart | 5 +- cw_bitcoin/pubspec.lock | 24 +- cw_bitcoin/pubspec.yaml | 16 +- .../lib/src/bitcoin_cash_wallet.dart | 72 +- .../lib/src/bitcoin_cash_wallet_service.dart | 36 +- .../src/pending_bitcoin_cash_transaction.dart | 27 +- cw_bitcoin_cash/pubspec.yaml | 10 +- cw_core/lib/sync_status.dart | 2 +- cw_core/lib/transaction_priority.dart | 16 +- .../.plugin_symlinks/path_provider_linux | 1 + cw_tron/pubspec.yaml | 13 +- lib/bitcoin/cw_bitcoin.dart | 79 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 18 +- lib/core/sync_status_title.dart | 2 +- lib/di.dart | 172 ++- lib/main.dart | 1 - .../screens/dashboard/pages/address_page.dart | 70 +- lib/src/screens/receive/receive_page.dart | 28 +- .../settings/silent_payments_settings.dart | 4 +- lib/store/settings_store.dart | 2 +- .../silent_payments_settings_view_model.dart | 3 + pubspec_base.yaml | 4 +- res/values/strings_pt.arb | 4 +- scripts/android/app_env.fish | 78 ++ tool/configure.dart | 25 +- 39 files changed, 1876 insertions(+), 1403 deletions(-) delete mode 100644 cw_bitcoin/lib/bitcoin_amount_format.dart create mode 120000 cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux create mode 100644 scripts/android/app_env.fish 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_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..1e9c0c2731 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,51 +1,59 @@ import 'package:cw_core/transaction_priority.dart'; -class BitcoinTransactionPriority extends TransactionPriority { - const BitcoinTransactionPriority({required String title, required int raw}) - : super(title: title, raw: raw); +// Unimportant: the lowest possible, confirms when it confirms no matter how long it takes +// 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) +// Elevated: medium fee, confirms soon, elevated because it's higher than normal, gets you in the next 1-2 blocks (about 30 mins) +// Priority: high fee, expected in the next block (about 10 mins). + +class BitcoinMempoolAPITransactionPriority extends TransactionPriority { + const BitcoinMempoolAPITransactionPriority({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); - static const BitcoinTransactionPriority custom = - BitcoinTransactionPriority(title: 'Custom', raw: 3); - - static BitcoinTransactionPriority deserialize({required int raw}) { + static const BitcoinMempoolAPITransactionPriority unimportant = + BitcoinMempoolAPITransactionPriority(title: 'Unimportant', raw: 0); + static const BitcoinMempoolAPITransactionPriority normal = + BitcoinMempoolAPITransactionPriority(title: 'Normal', raw: 1); + static const BitcoinMempoolAPITransactionPriority elevated = + BitcoinMempoolAPITransactionPriority(title: 'Elevated', raw: 2); + static const BitcoinMempoolAPITransactionPriority priority = + BitcoinMempoolAPITransactionPriority(title: 'Priority', raw: 3); + static const BitcoinMempoolAPITransactionPriority custom = + BitcoinMempoolAPITransactionPriority(title: 'Custom', raw: 4); + + static BitcoinMempoolAPITransactionPriority 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 BitcoinMempoolAPITransactionPriority.unimportant: + label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinTransactionPriority.medium: - label = 'Medium'; // S.current.transaction_priority_medium; + case BitcoinMempoolAPITransactionPriority.normal: + label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; break; - case BitcoinTransactionPriority.fast: - label = 'Fast'; + case BitcoinMempoolAPITransactionPriority.elevated: + label = 'Elevated'; + break; // S.current.transaction_priority_fast; + case BitcoinMempoolAPITransactionPriority.priority: + label = 'Priority'; break; // S.current.transaction_priority_fast; - case BitcoinTransactionPriority.custom: + case BitcoinMempoolAPITransactionPriority.custom: label = 'Custom'; break; default: @@ -61,47 +69,65 @@ class BitcoinTransactionPriority extends TransactionPriority { } } -class LitecoinTransactionPriority extends BitcoinTransactionPriority { - const LitecoinTransactionPriority({required String title, required int raw}) +class BitcoinElectrumTransactionPriority extends TransactionPriority { + const BitcoinElectrumTransactionPriority({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 = [ + unimportant, + normal, + elevated, + priority, + custom, + ]; - static LitecoinTransactionPriority deserialize({required int raw}) { + static const BitcoinElectrumTransactionPriority unimportant = + BitcoinElectrumTransactionPriority(title: 'Unimportant', raw: 0); + static const BitcoinElectrumTransactionPriority normal = + BitcoinElectrumTransactionPriority(title: 'Normal', raw: 1); + static const BitcoinElectrumTransactionPriority elevated = + BitcoinElectrumTransactionPriority(title: 'Elevated', raw: 2); + static const BitcoinElectrumTransactionPriority priority = + BitcoinElectrumTransactionPriority(title: 'Priority', raw: 3); + static const BitcoinElectrumTransactionPriority custom = + BitcoinElectrumTransactionPriority(title: 'Custom', raw: 4); + + static BitcoinElectrumTransactionPriority 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 LitecoinTransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for TransactionPriority deserialize'); } } - @override - String get units => 'Litoshi'; - @override String toString() { var label = ''; switch (this) { - case LitecoinTransactionPriority.slow: - label = 'Slow'; // S.current.transaction_priority_slow; + case BitcoinElectrumTransactionPriority.unimportant: + label = 'Unimportant'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case LitecoinTransactionPriority.medium: - label = 'Medium'; // S.current.transaction_priority_medium; + case BitcoinElectrumTransactionPriority.normal: + label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case LitecoinTransactionPriority.fast: - label = 'Fast'; // S.current.transaction_priority_fast; + case BitcoinElectrumTransactionPriority.elevated: + label = 'Medium'; // S.current.transaction_priority_medium; + break; // S.current.transaction_priority_fast; + case BitcoinElectrumTransactionPriority.priority: + label = 'Fast'; + break; // S.current.transaction_priority_fast; + case BitcoinElectrumTransactionPriority.custom: + label = 'Custom'; break; default: break; @@ -110,18 +136,52 @@ class LitecoinTransactionPriority extends BitcoinTransactionPriority { return label; } + String labelWithRate(int rate, int? customRate) { + final rateValue = this == custom ? customRate ??= 0 : rate; + return '${toString()} ($rateValue ${units}/byte)'; + } } -class BitcoinCashTransactionPriority extends BitcoinTransactionPriority { - const BitcoinCashTransactionPriority({required String title, required int raw}) - : super(title: title, raw: raw); - static const List all = [fast, medium, slow]; +class LitecoinTransactionPriority extends BitcoinElectrumTransactionPriority { + const LitecoinTransactionPriority({required super.title, required super.raw}); + + static const all = [slow, medium, fast]; + + 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 LitecoinTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); + } + } + + @override + String get units => 'lit'; +} + +class BitcoinCashTransactionPriority extends BitcoinElectrumTransactionPriority { + const BitcoinCashTransactionPriority({required super.title, required super.raw}); + + static const all = [slow, medium, fast]; + static const BitcoinCashTransactionPriority slow = - BitcoinCashTransactionPriority(title: 'Slow', raw: 0); + BitcoinCashTransactionPriority(title: 'Slow', raw: 0); static const BitcoinCashTransactionPriority medium = - BitcoinCashTransactionPriority(title: 'Medium', raw: 1); + BitcoinCashTransactionPriority(title: 'Medium', raw: 1); static const BitcoinCashTransactionPriority fast = - BitcoinCashTransactionPriority(title: 'Fast', raw: 2); + BitcoinCashTransactionPriority(title: 'Fast', raw: 2); static BitcoinCashTransactionPriority deserialize({required int raw}) { switch (raw) { @@ -132,32 +192,113 @@ class BitcoinCashTransactionPriority extends BitcoinTransactionPriority { case 2: return fast; default: - throw Exception('Unexpected token: $raw for BitcoinCashTransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); } } @override - String get units => 'Satoshi'; + String get units => 'satoshi'; +} - @override - String toString() { - var label = ''; +class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { + const BitcoinMempoolAPITransactionPriorities({ + required this.unimportant, + required this.normal, + required this.elevated, + required this.priority, + }); - 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; + final int unimportant; + final int normal; + final int elevated; + final int priority; + + @override + int operator [](TransactionPriority type) { + switch (type) { + case BitcoinMempoolAPITransactionPriority.unimportant: + return unimportant; + case BitcoinMempoolAPITransactionPriority.normal: + return normal; + case BitcoinMempoolAPITransactionPriority.elevated: + return elevated; + case BitcoinMempoolAPITransactionPriority.priority: + return priority; default: - break; + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } + } - return label; + @override + String labelWithRate(TransactionPriority priorityType, [int? rate]) { + late int rateValue; + + if (priorityType == BitcoinMempoolAPITransactionPriority.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)'; } } +class BitcoinElectrumTransactionPriorities implements TransactionPriorities { + const BitcoinElectrumTransactionPriorities({ + required this.unimportant, + required this.slow, + required this.medium, + required this.fast, + }); + + final int unimportant; + final int slow; + final int medium; + final int fast; + + @override + int operator [](TransactionPriority type) { + switch (type) { + case BitcoinElectrumTransactionPriority.unimportant: + return unimportant; + case BitcoinElectrumTransactionPriority.normal: + return slow; + case BitcoinElectrumTransactionPriority.elevated: + return medium; + case BitcoinElectrumTransactionPriority.priority: + return fast; + default: + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); + } + } + + @override + String labelWithRate(TransactionPriority priorityType, [int? rate]) { + return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; + } + + factory BitcoinElectrumTransactionPriorities.fromList(List list) { + if (list.length != 3) { + throw Exception( + 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); + } + + int unimportantFee = list[0]; + + // Electrum servers only provides 3 levels: slow, medium, fast + // so make "unimportant" always lower than slow (but not 0) + if (unimportantFee > 1) { + unimportantFee--; + } + + return BitcoinElectrumTransactionPriorities( + unimportant: unimportantFee, + slow: list[0], + medium: list[1], + fast: list[2], + ); + } +} diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index d3421980ae..a57ad9a8bb 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,11 +1,15 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; class BitcoinUnspent extends Unspent { BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout) : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); + factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) => + BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout); + factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) => BitcoinUnspent( address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 029b6f2417..b974eeb47f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,11 +1,16 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:isolate'; import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; -import 'package:cw_bitcoin/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,20 +18,28 @@ import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:sp_scanner/sp_scanner.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { + Future? _isolate; + StreamSubscription? _receiveStream; + BitcoinWalletBase({ required String password, required WalletInfo walletInfo, @@ -45,6 +58,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, + required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -64,6 +78,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ) { walletAddresses = BitcoinWalletAddresses( walletInfo, @@ -96,6 +111,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, + required bool mempoolAPIEnabled, }) async { late Uint8List seedBytes; @@ -127,6 +143,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -137,6 +154,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!) @@ -218,6 +236,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -285,4 +304,732 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return super.signMessage(message, address: address); } + + @action + Future setSilentPaymentsScanning(bool active) async { + silentPaymentsScanningActive = active; + + if (active) { + syncStatus = AttemptingScanSyncStatus(); + + final tip = currentChainTip!; + + if (tip == walletInfo.restoreHeight) { + syncStatus = SyncedTipSyncStatus(tip); + return; + } + + if (tip > walletInfo.restoreHeight) { + _setListeners(walletInfo.restoreHeight); + } + } else { + alwaysScan = false; + + _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + + if (rpc!.isConnected) { + syncStatus = SyncedSyncStatus(); + } else { + syncStatus = NotConnectedSyncStatus(); + } + } + } + + @override + @action + Future updateAllUnspents() async { + List updatedUnspentCoins = []; + + // Update unspents stored from scanned silent payment transactions + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspents != null) { + updatedUnspentCoins.addAll(tx.unspents!); + } + }); + + // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating + walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .forEach((addr) { + if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + }); + + await Future.wait(walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((address) async { + updatedUnspentCoins.addAll(await fetchUnspent(address)); + })); + + unspentCoins = updatedUnspentCoins; + + if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + unspentCoins.forEach((coin) => addCoinInfo(coin)); + return; + } + + await updateCoins(unspentCoins); + await refreshUnspentCoinsInfo(); + } + + @override + void updateCoin(BitcoinUnspent coin) { + final coinInfoList = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.hash.contains(coin.hash) && + element.vout == coin.vout, + ); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) + coin.bitcoinAddressRecord.balance += coinInfo.value; + } else { + addCoinInfo(coin); + } + } + + Future _setInitialHeight() async { + final validChainTip = currentChainTip != null && currentChainTip != 0; + if (validChainTip && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); + } + } + + @action + @override + Future startSync() async { + await _setInitialHeight(); + + 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 + void _updateSilentAddressRecord(BitcoinUnspent unspent) { + final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; + final silentAddress = walletAddresses.silentAddress!; + final silentPaymentAddress = SilentPaymentAddress( + version: silentAddress.version, + B_scan: silentAddress.B_scan, + B_spend: receiveAddressRecord.labelHex != null + ? silentAddress.B_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)), + ) + : silentAddress.B_spend, + ); + + final addressRecord = walletAddresses.silentAddresses + .firstWhere((address) => address.address == silentPaymentAddress.toString()); + addressRecord.txCount += 1; + addressRecord.balance += unspent.value; + + walletAddresses.addSilentAddresses( + [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], + ); + } + + @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(); + + 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, + 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.labelIndex >= 1) + .map((addr) => addr.labelIndex) + .toList(), + isSingleScan: doSingleScan ?? false, + )); + + _receiveStream?.cancel(); + _receiveStream = receivePort.listen((var message) async { + if (message is Map) { + for (final map in message.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + final existingTxInfo = transactionHistory.transactions[txid]; + final txAlreadyExisted = existingTxInfo != null; + + // Updating tx after re-scanned + if (txAlreadyExisted) { + existingTxInfo.amount = tx.amount; + existingTxInfo.confirmations = tx.confirmations; + existingTxInfo.height = tx.height; + + final newUnspents = tx.unspents! + .where((unspent) => !(existingTxInfo.unspents?.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value) ?? + false)) + .toList(); + + if (newUnspents.isNotEmpty) { + newUnspents.forEach(_updateSilentAddressRecord); + + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + + final newAmount = newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + + if (existingTxInfo.direction == TransactionDirection.incoming) { + existingTxInfo.amount += newAmount; + } + + // Updates existing TX + transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; + } + } else { + // else: First time seeing this TX after scanning + tx.unspents!.forEach(_updateSilentAddressRecord); + + // Add new TX record + transactionHistory.addMany(message); + // Update balance record + balance[currency]!.confirmed += tx.amount; + } + + await updateAllUnspents(); + } + } + } + + if (message is SyncResponse) { + if (message.syncStatus is UnsupportedSyncStatus) { + nodeSupportsSilentPayments = false; + } + + if (message.syncStatus is SyncingSyncStatus) { + var status = message.syncStatus as SyncingSyncStatus; + syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); + } else { + syncStatus = message.syncStatus; + } + + await walletInfo.updateRestoreHeight(message.height); + } + }); + } + + @override + @action + Future> fetchTransactions() async { + 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() 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(List addresses) async { + final balance = await super.fetchBalances(addresses); + + 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 updateFeeRates() async { + // Bitcoin only: use the mempool.space backend API for accurate fee rates + if (mempoolAPIEnabled) { + try { + final recommendedFees = await apiProvider!.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 should be higher than normal + if (normalFee == elevatedFee) { + elevatedFee++; + } + // priority should be higher than elevated + while (priorityFee <= elevatedFee) { + priorityFee++; + } + // this guarantees that, even if all fees are low and equal, + // higher priority fees can be taken when fees start surging + + feeRates = BitcoinMempoolAPITransactionPriorities( + unimportant: unimportantFee, + normal: normalFee, + elevated: elevatedFee, + priority: priorityFee, + ); + return; + } catch (e, stacktrace) { + callError(FlutterErrorDetails( + exception: e, + stack: stacktrace, + library: this.runtimeType.toString(), + )); + } + } else { + // Bitcoin only: Ideally this should be avoided, electrum is terrible at fee rates + await super.updateFeeRates(); + } + } + + @override + @action + void onHeadersResponse(ElectrumHeaderResponse response) { + super.onHeadersResponse(response); + + if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + _setListeners(walletInfo.restoreHeight); + } + } + + @override + @action + void syncStatusReaction(SyncStatus syncStatus) { + switch (syncStatus.runtimeType) { + case SyncingSyncStatus: + return; + case SyncedTipSyncStatus: + // Message is shown on the UI for 3 seconds, then reverted to synced + Timer(Duration(seconds: 3), () { + if (this.syncStatus is SyncedTipSyncStatus) this.syncStatus = SyncedSyncStatus(); + }); + break; + default: + super.syncStatusReaction(syncStatus); + } + } +} + +Future startRefresh(ScanData scanData) async { + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; + + final electrumClient = ElectrumApiProvider( + await ElectrumTCPService.connect( + scanData.node?.uri ?? Uri.parse("tcp://198.58.115.71:50001"), + ), + ); + + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); + + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); + + final listener = await electrumClient.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + + Future listenFn(ElectrumTweaksSubscribeResponse response) async { + // success or error msg + final noData = response.message != null; + + if (noData) { + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); + + if (nextCount > 0) { + final nextListener = await electrumClient.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + nextListener?.call(listenFn); + } + + return; + } + + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + + final tweakHeight = response.block; + + try { + final blockTweaks = response.blockTweaks; + + for (final txid in blockTweaks.keys) { + final tweakData = blockTweaks[txid]; + final outputPubkeys = tweakData!.outputPubkeys; + final tweak = tweakData.tweak; + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + isReplaced: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + isReceivedSilentPayment: true, + ); + + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); + + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + final matchingOutput = outputPubkeys[output]!; + final amount = matchingOutput.amount; + final pos = matchingOutput.vout; + + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + receivingOutputAddress, + labelIndex: 1, // TODO: get actual index/label + isUsed: true, + spendKey: scanData.silentAddress.b_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), + ), + txCount: 1, + balance: amount, + ); + + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + scanData.sendPort.send({txInfo.id: txInfo}); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + + syncHeight = tweakHeight; + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + scanData.sendPort.send(SyncResponse( + syncHeight, + SyncedTipSyncStatus(scanData.chainTip), + )); + + if (scanData.isSingleScan) { + scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); + } + } + } + + listener?.call(listenFn); + + // if (tweaksSubscription == null) { + // return scanData.sendPort.send( + // SyncResponse(syncHeight, UnsupportedSyncStatus()), + // ); + // } +} + +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()), + // ); + // } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index a6f047fa13..931c58e710 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -43,7 +43,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S type: P2shAddressType.p2wpkhInP2sh, ); case SegwitAddresType.p2wpkh: - return P2wpkhAddress.fromBip32(account: account, bip32: hd, index: index); + return P2wpkhAddress.fromBip32( + account: account, + bip32: hd, + index: index, + isElectrum: true, + ); // TODO: default: throw ArgumentError('Invalid address type'); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 06f2082e43..45ef9b653b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -21,11 +21,18 @@ class BitcoinWalletService extends WalletService< BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { - BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + BitcoinWalletService( + this.walletInfoSource, + this.unspentCoinsInfoSource, + this.alwaysScan, + this.mempoolAPIEnabled, + this.isDirect, + ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; + final bool mempoolAPIEnabled; final bool isDirect; @override @@ -37,7 +44,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 +64,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); @@ -80,7 +88,8 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, + encryptionFileUtils: encryptionFileUtilsFor(false), ); await wallet.init(); saveBackup(name); @@ -93,7 +102,8 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, + encryptionFileUtils: encryptionFileUtilsFor(false), ); await wallet.init(); return wallet; @@ -118,6 +128,7 @@ class BitcoinWalletService extends WalletService< walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -146,6 +157,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -175,6 +187,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 0a963bd6f2..34849def3c 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, @@ -321,7 +313,7 @@ class ElectrumClient { subscribe( id: 'blockchain.tweaks.subscribe', method: 'blockchain.tweaks.subscribe', - params: [height, count, false], + params: [height, count, true], ); Future tweaksRegister({ @@ -330,7 +322,7 @@ class ElectrumClient { List labels = const [], }) => call( - method: 'blockchain.tweaks.subscribe', + method: 'blockchain.tweaks.register', params: [secViewKey, pubSpendKey, labels], ); @@ -394,20 +386,20 @@ class ElectrumClient { return []; }); - Future> feeRates({BasedUtxoNetwork? network}) async { - try { - final topDoubleString = await estimatefee(p: 1); - final middleDoubleString = await estimatefee(p: 5); - final bottomDoubleString = await estimatefee(p: 10); - final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); - final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); - final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); - - 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: diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index 4e37f40b15..1cb26fe787 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { @@ -31,32 +31,35 @@ class ElectrumBalance extends Balance { int confirmed; int unconfirmed; - final int frozen; + int frozen; int secondConfirmed = 0; int secondUnconfirmed = 0; @override String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed - frozen); + BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed - frozen); @override - String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); + String get formattedAdditionalBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: unconfirmed); @override String get formattedUnAvailableBalance { - final frozenFormatted = bitcoinAmountToString(amount: frozen); + final frozenFormatted = BitcoinAmountUtils.bitcoinAmountToString(amount: frozen); return frozenFormatted == '0.0' ? '' : frozenFormatted; } @override - String get formattedSecondAvailableBalance => bitcoinAmountToString(amount: secondConfirmed); + String get formattedSecondAvailableBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: secondConfirmed); @override - String get formattedSecondAdditionalBalance => bitcoinAmountToString(amount: secondUnconfirmed); + String get formattedSecondAdditionalBalance => + BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed); @override String get formattedFullAvailableBalance => - bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); + BitcoinAmountUtils.bitcoinAmountToString(amount: confirmed + secondConfirmed - frozen); String toJSON() => json.encode({ 'confirmed': confirmed, diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f5857437c0..ccf9e20d7e 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -3,7 +3,6 @@ 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'; @@ -75,7 +74,8 @@ class ElectrumTransactionInfo extends TransactionInfo { final vout = vin['vout'] as int; final out = vin['tx']['vout'][vout] as Map; final outAddresses = (out['scriptPubKey']['addresses'] as List?)?.toSet(); - inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); + inputsAmount += + BitcoinAmountUtils.stringDoubleToBitcoinAmount((out['value'] as double? ?? 0).toString()); if (outAddresses?.intersection(addressesSet).isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -85,7 +85,8 @@ class ElectrumTransactionInfo extends TransactionInfo { for (dynamic out in vout) { final outAddresses = out['scriptPubKey']['addresses'] as List? ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount((out['value'] as double? ?? 0.0).toString()); + final value = BitcoinAmountUtils.stringDoubleToBitcoinAmount( + (out['value'] as double? ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -121,15 +122,23 @@ 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(addressFromOutputScript(outTransaction.scriptPubKey, network))) { + direction = TransactionDirection.outgoing; + inputAddresses.add(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 = []; @@ -220,11 +229,11 @@ class ElectrumTransactionInfo extends TransactionInfo { @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; + '${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String? feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' + ? '${formatAmount(BitcoinAmountUtils.bitcoinAmountToString(amount: fee!))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ee4e7d7bb0..6481045403 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -9,14 +9,12 @@ 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_amount_format.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'; @@ -29,7 +27,6 @@ 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'; @@ -41,8 +38,6 @@ 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; @@ -68,14 +63,14 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, + required this.mempoolAPIEnabled, }) : bip32 = getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), syncStatus = NotConnectedSyncStatus(), _password = password, - _feeRates = [], _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, unspentCoins = [], - _scripthashesUpdateSubject = {}, + scripthashesListening = {}, balance = ObservableMap.of(currency != null ? { currency: initialBalance ?? @@ -98,7 +93,7 @@ abstract class ElectrumWalletBase encryptionFileUtils: encryptionFileUtils, ); - reaction((_) => syncStatus, _syncStatusReaction); + reaction((_) => syncStatus, syncStatusReaction); sharedPrefs.complete(SharedPreferences.getInstance()); } @@ -129,7 +124,7 @@ abstract class ElectrumWalletBase 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) { @@ -142,6 +137,7 @@ abstract class ElectrumWalletBase } bool? alwaysScan; + bool mempoolAPIEnabled; final Bip32Slip10Secp256k1 bip32; final String? _mnemonic; @@ -156,6 +152,9 @@ abstract class ElectrumWalletBase bool isEnabledAutoGenerateSubaddress; late ElectrumClient electrumClient; + ElectrumApiProvider? electrumClient2; + BitcoinBaseElectrumRPCService? get rpc => electrumClient2?.rpc; + ApiProvider? apiProvider; Box unspentCoinsInfo; @override @@ -202,8 +201,6 @@ abstract class ElectrumWalletBase @override bool isTestnet; - bool get hasSilentPaymentsScanning => type == WalletType.bitcoin; - @observable bool nodeSupportsSilentPayments = true; @observable @@ -213,89 +210,8 @@ abstract class ElectrumWalletBase Completer sharedPrefs = Completer(); - Future checkIfMempoolAPIIsEnabled() async { - bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true; - return isMempoolAPIEnabled; - } - - // @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 setSilentPaymentsScanning(bool active) async { - silentPaymentsScanningActive = active; - - if (active) { - syncStatus = AttemptingScanSyncStatus(); - - final tip = await getUpdatedChainTip(); - - if (tip == walletInfo.restoreHeight) { - syncStatus = SyncedTipSyncStatus(tip); - return; - } - - if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); - } - } else { - alwaysScan = false; - - _isolate?.then((value) => value.kill(priority: Isolate.immediate)); - - if (electrumClient.isConnected) { - syncStatus = SyncedSyncStatus(); - } else { - syncStatus = NotConnectedSyncStatus(); - } - } - } - - int? _currentChainTip; - - Future getCurrentChainTip() async { - if ((_currentChainTip ?? 0) > 0) { - return _currentChainTip!; - } - _currentChainTip = await electrumClient.getCurrentBlockChainTip() ?? 0; - - return _currentChainTip!; - } - - Future getUpdatedChainTip() async { - final newTip = await electrumClient.getCurrentBlockChainTip(); - if (newTip != null && newTip > (_currentChainTip ?? 0)) { - _currentChainTip = newTip; - } - return _currentChainTip ?? 0; - } + @observable + int? currentChainTip; @override BitcoinWalletKeys get keys => BitcoinWalletKeys( @@ -306,15 +222,16 @@ abstract class ElectrumWalletBase String _password; List unspentCoins; - List _feeRates; - // ignore: prefer_final_fields - Map?> _scripthashesUpdateSubject; + @observable + TransactionPriorities? feeRates; + int feeRate(TransactionPriority priority) => feeRates![priority]; + + @observable + Set scripthashesListening; - // ignore: prefer_final_fields - BehaviorSubject? _chainTipUpdateSubject; + bool _chainTipListenerOn = false; bool _isTransactionUpdating; - Future? _isolate; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -331,173 +248,28 @@ abstract class ElectrumWalletBase 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( - delegatedScan, - 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.labelIndex >= 1) - .map((addr) => addr.labelIndex) - .toList(), - isSingleScan: doSingleScan ?? false, - )); - - _receiveStream?.cancel(); - _receiveStream = receivePort.listen((var message) async { - if (message is Map) { - for (final map in message.entries) { - final txid = map.key; - final tx = map.value; - - if (tx.unspents != null) { - final existingTxInfo = transactionHistory.transactions[txid]; - final txAlreadyExisted = existingTxInfo != null; - - // Updating tx after re-scanned - if (txAlreadyExisted) { - existingTxInfo.amount = tx.amount; - existingTxInfo.confirmations = tx.confirmations; - existingTxInfo.height = tx.height; - - final newUnspents = tx.unspents! - .where((unspent) => !(existingTxInfo.unspents?.any((element) => - element.hash.contains(unspent.hash) && - element.vout == unspent.vout && - element.value == unspent.value) ?? - false)) - .toList(); - - if (newUnspents.isNotEmpty) { - newUnspents.forEach(_updateSilentAddressRecord); - - existingTxInfo.unspents ??= []; - existingTxInfo.unspents!.addAll(newUnspents); - - final newAmount = newUnspents.length > 1 - ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) - : newUnspents[0].value; - - if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += newAmount; - } - - // Updates existing TX - transactionHistory.addOne(existingTxInfo); - // Update balance record - balance[currency]!.confirmed += newAmount; - } - } else { - // else: First time seeing this TX after scanning - tx.unspents!.forEach(_updateSilentAddressRecord); - - // Add new TX record - transactionHistory.addMany(message); - // Update balance record - balance[currency]!.confirmed += tx.amount; - } - - await updateAllUnspents(); - } - } - } - - if (message is SyncResponse) { - if (message.syncStatus is UnsupportedSyncStatus) { - nodeSupportsSilentPayments = false; - } - - if (message.syncStatus is SyncingSyncStatus) { - var status = message.syncStatus as SyncingSyncStatus; - syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); - } else { - syncStatus = message.syncStatus; - } - - await walletInfo.updateRestoreHeight(message.height); - } - }); - } - - void _updateSilentAddressRecord(BitcoinUnspent unspent) { - final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; - final silentAddress = walletAddresses.silentAddress!; - final silentPaymentAddress = SilentPaymentAddress( - version: silentAddress.version, - B_scan: silentAddress.B_scan, - B_spend: receiveAddressRecord.labelHex != null - ? silentAddress.B_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)), - ) - : silentAddress.B_spend, - ); - - final addressRecord = walletAddresses.silentAddresses - .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(); - - if (hasSilentPaymentsScanning) { - await _setInitialHeight(); - } + syncStatus = SynchronizingSyncStatus(); + await subscribeForHeaders(); await subscribeForUpdates(); - await updateTransactions(); - await updateAllUnspents(); - await updateBalance(); + // await updateTransactions(); + // await updateAllUnspents(); + // await updateBalance(); await updateFeeRates(); _updateFeeRateTimer ??= - Timer.periodic(const Duration(minutes: 1), (timer) async => await updateFeeRates()); + Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - if (alwaysScan == true) { - _setListeners(walletInfo.restoreHeight); - } else { - syncStatus = SyncedSyncStatus(); - } + syncStatus = SyncedSyncStatus(); } catch (e, stacktrace) { print(stacktrace); print("startSync $e"); @@ -506,40 +278,43 @@ abstract class ElectrumWalletBase } @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); - } - } + 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 callError(FlutterErrorDetails error) { + _onError?.call(error); + } - final feeRates = await electrumClient.feeRates(network: network); - if (feeRates != [0, 0, 0]) { - _feeRates = feeRates; - } else if (isTestnet) { - _feeRates = [1, 1, 1]; + @action + Future updateFeeRates() async { + try { + feeRates = BitcoinElectrumTransactionPriorities.fromList( + await electrumClient2!.getFeeRates(), + ); + } catch (e, stacktrace) { + // _onError?.call(FlutterErrorDetails( + // exception: e, + // stack: stacktrace, + // library: this.runtimeType.toString(), + // )); } } Node? node; Future getNodeIsElectrs() async { + return true; if (node == null) { return false; } @@ -562,6 +337,7 @@ abstract class ElectrumWalletBase } Future getNodeSupportsSilentPayments() async { + return true; // As of today (august 2024), only ElectrumRS supports silent payments if (!(await getNodeIsElectrs())) { return false; @@ -593,17 +369,28 @@ abstract class ElectrumWalletBase @action @override Future connectToNode({required Node node}) async { + scripthashesListening = {}; + _isTransactionUpdating = false; + _chainTipListenerOn = false; this.node = node; try { syncStatus = ConnectingSyncStatus(); await _receiveStream?.cancel(); - await electrumClient.close(); + rpc?.disconnect(); - electrumClient.onConnectionStatusChange = _onConnectionStatusChange; + // electrumClient.onConnectionStatusChange = _onConnectionStatusChange; - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + this.electrumClient2 = ElectrumApiProvider( + await ElectrumTCPService.connect( + node.uri, + onConnectionStatusChange: _onConnectionStatusChange, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), + ); + // await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e, stacktrace) { print(stacktrace); print("connectToNode $e"); @@ -695,10 +482,12 @@ abstract class ElectrumWalletBase .toHex(); } - final derivationPath = - "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}" - "/${utx.bitcoinAddressRecord.isChange ? "1" : "0"}" - "/${utx.bitcoinAddressRecord.index}"; + // TODO: isElectrum + final derivationPath = BitcoinAddressUtils.getDerivationPath( + type: utx.bitcoinAddressRecord.type, + account: utx.bitcoinAddressRecord.isChange ? 1 : 0, + index: utx.bitcoinAddressRecord.index, + ); publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); utxos.add( @@ -707,7 +496,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( @@ -761,11 +550,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) { @@ -891,11 +677,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(); @@ -957,8 +740,8 @@ abstract class ElectrumWalletBase // Estimate to user how much is needed to send to cover the fee final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1; throw BitcoinTransactionNoDustOnChangeException( - bitcoinAmountToString(amount: maxAmountWithReturningChange), - bitcoinAmountToString(amount: estimatedSendAll.amount), + BitcoinAmountUtils.bitcoinAmountToString(amount: maxAmountWithReturningChange), + BitcoinAmountUtils.bitcoinAmountToString(amount: estimatedSendAll.amount), ); } @@ -1010,33 +793,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 { @@ -1277,18 +1043,6 @@ abstract class ElectrumWalletBase 'alwaysScan': alwaysScan, }); - int feeRate(TransactionPriority priority) { - try { - if (priority is BitcoinTransactionPriority) { - return _feeRates[priority.raw]; - } - - return 0; - } catch (_) { - return 0; - } - } - int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount, {int? size}) => feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); @@ -1299,9 +1053,13 @@ abstract class ElectrumWalletBase @override int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount, int? size}) { - if (priority is BitcoinTransactionPriority) { - return calculateEstimatedFeeWithFeeRate(feeRate(priority), amount, - outputsCount: outputsCount, size: size); + if (priority is BitcoinMempoolAPITransactionPriority) { + return calculateEstimatedFeeWithFeeRate( + feeRate(priority), + amount, + outputsCount: outputsCount, + size: size, + ); } return 0; @@ -1384,11 +1142,9 @@ 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 @@ -1405,15 +1161,6 @@ abstract class ElectrumWalletBase 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) @@ -1435,55 +1182,60 @@ abstract class ElectrumWalletBase } await updateCoins(unspentCoins); - await _refreshUnspentCoinsInfo(); + await refreshUnspentCoinsInfo(); + } + + 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; + } else { + addCoinInfo(coin); + } } Future updateCoins(List newUnspentCoins) async { if (newUnspentCoins.isEmpty) { return; } - - newUnspentCoins.forEach((coin) { - final coinInfoList = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.hash.contains(coin.hash) && - element.vout == coin.vout, - ); - - if (coinInfoList.isNotEmpty) { - final coinInfo = coinInfoList.first; - - coin.isFrozen = coinInfo.isFrozen; - coin.isSending = coinInfo.isSending; - coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; - } else { - addCoinInfo(coin); - } - }); + newUnspentCoins.forEach(updateCoin); } @action - Future updateUnspentsForAddress(BitcoinAddressRecord address) async { - final newUnspentCoins = await fetchUnspent(address); + Future updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async { + final newUnspentCoins = await fetchUnspent(addressRecord); await updateCoins(newUnspentCoins); } @action Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; List updatedUnspentCoins = []; - unspents = await electrumClient.getListUnspent(address.scriptHash); + final unspents = await electrumClient2!.request( + ElectrumScriptHashListUnspent(scriptHash: address.scriptHash), + ); await Future.wait(unspents.map((unspent) async { try { - final coin = BitcoinUnspent.fromJSON(address, unspent); + final coin = BitcoinUnspent.fromUTXO(address, unspent); final tx = await fetchTransactionInfo(hash: coin.hash); coin.isChange = address.isChange; coin.confirmations = tx?.confirmations; + if (coin.isFrozen) { + balance[currency]!.frozen += coin.value; + } else { + balance[currency]!.confirmed += coin.value; + } updatedUnspentCoins.add(coin); } catch (_) {} @@ -1510,7 +1262,7 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.add(newInfo); } - Future _refreshUnspentCoinsInfo() async { + Future refreshUnspentCoinsInfo() async { try { final List keys = []; final currentWalletUnspentCoins = @@ -1535,8 +1287,6 @@ abstract class ElectrumWalletBase } } - int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); - Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { final bundle = await getTransactionExpanded(hash: tx.txHash); @@ -1615,7 +1365,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), @@ -1744,43 +1494,49 @@ abstract class ElectrumWalletBase Future getTransactionExpanded( {required String hash, int? height}) async { - String transactionHex; + String transactionHex = ''; int? time; int? confirmations; - final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); - - if (verboseTransaction.isEmpty) { - transactionHex = await electrumClient.getTransactionHex(hash: hash); + try { + final verboseTransaction = await electrumClient2!.request( + ElectrumGetTransactionVerbose(transactionHash: 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", - ), - ); + transactionHex = verboseTransaction['hex'] as String; + time = verboseTransaction['time'] as int?; + confirmations = verboseTransaction['confirmations'] as int?; + } catch (e) { + if (e is RPCError || e is TimeoutException) { + transactionHex = await electrumClient2!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); - if (blockHash.statusCode == 200 && - blockHash.body.isNotEmpty && - jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( + if (height != null && height > 0 && mempoolAPIEnabled) { + try { + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + + 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 (_) {} + } catch (_) {} + } } - } else { - transactionHex = verboseTransaction['hex'] as String; - time = verboseTransaction['time'] as int?; - confirmations = verboseTransaction['confirmations'] as int?; } if (height != null) { @@ -1789,7 +1545,7 @@ abstract class ElectrumWalletBase } if (confirmations == null) { - final tip = await getUpdatedChainTip(); + final tip = currentChainTip!; if (tip > 0 && height > 0) { // Add one because the block itself is the first confirmation confirmations = tip - height + 1; @@ -1801,14 +1557,18 @@ abstract class ElectrumWalletBase 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 { + String inputTransactionHex = ""; + try { + final verboseTransaction = await electrumClient2!.request( + ElectrumGetTransactionVerbose(transactionHash: vin.txId), + ); inputTransactionHex = verboseTransaction['hex'] as String; + } catch (e) { + if (e is RPCError || e is TimeoutException) { + inputTransactionHex = await electrumClient2!.request( + ElectrumGetTransactionHex(transactionHash: vin.txId), + ); + } } ins.add(BtcTransaction.fromRaw(inputTransactionHex)); @@ -1822,8 +1582,7 @@ abstract class ElectrumWalletBase ); } - 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), @@ -1832,24 +1591,19 @@ abstract class ElectrumWalletBase addresses: addressesSet, height: height, ); - } catch (e) { - if (e is FormatException && retryOnFailure == true) { - await Future.delayed(const Duration(seconds: 2)); - return fetchTransactionInfo(hash: hash, height: height); - } + } catch (e, s) { + print([e, s]); 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) { + if (type == WalletType.bitcoinCash) { await Future.wait(BITCOIN_CASH_ADDRESS_TYPES .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); } else if (type == WalletType.litecoin) { @@ -1857,23 +1611,6 @@ abstract class ElectrumWalletBase .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"); @@ -1886,63 +1623,34 @@ abstract class ElectrumWalletBase BitcoinAddressType type, ) async { final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); - final hiddenAddresses = addressesByType.where((addr) => addr.isChange == true); - final receiveAddresses = addressesByType.where((addr) => addr.isChange == 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()); + final history = await _fetchAddressHistory(addressRecord); if (history.isNotEmpty) { - addressRecord.txCount = history.length; historiesWithDetails.addAll(history); - - final matchedAddresses = addressRecord.isChange ? hiddenAddresses : receiveAddresses; - final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= - matchedAddresses.length - - (addressRecord.isChange - ? 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.isChange, - (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 { + BitcoinAddressRecord addressRecord, + ) async { String txid = ""; try { final Map historiesWithDetails = {}; - final history = await electrumClient.getHistory(addressRecord.scriptHash); + final history = await electrumClient2!.request(ElectrumScriptHashGetHistory( + scriptHash: addressRecord.scriptHash, + )); if (history.isNotEmpty) { addressRecord.setAsUsed(); + addressRecord.txCount = history.length; await Future.wait(history.map((transaction) async { txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; final storedTx = transactionHistory.transactions[txid]; @@ -1950,15 +1658,15 @@ abstract class ElectrumWalletBase 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; + if ((currentChainTip ?? 0) > 0) { + storedTx.confirmations = currentChainTip! - height + 1; } storedTx.isPending = storedTx.confirmations == 0; } historiesWithDetails[txid] = storedTx; } else { - final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true); + final tx = await fetchTransactionInfo(hash: txid, height: height); if (tx != null) { historiesWithDetails[txid] = tx; @@ -1972,6 +1680,37 @@ abstract class ElectrumWalletBase return Future.value(null); })); + + final totalAddresses = (addressRecord.isChange + ? walletAddresses.allAddresses + .where((addr) => addr.isChange && addr.type == addressRecord.type) + .length + : walletAddresses.allAddresses + .where((addr) => !addr.isChange && addr.type == addressRecord.type) + .length); + final gapLimit = (addressRecord.isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + print("gapLimit: $gapLimit"); + print("index: ${addressRecord.index}"); + final isUsedAddressUnderGap = addressRecord.index >= totalAddresses - gapLimit; + print("isUsedAddressAtGapLimit: $isUsedAddressUnderGap"); + print("total: $totalAddresses"); + + if (isUsedAddressUnderGap) { + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverAddresses( + walletAddresses.allAddresses + .where((addr) => + (addressRecord.isChange ? addr.isChange : !addr.isChange) && + addr.type == addressRecord.type) + .toList(), + addressRecord.isChange, + type: addressRecord.type, + ); + await subscribeForUpdates(newAddresses); + } } return historiesWithDetails; @@ -1985,23 +1724,12 @@ abstract class ElectrumWalletBase } } + @action Future updateTransactions() async { - print("updateTransactions() called!"); try { if (_isTransactionUpdating) { return; } - await getCurrentChainTip(); - - transactionHistory.transactions.values.forEach((tx) { - if (tx.unspents != null && - tx.unspents!.isNotEmpty && - tx.height != null && - tx.height! > 0 && - (_currentChainTip ?? 0) > 0) { - tx.confirmations = _currentChainTip! - tx.height! + 1; - } - }); _isTransactionUpdating = true; await fetchTransactions(); @@ -2014,55 +1742,45 @@ abstract class ElectrumWalletBase } } - Future subscribeForUpdates() async { - final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => - !_scripthashesUpdateSubject.containsKey(address.scriptHash) && - address.type != SegwitAddresType.mweb, + @action + Future subscribeForUpdates([ + Iterable? unsubscribedScriptHashes, + ]) async { + unsubscribedScriptHashes ??= walletAddresses.allAddresses.where( + (address) => !scripthashesListening.contains(address.scriptHash), ); - await Future.wait(unsubscribedScriptHashes.map((address) async { - final sh = address.scriptHash; - if (!(_scripthashesUpdateSubject[sh]?.isClosed ?? true)) { - try { - await _scripthashesUpdateSubject[sh]?.close(); - } catch (e) { - print("failed to close: $e"); - } - } - try { - _scripthashesUpdateSubject[sh] = await electrumClient.scripthashUpdate(sh); - } catch (e) { - print("failed scripthashUpdate: $e"); - } - _scripthashesUpdateSubject[sh]?.listen((event) async { - try { - await updateUnspentsForAddress(address); + await Future.wait(unsubscribedScriptHashes.map((addressRecord) async { + final scripthash = addressRecord.scriptHash; + final listener = await electrumClient2!.subscribe( + ElectrumScriptHashSubscribe(scriptHash: scripthash), + ); - await updateBalance(); + if (listener != null) { + scripthashesListening.add(scripthash); - await _fetchAddressHistory(address, await getCurrentChainTip()); - } catch (e, s) { - print("sub error: $e"); - _onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); - } - }); + // https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status + // The status of the script hash is the hash of the tx history, or null if the string is empty because there are no transactions + listener((status) async { + print("status: $status"); + + await _fetchAddressHistory(addressRecord); + print("_fetchAddressHistory: ${addressRecord.address}"); + await updateUnspentsForAddress(addressRecord); + print("updateUnspentsForAddress: ${addressRecord.address}"); + }); + } })); } - Future fetchBalances() async { - final addresses = walletAddresses.allAddresses - .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) - .toList(); + @action + Future fetchBalances(List addresses) async { final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { final addressRecord = addresses[i]; - final sh = addressRecord.scriptHash; - final balanceFuture = electrumClient.getBalance(sh); + final balanceFuture = electrumClient2!.request( + ElectrumGetScriptHashBalance(scriptHash: addressRecord.scriptHash), + ); balanceFutures.add(balanceFuture); } @@ -2082,21 +1800,6 @@ abstract class ElectrumWalletBase }); }); - 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); for (var i = 0; i < balances.length; i++) { @@ -2122,9 +1825,21 @@ abstract class ElectrumWalletBase ); } + @action + Future updateBalanceForAddress(BitcoinAddressRecord addressRecord) async { + final updatedBalance = await fetchBalances([addressRecord]); + if (balance[currency] == null) { + balance[currency] = updatedBalance; + } else { + balance[currency]!.confirmed += updatedBalance.confirmed; + balance[currency]!.unconfirmed += updatedBalance.unconfirmed; + balance[currency]!.frozen += updatedBalance.frozen; + } + } + + @action Future updateBalance() async { - print("updateBalance() called!"); - balance[currency] = await fetchBalances(); + balance[currency] = await fetchBalances(walletAddresses.allAddresses); await save(); } @@ -2210,33 +1925,22 @@ 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!); - } + @action + void onHeadersResponse(ElectrumHeaderResponse response) { + currentChainTip = response.height; + } - _chainTipUpdateSubject = electrumClient.chainTipSubscribe(); - _chainTipUpdateSubject?.listen((e) async { - final event = e as Map; - final height = int.tryParse(event['height'].toString()); + @action + Future subscribeForHeaders() async { + if (_chainTipListenerOn) return; - if (height != null) { - _currentChainTip = height; + final listener = electrumClient2!.subscribe(ElectrumHeaderSubscribe()); + if (listener == null) return; - if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - _setListeners(walletInfo.restoreHeight); - } - } - }); + _chainTipListenerOn = true; + listener(onHeadersResponse); } - static String _hardenedDerivationPath(String derivationPath) => - derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); - @action void _onConnectionStatusChange(ConnectionStatus status) { switch (status) { @@ -2250,14 +1954,14 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - if (syncStatus is! NotConnectedSyncStatus) { - syncStatus = NotConnectedSyncStatus(); - } + // if (syncStatus is! NotConnectedSyncStatus) { + // syncStatus = NotConnectedSyncStatus(); + // } break; case ConnectionStatus.failed: - if (syncStatus is! LostConnectionSyncStatus) { - syncStatus = LostConnectionSyncStatus(); - } + // if (syncStatus is! LostConnectionSyncStatus) { + // syncStatus = LostConnectionSyncStatus(); + // } break; case ConnectionStatus.connecting: if (syncStatus is! ConnectingSyncStatus) { @@ -2268,15 +1972,13 @@ abstract class ElectrumWalletBase } } - void _syncStatusReaction(SyncStatus syncStatus) async { - print("SYNC_STATUS_CHANGE: ${syncStatus}"); - if (syncStatus is SyncingSyncStatus) { - return; - } - + @action + void syncStatusReaction(SyncStatus syncStatus) { if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { // Needs to re-subscribe to all scripthashes when reconnected - _scripthashesUpdateSubject = {}; + scripthashesListening = {}; + _isTransactionUpdating = false; + _chainTipListenerOn = false; if (_isTryingToConnect) return; @@ -2295,13 +1997,6 @@ abstract class ElectrumWalletBase _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) { @@ -2367,7 +2062,6 @@ class ScanData { final ScanNode? node; final BasedUtxoNetwork network; final int chainTip; - final ElectrumClient electrumClient; final List transactionHistoryIds; final Map labels; final List labelIndexes; @@ -2380,7 +2074,6 @@ class ScanData { required this.node, required this.network, required this.chainTip, - required this.electrumClient, required this.transactionHistoryIds, required this.labels, required this.labelIndexes, @@ -2396,7 +2089,6 @@ class ScanData { network: scanData.network, chainTip: scanData.chainTip, transactionHistoryIds: scanData.transactionHistoryIds, - electrumClient: scanData.electrumClient, labels: scanData.labels, labelIndexes: scanData.labelIndexes, isSingleScan: scanData.isSingleScan, @@ -2411,320 +2103,6 @@ class SyncResponse { SyncResponse(this.height, this.syncStatus); } -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()), - ); - } -} - -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 = BitcoinReceivedSPAddressRecord( - receivingOutputAddress, - labelIndex: 0, - isUsed: true, - spendKey: scanData.silentAddress.b_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), - ), - txCount: 1, - balance: amount!, - ); - - final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount!, pos!); - - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); - }); - - 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, @@ -2760,24 +2138,6 @@ 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 { final List availableInputs; final List unconfirmedCoins; diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 68de355bd3..6cc706d024 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -265,7 +265,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress( + Future getChangeAddress( {List? inputs, List? outputs, bool isPegIn = false}) async { updateChangeAddresses(); @@ -557,19 +557,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future discoverAddresses(List addressList, bool isHidden, - Future Function(BitcoinAddressRecord) getAddressHistory, - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - final newAddresses = await _createNewAddresses(gap, - startIndex: addressList.length, isHidden: isHidden, type: type); + Future> discoverAddresses( + List addressList, + bool isHidden, { + 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; - - if (isLastAddressUsed) { - discoverAddresses(addressList, isHidden, getAddressHistory, type: type); - } + return newAddresses; } Future _generateInitialAddresses( diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 1ca89e00a7..45c6d92853 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -69,6 +70,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int? initialMwebHeight, bool? alwaysScan, + required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, password: password, @@ -82,6 +84,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ) { if (seedBytes != null) { mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( @@ -126,8 +129,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } 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) { @@ -152,19 +155,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) { @@ -193,6 +198,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -202,6 +208,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); @@ -275,6 +282,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -299,16 +307,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"); } @@ -557,8 +565,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 { @@ -772,8 +780,42 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future fetchBalances() async { - final balance = await super.fetchBalances(); + @action + Future> fetchTransactions() async { + try { + final Map historiesWithDetails = {}; + + await Future.wait(LITECOIN_ADDRESS_TYPES + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + + return historiesWithDetails; + } catch (e) { + print("fetchTransactions $e"); + return {}; + } + } + + @override + @action + Future subscribeForUpdates([ + Iterable? unsubscribedScriptHashes, + ]) async { + final unsubscribedScriptHashes = walletAddresses.allAddresses.where( + (address) => + !scripthashesListening.contains(address.scriptHash) && + address.type != SegwitAddresType.mweb, + ); + + return super.subscribeForUpdates(unsubscribedScriptHashes); + } + + @override + Future fetchBalances(List addresses) async { + final nonMwebAddresses = walletAddresses.allAddresses + .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) + .toList(); + final balance = await super.fetchBalances(nonMwebAddresses); + if (!mwebEnabled) { return balance; } @@ -871,25 +913,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) { @@ -899,7 +930,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(), + ) ]; } @@ -926,14 +959,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; diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 7cc266f5bc..2d68a86ada 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -23,12 +23,18 @@ class LitecoinWalletService extends WalletService< BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { LitecoinWalletService( - this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); + this.walletInfoSource, + this.unspentCoinsInfoSource, + this.alwaysScan, + this.isDirect, + this.mempoolAPIEnabled, + ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; final bool isDirect; + final bool mempoolAPIEnabled; @override WalletType getType() => WalletType.litecoin; @@ -55,6 +61,7 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -68,7 +75,6 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { - final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -80,6 +86,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); saveBackup(name); @@ -93,6 +100,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; @@ -135,6 +143,7 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + mempoolAPIEnabled: mempoolAPIEnabled, ); await currentWallet.renameWalletFiles(newName); @@ -186,6 +195,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 5ed84dbf45..4b77d984d2 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -4,7 +4,6 @@ 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'; @@ -51,10 +50,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; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 5cba9b734b..3ce7a37da6 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -86,20 +86,16 @@ packages: bitcoin_base: dependency: "direct overridden" description: - path: "." - ref: cake-update-v8 - resolved-ref: fc045a11db3d85d806ca67f75e8b916c706745a2 - url: "https://github.com/cake-tech/bitcoin_base" - source: git + path: "/home/rafael/Working/bitcoin_base/" + relative: false + source: path version: "4.7.0" blockchain_utils: dependency: "direct main" description: - path: "." - ref: cake-update-v2 - resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" - url: "https://github.com/cake-tech/blockchain_utils" - source: git + path: "/home/rafael/Working/blockchain_utils/" + relative: false + source: path version: "3.3.0" bluez: dependency: transitive @@ -917,11 +913,9 @@ packages: sp_scanner: dependency: "direct main" description: - path: "." - ref: "sp_v4.0.0" - resolved-ref: ca1add293bd1e06920aa049b655832da50d0dab2 - url: "https://github.com/cake-tech/sp_scanner" - source: git + path: "/home/rafael/Working/sp_scanner/" + relative: false + source: path version: "0.0.1" stack_trace: dependency: transitive diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9f1cee67d5..821f9b7f3c 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,16 +27,16 @@ dependencies: rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: + path: /home/rafael/Working/blockchain_utils/ + ledger_flutter: ^1.0.1 + ledger_bitcoin: git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + url: https://github.com/cake-tech/ledger-bitcoin 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 + path: /home/rafael/Working/sp_scanner/ bech32: git: url: https://github.com/cake-tech/bech32.git @@ -62,9 +62,9 @@ dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + path: /home/rafael/Working/bitcoin_base/ + blockchain_utils: + path: /home/rafael/Working/blockchain_utils/ 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 768c3fb4bd..9c4dba89b8 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -37,18 +37,21 @@ 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, + ) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, @@ -64,18 +67,23 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { }); } - 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, @@ -89,6 +97,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -98,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); @@ -161,6 +171,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, + mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -225,4 +236,19 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ); 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, + ); } 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 e1fa9d6e0a..f1f50855ac 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -2,7 +2,7 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_bitcoin/electrum.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -31,10 +31,10 @@ class PendingBitcoinCashTransaction with PendingTransaction { String get hex => _tx.toHex(); @override - String get amountFormatted => bitcoinAmountToString(amount: amount); + String get amountFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: amount); @override - String get feeFormatted => bitcoinAmountToString(amount: fee); + String get feeFormatted => BitcoinAmountUtils.bitcoinAmountToString(amount: fee); final List _listeners; @@ -74,15 +74,16 @@ class PendingBitcoinCashTransaction with PendingTransaction { void addListener(void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); - ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, - id: id, - height: 0, - amount: amount, - direction: TransactionDirection.outgoing, - date: DateTime.now(), - isPending: true, - confirmations: 0, - fee: fee, - isReplaced: false, + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo( + type, + id: id, + height: 0, + amount: amount, + direction: TransactionDirection.outgoing, + date: DateTime.now(), + isPending: true, + confirmations: 0, + fee: fee, + isReplaced: false, ); } diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index cd1e52f510..eb2eceef3e 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -26,9 +26,7 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ dev_dependencies: flutter_test: @@ -40,9 +38,9 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + path: /home/rafael/Working/bitcoin_base/ + blockchain_utils: + path: /home/rafael/Working/blockchain_utils/ # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285d..5790159dfa 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; } diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index c173f1ddda..5eb5576f3f 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -1,6 +1,16 @@ 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); } diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux new file mode 120000 index 0000000000..5dc8fb651e --- /dev/null +++ b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux @@ -0,0 +1 @@ +/home/rafael/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index e69fd7ca04..19c7781352 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -16,13 +16,9 @@ dependencies: cw_evm: path: ../cw_evm on_chain: - git: - url: https://github.com/cake-tech/On_chain - ref: cake-update-v2 + path: /home/rafael/Working/On_chain/ blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v2 + path: /home/rafael/Working/blockchain_utils/ mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -34,6 +30,11 @@ dev_dependencies: build_runner: ^2.3.3 mobx_codegen: ^2.1.1 hive_generator: ^1.1.3 + +dependency_overrides: + blockchain_utils: + path: /home/rafael/Working/blockchain_utils/ + flutter: # assets: # - images/a_dot_burr.jpeg diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index cc7e97cd98..e1efa93902 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -54,7 +54,7 @@ class CWBitcoin extends Bitcoin { name: name, hwAccountData: accountData, walletInfo: walletInfo); @override - TransactionPriority getMediumTransactionPriority() => BitcoinTransactionPriority.medium; + TransactionPriority getMediumTransactionPriority() => BitcoinElectrumTransactionPriority.elevated; @override List getWordList() => wordlist; @@ -72,14 +72,14 @@ class CWBitcoin extends Bitcoin { } @override - List getTransactionPriorities() => BitcoinTransactionPriority.all; + List getTransactionPriorities() => BitcoinElectrumTransactionPriority.all; @override List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => - BitcoinTransactionPriority.deserialize(raw: raw); + BitcoinElectrumTransactionPriority.deserialize(raw: raw); @override TransactionPriority deserializeLitecoinTransactionPriority(int raw) => @@ -113,7 +113,7 @@ class CWBitcoin extends Bitcoin { UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { final bitcoinFeeRate = - priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null; + priority == BitcoinElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( outputs .map((out) => OutputInfo( @@ -127,7 +127,7 @@ class CWBitcoin extends Bitcoin { formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo)) .toList(), - priority: priority as BitcoinTransactionPriority, + priority: priority as BitcoinElectrumTransactionPriority, feeRate: bitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -171,7 +171,7 @@ class CWBitcoin extends Bitcoin { wallet, wallet.type == WalletType.litecoin ? priority as LitecoinTransactionPriority - : priority as BitcoinTransactionPriority, + : priority as BitcoinElectrumTransactionPriority, ), ); @@ -189,19 +189,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 BitcoinElectrumTransactionPriority).labelWithRate(rate, customRate); @override List getUnspents(Object wallet, @@ -224,27 +225,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() => + BitcoinElectrumTransactionPriority.elevated; @override - TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom; + TransactionPriority getBitcoinTransactionPriorityCustom() => + BitcoinElectrumTransactionPriority.custom; @override TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; @override - TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow; + TransactionPriority getBitcoinTransactionPrioritySlow() => + BitcoinElectrumTransactionPriority.normal; @override TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; @@ -443,7 +469,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 +484,7 @@ class CWBitcoin extends Bitcoin { {int? size}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.feeAmountForPriority( - priority as BitcoinTransactionPriority, inputsCount, outputsCount); + priority as BitcoinElectrumTransactionPriority, inputsCount, outputsCount); } @override @@ -483,7 +509,7 @@ class CWBitcoin extends Bitcoin { @override int getMaxCustomFeeRate(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; - return (bitcoinWallet.feeRate(BitcoinTransactionPriority.fast) * 10).round(); + return (bitcoinWallet.feeRate(BitcoinElectrumTransactionPriority.priority) * 10).round(); } @override @@ -564,7 +590,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); } @@ -576,13 +602,14 @@ class CWBitcoin extends Bitcoin { @override Future registerSilentPaymentsKey(Object wallet, bool active) async { - return; + final bitcoinWallet = wallet as ElectrumWallet; + return await bitcoinWallet.registerSilentPaymentsKey(); } @override Future checkIfMempoolAPIIsEnabled(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; - return await bitcoinWallet.checkIfMempoolAPIIsEnabled(); + return await bitcoinWallet.mempoolAPIEnabled; } @override @@ -600,7 +627,7 @@ 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); } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index b744487034..a0cb406c2d 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,7 +39,10 @@ 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); 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 13ffd839e3..99c3c56b66 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -367,14 +367,14 @@ Future setup({ (WalletType type) => getIt.get(param1: type))); getIt.registerFactoryParam( - (newWalletArgs, _) => WalletNewVM( - getIt.get(), - getIt.get(param1:newWalletArgs.type), - _walletInfoSource, - getIt.get(param1: newWalletArgs.type), - getIt.get(), - newWalletArguments: newWalletArgs,)); - + (newWalletArgs, _) => WalletNewVM( + getIt.get(), + getIt.get(param1: newWalletArgs.type), + _walletInfoSource, + getIt.get(param1: newWalletArgs.type), + getIt.get(), + newWalletArguments: newWalletArgs, + )); getIt.registerFactory(() => NewWalletTypeViewModel(_walletInfoSource)); @@ -397,62 +397,52 @@ Future setup({ ); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_loadable'); getIt.registerFactory( - () => getIt.get( - param1: WalletUnlockArguments( - callback: (bool successful, _) { - if (successful) { - final authStore = getIt.get(); - authStore.allowed(); - }}), - param2: false, - instanceName: 'wallet_unlock_loadable'), - instanceName: 'wallet_password_login'); + () => getIt.get( + param1: WalletUnlockArguments(callback: (bool successful, _) { + if (successful) { + final authStore = getIt.get(); + authStore.allowed(); + } + }), + param2: false, + instanceName: 'wallet_unlock_loadable'), + instanceName: 'wallet_password_login'); getIt.registerFactoryParam((args, closable) { - return WalletUnlockPage( - getIt.get(param1: args), - args.callback, - args.authPasswordHandler, - closable: closable); + return WalletUnlockPage(getIt.get(param1: args), args.callback, + args.authPasswordHandler, + closable: closable); }, instanceName: 'wallet_unlock_verifiable'); getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockLoadableViewModel( - getIt.get(), - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockLoadableViewModel(getIt.get(), getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); - getIt.registerFactoryParam((args, _) { - final currentWalletName = getIt - .get() - .getString(PreferencesKey.currentWalletName) ?? ''; + getIt.registerFactoryParam( + (args, _) { + final currentWalletName = + getIt.get().getString(PreferencesKey.currentWalletName) ?? ''; final currentWalletTypeRaw = - getIt.get() - .getInt(PreferencesKey.currentWalletType) ?? 0; + getIt.get().getInt(PreferencesKey.currentWalletType) ?? 0; final currentWalletType = deserializeFromInt(currentWalletTypeRaw); - return WalletUnlockVerifiableViewModel( - getIt.get(), - walletName: args.walletName ?? currentWalletName, - walletType: args.walletType ?? currentWalletType); + return WalletUnlockVerifiableViewModel(getIt.get(), + walletName: args.walletName ?? currentWalletName, + walletType: args.walletType ?? currentWalletType); }); getIt.registerFactoryParam((WalletType type, _) => @@ -785,7 +775,6 @@ Future setup({ ); getIt.registerFactoryParam((arguments, _) { - return WalletEditPage( pageArguments: WalletEditPageArguments( walletEditViewModel: getIt.get(param1: arguments.walletListViewModel), @@ -884,8 +873,9 @@ Future setup({ getIt.registerFactory(() => TrocadorProvidersViewModel(getIt.get())); getIt.registerFactory(() { - return OtherSettingsViewModel(getIt.get(), getIt.get().wallet!, - getIt.get());}); + return OtherSettingsViewModel( + getIt.get(), getIt.get().wallet!, getIt.get()); + }); getIt.registerFactory(() { return SecuritySettingsViewModel(getIt.get()); @@ -893,7 +883,8 @@ Future setup({ getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedSettingsViewModel(getIt.get(), getIt.get())); + getIt.registerFactory( + () => SeedSettingsViewModel(getIt.get(), getIt.get())); getIt.registerFactoryParam((bool isWalletCreated, _) => WalletSeedPage(getIt.get(), isNewWalletCreated: isWalletCreated)); @@ -1037,6 +1028,7 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().silentPaymentsAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, ); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService( @@ -1044,16 +1036,22 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().mwebAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.bitcoinCash: - return bitcoinCash!.createBitcoinCashWalletService(_walletInfoSource, - _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return bitcoinCash!.createBitcoinCashWalletService( + _walletInfoSource, + _unspentCoinsInfoSource, + SettingsStoreBase.walletPasswordDirectInput, + getIt.get().useMempoolFeeAPI, + ); case WalletType.nano: case WalletType.banano: - return nano!.createNanoWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return nano!.createNanoWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.polygon: return polygon!.createPolygonWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); @@ -1061,7 +1059,8 @@ Future setup({ return solana!.createSolanaWalletService( _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.tron: - return tron!.createTronWalletService(_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); + return tron!.createTronWalletService( + _walletInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.wownero: return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: @@ -1100,40 +1099,36 @@ Future setup({ param1: derivations, ))); - getIt.registerFactoryParam, void>( - (params, _) { - final transactionInfo = params[0] as TransactionInfo; - final canReplaceByFee = params[1] as bool? ?? false; - final wallet = getIt.get().wallet!; + getIt.registerFactoryParam, void>((params, _) { + final transactionInfo = params[0] as TransactionInfo; + final canReplaceByFee = params[1] as bool? ?? false; + final wallet = getIt.get().wallet!; - return TransactionDetailsViewModel( - transactionInfo: transactionInfo, - transactionDescriptionBox: _transactionDescriptionBox, - wallet: wallet, - settingsStore: getIt.get(), - sendViewModel: getIt.get(), - canReplaceByFee: canReplaceByFee, - ); - } - ); + return TransactionDetailsViewModel( + transactionInfo: transactionInfo, + transactionDescriptionBox: _transactionDescriptionBox, + wallet: wallet, + settingsStore: getIt.get(), + sendViewModel: getIt.get(), + canReplaceByFee: canReplaceByFee, + ); + }); getIt.registerFactoryParam( - (TransactionInfo transactionInfo, _) => TransactionDetailsPage( - transactionDetailsViewModel: getIt.get( - param1: [transactionInfo, false]))); - - 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, _) { @@ -1155,8 +1150,7 @@ Future setup({ getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, - _transactionDescriptionBox, - getIt.get(), getIt.get())); + _transactionDescriptionBox, getIt.get(), getIt.get())); getIt.registerFactory(() => BackupViewModel( getIt.get(), getIt.get(), getIt.get())); diff --git a/lib/main.dart b/lib/main.dart index 29b216b22c..8c30dcf3b0 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/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index d2a4f3600d..ebf952a565 100644 --- a/lib/src/screens/settings/silent_payments_settings.dart +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -39,9 +39,9 @@ class SilentPaymentsSettingsPage extends BasePage { ), SettingsSwitcherCell( title: S.current.silent_payments_register_key, - value: _silentPaymentsSettingsViewModel.silentPaymentsAlwaysScan, + value: _silentPaymentsSettingsViewModel.silentPaymentsKeyRegistered, onValueChange: (_, bool value) { - _silentPaymentsSettingsViewModel.setSilentPaymentsAlwaysScan(value); + _silentPaymentsSettingsViewModel.registerSilentPaymentsKey(value); }, ), SettingsCellWithArrow( diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index cd39318f4c..bc06fbcc4b 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -602,7 +602,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; - static final walletPasswordDirectInput = Platform.isLinux; + static final walletPasswordDirectInput = false; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; 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 37c2f64867..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; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index d5fce76e9e..0402ba1594 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -132,9 +132,7 @@ dependency_overrides: flutter_secure_storage_platform_interface: 1.0.2 protobuf: ^3.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v8 + path: /home/rafael/Working/bitcoin_base/ ffi: 2.1.0 flutter_icons: diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 3d6b0c8bbd..4f222c2ed9 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -369,7 +369,7 @@ "litecoin_mweb": "Mweb", "litecoin_mweb_always_scan": "Definir mweb sempre digitalizando", "litecoin_mweb_description": "MWEB é um novo protocolo que traz transações mais rápidas, baratas e mais privadas para o Litecoin", - "litecoin_mweb_dismiss": "Liberar", + "litecoin_mweb_dismiss": "Ocultar", "litecoin_mweb_display_card": "Mostre o cartão MWEB", "litecoin_mweb_enable_later": "Você pode optar por ativar o MWEB novamente em Configurações de exibição.", "litecoin_mweb_pegin": "Peg in", @@ -942,4 +942,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} \ No newline at end of file +} diff --git a/scripts/android/app_env.fish b/scripts/android/app_env.fish new file mode 100644 index 0000000000..c290a35931 --- /dev/null +++ b/scripts/android/app_env.fish @@ -0,0 +1,78 @@ +#!/usr/bin/env fish + +set APP_ANDROID_NAME "" +set APP_ANDROID_VERSION "" +set APP_ANDROID_BUILD_VERSION "" +set APP_ANDROID_ID "" +set APP_ANDROID_PACKAGE "" +set APP_ANDROID_SCHEME "" + +set MONERO_COM "monero.com" +set CAKEWALLET cakewallet +set HAVEN haven + +set -l TYPES $MONERO_COM $CAKEWALLET $HAVEN +set APP_ANDROID_TYPE $argv[1] + +set MONERO_COM_NAME "Monero.com" +set MONERO_COM_VERSION "1.17.0" +set MONERO_COM_BUILD_NUMBER 103 +set MONERO_COM_BUNDLE_ID "com.monero.app" +set MONERO_COM_PACKAGE "com.monero.app" +set MONERO_COM_SCHEME "monero.com" + +set CAKEWALLET_NAME "Cake Wallet" +set CAKEWALLET_VERSION "4.20.0" +set CAKEWALLET_BUILD_NUMBER 232 +set CAKEWALLET_BUNDLE_ID "com.cakewallet.cake_wallet" +set CAKEWALLET_PACKAGE "com.cakewallet.cake_wallet" +set CAKEWALLET_SCHEME cakewallet + +set HAVEN_NAME Haven +set HAVEN_VERSION "1.0.0" +set HAVEN_BUILD_NUMBER 1 +set HAVEN_BUNDLE_ID "com.cakewallet.haven" +set HAVEN_PACKAGE "com.cakewallet.haven" + +if not contains $APP_ANDROID_TYPE $TYPES + echo "Wrong app type." + return 1 + exit 1 +end + +switch $APP_ANDROID_TYPE + case $MONERO_COM + set APP_ANDROID_NAME $MONERO_COM_NAME + set APP_ANDROID_VERSION $MONERO_COM_VERSION + set APP_ANDROID_BUILD_NUMBER $MONERO_COM_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $MONERO_COM_BUNDLE_ID + set APP_ANDROID_PACKAGE $MONERO_COM_PACKAGE + set APP_ANDROID_SCHEME $MONERO_COM_SCHEME + + case $CAKEWALLET + set APP_ANDROID_NAME $CAKEWALLET_NAME + set APP_ANDROID_VERSION $CAKEWALLET_VERSION + set APP_ANDROID_BUILD_NUMBER $CAKEWALLET_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $CAKEWALLET_BUNDLE_ID + set APP_ANDROID_PACKAGE $CAKEWALLET_PACKAGE + set APP_ANDROID_SCHEME $CAKEWALLET_SCHEME + + case $HAVEN + set APP_ANDROID_NAME $HAVEN_NAME + set APP_ANDROID_VERSION $HAVEN_VERSION + set APP_ANDROID_BUILD_NUMBER $HAVEN_BUILD_NUMBER + set APP_ANDROID_BUNDLE_ID $HAVEN_BUNDLE_ID + set APP_ANDROID_PACKAGE $HAVEN_PACKAGE + +end + +export APP_ANDROID_TYPE +export APP_ANDROID_NAME +export APP_ANDROID_VERSION +export APP_ANDROID_BUILD_NUMBER +export APP_ANDROID_BUNDLE_ID +export APP_ANDROID_PACKAGE +export APP_ANDROID_SCHEME +export APP_ANDROID_BUNDLE_ID +export APP_ANDROID_PACKAGE +export APP_ANDROID_SCHEME diff --git a/tool/configure.dart b/tool/configure.dart index 16370e977f..68408ee2e4 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -114,7 +114,6 @@ import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_wallet_service.dart'; import 'package:cw_bitcoin/bitcoin_wallet_creation_credentials.dart'; -import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; @@ -182,8 +181,19 @@ abstract class Bitcoin { List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); WalletService createBitcoinWalletService( - Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); - WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect); + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ); + WalletService createLitecoinWalletService( + Box walletInfoSource, + Box unspentCoinSource, + bool alwaysScan, + bool isDirect, + bool mempoolAPIEnabled, + ); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); TransactionPriority getLitecoinTransactionPriorityMedium(); @@ -1016,9 +1026,6 @@ abstract class Polygon { Future generateBitcoinCash(bool hasImplementation) async { final outputFile = File(bitcoinCashOutputPath); const bitcoinCashCommonHeaders = """ -import 'dart:typed_data'; - -import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -1036,7 +1043,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}); From 64caf8479ea7bce9ff0bb92bcd3f3648214b7d50 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 29 Oct 2024 20:52:19 -0300 Subject: [PATCH 03/64] fix: restore flow slow, checking unspents --- cw_bitcoin/lib/bitcoin_address_record.dart | 12 +- .../lib/bitcoin_hardware_wallet_service.dart | 5 +- cw_bitcoin/lib/bitcoin_unspent.dart | 6 + cw_bitcoin/lib/bitcoin_wallet.dart | 22 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 36 ++- .../bitcoin_wallet_creation_credentials.dart | 17 +- cw_bitcoin/lib/bitcoin_wallet_service.dart | 5 +- cw_bitcoin/lib/electrum_wallet.dart | 257 ++++++++---------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 194 ++++++------- .../lib/litecoin_hardware_wallet_service.dart | 13 +- cw_bitcoin/lib/litecoin_wallet.dart | 52 ++-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 22 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 1 + cw_bitcoin/pubspec.lock | 4 +- cw_bitcoin/pubspec.yaml | 4 - .../src/bitcoin_cash_wallet_addresses.dart | 6 +- cw_core/lib/wallet_keys_file.dart | 5 +- lib/bitcoin/cw_bitcoin.dart | 123 ++++----- .../screens/restore/wallet_restore_page.dart | 68 ++--- lib/store/settings_store.dart | 1 + .../restore/restore_from_qr_vm.dart | 19 +- lib/view_model/wallet_restore_view_model.dart | 2 - tool/configure.dart | 2 - 23 files changed, 394 insertions(+), 482 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 2c3abad0ff..43c1d5e14a 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -12,15 +12,19 @@ abstract class BaseBitcoinAddressRecord { String name = '', bool isUsed = false, required this.type, + bool? isHidden, }) : _txCount = txCount, _balance = balance, _name = name, - _isUsed = isUsed; + _isUsed = isUsed, + _isHidden = isHidden ?? isChange; @override bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; final String address; + final bool _isHidden; + bool get isHidden => _isHidden; bool isChange; final int index; int _txCount; @@ -54,6 +58,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { BitcoinAddressRecord( super.address, { required super.index, + super.isHidden, super.isChange = false, super.txCount = 0, super.balance = 0, @@ -76,6 +81,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] 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, @@ -95,6 +101,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String toJSON() => json.encode({ 'address': address, 'index': index, + 'isHidden': isHidden, 'isChange': isChange, 'isUsed': isUsed, 'txCount': txCount, @@ -117,6 +124,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { super.name = '', super.isUsed = false, super.type = SilentPaymentsAddresType.p2sp, + super.isHidden, this.labelHex, }) : super(index: labelIndex, isChange: labelIndex == 0) { if (labelIndex != 1 && labelHex == null) { @@ -165,7 +173,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { required this.spendKey, super.type = SegwitAddresType.p2tr, super.labelHex, - }); + }) : super(isHidden: true); factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index 582147e3d2..c63c1fe3a4 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -22,11 +22,10 @@ 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 bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); accounts.add(HardwareAccountData( - address: P2wpkhAddress.fromBip32(bip32: hd, account: i, index: 0) + address: P2wpkhAddress.fromBip32(bip32: bip32, isChange: false, index: i) .toAddress(BitcoinNetwork.mainnet), accountIndex: i, derivationPath: derivationPath, diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index a57ad9a8bb..b10eb47f68 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -29,4 +29,10 @@ class BitcoinUnspent extends Unspent { } final BaseBitcoinAddressRecord bitcoinAddressRecord; + + @override + bool operator ==(Object o) { + print('BitcoinUnspent operator =='); + return o is BitcoinUnspent && hash == o.hash && vout == o.vout; + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index b974eeb47f..6a0e3f4e7b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -7,6 +7,7 @@ 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/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'; @@ -45,7 +46,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, @@ -89,6 +90,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, bip32: bip32, network: networkParam ?? network, + isHardwareWallet: walletInfo.isHardwareWallet, ); autorun((_) { @@ -113,20 +115,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, required bool mempoolAPIEnabled, }) async { - late Uint8List seedBytes; + late List seedBytes; switch (walletInfo.derivationInfo?.derivationType) { case DerivationType.bip39: - seedBytes = await bip39.mnemonicToSeed( - mnemonic, - passphrase: passphrase ?? "", - ); + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); break; case DerivationType.electrum: default: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); break; } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -199,7 +199,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; - Uint8List? seedBytes = null; + List? seedBytes = null; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; @@ -360,7 +360,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); - unspentCoins = updatedUnspentCoins; + unspentCoins = updatedUnspentCoins.toSet(); if (unspentCoinsInfo.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); @@ -642,8 +642,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @action - Future fetchBalances(List addresses) async { - final balance = await super.fetchBalances(addresses); + Future fetchBalances() async { + final balance = await super.fetchBalances(); int totalFrozen = balance.frozen; int totalConfirmed = balance.confirmed; diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 931c58e710..ab7a45d4f9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,5 +1,4 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:blockchain_utils/bip/bip/bip32/bip32.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -21,34 +20,47 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S super.initialSilentAddressIndex = 0, }) : super(walletInfo); + @override + Future init() async { + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); + + if (!isHardwareWallet) { + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(type: SegwitAddresType.p2tr); + await generateInitialAddresses(type: SegwitAddresType.p2wsh); + } + + await updateAddressesInBox(); + } + @override BitcoinBaseAddress generateAddress({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) { switch (addressType) { case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromBip32(account: account, bip32: hd, index: index); + return P2pkhAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); case SegwitAddresType.p2tr: - return P2trAddress.fromBip32(account: account, bip32: hd, index: index); + return P2trAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); case SegwitAddresType.p2wsh: - return P2wshAddress.fromBip32(account: account, bip32: hd, index: index); + return P2wshAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); case P2shAddressType.p2wpkhInP2sh: return P2shAddress.fromBip32( - account: account, - bip32: hd, + bip32: bip32, + isChange: isChange, index: index, type: P2shAddressType.p2wpkhInP2sh, ); case SegwitAddresType.p2wpkh: return P2wpkhAddress.fromBip32( - account: account, - bip32: hd, + bip32: bip32, + isChange: isChange, index: index, - isElectrum: true, - ); // TODO: + isElectrum: false, // TODO: + ); default: throw ArgumentError('Invalid address type'); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index a1b1418b8d..cd615ad2b4 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, @@ -29,18 +27,13 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { required String password, required this.mnemonic, 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 45ef9b653b..a384523293 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -89,7 +89,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, mempoolAPIEnabled: mempoolAPIEnabled, - encryptionFileUtils: encryptionFileUtilsFor(false), + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); saveBackup(name); @@ -103,7 +103,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, mempoolAPIEnabled: mempoolAPIEnabled, - encryptionFileUtils: encryptionFileUtilsFor(false), + encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); return wallet; @@ -189,7 +189,6 @@ class BitcoinWalletService extends WalletService< encryptionFileUtils: encryptionFileUtilsFor(isDirect), mempoolAPIEnabled: mempoolAPIEnabled, ); - await wallet.save(); await wallet.init(); return wallet; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6481045403..373b680855 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -56,7 +56,7 @@ abstract class ElectrumWalletBase required this.encryptionFileUtils, String? xpub, String? mnemonic, - Uint8List? seedBytes, + List? seedBytes, this.passphrase, List? initialAddresses, ElectrumClient? electrumClient, @@ -69,7 +69,7 @@ abstract class ElectrumWalletBase _password = password, _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, - unspentCoins = [], + unspentCoins = {}, scripthashesListening = {}, balance = ObservableMap.of(currency != null ? { @@ -99,7 +99,7 @@ abstract class ElectrumWalletBase } 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"); @@ -121,7 +121,7 @@ abstract class ElectrumWalletBase return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } - static Bip32Slip10Secp256k1 bitcoinCashHDWallet(Uint8List seedBytes) => + static Bip32Slip10Secp256k1 bitcoinCashHDWallet(List seedBytes) => Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; int estimatedTransactionSize(int inputsCount, int outputsCounts) => @@ -221,7 +221,8 @@ abstract class ElectrumWalletBase ); String _password; - List unspentCoins; + @observable + Set unspentCoins; @observable TransactionPriorities? feeRates; @@ -242,7 +243,6 @@ abstract class ElectrumWalletBase Future init() async { await walletAddresses.init(); await transactionHistory.init(); - await save(); _autoSaveTimer = Timer.periodic(Duration(minutes: _autoSaveInterval), (_) async => await save()); @@ -263,13 +263,15 @@ abstract class ElectrumWalletBase // await updateTransactions(); // await updateAllUnspents(); - // await updateBalance(); + await updateBalance(); await updateFeeRates(); _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); syncStatus = SyncedSyncStatus(); + + await save(); } catch (e, stacktrace) { print(stacktrace); print("startSync $e"); @@ -459,7 +461,7 @@ abstract class ElectrumWalletBase } else if (!isHardwareWallet) { privkey = ECPrivate.fromBip32( bip32: walletAddresses.bip32, - account: utx.bitcoinAddressRecord.isChange ? 1 : 0, + account: BitcoinAddressUtils.getAccountFromChange(utx.bitcoinAddressRecord.isChange), index: utx.bitcoinAddressRecord.index, ); } @@ -660,11 +662,11 @@ 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 = BitcoinAddressUtils.getDerivationPath( + type: changeAddress.type, + account: changeAddress.isChange ? 1 : 0, + index: changeAddress.index, + ); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); @@ -1105,12 +1107,12 @@ 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(); await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); - await transactionHistory.save(); + // await transactionHistory.save(); } @override @@ -1174,7 +1176,7 @@ abstract class ElectrumWalletBase updatedUnspentCoins.addAll(await fetchUnspent(address)); })); - unspentCoins = updatedUnspentCoins; + unspentCoins = updatedUnspentCoins.toSet(); if (unspentCoinsInfo.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); @@ -1182,9 +1184,10 @@ abstract class ElectrumWalletBase } await updateCoins(unspentCoins); - await refreshUnspentCoinsInfo(); + // await refreshUnspentCoinsInfo(); } + @action void updateCoin(BitcoinUnspent coin) { final coinInfoList = unspentCoinsInfo.values.where( (element) => @@ -1204,7 +1207,8 @@ abstract class ElectrumWalletBase } } - Future updateCoins(List newUnspentCoins) async { + @action + Future updateCoins(Set newUnspentCoins) async { if (newUnspentCoins.isEmpty) { return; } @@ -1213,8 +1217,26 @@ abstract class ElectrumWalletBase @action Future updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async { - final newUnspentCoins = await fetchUnspent(addressRecord); + final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet(); await updateCoins(newUnspentCoins); + + print([1, unspentCoins.containsAll(newUnspentCoins)]); + if (!unspentCoins.containsAll(newUnspentCoins)) { + newUnspentCoins.forEach((coin) { + print(unspentCoins.contains(coin)); + print([coin.vout, coin.hash]); + print([unspentCoins.first.vout, unspentCoins.first.hash]); + if (!unspentCoins.contains(coin)) { + unspentCoins.add(coin); + } + }); + } + + // if (unspentCoinsInfo.length != unspentCoins.length) { + // unspentCoins.forEach(addCoinInfo); + // } + + // await refreshUnspentCoinsInfo(); } @action @@ -1231,11 +1253,6 @@ abstract class ElectrumWalletBase final tx = await fetchTransactionInfo(hash: coin.hash); coin.isChange = address.isChange; coin.confirmations = tx?.confirmations; - if (coin.isFrozen) { - balance[currency]!.frozen += coin.value; - } else { - balance[currency]!.confirmed += coin.value; - } updatedUnspentCoins.add(coin); } catch (_) {} @@ -1492,64 +1509,65 @@ abstract class ElectrumWalletBase } } - Future getTransactionExpanded( - {required String hash, int? height}) async { - String transactionHex = ''; + Future getTransactionExpanded({required String hash}) async { int? time; - int? confirmations; + int? height; - try { - final verboseTransaction = await electrumClient2!.request( - ElectrumGetTransactionVerbose(transactionHash: hash), - ); + final transactionHex = await electrumClient2!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); - transactionHex = verboseTransaction['hex'] as String; - time = verboseTransaction['time'] as int?; - confirmations = verboseTransaction['confirmations'] as int?; - } catch (e) { - if (e is RPCError || e is TimeoutException) { - transactionHex = await electrumClient2!.request( - ElectrumGetTransactionHex(transactionHash: hash), + // TODO: + // if (mempoolAPIEnabled) { + if (true) { + try { + final txVerbose = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", + ), ); - if (height != null && height > 0 && mempoolAPIEnabled) { - try { - final blockHash = await http.get( + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + ), + ); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", ), ); - 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()); - } + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); } - } catch (_) {} + } } - } + } catch (_) {} } + int? confirmations; + if (height != null) { if (time == null && height > 0) { time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } - if (confirmations == null) { - final tip = currentChainTip!; - if (tip > 0 && height > 0) { - // Add one because the block itself is the first confirmation - confirmations = tip - height + 1; - } + final tip = currentChainTip!; + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; } } @@ -1557,19 +1575,9 @@ abstract class ElectrumWalletBase final ins = []; for (final vin in original.inputs) { - String inputTransactionHex = ""; - try { - final verboseTransaction = await electrumClient2!.request( - ElectrumGetTransactionVerbose(transactionHash: vin.txId), - ); - inputTransactionHex = verboseTransaction['hex'] as String; - } catch (e) { - if (e is RPCError || e is TimeoutException) { - inputTransactionHex = await electrumClient2!.request( - ElectrumGetTransactionHex(transactionHash: vin.txId), - ); - } - } + final inputTransactionHex = await electrumClient2!.request( + ElectrumGetTransactionHex(transactionHash: vin.txId), + ); ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } @@ -1585,7 +1593,7 @@ abstract class ElectrumWalletBase 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, @@ -1674,7 +1682,6 @@ abstract class ElectrumWalletBase // Got a new transaction fetched, add it to the transaction history // instead of waiting all to finish, and next time it will be faster transactionHistory.addOne(tx); - await transactionHistory.save(); } } @@ -1682,34 +1689,26 @@ abstract class ElectrumWalletBase })); final totalAddresses = (addressRecord.isChange - ? walletAddresses.allAddresses - .where((addr) => addr.isChange && addr.type == addressRecord.type) + ? walletAddresses.changeAddresses + .where((addr) => addr.type == addressRecord.type) .length - : walletAddresses.allAddresses - .where((addr) => !addr.isChange && addr.type == addressRecord.type) + : walletAddresses.receiveAddresses + .where((addr) => addr.type == addressRecord.type) .length); final gapLimit = (addressRecord.isChange ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - print("gapLimit: $gapLimit"); - print("index: ${addressRecord.index}"); - final isUsedAddressUnderGap = addressRecord.index >= totalAddresses - gapLimit; - print("isUsedAddressAtGapLimit: $isUsedAddressUnderGap"); - print("total: $totalAddresses"); + final isUsedAddressUnderGap = addressRecord.index < totalAddresses && + (addressRecord.index >= totalAddresses - gapLimit); if (isUsedAddressUnderGap) { // Discover new addresses for the same address type until the gap limit is respected - final newAddresses = await walletAddresses.discoverAddresses( - walletAddresses.allAddresses - .where((addr) => - (addressRecord.isChange ? addr.isChange : !addr.isChange) && - addr.type == addressRecord.type) - .toList(), - addressRecord.isChange, + await walletAddresses.discoverAddresses( + isChange: addressRecord.isChange, + gap: gapLimit, type: addressRecord.type, ); - await subscribeForUpdates(newAddresses); } } @@ -1765,58 +1764,29 @@ abstract class ElectrumWalletBase print("status: $status"); await _fetchAddressHistory(addressRecord); - print("_fetchAddressHistory: ${addressRecord.address}"); await updateUnspentsForAddress(addressRecord); - print("updateUnspentsForAddress: ${addressRecord.address}"); }); } })); } @action - Future fetchBalances(List addresses) async { - final balanceFutures = >>[]; - for (var i = 0; i < addresses.length; i++) { - final addressRecord = addresses[i]; - final balanceFuture = electrumClient2!.request( - ElectrumGetScriptHashBalance(scriptHash: addressRecord.scriptHash), - ); - balanceFutures.add(balanceFuture); - } - + Future fetchBalances() async { 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; - } - }); - }); - - final balances = await Future.wait(balanceFutures); + unspentCoins.forEach((element) { + if (element.isFrozen) { + totalFrozen += element.value; + } - for (var i = 0; i < balances.length; i++) { - final addressRecord = addresses[i]; - final balance = balances[i]; - try { - 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(); - } - } catch (_) {} - } + if (element.confirmations == 0) { + totalUnconfirmed += element.value; + } else { + totalConfirmed += element.value; + } + }); return ElectrumBalance( confirmed: totalConfirmed, @@ -1825,22 +1795,9 @@ abstract class ElectrumWalletBase ); } - @action - Future updateBalanceForAddress(BitcoinAddressRecord addressRecord) async { - final updatedBalance = await fetchBalances([addressRecord]); - if (balance[currency] == null) { - balance[currency] = updatedBalance; - } else { - balance[currency]!.confirmed += updatedBalance.confirmed; - balance[currency]!.unconfirmed += updatedBalance.unconfirmed; - balance[currency]!.frozen += updatedBalance.frozen; - } - } - @action Future updateBalance() async { - balance[currency] = await fetchBalances(walletAddresses.allAddresses); - await save(); + balance[currency] = await fetchBalances(); } @override diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 6cc706d024..c6600841bb 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -43,15 +43,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { int initialSilentAddressIndex = 0, List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, - }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), + }) : _allAddresses = (initialAddresses ?? []).toSet(), addressesByReceiveType = ObservableList.of(([]).toSet()), - receiveAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => !addressRecord.isChange && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of((initialAddresses ?? []) - .where((addressRecord) => addressRecord.isChange && !addressRecord.isUsed) - .toSet()), + receiveAddresses = ObservableList.of( + (initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()), + // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + changeAddresses = ObservableList.of( + (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? @@ -92,7 +91,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; - final ObservableList _addresses; + @observable + final Set _allAddresses; final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -102,6 +102,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final ObservableList mwebAddresses; final BasedUtxoNetwork network; final Bip32Slip10Secp256k1 bip32; + final bool isHardwareWallet; @observable SilentPaymentOwner? silentAddress; @@ -116,7 +117,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { String? activeSilentAddress; @computed - List get allAddresses => _addresses; + List get allAddresses => _allAddresses.toList(); @override @computed @@ -177,21 +178,18 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return; } try { - final addressRecord = _addresses.firstWhere( + final addressRecord = _allAddresses.firstWhere( (addressRecord) => addressRecord.address == addr, ); previousAddressRecord = addressRecord; - receiveAddresses.remove(addressRecord); - receiveAddresses.insert(0, addressRecord); } catch (e) { print("ElectrumWalletAddressBase: set address ($addr): $e"); } } @override - String get primaryAddress => - getAddress(account: 0, index: 0, hd: bip32, addressType: addressPageType); + String get primaryAddress => getAddress(isChange: false, index: 0, addressType: addressPageType); Map currentReceiveAddressIndexByType; @@ -233,19 +231,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { if (walletInfo.type == WalletType.bitcoinCash) { - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); } else if (walletInfo.type == WalletType.litecoin) { - await _generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await _generateInitialAddresses(type: SegwitAddresType.mweb); + await generateInitialAddresses(type: SegwitAddresType.mweb); } } else if (walletInfo.type == WalletType.bitcoin) { - await _generateInitialAddresses(); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); if (!isHardwareWallet) { - await _generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await _generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await _generateInitialAddresses(type: SegwitAddresType.p2tr); - await _generateInitialAddresses(type: SegwitAddresType.p2wsh); + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(type: SegwitAddresType.p2tr); + await generateInitialAddresses(type: SegwitAddresType.p2wsh); } } @@ -265,14 +263,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress( + 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); + final newAddresses = await _createNewAddresses(gap, isChange: true); addAddresses(newAddresses); } @@ -331,47 +327,45 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); final address = BitcoinAddressRecord( - getAddress(account: 0, index: newAddressIndex, hd: bip32, addressType: addressPageType), + getAddress(isChange: false, index: newAddressIndex, addressType: addressPageType), index: newAddressIndex, isChange: false, name: label, type: addressPageType, network: network, ); - _addresses.add(address); + _allAddresses.add(address); Future.delayed(Duration.zero, () => updateAddressesByMatch()); return address; } BitcoinBaseAddress generateAddress({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) { throw UnimplementedError(); } String getAddress({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) { - return generateAddress(account: account, index: index, hd: hd, addressType: addressType) + return generateAddress(isChange: isChange, index: index, addressType: addressType) .toAddress(network); } Future getAddressAsync({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) async => - getAddress(account: account, index: index, hd: hd, addressType: addressType); + getAddress(isChange: isChange, index: index, addressType: addressType); + @action void addBitcoinAddressTypes() { - final lastP2wpkh = _addresses + final lastP2wpkh = _allAddresses .where((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() @@ -382,7 +376,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2WPKH'; } - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { addressesMap[lastP2pkh.address] = 'P2PKH'; @@ -390,7 +384,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2PKH'; } - final lastP2sh = _addresses.firstWhere((addressRecord) => + final lastP2sh = _allAddresses.firstWhere((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); if (lastP2sh.address != address) { addressesMap[lastP2sh.address] = 'P2SH'; @@ -398,7 +392,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2SH'; } - final lastP2tr = _addresses.firstWhere( + final lastP2tr = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); if (lastP2tr.address != address) { addressesMap[lastP2tr.address] = 'P2TR'; @@ -406,7 +400,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2TR'; } - final lastP2wsh = _addresses.firstWhere( + final lastP2wsh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); if (lastP2wsh.address != address) { addressesMap[lastP2wsh.address] = 'P2WSH'; @@ -429,8 +423,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }); } + @action void addLitecoinAddressTypes() { - final lastP2wpkh = _addresses + final lastP2wpkh = _allAddresses .where((addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) .toList() @@ -441,7 +436,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = 'Active - P2WPKH'; } - final lastMweb = _addresses.firstWhere( + final lastMweb = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); if (lastMweb.address != address) { addressesMap[lastMweb.address] = 'MWEB'; @@ -450,8 +445,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } } + @action void addBitcoinCashAddressTypes() { - final lastP2pkh = _addresses.firstWhere( + final lastP2pkh = _allAddresses.firstWhere( (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); if (lastP2pkh.address != address) { addressesMap[lastP2pkh.address] = 'P2PKH'; @@ -461,13 +457,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @override + @action Future updateAddressesInBox() async { try { addressesMap.clear(); addressesMap[address] = 'Active'; allAddressesMap.clear(); - _addresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { allAddressesMap[addressRecord.address] = addressRecord.name; }); @@ -494,7 +491,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAddress(String address, String label) { BaseBitcoinAddressRecord? foundAddress; - _addresses.forEach((addressRecord) { + _allAddresses.forEach((addressRecord) { if (addressRecord.address == address) { foundAddress = addressRecord; } @@ -513,11 +510,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (foundAddress != null) { foundAddress!.setNewName(label); - if (foundAddress is BitcoinAddressRecord) { - final index = _addresses.indexOf(foundAddress); - _addresses.remove(foundAddress); - _addresses.insert(index, foundAddress as BitcoinAddressRecord); - } else { + if (foundAddress is! BitcoinAddressRecord) { final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); silentAddresses.remove(foundAddress); silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); @@ -534,88 +527,62 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } addressesByReceiveType.clear(); - addressesByReceiveType.addAll(_addresses.where(_isAddressPageTypeMatch).toList()); + addressesByReceiveType.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList()); } @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAddresses = - _addresses.where((addressRecord) => !addressRecord.isChange && !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) => + final newAddresses = _allAddresses.where((addressRecord) => addressRecord.isChange && - !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)); changeAddresses.addAll(newAddresses); } @action - Future> discoverAddresses( - List addressList, - bool isHidden, { - BitcoinAddressType type = SegwitAddresType.p2wpkh, + Future> discoverAddresses({ + required bool isChange, + required int gap, + required BitcoinAddressType type, }) async { - final newAddresses = await _createNewAddresses( - gap, - startIndex: addressList.length, - isHidden: isHidden, - type: type, - ); + print("_allAddresses: ${_allAddresses.length}"); + final newAddresses = await _createNewAddresses(gap, isChange: isChange, type: type); addAddresses(newAddresses); + print("_allAddresses: ${_allAddresses.length}"); return newAddresses; } - Future _generateInitialAddresses( - {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { - var countOfReceiveAddresses = 0; - var countOfHiddenAddresses = 0; - - _addresses.forEach((addr) { - if (addr.type == type) { - if (addr.isChange) { - countOfHiddenAddresses += 1; - return; - } - - countOfReceiveAddresses += 1; - } - }); - - if (countOfReceiveAddresses < defaultReceiveAddressesCount) { - final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfReceiveAddresses, isHidden: false, type: type); - addAddresses(newAddresses); - } - - if (countOfHiddenAddresses < defaultChangeAddressesCount) { - final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses(addressesCount, - startIndex: countOfHiddenAddresses, isHidden: true, type: type); - addAddresses(newAddresses); - } + @action + Future generateInitialAddresses({required BitcoinAddressType type}) async { + await discoverAddresses(isChange: false, gap: defaultReceiveAddressesCount, type: type); + await discoverAddresses(isChange: true, gap: defaultChangeAddressesCount, type: type); } - Future> _createNewAddresses(int count, - {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { + @action + Future> _createNewAddresses( + int count, { + bool isChange = false, + BitcoinAddressType? type, + }) async { final list = []; + final startIndex = isChange ? totalCountOfChangeAddresses : totalCountOfReceiveAddresses; for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( await getAddressAsync( - account: _getAccount(isHidden), - index: i, - hd: bip32, - addressType: type ?? addressPageType), + isChange: isChange, + index: i, + addressType: type ?? addressPageType, + ), index: i, - isChange: isHidden, + isChange: isChange, type: type ?? addressPageType, network: network, ); @@ -627,11 +594,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @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(); } @action @@ -653,7 +619,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } void _validateAddresses() { - _addresses.forEach((element) async { + _allAddresses.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; @@ -661,18 +627,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (!element.isChange && element.address != await getAddressAsync( - account: 0, + isChange: false, index: element.index, - hd: bip32, addressType: element.type, )) { element.isChange = true; } else if (element.isChange && element.address != await getAddressAsync( - account: 1, + isChange: true, index: element.index, - hd: bip32, addressType: element.type, )) { element.isChange = false; @@ -692,11 +656,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - int _getAccount(bool isHidden) => isHidden ? 1 : 0; bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; - bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => - !addr.isChange && !addr.isUsed && addr.type == type; + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + return !addr.isChange && !addr.isUsed && addr.type == type; + } @action void deleteSilentPaymentAddress(String address) { diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart index 62840933c0..c2f2aa22e8 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,13 @@ 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.fromBip32(bip32: bip32, 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 45c6d92853..7c581ab4eb 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'dart:math'; @@ -87,8 +86,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mempoolAPIEnabled: mempoolAPIEnabled, ) { if (seedBytes != null) { - mwebHd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath( - "m/1000'") as Bip32Slip10Secp256k1; + mwebHd = + Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; mwebEnabled = alwaysScan ?? false; } else { mwebHd = null; @@ -772,7 +771,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); // copy coin control attributes to mwebCoins: - await updateCoins(mwebUnspentCoins); + await updateCoins(mwebUnspentCoins.toSet()); // get regular ltc unspents (this resets unspentCoins): await super.updateAllUnspents(); // add the mwebCoins: @@ -810,11 +809,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future fetchBalances(List addresses) async { - final nonMwebAddresses = walletAddresses.allAddresses - .where((address) => RegexUtils.addressTypeFromStr(address.address, network) is! MwebAddress) - .toList(); - final balance = await super.fetchBalances(nonMwebAddresses); + Future fetchBalances() async { + final balance = await super.fetchBalances(); if (!mwebEnabled) { return balance; @@ -980,8 +976,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (!mwebEnabled) { tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: false)) + (await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(isPegIn: false)) .address; return tx; } @@ -1021,10 +1016,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { bool isPegIn = !hasMwebInput && hasMwebOutput; bool isRegular = !hasMwebInput && !hasMwebOutput; - tx.changeAddressOverride = - (await (walletAddresses as LitecoinWalletAddresses) - .getChangeAddress(isPegIn: isPegIn || isRegular)) - .address; + tx.changeAddressOverride = (await (walletAddresses as LitecoinWalletAddresses) + .getChangeAddress(isPegIn: isPegIn || isRegular)) + .address; if (!hasMwebInput && !hasMwebOutput) { tx.isMweb = false; return tx; @@ -1058,7 +1052,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); final key = ECPrivate.fromBip32( bip32: walletAddresses.bip32, - account: utxo.bitcoinAddressRecord.isChange ? 1 : 0, + account: BitcoinAddressUtils.getAccountFromChange(utxo.bitcoinAddressRecord.isChange), index: utxo.bitcoinAddressRecord.index, ); final digest = tx2.getTransactionSegwitDigit( @@ -1277,8 +1271,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override void setLedgerConnection(LedgerConnection connection) { _ledgerConnection = connection; - _litecoinLedgerApp = - LitecoinLedgerApp(_ledgerConnection!, derivationPath: walletInfo.derivationInfo!.derivationPath!); + _litecoinLedgerApp = LitecoinLedgerApp(_ledgerConnection!, + derivationPath: walletInfo.derivationInfo!.derivationPath!); } @override @@ -1314,19 +1308,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { if (maybeChangePath != null) changePath ??= maybeChangePath.derivationPath; } - final rawHex = await _litecoinLedgerApp!.createTransaction( - inputs: readyInputs, - outputs: outputs - .map((e) => TransactionOutput.fromBigInt( - (e as BitcoinOutput).value, Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) - .toList(), - changePath: changePath, - sigHashType: 0x01, - additionals: ["bech32"], - isSegWit: true, - useTrustedInputForSegwit: true - ); + inputs: readyInputs, + outputs: outputs + .map((e) => TransactionOutput.fromBigInt((e as BitcoinOutput).value, + Uint8List.fromList(e.address.toScriptPubKey().toBytes()))) + .toList(), + changePath: changePath, + sigHashType: 0x01, + additionals: ["bech32"], + isSegWit: true, + useTrustedInputForSegwit: true); return BtcTransaction.fromRaw(rawHex); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index b6e7b44288..7ff87bfd53 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -6,7 +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/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_mweb/cw_mweb.dart'; @@ -15,11 +14,9 @@ import 'package:mobx/mobx.dart'; part 'litecoin_wallet_addresses.g.dart'; -class LitecoinWalletAddresses = LitecoinWalletAddressesBase - with _$LitecoinWalletAddresses; +class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalletAddresses; -abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses - with Store { +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { required super.bip32, @@ -44,14 +41,13 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses List mwebAddrs = []; bool generating = false; - List get scanSecret => - mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendPubkey => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).publicKey.pubKey.compressed; @override Future init() async { - if (!isHardwareWallet) await initMwebAddresses(); + if (!super.isHardwareWallet) await initMwebAddresses(); await super.init(); } @@ -122,31 +118,29 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses @override BitcoinBaseAddress generateAddress({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) { if (addressType == SegwitAddresType.mweb) { return MwebAddress.fromAddress(address: mwebAddrs[0], network: network); } - return P2wpkhAddress.fromBip32(account: account, bip32: hd, index: index); + return P2wpkhAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); } } @override Future getAddressAsync({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) async { if (addressType == SegwitAddresType.mweb) { await ensureMwebAddressUpToIndexExists(index); } - return getAddress(account: account, index: index, hd: hd, addressType: addressType); + return getAddress(isChange: isChange, index: index, addressType: addressType); } @action diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 2d68a86ada..d13dcc8a4f 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -170,6 +170,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/pubspec.lock b/cw_bitcoin/pubspec.lock index 3ce7a37da6..ad03398e34 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -564,10 +564,10 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: ea3ed586e1697776dacf42ac979095f1ca3bd143bf007cbe5c78e09cb6943f42 + sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.4.1" ledger_litecoin: dependency: "direct main" description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 821f9b7f3c..94ae3e0466 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -28,10 +28,6 @@ dependencies: cryptography: ^2.0.5 blockchain_utils: path: /home/rafael/Working/blockchain_utils/ - ledger_flutter: ^1.0.1 - ledger_bitcoin: - git: - url: https://github.com/cake-tech/ledger-bitcoin cw_mweb: path: ../cw_mweb grpc: ^3.2.4 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 ae195bf6b6..704d1e843a 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -1,5 +1,4 @@ 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_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -22,10 +21,9 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi @override BitcoinBaseAddress generateAddress({ - required int account, + required bool isChange, required int index, - required Bip32Slip10Secp256k1 hd, required BitcoinAddressType addressType, }) => - P2pkhAddress.fromBip32(account: account, bip32: hd, index: index); + P2pkhAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); } 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 BitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: mnemonic, password: password, - derivationType: derivationType, - derivationPath: derivationPath, passphrase: passphrase, ); @@ -373,66 +369,71 @@ class CWBitcoin extends Bitcoin { } 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 ?? ''); - } - - for (DerivationInfo dInfo in electrum_derivations[dType]!) { - 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"; - } + try { + late List seedBytes; + if (dType == DerivationType.electrum) { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + } else if (dType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } - final hd = Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath(balancePath) - as Bip32Slip10Secp256k1; - - // derive address at index 0: - String? address; - switch (dInfoCopy.scriptType) { - case "p2wpkh": - address = P2wpkhAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); - break; - case "p2pkh": - address = P2pkhAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); - break; - case "p2wpkh-p2sh": - address = P2shAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); - break; - case "p2tr": - address = P2trAddress.fromBip32(bip32: hd, account: 0, index: 0).toAddress(network); - break; - default: - continue; + for (DerivationInfo dInfo in electrum_derivations[dType]!) { + 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 bip32 = Bip32Slip10Secp256k1.fromSeed(seedBytes); + final bip32BalancePath = Bip32PathParser.parse(balancePath); + + // derive address at index 0: + final path = bip32BalancePath.addElem(Bip32KeyIndex(0)); + String? address; + switch (dInfoCopy.scriptType) { + case "p2wpkh": + address = P2wpkhAddress.fromPath(bip32: bip32, path: path).toAddress(network); + break; + case "p2pkh": + address = P2pkhAddress.fromPath(bip32: bip32, path: path).toAddress(network); + break; + case "p2wpkh-p2sh": + address = P2shAddress.fromPath(bip32: bip32, path: path).toAddress(network); + break; + case "p2tr": + address = P2trAddress.fromPath(bip32: bip32, path: path).toAddress(network); + 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"); } - - 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"); } + } catch (e) { + print("seed error: $e"); } } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 6215e26c35..97a612d02b 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: @@ -379,38 +383,40 @@ class WalletRestorePage extends BasePage { walletRestoreViewModel.state = IsExecutingState(); - DerivationInfo? dInfo; + if (walletRestoreViewModel.type == WalletType.nano) { + DerivationInfo? dInfo; - // get info about the different derivations: - List derivations = - await walletRestoreViewModel.getDerivationInfo(_credentials()); + // 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; + 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; - } else { - // if we have multiple possible derivations, and none (or multiple) have histories - // we just default to the most common one: - dInfo = walletRestoreViewModel.getCommonRestoreDerivation(); - } + 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; + this.derivationInfo = dInfo; + } await walletRestoreViewModel.create(options: _credentials()); seedSettingsViewModel.setPassphrase(null); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index bc06fbcc4b..bcea80a540 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -602,6 +602,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; + // static final walletPasswordDirectInput = Platform.isLinux; static final walletPasswordDirectInput = false; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index cbdad85b88..0a2c04d7f6 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,11 @@ 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!, ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( @@ -144,8 +134,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 +179,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/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index d37b69f746..59623057da 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -105,8 +105,6 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, passphrase: passphrase, - derivationType: derivationInfo!.derivationType!, - derivationPath: derivationInfo.derivationPath!, ); case WalletType.haven: return haven!.createHavenRestoreWalletFromSeedCredentials( diff --git a/tool/configure.dart b/tool/configure.dart index 68408ee2e4..07e5231257 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -148,8 +148,6 @@ abstract class Bitcoin { required String name, required String mnemonic, required String password, - required DerivationType derivationType, - required String derivationPath, String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); From 433686bce3de78f436c75b44f31330ca6dcb89e8 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 30 Oct 2024 12:13:59 -0300 Subject: [PATCH 04/64] feat: derivationinfo to address records --- cw_bitcoin/lib/bitcoin_address_record.dart | 21 ++- .../lib/bitcoin_hardware_wallet_service.dart | 9 +- cw_bitcoin/lib/bitcoin_unspent.dart | 5 +- cw_bitcoin/lib/bitcoin_wallet.dart | 57 ++++++- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 29 +++- .../lib/electrum_transaction_history.dart | 3 - cw_bitcoin/lib/electrum_wallet.dart | 146 +++++------------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 115 +++++++------- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 3 +- .../lib/litecoin_hardware_wallet_service.dart | 7 +- cw_bitcoin/lib/litecoin_wallet.dart | 11 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 18 ++- cw_bitcoin/pubspec.lock | 20 +-- .../lib/src/bitcoin_cash_wallet.dart | 2 + .../src/bitcoin_cash_wallet_addresses.dart | 8 +- cw_core/lib/wallet_info.dart | 5 +- 16 files changed, 250 insertions(+), 209 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 43c1d5e14a..72ca4b23ee 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -6,7 +6,7 @@ abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( this.address, { required this.index, - this.isChange = false, + bool isChange = false, int txCount = 0, int balance = 0, String name = '', @@ -17,7 +17,8 @@ abstract class BaseBitcoinAddressRecord { _balance = balance, _name = name, _isUsed = isUsed, - _isHidden = isHidden ?? isChange; + _isHidden = isHidden ?? isChange, + _isChange = isChange; @override bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; @@ -25,7 +26,8 @@ abstract class BaseBitcoinAddressRecord { final String address; final bool _isHidden; bool get isHidden => _isHidden; - bool isChange; + final bool _isChange; + bool get isChange => _isChange; final int index; int _txCount; int _balance; @@ -55,9 +57,12 @@ abstract class BaseBitcoinAddressRecord { } class BitcoinAddressRecord extends BaseBitcoinAddressRecord { + final BitcoinDerivationInfo derivationInfo; + BitcoinAddressRecord( super.address, { required super.index, + required this.derivationInfo, super.isHidden, super.isChange = false, super.txCount = 0, @@ -81,6 +86,9 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, + derivationInfo: BitcoinDerivationInfo.fromJSON( + decoded['derivationInfo'] as Map, + ), isHidden: decoded['isHidden'] as bool? ?? false, isChange: decoded['isChange'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false, @@ -101,6 +109,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String toJSON() => json.encode({ 'address': address, 'index': index, + 'derivationInfo': derivationInfo.toJSON(), 'isHidden': isHidden, 'isChange': isChange, 'isUsed': isUsed, @@ -116,6 +125,8 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { int get labelIndex => index; final String? labelHex; + static bool isChangeAddress(int labelIndex) => labelIndex == 0; + BitcoinSilentPaymentAddressRecord( super.address, { required int labelIndex, @@ -126,9 +137,9 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { super.type = SilentPaymentsAddresType.p2sp, super.isHidden, this.labelHex, - }) : super(index: labelIndex, isChange: labelIndex == 0) { + }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) { if (labelIndex != 1 && labelHex == null) { - throw ArgumentError('label must be provided for silent address index > 0'); + throw ArgumentError('label must be provided for silent address index != 1'); } } diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index c63c1fe3a4..415ae0e987 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -24,9 +24,14 @@ class BitcoinHardwareWalletService { final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); final bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); + final fullPath = Bip32PathParser.parse(derivationPath).addElem(Bip32KeyIndex(0)); + + final address = ECPublic.fromBip32(bip32.derive(fullPath).publicKey) + .toP2wpkhAddress() + .toAddress(BitcoinNetwork.mainnet); + accounts.add(HardwareAccountData( - address: P2wpkhAddress.fromBip32(bip32: bip32, isChange: false, index: i) - .toAddress(BitcoinNetwork.mainnet), + address: address, accountIndex: i, derivationPath: derivationPath, masterFingerprint: masterFp, diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index b10eb47f68..6dd741b634 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -32,7 +32,10 @@ class BitcoinUnspent extends Unspent { @override bool operator ==(Object o) { - print('BitcoinUnspent operator =='); + if (identical(this, o)) return true; return o is BitcoinUnspent && hash == o.hash && vout == o.vout; } + + @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 6a0e3f4e7b..ec2384a08b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -360,7 +360,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); - unspentCoins = updatedUnspentCoins.toSet(); + unspentCoins.addAll(updatedUnspentCoins); if (unspentCoinsInfo.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); @@ -1033,3 +1033,58 @@ Future delegatedScan(ScanData scanData) async { // ); // } } + +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 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.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, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } +} + +class SyncResponse { + final int height; + final SyncStatus syncStatus; + + SyncResponse(this.height, this.syncStatus); +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index ab7a45d4f9..37a297b315 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -39,27 +39,44 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) { switch (addressType) { case P2pkhAddressType.p2pkh: - return P2pkhAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); + return P2pkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); case SegwitAddresType.p2tr: - return P2trAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); + return P2trAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); case SegwitAddresType.p2wsh: - return P2wshAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); + return P2wshAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); case P2shAddressType.p2wpkhInP2sh: - return P2shAddress.fromBip32( + return P2shAddress.fromDerivation( bip32: bip32, + derivationInfo: derivationInfo, isChange: isChange, index: index, type: P2shAddressType.p2wpkhInP2sh, ); case SegwitAddresType.p2wpkh: - return P2wpkhAddress.fromBip32( + return P2wpkhAddress.fromDerivation( bip32: bip32, + derivationInfo: derivationInfo, isChange: isChange, index: index, - isElectrum: false, // TODO: ); default: throw ArgumentError('Invalid address type'); 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_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 373b680855..1d0bcfa2dc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -38,7 +37,6 @@ 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:hex/hex.dart'; import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -69,7 +67,8 @@ abstract class ElectrumWalletBase _password = password, _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, - unspentCoins = {}, + // TODO: inital unspent coins + unspentCoins = ObservableSet(), scripthashesListening = {}, balance = ObservableMap.of(currency != null ? { @@ -221,8 +220,7 @@ abstract class ElectrumWalletBase ); String _password; - @observable - Set unspentCoins; + ObservableSet unspentCoins; @observable TransactionPriorities? feeRates; @@ -404,7 +402,7 @@ abstract class ElectrumWalletBase bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; - UtxoDetails _createUTXOS({ + TxCreateUtxoDetails _createUTXOS({ required bool sendAll, required int credentialsAmount, required bool paysToSilentPayment, @@ -484,13 +482,13 @@ abstract class ElectrumWalletBase .toHex(); } - // TODO: isElectrum - final derivationPath = BitcoinAddressUtils.getDerivationPath( - type: utx.bitcoinAddressRecord.type, - account: utx.bitcoinAddressRecord.isChange ? 1 : 0, - index: 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( @@ -521,7 +519,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoInputsException(); } - return UtxoDetails( + return TxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -662,11 +660,7 @@ abstract class ElectrumWalletBase isChange: true, )); - final changeDerivationPath = BitcoinAddressUtils.getDerivationPath( - type: changeAddress.type, - account: changeAddress.isChange ? 1 : 0, - index: changeAddress.index, - ); + final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); @@ -1176,7 +1170,7 @@ abstract class ElectrumWalletBase updatedUnspentCoins.addAll(await fetchUnspent(address)); })); - unspentCoins = updatedUnspentCoins.toSet(); + unspentCoins.addAll(updatedUnspentCoins); if (unspentCoinsInfo.length != updatedUnspentCoins.length) { unspentCoins.forEach((coin) => addCoinInfo(coin)); @@ -1220,17 +1214,7 @@ abstract class ElectrumWalletBase final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet(); await updateCoins(newUnspentCoins); - print([1, unspentCoins.containsAll(newUnspentCoins)]); - if (!unspentCoins.containsAll(newUnspentCoins)) { - newUnspentCoins.forEach((coin) { - print(unspentCoins.contains(coin)); - print([coin.vout, coin.hash]); - print([unspentCoins.first.vout, unspentCoins.first.hash]); - if (!unspentCoins.contains(coin)) { - unspentCoins.add(coin); - } - }); - } + unspentCoins.addAll(newUnspentCoins); // if (unspentCoinsInfo.length != unspentCoins.length) { // unspentCoins.forEach(addCoinInfo); @@ -1400,7 +1384,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'); @@ -1708,6 +1692,7 @@ abstract class ElectrumWalletBase isChange: addressRecord.isChange, gap: gapLimit, type: addressRecord.type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.type), ); } } @@ -1805,21 +1790,17 @@ abstract class ElectrumWalletBase @override Future signMessage(String message, {String? address = null}) async { - Bip32Slip10Secp256k1 HD = bip32; + final record = walletAddresses.getFromAddresses(address!); - final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address); + final path = Bip32PathParser.parse(walletInfo.derivationInfo!.derivationPath!) + .addElem( + Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(record.isChange)), + ) + .addElem(Bip32KeyIndex(record.index)); - 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 priv = ECPrivate.fromHex(bip32.derive(path).privateKey.toHex()); - String messagePrefix = '\x18Bitcoin Signed Message:\n'; - final hexEncoded = priv.signMessage(utf8.encode(message), messagePrefix: messagePrefix); + final hexEncoded = priv.signMessage(StringUtils.encode(message)); final decodedSig = hex.decode(hexEncoded); return base64Encode(decodedSig); } @@ -1835,7 +1816,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) { @@ -1845,7 +1826,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); @@ -1911,14 +1892,14 @@ abstract class ElectrumWalletBase break; case ConnectionStatus.disconnected: - // if (syncStatus is! NotConnectedSyncStatus) { - // syncStatus = NotConnectedSyncStatus(); - // } + if (syncStatus is! NotConnectedSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } break; case ConnectionStatus.failed: - // if (syncStatus is! LostConnectionSyncStatus) { - // syncStatus = LostConnectionSyncStatus(); - // } + if (syncStatus is! LostConnectionSyncStatus) { + syncStatus = LostConnectionSyncStatus(); + } break; case ConnectionStatus.connecting: if (syncStatus is! ConnectingSyncStatus) { @@ -1989,7 +1970,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:'); @@ -2005,61 +1986,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 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.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, - labels: scanData.labels, - labelIndexes: scanData.labelIndexes, - isSingleScan: scanData.isSingleScan, - ); - } -} - -class SyncResponse { - final int height; - final SyncStatus syncStatus; - - SyncResponse(this.height, this.syncStatus); -} - class EstimatedTxResult { EstimatedTxResult({ required this.utxos, @@ -2095,7 +2021,7 @@ class PublicKeyWithDerivationPath { final String publicKey; } -class UtxoDetails { +class TxCreateUtxoDetails { final List availableInputs; final List unconfirmedCoins; final List utxos; @@ -2106,7 +2032,7 @@ class UtxoDetails { final bool spendsSilentPayment; final bool spendsUnconfirmedTX; - UtxoDetails({ + TxCreateUtxoDetails({ required this.availableInputs, required this.unconfirmedCoins, required this.utxos, diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index c6600841bb..81ed23d28b 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { int initialSilentAddressIndex = 0, List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, - }) : _allAddresses = (initialAddresses ?? []).toSet(), + }) : _allAddresses = ObservableSet.of(initialAddresses ?? []), addressesByReceiveType = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of( @@ -63,11 +63,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { mwebAddresses = ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { - silentAddress = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate.fromHex(bip32.derive(SCAN_PATH).privateKey.toHex()), - b_spend: ECPrivate.fromHex(bip32.derive(SPEND_PATH).privateKey.toHex()), - network: network, - ); + // TODO: initial silent address, not every time + silentAddress = SilentPaymentOwner.fromBip32(bip32); + if (silentAddresses.length == 0) { silentAddresses.add(BitcoinSilentPaymentAddressRecord( silentAddress.toString(), @@ -91,8 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; - @observable - final Set _allAddresses; + final ObservableSet _allAddresses; final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -119,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed List get allAddresses => _allAddresses.toList(); + BitcoinAddressRecord getFromAddresses(String address) { + return _allAddresses.firstWhere((element) => element.address == address); + } + @override @computed String get address { @@ -189,7 +190,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @override - String get primaryAddress => getAddress(isChange: false, index: 0, addressType: addressPageType); + String get primaryAddress => _allAddresses.first.address; Map currentReceiveAddressIndexByType; @@ -250,7 +251,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); - _validateAddresses(); await updateAddressesInBox(); if (currentReceiveAddressIndex >= receiveAddresses.length) { @@ -263,15 +263,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress( - {List? inputs, List? outputs, bool isPegIn = false}) async { + Future getChangeAddress({ + List? inputs, + List? outputs, + bool isPegIn = false, + }) async { updateChangeAddresses(); - if (changeAddresses.isEmpty) { - final newAddresses = await _createNewAddresses(gap, isChange: true); - addAddresses(newAddresses); - } - if (currentChangeAddressIndex >= changeAddresses.length) { currentChangeAddressIndex = 0; } @@ -326,13 +324,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final newAddressIndex = addressesByReceiveType.fold( 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); + final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType); final address = BitcoinAddressRecord( - getAddress(isChange: false, index: newAddressIndex, addressType: addressPageType), + getAddress( + isChange: false, + index: newAddressIndex, + addressType: addressPageType, + derivationInfo: derivationInfo, + ), index: newAddressIndex, isChange: false, name: label, type: addressPageType, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), ); _allAddresses.add(address); Future.delayed(Duration.zero, () => updateAddressesByMatch()); @@ -343,6 +348,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) { throw UnimplementedError(); } @@ -351,17 +357,28 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) { - return generateAddress(isChange: isChange, index: index, addressType: addressType) - .toAddress(network); + return generateAddress( + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ).toAddress(network); } Future getAddressAsync({ required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) async => - getAddress(isChange: isChange, index: index, addressType: addressType); + getAddress( + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ); @action void addBitcoinAddressTypes() { @@ -551,23 +568,41 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required bool isChange, required int gap, required BitcoinAddressType type, + required BitcoinDerivationInfo derivationInfo, }) async { - print("_allAddresses: ${_allAddresses.length}"); - final newAddresses = await _createNewAddresses(gap, isChange: isChange, type: type); + final newAddresses = await _createNewAddresses( + gap, + isChange: isChange, + type: type, + derivationInfo: derivationInfo, + ); addAddresses(newAddresses); - print("_allAddresses: ${_allAddresses.length}"); return newAddresses; } @action Future generateInitialAddresses({required BitcoinAddressType type}) async { - await discoverAddresses(isChange: false, gap: defaultReceiveAddressesCount, type: type); - await discoverAddresses(isChange: true, gap: defaultChangeAddressesCount, type: type); + // TODO: try all other derivations + final derivationInfo = BitcoinAddressUtils.getDerivationFromType(type); + + await discoverAddresses( + isChange: false, + gap: defaultReceiveAddressesCount, + type: type, + derivationInfo: derivationInfo, + ); + await discoverAddresses( + isChange: true, + gap: defaultChangeAddressesCount, + type: type, + derivationInfo: derivationInfo, + ); } @action Future> _createNewAddresses( int count, { + required BitcoinDerivationInfo derivationInfo, bool isChange = false, BitcoinAddressType? type, }) async { @@ -580,11 +615,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { isChange: isChange, index: i, addressType: type ?? addressPageType, + derivationInfo: derivationInfo, ), index: i, isChange: isChange, type: type ?? addressPageType, network: network, + derivationInfo: derivationInfo, ); list.add(address); } @@ -618,32 +655,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } - void _validateAddresses() { - _allAddresses.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.isChange && - element.address != - await getAddressAsync( - isChange: false, - index: element.index, - addressType: element.type, - )) { - element.isChange = true; - } else if (element.isChange && - element.address != - await getAddressAsync( - isChange: true, - index: element.index, - addressType: element.type, - )) { - element.isChange = false; - } - }); - } - @action Future setAddressType(BitcoinAddressType type) async { _addressPageType = type; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index f7c2e1a28e..959618dcf8 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -3,7 +3,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_core/encryption_file_utils.dart'; -import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -92,7 +91,7 @@ class ElectrumWalletSnapshot { final derivationType = DerivationType .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; - final derivationPath = data['derivationPath'] as String? ?? electrum_path; + final derivationPath = data['derivationPath'] as String? ?? ELECTRUM_PATH; try { regularAddressIndexByType = { diff --git a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart index c2f2aa22e8..c53a8713d5 100644 --- a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -28,7 +28,12 @@ class LitecoinHardwareWalletService { final bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0)); - final address = P2wpkhAddress.fromBip32(bip32: bip32, isChange: false, index: 0); + final address = P2wpkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: BitcoinDerivationInfos.LITECOIN, + isChange: false, + index: 0, + ); accounts.add(HardwareAccountData( address: address.toAddress(LitecoinNetwork.mainnet), diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 7c581ab4eb..4ad64e0da8 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -21,7 +21,6 @@ import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cw_bitcoin/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'; @@ -243,7 +242,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; @@ -435,13 +434,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); diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 7ff87bfd53..72e19149b6 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -103,6 +103,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with index: e.key, type: SegwitAddresType.mweb, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), )) .toList(); addMwebAddresses(addressRecords); @@ -121,12 +122,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with 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.fromBip32(bip32: bip32, isChange: isChange, index: index); + return P2wpkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); } } @@ -135,12 +142,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) async { if (addressType == SegwitAddresType.mweb) { await ensureMwebAddressUpToIndexExists(index); } - return getAddress(isChange: isChange, index: index, addressType: addressType); + return getAddress( + isChange: isChange, + index: index, + addressType: addressType, + derivationInfo: derivationInfo, + ); } @action @@ -194,6 +207,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with index: 0, type: SegwitAddresType.mweb, network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), ); } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index ad03398e34..d02a50e3b3 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -411,10 +411,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: @@ -467,10 +467,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: @@ -845,10 +845,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: @@ -1041,18 +1041,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_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 9c4dba89b8..10a8a212fb 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -153,6 +153,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), ); } catch (_) { return BitcoinAddressRecord( @@ -161,6 +162,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { isChange: addr.isChange, type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), ); } }).toList(), 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 704d1e843a..34ba748fc4 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -24,6 +24,12 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required bool isChange, required int index, required BitcoinAddressType addressType, + required BitcoinDerivationInfo derivationInfo, }) => - P2pkhAddress.fromBip32(bip32: bip32, isChange: isChange, index: index); + P2pkhAddress.fromDerivation( + bip32: bip32, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index bd035e30a7..53a3930b04 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -189,16 +189,13 @@ class WalletInfo extends HiveObject { @HiveField(22) String? parentAddress; - + @HiveField(23) List? hiddenAddresses; @HiveField(24) List? manualAddresses; - - - String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { From f3a0ff700128e50e592b4754dfb0f77d85074a1a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 30 Oct 2024 19:16:00 -0300 Subject: [PATCH 05/64] feat: init electrum worker --- cw_bitcoin/lib/electrum_wallet.dart | 98 ++++++++++++---- cw_bitcoin/lib/electrum_worker.dart | 171 ++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker.dart diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 1d0bcfa2dc..1c43c03ace 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_worker.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -46,6 +48,11 @@ class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; abstract class ElectrumWalletBase extends WalletBase with Store, WalletKeysFile { + ReceivePort? receivePort; + SendPort? workerSendPort; + StreamSubscription? _workerSubscription; + Isolate? _workerIsolate; + ElectrumWalletBase({ required String password, required WalletInfo walletInfo, @@ -97,6 +104,45 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } + void _handleWorkerResponse(dynamic response) { + print('Main: worker response: $response'); + + final workerResponse = ElectrumWorkerResponse.fromJson( + jsonDecode(response.toString()) as Map, + ); + + if (workerResponse.error != null) { + // Handle error + print('Worker error: ${workerResponse.error}'); + return; + } + + switch (workerResponse.method) { + case 'connectionStatus': + final status = workerResponse.data as String; + final connectionStatus = ConnectionStatus.values.firstWhere( + (e) => e.toString() == status, + ); + _onConnectionStatusChange(connectionStatus); + break; + case 'fetchBalances': + final balance = ElectrumBalance.fromJSON( + jsonDecode(workerResponse.data.toString()).toString(), + ); + // Update the balance state + // this.balance[currency] = balance!; + break; + // Handle other responses... + } + } + + // Don't forget to clean up in the close method + // @override + // Future close({required bool shouldCleanup}) async { + // await _workerSubscription?.cancel(); + // await super.close(shouldCleanup: shouldCleanup); + // } + static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { @@ -234,7 +280,6 @@ abstract class ElectrumWalletBase void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; - StreamSubscription? _receiveStream; Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; @@ -256,13 +301,19 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); - await subscribeForHeaders(); - await subscribeForUpdates(); + // await subscribeForHeaders(); + // await subscribeForUpdates(); // await updateTransactions(); // await updateAllUnspents(); - await updateBalance(); - await updateFeeRates(); + // await updateBalance(); + // await updateFeeRates(); + workerSendPort?.send( + ElectrumWorkerMessage( + method: 'blockchain.scripthash.get_balance', + params: {'scriptHash': scriptHashes.first}, + ).toJson(), + ); _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); @@ -369,28 +420,34 @@ abstract class ElectrumWalletBase @action @override Future connectToNode({required Node node}) async { - scripthashesListening = {}; - _isTransactionUpdating = false; - _chainTipListenerOn = false; this.node = node; try { syncStatus = ConnectingSyncStatus(); - await _receiveStream?.cancel(); - rpc?.disconnect(); + if (_workerIsolate != null) { + _workerIsolate!.kill(priority: Isolate.immediate); + _workerSubscription?.cancel(); + receivePort?.close(); + } - // electrumClient.onConnectionStatusChange = _onConnectionStatusChange; + receivePort = ReceivePort(); - this.electrumClient2 = ElectrumApiProvider( - await ElectrumTCPService.connect( - node.uri, - onConnectionStatusChange: _onConnectionStatusChange, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), - ); - // await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); + + _workerSubscription = receivePort!.listen((message) { + if (message is SendPort) { + workerSendPort = message; + workerSendPort!.send( + ElectrumWorkerMessage( + method: 'connect', + params: {'uri': node.uri.toString()}, + ).toJson(), + ); + } else { + _handleWorkerResponse(message); + } + }); } catch (e, stacktrace) { print(stacktrace); print("connectToNode $e"); @@ -1146,7 +1203,6 @@ abstract class ElectrumWalletBase @override Future close({required bool shouldCleanup}) async { try { - await _receiveStream?.cancel(); await electrumClient.close(); } catch (_) {} _autoSaveTimer?.cancel(); diff --git a/cw_bitcoin/lib/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker.dart new file mode 100644 index 0000000000..553c4df6a9 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker.dart @@ -0,0 +1,171 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_balance.dart'; + +class ElectrumWorkerMessage { + final String method; + final Map params; + + ElectrumWorkerMessage({ + required this.method, + required this.params, + }); + + Map toJson() => { + 'method': method, + 'params': params, + }; + + factory ElectrumWorkerMessage.fromJson(Map json) { + return ElectrumWorkerMessage( + method: json['method'] as String, + params: json['params'] as Map, + ); + } +} + +class ElectrumWorkerResponse { + final String method; + final dynamic data; + final String? error; + + ElectrumWorkerResponse({ + required this.method, + required this.data, + this.error, + }); + + Map toJson() => { + 'method': method, + 'data': data, + 'error': error, + }; + + factory ElectrumWorkerResponse.fromJson(Map json) { + return ElectrumWorkerResponse( + method: json['method'] as String, + data: json['data'], + error: json['error'] as String?, + ); + } +} + +class ElectrumWorker { + final SendPort sendPort; + ElectrumApiProvider? _electrumClient; + + 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); + } + + Future _handleConnect({ + required Uri uri, + }) async { + _electrumClient = ElectrumApiProvider( + await ElectrumTCPService.connect( + uri, + onConnectionStatusChange: (status) { + _sendResponse('connectionStatus', status.toString()); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), + ); + } + + void handleMessage(dynamic message) async { + try { + final workerMessage = ElectrumWorkerMessage.fromJson(message as Map); + + switch (workerMessage.method) { + case 'connect': + final uri = Uri.parse(workerMessage.params['uri'] as String); + await _handleConnect(uri: uri); + break; + case 'blockchain.scripthash.get_balance': + await _handleGetBalance(workerMessage); + break; + case 'blockchain.scripthash.get_history': + // await _handleGetHistory(workerMessage); + break; + case 'blockchain.scripthash.listunspent': + // await _handleListUnspent(workerMessage); + break; + // Add other method handlers here + default: + _sendError(workerMessage.method, 'Unsupported method: ${workerMessage.method}'); + } + } catch (e, s) { + print(s); + _sendError('unknown', e.toString()); + } + } + + void _sendResponse(String method, dynamic data) { + final response = ElectrumWorkerResponse( + method: method, + data: data, + ); + sendPort.send(jsonEncode(response.toJson())); + } + + void _sendError(String method, String error) { + final response = ElectrumWorkerResponse( + method: method, + data: null, + error: error, + ); + sendPort.send(jsonEncode(response.toJson())); + } + + Future _handleGetBalance(ElectrumWorkerMessage message) async { + try { + final scriptHash = message.params['scriptHash'] as String; + final result = await _electrumClient!.request( + ElectrumGetScriptHashBalance(scriptHash: scriptHash), + ); + + final balance = ElectrumBalance( + confirmed: result['confirmed'] as int? ?? 0, + unconfirmed: result['unconfirmed'] as int? ?? 0, + frozen: 0, + ); + + _sendResponse(message.method, balance.toJSON()); + } catch (e, s) { + print(s); + _sendError(message.method, e.toString()); + } + } + + // Future _handleGetHistory(ElectrumWorkerMessage message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await electrumClient.getHistory(scriptHash); + // _sendResponse(message.method, jsonEncode(result)); + // } catch (e) { + // _sendError(message.method, e.toString()); + // } + // } + + // Future _handleListUnspent(ElectrumWorkerMessage message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await electrumClient.listUnspent(scriptHash); + // _sendResponse(message.method, jsonEncode(result)); + // } catch (e) { + // _sendError(message.method, e.toString()); + // } + // } +} From 02fabf8594deb219ec5940e15bdcdafce977bbe2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 31 Oct 2024 11:39:02 -0300 Subject: [PATCH 06/64] feat: electrum worker types --- cw_bitcoin/lib/electrum_wallet.dart | 120 ++++++------ cw_bitcoin/lib/electrum_worker.dart | 175 +++++++----------- .../lib/electrum_worker/electrum_worker.dart | 171 +++++++++++++++++ .../electrum_worker_methods.dart | 15 ++ .../electrum_worker_params.dart | 45 +++++ .../electrum_worker/methods/connection.dart | 50 +++++ .../methods/headers_subscribe.dart | 44 +++++ .../lib/electrum_worker/methods/methods.dart | 6 + .../methods/scripthashes_subscribe.dart | 48 +++++ 9 files changed, 510 insertions(+), 164 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/electrum_worker.dart create mode 100644 cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart create mode 100644 cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/connection.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/methods.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 1c43c03ace..e95416b63f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -4,7 +4,9 @@ import 'dart:io'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/electrum_worker.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/methods/methods.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.dart'; @@ -104,35 +106,55 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } - void _handleWorkerResponse(dynamic response) { - print('Main: worker response: $response'); - - final workerResponse = ElectrumWorkerResponse.fromJson( - jsonDecode(response.toString()) as Map, - ); + @action + void _handleWorkerResponse(dynamic message) { + print('Main: received message: $message'); - if (workerResponse.error != null) { - // Handle error - print('Worker error: ${workerResponse.error}'); - return; + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; } + final workerMethod = messageJson['method'] as String; + + // if (workerResponse.error != null) { + // print('Worker error: ${workerResponse.error}'); + + // switch (workerResponse.method) { + // // case 'connectionStatus': + // // final status = ConnectionStatus.values.firstWhere( + // // (e) => e.toString() == workerResponse.error, + // // ); + // // _onConnectionStatusChange(status); + // // break; + // // case 'fetchBalances': + // // // Update the balance state + // // // this.balance[currency] = balance!; + // // break; + // case 'blockchain.headers.subscribe': + // _chainTipListenerOn = false; + // break; + // } + // return; + // } - switch (workerResponse.method) { - case 'connectionStatus': - final status = workerResponse.data as String; - final connectionStatus = ConnectionStatus.values.firstWhere( - (e) => e.toString() == status, - ); - _onConnectionStatusChange(connectionStatus); + switch (workerMethod) { + case ElectrumWorkerMethods.connectionMethod: + final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); + _onConnectionStatusChange(response.result); break; - case 'fetchBalances': - final balance = ElectrumBalance.fromJSON( - jsonDecode(workerResponse.data.toString()).toString(), - ); - // Update the balance state - // this.balance[currency] = balance!; + case ElectrumRequestMethods.headersSubscribeMethod: + final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); + onHeadersResponse(response.result); break; - // Handle other responses... + // case 'fetchBalances': + // final balance = ElectrumBalance.fromJSON( + // jsonDecode(workerResponse.data.toString()).toString(), + // ); + // Update the balance state + // this.balance[currency] = balance!; + // break; } } @@ -301,19 +323,13 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); - // await subscribeForHeaders(); - // await subscribeForUpdates(); + await subscribeForHeaders(); + await subscribeForUpdates(); // await updateTransactions(); // await updateAllUnspents(); // await updateBalance(); // await updateFeeRates(); - workerSendPort?.send( - ElectrumWorkerMessage( - method: 'blockchain.scripthash.get_balance', - params: {'scriptHash': scriptHashes.first}, - ).toJson(), - ); _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); @@ -439,10 +455,7 @@ abstract class ElectrumWalletBase if (message is SendPort) { workerSendPort = message; workerSendPort!.send( - ElectrumWorkerMessage( - method: 'connect', - params: {'uri': node.uri.toString()}, - ).toJson(), + ElectrumWorkerConnectionRequest(uri: node.uri).toJson(), ); } else { _handleWorkerResponse(message); @@ -1790,25 +1803,17 @@ abstract class ElectrumWalletBase (address) => !scripthashesListening.contains(address.scriptHash), ); - await Future.wait(unsubscribedScriptHashes.map((addressRecord) async { - final scripthash = addressRecord.scriptHash; - final listener = await electrumClient2!.subscribe( - ElectrumScriptHashSubscribe(scriptHash: scripthash), - ); - - if (listener != null) { - scripthashesListening.add(scripthash); - - // https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status - // The status of the script hash is the hash of the tx history, or null if the string is empty because there are no transactions - listener((status) async { - print("status: $status"); + Map scripthashByAddress = {}; + List scriptHashesList = []; + walletAddresses.allAddresses.forEach((addressRecord) { + scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; + scriptHashesList.add(addressRecord.scriptHash); + }); - await _fetchAddressHistory(addressRecord); - await updateUnspentsForAddress(addressRecord); - }); - } - })); + workerSendPort!.send( + ElectrumWorkerScripthashesSubscribeRequest(scripthashByAddress: scripthashByAddress).toJson(), + ); + scripthashesListening.addAll(scriptHashesList); } @action @@ -1928,11 +1933,8 @@ abstract class ElectrumWalletBase Future subscribeForHeaders() async { if (_chainTipListenerOn) return; - final listener = electrumClient2!.subscribe(ElectrumHeaderSubscribe()); - if (listener == null) return; - + workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); _chainTipListenerOn = true; - listener(onHeadersResponse); } @action diff --git a/cw_bitcoin/lib/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker.dart index 553c4df6a9..c28fe91abe 100644 --- a/cw_bitcoin/lib/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker.dart @@ -3,55 +3,9 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; - -class ElectrumWorkerMessage { - final String method; - final Map params; - - ElectrumWorkerMessage({ - required this.method, - required this.params, - }); - - Map toJson() => { - 'method': method, - 'params': params, - }; - - factory ElectrumWorkerMessage.fromJson(Map json) { - return ElectrumWorkerMessage( - method: json['method'] as String, - params: json['params'] as Map, - ); - } -} - -class ElectrumWorkerResponse { - final String method; - final dynamic data; - final String? error; - - ElectrumWorkerResponse({ - required this.method, - required this.data, - this.error, - }); - - Map toJson() => { - 'method': method, - 'data': data, - 'error': error, - }; - - factory ElectrumWorkerResponse.fromJson(Map json) { - return ElectrumWorkerResponse( - method: json['method'] as String, - data: json['data'], - error: json['error'] as String?, - ); - } -} +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +// import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; class ElectrumWorker { final SendPort sendPort; @@ -69,33 +23,36 @@ class ElectrumWorker { receivePort.listen(worker.handleMessage); } - Future _handleConnect({ - required Uri uri, - }) async { - _electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( - uri, - onConnectionStatusChange: (status) { - _sendResponse('connectionStatus', status.toString()); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), - ); + 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 { - final workerMessage = ElectrumWorkerMessage.fromJson(message as Map); + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + final workerMethod = messageJson['method'] as String; - switch (workerMessage.method) { - case 'connect': - final uri = Uri.parse(workerMessage.params['uri'] as String); - await _handleConnect(uri: uri); - break; - case 'blockchain.scripthash.get_balance': - await _handleGetBalance(workerMessage); + switch (workerMethod) { + case ElectrumWorkerMethods.connectionMethod: + await _handleConnect(ElectrumWorkerConnectRequest.fromJson(messageJson)); break; + // case 'blockchain.headers.subscribe': + // await _handleHeadersSubscribe(); + // break; + // case 'blockchain.scripthash.get_balance': + // await _handleGetBalance(message); + // break; case 'blockchain.scripthash.get_history': // await _handleGetHistory(workerMessage); break; @@ -103,51 +60,59 @@ class ElectrumWorker { // await _handleListUnspent(workerMessage); break; // Add other method handlers here - default: - _sendError(workerMessage.method, 'Unsupported method: ${workerMessage.method}'); + // default: + // _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}'); } } catch (e, s) { print(s); - _sendError('unknown', e.toString()); + _sendError(ElectrumWorkerErrorResponse(error: e.toString())); } } - void _sendResponse(String method, dynamic data) { - final response = ElectrumWorkerResponse( - method: method, - data: data, + Future _handleConnect(ElectrumWorkerConnectRequest request) async { + _electrumClient = ElectrumApiProvider( + await ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse(ElectrumWorkerConnectResponse(status: status.toString())); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), ); - sendPort.send(jsonEncode(response.toJson())); } - void _sendError(String method, String error) { - final response = ElectrumWorkerResponse( - method: method, - data: null, - error: error, - ); - sendPort.send(jsonEncode(response.toJson())); - } + // Future _handleHeadersSubscribe() async { + // final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); + // if (listener == null) { + // _sendError('blockchain.headers.subscribe', 'Failed to subscribe'); + // return; + // } - Future _handleGetBalance(ElectrumWorkerMessage message) async { - try { - final scriptHash = message.params['scriptHash'] as String; - final result = await _electrumClient!.request( - ElectrumGetScriptHashBalance(scriptHash: scriptHash), - ); - - final balance = ElectrumBalance( - confirmed: result['confirmed'] as int? ?? 0, - unconfirmed: result['unconfirmed'] as int? ?? 0, - frozen: 0, - ); - - _sendResponse(message.method, balance.toJSON()); - } catch (e, s) { - print(s); - _sendError(message.method, e.toString()); - } - } + // listener((event) { + // _sendResponse('blockchain.headers.subscribe', event); + // }); + // } + + // Future _handleGetBalance(ElectrumWorkerRequest message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scriptHash), + // ); + + // final balance = ElectrumBalance( + // confirmed: result['confirmed'] as int? ?? 0, + // unconfirmed: result['unconfirmed'] as int? ?? 0, + // frozen: 0, + // ); + + // _sendResponse(message.method, balance.toJSON()); + // } catch (e, s) { + // print(s); + // _sendError(message.method, e.toString()); + // } + // } // Future _handleGetHistory(ElectrumWorkerMessage message) async { // try { 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..26385bff08 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -0,0 +1,171 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +// import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; + +class ElectrumWorker { + final SendPort sendPort; + ElectrumApiProvider? _electrumClient; + + 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 ElectrumRequestMethods.headersSubscribeMethod: + await _handleHeadersSubscribe(); + break; + case ElectrumRequestMethods.scripthashesSubscribeMethod: + await _handleScriphashesSubscribe( + ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson), + ); + break; + // case 'blockchain.scripthash.get_balance': + // await _handleGetBalance(message); + // break; + case 'blockchain.scripthash.get_history': + // await _handleGetHistory(workerMessage); + break; + case 'blockchain.scripthash.listunspent': + // await _handleListUnspent(workerMessage); + break; + // Add other method handlers here + // default: + // _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}'); + } + } catch (e, s) { + print(s); + _sendError(ElectrumWorkerErrorResponse(error: e.toString())); + } + } + + Future _handleConnect(ElectrumWorkerConnectionRequest request) async { + _electrumClient = ElectrumApiProvider( + await ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse(ElectrumWorkerConnectionResponse(status: status)); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), + ); + } + + Future _handleHeadersSubscribe() async { + final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); + if (listener == null) { + _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); + return; + } + + listener((event) { + _sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event)); + }); + } + + Future _handleScriphashesSubscribe( + ElectrumWorkerScripthashesSubscribeRequest request, + ) async { + await Future.wait(request.scripthashByAddress.entries.map((entry) async { + final address = entry.key; + final scripthash = entry.value; + final listener = await _electrumClient!.subscribe( + ElectrumScriptHashSubscribe(scriptHash: scripthash), + ); + + if (listener == null) { + _sendError(ElectrumWorkerScripthashesSubscribeError(error: 'Failed to subscribe')); + return; + } + + // https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status + // The status of the script hash is the hash of the tx history, or null if the string is empty because there are no transactions + listener((status) async { + print("status: $status"); + + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: status}, + )); + }); + })); + } + + // Future _handleGetBalance(ElectrumWorkerRequest message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scriptHash), + // ); + + // final balance = ElectrumBalance( + // confirmed: result['confirmed'] as int? ?? 0, + // unconfirmed: result['unconfirmed'] as int? ?? 0, + // frozen: 0, + // ); + + // _sendResponse(message.method, balance.toJSON()); + // } catch (e, s) { + // print(s); + // _sendError(message.method, e.toString()); + // } + // } + + // Future _handleGetHistory(ElectrumWorkerMessage message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await electrumClient.getHistory(scriptHash); + // _sendResponse(message.method, jsonEncode(result)); + // } catch (e) { + // _sendError(message.method, e.toString()); + // } + // } + + // Future _handleListUnspent(ElectrumWorkerMessage message) async { + // try { + // final scriptHash = message.params['scriptHash'] as String; + // final result = await electrumClient.listUnspent(scriptHash); + // _sendResponse(message.method, jsonEncode(result)); + // } catch (e) { + // _sendError(message.method, e.toString()); + // } + // } +} 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..c171e2cae1 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -0,0 +1,15 @@ +class ElectrumWorkerMethods { + const ElectrumWorkerMethods._(this.method); + final String method; + + static const String connectionMethod = "connection"; + static const String unknownMethod = "unknown"; + + static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); + static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); + + @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..f666eed1df --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -0,0 +1,45 @@ +// import 'dart:convert'; + +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; + +abstract class ElectrumWorkerRequest { + abstract final String method; + + Map toJson(); + ElectrumWorkerRequest.fromJson(Map json); +} + +class ElectrumWorkerResponse { + ElectrumWorkerResponse({required this.method, required this.result, this.error}); + + final String method; + final RESULT result; + final String? error; + + RESPONSE resultJson(RESULT result) { + throw UnimplementedError(); + } + + factory ElectrumWorkerResponse.fromJson(Map json) { + throw UnimplementedError(); + } + + Map toJson() { + return {'method': method, 'result': resultJson(result), 'error': error}; + } +} + +class ElectrumWorkerErrorResponse { + ElectrumWorkerErrorResponse({required this.error}); + + String get method => ElectrumWorkerMethods.unknown.method; + final String error; + + factory ElectrumWorkerErrorResponse.fromJson(Map json) { + return ElectrumWorkerErrorResponse(error: json['error'] as String); + } + + Map toJson() { + return {'method': method, 'error': error}; + } +} 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..1abbcb81e4 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -0,0 +1,50 @@ +part of 'methods.dart'; + +class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { + ElectrumWorkerConnectionRequest({required this.uri}); + + final Uri uri; + + @override + final String method = ElectrumWorkerMethods.connect.method; + + @override + factory ElectrumWorkerConnectionRequest.fromJson(Map json) { + return ElectrumWorkerConnectionRequest(uri: Uri.parse(json['params'] as String)); + } + + @override + Map toJson() { + return {'method': method, 'params': uri.toString()}; + } +} + +class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse { + ElectrumWorkerConnectionError({required String error}) : super(error: error); + + @override + String get method => ElectrumWorkerMethods.connect.method; +} + +class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse { + ElectrumWorkerConnectionResponse({required ConnectionStatus status, super.error}) + : 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?, + ); + } +} 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..619f32aedc --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart @@ -0,0 +1,44 @@ +part of 'methods.dart'; + +class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerHeadersSubscribeRequest(); + + @override + final String method = ElectrumRequestMethods.headersSubscribe.method; + + @override + factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerHeadersSubscribeRequest(); + } + + @override + Map toJson() { + return {'method': method}; + } +} + +class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error); + + @override + final String method = ElectrumRequestMethods.headersSubscribe.method; +} + +class ElectrumWorkerHeadersSubscribeResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error}) + : 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?, + ); + } +} 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..32247c2f2c --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -0,0 +1,6 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +part 'connection.dart'; +part 'headers_subscribe.dart'; +part 'scripthashes_subscribe.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..35a73ef49a --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -0,0 +1,48 @@ +part of 'methods.dart'; + +class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress}); + + final Map scripthashByAddress; + + @override + final String method = ElectrumRequestMethods.scriptHashSubscribe.method; + + @override + factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: json['scripthashes'] as Map, + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashByAddress}; + } +} + +class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error); + + @override + final String method = ElectrumRequestMethods.scriptHashSubscribe.method; +} + +class ElectrumWorkerScripthashesSubscribeResponse + extends ElectrumWorkerResponse?, Map?> { + ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error}) + : 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?, + ); + } +} From 4a4250a905c039cf60d863b9df30b8d1fca85b09 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 1 Nov 2024 17:31:26 -0300 Subject: [PATCH 07/64] feat: tx history worker --- cw_bitcoin/lib/bitcoin_address_record.dart | 29 +- cw_bitcoin/lib/bitcoin_wallet.dart | 187 +++--- cw_bitcoin/lib/bitcoin_wallet_service.dart | 6 - cw_bitcoin/lib/electrum_wallet.dart | 555 +++++++----------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 15 +- cw_bitcoin/lib/electrum_worker.dart | 136 ----- .../lib/electrum_worker/electrum_worker.dart | 256 ++++++-- .../electrum_worker/methods/get_balance.dart | 52 ++ .../electrum_worker/methods/get_history.dart | 110 ++++ .../methods/list_unspents.dart | 53 ++ .../lib/electrum_worker/methods/methods.dart | 7 + cw_bitcoin/lib/litecoin_wallet.dart | 211 +++---- lib/bitcoin/cw_bitcoin.dart | 4 +- 13 files changed, 922 insertions(+), 699 deletions(-) delete mode 100644 cw_bitcoin/lib/electrum_worker.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_balance.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_history.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 72ca4b23ee..c90e7d65d6 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -24,7 +24,7 @@ abstract class BaseBitcoinAddressRecord { bool operator ==(Object o) => o is BaseBitcoinAddressRecord && address == o.address; final String address; - final bool _isHidden; + bool _isHidden; bool get isHidden => _isHidden; final bool _isChange; bool get isChange => _isChange; @@ -46,7 +46,12 @@ abstract class BaseBitcoinAddressRecord { bool get isUsed => _isUsed; - void setAsUsed() => _isUsed = true; + void setAsUsed() { + _isUsed = true; + // TODO: check is hidden flow on addr list + _isHidden = true; + } + void setNewName(String label) => _name = label; int get hashCode => address.hashCode; @@ -119,6 +124,26 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 'type': type.toString(), 'scriptHash': scriptHash, }); + + @override + operator ==(Object other) { + if (identical(this, other)) return true; + + return other is BitcoinAddressRecord && + other.address == address && + other.index == index && + other.derivationInfo == derivationInfo && + other.scriptHash == scriptHash && + other.type == type; + } + + @override + int get hashCode => + address.hashCode ^ + index.hashCode ^ + derivationInfo.hashCode ^ + scriptHash.hashCode ^ + type.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ec2384a08b..3ad83b54f9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -11,7 +11,7 @@ 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_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'; @@ -240,6 +240,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } + Future getNodeSupportsSilentPayments() async { + return true; + // As of today (august 2024), only ElectrumRS supports silent payments + // if (!(await getNodeIsElectrs())) { + // return false; + // } + + // if (node == null) { + // return false; + // } + + // try { + // final tweaksResponse = await electrumClient.getTweaks(height: 0); + + // if (tweaksResponse != null) { + // node!.supportsSilentPayments = true; + // node!.save(); + // return node!.supportsSilentPayments!; + // } + // } on RequestFailedTimeoutException catch (_) { + // node!.supportsSilentPayments = false; + // node!.save(); + // return node!.supportsSilentPayments!; + // } catch (_) {} + + // node!.supportsSilentPayments = false; + // node!.save(); + // return node!.supportsSilentPayments!; + } + LedgerConnection? _ledgerConnection; BitcoinLedgerApp? _bitcoinLedgerApp; @@ -327,11 +357,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { _isolate?.then((value) => value.kill(priority: Isolate.immediate)); - if (rpc!.isConnected) { - syncStatus = SyncedSyncStatus(); - } else { - syncStatus = NotConnectedSyncStatus(); - } + // if (rpc!.isConnected) { + // syncStatus = SyncedSyncStatus(); + // } else { + // syncStatus = NotConnectedSyncStatus(); + // } } } @@ -367,7 +397,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return; } - await updateCoins(unspentCoins); + await updateCoins(unspentCoins.toSet()); await refreshUnspentCoinsInfo(); } @@ -449,6 +479,20 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // } // } + @action + Future registerSilentPaymentsKey() async { + final registered = await electrumClient.tweaksRegister( + secViewKey: walletAddresses.silentAddress!.b_scan.toHex(), + pubSpendKey: walletAddresses.silentAddress!.B_spend.toHex(), + labels: walletAddresses.silentAddresses + .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + .map((addr) => addr.labelIndex) + .toList(), + ); + + print("registered: $registered"); + } + @action void _updateSilentAddressRecord(BitcoinUnspent unspent) { final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; @@ -593,41 +637,42 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override @action Future> fetchTransactions() async { - 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 {}; - } + 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() async { + Future updateTransactions([List? addresses]) async { super.updateTransactions(); transactionHistory.transactions.values.forEach((tx) { @@ -641,32 +686,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }); } - @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; - } - }); - } - }); + // @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, - ); - } + // return ElectrumBalance( + // confirmed: totalConfirmed, + // unconfirmed: balance.unconfirmed, + // frozen: totalFrozen, + // ); + // } @override @action @@ -713,15 +758,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - @override - @action - void onHeadersResponse(ElectrumHeaderResponse response) { - super.onHeadersResponse(response); + // @override + // @action + // void onHeadersResponse(ElectrumHeaderResponse response) { + // super.onHeadersResponse(response); - if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - _setListeners(walletInfo.restoreHeight); - } - } + // if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + // _setListeners(walletInfo.restoreHeight); + // } + // } @override @action diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index a384523293..941c252650 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,7 +13,6 @@ 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, @@ -172,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; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e95416b63f..a7745c2054 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.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'; +// import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -35,13 +35,13 @@ 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/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:http/http.dart' as http; +// import 'package:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -77,8 +77,8 @@ abstract class ElectrumWalletBase _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, // TODO: inital unspent coins - unspentCoins = ObservableSet(), - scripthashesListening = {}, + unspentCoins = BitcoinUnspentCoins(), + scripthashesListening = [], balance = ObservableMap.of(currency != null ? { currency: initialBalance ?? @@ -107,7 +107,7 @@ abstract class ElectrumWalletBase } @action - void _handleWorkerResponse(dynamic message) { + Future _handleWorkerResponse(dynamic message) async { print('Main: received message: $message'); Map messageJson; @@ -146,15 +146,17 @@ abstract class ElectrumWalletBase break; case ElectrumRequestMethods.headersSubscribeMethod: final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); - onHeadersResponse(response.result); + 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 'fetchBalances': - // final balance = ElectrumBalance.fromJSON( - // jsonDecode(workerResponse.data.toString()).toString(), - // ); - // Update the balance state - // this.balance[currency] = balance!; - // break; } } @@ -219,8 +221,6 @@ abstract class ElectrumWalletBase bool isEnabledAutoGenerateSubaddress; late ElectrumClient electrumClient; - ElectrumApiProvider? electrumClient2; - BitcoinBaseElectrumRPCService? get rpc => electrumClient2?.rpc; ApiProvider? apiProvider; Box unspentCoinsInfo; @@ -235,10 +235,10 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - Set get addressesSet => walletAddresses.allAddresses + List get addressesSet => walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .map((addr) => addr.address) - .toSet(); + .toList(); List get scriptHashes => walletAddresses.addressesByReceiveType .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) @@ -288,14 +288,14 @@ abstract class ElectrumWalletBase ); String _password; - ObservableSet unspentCoins; + BitcoinUnspentCoins unspentCoins; @observable TransactionPriorities? feeRates; int feeRate(TransactionPriority priority) => feeRates![priority]; @observable - Set scripthashesListening; + List scripthashesListening; bool _chainTipListenerOn = false; bool _isTransactionUpdating; @@ -323,16 +323,22 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); + // INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero await subscribeForHeaders(); - await subscribeForUpdates(); - // await updateTransactions(); + // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later. + await updateTransactions(); + // await updateAllUnspents(); - // await updateBalance(); + // INFO: THIRD: Start loading the TX history + await updateBalance(); + + // await subscribeForUpdates(); + // await updateFeeRates(); - _updateFeeRateTimer ??= - Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); + // _updateFeeRateTimer ??= + // Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); syncStatus = SyncedSyncStatus(); @@ -344,20 +350,6 @@ abstract class ElectrumWalletBase } } - @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 callError(FlutterErrorDetails error) { _onError?.call(error); @@ -366,9 +358,9 @@ abstract class ElectrumWalletBase @action Future updateFeeRates() async { try { - feeRates = BitcoinElectrumTransactionPriorities.fromList( - await electrumClient2!.getFeeRates(), - ); + // feeRates = BitcoinElectrumTransactionPriorities.fromList( + // await electrumClient2!.getFeeRates(), + // ); } catch (e, stacktrace) { // _onError?.call(FlutterErrorDetails( // exception: e, @@ -403,36 +395,6 @@ abstract class ElectrumWalletBase return node!.isElectrs!; } - Future getNodeSupportsSilentPayments() async { - return true; - // As of today (august 2024), only ElectrumRS supports silent payments - if (!(await getNodeIsElectrs())) { - return false; - } - - if (node == null) { - return false; - } - - try { - final tweaksResponse = await electrumClient.getTweaks(height: 0); - - if (tweaksResponse != null) { - node!.supportsSilentPayments = true; - node!.save(); - return node!.supportsSilentPayments!; - } - } on RequestFailedTimeoutException catch (_) { - node!.supportsSilentPayments = false; - node!.save(); - return node!.supportsSilentPayments!; - } catch (_) {} - - node!.supportsSilentPayments = false; - node!.save(); - return node!.supportsSilentPayments!; - } - @action @override Future connectToNode({required Node node}) async { @@ -1176,7 +1138,7 @@ abstract class ElectrumWalletBase final path = await makePath(); await encryptionFileUtils.write(path: path, password: _password, data: toJSON()); - // await transactionHistory.save(); + await transactionHistory.save(); } @override @@ -1226,28 +1188,23 @@ abstract class ElectrumWalletBase Future updateAllUnspents() async { List updatedUnspentCoins = []; - // 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; + Set scripthashes = {}; + walletAddresses.allAddresses.forEach((addressRecord) { + scripthashes.add(addressRecord.scriptHash); }); + workerSendPort!.send( + ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(), + ); + await Future.wait(walletAddresses.allAddresses .where((element) => element.type != SegwitAddresType.mweb) .map((address) async { updatedUnspentCoins.addAll(await fetchUnspent(address)); })); - unspentCoins.addAll(updatedUnspentCoins); - - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; - } - - await updateCoins(unspentCoins); - // await refreshUnspentCoinsInfo(); + await updateCoins(unspentCoins.toSet()); + await refreshUnspentCoinsInfo(); } @action @@ -1294,18 +1251,17 @@ abstract class ElectrumWalletBase @action Future> fetchUnspent(BitcoinAddressRecord address) async { + List> unspents = []; List updatedUnspentCoins = []; - final unspents = await electrumClient2!.request( - ElectrumScriptHashListUnspent(scriptHash: address.scriptHash), - ); + unspents = await electrumClient.getListUnspent(address.scriptHash); await Future.wait(unspents.map((unspent) async { try { - final coin = BitcoinUnspent.fromUTXO(address, unspent); - final tx = await fetchTransactionInfo(hash: coin.hash); - coin.isChange = address.isChange; - coin.confirmations = tx?.confirmations; + final coin = BitcoinUnspent.fromJSON(address, unspent); + // final tx = await fetchTransactionInfo(hash: coin.hash); + coin.isChange = address.isHidden; + // coin.confirmations = tx?.confirmations; updatedUnspentCoins.add(coin); } catch (_) {} @@ -1332,6 +1288,7 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.add(newInfo); } + // TODO: ? Future refreshUnspentCoinsInfo() async { try { final List keys = []; @@ -1415,7 +1372,7 @@ abstract class ElectrumWalletBase final vout = input.txIndex; final outTransaction = inputTransaction.outputs[vout]; final address = addressFromOutputScript(outTransaction.scriptPubKey, network); - allInputsAmount += outTransaction.amount.toInt(); + // allInputsAmount += outTransaction.amount.toInt(); final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); @@ -1565,72 +1522,15 @@ abstract class ElectrumWalletBase Future getTransactionExpanded({required String hash}) async { int? time; int? height; - - final transactionHex = await electrumClient2!.request( - ElectrumGetTransactionHex(transactionHash: hash), - ); - - // TODO: - // if (mempoolAPIEnabled) { - if (true) { - try { - final txVerbose = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", - ), - ); - - if (txVerbose.statusCode == 200 && - txVerbose.body.isNotEmpty && - jsonDecode(txVerbose.body) != null) { - height = jsonDecode(txVerbose.body)['block_height'] as int; - - final blockHash = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", - ), - ); - - if (blockHash.statusCode == 200 && - blockHash.body.isNotEmpty && - jsonDecode(blockHash.body) != null) { - final blockResponse = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", - ), - ); - - if (blockResponse.statusCode == 200 && - blockResponse.body.isNotEmpty && - jsonDecode(blockResponse.body)['timestamp'] != null) { - time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); - } - } - } - } catch (_) {} - } + final transactionHex = await electrumClient.getTransactionHex(hash: hash); int? confirmations; - if (height != null) { - if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); - } - - final tip = currentChainTip!; - if (tip > 0 && height > 0) { - // Add one because the block itself is the first confirmation - confirmations = tip - height + 1; - } - } - final original = BtcTransaction.fromRaw(transactionHex); final ins = []; for (final vin in original.inputs) { - final inputTransactionHex = await electrumClient2!.request( - ElectrumGetTransactionHex(transactionHash: vin.txId), - ); + final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } @@ -1643,207 +1543,62 @@ abstract class ElectrumWalletBase ); } - Future fetchTransactionInfo({required String hash, int? height}) async { - try { - return ElectrumTransactionInfo.fromElectrumBundle( - await getTransactionExpanded(hash: hash), - walletInfo.type, - network, - addresses: addressesSet, - height: height, - ); - } catch (e, s) { - print([e, s]); - return null; - } - } - @override @action Future> fetchTransactions() async { - try { - final Map historiesWithDetails = {}; - - if (type == WalletType.bitcoinCash) { - await Future.wait(BITCOIN_CASH_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - } else if (type == WalletType.litecoin) { - await Future.wait(LITECOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - } - - return historiesWithDetails; - } catch (e) { - print("fetchTransactions $e"); - return {}; - } - } - - Future fetchTransactionsForAddressType( - Map historiesWithDetails, - BitcoinAddressType type, - ) async { - final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); - await Future.wait(addressesByType.map((addressRecord) async { - final history = await _fetchAddressHistory(addressRecord); - - if (history.isNotEmpty) { - historiesWithDetails.addAll(history); - } - })); - } - - Future> _fetchAddressHistory( - BitcoinAddressRecord addressRecord, - ) async { - String txid = ""; - - try { - final Map historiesWithDetails = {}; - - final history = await electrumClient2!.request(ElectrumScriptHashGetHistory( - scriptHash: addressRecord.scriptHash, - )); - - if (history.isNotEmpty) { - addressRecord.setAsUsed(); - addressRecord.txCount = history.length; - - 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 ((currentChainTip ?? 0) > 0) { - storedTx.confirmations = currentChainTip! - height + 1; - } - storedTx.isPending = storedTx.confirmations == 0; - } - - historiesWithDetails[txid] = storedTx; - } else { - final tx = await fetchTransactionInfo(hash: txid, height: height); - - if (tx != null) { - historiesWithDetails[txid] = tx; - - // Got a new transaction fetched, add it to the transaction history - // instead of waiting all to finish, and next time it will be faster - transactionHistory.addOne(tx); - } - } - - return Future.value(null); - })); - - final totalAddresses = (addressRecord.isChange - ? walletAddresses.changeAddresses - .where((addr) => addr.type == addressRecord.type) - .length - : walletAddresses.receiveAddresses - .where((addr) => addr.type == addressRecord.type) - .length); - final gapLimit = (addressRecord.isChange - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - - final isUsedAddressUnderGap = addressRecord.index < totalAddresses && - (addressRecord.index >= totalAddresses - gapLimit); - - if (isUsedAddressUnderGap) { - // Discover new addresses for the same address type until the gap limit is respected - await walletAddresses.discoverAddresses( - isChange: addressRecord.isChange, - gap: gapLimit, - type: addressRecord.type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.type), - ); - } - } - - return historiesWithDetails; - } catch (e, stacktrace) { - _onError?.call(FlutterErrorDetails( - exception: "$txid - $e", - stack: stacktrace, - library: this.runtimeType.toString(), - )); - return {}; - } + throw UnimplementedError(); } @action - Future updateTransactions() async { - try { - if (_isTransactionUpdating) { - return; - } + Future updateTransactions([List? addresses]) async { + // TODO: all + addresses ??= walletAddresses.allAddresses + .where( + (element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false, + ) + .toList(); - _isTransactionUpdating = true; - await fetchTransactions(); - walletAddresses.updateReceiveAddresses(); - _isTransactionUpdating = false; - } catch (e, stacktrace) { - print(stacktrace); - print(e); - _isTransactionUpdating = false; - } + workerSendPort!.send( + ElectrumWorkerGetHistoryRequest( + addresses: addresses, + storedTxs: transactionHistory.transactions.values.toList(), + walletType: type, + // If we still don't have currentChainTip, txs will still be fetched but shown + // with confirmations as 0 but will be auto fixed on onHeadersResponse + chainTip: currentChainTip ?? 0, + network: network, + // mempoolAPIEnabled: mempoolAPIEnabled, + // TODO: + mempoolAPIEnabled: true, + ).toJson(), + ); } @action - Future subscribeForUpdates([ - Iterable? unsubscribedScriptHashes, - ]) async { - unsubscribedScriptHashes ??= walletAddresses.allAddresses.where( - (address) => !scripthashesListening.contains(address.scriptHash), + Future subscribeForUpdates([Iterable? unsubscribedScriptHashes]) async { + unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where( + (sh) => !scripthashesListening.contains(sh), ); Map scripthashByAddress = {}; - List scriptHashesList = []; walletAddresses.allAddresses.forEach((addressRecord) { scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; - scriptHashesList.add(addressRecord.scriptHash); }); workerSendPort!.send( - ElectrumWorkerScripthashesSubscribeRequest(scripthashByAddress: scripthashByAddress).toJson(), + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + ).toJson(), ); - scripthashesListening.addAll(scriptHashesList); - } - - @action - Future fetchBalances() async { - var totalFrozen = 0; - var totalConfirmed = 0; - var totalUnconfirmed = 0; - - unspentCoins.forEach((element) { - if (element.isFrozen) { - totalFrozen += element.value; - } - - if (element.confirmations == 0) { - totalUnconfirmed += element.value; - } else { - totalConfirmed += element.value; - } - }); - return ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: totalFrozen, - ); + scripthashesListening.addAll(scripthashByAddress.values); } @action Future updateBalance() async { - balance[currency] = await fetchBalances(); + workerSendPort!.send( + ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(), + ); } @override @@ -1925,12 +1680,102 @@ abstract class ElectrumWalletBase } @action - void onHeadersResponse(ElectrumHeaderResponse response) { + Future onHistoriesResponse(List histories) async { + final firstAddress = histories.first; + final isChange = firstAddress.addressRecord.isChange; + final type = firstAddress.addressRecord.type; + + final totalAddresses = histories.length; + final gapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + bool hasUsedAddressesUnderGap = false; + + final addressesWithHistory = []; + + for (final addressHistory in histories) { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + final address = addressHistory.addressRecord; + addressesWithHistory.add(address); + + hasUsedAddressesUnderGap = + address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } + } + } + + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } + + if (hasUsedAddressesUnderGap) { + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverAddresses( + isChange: isChange, + gap: gapLimit, + type: type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), + ); + + if (newAddresses.isNotEmpty) { + // Update the transactions for the new discovered addresses + await updateTransactions(newAddresses); + } + } + } + + @action + void onBalanceResponse(ElectrumBalance balanceResult) { + var totalFrozen = 0; + var totalConfirmed = balanceResult.confirmed; + var totalUnconfirmed = balanceResult.unconfirmed; + + unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { + if (unspentCoinInfo.isFrozen) { + // TODO: verify this works well + totalFrozen += unspentCoinInfo.value; + totalConfirmed -= unspentCoinInfo.value; + totalUnconfirmed -= unspentCoinInfo.value; + } + }); + + balance[currency] = ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, + ); + } + + @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 { + print(_chainTipListenerOn); if (_chainTipListenerOn) return; workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); @@ -1970,12 +1815,17 @@ abstract class ElectrumWalletBase @action void syncStatusReaction(SyncStatus syncStatus) { - if (syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus) { + final isDisconnectedStatus = + syncStatus is NotConnectedSyncStatus || syncStatus is LostConnectionSyncStatus; + + if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) { // Needs to re-subscribe to all scripthashes when reconnected - scripthashesListening = {}; + scripthashesListening = []; _isTransactionUpdating = false; _chainTipListenerOn = false; + } + if (isDisconnectedStatus) { if (_isTryingToConnect) return; _isTryingToConnect = true; @@ -1985,10 +1835,7 @@ abstract class ElectrumWalletBase this.syncStatus is LostConnectionSyncStatus) { if (node == null) return; - this.electrumClient.connectToUri( - node!.uri, - useSSL: node!.useSSL ?? false, - ); + connectToNode(node: this.node!); } _isTryingToConnect = false; }); @@ -2102,3 +1949,35 @@ class TxCreateUtxoDetails { required this.spendsUnconfirmedTX, }); } + +class BitcoinUnspentCoins extends ObservableList { + BitcoinUnspentCoins() : super(); + + List forInfo(Iterable unspentCoinsInfo) { + return unspentCoinsInfo.where((element) { + final info = this.firstWhereOrNull( + (info) => + element.hash == info.hash && + element.vout == info.vout && + element.address == info.bitcoinAddressRecord.address && + element.value == info.value, + ); + + return info != null; + }).toList(); + } + + List fromInfo(Iterable unspentCoinsInfo) { + return this.where((element) { + final info = unspentCoinsInfo.firstWhereOrNull( + (info) => + element.hash == info.hash && + element.vout == info.vout && + element.bitcoinAddressRecord.address == info.address && + element.value == info.value, + ); + + return info != null; + }).toList(); + } +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 81ed23d28b..468947c150 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -43,7 +43,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { int initialSilentAddressIndex = 0, List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, - }) : _allAddresses = ObservableSet.of(initialAddresses ?? []), + }) : _allAddresses = ObservableList.of(initialAddresses ?? []), addressesByReceiveType = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of( @@ -89,7 +89,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; - final ObservableSet _allAddresses; + final ObservableList _allAddresses; final ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -116,6 +116,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed List get allAddresses => _allAddresses.toList(); + @computed + Set get allScriptHashes => + _allAddresses.map((addressRecord) => addressRecord.scriptHash).toSet(); + BitcoinAddressRecord getFromAddresses(String address) { return _allAddresses.firstWhere((element) => element.address == address); } @@ -629,6 +633,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return list; } + @action + void updateAdresses(Iterable addresses) { + for (final address in addresses) { + _allAddresses.replaceRange(address.index, address.index + 1, [address]); + } + } + @action void addAddresses(Iterable addresses) { this._allAddresses.addAll(addresses); diff --git a/cw_bitcoin/lib/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker.dart deleted file mode 100644 index c28fe91abe..0000000000 --- a/cw_bitcoin/lib/electrum_worker.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:isolate'; - -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; -// import 'package:cw_bitcoin/electrum_balance.dart'; -import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; - -class ElectrumWorker { - final SendPort sendPort; - ElectrumApiProvider? _electrumClient; - - 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(ElectrumWorkerConnectRequest.fromJson(messageJson)); - break; - // case 'blockchain.headers.subscribe': - // await _handleHeadersSubscribe(); - // break; - // case 'blockchain.scripthash.get_balance': - // await _handleGetBalance(message); - // break; - case 'blockchain.scripthash.get_history': - // await _handleGetHistory(workerMessage); - break; - case 'blockchain.scripthash.listunspent': - // await _handleListUnspent(workerMessage); - break; - // Add other method handlers here - // default: - // _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}'); - } - } catch (e, s) { - print(s); - _sendError(ElectrumWorkerErrorResponse(error: e.toString())); - } - } - - Future _handleConnect(ElectrumWorkerConnectRequest request) async { - _electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectResponse(status: status.toString())); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), - ); - } - - // Future _handleHeadersSubscribe() async { - // final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); - // if (listener == null) { - // _sendError('blockchain.headers.subscribe', 'Failed to subscribe'); - // return; - // } - - // listener((event) { - // _sendResponse('blockchain.headers.subscribe', event); - // }); - // } - - // Future _handleGetBalance(ElectrumWorkerRequest message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await _electrumClient!.request( - // ElectrumGetScriptHashBalance(scriptHash: scriptHash), - // ); - - // final balance = ElectrumBalance( - // confirmed: result['confirmed'] as int? ?? 0, - // unconfirmed: result['unconfirmed'] as int? ?? 0, - // frozen: 0, - // ); - - // _sendResponse(message.method, balance.toJSON()); - // } catch (e, s) { - // print(s); - // _sendError(message.method, e.toString()); - // } - // } - - // Future _handleGetHistory(ElectrumWorkerMessage message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await electrumClient.getHistory(scriptHash); - // _sendResponse(message.method, jsonEncode(result)); - // } catch (e) { - // _sendError(message.method, e.toString()); - // } - // } - - // Future _handleListUnspent(ElectrumWorkerMessage message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await electrumClient.listUnspent(scriptHash); - // _sendResponse(message.method, jsonEncode(result)); - // } catch (e) { - // _sendError(message.method, e.toString()); - // } - // } -} diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 26385bff08..8b372bd3f3 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -3,10 +3,16 @@ import 'dart:convert'; import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.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_balance.dart'; import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; +import 'package:http/http.dart' as http; + +// TODO: ping class ElectrumWorker { final SendPort sendPort; @@ -58,11 +64,15 @@ class ElectrumWorker { ElectrumWorkerScripthashesSubscribeRequest.fromJson(messageJson), ); break; - // case 'blockchain.scripthash.get_balance': - // await _handleGetBalance(message); - // break; - case 'blockchain.scripthash.get_history': - // await _handleGetHistory(workerMessage); + case ElectrumRequestMethods.getBalanceMethod: + await _handleGetBalance( + ElectrumWorkerGetBalanceRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.getHistoryMethod: + await _handleGetHistory( + ElectrumWorkerGetHistoryRequest.fromJson(messageJson), + ); break; case 'blockchain.scripthash.listunspent': // await _handleListUnspent(workerMessage); @@ -108,6 +118,7 @@ class ElectrumWorker { await Future.wait(request.scripthashByAddress.entries.map((entry) async { final address = entry.key; final scripthash = entry.value; + final listener = await _electrumClient!.subscribe( ElectrumScriptHashSubscribe(scriptHash: scripthash), ); @@ -129,43 +140,214 @@ class ElectrumWorker { })); } - // Future _handleGetBalance(ElectrumWorkerRequest message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await _electrumClient!.request( - // ElectrumGetScriptHashBalance(scriptHash: scriptHash), - // ); + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async { + final Map histories = {}; + final addresses = result.addresses; - // final balance = ElectrumBalance( - // confirmed: result['confirmed'] as int? ?? 0, - // unconfirmed: result['unconfirmed'] as int? ?? 0, - // frozen: 0, - // ); + await Future.wait(addresses.map((addressRecord) async { + final history = await _electrumClient!.request(ElectrumScriptHashGetHistory( + scriptHash: addressRecord.scriptHash, + )); - // _sendResponse(message.method, balance.toJSON()); - // } catch (e, s) { - // print(s); - // _sendError(message.method, e.toString()); - // } - // } + if (history.isNotEmpty) { + addressRecord.setAsUsed(); + addressRecord.txCount = history.length; + + await Future.wait(history.map((transaction) async { + final txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + late ElectrumTransactionInfo tx; + + try { + // Exception thrown on null + tx = result.storedTxs.firstWhere((tx) => tx.id == txid); + + if (height > 0) { + tx.height = height; + + // the tx's block itself is the first confirmation so add 1 + tx.confirmations = result.chainTip - height + 1; + tx.isPending = tx.confirmations == 0; + } + } catch (_) { + tx = ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded( + hash: txid, + currentChainTip: result.chainTip, + mempoolAPIEnabled: result.mempoolAPIEnabled, + ), + result.walletType, + result.network, + addresses: result.addresses.map((addr) => addr.address).toSet(), + height: height, + ); + } + + final addressHistories = histories[addressRecord.address]; + if (addressHistories != null) { + addressHistories.txs.add(tx); + } else { + histories[addressRecord.address] = AddressHistoriesResponse( + addressRecord: addressRecord, + txs: [tx], + walletType: result.walletType, + ); + } + + return Future.value(null); + })); + } + + return histories; + })); + + _sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList())); + } + + Future getTransactionExpanded({ + required String hash, + required int currentChainTip, + required bool mempoolAPIEnabled, + bool getConfirmations = true, + }) async { + int? time; + int? height; + int? confirmations; + + final transactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); + + if (getConfirmations) { + if (mempoolAPIEnabled) { + try { + final txVerbose = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", + ), + ); + + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + ), + ); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + ), + ); + + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } + } catch (_) {} + } - // Future _handleGetHistory(ElectrumWorkerMessage message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await electrumClient.getHistory(scriptHash); - // _sendResponse(message.method, jsonEncode(result)); - // } catch (e) { - // _sendError(message.method, e.toString()); + if (height != null) { + if (time == null && height > 0) { + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + } + + final tip = currentChainTip; + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; + } + } + } + + final original = BtcTransaction.fromRaw(transactionHex); + final ins = []; + + for (final vin in original.inputs) { + final inputTransactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: vin.txId), + ); + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } + + return ElectrumTransactionBundle( + original, + ins: ins, + time: time, + confirmations: confirmations ?? 0, + ); + } + + // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { + // final balanceFutures = >>[]; + + // for (final scripthash in request.scripthashes) { + // final balanceFuture = _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scripthash), + // ); + // balanceFutures.add(balanceFuture); // } - // } - // Future _handleListUnspent(ElectrumWorkerMessage message) async { - // try { - // final scriptHash = message.params['scriptHash'] as String; - // final result = await electrumClient.listUnspent(scriptHash); - // _sendResponse(message.method, jsonEncode(result)); - // } catch (e) { - // _sendError(message.method, e.toString()); + // 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, + ), + )); + } } 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..fc79967e1d --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -0,0 +1,52 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + + final Set scripthashes; + + @override + final String method = ElectrumRequestMethods.getBalance.method; + + @override + factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { + return ElectrumWorkerGetBalanceRequest( + scripthashes: (json['scripthashes'] as List).toSet(), + ); + } + + @override + Map toJson() { + return {'method': method, 'scripthashes': scripthashes.toList()}; + } +} + +class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetBalanceError({required String error}) : super(error: error); + + @override + final String method = ElectrumRequestMethods.getBalance.method; +} + +class ElectrumWorkerGetBalanceResponse + extends ElectrumWorkerResponse?> { + ElectrumWorkerGetBalanceResponse({required super.result, super.error}) + : 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?, + ); + } +} 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..584f4b6d11 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -0,0 +1,110 @@ +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, + }); + + final List addresses; + final List storedTxs; + final WalletType walletType; + final int chainTip; + final BasedUtxoNetwork network; + final bool mempoolAPIEnabled; + + @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, + ); + } + + @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 String error}) : super(error: error); + + @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(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?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart new file mode 100644 index 0000000000..c3a626a0b0 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart @@ -0,0 +1,53 @@ +// part of 'methods.dart'; + +// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { +// ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + +// final Set scripthashes; + +// @override +// final String method = ElectrumRequestMethods.getBalance.method; + +// @override +// factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { +// return ElectrumWorkerGetBalanceRequest( +// scripthashes: (json['scripthashes'] as List).toSet(), +// ); +// } + +// @override +// Map toJson() { +// return {'method': method, 'scripthashes': scripthashes.toList()}; +// } +// } + +// class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { +// ElectrumWorkerGetBalanceError({required String error}) : super(error: error); + +// @override +// final String method = ElectrumRequestMethods.getBalance.method; +// } + +// class ElectrumWorkerGetBalanceResponse +// extends ElectrumWorkerResponse?> { +// ElectrumWorkerGetBalanceResponse({required super.result, super.error}) +// : 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?, +// ); +// } +// } + diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 32247c2f2c..31b82bf9e2 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -1,6 +1,13 @@ +import 'dart:convert'; 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'; part 'connection.dart'; part 'headers_subscribe.dart'; part 'scripthashes_subscribe.dart'; +part 'get_balance.dart'; +part 'get_history.dart'; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 4ad64e0da8..716ec0ca52 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -774,113 +774,114 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action Future> fetchTransactions() async { - try { - final Map historiesWithDetails = {}; - - await Future.wait(LITECOIN_ADDRESS_TYPES - .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - - return historiesWithDetails; - } catch (e) { - print("fetchTransactions $e"); - return {}; - } - } - - @override - @action - Future subscribeForUpdates([ - Iterable? unsubscribedScriptHashes, - ]) async { - final unsubscribedScriptHashes = walletAddresses.allAddresses.where( - (address) => - !scripthashesListening.contains(address.scriptHash) && - address.type != SegwitAddresType.mweb, - ); - - return super.subscribeForUpdates(unsubscribedScriptHashes); + throw UnimplementedError(); + // try { + // final Map historiesWithDetails = {}; + + // await Future.wait(LITECOIN_ADDRESS_TYPES + // .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + + // return historiesWithDetails; + // } catch (e) { + // print("fetchTransactions $e"); + // return {}; + // } } - @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 + // @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) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 70d2930418..f6bea44838 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -603,7 +603,7 @@ class CWBitcoin extends Bitcoin { @override Future registerSilentPaymentsKey(Object wallet, bool active) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return await bitcoinWallet.registerSilentPaymentsKey(); } @@ -634,7 +634,7 @@ class CWBitcoin extends Bitcoin { @override Future getNodeIsElectrsSPEnabled(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return bitcoinWallet.getNodeSupportsSilentPayments(); } From a3e131d3691dbb91adf1cc35981c9186baebaa62 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 4 Nov 2024 19:29:25 -0300 Subject: [PATCH 08/64] feat: all address derivations --- cw_bitcoin/lib/bitcoin_address_record.dart | 5 + cw_bitcoin/lib/bitcoin_wallet.dart | 105 +++++++++++++++--- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 38 ++++++- .../bitcoin_wallet_creation_credentials.dart | 1 + cw_bitcoin/lib/electrum_wallet.dart | 39 ++++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 68 +++++++++--- cw_bitcoin/lib/litecoin_wallet.dart | 8 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 23 ++-- .../lib/src/bitcoin_cash_wallet.dart | 9 +- .../src/bitcoin_cash_wallet_addresses.dart | 3 +- cw_core/lib/wallet_credentials.dart | 2 + cw_core/lib/wallet_info.dart | 6 + lib/bitcoin/cw_bitcoin.dart | 105 +++++------------- .../screens/restore/wallet_restore_page.dart | 12 +- .../restore/restore_from_qr_vm.dart | 1 + lib/view_model/wallet_creation_vm.dart | 30 ++++- lib/view_model/wallet_restore_view_model.dart | 29 ++++- tool/configure.dart | 3 +- 18 files changed, 330 insertions(+), 157 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index c90e7d65d6..a15364e6c3 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( @@ -63,11 +64,13 @@ abstract class BaseBitcoinAddressRecord { class BitcoinAddressRecord extends BaseBitcoinAddressRecord { final BitcoinDerivationInfo derivationInfo; + final CWBitcoinDerivationType derivationType; BitcoinAddressRecord( super.address, { required super.index, required this.derivationInfo, + required this.derivationType, super.isHidden, super.isChange = false, super.txCount = 0, @@ -94,6 +97,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 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, @@ -115,6 +119,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 'address': address, 'index': index, 'derivationInfo': derivationInfo.toJSON(), + 'derivationType': derivationType.index, 'isHidden': isHidden, 'isChange': isChange, 'isUsed': isUsed, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 3ad83b54f9..8555fdab89 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -2,16 +2,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; -import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/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_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'; @@ -60,6 +58,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, bool? alwaysScan, required bool mempoolAPIEnabled, + super.hdWallets, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -88,9 +87,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, initialSilentAddresses: initialSilentAddresses, initialSilentAddressIndex: initialSilentAddressIndex, - bip32: bip32, network: networkParam ?? network, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { @@ -116,15 +115,49 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required bool mempoolAPIEnabled, }) async { late List seedBytes; + final Map hdWallets = {}; - switch (walletInfo.derivationInfo?.derivationType) { - case DerivationType.bip39: + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; + + try { + hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( + seedBytes, + ElectrumWalletBase.getKeyNetVersion(network ?? BitcoinNetwork.mainnet), + ).derivePath( + _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), + ) as Bip32Slip10Secp256k1; + } catch (e) { + print("bip39 seed error: $e"); + } break; - case DerivationType.electrum: - default: - seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, 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"); + } + } + + try { + hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( + seedBytes, + ).derivePath( + _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), + ) as Bip32Slip10Secp256k1; + } catch (_) {} break; + } } return BitcoinWallet( @@ -144,6 +177,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: addressPageType, networkParam: network, mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: hdWallets, ); } @@ -200,21 +234,52 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; List? seedBytes = null; + final Map hdWallets = {}; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; if (mnemonic != null) { - switch (walletInfo.derivationInfo!.derivationType) { - case DerivationType.electrum: - seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; + + try { + hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( + seedBytes, + ElectrumWalletBase.getKeyNetVersion(network), + ).derivePath( + _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), + ) as Bip32Slip10Secp256k1; + } catch (e) { + print("bip39 seed error: $e"); + } 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"); + } + } + + try { + hdWallets[CWBitcoinDerivationType.old] = + Bip32Slip10Secp256k1.fromSeed(seedBytes!).derivePath( + _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), + ) as Bip32Slip10Secp256k1; + } catch (_) {} break; + } } } @@ -237,6 +302,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { networkParam: network, alwaysScan: alwaysScan, mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: hdWallets, ); } @@ -784,6 +850,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.syncStatusReaction(syncStatus); } } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); } Future startRefresh(ScanData scanData) async { diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 37a297b315..c5419a6f0f 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,4 +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_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -10,9 +11,9 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { - required super.bip32, required super.network, required super.isHardwareWallet, + required super.hdWallets, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, @@ -36,36 +37,61 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) { + final hdWallet = hdWallets[derivationType]!; + + if (derivationType == CWBitcoinDerivationType.old) { + final pub = hdWallet + .childKey(Bip32KeyIndex(isChange ? 1 : 0)) + .childKey(Bip32KeyIndex(index)) + .publicKey; + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return ECPublic.fromBip32(pub).toP2pkhAddress(); + case SegwitAddresType.p2tr: + return ECPublic.fromBip32(pub).toP2trAddress(); + case SegwitAddresType.p2wsh: + return ECPublic.fromBip32(pub).toP2wshAddress(); + case P2shAddressType.p2wpkhInP2sh: + return ECPublic.fromBip32(pub).toP2wpkhInP2sh(); + case SegwitAddresType.p2wpkh: + return ECPublic.fromBip32(pub).toP2wpkhAddress(); + default: + throw ArgumentError('Invalid address type'); + } + } + switch (addressType) { case P2pkhAddressType.p2pkh: return P2pkhAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, ); case SegwitAddresType.p2tr: return P2trAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, ); case SegwitAddresType.p2wsh: return P2wshAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, ); case P2shAddressType.p2wpkhInP2sh: return P2shAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, @@ -73,7 +99,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S ); case SegwitAddresType.p2wpkh: return P2wpkhAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index cd615ad2b4..bab72b6251 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -26,6 +26,7 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { required String name, required String password, required this.mnemonic, + required super.derivations, WalletInfo? walletInfo, String? passphrase, }) : super( diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a7745c2054..7986b2cb6e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -61,6 +61,7 @@ abstract class ElectrumWalletBase required Box unspentCoinsInfo, required this.network, required this.encryptionFileUtils, + Map? hdWallets, String? xpub, String? mnemonic, List? seedBytes, @@ -71,7 +72,16 @@ abstract class ElectrumWalletBase CryptoCurrency? currency, this.alwaysScan, required this.mempoolAPIEnabled, - }) : bip32 = getAccountHDWallet(currency, network, seedBytes, xpub, walletInfo.derivationInfo), + }) : hdWallets = hdWallets ?? + { + CWBitcoinDerivationType.bip39: getAccountHDWallet( + currency, + network, + seedBytes, + xpub, + walletInfo.derivationInfo, + ) + }, syncStatus = NotConnectedSyncStatus(), _password = password, _isTransactionUpdating = false, @@ -175,24 +185,12 @@ abstract class ElectrumWalletBase } if (seedBytes != null) { - switch (currency) { - case CryptoCurrency.btc: - case CryptoCurrency.ltc: - case CryptoCurrency.tbtc: - return Bip32Slip10Secp256k1.fromSeed(seedBytes); - case CryptoCurrency.bch: - return bitcoinCashHDWallet(seedBytes); - default: - throw Exception("Unsupported currency"); - } + return Bip32Slip10Secp256k1.fromSeed(seedBytes); } return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } - static Bip32Slip10Secp256k1 bitcoinCashHDWallet(List seedBytes) => - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/145'/0'") as Bip32Slip10Secp256k1; - int estimatedTransactionSize(int inputsCount, int outputsCounts) => inputsCount * 68 + outputsCounts * 34 + 10; @@ -208,7 +206,8 @@ abstract class ElectrumWalletBase bool? alwaysScan; bool mempoolAPIEnabled; - final Bip32Slip10Secp256k1 bip32; + final Map hdWallets; + Bip32Slip10Secp256k1 get bip32 => walletAddresses.bip32; final String? _mnemonic; final EncryptionFileUtils encryptionFileUtils; @@ -1681,11 +1680,17 @@ abstract class ElectrumWalletBase @action Future onHistoriesResponse(List histories) async { + if (histories.isEmpty) { + return; + } + final firstAddress = histories.first; final isChange = firstAddress.addressRecord.isChange; final type = firstAddress.addressRecord.type; - final totalAddresses = histories.length; + final totalAddresses = (isChange + ? walletAddresses.receiveAddresses.where((element) => element.type == type).length + : walletAddresses.changeAddresses.where((element) => element.type == type).length); final gapLimit = (isChange ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); @@ -1717,7 +1722,7 @@ abstract class ElectrumWalletBase // Discover new addresses for the same address type until the gap limit is respected final newAddresses = await walletAddresses.discoverAddresses( isChange: isChange, - gap: gapLimit, + derivationType: firstAddress.addressRecord.derivationType, type: type, derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), ); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 468947c150..f05adbd84c 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -11,6 +11,8 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; +enum CWBitcoinDerivationType { old, electrum, bip39, mweb } + class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; const List BITCOIN_ADDRESS_TYPES = [ @@ -33,7 +35,7 @@ const List BITCOIN_CASH_ADDRESS_TYPES = [ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase( WalletInfo walletInfo, { - required this.bip32, + required this.hdWallets, required this.network, required this.isHardwareWallet, List? initialAddresses, @@ -98,7 +100,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { // TODO: add this variable in `litecoin_wallet_addresses` and just add a cast in cw_bitcoin to use it final ObservableList mwebAddresses; final BasedUtxoNetwork network; - final Bip32Slip10Secp256k1 bip32; + + final Map hdWallets; + Bip32Slip10Secp256k1 get bip32 => + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + final bool isHardwareWallet; @observable @@ -331,6 +337,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType); final address = BitcoinAddressRecord( getAddress( + derivationType: CWBitcoinDerivationType.bip39, isChange: false, index: newAddressIndex, addressType: addressPageType, @@ -342,6 +349,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { type: addressPageType, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), + derivationType: CWBitcoinDerivationType.bip39, ); _allAddresses.add(address); Future.delayed(Duration.zero, () => updateAddressesByMatch()); @@ -349,6 +357,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, @@ -358,12 +367,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } String getAddress({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) { return generateAddress( + derivationType: derivationType, isChange: isChange, index: index, addressType: addressType, @@ -372,12 +383,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } Future getAddressAsync({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) async => getAddress( + derivationType: derivationType, isChange: isChange, index: index, addressType: addressType, @@ -569,12 +582,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future> discoverAddresses({ + required CWBitcoinDerivationType derivationType, required bool isChange, - required int gap, required BitcoinAddressType type, required BitcoinDerivationInfo derivationInfo, }) async { + final gap = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + final newAddresses = await _createNewAddresses( + derivationType: derivationType, gap, isChange: isChange, type: type, @@ -586,36 +604,44 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future generateInitialAddresses({required BitcoinAddressType type}) async { - // TODO: try all other derivations - final derivationInfo = BitcoinAddressUtils.getDerivationFromType(type); + for (final derivationType in hdWallets.keys) { + final derivationInfo = BitcoinAddressUtils.getDerivationFromType( + type, + isElectrum: derivationType == CWBitcoinDerivationType.electrum, + ); - await discoverAddresses( - isChange: false, - gap: defaultReceiveAddressesCount, - type: type, - derivationInfo: derivationInfo, - ); - await discoverAddresses( - isChange: true, - gap: defaultChangeAddressesCount, - type: type, - derivationInfo: derivationInfo, - ); + await discoverAddresses( + derivationType: derivationType, + isChange: false, + type: type, + derivationInfo: derivationInfo, + ); + await discoverAddresses( + derivationType: derivationType, + isChange: true, + type: type, + derivationInfo: derivationInfo, + ); + } } @action Future> _createNewAddresses( int count, { + required CWBitcoinDerivationType derivationType, required BitcoinDerivationInfo derivationInfo, bool isChange = false, BitcoinAddressType? type, }) async { final list = []; - final startIndex = isChange ? totalCountOfChangeAddresses : totalCountOfReceiveAddresses; + final startIndex = (isChange ? receiveAddresses : changeAddresses) + .where((addr) => addr.derivationType == derivationType && addr.type == type) + .length; for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( await getAddressAsync( + derivationType: derivationType, isChange: isChange, index: i, addressType: type ?? addressPageType, @@ -623,9 +649,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ), index: i, isChange: isChange, + isHidden: derivationType == CWBitcoinDerivationType.old, type: type ?? addressPageType, network: network, derivationInfo: derivationInfo, + derivationType: derivationType, ); list.add(address); } @@ -646,6 +674,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); updateReceiveAddresses(); updateChangeAddresses(); + + this.hiddenAddresses.addAll(addresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); } @action diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 716ec0ca52..ce583759ab 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -98,11 +98,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, initialMwebAddresses: initialMwebAddresses, - bip32: bip32, network: network, mwebHd: mwebHd, mwebEnabled: mwebEnabled, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -169,6 +169,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required bool mempoolAPIEnabled, }) async { late Uint8List seedBytes; + late BitcoinDerivationType derivationType; switch (walletInfo.derivationInfo?.derivationType) { case DerivationType.bip39: @@ -176,10 +177,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic, passphrase: passphrase ?? "", ); + derivationType = BitcoinDerivationType.bip39; break; case DerivationType.electrum: default: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + derivationType = BitcoinDerivationType.electrum; break; } return LitecoinWallet( @@ -246,6 +249,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; + late BitcoinDerivationType derivationType; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; @@ -256,10 +260,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic, passphrase: passphrase ?? "", ); + derivationType = BitcoinDerivationType.bip39; break; case DerivationType.electrum: default: seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + derivationType = BitcoinDerivationType.electrum; break; } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 72e19149b6..f9871a9374 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -19,11 +19,11 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { - required super.bip32, required super.network, required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, + required super.hdWallets, super.initialAddresses, super.initialMwebAddresses, super.initialRegularAddressIndex, @@ -98,13 +98,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, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), - )) + .map( + (e) => BitcoinAddressRecord( + e.value, + index: e.key, + type: SegwitAddresType.mweb, + network: network, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, + ), + ) .toList(); addMwebAddresses(addressRecords); print("set ${addressRecords.length} mweb addresses"); @@ -119,6 +122,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, @@ -139,6 +143,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override Future getAddressAsync({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, @@ -149,6 +154,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } return getAddress( + derivationType: derivationType, isChange: isChange, index: index, addressType: addressType, @@ -208,6 +214,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with type: SegwitAddresType.mweb, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), + derivationType: CWBitcoinDerivationType.bip39, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 10a8a212fb..0045801a74 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'; @@ -51,16 +52,17 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, mempoolAPIEnabled: mempoolAPIEnabled, + hdWallets: {CWBitcoinDerivationType.bip39: bitcoinCashHDWallet(seedBytes)}, ) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, - bip32: bip32, network: network, initialAddressPageType: addressPageType, isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -154,6 +156,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + derivationType: CWBitcoinDerivationType.bip39, ); } catch (_) { return BitcoinAddressRecord( @@ -163,6 +166,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + derivationType: CWBitcoinDerivationType.bip39, ); } }).toList(), @@ -253,4 +257,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { 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 34ba748fc4..09b603c6ea 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -10,9 +10,9 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinCashWalletAddressesBase( WalletInfo walletInfo, { - required super.bip32, required super.network, required super.isHardwareWallet, + required super.hdWallets, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, @@ -21,6 +21,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi @override BitcoinBaseAddress generateAddress({ + required CWBitcoinDerivationType derivationType, required bool isChange, required int index, required BitcoinAddressType addressType, 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 53a3930b04..ab674d9b42 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -79,6 +79,7 @@ class WalletInfo extends HiveObject { this.yatLastUsedAddressRaw, this.showIntroCakePayCard, this.derivationInfo, + this.derivations, this.hardwareWalletType, this.parentAddress, ) : _yatLastUsedAddressController = StreamController.broadcast(); @@ -97,6 +98,7 @@ class WalletInfo extends HiveObject { String yatEid = '', String yatLastUsedAddressRaw = '', DerivationInfo? derivationInfo, + List? derivations, HardwareWalletType? hardwareWalletType, String? parentAddress, }) { @@ -114,6 +116,7 @@ class WalletInfo extends HiveObject { yatLastUsedAddressRaw, showIntroCakePayCard, derivationInfo, + derivations, hardwareWalletType, parentAddress, ); @@ -196,6 +199,9 @@ class WalletInfo extends HiveObject { @HiveField(24) List? manualAddresses; + @HiveField(25) + List? derivations; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index f6bea44838..1461c18433 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -5,6 +5,7 @@ class CWBitcoin extends Bitcoin { required String name, required String mnemonic, required String password, + required List? derivations, String? passphrase, }) => BitcoinRestoreWalletFromSeedCredentials( @@ -12,6 +13,7 @@ class CWBitcoin extends Bitcoin { mnemonic: mnemonic, password: password, passphrase: passphrase, + derivations: derivations, ); @override @@ -342,20 +344,12 @@ class CWBitcoin extends Bitcoin { } @override - Future> getDerivationsFromMnemonic({ + Future> getDerivationsFromMnemonic({ required String mnemonic, required Node node, String? passphrase, }) async { - List list = []; - - List types = await compareDerivationMethods(mnemonic: mnemonic, node: node); - if (types.length == 1 && types.first == DerivationType.electrum) { - return [getElectrumDerivations()[DerivationType.electrum]!.first]; - } - - final electrumClient = ElectrumClient(); - await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); + List list = []; late BasedUtxoNetwork network; switch (node.type) { @@ -368,77 +362,34 @@ class CWBitcoin extends Bitcoin { break; } - for (DerivationType dType in electrum_derivations.keys) { - try { - late List seedBytes; - if (dType == DerivationType.electrum) { - seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); - } else if (dType == DerivationType.bip39) { - seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - } - - for (DerivationInfo dInfo in electrum_derivations[dType]!) { - 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 bip32 = Bip32Slip10Secp256k1.fromSeed(seedBytes); - final bip32BalancePath = Bip32PathParser.parse(balancePath); - - // derive address at index 0: - final path = bip32BalancePath.addElem(Bip32KeyIndex(0)); - String? address; - switch (dInfoCopy.scriptType) { - case "p2wpkh": - address = P2wpkhAddress.fromPath(bip32: bip32, path: path).toAddress(network); - break; - case "p2pkh": - address = P2pkhAddress.fromPath(bip32: bip32, path: path).toAddress(network); - break; - case "p2wpkh-p2sh": - address = P2shAddress.fromPath(bip32: bip32, path: path).toAddress(network); - break; - case "p2tr": - address = P2trAddress.fromPath(bip32: bip32, path: path).toAddress(network); - 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"); - } + var electrumSeedBytes; + try { + electrumSeedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + } catch (e) { + print("electrum_v2 seed error: $e"); + + if (passphrase != null && passphrase.isEmpty) { + try { + // TODO: language pick + electrumSeedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + } catch (e) { + print("electrum_v1 seed error: $e"); } - } catch (e) { - print("seed error: $e"); } } - // sort the list such that derivations with the most transactions are first: - list.sort((a, b) => b.transactionsCount.compareTo(a.transactionsCount)); + if (electrumSeedBytes != null) { + list.add(BitcoinDerivationInfos.ELECTRUM); + } + + var bip39SeedBytes; + try { + bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } catch (_) {} + + if (bip39SeedBytes != null) { + list.add(BitcoinDerivationInfos.BIP84); + } return list; } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 97a612d02b..9a9fa1152a 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -109,6 +109,7 @@ class WalletRestorePage extends BasePage { // DerivationType derivationType = DerivationType.unknown; // String? derivationPath = null; DerivationInfo? derivationInfo; + List? derivations; @override Function(BuildContext)? get pushToNextWidget => (context) { @@ -346,6 +347,7 @@ class WalletRestorePage extends BasePage { } credentials['derivationInfo'] = this.derivationInfo; + credentials['derivations'] = this.derivations; credentials['walletType'] = walletRestoreViewModel.type; return credentials; } @@ -383,13 +385,13 @@ class WalletRestorePage extends BasePage { walletRestoreViewModel.state = IsExecutingState(); + // get info about the different derivations: + List derivations = + await walletRestoreViewModel.getDerivationInfo(_credentials()); + if (walletRestoreViewModel.type == WalletType.nano) { 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++) { @@ -416,6 +418,8 @@ class WalletRestorePage extends BasePage { } this.derivationInfo = dInfo; + } else { + this.derivations = derivations; } await walletRestoreViewModel.create(options: _credentials()); diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 0a2c04d7f6..2849b77ec8 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -118,6 +118,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store mnemonic: restoreWallet.mnemonicSeed ?? '', password: password, passphrase: restoreWallet.passphrase, + derivations: [], ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 17a8d6d28e..95cf0256c4 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -101,6 +101,7 @@ abstract class WalletCreationVMBase with Store { address: '', showIntroCakePayCard: (!walletCreationService.typeExists(type)) && type != WalletType.haven, derivationInfo: credentials.derivationInfo ?? getDefaultCreateDerivation(), + derivations: credentials.derivations, hardwareWalletType: credentials.hardwareWalletType, parentAddress: credentials.parentAddress, ); @@ -200,15 +201,36 @@ abstract class WalletCreationVMBase with Store { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: - final derivationList = await bitcoin!.getDerivationsFromMnemonic( + final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, node: node, passphrase: restoreWallet.passphrase, ); - if (derivationList.firstOrNull?.transactionsCount == 0 && derivationList.length > 1) - return []; - return derivationList; + List list = []; + for (var derivation in bitcoinDerivations) { + if (derivation.derivationType == DerivationType.electrum) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ), + ); + } else { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + ); + } + } + + return list; case WalletType.nano: return nanoUtil!.getDerivationsFromMnemonic( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 59623057da..bf1168f01c 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -91,6 +91,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final height = options['height'] as int? ?? 0; name = options['name'] as String; DerivationInfo? derivationInfo = options["derivationInfo"] as DerivationInfo?; + List? derivations = options["derivations"] as List?; if (mode == WalletRestoreMode.seed) { final seed = options['seed'] as String; @@ -105,6 +106,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, passphrase: passphrase, + derivations: derivations, ); case WalletType.haven: return haven!.createHavenRestoreWalletFromSeedCredentials( @@ -254,11 +256,36 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.litecoin: String? mnemonic = credentials['seed'] as String?; String? passphrase = credentials['passphrase'] as String?; - return bitcoin!.getDerivationsFromMnemonic( + final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( mnemonic: mnemonic!, node: node, passphrase: passphrase, ); + + List list = []; + for (var derivation in bitcoinDerivations) { + if (derivation.derivationType.toString().endsWith("electrum")) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + description: "Electrum", + scriptType: "p2wpkh", + ), + ); + } else { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: "m/84'/0'/0'", + description: "Standard BIP84 native segwit", + scriptType: "p2wpkh", + ), + ); + } + } + + return list; case WalletType.nano: String? mnemonic = credentials['seed'] as String?; String? seedKey = credentials['private_key'] as String?; diff --git a/tool/configure.dart b/tool/configure.dart index 07e5231257..d159bffe19 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -148,6 +148,7 @@ abstract class Bitcoin { required String name, required String mnemonic, required String password, + required List? derivations, String? passphrase, }); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); @@ -199,7 +200,7 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPrioritySlow(); Future> compareDerivationMethods( {required String mnemonic, required Node node}); - Future> getDerivationsFromMnemonic( + Future> getDerivationsFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); From c9a50233c17ba805d957d484a82682d30a8f60c1 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 12:49:07 -0300 Subject: [PATCH 09/64] feat: unspents and tweaks subscribe method --- cw_bitcoin/lib/address_from_output.dart | 23 - cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- cw_bitcoin/lib/bitcoin_unspent.dart | 4 +- cw_bitcoin/lib/bitcoin_wallet.dart | 708 +++++------------- cw_bitcoin/lib/bitcoin_wallet_service.dart | 2 +- cw_bitcoin/lib/electrum_transaction_info.dart | 38 +- cw_bitcoin/lib/electrum_wallet.dart | 472 ++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 9 +- .../lib/electrum_worker/electrum_worker.dart | 503 +++++++++++-- .../electrum_worker_methods.dart | 2 + .../electrum_worker_params.dart | 18 +- .../electrum_worker/methods/broadcast.dart | 56 ++ .../electrum_worker/methods/connection.dart | 35 +- .../electrum_worker/methods/get_balance.dart | 17 +- .../electrum_worker/methods/get_history.dart | 16 +- .../methods/get_tx_expanded.dart | 63 ++ .../methods/headers_subscribe.dart | 20 +- .../electrum_worker/methods/list_unspent.dart | 60 ++ .../methods/list_unspents.dart | 53 -- .../lib/electrum_worker/methods/methods.dart | 7 +- .../methods/scripthashes_subscribe.dart | 20 +- .../methods/tweaks_subscribe.dart | 157 ++++ cw_bitcoin/lib/litecoin_wallet.dart | 31 +- .../lib/pending_bitcoin_transaction.dart | 62 +- cw_core/lib/sync_status.dart | 55 ++ 25 files changed, 1435 insertions(+), 1002 deletions(-) delete mode 100644 cw_bitcoin/lib/address_from_output.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/broadcast.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart delete mode 100644 cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart 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 a15364e6c3..d4dd8319fa 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -139,7 +139,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { other.index == index && other.derivationInfo == derivationInfo && other.scriptHash == scriptHash && - other.type == type; + other.type == type && + other.derivationType == derivationType; } @override @@ -148,7 +149,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { index.hashCode ^ derivationInfo.hashCode ^ scriptHash.hashCode ^ - type.hashCode; + type.hashCode ^ + derivationType.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 6dd741b634..93d9c25d5d 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -14,8 +14,8 @@ class BitcoinUnspent extends Unspent { BitcoinUnspent( address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), json['tx_hash'] as String, - json['value'] as int, - json['tx_pos'] as int, + int.parse(json['value'].toString()), + int.parse(json['tx_pos'].toString()), ); Map toJson() { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 8555fdab89..e695ce67f1 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,6 +5,7 @@ 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/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'; @@ -36,7 +37,6 @@ part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { - Future? _isolate; StreamSubscription? _receiveStream; BitcoinWalletBase({ @@ -121,18 +121,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (derivation.derivationType == DerivationType.bip39) { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; - - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ElectrumWalletBase.getKeyNetVersion(network ?? BitcoinNetwork.mainnet), - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (e) { - print("bip39 seed error: $e"); - } break; } else { try { @@ -149,17 +137,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (_) {} break; } } + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -243,18 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (derivation.derivationType == DerivationType.bip39) { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - hdWallets[CWBitcoinDerivationType.old] = hdWallets[CWBitcoinDerivationType.bip39]!; - try { - hdWallets[CWBitcoinDerivationType.old] = Bip32Slip10Secp256k1.fromSeed( - seedBytes, - ElectrumWalletBase.getKeyNetVersion(network), - ).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (e) { - print("bip39 seed error: $e"); - } break; } else { try { @@ -272,15 +245,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - try { - hdWallets[CWBitcoinDerivationType.old] = - Bip32Slip10Secp256k1.fromSeed(seedBytes!).derivePath( - _hardenedDerivationPath(derivation.derivationPath ?? electrum_path), - ) as Bip32Slip10Secp256k1; - } catch (_) {} break; } } + + hdWallets[CWBitcoinDerivationType.old] = + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; } return BitcoinWallet( @@ -362,7 +332,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 rawTx = + (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( @@ -421,7 +392,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } else { alwaysScan = false; - _isolate?.then((value) => value.kill(priority: Isolate.immediate)); + // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); // if (rpc!.isConnected) { // syncStatus = SyncedSyncStatus(); @@ -431,41 +402,41 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - @override - @action - Future updateAllUnspents() async { - List updatedUnspentCoins = []; + // @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!); - } - }); + // // 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; - }); + // // Set the balance of all non-silent payment and non-mweb addresses to 0 before updating + // walletAddresses.allAddresses + // .where((element) => element.type != SegwitAddresType.mweb) + // .forEach((addr) { + // if (addr is! BitcoinSilentPaymentAddressRecord) addr.balance = 0; + // }); - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); + // await Future.wait(walletAddresses.allAddresses + // .where((element) => element.type != SegwitAddresType.mweb) + // .map((address) async { + // updatedUnspentCoins.addAll(await fetchUnspent(address)); + // })); - unspentCoins.addAll(updatedUnspentCoins); + // unspentCoins.addAll(updatedUnspentCoins); - if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - unspentCoins.forEach((coin) => addCoinInfo(coin)); - return; - } + // if (unspentCoinsInfo.length != updatedUnspentCoins.length) { + // unspentCoins.forEach((coin) => addCoinInfo(coin)); + // return; + // } - await updateCoins(unspentCoins.toSet()); - await refreshUnspentCoinsInfo(); - } + // await updateCoins(unspentCoins.toSet()); + // await refreshUnspentCoinsInfo(); + // } @override void updateCoin(BitcoinUnspent coin) { @@ -489,17 +460,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - Future _setInitialHeight() async { - final validChainTip = currentChainTip != null && currentChainTip != 0; - if (validChainTip && walletInfo.restoreHeight == 0) { - await walletInfo.updateRestoreHeight(currentChainTip!); - } - } - @action @override Future startSync() async { - await _setInitialHeight(); + await _setInitialScanHeight(); await super.startSync(); @@ -547,16 +511,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @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"); + // 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 @@ -583,6 +547,103 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } + @override + @action + Future handleWorkerResponse(dynamic message) async { + super.handleWorkerResponse(message); + + Map messageJson; + if (message is String) { + messageJson = jsonDecode(message) as Map; + } else { + messageJson = message as Map; + } + final workerMethod = messageJson['method'] as String; + + switch (workerMethod) { + case ElectrumRequestMethods.tweaksSubscribeMethod: + final response = ElectrumWorkerTweaksSubscribeResponse.fromJson(messageJson); + onTweaksSyncResponse(response.result); + break; + } + } + + @action + Future onTweaksSyncResponse(TweaksSyncResponse result) async { + if (result.transactions?.isNotEmpty == true) { + for (final map in result.transactions!.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + final existingTxInfo = transactionHistory.transactions[txid]; + final txAlreadyExisted = existingTxInfo != null; + + // Updating tx after re-scanned + if (txAlreadyExisted) { + existingTxInfo.amount = tx.amount; + existingTxInfo.confirmations = tx.confirmations; + existingTxInfo.height = tx.height; + + final newUnspents = tx.unspents! + .where((unspent) => !(existingTxInfo.unspents?.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value) ?? + false)) + .toList(); + + if (newUnspents.isNotEmpty) { + newUnspents.forEach(_updateSilentAddressRecord); + + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + + final newAmount = newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + + if (existingTxInfo.direction == TransactionDirection.incoming) { + existingTxInfo.amount += newAmount; + } + + // Updates existing TX + transactionHistory.addOne(existingTxInfo); + // Update balance record + balance[currency]!.confirmed += newAmount; + } + } else { + // else: First time seeing this TX after scanning + tx.unspents!.forEach(_updateSilentAddressRecord); + + // Add new TX record + transactionHistory.addOne(tx); + // Update balance record + balance[currency]!.confirmed += tx.amount; + } + + await updateAllUnspents(); + } + } + } + + final newSyncStatus = result.syncStatus; + + if (newSyncStatus != null) { + if (newSyncStatus is UnsupportedSyncStatus) { + nodeSupportsSilentPayments = false; + } + + if (newSyncStatus is SyncingSyncStatus) { + syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); + } else { + syncStatus = newSyncStatus; + } + + await walletInfo.updateRestoreHeight(result.height!); + } + } + @action Future _setListeners(int height, {bool? doSingleScan}) async { if (currentChainTip == null) { @@ -598,106 +659,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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, + workerSendPort!.send( + ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData( silentAddress: walletAddresses.silentAddress!, network: network, height: height, chainTip: chainTip, 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.labelIndex >= 1) .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, - )); - - _receiveStream?.cancel(); - _receiveStream = receivePort.listen((var message) async { - if (message is Map) { - for (final map in message.entries) { - final txid = map.key; - final tx = map.value; - - if (tx.unspents != null) { - final existingTxInfo = transactionHistory.transactions[txid]; - final txAlreadyExisted = existingTxInfo != null; - - // Updating tx after re-scanned - if (txAlreadyExisted) { - existingTxInfo.amount = tx.amount; - existingTxInfo.confirmations = tx.confirmations; - existingTxInfo.height = tx.height; - - final newUnspents = tx.unspents! - .where((unspent) => !(existingTxInfo.unspents?.any((element) => - element.hash.contains(unspent.hash) && - element.vout == unspent.vout && - element.value == unspent.value) ?? - false)) - .toList(); - - if (newUnspents.isNotEmpty) { - newUnspents.forEach(_updateSilentAddressRecord); - - existingTxInfo.unspents ??= []; - existingTxInfo.unspents!.addAll(newUnspents); - - final newAmount = newUnspents.length > 1 - ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) - : newUnspents[0].value; - - if (existingTxInfo.direction == TransactionDirection.incoming) { - existingTxInfo.amount += newAmount; - } - - // Updates existing TX - transactionHistory.addOne(existingTxInfo); - // Update balance record - balance[currency]!.confirmed += newAmount; - } - } else { - // else: First time seeing this TX after scanning - tx.unspents!.forEach(_updateSilentAddressRecord); - - // Add new TX record - transactionHistory.addMany(message); - // Update balance record - balance[currency]!.confirmed += tx.amount; - } - - await updateAllUnspents(); - } - } - } - - if (message is SyncResponse) { - if (message.syncStatus is UnsupportedSyncStatus) { - nodeSupportsSilentPayments = false; - } - - if (message.syncStatus is SyncingSyncStatus) { - var status = message.syncStatus as SyncingSyncStatus; - syncStatus = SyncingSyncStatus(status.blocksLeft, status.ptc); - } else { - syncStatus = message.syncStatus; - } - - await walletInfo.updateRestoreHeight(message.height); - } - }); + ), + ).toJson(), + ); } @override @@ -824,15 +802,28 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - // @override - // @action - // void onHeadersResponse(ElectrumHeaderResponse response) { - // super.onHeadersResponse(response); + @override + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + super.onHeadersResponse(response); - // if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - // _setListeners(walletInfo.restoreHeight); - // } - // } + _setInitialScanHeight(); + + // New headers received, start scanning + if (alwaysScan == true && syncStatus is SyncedSyncStatus) { + _setListeners(walletInfo.restoreHeight); + } + } + + Future _setInitialScanHeight() async { + final validChainTip = currentChainTip != null && currentChainTip != 0; + if (validChainTip && walletInfo.restoreHeight == 0) { + await walletInfo.updateRestoreHeight(currentChainTip!); + } + } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @override @action @@ -850,355 +841,4 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.syncStatusReaction(syncStatus); } } - - static String _hardenedDerivationPath(String derivationPath) => - derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); -} - -Future startRefresh(ScanData scanData) async { - int syncHeight = scanData.height; - int initialSyncHeight = syncHeight; - - final electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( - scanData.node?.uri ?? Uri.parse("tcp://198.58.115.71:50001"), - ), - ); - - int getCountPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } - - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } - - final receiver = Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, - ); - - // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, StartingScanSyncStatus(syncHeight))); - - final listener = await electrumClient.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), - ); - - Future listenFn(ElectrumTweaksSubscribeResponse response) async { - // success or error msg - final noData = response.message != null; - - if (noData) { - // re-subscribe to continue receiving messages, starting from the next unscanned height - final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - - if (nextCount > 0) { - final nextListener = await electrumClient.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), - ); - nextListener?.call(listenFn); - } - - return; - } - - // Continuous status UI update, send how many blocks left to scan - final syncingStatus = scanData.isSingleScan - ? SyncingSyncStatus(1, 0) - : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); - scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); - - final tweakHeight = response.block; - - try { - final blockTweaks = response.blockTweaks; - - for (final txid in blockTweaks.keys) { - final tweakData = blockTweaks[txid]; - final outputPubkeys = tweakData!.outputPubkeys; - final tweak = tweakData.tweak; - - try { - // scanOutputs called from rust here - final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); - - if (addToWallet.isEmpty) { - // no results tx, continue to next tx - continue; - } - - // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) - final txInfo = ElectrumTransactionInfo( - WalletType.bitcoin, - id: txid, - height: tweakHeight, - amount: 0, - fee: 0, - direction: TransactionDirection.incoming, - isPending: false, - isReplaced: false, - date: scanData.network == BitcoinNetwork.mainnet - ? getDateByBitcoinHeight(tweakHeight) - : DateTime.now(), - confirmations: scanData.chainTip - tweakHeight + 1, - unspents: [], - isReceivedSilentPayment: true, - ); - - addToWallet.forEach((label, value) { - (value as Map).forEach((output, tweak) { - final t_k = tweak.toString(); - - final receivingOutputAddress = ECPublic.fromHex(output) - .toTaprootAddress(tweak: false) - .toAddress(scanData.network); - - final matchingOutput = outputPubkeys[output]!; - final amount = matchingOutput.amount; - final pos = matchingOutput.vout; - - final receivedAddressRecord = BitcoinReceivedSPAddressRecord( - receivingOutputAddress, - labelIndex: 1, // TODO: get actual index/label - isUsed: true, - spendKey: scanData.silentAddress.b_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), - ), - txCount: 1, - balance: amount, - ); - - final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); - - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; - }); - }); - - scanData.sendPort.send({txInfo.id: txInfo}); - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - } - } - } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); - } - - syncHeight = tweakHeight; - - if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { - if (tweakHeight >= scanData.chainTip) - scanData.sendPort.send(SyncResponse( - syncHeight, - SyncedTipSyncStatus(scanData.chainTip), - )); - - if (scanData.isSingleScan) { - scanData.sendPort.send(SyncResponse(syncHeight, SyncedSyncStatus())); - } - } - } - - listener?.call(listenFn); - - // if (tweaksSubscription == null) { - // return scanData.sendPort.send( - // SyncResponse(syncHeight, UnsupportedSyncStatus()), - // ); - // } -} - -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); -} - -class ScanData { - final SendPort sendPort; - final SilentPaymentOwner silentAddress; - final int height; - final ScanNode? node; - final BasedUtxoNetwork network; - final int chainTip; - 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.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, - labels: scanData.labels, - labelIndexes: scanData.labelIndexes, - isSingleScan: scanData.isSingleScan, - ); - } -} - -class SyncResponse { - final int height; - final SyncStatus syncStatus; - - SyncResponse(this.height, this.syncStatus); } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 941c252650..b310c1db3c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -23,8 +23,8 @@ class BitcoinWalletService extends WalletService< this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, - this.mempoolAPIEnabled, this.isDirect, + this.mempoolAPIEnabled, ); final Box walletInfoSource; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index ccf9e20d7e..f751205319 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,7 +1,6 @@ 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_unspent.dart'; import 'package:cw_core/transaction_direction.dart'; @@ -11,13 +10,35 @@ import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; class ElectrumTransactionBundle { - ElectrumTransactionBundle(this.originalTransaction, - {required this.ins, required this.confirmations, this.time}); + ElectrumTransactionBundle( + this.originalTransaction, { + required this.ins, + required this.confirmations, + this.time, + }); final BtcTransaction originalTransaction; final List ins; final int? time; final int confirmations; + + Map toJson() { + return { + 'originalTransaction': originalTransaction.toHex(), + 'ins': ins.map((e) => e.toHex()).toList(), + 'confirmations': confirmations, + 'time': time, + }; + } + + static ElectrumTransactionBundle fromJson(Map data) { + return ElectrumTransactionBundle( + BtcTransaction.fromRaw(data['originalTransaction'] as String), + ins: (data['ins'] as List).map((e) => BtcTransaction.fromRaw(e as String)).toList(), + confirmations: data['confirmations'] as int, + time: data['time'] as int?, + ); + } } class ElectrumTransactionInfo extends TransactionInfo { @@ -128,9 +149,11 @@ class ElectrumTransactionInfo extends TransactionInfo { final inputTransaction = bundle.ins[i]; final outTransaction = inputTransaction.outputs[input.txIndex]; inputAmount += outTransaction.amount.toInt(); - if (addresses.contains(addressFromOutputScript(outTransaction.scriptPubKey, network))) { + if (addresses.contains( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) { direction = TransactionDirection.outgoing; - inputAddresses.add(addressFromOutputScript(outTransaction.scriptPubKey, network)); + inputAddresses.add( + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network)); } } } catch (e) { @@ -144,8 +167,9 @@ class ElectrumTransactionInfo extends TransactionInfo { 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); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 7986b2cb6e..9964751ee3 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -6,11 +6,11 @@ import 'dart:isolate'; import 'package:bitcoin_base/bitcoin_base.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'; @@ -25,7 +25,7 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.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'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -35,13 +35,11 @@ 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:http/http.dart' as http; part 'electrum_wallet.g.dart'; @@ -52,8 +50,11 @@ abstract class ElectrumWalletBase with Store, WalletKeysFile { ReceivePort? receivePort; SendPort? workerSendPort; - StreamSubscription? _workerSubscription; + StreamSubscription? _workerSubscription; Isolate? _workerIsolate; + final Map _responseCompleters = {}; + final Map _errorCompleters = {}; + int _messageId = 0; ElectrumWalletBase({ required String password, @@ -67,7 +68,6 @@ abstract class ElectrumWalletBase List? seedBytes, this.passphrase, List? initialAddresses, - ElectrumClient? electrumClient, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, @@ -103,7 +103,6 @@ abstract class ElectrumWalletBase this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, super(walletInfo) { - this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, @@ -116,8 +115,27 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } + Future sendWorker(ElectrumWorkerRequest request) { + final messageId = ++_messageId; + + final completer = Completer(); + _responseCompleters[messageId] = completer; + + final json = request.toJson(); + json['id'] = messageId; + workerSendPort!.send(json); + + try { + return completer.future.timeout(Duration(seconds: 5)); + } catch (e) { + _errorCompleters.addAll({messageId: e}); + _responseCompleters.remove(messageId); + rethrow; + } + } + @action - Future _handleWorkerResponse(dynamic message) async { + Future handleWorkerResponse(dynamic message) async { print('Main: received message: $message'); Map messageJson; @@ -149,6 +167,12 @@ abstract class ElectrumWalletBase // return; // } + final responseId = messageJson['id'] as int?; + if (responseId != null && _responseCompleters.containsKey(responseId)) { + _responseCompleters[responseId]!.complete(message); + _responseCompleters.remove(responseId); + } + switch (workerMethod) { case ElectrumWorkerMethods.connectionMethod: final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); @@ -157,7 +181,6 @@ abstract class ElectrumWalletBase case ElectrumRequestMethods.headersSubscribeMethod: final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); await onHeadersResponse(response.result); - break; case ElectrumRequestMethods.getBalanceMethod: final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson); @@ -167,6 +190,10 @@ abstract class ElectrumWalletBase final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson); onHistoriesResponse(response.result); break; + case ElectrumRequestMethods.listunspentMethod: + final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); + onUnspentResponse(response.result); + break; } } @@ -219,7 +246,6 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - late ElectrumClient electrumClient; ApiProvider? apiProvider; Box unspentCoinsInfo; @@ -298,6 +324,7 @@ abstract class ElectrumWalletBase bool _chainTipListenerOn = false; bool _isTransactionUpdating; + bool _isInitialSync = true; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -323,16 +350,18 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); // INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero - await subscribeForHeaders(); + await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later. await updateTransactions(); - // await updateAllUnspents(); // INFO: THIRD: Start loading the TX history await updateBalance(); - // await subscribeForUpdates(); + // INFO: FOURTH: Finish with unspents + await updateAllUnspents(); + + _isInitialSync = false; // await updateFeeRates(); @@ -377,7 +406,7 @@ abstract class ElectrumWalletBase return false; } - final version = await electrumClient.version(); + // final version = await electrumClient.version(); if (version.isNotEmpty) { final server = version[0]; @@ -416,10 +445,13 @@ abstract class ElectrumWalletBase if (message is SendPort) { workerSendPort = message; workerSendPort!.send( - ElectrumWorkerConnectionRequest(uri: node.uri).toJson(), + ElectrumWorkerConnectionRequest( + uri: node.uri, + network: network, + ).toJson(), ); } else { - _handleWorkerResponse(message); + handleWorkerResponse(message); } }); } catch (e, stacktrace) { @@ -927,11 +959,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 @@ -1007,11 +1038,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, @@ -1177,7 +1207,9 @@ abstract class ElectrumWalletBase @override Future close({required bool shouldCleanup}) async { try { - await electrumClient.close(); + _workerIsolate!.kill(priority: Isolate.immediate); + await _workerSubscription?.cancel(); + receivePort?.close(); } catch (_) {} _autoSaveTimer?.cancel(); _updateFeeRateTimer?.cancel(); @@ -1185,25 +1217,15 @@ abstract class ElectrumWalletBase @action Future updateAllUnspents() async { - List updatedUnspentCoins = []; - - Set scripthashes = {}; - walletAddresses.allAddresses.forEach((addressRecord) { - scripthashes.add(addressRecord.scriptHash); - }); - - workerSendPort!.send( - ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(), + final req = ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), ); - await Future.wait(walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) - .map((address) async { - updatedUnspentCoins.addAll(await fetchUnspent(address)); - })); - - await updateCoins(unspentCoins.toSet()); - await refreshUnspentCoinsInfo(); + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } @action @@ -1227,46 +1249,38 @@ abstract class ElectrumWalletBase } @action - Future updateCoins(Set newUnspentCoins) async { - if (newUnspentCoins.isEmpty) { - return; - } - newUnspentCoins.forEach(updateCoin); - } - - @action - Future updateUnspentsForAddress(BitcoinAddressRecord addressRecord) async { - final newUnspentCoins = (await fetchUnspent(addressRecord)).toSet(); - await updateCoins(newUnspentCoins); + Future onUnspentResponse(Map> unspents) async { + final updatedUnspentCoins = []; - unspentCoins.addAll(newUnspentCoins); - - // if (unspentCoinsInfo.length != unspentCoins.length) { - // unspentCoins.forEach(addCoinInfo); - // } + await Future.wait(unspents.entries.map((entry) async { + final unspent = entry.value; + final scriptHash = entry.key; - // await refreshUnspentCoinsInfo(); - } - - @action - Future> fetchUnspent(BitcoinAddressRecord address) async { - List> unspents = []; - List updatedUnspentCoins = []; + final addressRecord = walletAddresses.allAddresses.firstWhereOrNull( + (element) => element.scriptHash == scriptHash, + ); - unspents = await electrumClient.getListUnspent(address.scriptHash); + if (addressRecord == null) { + return null; + } - await Future.wait(unspents.map((unspent) async { - try { - final coin = BitcoinUnspent.fromJSON(address, unspent); - // final tx = await fetchTransactionInfo(hash: coin.hash); - coin.isChange = address.isHidden; - // coin.confirmations = tx?.confirmations; + 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); + if (tx != null) { + coin.confirmations = tx.confirmations; + } updatedUnspentCoins.add(coin); - } catch (_) {} + })); })); - return updatedUnspentCoins; + unspentCoins.clear(); + unspentCoins.addAll(updatedUnspentCoins); + unspentCoins.forEach(updateCoin); + + await refreshUnspentCoinsInfo(); } @action @@ -1287,7 +1301,6 @@ abstract class ElectrumWalletBase await unspentCoinsInfo.add(newInfo); } - // TODO: ? Future refreshUnspentCoinsInfo() async { try { final List keys = []; @@ -1313,6 +1326,92 @@ abstract class ElectrumWalletBase } } + @action + Future onHeadersResponse(ElectrumHeaderResponse response) async { + currentChainTip = response.height; + + bool updated = false; + transactionHistory.transactions.values.forEach((tx) { + if (tx.height != null && tx.height! > 0) { + final newConfirmations = currentChainTip! - tx.height! + 1; + + if (tx.confirmations != newConfirmations) { + tx.confirmations = newConfirmations; + tx.isPending = tx.confirmations == 0; + updated = true; + } + } + }); + + if (updated) { + await save(); + } + } + + @action + Future subscribeForHeaders() async { + if (_chainTipListenerOn) return; + + workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); + _chainTipListenerOn = true; + } + + @action + Future onHistoriesResponse(List histories) async { + if (histories.isEmpty) { + return; + } + + final firstAddress = histories.first; + final isChange = firstAddress.addressRecord.isChange; + final type = firstAddress.addressRecord.type; + + final totalAddresses = (isChange + ? walletAddresses.receiveAddresses.where((element) => element.type == type).length + : walletAddresses.changeAddresses.where((element) => element.type == type).length); + final gapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + bool hasUsedAddressesUnderGap = false; + final addressesWithHistory = []; + + for (final addressHistory in histories) { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + final address = addressHistory.addressRecord; + addressesWithHistory.add(address); + + hasUsedAddressesUnderGap = + address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } + } + } + + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } + + if (hasUsedAddressesUnderGap) { + // Discover new addresses for the same address type until the gap limit is respected + final newAddresses = await walletAddresses.discoverAddresses( + isChange: isChange, + derivationType: firstAddress.addressRecord.derivationType, + type: type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), + ); + + if (newAddresses.isNotEmpty) { + // Update the transactions for the new discovered addresses + await updateTransactions(newAddresses); + } + } + } + Future canReplaceByFee(ElectrumTransactionInfo tx) async { try { final bundle = await getTransactionExpanded(hash: tx.txHash); @@ -1331,8 +1430,9 @@ abstract class ElectrumWalletBase 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; @@ -1370,7 +1470,8 @@ 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); // allInputsAmount += outTransaction.amount.toInt(); final addressRecord = @@ -1417,7 +1518,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()))); } @@ -1496,10 +1597,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 { @@ -1519,27 +1619,23 @@ abstract class ElectrumWalletBase } Future getTransactionExpanded({required String hash}) async { - int? time; - int? height; - final transactionHex = await electrumClient.getTransactionHex(hash: hash); - - int? confirmations; - - final original = BtcTransaction.fromRaw(transactionHex); - final ins = []; - - for (final vin in original.inputs) { - final inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); + return await sendWorker( + ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!)) + as ElectrumTransactionBundle; + } - ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + Future fetchTransactionInfo({required String hash, int? height}) async { + try { + return ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded(hash: hash), + walletInfo.type, + network, + addresses: walletAddresses.allAddresses.map((e) => e.address).toSet(), + height: height, + ); + } catch (_) { + return null; } - - return ElectrumTransactionBundle( - original, - ins: ins, - time: time, - confirmations: confirmations ?? 0, - ); } @override @@ -1550,27 +1646,24 @@ abstract class ElectrumWalletBase @action Future updateTransactions([List? addresses]) async { - // TODO: all - addresses ??= walletAddresses.allAddresses - .where( - (element) => element.type == SegwitAddresType.p2wpkh && element.isChange == false, - ) - .toList(); - - workerSendPort!.send( - ElectrumWorkerGetHistoryRequest( - addresses: addresses, - storedTxs: transactionHistory.transactions.values.toList(), - walletType: type, - // If we still don't have currentChainTip, txs will still be fetched but shown - // with confirmations as 0 but will be auto fixed on onHeadersResponse - chainTip: currentChainTip ?? 0, - network: network, - // mempoolAPIEnabled: mempoolAPIEnabled, - // TODO: - mempoolAPIEnabled: true, - ).toJson(), + addresses ??= walletAddresses.allAddresses.toList(); + + final req = ElectrumWorkerGetHistoryRequest( + addresses: addresses, + storedTxs: transactionHistory.transactions.values.toList(), + walletType: type, + // If we still don't have currentChainTip, txs will still be fetched but shown + // with confirmations as 0 but will be auto fixed on onHeadersResponse + chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), + network: network, + mempoolAPIEnabled: mempoolAPIEnabled, ); + + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } } @action @@ -1594,16 +1687,41 @@ abstract class ElectrumWalletBase } @action - Future updateBalance() async { - workerSendPort!.send( - ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes).toJson(), + void onBalanceResponse(ElectrumBalance balanceResult) { + var totalFrozen = 0; + var totalConfirmed = balanceResult.confirmed; + var totalUnconfirmed = balanceResult.unconfirmed; + + unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { + if (unspentCoinInfo.isFrozen) { + // TODO: verify this works well + totalFrozen += unspentCoinInfo.value; + totalConfirmed -= unspentCoinInfo.value; + totalUnconfirmed -= unspentCoinInfo.value; + } + }); + + balance[currency] = ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: totalFrozen, ); } + @action + Future updateBalance() async { + final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); + + if (_isInitialSync) { + await sendWorker(req); + } else { + workerSendPort!.send(req.toJson()); + } + } + @override void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; - @override Future signMessage(String message, {String? address = null}) async { final record = walletAddresses.getFromAddresses(address!); @@ -1678,115 +1796,6 @@ abstract class ElectrumWalletBase return false; } - @action - Future onHistoriesResponse(List histories) async { - if (histories.isEmpty) { - return; - } - - final firstAddress = histories.first; - final isChange = firstAddress.addressRecord.isChange; - final type = firstAddress.addressRecord.type; - - final totalAddresses = (isChange - ? walletAddresses.receiveAddresses.where((element) => element.type == type).length - : walletAddresses.changeAddresses.where((element) => element.type == type).length); - final gapLimit = (isChange - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - bool hasUsedAddressesUnderGap = false; - - final addressesWithHistory = []; - - for (final addressHistory in histories) { - final txs = addressHistory.txs; - - if (txs.isNotEmpty) { - final address = addressHistory.addressRecord; - addressesWithHistory.add(address); - - hasUsedAddressesUnderGap = - address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); - - for (final tx in txs) { - transactionHistory.addOne(tx); - } - } - } - - if (addressesWithHistory.isNotEmpty) { - walletAddresses.updateAdresses(addressesWithHistory); - } - - if (hasUsedAddressesUnderGap) { - // Discover new addresses for the same address type until the gap limit is respected - final newAddresses = await walletAddresses.discoverAddresses( - isChange: isChange, - derivationType: firstAddress.addressRecord.derivationType, - type: type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), - ); - - if (newAddresses.isNotEmpty) { - // Update the transactions for the new discovered addresses - await updateTransactions(newAddresses); - } - } - } - - @action - void onBalanceResponse(ElectrumBalance balanceResult) { - var totalFrozen = 0; - var totalConfirmed = balanceResult.confirmed; - var totalUnconfirmed = balanceResult.unconfirmed; - - unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { - if (unspentCoinInfo.isFrozen) { - // TODO: verify this works well - totalFrozen += unspentCoinInfo.value; - totalConfirmed -= unspentCoinInfo.value; - totalUnconfirmed -= unspentCoinInfo.value; - } - }); - - balance[currency] = ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: totalFrozen, - ); - } - - @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 { - print(_chainTipListenerOn); - if (_chainTipListenerOn) return; - - workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); - _chainTipListenerOn = true; - } - @action void _onConnectionStatusChange(ConnectionStatus status) { switch (status) { @@ -1862,14 +1871,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); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index f05adbd84c..44e3be7f9e 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -649,7 +649,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ), index: i, isChange: isChange, - isHidden: derivationType == CWBitcoinDerivationType.old, + isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh, type: type ?? addressPageType, network: network, derivationInfo: derivationInfo, @@ -664,7 +664,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAdresses(Iterable addresses) { for (final address in addresses) { - _allAddresses.replaceRange(address.index, address.index + 1, [address]); + final index = _allAddresses.indexWhere((element) => element.address == address.address); + _allAddresses.replaceRange(index, index + 1, [address]); + + updateAddressesByMatch(); + updateReceiveAddresses(); + updateChangeAddresses(); } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 8b372bd3f3..102d6c313e 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -3,16 +3,20 @@ 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_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_balance.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; - -// TODO: ping +import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; @@ -56,8 +60,15 @@ class ElectrumWorker { ElectrumWorkerConnectionRequest.fromJson(messageJson), ); break; + case ElectrumWorkerMethods.txHashMethod: + await _handleGetTxExpanded( + ElectrumWorkerTxExpandedRequest.fromJson(messageJson), + ); + break; case ElectrumRequestMethods.headersSubscribeMethod: - await _handleHeadersSubscribe(); + await _handleHeadersSubscribe( + ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson), + ); break; case ElectrumRequestMethods.scripthashesSubscribeMethod: await _handleScriphashesSubscribe( @@ -74,12 +85,21 @@ class ElectrumWorker { ElectrumWorkerGetHistoryRequest.fromJson(messageJson), ); break; - case 'blockchain.scripthash.listunspent': - // await _handleListUnspent(workerMessage); + case ElectrumRequestMethods.listunspentMethod: + await _handleListUnspent( + ElectrumWorkerListUnspentRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.broadcastMethod: + await _handleBroadcast( + ElectrumWorkerBroadcastRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.tweaksSubscribeMethod: + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); break; - // Add other method handlers here - // default: - // _sendError(workerMethod, 'Unsupported method: ${workerMessage.method}'); } } catch (e, s) { print(s); @@ -88,11 +108,11 @@ class ElectrumWorker { } Future _handleConnect(ElectrumWorkerConnectionRequest request) async { - _electrumClient = ElectrumApiProvider( - await ElectrumTCPService.connect( + _electrumClient = await ElectrumApiProvider.connect( + ElectrumTCPService.connect( request.uri, onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status)); + _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); }, defaultRequestTimeOut: const Duration(seconds: 5), connectionTimeOut: const Duration(seconds: 5), @@ -100,7 +120,7 @@ class ElectrumWorker { ); } - Future _handleHeadersSubscribe() async { + Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); if (listener == null) { _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); @@ -108,7 +128,9 @@ class ElectrumWorker { } listener((event) { - _sendResponse(ElectrumWorkerHeadersSubscribeResponse(result: event)); + _sendResponse( + ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id), + ); }); } @@ -135,6 +157,7 @@ class ElectrumWorker { _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( result: {address: status}, + id: request.id, )); }); })); @@ -171,7 +194,7 @@ class ElectrumWorker { } } catch (_) { tx = ElectrumTransactionInfo.fromElectrumBundle( - await getTransactionExpanded( + await _getTransactionExpanded( hash: txid, currentChainTip: result.chainTip, mempoolAPIEnabled: result.mempoolAPIEnabled, @@ -201,10 +224,113 @@ class ElectrumWorker { return histories; })); - _sendResponse(ElectrumWorkerGetHistoryResponse(result: histories.values.toList())); + _sendResponse(ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: result.id, + )); + } + + // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { + // final balanceFutures = >>[]; + + // for (final scripthash in request.scripthashes) { + // final balanceFuture = _electrumClient!.request( + // ElectrumGetScriptHashBalance(scriptHash: scripthash), + // ); + // balanceFutures.add(balanceFuture); + // } + + // var totalConfirmed = 0; + // var totalUnconfirmed = 0; + + // final balances = await Future.wait(balanceFutures); + + // for (final balance in balances) { + // final confirmed = balance['confirmed'] as int? ?? 0; + // final unconfirmed = balance['unconfirmed'] as int? ?? 0; + // totalConfirmed += confirmed; + // totalUnconfirmed += unconfirmed; + // } + + // _sendResponse(ElectrumWorkerGetBalanceResponse( + // result: ElectrumBalance( + // confirmed: totalConfirmed, + // unconfirmed: totalUnconfirmed, + // frozen: 0, + // ), + // )); + // } + + Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { + final balanceFutures = >>[]; + + for (final scripthash in request.scripthashes) { + final balanceFuture = _electrumClient!.request( + ElectrumGetScriptHashBalance(scriptHash: scripthash), + ); + balanceFutures.add(balanceFuture); + } + + var totalConfirmed = 0; + var totalUnconfirmed = 0; + + final balances = await Future.wait(balanceFutures); + + for (final balance in balances) { + final confirmed = balance['confirmed'] as int? ?? 0; + final unconfirmed = balance['unconfirmed'] as int? ?? 0; + totalConfirmed += confirmed; + totalUnconfirmed += unconfirmed; + } + + _sendResponse( + ElectrumWorkerGetBalanceResponse( + result: ElectrumBalance( + confirmed: totalConfirmed, + unconfirmed: totalUnconfirmed, + frozen: 0, + ), + id: request.id, + ), + ); + } + + Future _handleListUnspent(ElectrumWorkerListUnspentRequest request) async { + final unspents = >{}; + + await Future.wait(request.scripthashes.map((scriptHash) async { + final scriptHashUnspents = await _electrumClient!.request( + ElectrumScriptHashListUnspent(scriptHash: scriptHash), + ); + + if (scriptHashUnspents.isNotEmpty) { + unspents[scriptHash] = scriptHashUnspents; + } + })); + + _sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id)); + } + + Future _handleBroadcast(ElectrumWorkerBroadcastRequest request) async { + final txHash = await _electrumClient!.request( + ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw), + ); + + _sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id)); + } + + Future _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async { + final tx = await _getTransactionExpanded( + hash: request.txHash, + currentChainTip: request.currentChainTip, + mempoolAPIEnabled: false, + getConfirmations: false, + ); + + _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); } - Future getTransactionExpanded({ + Future _getTransactionExpanded({ required String hash, required int currentChainTip, required bool mempoolAPIEnabled, @@ -289,65 +415,312 @@ class ElectrumWorker { ); } - // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { - // final balanceFutures = >>[]; + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { + final scanData = request.scanData; + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; - // for (final scripthash in request.scripthashes) { - // final balanceFuture = _electrumClient!.request( - // ElectrumGetScriptHashBalance(scriptHash: scripthash), - // ); - // balanceFutures.add(balanceFuture); - // } + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } - // var totalConfirmed = 0; - // var totalUnconfirmed = 0; + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } - // final balances = await Future.wait(balanceFutures); + final receiver = Receiver( + scanData.silentAddress.b_scan.toHex(), + scanData.silentAddress.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + scanData.labelIndexes.length, + ); - // for (final balance in balances) { - // final confirmed = balance['confirmed'] as int? ?? 0; - // final unconfirmed = balance['unconfirmed'] as int? ?? 0; - // totalConfirmed += confirmed; - // totalUnconfirmed += unconfirmed; - // } + // Initial status UI update, send how many blocks in total to scan + final initialCount = getCountPerRequest(syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: StartingScanSyncStatus(syncHeight), + ), + )); - // _sendResponse(ElectrumWorkerGetBalanceResponse( - // result: ElectrumBalance( - // confirmed: totalConfirmed, - // unconfirmed: totalUnconfirmed, - // frozen: 0, - // ), - // )); - // } + print([syncHeight, initialCount]); + final listener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); - Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { - final balanceFutures = >>[]; + Future listenFn(ElectrumTweaksSubscribeResponse response) async { + // success or error msg + final noData = response.message != null; - for (final scripthash in request.scripthashes) { - final balanceFuture = _electrumClient!.request( - ElectrumGetScriptHashBalance(scriptHash: scripthash), - ); - balanceFutures.add(balanceFuture); - } + if (noData) { + // re-subscribe to continue receiving messages, starting from the next unscanned height + final nextHeight = syncHeight + 1; + final nextCount = getCountPerRequest(nextHeight); - var totalConfirmed = 0; - var totalUnconfirmed = 0; + if (nextCount > 0) { + final nextListener = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + ); + nextListener?.call(listenFn); + } - final balances = await Future.wait(balanceFutures); + return; + } - for (final balance in balances) { - final confirmed = balance['confirmed'] as int? ?? 0; - final unconfirmed = balance['unconfirmed'] as int? ?? 0; - totalConfirmed += confirmed; - totalUnconfirmed += unconfirmed; + // Continuous status UI update, send how many blocks left to scan + final syncingStatus = scanData.isSingleScan + ? SyncingSyncStatus(1, 0) + : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus), + )); + + final tweakHeight = response.block; + + try { + final blockTweaks = response.blockTweaks; + + for (final txid in blockTweaks.keys) { + final tweakData = blockTweaks[txid]; + final outputPubkeys = tweakData!.outputPubkeys; + final tweak = tweakData.tweak; + + try { + // scanOutputs called from rust here + final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + + if (addToWallet.isEmpty) { + // no results tx, continue to next tx + continue; + } + + // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) + final txInfo = ElectrumTransactionInfo( + WalletType.bitcoin, + id: txid, + height: tweakHeight, + amount: 0, + fee: 0, + direction: TransactionDirection.incoming, + isPending: false, + isReplaced: false, + date: scanData.network == BitcoinNetwork.mainnet + ? getDateByBitcoinHeight(tweakHeight) + : DateTime.now(), + confirmations: scanData.chainTip - tweakHeight + 1, + unspents: [], + isReceivedSilentPayment: true, + ); + + addToWallet.forEach((label, value) { + (value as Map).forEach((output, tweak) { + final t_k = tweak.toString(); + + final receivingOutputAddress = ECPublic.fromHex(output) + .toTaprootAddress(tweak: false) + .toAddress(scanData.network); + + final matchingOutput = outputPubkeys[output]!; + final amount = matchingOutput.amount; + final pos = matchingOutput.vout; + + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( + receivingOutputAddress, + labelIndex: 1, // TODO: get actual index/label + isUsed: true, + spendKey: scanData.silentAddress.b_spend.tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), + ), + txCount: 1, + balance: amount, + ); + + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); + + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); + }); + + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}), + )); + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + } + } catch (e, stacktrace) { + print(stacktrace); + print(e.toString()); + } + + syncHeight = tweakHeight; + + if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if (tweakHeight >= scanData.chainTip) + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: SyncedTipSyncStatus(scanData.chainTip), + ), + )); + + if (scanData.isSingleScan) { + _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()), + )); + } + } } - _sendResponse(ElectrumWorkerGetBalanceResponse( - result: ElectrumBalance( - confirmed: totalConfirmed, - unconfirmed: totalUnconfirmed, - frozen: 0, - ), - )); + listener?.call(listenFn); + + // if (tweaksSubscription == null) { + // return scanData.sendPort.send( + // SyncResponse(syncHeight, UnsupportedSyncStatus()), + // ); + // } } } + +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 index c171e2cae1..6bd4d296e7 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -4,9 +4,11 @@ class ElectrumWorkerMethods { static const String connectionMethod = "connection"; static const String unknownMethod = "unknown"; + static const String txHashMethod = "txHash"; static const ElectrumWorkerMethods connect = ElectrumWorkerMethods._(connectionMethod); static const ElectrumWorkerMethods unknown = ElectrumWorkerMethods._(unknownMethod); + static const ElectrumWorkerMethods txHash = ElectrumWorkerMethods._(txHashMethod); @override String toString() { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart index f666eed1df..ea3c0b1994 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -4,17 +4,24 @@ 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}); + 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(); @@ -25,21 +32,22 @@ class ElectrumWorkerResponse { } Map toJson() { - return {'method': method, 'result': resultJson(result), 'error': error}; + return {'method': method, 'result': resultJson(result), 'error': error, 'id': id}; } } class ElectrumWorkerErrorResponse { - ElectrumWorkerErrorResponse({required this.error}); + 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); + return ElectrumWorkerErrorResponse(error: json['error'] as String, id: json['id'] as int); } Map toJson() { - return {'method': method, 'error': error}; + 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/connection.dart b/cw_bitcoin/lib/electrum_worker/methods/connection.dart index 1abbcb81e4..2512c6cfd4 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/connection.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -1,34 +1,56 @@ part of 'methods.dart'; class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { - ElectrumWorkerConnectionRequest({required this.uri}); + ElectrumWorkerConnectionRequest({ + required this.uri, + required this.network, + this.id, + }); final Uri uri; + final BasedUtxoNetwork network; + final int? id; @override final String method = ElectrumWorkerMethods.connect.method; @override factory ElectrumWorkerConnectionRequest.fromJson(Map json) { - return ElectrumWorkerConnectionRequest(uri: Uri.parse(json['params'] as String)); + return ElectrumWorkerConnectionRequest( + uri: Uri.parse(json['uri'] as String), + network: BasedUtxoNetwork.values.firstWhere( + (e) => e.toString() == json['network'] as String, + ), + id: json['id'] as int?, + ); } @override Map toJson() { - return {'method': method, 'params': uri.toString()}; + return { + 'method': method, + 'uri': uri.toString(), + 'network': network.toString(), + }; } } class ElectrumWorkerConnectionError extends ElectrumWorkerErrorResponse { - ElectrumWorkerConnectionError({required String error}) : super(error: error); + 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( + ElectrumWorkerConnectionResponse({ + required ConnectionStatus status, + super.error, + super.id, + }) : super( result: status, method: ElectrumWorkerMethods.connect.method, ); @@ -45,6 +67,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse 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 index fc79967e1d..2fc5513675 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -1,9 +1,10 @@ part of 'methods.dart'; class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { - ElectrumWorkerGetBalanceRequest({required this.scripthashes}); + ElectrumWorkerGetBalanceRequest({required this.scripthashes, this.id}); final Set scripthashes; + final int? id; @override final String method = ElectrumRequestMethods.getBalance.method; @@ -12,6 +13,7 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { return ElectrumWorkerGetBalanceRequest( scripthashes: (json['scripthashes'] as List).toSet(), + id: json['id'] as int?, ); } @@ -22,7 +24,10 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { } class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { - ElectrumWorkerGetBalanceError({required String error}) : super(error: error); + ElectrumWorkerGetBalanceError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.getBalance.method; @@ -30,8 +35,11 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { class ElectrumWorkerGetBalanceResponse extends ElectrumWorkerResponse?> { - ElectrumWorkerGetBalanceResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.getBalance.method); + ElectrumWorkerGetBalanceResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getBalance.method); @override Map? resultJson(result) { @@ -47,6 +55,7 @@ class ElectrumWorkerGetBalanceResponse frozen: 0, ), 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 index 584f4b6d11..021ed6899e 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -8,6 +8,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { required this.chainTip, required this.network, required this.mempoolAPIEnabled, + this.id, }); final List addresses; @@ -16,6 +17,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { final int chainTip; final BasedUtxoNetwork network; final bool mempoolAPIEnabled; + final int? id; @override final String method = ElectrumRequestMethods.getHistory.method; @@ -35,6 +37,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { chainTip: json['chainTip'] as int, network: BasedUtxoNetwork.fromName(json['network'] as String), mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, ); } @@ -53,7 +56,10 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { } class ElectrumWorkerGetHistoryError extends ElectrumWorkerErrorResponse { - ElectrumWorkerGetHistoryError({required String error}) : super(error: error); + ElectrumWorkerGetHistoryError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.getHistory.method; @@ -90,8 +96,11 @@ class AddressHistoriesResponse { class ElectrumWorkerGetHistoryResponse extends ElectrumWorkerResponse, List>> { - ElectrumWorkerGetHistoryResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.getHistory.method); + ElectrumWorkerGetHistoryResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.getHistory.method); @override List> resultJson(result) { @@ -105,6 +114,7 @@ class ElectrumWorkerGetHistoryResponse .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..a2dfcda17a --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -0,0 +1,63 @@ +part of 'methods.dart'; + +class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { + ElectrumWorkerTxExpandedRequest({ + required this.txHash, + required this.currentChainTip, + this.id, + }); + + final String txHash; + final int currentChainTip; + final int? id; + + @override + final String method = ElectrumWorkerMethods.txHash.method; + + @override + factory ElectrumWorkerTxExpandedRequest.fromJson(Map json) { + return ElectrumWorkerTxExpandedRequest( + txHash: json['txHash'] as String, + currentChainTip: json['currentChainTip'] as int, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'txHash': txHash, 'currentChainTip': currentChainTip}; + } +} + +class ElectrumWorkerTxExpandedError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTxExpandedError({ + required String error, + super.id, + }) : super(error: error); + + @override + String get method => ElectrumWorkerMethods.txHash.method; +} + +class ElectrumWorkerTxExpandedResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTxExpandedResponse({ + required ElectrumTransactionBundle expandedTx, + super.error, + super.id, + }) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTxExpandedResponse.fromJson(Map json) { + return ElectrumWorkerTxExpandedResponse( + expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart index 619f32aedc..de02f5d249 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart @@ -1,14 +1,17 @@ part of 'methods.dart'; class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { - ElectrumWorkerHeadersSubscribeRequest(); + ElectrumWorkerHeadersSubscribeRequest({this.id}); @override final String method = ElectrumRequestMethods.headersSubscribe.method; + final int? id; @override factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map json) { - return ElectrumWorkerHeadersSubscribeRequest(); + return ElectrumWorkerHeadersSubscribeRequest( + id: json['id'] as int?, + ); } @override @@ -18,7 +21,10 @@ class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { } class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { - ElectrumWorkerHeadersSubscribeError({required String error}) : super(error: error); + ElectrumWorkerHeadersSubscribeError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.headersSubscribe.method; @@ -26,8 +32,11 @@ class ElectrumWorkerHeadersSubscribeError extends ElectrumWorkerErrorResponse { class ElectrumWorkerHeadersSubscribeResponse extends ElectrumWorkerResponse> { - ElectrumWorkerHeadersSubscribeResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.headersSubscribe.method); + ElectrumWorkerHeadersSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.headersSubscribe.method); @override Map resultJson(result) { @@ -39,6 +48,7 @@ class ElectrumWorkerHeadersSubscribeResponse 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/list_unspents.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart deleted file mode 100644 index c3a626a0b0..0000000000 --- a/cw_bitcoin/lib/electrum_worker/methods/list_unspents.dart +++ /dev/null @@ -1,53 +0,0 @@ -// part of 'methods.dart'; - -// class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { -// ElectrumWorkerGetBalanceRequest({required this.scripthashes}); - -// final Set scripthashes; - -// @override -// final String method = ElectrumRequestMethods.getBalance.method; - -// @override -// factory ElectrumWorkerGetBalanceRequest.fromJson(Map json) { -// return ElectrumWorkerGetBalanceRequest( -// scripthashes: (json['scripthashes'] as List).toSet(), -// ); -// } - -// @override -// Map toJson() { -// return {'method': method, 'scripthashes': scripthashes.toList()}; -// } -// } - -// class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse { -// ElectrumWorkerGetBalanceError({required String error}) : super(error: error); - -// @override -// final String method = ElectrumRequestMethods.getBalance.method; -// } - -// class ElectrumWorkerGetBalanceResponse -// extends ElectrumWorkerResponse?> { -// ElectrumWorkerGetBalanceResponse({required super.result, super.error}) -// : 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?, -// ); -// } -// } - diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 31b82bf9e2..6ace715d0a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; @@ -6,8 +5,14 @@ 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'; + 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'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart index 35a73ef49a..31f9abe76d 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -1,9 +1,13 @@ part of 'methods.dart'; class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerRequest { - ElectrumWorkerScripthashesSubscribeRequest({required this.scripthashByAddress}); + ElectrumWorkerScripthashesSubscribeRequest({ + required this.scripthashByAddress, + this.id, + }); final Map scripthashByAddress; + final int? id; @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @@ -12,6 +16,7 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { return ElectrumWorkerScripthashesSubscribeRequest( scripthashByAddress: json['scripthashes'] as Map, + id: json['id'] as int?, ); } @@ -22,7 +27,10 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques } class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorResponse { - ElectrumWorkerScripthashesSubscribeError({required String error}) : super(error: error); + ElectrumWorkerScripthashesSubscribeError({ + required super.error, + super.id, + }) : super(); @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @@ -30,8 +38,11 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon class ElectrumWorkerScripthashesSubscribeResponse extends ElectrumWorkerResponse?, Map?> { - ElectrumWorkerScripthashesSubscribeResponse({required super.result, super.error}) - : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); + ElectrumWorkerScripthashesSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); @override Map? resultJson(result) { @@ -43,6 +54,7 @@ class ElectrumWorkerScripthashesSubscribeResponse return ElectrumWorkerScripthashesSubscribeResponse( result: json['result'] as Map?, error: json['error'] as String?, + id: json['id'] as int?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart new file mode 100644 index 0000000000..0a6f36dc94 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -0,0 +1,157 @@ +part of 'methods.dart'; + +class ScanData { + final SilentPaymentOwner silentAddress; + final int height; + final BasedUtxoNetwork network; + final int chainTip; + final List transactionHistoryIds; + final Map labels; + final List labelIndexes; + final bool isSingleScan; + + ScanData({ + required this.silentAddress, + required this.height, + required this.network, + required this.chainTip, + required this.transactionHistoryIds, + required this.labels, + required this.labelIndexes, + required this.isSingleScan, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + silentAddress: scanData.silentAddress, + height: newHeight, + network: scanData.network, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + labels: scanData.labels, + labelIndexes: scanData.labelIndexes, + isSingleScan: scanData.isSingleScan, + ); + } + + Map toJson() { + return { + 'silentAddress': silentAddress.toJson(), + 'height': height, + 'network': network.value, + 'chainTip': chainTip, + 'transactionHistoryIds': transactionHistoryIds, + 'labels': labels, + 'labelIndexes': labelIndexes, + 'isSingleScan': isSingleScan, + }; + } + + static ScanData fromJson(Map json) { + return ScanData( + silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map), + height: json['height'] as int, + network: BasedUtxoNetwork.fromName(json['network'] as String), + chainTip: json['chainTip'] as int, + transactionHistoryIds: + (json['transactionHistoryIds'] as List).map((e) => e as String).toList(), + labels: json['labels'] as Map, + labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(), + isSingleScan: json['isSingleScan'] as bool, + ); + } +} + +class ElectrumWorkerTweaksSubscribeRequest implements ElectrumWorkerRequest { + ElectrumWorkerTweaksSubscribeRequest({ + required this.scanData, + this.id, + }); + + final ScanData scanData; + final int? id; + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; + + @override + factory ElectrumWorkerTweaksSubscribeRequest.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeRequest( + scanData: ScanData.fromJson(json['scanData'] as Map), + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'scanData': scanData.toJson()}; + } +} + +class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTweaksSubscribeError({ + required super.error, + super.id, + }) : super(); + + @override + final String method = ElectrumRequestMethods.tweaksSubscribe.method; +} + +class TweaksSyncResponse { + int? height; + SyncStatus? syncStatus; + Map? transactions = {}; + + TweaksSyncResponse({this.height, this.syncStatus, this.transactions}); + + Map toJson() { + return { + 'height': height, + 'syncStatus': syncStatus == null ? null : syncStatusToJson(syncStatus!), + 'transactions': transactions?.map((key, value) => MapEntry(key, value.toJson())), + }; + } + + static TweaksSyncResponse fromJson(Map json) { + return TweaksSyncResponse( + height: json['height'] as int?, + syncStatus: json['syncStatus'] == null + ? null + : syncStatusFromJson(json['syncStatus'] as Map), + transactions: json['transactions'] == null + ? null + : (json['transactions'] as Map).map( + (key, value) => MapEntry( + key, + ElectrumTransactionInfo.fromJson( + value as Map, + WalletType.bitcoin, + )), + ), + ); + } +} + +class ElectrumWorkerTweaksSubscribeResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerTweaksSubscribeResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.tweaksSubscribe.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerTweaksSubscribeResponse.fromJson(Map json) { + return ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse.fromJson(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index ce583759ab..277864af78 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -6,7 +6,7 @@ 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_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -109,22 +109,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); 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 SynchronizingSyncStatus) { @@ -348,8 +335,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return; } - final nodeHeight = - await electrumClient.getCurrentBlockChainTip() ?? 0; // current block height of our node + final nodeHeight = await currentChainTip ?? 0; if (nodeHeight == 0) { // we aren't connected to the ltc node yet @@ -635,7 +621,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } final status = await CwMweb.status(StatusRequest()); - final height = await electrumClient.getCurrentBlockChainTip(); + final height = await currentChainTip; if (height == null || status.blockHeaderHeight != height) return; if (status.mwebUtxosHeight != height) return; // we aren't synced int amount = 0; @@ -770,7 +756,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); // copy coin control attributes to mwebCoins: - await updateCoins(mwebUnspentCoins.toSet()); + // await updateCoins(mwebUnspentCoins); // get regular ltc unspents (this resets unspentCoins): await super.updateAllUnspents(); // add the mwebCoins: @@ -1289,7 +1275,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/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 4b77d984d2..a8088f6429 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,9 +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/electrum_transaction_info.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/wallet_type.dart'; @@ -14,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, @@ -28,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; @@ -79,40 +78,39 @@ class PendingBitcoinTransaction with PendingTransaction { Future _commit() async { int? callId; - final result = await electrumClient.broadcastTransaction( - transactionRaw: hex, network: network, idCallback: (id) => callId = id); + final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String; - if (result.isEmpty) { - if (callId != null) { - final error = electrumClient.getErrorMessage(callId!); + // if (result.isEmpty) { + // if (callId != null) { + // final error = sendWorker(getErrorMessage(callId!)); - if (error.contains("dust")) { - if (hasChange) { - throw BitcoinTransactionCommitFailedDustChange(); - } else if (!isSendAll) { - throw BitcoinTransactionCommitFailedDustOutput(); - } else { - throw BitcoinTransactionCommitFailedDustOutputSendAll(); - } - } + // if (error.contains("dust")) { + // if (hasChange) { + // throw BitcoinTransactionCommitFailedDustChange(); + // } else if (!isSendAll) { + // throw BitcoinTransactionCommitFailedDustOutput(); + // } else { + // throw BitcoinTransactionCommitFailedDustOutputSendAll(); + // } + // } - if (error.contains("bad-txns-vout-negative")) { - throw BitcoinTransactionCommitFailedVoutNegative(); - } + // if (error.contains("bad-txns-vout-negative")) { + // throw BitcoinTransactionCommitFailedVoutNegative(); + // } - if (error.contains("non-BIP68-final")) { - throw BitcoinTransactionCommitFailedBIP68Final(); - } + // if (error.contains("non-BIP68-final")) { + // throw BitcoinTransactionCommitFailedBIP68Final(); + // } - if (error.contains("min fee not met")) { - throw BitcoinTransactionCommitFailedLessThanMin(); - } + // if (error.contains("min fee not met")) { + // throw BitcoinTransactionCommitFailedLessThanMin(); + // } - throw BitcoinTransactionCommitFailed(errorMessage: error); - } + // throw BitcoinTransactionCommitFailed(errorMessage: error); + // } - throw BitcoinTransactionCommitFailed(); - } + // throw BitcoinTransactionCommitFailed(); + // } } Future _ltcCommit() async { diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 5790159dfa..6b4a5da930 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -96,3 +96,58 @@ class LostConnectionSyncStatus extends NotConnectedSyncStatus { @override String toString() => 'Reconnecting'; } + +Map syncStatusToJson(SyncStatus? status) { + if (status == null) { + return {}; + } + return { + 'progress': status.progress(), + 'type': status.runtimeType.toString(), + 'data': status is SyncingSyncStatus + ? {'blocksLeft': status.blocksLeft, 'ptc': status.ptc} + : status is SyncedTipSyncStatus + ? {'tip': status.tip} + : status is FailedSyncStatus + ? {'error': status.error} + : status is StartingScanSyncStatus + ? {'beginHeight': status.beginHeight} + : null + }; +} + +SyncStatus syncStatusFromJson(Map json) { + final type = json['type'] as String; + final data = json['data'] as Map?; + + switch (type) { + case 'StartingScanSyncStatus': + return StartingScanSyncStatus(data!['beginHeight'] as int); + case 'SyncingSyncStatus': + return SyncingSyncStatus(data!['blocksLeft'] as int, data['ptc'] as double); + case 'SyncedTipSyncStatus': + return SyncedTipSyncStatus(data!['tip'] as int); + case 'FailedSyncStatus': + return FailedSyncStatus(error: data!['error'] as String?); + case 'SynchronizingSyncStatus': + return SynchronizingSyncStatus(); + case 'NotConnectedSyncStatus': + return NotConnectedSyncStatus(); + case 'AttemptingSyncStatus': + return AttemptingSyncStatus(); + case 'AttemptingScanSyncStatus': + return AttemptingScanSyncStatus(); + case 'ConnectedSyncStatus': + return ConnectedSyncStatus(); + case 'ConnectingSyncStatus': + return ConnectingSyncStatus(); + case 'UnsupportedSyncStatus': + return UnsupportedSyncStatus(); + case 'TimedOutSyncStatus': + return TimedOutSyncStatus(); + case 'LostConnectionSyncStatus': + return LostConnectionSyncStatus(); + default: + throw Exception('Unknown sync status type: $type'); + } +} From a4561d254790a6379b2a6d9a664e8f9ca7972016 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:30:44 -0300 Subject: [PATCH 10/64] chore: deps --- .gitignore | 1 + cw_bitcoin/pubspec.lock | 24 +++++++++++++++--------- cw_bitcoin/pubspec.yaml | 16 ++++++++++++---- cw_bitcoin_cash/pubspec.yaml | 12 +++++++++--- cw_tron/pubspec.yaml | 12 +++++++++--- pubspec_base.yaml | 4 +++- 6 files changed, 49 insertions(+), 20 deletions(-) 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/pubspec.lock b/cw_bitcoin/pubspec.lock index d02a50e3b3..76bbdcc251 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -86,16 +86,20 @@ packages: bitcoin_base: dependency: "direct overridden" description: - path: "/home/rafael/Working/bitcoin_base/" - relative: false - source: path + path: "." + 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: "/home/rafael/Working/blockchain_utils/" - relative: false - source: path + path: "." + ref: cake-update-v3 + resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" + url: "https://github.com/cake-tech/blockchain_utils.git" + source: git version: "3.3.0" bluez: dependency: transitive @@ -913,9 +917,11 @@ packages: sp_scanner: dependency: "direct main" description: - path: "/home/rafael/Working/sp_scanner/" - relative: false - source: path + path: "." + ref: cake-update-v3 + resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b" + url: "https://github.com/cake-tech/sp_scanner.git" + source: git version: "0.0.1" stack_trace: dependency: transitive diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 94ae3e0466..9e69f4eb04 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,12 +27,16 @@ dependencies: rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + ref: cake-update-v3 cw_mweb: path: ../cw_mweb grpc: ^3.2.4 sp_scanner: - path: /home/rafael/Working/sp_scanner/ + git: + url: https://github.com/cake-tech/sp_scanner.git + ref: cake-update-v3 bech32: git: url: https://github.com/cake-tech/bech32.git @@ -58,9 +62,13 @@ dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base/ + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + ref: cake-update-v3 pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index eb2eceef3e..e1336d0d62 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -26,7 +26,9 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + ref: cake-update-v3 dev_dependencies: flutter_test: @@ -38,9 +40,13 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base/ + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + 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_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 19c7781352..10eb3fbda9 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -16,9 +16,13 @@ dependencies: cw_evm: path: ../cw_evm on_chain: - path: /home/rafael/Working/On_chain/ + git: + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v3 blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + ref: cake-update-v3 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -33,7 +37,9 @@ dev_dependencies: dependency_overrides: blockchain_utils: - path: /home/rafael/Working/blockchain_utils/ + git: + url: https://github.com/cake-tech/blockchain_utils.git + ref: cake-update-v3 flutter: # assets: diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 0402ba1594..72c6cd6bcc 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -132,7 +132,9 @@ dependency_overrides: flutter_secure_storage_platform_interface: 1.0.2 protobuf: ^3.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base/ + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 ffi: 2.1.0 flutter_icons: From 7964b2a056616311445f3d3bbfc07cd364b1b791 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 5 Nov 2024 13:41:11 -0300 Subject: [PATCH 11/64] chore: deps --- cw_bitcoin/pubspec.yaml | 4 ++-- cw_bitcoin_cash/pubspec.yaml | 4 ++-- cw_tron/pubspec.yaml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9e69f4eb04..2510d172d7 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: cryptography: ^2.0.5 blockchain_utils: git: - url: https://github.com/cake-tech/blockchain_utils.git + url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 cw_mweb: path: ../cw_mweb @@ -67,7 +67,7 @@ dependency_overrides: ref: cake-update-v15 blockchain_utils: git: - url: https://github.com/cake-tech/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/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index e1336d0d62..e3f7eb0ab5 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: ref: Add-Support-For-OP-Return-data blockchain_utils: git: - url: https://github.com/cake-tech/blockchain_utils.git + url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 dev_dependencies: @@ -45,7 +45,7 @@ dependency_overrides: ref: cake-update-v15 blockchain_utils: git: - url: https://github.com/cake-tech/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 diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 10eb3fbda9..9da2217bb7 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: ref: cake-update-v3 blockchain_utils: git: - url: https://github.com/cake-tech/blockchain_utils.git + url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 mobx: ^2.3.0+1 bip39: ^1.0.6 @@ -38,7 +38,7 @@ dev_dependencies: dependency_overrides: blockchain_utils: git: - url: https://github.com/cake-tech/blockchain_utils.git + url: https://github.com/cake-tech/blockchain_utils ref: cake-update-v3 flutter: From 884a822cea7f88a1a025c117fd69a652e756bc5a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 6 Nov 2024 12:06:52 -0300 Subject: [PATCH 12/64] fix: fee and addresses --- .../lib/bitcoin_transaction_priority.dart | 249 +++++++++--------- cw_bitcoin/lib/bitcoin_wallet.dart | 63 ++--- cw_bitcoin/lib/electrum_wallet.dart | 143 ++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 + .../lib/electrum_worker/electrum_worker.dart | 80 +++++- .../lib/electrum_worker/methods/get_fees.dart | 60 +++++ .../lib/electrum_worker/methods/methods.dart | 4 + .../lib/electrum_worker/methods/version.dart | 52 ++++ cw_bitcoin/lib/litecoin_wallet.dart | 19 +- .../lib/src/bitcoin_cash_wallet.dart | 8 +- cw_core/lib/transaction_priority.dart | 4 + lib/bitcoin/cw_bitcoin.dart | 47 ++-- lib/bitcoin_cash/cw_bitcoin_cash.dart | 9 +- lib/store/settings_store.dart | 3 +- 14 files changed, 435 insertions(+), 310 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_fees.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/version.dart diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 1e9c0c2731..26a4c2f626 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,25 +1,25 @@ import 'package:cw_core/transaction_priority.dart'; +class BitcoinTransactionPriority extends TransactionPriority { + const BitcoinTransactionPriority({required super.title, required super.raw}); + // 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: 4); -class BitcoinMempoolAPITransactionPriority extends TransactionPriority { - const BitcoinMempoolAPITransactionPriority({required super.title, required super.raw}); - - static const BitcoinMempoolAPITransactionPriority unimportant = - BitcoinMempoolAPITransactionPriority(title: 'Unimportant', raw: 0); - static const BitcoinMempoolAPITransactionPriority normal = - BitcoinMempoolAPITransactionPriority(title: 'Normal', raw: 1); - static const BitcoinMempoolAPITransactionPriority elevated = - BitcoinMempoolAPITransactionPriority(title: 'Elevated', raw: 2); - static const BitcoinMempoolAPITransactionPriority priority = - BitcoinMempoolAPITransactionPriority(title: 'Priority', raw: 3); - static const BitcoinMempoolAPITransactionPriority custom = - BitcoinMempoolAPITransactionPriority(title: 'Custom', raw: 4); - - static BitcoinMempoolAPITransactionPriority deserialize({required int raw}) { + static BitcoinTransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return unimportant; @@ -41,19 +41,19 @@ class BitcoinMempoolAPITransactionPriority extends TransactionPriority { var label = ''; switch (this) { - case BitcoinMempoolAPITransactionPriority.unimportant: + case BitcoinTransactionPriority.unimportant: label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinMempoolAPITransactionPriority.normal: + case BitcoinTransactionPriority.normal: label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; break; - case BitcoinMempoolAPITransactionPriority.elevated: + case BitcoinTransactionPriority.elevated: label = 'Elevated'; break; // S.current.transaction_priority_fast; - case BitcoinMempoolAPITransactionPriority.priority: + case BitcoinTransactionPriority.priority: label = 'Priority'; break; // S.current.transaction_priority_fast; - case BitcoinMempoolAPITransactionPriority.custom: + case BitcoinTransactionPriority.custom: label = 'Custom'; break; default: @@ -69,64 +69,53 @@ class BitcoinMempoolAPITransactionPriority extends TransactionPriority { } } -class BitcoinElectrumTransactionPriority extends TransactionPriority { - const BitcoinElectrumTransactionPriority({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 = [ - unimportant, - normal, - elevated, - priority, - custom, - ]; - - static const BitcoinElectrumTransactionPriority unimportant = - BitcoinElectrumTransactionPriority(title: 'Unimportant', raw: 0); - static const BitcoinElectrumTransactionPriority normal = - BitcoinElectrumTransactionPriority(title: 'Normal', raw: 1); - static const BitcoinElectrumTransactionPriority elevated = - BitcoinElectrumTransactionPriority(title: 'Elevated', raw: 2); - static const BitcoinElectrumTransactionPriority priority = - BitcoinElectrumTransactionPriority(title: 'Priority', raw: 3); - static const BitcoinElectrumTransactionPriority custom = - BitcoinElectrumTransactionPriority(title: 'Custom', raw: 4); - - static BitcoinElectrumTransactionPriority deserialize({required int raw}) { + static const List all = [fast, medium, slow, custom]; + + 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 unimportant; + return slow; case 1: - return normal; + return medium; case 2: - return elevated; + return fast; case 3: - return priority; - case 4: return custom; default: - throw Exception('Unexpected token: $raw for TransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); } } + String get units => 'sat'; + @override String toString() { var label = ''; switch (this) { - case BitcoinElectrumTransactionPriority.unimportant: - label = 'Unimportant'; // '${S.current.transaction_priority_slow} ~24hrs'; - break; - case BitcoinElectrumTransactionPriority.normal: + case ElectrumTransactionPriority.slow: label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinElectrumTransactionPriority.elevated: + case ElectrumTransactionPriority.medium: label = 'Medium'; // S.current.transaction_priority_medium; - break; // S.current.transaction_priority_fast; - case BitcoinElectrumTransactionPriority.priority: + break; + case ElectrumTransactionPriority.fast: label = 'Fast'; break; // S.current.transaction_priority_fast; - case BitcoinElectrumTransactionPriority.custom: + case ElectrumTransactionPriority.custom: label = 'Custom'; break; default: @@ -142,88 +131,48 @@ class BitcoinElectrumTransactionPriority extends TransactionPriority { } } -class LitecoinTransactionPriority extends BitcoinElectrumTransactionPriority { +class LitecoinTransactionPriority extends ElectrumTransactionPriority { const LitecoinTransactionPriority({required super.title, required super.raw}); - static const all = [slow, medium, fast]; - - 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 LitecoinTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return slow; - case 1: - return medium; - case 2: - return fast; - default: - throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); - } - } - @override String get units => 'lit'; } -class BitcoinCashTransactionPriority extends BitcoinElectrumTransactionPriority { +class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { const BitcoinCashTransactionPriority({required super.title, required super.raw}); - static const all = [slow, medium, fast]; - - 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); - - static BitcoinCashTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return slow; - case 1: - return medium; - case 2: - return fast; - default: - throw Exception('Unexpected token: $raw for LitecoinTransactionPriority deserialize'); - } - } - @override String get units => 'satoshi'; } -class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { - const BitcoinMempoolAPITransactionPriorities({ +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 BitcoinMempoolAPITransactionPriority.unimportant: + case BitcoinTransactionPriority.unimportant: return unimportant; - case BitcoinMempoolAPITransactionPriority.normal: + case BitcoinTransactionPriority.normal: return normal; - case BitcoinMempoolAPITransactionPriority.elevated: + case BitcoinTransactionPriority.elevated: return elevated; - case BitcoinMempoolAPITransactionPriority.priority: + case BitcoinTransactionPriority.priority: return priority; + case BitcoinTransactionPriority.custom: + return custom; default: throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } @@ -233,7 +182,7 @@ class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { String labelWithRate(TransactionPriority priorityType, [int? rate]) { late int rateValue; - if (priorityType == BitcoinMempoolAPITransactionPriority.custom) { + if (priorityType == BitcoinTransactionPriority.custom) { if (rate == null) { throw Exception('Rate must be provided for custom transaction priority'); } @@ -244,32 +193,53 @@ class BitcoinMempoolAPITransactionPriorities implements TransactionPriorities { return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)'; } + + @override + Map toJson() { + return { + 'unimportant': unimportant, + 'normal': normal, + 'elevated': elevated, + 'priority': priority, + 'custom': custom, + }; + } + + static BitcoinTransactionPriorities fromJson(Map json) { + return BitcoinTransactionPriorities( + unimportant: json['unimportant'] as int, + normal: json['normal'] as int, + elevated: json['elevated'] as int, + priority: json['priority'] as int, + custom: json['custom'] as int, + ); + } } -class BitcoinElectrumTransactionPriorities implements TransactionPriorities { - const BitcoinElectrumTransactionPriorities({ - required this.unimportant, +class ElectrumTransactionPriorities implements TransactionPriorities { + const ElectrumTransactionPriorities({ required this.slow, required this.medium, required this.fast, + required this.custom, }); - final int unimportant; final int slow; final int medium; final int fast; + final int custom; @override int operator [](TransactionPriority type) { switch (type) { - case BitcoinElectrumTransactionPriority.unimportant: - return unimportant; - case BitcoinElectrumTransactionPriority.normal: + case ElectrumTransactionPriority.slow: return slow; - case BitcoinElectrumTransactionPriority.elevated: + case ElectrumTransactionPriority.medium: return medium; - case BitcoinElectrumTransactionPriority.priority: + case ElectrumTransactionPriority.fast: return fast; + case ElectrumTransactionPriority.custom: + return custom; default: throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } @@ -280,25 +250,46 @@ class BitcoinElectrumTransactionPriorities implements TransactionPriorities { return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; } - factory BitcoinElectrumTransactionPriorities.fromList(List list) { + factory ElectrumTransactionPriorities.fromList(List list) { if (list.length != 3) { throw Exception( 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); } - int unimportantFee = list[0]; - - // Electrum servers only provides 3 levels: slow, medium, fast - // so make "unimportant" always lower than slow (but not 0) - if (unimportantFee > 1) { - unimportantFee--; - } - - return BitcoinElectrumTransactionPriorities( - unimportant: unimportantFee, + 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_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index e695ce67f1..748acdbbe3 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -276,6 +276,24 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); } + Future getNodeIsElectrs() async { + final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List; + + if (version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + node!.save(); + return node!.isElectrs!; + } + } + + node!.isElectrs = false; + node!.save(); + return node!.isElectrs!; + } + Future getNodeSupportsSilentPayments() async { return true; // As of today (august 2024), only ElectrumRS supports silent payments @@ -757,51 +775,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // ); // } - @override - @action - Future updateFeeRates() async { - // Bitcoin only: use the mempool.space backend API for accurate fee rates - if (mempoolAPIEnabled) { - try { - final recommendedFees = await apiProvider!.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 should be higher than normal - if (normalFee == elevatedFee) { - elevatedFee++; - } - // priority should be higher than elevated - while (priorityFee <= elevatedFee) { - priorityFee++; - } - // this guarantees that, even if all fees are low and equal, - // higher priority fees can be taken when fees start surging - - feeRates = BitcoinMempoolAPITransactionPriorities( - unimportant: unimportantFee, - normal: normalFee, - elevated: elevatedFee, - priority: priorityFee, - ); - return; - } catch (e, stacktrace) { - callError(FlutterErrorDetails( - exception: e, - stack: stacktrace, - library: this.runtimeType.toString(), - )); - } - } else { - // Bitcoin only: Ideally this should be avoided, electrum is terrible at fee rates - await super.updateFeeRates(); - } - } - @override @action Future onHeadersResponse(ElectrumHeaderResponse response) async { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 9964751ee3..47d69d6700 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -16,7 +16,6 @@ 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_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -84,7 +83,6 @@ abstract class ElectrumWalletBase }, syncStatus = NotConnectedSyncStatus(), _password = password, - _isTransactionUpdating = false, isEnabledAutoGenerateSubaddress = true, // TODO: inital unspent coins unspentCoins = BitcoinUnspentCoins(), @@ -115,6 +113,7 @@ abstract class ElectrumWalletBase 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; @@ -144,28 +143,14 @@ abstract class ElectrumWalletBase } else { messageJson = message as Map; } + final workerMethod = messageJson['method'] as String; + final workerError = messageJson['error'] as String?; - // if (workerResponse.error != null) { - // print('Worker error: ${workerResponse.error}'); - - // switch (workerResponse.method) { - // // case 'connectionStatus': - // // final status = ConnectionStatus.values.firstWhere( - // // (e) => e.toString() == workerResponse.error, - // // ); - // // _onConnectionStatusChange(status); - // // break; - // // case 'fetchBalances': - // // // Update the balance state - // // // this.balance[currency] = balance!; - // // break; - // case 'blockchain.headers.subscribe': - // _chainTipListenerOn = false; - // break; - // } - // return; - // } + if (workerError != null) { + print('Worker error: $workerError'); + return; + } final responseId = messageJson['id'] as int?; if (responseId != null && _responseCompleters.containsKey(responseId)) { @@ -194,16 +179,13 @@ abstract class ElectrumWalletBase final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); onUnspentResponse(response.result); break; + case ElectrumRequestMethods.estimateFeeMethod: + final response = ElectrumWorkerGetFeesResponse.fromJson(messageJson); + onFeesResponse(response.result); + break; } } - // Don't forget to clean up in the close method - // @override - // Future close({required bool shouldCleanup}) async { - // await _workerSubscription?.cancel(); - // await super.close(shouldCleanup: shouldCleanup); - // } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { if (seedBytes == null && xpub == null) { @@ -317,13 +299,30 @@ abstract class ElectrumWalletBase @observable TransactionPriorities? feeRates; - int feeRate(TransactionPriority priority) => feeRates![priority]; + + int feeRate(TransactionPriority priority) { + if (priority is ElectrumTransactionPriority && feeRates is BitcoinTransactionPriorities) { + final rates = feeRates as BitcoinTransactionPriorities; + + switch (priority) { + case ElectrumTransactionPriority.slow: + return rates.normal; + case ElectrumTransactionPriority.medium: + return rates.elevated; + case ElectrumTransactionPriority.fast: + return rates.priority; + case ElectrumTransactionPriority.custom: + return rates.custom; + } + } + + return feeRates![priority]; + } @observable List scripthashesListening; bool _chainTipListenerOn = false; - bool _isTransactionUpdating; bool _isInitialSync = true; void Function(FlutterErrorDetails)? _onError; @@ -361,13 +360,12 @@ abstract class ElectrumWalletBase // INFO: FOURTH: Finish with unspents await updateAllUnspents(); - _isInitialSync = false; - - // await updateFeeRates(); + await updateFeeRates(); - // _updateFeeRateTimer ??= - // Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); + _updateFeeRateTimer ??= + Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); + _isInitialSync = false; syncStatus = SyncedSyncStatus(); await save(); @@ -385,44 +383,18 @@ abstract class ElectrumWalletBase @action Future updateFeeRates() async { - try { - // feeRates = BitcoinElectrumTransactionPriorities.fromList( - // await electrumClient2!.getFeeRates(), - // ); - } catch (e, stacktrace) { - // _onError?.call(FlutterErrorDetails( - // exception: e, - // stack: stacktrace, - // library: this.runtimeType.toString(), - // )); - } + workerSendPort!.send( + ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: mempoolAPIEnabled).toJson(), + ); } - Node? node; - - Future getNodeIsElectrs() async { - return true; - 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 onFeesResponse(TransactionPriorities result) async { + feeRates = result; } + Node? node; + @action @override Future connectToNode({required Node node}) async { @@ -520,11 +492,14 @@ abstract class ElectrumWalletBase spendsSilentPayment = true; isSilentPayment = true; } else if (!isHardwareWallet) { - privkey = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: BitcoinAddressUtils.getAccountFromChange(utx.bitcoinAddressRecord.isChange), - index: utx.bitcoinAddressRecord.index, - ); + 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)); @@ -1110,7 +1085,7 @@ abstract class ElectrumWalletBase @override int calculateEstimatedFee(TransactionPriority? priority, int? amount, {int? outputsCount, int? size}) { - if (priority is BitcoinMempoolAPITransactionPriority) { + if (priority is BitcoinTransactionPriority) { return calculateEstimatedFeeWithFeeRate( feeRate(priority), amount, @@ -1478,11 +1453,13 @@ abstract class ElectrumWalletBase walletAddresses.allAddresses.firstWhere((element) => element.address == address); final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network); - final privkey = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: addressRecord.isChange ? 1 : 0, - index: addressRecord.index, - ); + 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); @@ -1694,10 +1671,7 @@ abstract class ElectrumWalletBase unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { if (unspentCoinInfo.isFrozen) { - // TODO: verify this works well totalFrozen += unspentCoinInfo.value; - totalConfirmed -= unspentCoinInfo.value; - totalUnconfirmed -= unspentCoinInfo.value; } }); @@ -1835,7 +1809,6 @@ abstract class ElectrumWalletBase if (syncStatus is ConnectingSyncStatus || isDisconnectedStatus) { // Needs to re-subscribe to all scripthashes when reconnected scripthashesListening = []; - _isTransactionUpdating = false; _chainTipListenerOn = false; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 44e3be7f9e..789a0e4913 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -605,6 +605,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future generateInitialAddresses({required BitcoinAddressType type}) async { for (final derivationType in hdWallets.keys) { + if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) { + continue; + } + final derivationInfo = BitcoinAddressUtils.getDerivationFromType( type, isElectrum: derivationType == CWBitcoinDerivationType.electrum, diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 102d6c313e..67ded289d5 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -5,6 +5,7 @@ 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'; @@ -21,6 +22,7 @@ import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; + BasedUtxoNetwork? _network; ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -100,6 +102,16 @@ class ElectrumWorker { ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), ); break; + case ElectrumRequestMethods.estimateFeeMethod: + await _handleGetFeeRates( + ElectrumWorkerGetFeesRequest.fromJson(messageJson), + ); + break; + case ElectrumRequestMethods.versionMethod: + await _handleGetVersion( + ElectrumWorkerGetVersionRequest.fromJson(messageJson), + ); + break; } } catch (e, s) { print(s); @@ -108,6 +120,8 @@ class ElectrumWorker { } Future _handleConnect(ElectrumWorkerConnectionRequest request) async { + _network = request.network; + _electrumClient = await ElectrumApiProvider.connect( ElectrumTCPService.connect( request.uri, @@ -415,6 +429,56 @@ class ElectrumWorker { ); } + Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { + if (request.mempoolAPIEnabled) { + try { + final recommendedFees = await ApiProvider.fromMempool( + _network!, + baseUrl: "http://mempool.cakewallet.com:8999/api", + ).getRecommendedFeeRate(); + + final unimportantFee = recommendedFees.economyFee!.satoshis; + final normalFee = recommendedFees.low.satoshis; + int elevatedFee = recommendedFees.medium.satoshis; + int priorityFee = recommendedFees.high.satoshis; + + // Bitcoin only: adjust fee rates to avoid equal fee values + // elevated fee should be higher than normal fee + if (normalFee == elevatedFee) { + elevatedFee++; + } + // priority fee should be higher than elevated fee + while (priorityFee <= elevatedFee) { + priorityFee++; + } + // this guarantees that, even if all fees are low and equal, + // higher priority fee txs can be consumed when chain fees start surging + + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: BitcoinTransactionPriorities( + unimportant: unimportantFee, + normal: normalFee, + elevated: elevatedFee, + priority: priorityFee, + custom: unimportantFee, + ), + ), + ); + } catch (e) { + _sendError(ElectrumWorkerGetFeesError(error: e.toString())); + } + } else { + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: ElectrumTransactionPriorities.fromList( + await _electrumClient!.getFeeRates(), + ), + ), + ); + } + } + Future _handleScanSilentPayments(ElectrumWorkerTweaksSubscribeRequest request) async { final scanData = request.scanData; int syncHeight = scanData.height; @@ -446,7 +510,6 @@ class ElectrumWorker { ), )); - print([syncHeight, initialCount]); final listener = await _electrumClient!.subscribe( ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), ); @@ -578,12 +641,17 @@ class ElectrumWorker { } listener?.call(listenFn); + } - // if (tweaksSubscription == null) { - // return scanData.sendPort.send( - // SyncResponse(syncHeight, UnsupportedSyncStatus()), - // ); - // } + Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { + _sendResponse(ElectrumWorkerGetVersionResponse( + result: (await _electrumClient!.request( + ElectrumVersion( + clientName: "", + protocolVersion: ["1.4"], + ), + )), + id: request.id)); } } 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..be81e53469 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -0,0 +1,60 @@ +part of 'methods.dart'; + +class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { + ElectrumWorkerGetFeesRequest({ + required this.mempoolAPIEnabled, + this.id, + }); + + final bool mempoolAPIEnabled; + final int? id; + + @override + final String method = ElectrumRequestMethods.estimateFee.method; + + @override + factory ElectrumWorkerGetFeesRequest.fromJson(Map json) { + return ElectrumWorkerGetFeesRequest( + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + ); + } + + @override + Map toJson() { + return {'method': method, 'mempoolAPIEnabled': mempoolAPIEnabled}; + } +} + +class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse { + ElectrumWorkerGetFeesError({ + required super.error, + super.id, + }) : super(); + + @override + String get method => ElectrumRequestMethods.estimateFee.method; +} + +class ElectrumWorkerGetFeesResponse + extends ElectrumWorkerResponse> { + ElectrumWorkerGetFeesResponse({ + required super.result, + super.error, + super.id, + }) : super(method: ElectrumRequestMethods.estimateFee.method); + + @override + Map resultJson(result) { + return result.toJson(); + } + + @override + factory ElectrumWorkerGetFeesResponse.fromJson(Map json) { + return ElectrumWorkerGetFeesResponse( + result: deserializeTransactionPriorities(json['result'] as Map), + error: json['error'] as String?, + id: json['id'] as int?, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 6ace715d0a..295522d39a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -6,6 +6,8 @@ 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'; @@ -16,3 +18,5 @@ part 'get_tx_expanded.dart'; part 'broadcast.dart'; part 'list_unspent.dart'; part 'tweaks_subscribe.dart'; +part 'get_fees.dart'; +part 'version.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/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_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 277864af78..8158907572 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -877,13 +877,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @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; } } @@ -1036,11 +1036,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { witnesses: tx2.inputs.asMap().entries.map((e) { final utxo = unspentCoins .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); - final key = ECPrivate.fromBip32( - bip32: walletAddresses.bip32, - account: BitcoinAddressUtils.getAccountFromChange(utxo.bitcoinAddressRecord.isChange), - index: utxo.bitcoinAddressRecord.index, - ); + 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(), diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 0045801a74..0019d32c69 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -209,13 +209,13 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override int feeRate(TransactionPriority priority) { - if (priority is BitcoinCashTransactionPriority) { + if (priority is ElectrumTransactionPriority) { switch (priority) { - case BitcoinCashTransactionPriority.slow: + case ElectrumTransactionPriority.slow: return 1; - case BitcoinCashTransactionPriority.medium: + case ElectrumTransactionPriority.medium: return 5; - case BitcoinCashTransactionPriority.fast: + case ElectrumTransactionPriority.fast: return 10; } } diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index 5eb5576f3f..35282f49e4 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -13,4 +13,8 @@ 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/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 1461c18433..d4bc6799be 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -52,7 +52,7 @@ class CWBitcoin extends Bitcoin { name: name, hwAccountData: accountData, walletInfo: walletInfo); @override - TransactionPriority getMediumTransactionPriority() => BitcoinElectrumTransactionPriority.elevated; + TransactionPriority getMediumTransactionPriority() => ElectrumTransactionPriority.medium; @override List getWordList() => wordlist; @@ -70,18 +70,18 @@ class CWBitcoin extends Bitcoin { } @override - List getTransactionPriorities() => BitcoinElectrumTransactionPriority.all; + List getTransactionPriorities() => ElectrumTransactionPriority.all; @override - List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; + List getLitecoinTransactionPriorities() => ElectrumTransactionPriority.all; @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => - BitcoinElectrumTransactionPriority.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) { @@ -111,7 +111,7 @@ class CWBitcoin extends Bitcoin { UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { final bitcoinFeeRate = - priority == BitcoinElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; + priority == ElectrumTransactionPriority.custom && feeRate != null ? feeRate : null; return BitcoinTransactionCredentials( outputs .map((out) => OutputInfo( @@ -125,7 +125,7 @@ class CWBitcoin extends Bitcoin { formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo)) .toList(), - priority: priority as BitcoinElectrumTransactionPriority, + priority: priority as ElectrumTransactionPriority, feeRate: bitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -165,12 +165,7 @@ class CWBitcoin extends Bitcoin { final p2shAddr = sk.getPublic().toP2pkhInP2sh(); final estimatedTx = await electrumWallet.estimateSendAllTx( [BitcoinOutput(address: p2shAddr, value: BigInt.zero)], - getFeeRate( - wallet, - wallet.type == WalletType.litecoin - ? priority as LitecoinTransactionPriority - : priority as BitcoinElectrumTransactionPriority, - ), + getFeeRate(wallet, priority), ); return estimatedTx.amount; @@ -200,7 +195,7 @@ class CWBitcoin extends Bitcoin { @override String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}) => - (priority as BitcoinElectrumTransactionPriority).labelWithRate(rate, customRate); + (priority as ElectrumTransactionPriority).labelWithRate(rate, customRate); @override List getUnspents(Object wallet, @@ -256,22 +251,19 @@ class CWBitcoin extends Bitcoin { } @override - TransactionPriority getBitcoinTransactionPriorityMedium() => - BitcoinElectrumTransactionPriority.elevated; + TransactionPriority getBitcoinTransactionPriorityMedium() => ElectrumTransactionPriority.fast; @override - TransactionPriority getBitcoinTransactionPriorityCustom() => - BitcoinElectrumTransactionPriority.custom; + TransactionPriority getBitcoinTransactionPriorityCustom() => ElectrumTransactionPriority.custom; @override - TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium; + TransactionPriority getLitecoinTransactionPriorityMedium() => ElectrumTransactionPriority.medium; @override - TransactionPriority getBitcoinTransactionPrioritySlow() => - BitcoinElectrumTransactionPriority.normal; + TransactionPriority getBitcoinTransactionPrioritySlow() => ElectrumTransactionPriority.medium; @override - TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; + TransactionPriority getLitecoinTransactionPrioritySlow() => ElectrumTransactionPriority.slow; @override Future setAddressType(Object wallet, dynamic option) async { @@ -436,7 +428,7 @@ class CWBitcoin extends Bitcoin { {int? size}) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.feeAmountForPriority( - priority as BitcoinElectrumTransactionPriority, inputsCount, outputsCount); + priority as ElectrumTransactionPriority, inputsCount, outputsCount); } @override @@ -460,8 +452,13 @@ class CWBitcoin extends Bitcoin { @override int getMaxCustomFeeRate(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; - return (bitcoinWallet.feeRate(BitcoinElectrumTransactionPriority.priority) * 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 diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index a0cb406c2d..0a91313490 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -48,15 +48,14 @@ class CWBitcoinCash extends BitcoinCash { @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/store/settings_store.dart b/lib/store/settings_store.dart index bcea80a540..cd39318f4c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -602,8 +602,7 @@ abstract class SettingsStoreBase with Store { static const defaultActionsMode = 11; static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenMinutes; static const defaultAutoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.initialized; - // static final walletPasswordDirectInput = Platform.isLinux; - static final walletPasswordDirectInput = false; + static final walletPasswordDirectInput = Platform.isLinux; static const defaultSeedPhraseLength = SeedPhraseLength.twelveWords; static const defaultMoneroSeedType = MoneroSeedType.defaultSeedType; static const defaultBitcoinSeedType = BitcoinSeedType.defaultDerivationType; From 28804b8ff2cf628b13e98a032c37105f052625d0 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Wed, 6 Nov 2024 18:23:05 +0200 Subject: [PATCH 13/64] Improve sending tx for electrum (#1790) * Enhance the code for sending/sending-ALL for Electrum * remove print statements [skip ci] * update bitcoin base and minor reformatting --- cw_bitcoin/lib/electrum_wallet.dart | 157 +++++++++-------------- ios/Podfile.lock | 18 +-- lib/view_model/send/send_view_model.dart | 18 +-- 3 files changed, 77 insertions(+), 116 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 47d69d6700..fceaf5d127 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -439,8 +439,8 @@ abstract class ElectrumWalletBase TxCreateUtxoDetails _createUTXOS({ required bool sendAll, - required int credentialsAmount, required bool paysToSilentPayment, + int credentialsAmount = 0, int? inputsCount, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { @@ -574,13 +574,11 @@ abstract class ElectrumWalletBase List outputs, int feeRate, { String? memo, - int credentialsAmount = 0, bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = _createUTXOS( sendAll: true, - credentialsAmount: credentialsAmount, paysToSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -603,23 +601,11 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); } - if (amount <= 0) { - throw BitcoinTransactionWrongBalanceException(); - } - // Attempting to send less than the dust limit if (_isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); } - if (credentialsAmount > 0) { - final amountLeftForFee = amount - credentialsAmount; - if (amountLeftForFee > 0 && _isBelowDust(amountLeftForFee)) { - amount -= amountLeftForFee; - fee += amountLeftForFee; - } - } - if (outputs.length == 1) { outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } @@ -649,6 +635,11 @@ abstract class ElectrumWalletBase bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { + // Attempting to send less than the dust limit + if (_isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + final utxoDetails = _createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, @@ -726,7 +717,43 @@ abstract class ElectrumWalletBase final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - if (!_isBelowDust(amountLeftForChange)) { + if (_isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + updatedOutputs.removeLast(); + outputs.removeLast(); + + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } else { // Here, lastOutput already is change, return the amount left without the fee to the user's address. updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( address: lastOutput.address, @@ -740,88 +767,21 @@ abstract class ElectrumWalletBase isSilentPayment: lastOutput.isSilentPayment, isChange: true, ); - } else { - // If has change that is lower than dust, will end up with tx rejected by network rules, so estimate again without the added change - updatedOutputs.removeLast(); - outputs.removeLast(); - - // Still has inputs to spend before failing - if (!spendingAllCoins) { - return estimateTxForAmount( - credentialsAmount, - outputs, - updatedOutputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - hasSilentPayment: hasSilentPayment, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); - } - final estimatedSendAll = await estimateSendAllTx( - updatedOutputs, - feeRate, + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); - - if (estimatedSendAll.amount == credentialsAmount) { - return estimatedSendAll; - } - - // Estimate to user how much is needed to send to cover the fee - final maxAmountWithReturningChange = utxoDetails.allInputsAmount - _dustAmount - fee - 1; - throw BitcoinTransactionNoDustOnChangeException( - BitcoinAmountUtils.bitcoinAmountToString(amount: maxAmountWithReturningChange), - BitcoinAmountUtils.bitcoinAmountToString(amount: estimatedSendAll.amount), - ); - } - - // Attempting to send less than the dust limit - if (_isBelowDust(amount)) { - throw BitcoinTransactionNoDustException(); - } - - final totalAmount = amount + fee; - - if (totalAmount > (balance[currency]!.confirmed + balance[currency]!.secondConfirmed)) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (totalAmount > utxoDetails.allInputsAmount) { - if (spendingAllCoins) { - throw BitcoinTransactionWrongBalanceException(); - } else { - updatedOutputs.removeLast(); - outputs.removeLast(); - return estimateTxForAmount( - credentialsAmount, - outputs, - updatedOutputs, - feeRate, - inputsCount: utxoDetails.utxos.length + 1, - memo: memo, - useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - hasSilentPayment: hasSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); - } } - - return EstimatedTxResult( - utxos: utxoDetails.utxos, - inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, - publicKeys: utxoDetails.publicKeys, - fee: fee, - amount: amount, - hasChange: true, - isSendAll: false, - memo: memo, - spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, - ); } Future calcFee({ @@ -895,15 +855,20 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; - final updatedOutputs = - outputs.map((e) => BitcoinOutput(address: e.address, value: e.value)).toList(); + final updatedOutputs = outputs + .map((e) => BitcoinOutput( + address: e.address, + value: e.value, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(); if (sendAll) { estimatedTx = await estimateSendAllTx( updatedOutputs, feeRateInt, memo: memo, - credentialsAmount: credentialsAmount, hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 322ef6f86f..a254446ee1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -109,12 +109,7 @@ PODS: - FlutterMacOS - permission_handler_apple (9.1.1): - Flutter - - Protobuf (3.27.2) - ReachabilitySwift (5.2.3) - - reactive_ble_mobile (0.0.1): - - Flutter - - Protobuf (~> 3.5) - - SwiftProtobuf (~> 1.0) - SDWebImage (5.19.4): - SDWebImage/Core (= 5.19.4) - SDWebImage/Core (5.19.4) @@ -132,6 +127,9 @@ PODS: - Toast (4.1.1) - uni_links (0.0.1): - Flutter + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -161,12 +159,12 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) + - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - workmanager (from `.symlinks/plugins/workmanager/ios`) @@ -178,7 +176,6 @@ SPEC REPOS: - DKPhotoGallery - MTBBarcodeScanner - OrderedSet - - Protobuf - ReachabilitySwift - SDWebImage - SwiftProtobuf @@ -226,8 +223,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - reactive_ble_mobile: - :path: ".symlinks/plugins/reactive_ble_mobile/ios" sensitive_clipboard: :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: @@ -238,6 +233,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" + universal_ble: + :path: ".symlinks/plugins/universal_ble/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: @@ -271,9 +268,7 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: fb2c13674723f76ff6eede14f78847a776455fa2 ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 - reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad @@ -283,6 +278,7 @@ SPEC CHECKSUMS: SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1403eb1095..6ed9249c9e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -128,7 +128,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; @@ -416,7 +416,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; @@ -482,18 +482,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(); From 57f486025e5dfa8d4b0255b91eeb7c3054341770 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 7 Nov 2024 13:01:32 -0300 Subject: [PATCH 14/64] fix: tx dates --- cw_bitcoin/lib/bitcoin_address_record.dart | 23 +- .../lib/bitcoin_hardware_wallet_service.dart | 12 +- cw_bitcoin/lib/bitcoin_wallet.dart | 39 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 216 ++++++++++- cw_bitcoin/lib/electrum_transaction_info.dart | 103 ++++-- cw_bitcoin/lib/electrum_wallet.dart | 110 +++--- cw_bitcoin/lib/electrum_wallet_addresses.dart | 349 +++++------------- .../lib/electrum_worker/electrum_worker.dart | 95 ++--- .../methods/get_tx_expanded.dart | 10 +- cw_bitcoin/lib/litecoin_wallet.dart | 4 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 15 +- .../lib/pending_bitcoin_transaction.dart | 27 +- .../lib/src/bitcoin_cash_wallet.dart | 6 +- .../src/bitcoin_cash_wallet_addresses.dart | 4 +- cw_core/lib/transaction_info.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 35 +- scripts/android/app_env.fish | 78 ---- tool/configure.dart | 4 +- 18 files changed, 557 insertions(+), 575 deletions(-) delete mode 100644 scripts/android/app_env.fish diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index d4dd8319fa..6c8fa82f6e 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -12,7 +12,7 @@ abstract class BaseBitcoinAddressRecord { int balance = 0, String name = '', bool isUsed = false, - required this.type, + required this.addressType, bool? isHidden, }) : _txCount = txCount, _balance = balance, @@ -49,7 +49,6 @@ abstract class BaseBitcoinAddressRecord { void setAsUsed() { _isUsed = true; - // TODO: check is hidden flow on addr list _isHidden = true; } @@ -57,7 +56,7 @@ abstract class BaseBitcoinAddressRecord { int get hashCode => address.hashCode; - BitcoinAddressType type; + BitcoinAddressType addressType; String toJSON(); } @@ -77,7 +76,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { super.balance = 0, super.name = '', super.isUsed = false, - required super.type, + required super.addressType, String? scriptHash, BasedUtxoNetwork? network, }) { @@ -104,7 +103,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 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, @@ -126,7 +125,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 'txCount': txCount, 'name': name, 'balance': balance, - 'type': type.toString(), + 'type': addressType.toString(), 'scriptHash': scriptHash, }); @@ -139,7 +138,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { other.index == index && other.derivationInfo == derivationInfo && other.scriptHash == scriptHash && - other.type == type && + other.addressType == addressType && other.derivationType == derivationType; } @@ -149,7 +148,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { index.hashCode ^ derivationInfo.hashCode ^ scriptHash.hashCode ^ - type.hashCode ^ + addressType.hashCode ^ derivationType.hashCode; } @@ -166,7 +165,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { super.balance = 0, super.name = '', super.isUsed = false, - super.type = SilentPaymentsAddresType.p2sp, + super.addressType = SilentPaymentsAddresType.p2sp, super.isHidden, this.labelHex, }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) { @@ -198,7 +197,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { 'txCount': txCount, 'name': name, 'balance': balance, - 'type': type.toString(), + 'type': addressType.toString(), 'labelHex': labelHex, }); } @@ -214,7 +213,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.name = '', super.isUsed = false, required this.spendKey, - super.type = SegwitAddresType.p2tr, + super.addressType = SegwitAddresType.p2tr, super.labelHex, }) : super(isHidden: true); @@ -241,7 +240,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { 'txCount': txCount, 'name': name, 'balance': balance, - 'type': type.toString(), + 'type': addressType.toString(), 'labelHex': labelHex, 'spend_key': spendKey.toString(), }); diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index 415ae0e987..15ab4f6c13 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -22,13 +22,13 @@ class BitcoinHardwareWalletService { for (final i in indexRange) { final derivationPath = "m/84'/0'/$i'"; final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); - final bip32 = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(Bip32KeyIndex(0)); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub) + .childKey(Bip32KeyIndex(0)) + .childKey(Bip32KeyIndex(index)); - final fullPath = Bip32PathParser.parse(derivationPath).addElem(Bip32KeyIndex(0)); - - final address = ECPublic.fromBip32(bip32.derive(fullPath).publicKey) - .toP2wpkhAddress() - .toAddress(BitcoinNetwork.mainnet); + final address = ECPublic.fromBip32( + hd.publicKey, + ).toP2wpkhAddress().toAddress(BitcoinNetwork.mainnet); accounts.add(HardwareAccountData( address: address, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 748acdbbe3..7b68397b9c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1,13 +1,12 @@ 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/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_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'; @@ -18,27 +17,24 @@ 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/get_height_by_date.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/foundation.dart'; +// import 'package:cw_core/wallet_type.dart'; +// import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; -import 'package:sp_scanner/sp_scanner.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { - StreamSubscription? _receiveStream; - BitcoinWalletBase({ required String password, required WalletInfo walletInfo, @@ -83,10 +79,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletAddresses = BitcoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, network: networkParam ?? network, isHardwareWallet: walletInfo.isHardwareWallet, hdWallets: hdWallets, @@ -544,18 +537,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @action void _updateSilentAddressRecord(BitcoinUnspent unspent) { final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; - final silentAddress = walletAddresses.silentAddress!; + final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; + final silentPaymentWallet = walletAddresses.silentPaymentWallet; final silentPaymentAddress = SilentPaymentAddress( - version: silentAddress.version, - B_scan: silentAddress.B_scan, + version: silentPaymentWallet.version, + B_scan: silentPaymentWallet.B_scan, B_spend: receiveAddressRecord.labelHex != null - ? silentAddress.B_spend.tweakAdd( + ? silentPaymentWallet.B_spend.tweakAdd( BigintUtils.fromBytes(BytesUtils.fromHexString(receiveAddressRecord.labelHex!)), ) - : silentAddress.B_spend, + : silentPaymentWallet.B_spend, ); - final addressRecord = walletAddresses.silentAddresses + final addressRecord = walletAddresses.silentPaymentAddresses .firstWhere((address) => address.address == silentPaymentAddress.toString()); addressRecord.txCount += 1; addressRecord.balance += unspent.value; @@ -677,17 +671,19 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { syncStatus = AttemptingScanSyncStatus(); + final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; workerSendPort!.send( ElectrumWorkerTweaksSubscribeRequest( scanData: ScanData( - silentAddress: walletAddresses.silentAddress!, + silentAddress: walletAddresses.silentPaymentWallet, network: network, height: height, chainTip: chainTip, transactionHistoryIds: transactionHistory.transactions.keys.toList(), labels: walletAddresses.labels, - labelIndexes: walletAddresses.silentAddresses - .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + labelIndexes: walletAddresses.silentPaymentAddresses + .where((addr) => + addr.addressType == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, @@ -795,9 +791,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - static String _hardenedDerivationPath(String derivationPath) => - derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); - @override @action void syncStatusReaction(SyncStatus syncStatus) { diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index c5419a6f0f..1776078620 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,5 +1,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/electrum_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -15,26 +16,97 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.isHardwareWallet, required super.hdWallets, super.initialAddresses, - super.initialRegularAddressIndex, - super.initialChangeAddressIndex, - super.initialSilentAddresses, - super.initialSilentAddressIndex = 0, - }) : super(walletInfo); + List? initialSilentAddresses, + List? initialReceivedSPAddresses, + }) : silentPaymentAddresses = ObservableList.of( + (initialSilentAddresses ?? []).toSet(), + ), + receivedSPAddresses = ObservableList.of( + (initialReceivedSPAddresses ?? []).toSet(), + ), + super(walletInfo) { + silentPaymentWallet = SilentPaymentOwner.fromBip32(hdWallet); + } + + @observable + late final SilentPaymentOwner silentPaymentWallet; + final ObservableList silentPaymentAddresses; + final ObservableList receivedSPAddresses; + + @observable + String? activeSilentAddress; @override Future init() async { - await generateInitialAddresses(type: SegwitAddresType.p2wpkh); + 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); + } + + if (silentPaymentAddresses.length == 0) { + silentPaymentAddresses.add(BitcoinSilentPaymentAddressRecord( + silentPaymentWallet.toString(), + labelIndex: 1, + name: "", + addressType: SilentPaymentsAddresType.p2sp, + )); + silentPaymentAddresses.add(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, @@ -108,4 +180,128 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S 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 + @action + void addBitcoinAddressTypes() { + super.addBitcoinAddressTypes(); + + silentPaymentAddresses.forEach((addressRecord) { + if (addressRecord.addressType != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { + return; + } + + 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 (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 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; + } + + 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/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index f751205319..20ee668265 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -15,11 +15,13 @@ class ElectrumTransactionBundle { required this.ins, required this.confirmations, this.time, + this.dateValidated, }); final BtcTransaction originalTransaction; final List ins; final int? time; + final bool? dateValidated; final int confirmations; Map toJson() { @@ -37,6 +39,7 @@ class ElectrumTransactionBundle { ins: (data['ins'] as List).map((e) => BtcTransaction.fromRaw(e as String)).toList(), confirmations: data['confirmations'] as int, time: data['time'] as int?, + dateValidated: data['dateValidated'] as bool?, ); } } @@ -44,6 +47,7 @@ class ElectrumTransactionBundle { class ElectrumTransactionInfo extends TransactionInfo { List? unspents; bool isReceivedSilentPayment; + int? time; ElectrumTransactionInfo( this.type, { @@ -57,6 +61,8 @@ class ElectrumTransactionInfo extends TransactionInfo { required bool isPending, bool isReplaced = false, required DateTime date, + required int? time, + bool? dateValidated, required int confirmations, String? to, this.unspents, @@ -70,9 +76,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.dateValidated = dateValidated; this.to = to; } @@ -82,9 +90,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; @@ -118,21 +125,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(); @@ -205,18 +219,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, + dateValidated: bundle.dateValidated, + ); } factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { @@ -244,6 +262,8 @@ class ElectrumTransactionInfo extends TransactionInfo { .map((unspent) => BitcoinUnspent.fromJSON(null, unspent as Map)) .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, + time: data['time'] as int?, + dateValidated: data['dateValidated'] as bool?, ); } @@ -267,18 +287,21 @@ 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); + 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, + time: info.time, + ); } Map toJson() { @@ -288,6 +311,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; @@ -297,6 +321,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; + m['dateValidated'] = dateValidated; return m; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 47d69d6700..d45a3f9d66 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -216,7 +216,7 @@ abstract class ElectrumWalletBase bool mempoolAPIEnabled; final Map hdWallets; - Bip32Slip10Secp256k1 get bip32 => walletAddresses.bip32; + Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; final String? _mnemonic; final EncryptionFileUtils encryptionFileUtils; @@ -243,7 +243,7 @@ abstract class ElectrumWalletBase SyncStatus syncStatus; List get addressesSet => walletAddresses.allAddresses - .where((element) => element.type != SegwitAddresType.mweb) + .where((element) => element.addressType != SegwitAddresType.mweb) .map((addr) => addr.address) .toList(); @@ -348,20 +348,20 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); - // INFO: FIRST: Call subscribe for headers, get the initial chainTip update in case it is zero + // INFO: FIRST: Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); - // INFO: SECOND: Start loading transaction histories for every address, this will help discover addresses until the unused gap limit has been reached, which will help finding the full balance and unspents later. + // 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(); - // INFO: THIRD: Start loading the TX history + // INFO: THIRD: Get the full wallet's balance with all addresses considered await updateBalance(); - // INFO: FOURTH: Finish with unspents + // 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(seconds: 5), (timer) async => await updateFeeRates()); @@ -460,9 +460,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; } @@ -475,7 +475,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(); } } @@ -514,7 +514,7 @@ abstract class ElectrumWalletBase pubKeyHex = privkey.getPublic().toHex(); } else { - pubKeyHex = walletAddresses.bip32 + pubKeyHex = walletAddresses.hdWallet .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)) .publicKey .toHex(); @@ -1060,18 +1060,13 @@ 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, }); @@ -1337,33 +1332,59 @@ abstract class ElectrumWalletBase return; } - final firstAddress = histories.first; - final isChange = firstAddress.addressRecord.isChange; - final type = firstAddress.addressRecord.type; - - final totalAddresses = (isChange - ? walletAddresses.receiveAddresses.where((element) => element.type == type).length - : walletAddresses.changeAddresses.where((element) => element.type == type).length); - final gapLimit = (isChange - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - - bool hasUsedAddressesUnderGap = false; final addressesWithHistory = []; + BitcoinAddressType? lastDiscoveredType; for (final addressHistory in histories) { final txs = addressHistory.txs; if (txs.isNotEmpty) { - final address = addressHistory.addressRecord; - addressesWithHistory.add(address); + 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; - hasUsedAddressesUnderGap = - address.index < totalAddresses && (address.index >= totalAddresses - gapLimit); + 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), + ); + + 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); + } + } } } @@ -1371,20 +1392,7 @@ abstract class ElectrumWalletBase walletAddresses.updateAdresses(addressesWithHistory); } - if (hasUsedAddressesUnderGap) { - // Discover new addresses for the same address type until the gap limit is respected - final newAddresses = await walletAddresses.discoverAddresses( - isChange: isChange, - derivationType: firstAddress.addressRecord.derivationType, - type: type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(type), - ); - - if (newAddresses.isNotEmpty) { - // Update the transactions for the new discovered addresses - await updateTransactions(newAddresses); - } - } + walletAddresses.updateHiddenAddresses(); } Future canReplaceByFee(ElectrumTransactionInfo tx) async { @@ -1597,8 +1605,12 @@ abstract class ElectrumWalletBase Future getTransactionExpanded({required String hash}) async { return await sendWorker( - ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!)) - as ElectrumTransactionBundle; + ElectrumWorkerTxExpandedRequest( + txHash: hash, + currentChainTip: currentChainTip!, + mempoolAPIEnabled: mempoolAPIEnabled, + ), + ) as ElectrumTransactionBundle; } Future fetchTransactionInfo({required String hash, int? height}) async { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 789a0e4913..0acc450f9a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -39,10 +39,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.network, required this.isHardwareWallet, List? initialAddresses, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, - List? initialSilentAddresses, - int initialSilentAddressIndex = 0, List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, }) : _allAddresses = ObservableList.of(initialAddresses ?? []), @@ -53,37 +49,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type changeAddresses = ObservableList.of( (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), - currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, - currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), - silentAddresses = ObservableList.of( - (initialSilentAddresses ?? []).toSet()), - currentSilentAddressIndex = initialSilentAddressIndex, mwebAddresses = ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { - // TODO: initial silent address, not every time - silentAddress = SilentPaymentOwner.fromBip32(bip32); - - if (silentAddresses.length == 0) { - silentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentAddress.toString(), - labelIndex: 1, - name: "", - type: SilentPaymentsAddresType.p2sp, - )); - silentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentAddress!.toLabeledSilentPaymentAddress(0).toString(), - name: "", - labelIndex: 0, - labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(0)), - type: SilentPaymentsAddresType.p2sp, - )); - } - updateAddressesByMatch(); } @@ -95,30 +67,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { 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 Map hdWallets; - Bip32Slip10Secp256k1 get bip32 => + Bip32Slip10Secp256k1 get hdWallet => hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; final bool isHardwareWallet; - @observable - SilentPaymentOwner? silentAddress; - @observable late BitcoinAddressType _addressPageType; @computed BitcoinAddressType get addressPageType => _addressPageType; - @observable - String? activeSilentAddress; - @computed List get allAddresses => _allAddresses.toList(); @@ -133,14 +97,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override @computed String get address { - if (addressPageType == SilentPaymentsAddresType.p2sp) { - if (activeSilentAddress != null) { - return activeSilentAddress!; - } - - return silentAddress.toString(); - } - String receiveAddress; final typeMatchingReceiveAddresses = @@ -151,7 +107,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) { @@ -169,25 +125,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { - if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { - return; - } - if (addressPageType == SilentPaymentsAddresType.p2sp) { - late BitcoinSilentPaymentAddressRecord selected; - try { - selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); - } catch (_) { - selected = silentAddresses[0]; - } - - if (selected.labelHex != null && silentAddress != null) { - activeSilentAddress = - silentAddress!.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); - } else { - activeSilentAddress = silentAddress!.toString(); - } - return; - } try { final addressRecord = _allAddresses.firstWhere( (addressRecord) => addressRecord.address == addr, @@ -202,24 +139,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override String get primaryAddress => _allAddresses.first.address; - 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; - @observable BitcoinAddressRecord? previousAddressRecord; @@ -242,19 +161,19 @@ 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(type: SegwitAddresType.p2wpkh); + 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); } } @@ -262,14 +181,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateReceiveAddresses(); updateChangeAddresses(); await updateAddressesInBox(); - - if (currentReceiveAddressIndex >= receiveAddresses.length) { - currentReceiveAddressIndex = 0; - } - - if (currentChangeAddressIndex >= changeAddresses.length) { - currentChangeAddressIndex = 0; - } } @action @@ -280,57 +191,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }) async { updateChangeAddresses(); - 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.labelHex; - - 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(), - labelIndex: currentSilentAddressIndex, - name: label, - labelHex: BytesUtils.toHexString(silentAddress!.generateLabel(currentSilentAddressIndex)), - type: SilentPaymentsAddresType.p2sp, - ); - - silentAddresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesByMatch()); - - return address; - } - final newAddressIndex = addressesByReceiveType.fold( 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); @@ -346,7 +215,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { index: newAddressIndex, isChange: false, name: label, - type: addressPageType, + addressType: addressPageType, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), derivationType: CWBitcoinDerivationType.bip39, @@ -405,56 +274,42 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { .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 = _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 = _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 = _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 = _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.isChange) { - return; - } - - if (addressRecord.address != address) { - addressesMap[addressRecord.address] = addressRecord.name.isEmpty - ? "Silent Payments" - : "Silent Payments - " + addressRecord.name; - } else { - addressesMap[address] = 'Active - Silent Payments'; - } - }); } @action @@ -465,17 +320,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { .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 = _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'; } } @@ -484,9 +339,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { 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'; } } @@ -495,7 +350,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Future updateAddressesInBox() async { try { addressesMap.clear(); - addressesMap[address] = 'Active'; + addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address'; allAddressesMap.clear(); _allAddresses.forEach((addressRecord) { @@ -530,11 +385,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); - silentAddresses.forEach((addressRecord) { - if (addressRecord.address == address) { - foundAddress = addressRecord; - } - }); mwebAddresses.forEach((addressRecord) { if (addressRecord.address == address) { foundAddress = addressRecord; @@ -543,23 +393,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (foundAddress != null) { foundAddress!.setNewName(label); - - if (foundAddress is! BitcoinAddressRecord) { - 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(_allAddresses.where(_isAddressPageTypeMatch).toList()); } @@ -576,93 +414,80 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { changeAddresses.removeRange(0, changeAddresses.length); final newAddresses = _allAddresses.where((addressRecord) => addressRecord.isChange && - (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); + (walletInfo.type != WalletType.bitcoin || + addressRecord.addressType == SegwitAddresType.p2wpkh)); changeAddresses.addAll(newAddresses); } @action - Future> discoverAddresses({ + Future> discoverNewAddresses({ required CWBitcoinDerivationType derivationType, required bool isChange, - required BitcoinAddressType type, + required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) async { - final gap = (isChange + final count = (isChange ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); - final newAddresses = await _createNewAddresses( - derivationType: derivationType, - gap, - isChange: isChange, - type: type, - derivationInfo: derivationInfo, - ); + final startIndex = (isChange ? receiveAddresses : changeAddresses) + .where((addr) => addr.derivationType == derivationType && addr.addressType == addressType) + .length; + + final newAddresses = []; + 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: derivationType == CWBitcoinDerivationType.old, + addressType: addressType, + network: network, + derivationInfo: derivationInfo, + derivationType: derivationType, + ); + newAddresses.add(address); + } + addAddresses(newAddresses); return newAddresses; } @action - Future generateInitialAddresses({required BitcoinAddressType type}) async { + Future generateInitialAddresses({required BitcoinAddressType addressType}) async { + if (_allAddresses.where((addr) => addr.addressType == addressType).isNotEmpty) { + return; + } + for (final derivationType in hdWallets.keys) { - if (derivationType == CWBitcoinDerivationType.old && type == SegwitAddresType.p2wpkh) { + if (derivationType == CWBitcoinDerivationType.old && addressType == SegwitAddresType.p2wpkh) { continue; } final derivationInfo = BitcoinAddressUtils.getDerivationFromType( - type, + addressType, isElectrum: derivationType == CWBitcoinDerivationType.electrum, ); - await discoverAddresses( + await discoverNewAddresses( derivationType: derivationType, isChange: false, - type: type, + addressType: addressType, derivationInfo: derivationInfo, ); - await discoverAddresses( + await discoverNewAddresses( derivationType: derivationType, isChange: true, - type: type, - derivationInfo: derivationInfo, - ); - } - } - - @action - Future> _createNewAddresses( - int count, { - required CWBitcoinDerivationType derivationType, - required BitcoinDerivationInfo derivationInfo, - bool isChange = false, - BitcoinAddressType? type, - }) async { - final list = []; - final startIndex = (isChange ? receiveAddresses : changeAddresses) - .where((addr) => addr.derivationType == derivationType && addr.type == type) - .length; - - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord( - await getAddressAsync( - derivationType: derivationType, - isChange: isChange, - index: i, - addressType: type ?? addressPageType, - derivationInfo: derivationInfo, - ), - index: i, - isChange: isChange, - isHidden: derivationType == CWBitcoinDerivationType.old && type != SegwitAddresType.p2wpkh, - type: type ?? addressPageType, - network: network, + addressType: addressType, derivationInfo: derivationInfo, - derivationType: derivationType, ); - list.add(address); } - - return list; } @action @@ -690,12 +515,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @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 @@ -719,18 +543,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => + addr.addressType == type; bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { - return !addr.isChange && !addr.isUsed && addr.type == type; + return !addr.isChange && !addr.isUsed && addr.addressType == type; } - @action - void deleteSilentPaymentAddress(String address) { - final addressRecord = silentAddresses.firstWhere((addressRecord) => - addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address); - - silentAddresses.remove(addressRecord); - updateAddressesByMatch(); + Map toJson() { + return { + 'allAddresses': _allAddresses.map((address) => address.toJSON()).toList(), + 'addressPageType': addressPageType.toString(), + }; } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 67ded289d5..c5d5a6e2ba 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -193,10 +193,10 @@ class ElectrumWorker { await Future.wait(history.map((transaction) async { final txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; - late ElectrumTransactionInfo tx; + ElectrumTransactionInfo? tx; try { - // Exception thrown on null + // Exception thrown on null, handled on catch tx = result.storedTxs.firstWhere((tx) => tx.id == txid); if (height > 0) { @@ -206,12 +206,18 @@ class ElectrumWorker { tx.confirmations = result.chainTip - height + 1; tx.isPending = tx.confirmations == 0; } - } catch (_) { + } 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?.dateValidated != true) { tx = ElectrumTransactionInfo.fromElectrumBundle( await _getTransactionExpanded( hash: txid, currentChainTip: result.chainTip, mempoolAPIEnabled: result.mempoolAPIEnabled, + confirmations: tx?.confirmations, + date: tx?.date, ), result.walletType, result.network, @@ -222,11 +228,11 @@ class ElectrumWorker { final addressHistories = histories[addressRecord.address]; if (addressHistories != null) { - addressHistories.txs.add(tx); + addressHistories.txs.add(tx!); } else { histories[addressRecord.address] = AddressHistoriesResponse( addressRecord: addressRecord, - txs: [tx], + txs: [tx!], walletType: result.walletType, ); } @@ -338,7 +344,6 @@ class ElectrumWorker { hash: request.txHash, currentChainTip: request.currentChainTip, mempoolAPIEnabled: false, - getConfirmations: false, ); _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); @@ -348,65 +353,63 @@ class ElectrumWorker { required String hash, required int currentChainTip, required bool mempoolAPIEnabled, - bool getConfirmations = true, + int? confirmations, + DateTime? date, }) async { int? time; int? height; - int? confirmations; + bool? dateValidated; final transactionHex = await _electrumClient!.request( ElectrumGetTransactionHex(transactionHash: hash), ); - if (getConfirmations) { - if (mempoolAPIEnabled) { - try { - final txVerbose = await http.get( + if (mempoolAPIEnabled) { + try { + final txVerbose = await http.get( + Uri.parse( + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", + ), + ); + + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - if (txVerbose.statusCode == 200 && - txVerbose.body.isNotEmpty && - jsonDecode(txVerbose.body) != null) { - height = jsonDecode(txVerbose.body)['block_height'] as int; - - final blockHash = await http.get( + if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { + final blockResponse = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", ), ); - 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()); + 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); + dateValidated = newDate == date; } } } - } catch (_) {} - } - - if (height != null) { - if (time == null && height > 0) { - time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); } + } catch (_) {} + } - final tip = currentChainTip; - if (tip > 0 && height > 0) { - // Add one because the block itself is the first confirmation - confirmations = tip - height + 1; - } + 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; } } @@ -415,6 +418,7 @@ class ElectrumWorker { for (final vin in original.inputs) { final inputTransactionHex = await _electrumClient!.request( + // TODO: _getTXHex ElectrumGetTransactionHex(transactionHash: vin.txId), ); @@ -426,6 +430,7 @@ class ElectrumWorker { ins: ins, time: time, confirmations: confirmations ?? 0, + dateValidated: dateValidated, ); } @@ -570,9 +575,11 @@ class ElectrumWorker { 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, diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart index a2dfcda17a..1824a0686e 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -4,11 +4,13 @@ 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 @@ -19,13 +21,19 @@ class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { 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}; + return { + 'method': method, + 'txHash': txHash, + 'currentChainTip': currentChainTip, + 'mempoolAPIEnabled': mempoolAPIEnabled, + }; } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 8158907572..6d9ca80061 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -95,8 +95,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletAddresses = LitecoinWalletAddresses( walletInfo, initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, initialMwebAddresses: initialMwebAddresses, network: network, mwebHd: mwebHd, @@ -486,6 +484,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], @@ -656,6 +655,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: [], diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index f9871a9374..9fb2ecc79a 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -26,8 +26,6 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with 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); @@ -102,7 +100,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with (e) => BitcoinAddressRecord( e.value, index: e.key, - type: SegwitAddresType.mweb, + addressType: SegwitAddresType.mweb, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), derivationType: CWBitcoinDerivationType.bip39, @@ -133,7 +131,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } return P2wpkhAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, @@ -164,8 +162,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @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) { @@ -211,7 +212,7 @@ 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, diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index a8088f6429..bbe3f7a442 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -139,16 +139,19 @@ 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(), + time: null, + isPending: true, + isReplaced: false, + confirmations: 0, + inputAddresses: _tx.inputs.map((input) => input.txId).toList(), + outputAddresses: outputAddresses, + fee: fee, + ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 0019d32c69..af39965b95 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -57,8 +57,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, network: network, initialAddressPageType: addressPageType, isHardwareWallet: walletInfo.isHardwareWallet, @@ -153,7 +151,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { addr.address, index: addr.index, isChange: addr.isChange, - type: P2pkhAddressType.p2pkh, + addressType: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), derivationType: CWBitcoinDerivationType.bip39, @@ -163,7 +161,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { AddressUtils.getCashAddrFormat(addr.address), index: addr.index, isChange: addr.isChange, - type: P2pkhAddressType.p2pkh, + addressType: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), derivationType: CWBitcoinDerivationType.bip39, 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 09b603c6ea..5526f96cec 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -14,8 +14,6 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required super.isHardwareWallet, required super.hdWallets, super.initialAddresses, - super.initialRegularAddressIndex, - super.initialChangeAddressIndex, super.initialAddressPageType, }) : super(walletInfo); @@ -28,7 +26,7 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi required BitcoinDerivationInfo derivationInfo, }) => P2pkhAddress.fromDerivation( - bip32: bip32, + bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 9d0c968d82..a41660288d 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? dateValidated; int? height; late int confirmations; String amountFormatted(); @@ -27,4 +28,3 @@ abstract class TransactionInfo extends Object with Keyable { late Map additionalInfo; } - diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index d4bc6799be..032eaf3eae 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -204,9 +204,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; } @@ -492,24 +492,23 @@ class CWBitcoin extends Bitcoin { @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.isChange)) + id: addr.index, + name: addr.name, + address: addr.address, + 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.silentPaymentAddresses .map((addr) => ElectrumSubAddress( id: addr.index, name: addr.name, @@ -588,8 +587,8 @@ class CWBitcoin extends Bitcoin { @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 @@ -680,8 +679,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/scripts/android/app_env.fish b/scripts/android/app_env.fish deleted file mode 100644 index c290a35931..0000000000 --- a/scripts/android/app_env.fish +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env fish - -set APP_ANDROID_NAME "" -set APP_ANDROID_VERSION "" -set APP_ANDROID_BUILD_VERSION "" -set APP_ANDROID_ID "" -set APP_ANDROID_PACKAGE "" -set APP_ANDROID_SCHEME "" - -set MONERO_COM "monero.com" -set CAKEWALLET cakewallet -set HAVEN haven - -set -l TYPES $MONERO_COM $CAKEWALLET $HAVEN -set APP_ANDROID_TYPE $argv[1] - -set MONERO_COM_NAME "Monero.com" -set MONERO_COM_VERSION "1.17.0" -set MONERO_COM_BUILD_NUMBER 103 -set MONERO_COM_BUNDLE_ID "com.monero.app" -set MONERO_COM_PACKAGE "com.monero.app" -set MONERO_COM_SCHEME "monero.com" - -set CAKEWALLET_NAME "Cake Wallet" -set CAKEWALLET_VERSION "4.20.0" -set CAKEWALLET_BUILD_NUMBER 232 -set CAKEWALLET_BUNDLE_ID "com.cakewallet.cake_wallet" -set CAKEWALLET_PACKAGE "com.cakewallet.cake_wallet" -set CAKEWALLET_SCHEME cakewallet - -set HAVEN_NAME Haven -set HAVEN_VERSION "1.0.0" -set HAVEN_BUILD_NUMBER 1 -set HAVEN_BUNDLE_ID "com.cakewallet.haven" -set HAVEN_PACKAGE "com.cakewallet.haven" - -if not contains $APP_ANDROID_TYPE $TYPES - echo "Wrong app type." - return 1 - exit 1 -end - -switch $APP_ANDROID_TYPE - case $MONERO_COM - set APP_ANDROID_NAME $MONERO_COM_NAME - set APP_ANDROID_VERSION $MONERO_COM_VERSION - set APP_ANDROID_BUILD_NUMBER $MONERO_COM_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $MONERO_COM_BUNDLE_ID - set APP_ANDROID_PACKAGE $MONERO_COM_PACKAGE - set APP_ANDROID_SCHEME $MONERO_COM_SCHEME - - case $CAKEWALLET - set APP_ANDROID_NAME $CAKEWALLET_NAME - set APP_ANDROID_VERSION $CAKEWALLET_VERSION - set APP_ANDROID_BUILD_NUMBER $CAKEWALLET_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $CAKEWALLET_BUNDLE_ID - set APP_ANDROID_PACKAGE $CAKEWALLET_PACKAGE - set APP_ANDROID_SCHEME $CAKEWALLET_SCHEME - - case $HAVEN - set APP_ANDROID_NAME $HAVEN_NAME - set APP_ANDROID_VERSION $HAVEN_VERSION - set APP_ANDROID_BUILD_NUMBER $HAVEN_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $HAVEN_BUNDLE_ID - set APP_ANDROID_PACKAGE $HAVEN_PACKAGE - -end - -export APP_ANDROID_TYPE -export APP_ANDROID_NAME -export APP_ANDROID_VERSION -export APP_ANDROID_BUILD_NUMBER -export APP_ANDROID_BUNDLE_ID -export APP_ANDROID_PACKAGE -export APP_ANDROID_SCHEME -export APP_ANDROID_BUNDLE_ID -export APP_ANDROID_PACKAGE -export APP_ANDROID_SCHEME diff --git a/tool/configure.dart b/tool/configure.dart index d159bffe19..4c2916e00a 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,15 +98,14 @@ 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/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'; From 6e8b3d768ed4073d17a2bfa1779795b803498011 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 7 Nov 2024 13:04:45 -0300 Subject: [PATCH 15/64] misc --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 1776078620..01f9400435 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -29,7 +29,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S } @observable - late final SilentPaymentOwner silentPaymentWallet; + late SilentPaymentOwner silentPaymentWallet; final ObservableList silentPaymentAddresses; final ObservableList receivedSPAddresses; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 8d33a499eb..a06d91aa1f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -779,7 +779,6 @@ abstract class ElectrumWalletBase memo: memo, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, spendsSilentPayment: utxoDetails.spendsSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, ); } } From 3950f6cd177fa5020705109a4e1a8a00ce9a5632 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sat, 16 Nov 2024 14:53:00 -0300 Subject: [PATCH 16/64] refactor: misc --- cw_bitcoin/lib/bitcoin_address_record.dart | 10 +- cw_bitcoin/lib/bitcoin_unspent.dart | 24 ++- cw_bitcoin/lib/bitcoin_wallet.dart | 149 +++++++++++------- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 140 +++++++++++----- cw_bitcoin/lib/electrum_transaction_info.dart | 16 +- cw_bitcoin/lib/electrum_wallet.dart | 15 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 68 ++++---- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 9 ++ .../lib/electrum_worker/electrum_worker.dart | 137 ++++++++++------ .../electrum_worker/methods/connection.dart | 4 + .../methods/tweaks_subscribe.dart | 20 +-- cw_core/lib/sync_status.dart | 3 + cw_core/lib/transaction_info.dart | 2 +- cw_core/lib/wallet_info.dart | 2 + lib/bitcoin/cw_bitcoin.dart | 102 ++++++++++-- .../screens/receive/widgets/address_cell.dart | 90 +++++++---- .../wallet_address_list_item.dart | 2 + .../wallet_address_list_view_model.dart | 83 ++++------ lib/view_model/wallet_creation_vm.dart | 31 +--- lib/view_model/wallet_restore_view_model.dart | 37 ++--- tool/configure.dart | 2 + 21 files changed, 585 insertions(+), 361 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 6c8fa82f6e..399cdd0770 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -153,6 +153,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { + final String derivationPath; int get labelIndex => index; final String? labelHex; @@ -161,6 +162,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { BitcoinSilentPaymentAddressRecord( super.address, { required int labelIndex, + this.derivationPath = BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, super.txCount = 0, super.balance = 0, super.name = '', @@ -180,6 +182,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, + derivationPath: decoded['derivationPath'] as String, labelIndex: decoded['labelIndex'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, @@ -192,6 +195,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { @override String toJSON() => json.encode({ 'address': address, + 'derivationPath': derivationPath, 'labelIndex': labelIndex, 'isUsed': isUsed, 'txCount': txCount, @@ -222,13 +226,15 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { return BitcoinReceivedSPAddressRecord( decoded['address'] as String, - labelIndex: decoded['index'] as int, + 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: ECPrivate.fromHex(decoded['spendKey'] as String), + spendKey: (decoded['spendKey'] as String?) == null + ? ECPrivate.random() + : ECPrivate.fromHex(decoded['spendKey'] as String), ); } diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 93d9c25d5d..618ce8f0f0 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -10,17 +10,27 @@ class BitcoinUnspent extends Unspent { factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) => BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout); - factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) => - BitcoinUnspent( - address ?? BitcoinAddressRecord.fromJSON(json['address_record'].toString()), - json['tx_hash'] as String, - int.parse(json['value'].toString()), - int.parse(json['tx_pos'].toString()), - ); + 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, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 7b68397b9c..dd4a1f4003 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -55,6 +55,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { bool? alwaysScan, required bool mempoolAPIEnabled, super.hdWallets, + super.initialUnspentCoins, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -111,6 +112,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final Map hdWallets = {}; 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); @@ -134,8 +139,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - hdWallets[CWBitcoinDerivationType.old] = - hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = hdWallets[CWBitcoinDerivationType.bip39]!; + } return BitcoinWallet( mnemonic: mnemonic, @@ -155,6 +164,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { networkParam: network, mempoolAPIEnabled: mempoolAPIEnabled, hdWallets: hdWallets, + initialUnspentCoins: [], ); } @@ -217,6 +227,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (mnemonic != null) { 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); @@ -242,8 +256,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - hdWallets[CWBitcoinDerivationType.old] = - hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = hdWallets[CWBitcoinDerivationType.bip39]!; + } } return BitcoinWallet( @@ -266,6 +284,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { alwaysScan: alwaysScan, mempoolAPIEnabled: mempoolAPIEnabled, hdWallets: hdWallets, + initialUnspentCoins: snp?.unspentCoins ?? [], ); } @@ -413,41 +432,69 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - // @override - // @action - // Future updateAllUnspents() async { - // List updatedUnspentCoins = []; + @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!); - // } - // }); + // 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; - // }); + unspentCoins.addAll(updatedUnspentCoins); + + await super.updateAllUnspents(); - // await Future.wait(walletAddresses.allAddresses - // .where((element) => element.type != SegwitAddresType.mweb) - // .map((address) async { - // updatedUnspentCoins.addAll(await fetchUnspent(address)); - // })); + final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; - // unspentCoins.addAll(updatedUnspentCoins); + walletAddresses.silentPaymentAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); + walletAddresses.receivedSPAddresses.forEach((addressRecord) { + addressRecord.txCount = 0; + addressRecord.balance = 0; + }); - // if (unspentCoinsInfo.length != updatedUnspentCoins.length) { - // unspentCoins.forEach((coin) => addCoinInfo(coin)); - // return; - // } + final silentPaymentWallet = walletAddresses.silentPaymentWallet; - // await updateCoins(unspentCoins.toSet()); - // await refreshUnspentCoinsInfo(); - // } + 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) { @@ -536,26 +583,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @action void _updateSilentAddressRecord(BitcoinUnspent unspent) { - final receiveAddressRecord = unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord; final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; - final silentPaymentWallet = walletAddresses.silentPaymentWallet; - 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, - ); - - final addressRecord = walletAddresses.silentPaymentAddresses - .firstWhere((address) => address.address == silentPaymentAddress.toString()); - addressRecord.txCount += 1; - addressRecord.balance += unspent.value; - - walletAddresses.addSilentAddresses( - [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord], + walletAddresses.addReceivedSPAddresses( + [unspent.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord], ); } @@ -583,6 +613,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @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; @@ -628,9 +667,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // else: First time seeing this TX after scanning tx.unspents!.forEach(_updateSilentAddressRecord); - // Add new TX record transactionHistory.addOne(tx); - // Update balance record balance[currency]!.confirmed += tx.amount; } @@ -654,6 +691,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { await walletInfo.updateRestoreHeight(result.height!); } + + await save(); } @action @@ -675,7 +714,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { workerSendPort!.send( ElectrumWorkerTweaksSubscribeRequest( scanData: ScanData( - silentAddress: walletAddresses.silentPaymentWallet, + silentPaymentsWallets: walletAddresses.silentPaymentWallets, network: network, height: height, chainTip: chainTip, diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 01f9400435..1016867fa6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -26,13 +26,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S ), super(walletInfo) { silentPaymentWallet = SilentPaymentOwner.fromBip32(hdWallet); + silentPaymentWallets = [silentPaymentWallet!]; } @observable - late SilentPaymentOwner silentPaymentWallet; + SilentPaymentOwner? silentPaymentWallet; final ObservableList silentPaymentAddresses; final ObservableList receivedSPAddresses; + @observable + List silentPaymentWallets = []; + @observable String? activeSilentAddress; @@ -48,19 +52,70 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S } if (silentPaymentAddresses.length == 0) { - silentPaymentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentPaymentWallet.toString(), - labelIndex: 1, - name: "", - addressType: SilentPaymentsAddresType.p2sp, - )); - silentPaymentAddresses.add(BitcoinSilentPaymentAddressRecord( - silentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), - name: "", - labelIndex: 0, - labelHex: BytesUtils.toHexString(silentPaymentWallet.generateLabel(0)), - addressType: SilentPaymentsAddresType.p2sp, - )); + 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(); @@ -97,7 +152,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (selected.labelHex != null) { activeSilentAddress = - silentPaymentWallet.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); + silentPaymentWallet!.toLabeledSilentPaymentAddress(selected.labelIndex).toString(); } else { activeSilentAddress = silentPaymentWallet.toString(); } @@ -117,27 +172,27 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S }) { final hdWallet = hdWallets[derivationType]!; - if (derivationType == CWBitcoinDerivationType.old) { - final pub = hdWallet - .childKey(Bip32KeyIndex(isChange ? 1 : 0)) - .childKey(Bip32KeyIndex(index)) - .publicKey; - - switch (addressType) { - case P2pkhAddressType.p2pkh: - return ECPublic.fromBip32(pub).toP2pkhAddress(); - case SegwitAddresType.p2tr: - return ECPublic.fromBip32(pub).toP2trAddress(); - case SegwitAddresType.p2wsh: - return ECPublic.fromBip32(pub).toP2wshAddress(); - case P2shAddressType.p2wpkhInP2sh: - return ECPublic.fromBip32(pub).toP2wpkhInP2sh(); - case SegwitAddresType.p2wpkh: - return ECPublic.fromBip32(pub).toP2wpkhAddress(); - default: - throw ArgumentError('Invalid address type'); - } - } + // 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: @@ -191,10 +246,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S 1; final address = BitcoinSilentPaymentAddressRecord( - silentPaymentWallet.toLabeledSilentPaymentAddress(currentSPLabelIndex).toString(), + silentPaymentWallet!.toLabeledSilentPaymentAddress(currentSPLabelIndex).toString(), labelIndex: currentSPLabelIndex, name: label, - labelHex: BytesUtils.toHexString(silentPaymentWallet.generateLabel(currentSPLabelIndex)), + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(currentSPLabelIndex)), addressType: SilentPaymentsAddresType.p2sp, ); @@ -270,6 +325,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S 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) => diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index dc51cdbe6e..b5e4bede55 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -15,13 +15,13 @@ class ElectrumTransactionBundle { required this.ins, required this.confirmations, this.time, - this.dateValidated, + this.isDateValidated, }); final BtcTransaction originalTransaction; final List ins; final int? time; - final bool? dateValidated; + final bool? isDateValidated; final int confirmations; Map toJson() { @@ -39,7 +39,7 @@ class ElectrumTransactionBundle { ins: (data['ins'] as List).map((e) => BtcTransaction.fromRaw(e as String)).toList(), confirmations: data['confirmations'] as int, time: data['time'] as int?, - dateValidated: data['dateValidated'] as bool?, + isDateValidated: data['isDateValidated'] as bool?, ); } } @@ -62,7 +62,7 @@ class ElectrumTransactionInfo extends TransactionInfo { bool isReplaced = false, required DateTime date, required int? time, - bool? dateValidated, + bool? isDateValidated, required int confirmations, String? to, this.unspents, @@ -81,7 +81,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.isPending = isPending; this.isReplaced = isReplaced; this.confirmations = confirmations; - this.dateValidated = dateValidated; + this.isDateValidated = isDateValidated; this.to = to; this.additionalInfo = additionalInfo ?? {}; } @@ -235,7 +235,7 @@ class ElectrumTransactionInfo extends TransactionInfo { date: date, confirmations: bundle.confirmations, time: bundle.time, - dateValidated: bundle.dateValidated, + isDateValidated: bundle.isDateValidated, ); } @@ -265,7 +265,7 @@ class ElectrumTransactionInfo extends TransactionInfo { .toList(), isReceivedSilentPayment: data['isReceivedSilentPayment'] as bool? ?? false, time: data['time'] as int?, - dateValidated: data['dateValidated'] as bool?, + isDateValidated: data['isDateValidated'] as bool?, additionalInfo: data['additionalInfo'] as Map?, ); } @@ -326,7 +326,7 @@ class ElectrumTransactionInfo extends TransactionInfo { m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; m['additionalInfo'] = additionalInfo; - m['dateValidated'] = dateValidated; + m['isDateValidated'] = isDateValidated; return m; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4412cf4f19..d5938ce50d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -71,6 +71,7 @@ abstract class ElectrumWalletBase CryptoCurrency? currency, this.alwaysScan, required this.mempoolAPIEnabled, + List initialUnspentCoins = const [], }) : hdWallets = hdWallets ?? { CWBitcoinDerivationType.bip39: getAccountHDWallet( @@ -84,8 +85,7 @@ abstract class ElectrumWalletBase syncStatus = NotConnectedSyncStatus(), _password = password, isEnabledAutoGenerateSubaddress = true, - // TODO: inital unspent coins - unspentCoins = BitcoinUnspentCoins(), + unspentCoins = BitcoinUnspentCoins.of(initialUnspentCoins), scripthashesListening = [], balance = ObservableMap.of(currency != null ? { @@ -419,6 +419,7 @@ abstract class ElectrumWalletBase workerSendPort!.send( ElectrumWorkerConnectionRequest( uri: node.uri, + useSSL: node.useSSL ?? false, network: network, ).toJson(), ); @@ -1036,6 +1037,7 @@ abstract class ElectrumWalletBase 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'alwaysScan': alwaysScan, + 'unspents': unspentCoins.map((e) => e.toJson()).toList(), }); int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount, @@ -1214,7 +1216,6 @@ abstract class ElectrumWalletBase })); })); - unspentCoins.clear(); unspentCoins.addAll(updatedUnspentCoins); unspentCoins.forEach(updateCoin); @@ -1918,9 +1919,15 @@ class TxCreateUtxoDetails { }); } -class BitcoinUnspentCoins extends ObservableList { +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( diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 0acc450f9a..15056fb671 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -11,26 +11,14 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -enum CWBitcoinDerivationType { old, electrum, bip39, mweb } +enum CWBitcoinDerivationType { old_electrum, electrum, old_bip39, bip39, mweb } -class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; - -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 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( @@ -435,6 +423,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { .length; final newAddresses = []; + for (var i = startIndex; i < count + startIndex; i++) { final address = BitcoinAddressRecord( await getAddressAsync( @@ -446,7 +435,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ), index: i, isChange: isChange, - isHidden: derivationType == CWBitcoinDerivationType.old, + isHidden: OLD_DERIVATION_TYPES.contains(derivationType), addressType: addressType, network: network, derivationInfo: derivationInfo, @@ -466,27 +455,38 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } for (final derivationType in hdWallets.keys) { - if (derivationType == CWBitcoinDerivationType.old && addressType == SegwitAddresType.p2wpkh) { + // p2wpkh has always had the right derivations, skip if creating old derivations + if (OLD_DERIVATION_TYPES.contains(derivationType) && addressType == SegwitAddresType.p2wpkh) { continue; } - final derivationInfo = BitcoinAddressUtils.getDerivationFromType( - addressType, - isElectrum: derivationType == CWBitcoinDerivationType.electrum, - ); + final isElectrum = derivationType == CWBitcoinDerivationType.electrum || + derivationType == CWBitcoinDerivationType.old_electrum; - await discoverNewAddresses( - derivationType: derivationType, - isChange: false, - addressType: addressType, - derivationInfo: derivationInfo, - ); - await discoverNewAddresses( - derivationType: derivationType, - isChange: true, - addressType: addressType, - derivationInfo: derivationInfo, + final derivationInfos = walletInfo.derivations?.where( + (element) => element.scriptType == addressType.toString(), ); + + 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, + ); + } } } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 959618dcf8..829f10de39 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -1,6 +1,7 @@ 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_core/pathForWallet.dart'; @@ -23,6 +24,7 @@ class ElectrumWalletSnapshot { required this.silentAddressIndex, required this.mwebAddresses, required this.alwaysScan, + required this.unspentCoins, this.passphrase, this.derivationType, this.derivationPath, @@ -32,6 +34,7 @@ class ElectrumWalletSnapshot { final String password; final WalletType type; final String? addressPageType; + List unspentCoins; @deprecated String? mnemonic; @@ -127,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 index c5d5a6e2ba..9e8909dc65 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -123,27 +123,41 @@ class ElectrumWorker { _network = request.network; _electrumClient = await ElectrumApiProvider.connect( - ElectrumTCPService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), + request.useSSL + ? ElectrumSSLService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ) + : ElectrumTCPService.connect( + request.uri, + onConnectionStatusChange: (status) { + _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); + }, + defaultRequestTimeOut: const Duration(seconds: 5), + connectionTimeOut: const Duration(seconds: 5), + ), ); } Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { - final listener = _electrumClient!.subscribe(ElectrumHeaderSubscribe()); - if (listener == null) { + final req = ElectrumHeaderSubscribe(); + + final stream = _electrumClient!.subscribe(req); + if (stream == null) { _sendError(ElectrumWorkerHeadersSubscribeError(error: 'Failed to subscribe')); return; } - listener((event) { + stream.listen((event) { _sendResponse( - ElectrumWorkerHeadersSubscribeResponse(result: event, id: request.id), + ElectrumWorkerHeadersSubscribeResponse( + result: req.onResponse(event), + id: request.id, + ), ); }); } @@ -155,22 +169,22 @@ class ElectrumWorker { final address = entry.key; final scripthash = entry.value; - final listener = await _electrumClient!.subscribe( - ElectrumScriptHashSubscribe(scriptHash: scripthash), - ); + final req = ElectrumScriptHashSubscribe(scriptHash: scripthash); + + final stream = await _electrumClient!.subscribe(req); - if (listener == null) { + 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 - listener((status) async { + stream.listen((status) async { print("status: $status"); _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( - result: {address: status}, + result: {address: req.onResponse(status)}, id: request.id, )); }); @@ -210,7 +224,7 @@ class ElectrumWorker { // 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?.dateValidated != true) { + if (tx?.isDateValidated != true) { tx = ElectrumTransactionInfo.fromElectrumBundle( await _getTransactionExpanded( hash: txid, @@ -358,7 +372,7 @@ class ElectrumWorker { }) async { int? time; int? height; - bool? dateValidated; + bool? isDateValidated; final transactionHex = await _electrumClient!.request( ElectrumGetTransactionHex(transactionHash: hash), @@ -397,7 +411,7 @@ class ElectrumWorker { if (date != null) { final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); - dateValidated = newDate == date; + isDateValidated = newDate == date; } } } @@ -430,7 +444,7 @@ class ElectrumWorker { ins: ins, time: time, confirmations: confirmations ?? 0, - dateValidated: dateValidated, + isDateValidated: isDateValidated, ); } @@ -498,12 +512,16 @@ class ElectrumWorker { return amountLeft; } - final receiver = Receiver( - scanData.silentAddress.b_scan.toHex(), - scanData.silentAddress.B_spend.toHex(), - scanData.network == BitcoinNetwork.testnet, - scanData.labelIndexes, - scanData.labelIndexes.length, + 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 @@ -515,24 +533,38 @@ class ElectrumWorker { ), )); - final listener = await _electrumClient!.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + final req = ElectrumTweaksSubscribe( + height: syncHeight, + count: initialCount, + historicalMode: false, ); - Future listenFn(ElectrumTweaksSubscribeResponse response) async { + final stream = await _electrumClient!.subscribe(req); + + Future listenFn(Map event, ElectrumTweaksSubscribe req) async { + final response = req.onResponse(event); + // 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; final nextCount = getCountPerRequest(nextHeight); if (nextCount > 0) { - final nextListener = await _electrumClient!.subscribe( - ElectrumTweaksSubscribe(height: syncHeight, count: initialCount), + final nextStream = await _electrumClient!.subscribe( + ElectrumTweaksSubscribe( + height: syncHeight, + count: initialCount, + historicalMode: false, + ), ); - nextListener?.call(listenFn); + nextStream?.listen((event) => listenFn(event, req)); } return; @@ -558,7 +590,18 @@ class ElectrumWorker { try { // scanOutputs called from rust here - final addToWallet = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + 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 @@ -601,7 +644,7 @@ class ElectrumWorker { receivingOutputAddress, labelIndex: 1, // TODO: get actual index/label isUsed: true, - spendKey: scanData.silentAddress.b_spend.tweakAdd( + spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd( BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), ), txCount: 1, @@ -618,6 +661,8 @@ class ElectrumWorker { _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}), )); + + return; } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -631,23 +676,23 @@ class ElectrumWorker { syncHeight = tweakHeight; if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { - if (tweakHeight >= scanData.chainTip) - _sendResponse(ElectrumWorkerTweaksSubscribeResponse( + _sendResponse( + ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( height: syncHeight, - syncStatus: SyncedTipSyncStatus(scanData.chainTip), + syncStatus: scanData.isSingleScan + ? SyncedSyncStatus() + : SyncedTipSyncStatus(scanData.chainTip), ), - )); + ), + ); - if (scanData.isSingleScan) { - _sendResponse(ElectrumWorkerTweaksSubscribeResponse( - result: TweaksSyncResponse(height: syncHeight, syncStatus: SyncedSyncStatus()), - )); - } + stream?.close(); + return; } } - listener?.call(listenFn); + stream?.listen((event) => listenFn(event, req)); } Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { diff --git a/cw_bitcoin/lib/electrum_worker/methods/connection.dart b/cw_bitcoin/lib/electrum_worker/methods/connection.dart index 2512c6cfd4..4ff27665cc 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/connection.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -4,10 +4,12 @@ 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; @@ -21,6 +23,7 @@ class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { network: BasedUtxoNetwork.values.firstWhere( (e) => e.toString() == json['network'] as String, ), + useSSL: json['useSSL'] as bool, id: json['id'] as int?, ); } @@ -31,6 +34,7 @@ class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { 'method': method, 'uri': uri.toString(), 'network': network.toString(), + 'useSSL': useSSL, }; } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart index 0a6f36dc94..c51670cdcd 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -1,7 +1,7 @@ part of 'methods.dart'; class ScanData { - final SilentPaymentOwner silentAddress; + final List silentPaymentsWallets; final int height; final BasedUtxoNetwork network; final int chainTip; @@ -11,7 +11,7 @@ class ScanData { final bool isSingleScan; ScanData({ - required this.silentAddress, + required this.silentPaymentsWallets, required this.height, required this.network, required this.chainTip, @@ -23,7 +23,7 @@ class ScanData { factory ScanData.fromHeight(ScanData scanData, int newHeight) { return ScanData( - silentAddress: scanData.silentAddress, + silentPaymentsWallets: scanData.silentPaymentsWallets, height: newHeight, network: scanData.network, chainTip: scanData.chainTip, @@ -36,7 +36,7 @@ class ScanData { Map toJson() { return { - 'silentAddress': silentAddress.toJson(), + 'silentAddress': silentPaymentsWallets.map((e) => e.toJson()).toList(), 'height': height, 'network': network.value, 'chainTip': chainTip, @@ -49,7 +49,9 @@ class ScanData { static ScanData fromJson(Map json) { return ScanData( - silentAddress: SilentPaymentOwner.fromJson(json['silentAddress'] as Map), + 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, @@ -123,11 +125,9 @@ class TweaksSyncResponse { ? null : (json['transactions'] as Map).map( (key, value) => MapEntry( - key, - ElectrumTransactionInfo.fromJson( - value as Map, - WalletType.bitcoin, - )), + key, + ElectrumTransactionInfo.fromJson(value as Map, WalletType.bitcoin), + ), ), ); } diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 6b4a5da930..98bc9d886a 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -101,6 +101,7 @@ Map syncStatusToJson(SyncStatus? status) { if (status == null) { return {}; } + return { 'progress': status.progress(), 'type': status.runtimeType.toString(), @@ -127,6 +128,8 @@ SyncStatus syncStatusFromJson(Map json) { 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': diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 8a3e5ddaec..5cafbdff85 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -9,7 +9,7 @@ abstract class TransactionInfo extends Object with Keyable { late TransactionDirection direction; late bool isPending; late DateTime date; - bool? dateValidated; + bool? isDateValidated; int? height; late int confirmations; String amountFormatted(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index ab674d9b42..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) diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 032eaf3eae..a0be212eba 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -136,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.isChange)) + isChange: addr.isChange, + ), + ) .toList(); } @@ -336,12 +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 = []; + final list = []; late BasedUtxoNetwork network; switch (node.type) { @@ -371,7 +408,15 @@ class CWBitcoin extends Bitcoin { } if (electrumSeedBytes != null) { - list.add(BitcoinDerivationInfos.ELECTRUM); + for (final addressType in BITCOIN_ADDRESS_TYPES) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + scriptType: addressType.value, + ), + ); + } } var bip39SeedBytes; @@ -380,7 +425,17 @@ class CWBitcoin extends Bitcoin { } catch (_) {} if (bip39SeedBytes != null) { - list.add(BitcoinDerivationInfos.BIP84); + for (final addressType in BITCOIN_ADDRESS_TYPES) { + list.add( + DerivationInfo( + derivationType: DerivationType.bip39, + derivationPath: BitcoinAddressUtils.getDerivationFromType( + addressType, + ).derivationPath.toString(), + scriptType: addressType.value, + ), + ); + } } return list; @@ -490,6 +545,22 @@ 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 walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; @@ -498,6 +569,9 @@ class CWBitcoin extends Bitcoin { 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, @@ -508,14 +582,16 @@ class CWBitcoin extends Bitcoin { @override List getSilentPaymentReceivedAddresses(Object wallet) { final walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; - return walletAddresses.silentPaymentAddresses + return walletAddresses.receivedSPAddresses .map((addr) => ElectrumSubAddress( - id: addr.index, - name: addr.name, - address: addr.address, - txCount: addr.txCount, - balance: addr.balance, - isChange: addr.isChange)) + id: addr.index, + name: addr.name, + address: addr.address, + derivationPath: "", + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isChange, + )) .toList(); } diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index beef7c7625..38421c1da1 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,21 @@ class AddressCell extends StatelessWidget { ), ], ), + if (derivationPath.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: 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/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 95cf0256c4..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'; @@ -193,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); @@ -201,37 +200,11 @@ abstract class WalletCreationVMBase with Store { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: - final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( + return await bitcoin!.getDerivationInfosFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, node: node, passphrase: restoreWallet.passphrase, ); - - List list = []; - for (var derivation in bitcoinDerivations) { - if (derivation.derivationType == DerivationType.electrum) { - list.add( - DerivationInfo( - derivationType: DerivationType.electrum, - derivationPath: "m/0'", - description: "Electrum", - scriptType: "p2wpkh", - ), - ); - } else { - list.add( - DerivationInfo( - derivationType: DerivationType.bip39, - derivationPath: "m/84'/0'/0'", - description: "Standard BIP84 native segwit", - scriptType: "p2wpkh", - ), - ); - } - } - - return list; - case WalletType.nano: return nanoUtil!.getDerivationsFromMnemonic( mnemonic: restoreWallet.mnemonicSeed!, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index bf1168f01c..4c17998f41 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -91,14 +91,17 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final height = options['height'] as int? ?? 0; name = options['name'] as String; DerivationInfo? derivationInfo = options["derivationInfo"] as DerivationInfo?; - List? derivations = options["derivations"] as List?; if (mode == WalletRestoreMode.seed) { final seed = options['seed'] as String; 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( @@ -106,7 +109,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, passphrase: passphrase, - derivations: derivations, + derivations: options["derivations"] as List?, ); case WalletType.haven: return haven!.createHavenRestoreWalletFromSeedCredentials( @@ -256,36 +259,16 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.litecoin: String? mnemonic = credentials['seed'] as String?; String? passphrase = credentials['passphrase'] as String?; - final bitcoinDerivations = await bitcoin!.getDerivationsFromMnemonic( + final list = await bitcoin!.getDerivationInfosFromMnemonic( mnemonic: mnemonic!, node: node, passphrase: passphrase, ); - List list = []; - for (var derivation in bitcoinDerivations) { - if (derivation.derivationType.toString().endsWith("electrum")) { - list.add( - DerivationInfo( - derivationType: DerivationType.electrum, - derivationPath: "m/0'", - description: "Electrum", - scriptType: "p2wpkh", - ), - ); - } else { - list.add( - DerivationInfo( - derivationType: DerivationType.bip39, - derivationPath: "m/84'/0'/0'", - description: "Standard BIP84 native segwit", - scriptType: "p2wpkh", - ), - ); - } - } + // is restoring? = add old used derivations + final oldList = bitcoin!.getOldDerivationInfos(list); - return list; + return oldList; case WalletType.nano: String? mnemonic = credentials['seed'] as String?; String? seedKey = credentials['private_key'] as String?; diff --git a/tool/configure.dart b/tool/configure.dart index d81f3f1a13..d4de3ee057 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -176,6 +176,7 @@ 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, @@ -198,6 +199,7 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPrioritySlow(); Future> compareDerivationMethods( {required String mnemonic, required Node node}); + List getOldDerivationInfos(List list); Future> getDerivationsFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); From 75aaa6f23e0d8c93c2eeb6844b31bb848d5755ae Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 12:18:35 -0300 Subject: [PATCH 17/64] chore: build scripts --- tool/configure.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tool/configure.dart b/tool/configure.dart index 9598e5270e..18e13e56b6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -122,7 +122,23 @@ import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ - + 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, @@ -200,7 +216,7 @@ abstract class Bitcoin { Future> compareDerivationMethods( {required String mnemonic, required Node node}); List getOldDerivationInfos(List list); - Future> getDerivationsFromMnemonic( + Future> getDerivationInfosFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); From aa6f932d8a4a1b563c7d1349d77a70d543a0990e Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 12:33:35 -0300 Subject: [PATCH 18/64] chore: build errors --- cw_bitcoin/lib/electrum_wallet.dart | 2 +- tool/configure.dart | 41 ++++++++++++++++------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index e99d7b9815..45575dc963 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1767,7 +1767,7 @@ abstract class ElectrumWalletBase case ConnectionStatus.disconnected: if (syncStatus is! NotConnectedSyncStatus && syncStatus is! ConnectingSyncStatus && - syncStatus is! SyncronizingSyncStatus) { + syncStatus is! SynchronizingSyncStatus) { syncStatus = NotConnectedSyncStatus(); } break; diff --git a/tool/configure.dart b/tool/configure.dart index 18e13e56b6..9254fe3115 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -122,37 +122,40 @@ import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ - 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 { +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 { From b9f76bd24195e43d7bf746031f190d896d5fa531 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 15:23:13 -0300 Subject: [PATCH 19/64] fix: btc create --- cw_bitcoin/lib/bitcoin_wallet.dart | 43 +++--------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index dd4a1f4003..ecd4a48b62 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -108,44 +108,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, required bool mempoolAPIEnabled, }) async { - late List seedBytes; - final Map hdWallets = {}; - - 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.bip39]!; - } - return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -157,13 +119,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - seedBytes: seedBytes, + seedBytes: walletInfo.derivationInfo?.derivationType == DerivationType.electrum + ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) + : Bip39SeedGenerator.generateFromString(mnemonic, passphrase), initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, mempoolAPIEnabled: mempoolAPIEnabled, - hdWallets: hdWallets, initialUnspentCoins: [], ); } From 9bb3d9f35b6a10164f0b669ed9fc80e7015181a7 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 17 Nov 2024 15:32:54 -0300 Subject: [PATCH 20/64] fix: btc restore --- cw_bitcoin/lib/bitcoin_wallet.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ecd4a48b62..69433bfc65 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -223,7 +223,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; } if (hdWallets[CWBitcoinDerivationType.electrum] != null) { - hdWallets[CWBitcoinDerivationType.old_electrum] = hdWallets[CWBitcoinDerivationType.bip39]!; + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; } } From c35dec0d09bee073f06372ba6b035abc1c474ba0 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 22 Nov 2024 21:29:29 -0300 Subject: [PATCH 21/64] feat: restore & scan imp --- cw_bitcoin/lib/bitcoin_wallet.dart | 149 ++++++++---- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 9 + cw_bitcoin/lib/electrum_wallet.dart | 103 +++++---- .../lib/electrum_worker/electrum_worker.dart | 217 +++++++++++------- .../electrum_worker_methods.dart | 4 + .../methods/check_tweaks_method.dart | 49 ++++ .../lib/electrum_worker/methods/get_fees.dart | 8 +- .../lib/electrum_worker/methods/methods.dart | 2 + .../methods/stop_scanning.dart | 49 ++++ cw_core/lib/get_height_by_date.dart | 3 + lib/bitcoin/cw_bitcoin.dart | 4 +- .../screens/receive/widgets/address_cell.dart | 23 +- .../screens/receive/widgets/address_list.dart | 4 + 13 files changed, 450 insertions(+), 174 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/methods/check_tweaks_method.dart create mode 100644 cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 69433bfc65..f6d390389b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -108,6 +108,53 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, required bool mempoolAPIEnabled, }) async { + List? seedBytes = null; + final Map hdWallets = {}; + + 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 { + seedBytes = walletInfo.derivationInfo?.derivationType == DerivationType.electrum + ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) + : Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + } + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -119,9 +166,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - seedBytes: walletInfo.derivationInfo?.derivationType == DerivationType.electrum - ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) - : Bip39SeedGenerator.generateFromString(mnemonic, passphrase), + seedBytes: seedBytes, + hdWallets: hdWallets, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -253,9 +299,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future getNodeIsElectrs() async { - final version = await sendWorker(ElectrumWorkerGetVersionRequest()) as List; + if (node?.uri.host.contains("electrs") ?? false) { + return true; + } + + final version = await sendWorker(ElectrumWorkerGetVersionRequest()); - if (version.isNotEmpty) { + if (version is List && version.isNotEmpty) { final server = version[0]; if (server.toLowerCase().contains('electrs')) { @@ -263,6 +313,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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; @@ -271,33 +325,39 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future getNodeSupportsSilentPayments() async { - return true; + // 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; - // } + if (!(await getNodeIsElectrs())) { + return false; + } - // if (node == null) { - // return false; - // } + try { + final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String; + final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson( + json.decode(workerResponse) as Map, + ); + final supportsScanning = tweaksResponse.result == true; - // 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!; + if (supportsScanning) { + node!.supportsSilentPayments = true; + node!.save(); + return node!.supportsSilentPayments!; + } + } catch (_) {} + + node!.supportsSilentPayments = false; + node!.save(); + return node!.supportsSilentPayments!; } LedgerConnection? _ledgerConnection; @@ -383,16 +443,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (tip > walletInfo.restoreHeight) { _setListeners(walletInfo.restoreHeight); } - } else { - alwaysScan = false; - - // _isolate?.then((value) => value.kill(priority: Isolate.immediate)); - - // if (rpc!.isConnected) { - // syncStatus = SyncedSyncStatus(); - // } else { - // syncStatus = NotConnectedSyncStatus(); - // } + } else if (syncStatus is! SyncedSyncStatus) { + await sendWorker(ElectrumWorkerStopScanningRequest()); + await startSync(); } } @@ -565,9 +618,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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; @@ -651,9 +711,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { syncStatus = SyncingSyncStatus(newSyncStatus.blocksLeft, newSyncStatus.ptc); } else { syncStatus = newSyncStatus; + + if (newSyncStatus is SyncedSyncStatus) { + silentPaymentsScanningActive = false; + } } - await walletInfo.updateRestoreHeight(result.height!); + final height = result.height; + if (height != null) { + await walletInfo.updateRestoreHeight(height); + } } await save(); @@ -801,6 +868,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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(); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 1016867fa6..2f2f87084a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -361,6 +361,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return labels; } + @override + @action + void updateHiddenAddresses() { + super.updateHiddenAddresses(); + this.hiddenAddresses.addAll(silentPaymentAddresses + .where((addressRecord) => addressRecord.isHidden) + .map((addressRecord) => addressRecord.address)); + } + Map toJson() { final json = super.toJson(); json['silentPaymentAddresses'] = diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 45575dc963..de14efadd5 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -125,7 +125,7 @@ abstract class ElectrumWalletBase workerSendPort!.send(json); try { - return completer.future.timeout(Duration(seconds: 5)); + return completer.future.timeout(Duration(seconds: 30)); } catch (e) { _errorCompleters.addAll({messageId: e}); _responseCompleters.remove(messageId); @@ -146,13 +146,8 @@ abstract class ElectrumWalletBase final workerMethod = messageJson['method'] as String; final workerError = messageJson['error'] as String?; - - if (workerError != null) { - print('Worker error: $workerError'); - return; - } - final responseId = messageJson['id'] as int?; + if (responseId != null && _responseCompleters.containsKey(responseId)) { _responseCompleters[responseId]!.complete(message); _responseCompleters.remove(responseId); @@ -160,6 +155,11 @@ abstract class ElectrumWalletBase switch (workerMethod) { case ElectrumWorkerMethods.connectionMethod: + if (workerError != null) { + _onConnectionStatusChange(ConnectionStatus.failed); + break; + } + final response = ElectrumWorkerConnectionResponse.fromJson(messageJson); _onConnectionStatusChange(response.result); break; @@ -214,6 +214,7 @@ abstract class ElectrumWalletBase bool? alwaysScan; bool mempoolAPIEnabled; + bool _updatingHistories = false; final Map hdWallets; Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; @@ -323,7 +324,8 @@ abstract class ElectrumWalletBase List scripthashesListening; bool _chainTipListenerOn = false; - bool _isInitialSync = true; + // TODO: improve this + int _syncedTimes = 0; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -348,9 +350,11 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); - // INFO: FIRST: Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) + // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); + _syncedTimes = 0; + // 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(); @@ -365,13 +369,14 @@ abstract class ElectrumWalletBase _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - _isInitialSync = false; - syncStatus = SyncedSyncStatus(); + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } await save(); } catch (e, stacktrace) { - print(stacktrace); print("startSync $e"); + print(stacktrace); syncStatus = FailedSyncStatus(); } } @@ -389,8 +394,10 @@ abstract class ElectrumWalletBase } @action - Future onFeesResponse(TransactionPriorities result) async { - feeRates = result; + Future onFeesResponse(TransactionPriorities? result) async { + if (result != null) { + feeRates = result; + } } Node? node; @@ -400,8 +407,6 @@ abstract class ElectrumWalletBase Future connectToNode({required Node node}) async { this.node = node; - if (syncStatus is ConnectingSyncStatus) return; - try { syncStatus = ConnectingSyncStatus(); @@ -416,6 +421,7 @@ abstract class ElectrumWalletBase _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); _workerSubscription = receivePort!.listen((message) { + print('Main: received message: $message'); if (message is SendPort) { workerSendPort = message; workerSendPort!.send( @@ -1159,15 +1165,11 @@ abstract class ElectrumWalletBase @action Future updateAllUnspents() async { - final req = ElectrumWorkerListUnspentRequest( - scripthashes: walletAddresses.allScriptHashes.toList(), + workerSendPort!.send( + ElectrumWorkerListUnspentRequest( + scripthashes: walletAddresses.allScriptHashes.toList(), + ).toJson(), ); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } } @action @@ -1222,6 +1224,11 @@ abstract class ElectrumWalletBase unspentCoins.forEach(updateCoin); await refreshUnspentCoinsInfo(); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } @action @@ -1299,10 +1306,13 @@ abstract class ElectrumWalletBase @action Future onHistoriesResponse(List histories) async { - if (histories.isEmpty) { + if (histories.isEmpty || _updatingHistories) { + _updatingHistories = false; return; } + _updatingHistories = true; + final addressesWithHistory = []; BitcoinAddressType? lastDiscoveredType; @@ -1340,7 +1350,13 @@ abstract class ElectrumWalletBase isChange: isChange, derivationType: addressRecord.derivationType, addressType: addressRecord.addressType, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressRecord.addressType), + derivationInfo: BitcoinAddressUtils.getDerivationFromType( + addressRecord.addressType, + isElectrum: [ + CWBitcoinDerivationType.electrum, + CWBitcoinDerivationType.old_electrum, + ].contains(addressRecord.derivationType), + ), ); final newAddressList = @@ -1364,6 +1380,12 @@ abstract class ElectrumWalletBase } walletAddresses.updateHiddenAddresses(); + _updatingHistories = false; + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } Future canReplaceByFee(ElectrumTransactionInfo tx) async { @@ -1606,10 +1628,8 @@ abstract class ElectrumWalletBase @action Future updateTransactions([List? addresses]) async { - addresses ??= walletAddresses.allAddresses.toList(); - - final req = ElectrumWorkerGetHistoryRequest( - addresses: addresses, + 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 @@ -1617,13 +1637,7 @@ abstract class ElectrumWalletBase chainTip: currentChainTip ?? getBitcoinHeightByDate(date: DateTime.now()), network: network, mempoolAPIEnabled: mempoolAPIEnabled, - ); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } + ).toJson()); } @action @@ -1663,17 +1677,18 @@ abstract class ElectrumWalletBase unconfirmed: totalUnconfirmed, frozen: totalFrozen, ); + + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } } @action Future updateBalance() async { - final req = ElectrumWorkerGetBalanceRequest(scripthashes: walletAddresses.allScriptHashes); - - if (_isInitialSync) { - await sendWorker(req); - } else { - workerSendPort!.send(req.toJson()); - } + workerSendPort!.send(ElectrumWorkerGetBalanceRequest( + scripthashes: walletAddresses.allScriptHashes, + ).toJson()); } @override diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 9e8909dc65..3a0cdf4cb8 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -23,6 +23,8 @@ class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; BasedUtxoNetwork? _network; + bool _isScanning = false; + bool _stopScanRequested = false; ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -45,8 +47,6 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { - print("Worker: received message: $message"); - try { Map messageJson; if (message is String) { @@ -97,12 +97,37 @@ class ElectrumWorker { ElectrumWorkerBroadcastRequest.fromJson(messageJson), ); break; - case ElectrumRequestMethods.tweaksSubscribeMethod: - await _handleScanSilentPayments( - ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + case ElectrumWorkerMethods.checkTweaksMethod: + await _handleCheckTweaks( + ElectrumWorkerCheckTweaksRequest.fromJson(messageJson), + ); + break; + case ElectrumWorkerMethods.stopScanningMethod: + await _handleStopScanning( + ElectrumWorkerStopScanningRequest.fromJson(messageJson), ); break; case ElectrumRequestMethods.estimateFeeMethod: + 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), ); @@ -113,8 +138,7 @@ class ElectrumWorker { ); break; } - } catch (e, s) { - print(s); + } catch (e) { _sendError(ElectrumWorkerErrorResponse(error: e.toString())); } } @@ -122,25 +146,29 @@ class ElectrumWorker { Future _handleConnect(ElectrumWorkerConnectionRequest request) async { _network = request.network; - _electrumClient = await ElectrumApiProvider.connect( - request.useSSL - ? ElectrumSSLService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ) - : ElectrumTCPService.connect( - request.uri, - onConnectionStatusChange: (status) { - _sendResponse(ElectrumWorkerConnectionResponse(status: status, id: request.id)); - }, - defaultRequestTimeOut: const Duration(seconds: 5), - connectionTimeOut: const Duration(seconds: 5), - ), - ); + 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 { @@ -230,6 +258,7 @@ class ElectrumWorker { hash: txid, currentChainTip: result.chainTip, mempoolAPIEnabled: result.mempoolAPIEnabled, + getTime: true, confirmations: tx?.confirmations, date: tx?.date, ), @@ -367,6 +396,7 @@ class ElectrumWorker { required String hash, required int currentChainTip, required bool mempoolAPIEnabled, + bool getTime = false, int? confirmations, DateTime? date, }) async { @@ -378,52 +408,54 @@ class ElectrumWorker { ElectrumGetTransactionHex(transactionHash: hash), ); - if (mempoolAPIEnabled) { - try { - final txVerbose = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", - ), - ); - - if (txVerbose.statusCode == 200 && - txVerbose.body.isNotEmpty && - jsonDecode(txVerbose.body) != null) { - height = jsonDecode(txVerbose.body)['block_height'] as int; - - final blockHash = await http.get( + if (getTime) { + if (mempoolAPIEnabled) { + try { + final txVerbose = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", + "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", ), ); - if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { - final blockResponse = await http.get( + if (txVerbose.statusCode == 200 && + txVerbose.body.isNotEmpty && + jsonDecode(txVerbose.body) != null) { + height = jsonDecode(txVerbose.body)['block_height'] as int; + + final blockHash = await http.get( Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", + "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", ), ); - 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; + if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { + 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()); + + if (date != null) { + final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); + isDateValidated = newDate == date; + } } } } - } - } catch (_) {} - } + } 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; + 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; + } } } @@ -498,19 +530,46 @@ class ElectrumWorker { } } + 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; - int syncHeight = scanData.height; - int initialSyncHeight = syncHeight; - int getCountPerRequest(int syncHeight) { - if (scanData.isSingleScan) { - return 1; - } + // 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), + // ); + // }, + // ), + // ); - final amountLeft = scanData.chainTip - syncHeight + 1; - return amountLeft; - } + int syncHeight = scanData.height; + int initialSyncHeight = syncHeight; final receivers = scanData.silentPaymentsWallets.map( (wallet) { @@ -525,7 +584,6 @@ class ElectrumWorker { ); // Initial status UI update, send how many blocks in total to scan - final initialCount = getCountPerRequest(syncHeight); _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( height: syncHeight, @@ -535,14 +593,19 @@ class ElectrumWorker { final req = ElectrumTweaksSubscribe( height: syncHeight, - count: initialCount, + count: 1, historicalMode: false, ); final stream = await _electrumClient!.subscribe(req); - Future listenFn(Map event, ElectrumTweaksSubscribe req) async { + 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; @@ -554,13 +617,12 @@ class ElectrumWorker { // re-subscribe to continue receiving messages, starting from the next unscanned height final nextHeight = syncHeight + 1; - final nextCount = getCountPerRequest(nextHeight); - if (nextCount > 0) { - final nextStream = await _electrumClient!.subscribe( + if (nextHeight <= scanData.chainTip) { + final nextStream = _electrumClient!.subscribe( ElectrumTweaksSubscribe( - height: syncHeight, - count: initialCount, + height: nextHeight, + count: 1, historicalMode: false, ), ); @@ -693,6 +755,7 @@ class ElectrumWorker { } stream?.listen((event) => listenFn(event, req)); + _isScanning = false; } Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart index 6bd4d296e7..4d9c85a47b 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -5,10 +5,14 @@ class ElectrumWorkerMethods { 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() { 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/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart index be81e53469..1892e2cb75 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -37,7 +37,7 @@ class ElectrumWorkerGetFeesError extends ElectrumWorkerErrorResponse { } class ElectrumWorkerGetFeesResponse - extends ElectrumWorkerResponse> { + extends ElectrumWorkerResponse> { ElectrumWorkerGetFeesResponse({ required super.result, super.error, @@ -46,13 +46,15 @@ class ElectrumWorkerGetFeesResponse @override Map resultJson(result) { - return result.toJson(); + return result?.toJson() ?? {}; } @override factory ElectrumWorkerGetFeesResponse.fromJson(Map json) { return ElectrumWorkerGetFeesResponse( - result: deserializeTransactionPriorities(json['result'] as Map), + 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/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 295522d39a..8f23d1d6a3 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -20,3 +20,5 @@ 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/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_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 2b0b77a895..15451f31f0 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, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a0be212eba..3eb560ba7f 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -422,7 +422,9 @@ class CWBitcoin extends Bitcoin { var bip39SeedBytes; try { bip39SeedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - } catch (_) {} + } catch (e) { + print("bip39 seed error: $e"); + } if (bip39SeedBytes != null) { for (final addressType in BITCOIN_ADDRESS_TYPES) { diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 38421c1da1..5a1267bb4a 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -161,16 +161,21 @@ class AddressCell extends StatelessWidget { if (derivationPath.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Flexible( - child: AutoSizeText( - derivationPath, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: isChange ? 10 : 14, - color: textColor, + 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) diff --git a/lib/src/screens/receive/widgets/address_list.dart b/lib/src/screens/receive/widgets/address_list.dart index 0d5805e526..004690b67a 100644 --- a/lib/src/screens/receive/widgets/address_list.dart +++ b/lib/src/screens/receive/widgets/address_list.dart @@ -167,6 +167,10 @@ class _AddressListState extends State { : backgroundColor, textColor: textColor, onTap: (_) { + if (item.isChange || item.isHidden) { + return; + } + if (widget.onSelect != null) { widget.onSelect!(item.address); return; From b7ff9ab32b3bb631d60a65e7c4c4ba8d8d336fcf Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sat, 23 Nov 2024 11:35:50 -0300 Subject: [PATCH 22/64] fix: api & create --- cw_bitcoin/lib/bitcoin_wallet.dart | 14 ++++-- cw_bitcoin/lib/electrum_wallet.dart | 5 +++ cw_bitcoin/lib/electrum_wallet_addresses.dart | 24 +++++++++- .../lib/electrum_worker/electrum_worker.dart | 44 ++++++++++--------- cw_core/lib/get_height_by_date.dart | 2 +- 5 files changed, 64 insertions(+), 25 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index f6d390389b..653faddf45 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -150,9 +150,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { hdWallets[CWBitcoinDerivationType.electrum]!; } } else { - seedBytes = walletInfo.derivationInfo?.derivationType == DerivationType.electrum - ? ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase) - : Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + 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( diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index de14efadd5..fb6fc92b10 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1308,6 +1308,11 @@ abstract class ElectrumWalletBase Future onHistoriesResponse(List histories) async { if (histories.isEmpty || _updatingHistories) { _updatingHistories = false; + _syncedTimes++; + if (_syncedTimes == 3) { + syncStatus = SyncedSyncStatus(); + } + return; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 15056fb671..64b49ba5b0 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -467,7 +467,29 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { (element) => element.scriptType == addressType.toString(), ); - for (final derivationInfo in derivationInfos ?? []) { + 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 (final derivationInfo in derivationInfos) { final bitcoinDerivationInfo = BitcoinDerivationInfo( derivationType: isElectrum ? BitcoinDerivationType.electrum : BitcoinDerivationType.bip39, derivationPath: derivationInfo.derivationPath!, diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 3a0cdf4cb8..14bb0c4dbb 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -47,6 +47,8 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { + print("Worker received message: $message"); + try { Map messageJson; if (message is String) { @@ -107,7 +109,6 @@ class ElectrumWorker { ElectrumWorkerStopScanningRequest.fromJson(messageJson), ); break; - case ElectrumRequestMethods.estimateFeeMethod: case ElectrumRequestMethods.tweaksSubscribeMethod: if (_isScanning) { _stopScanRequested = false; @@ -279,12 +280,8 @@ class ElectrumWorker { walletType: result.walletType, ); } - - return Future.value(null); })); } - - return histories; })); _sendResponse(ElectrumWorkerGetHistoryResponse( @@ -411,29 +408,36 @@ class ElectrumWorker { if (getTime) { if (mempoolAPIEnabled) { try { - final txVerbose = await http.get( - Uri.parse( - "http://mempool.cakewallet.com:8999/api/v1/tx/$hash/status", - ), - ); + // 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( - "http://mempool.cakewallet.com:8999/api/v1/block-height/$height", - ), - ); + 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( - "http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}", - ), - ); + 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 && diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 15451f31f0..dcf9c66759 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -273,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()}", ), ); From e16a2180c4075ed5d094ffc4a9d3d510a0aa750f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 24 Nov 2024 19:05:09 -0300 Subject: [PATCH 23/64] feat: fix rescan & stop, new card --- cw_bitcoin/lib/bitcoin_wallet.dart | 94 +++++----- cw_bitcoin/lib/electrum_wallet.dart | 5 - .../lib/electrum_worker/electrum_worker.dart | 93 +++++----- .../methods/tweaks_subscribe.dart | 5 + lib/bitcoin/cw_bitcoin.dart | 13 +- lib/di.dart | 56 +++--- lib/router.dart | 21 ++- .../screens/dashboard/pages/address_page.dart | 11 +- .../screens/dashboard/pages/balance_page.dart | 168 ++++++++++++++---- lib/src/screens/rescan/rescan_page.dart | 42 +++-- .../dashboard/dashboard_view_model.dart | 10 +- .../wallet_address_list_view_model.dart | 24 ++- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 3 +- res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 2 + 41 files changed, 401 insertions(+), 173 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 653faddf45..20a399a880 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -35,6 +35,13 @@ part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; abstract class BitcoinWalletBase extends ElectrumWallet with Store { + @observable + bool nodeSupportsSilentPayments = true; + @observable + bool silentPaymentsScanningActive = false; + @observable + bool allowedToSwitchNodesForScanning = false; + BitcoinWalletBase({ required String password, required WalletInfo walletInfo, @@ -307,63 +314,68 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } Future getNodeIsElectrs() async { - if (node?.uri.host.contains("electrs") ?? false) { - return true; + if (node?.isElectrs != null) { + return node!.isElectrs!; + } + + final isNamedElectrs = node?.uri.host.contains("electrs") ?? false; + if (isNamedElectrs) { + node!.isElectrs = true; } - final version = await sendWorker(ElectrumWorkerGetVersionRequest()); + final isNamedFulcrum = node!.uri.host.contains("fulcrum"); + if (isNamedFulcrum) { + node!.isElectrs = false; + } - if (version is List && version.isNotEmpty) { - final server = version[0]; + if (node!.isElectrs == null) { + final version = await sendWorker(ElectrumWorkerGetVersionRequest()); - if (server.toLowerCase().contains('electrs')) { + if (version is List && version.isNotEmpty) { + final server = version[0]; + + if (server.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + } + } else if (version is String && version.toLowerCase().contains('electrs')) { node!.isElectrs = true; - node!.save(); - return node!.isElectrs!; + } else { + node!.isElectrs = false; } - } 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; + if (node?.supportsSilentPayments != null) { + return node!.supportsSilentPayments!; } // As of today (august 2024), only ElectrumRS supports silent payments - if (!(await getNodeIsElectrs())) { - return false; + final isElectrs = await getNodeIsElectrs(); + if (!isElectrs) { + node!.supportsSilentPayments = false; } - try { - final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String; - final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson( - json.decode(workerResponse) as Map, - ); - final supportsScanning = tweaksResponse.result == true; + if (node!.supportsSilentPayments == null) { + 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!; + if (supportsScanning) { + node!.supportsSilentPayments = true; + } else { + node!.supportsSilentPayments = false; + } + } catch (_) { + node!.supportsSilentPayments = false; } - } catch (_) {} - - node!.supportsSilentPayments = false; + } node!.save(); return node!.supportsSilentPayments!; } @@ -437,8 +449,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; + final nodeSupportsSilentPayments = await getNodeSupportsSilentPayments(); + final isAllowedToScan = nodeSupportsSilentPayments || allowedToSwitchNodesForScanning; - if (active) { + if (active && isAllowedToScan) { syncStatus = AttemptingScanSyncStatus(); final tip = currentChainTip!; @@ -730,8 +744,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { await walletInfo.updateRestoreHeight(height); } } - - await save(); } @action @@ -765,6 +777,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, + shouldSwitchNodes: + !(await getNodeSupportsSilentPayments()) && allowedToSwitchNodesForScanning, ), ).toJson(), ); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index fb6fc92b10..60ad226351 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -276,11 +276,6 @@ abstract class ElectrumWalletBase @override bool isTestnet; - @observable - bool nodeSupportsSilentPayments = true; - @observable - bool silentPaymentsScanningActive = false; - bool _isTryingToConnect = false; Completer sharedPrefs = Completer(); diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 14bb0c4dbb..14b1bab7f4 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -14,17 +14,15 @@ 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:rxdart/rxdart.dart'; import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; BasedUtxoNetwork? _network; - bool _isScanning = false; - bool _stopScanRequested = false; + BehaviorSubject>? _scanningStream; ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -47,7 +45,7 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { - print("Worker received message: $message"); + print("Worker: received message: $message"); try { Map messageJson; @@ -105,27 +103,15 @@ class ElectrumWorker { ); break; case ElectrumWorkerMethods.stopScanningMethod: + print("Worker: received message: $message"); 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()), - ), - ); - } + await _handleScanSilentPayments( + ElectrumWorkerTweaksSubscribeRequest.fromJson(messageJson), + ); break; case ElectrumRequestMethods.estimateFeeMethod: @@ -550,28 +536,25 @@ class ElectrumWorker { } Future _handleStopScanning(ElectrumWorkerStopScanningRequest request) async { - _stopScanRequested = true; + _scanningStream?.close(); + _scanningStream = null; _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), - // ); - // }, - // ), - // ); + var scanningClient = _electrumClient; + if (scanData.shouldSwitchNodes) { + scanningClient = await ElectrumApiProvider.connect( + ElectrumTCPService.connect( + Uri.parse("tcp://electrs.cakewallet.com:50001"), + ), + ); + } int syncHeight = scanData.height; int initialSyncHeight = syncHeight; @@ -587,6 +570,15 @@ class ElectrumWorker { }, ); + int getCountPerRequest(int syncHeight) { + if (scanData.isSingleScan) { + return 1; + } + + final amountLeft = scanData.chainTip - syncHeight + 1; + return amountLeft; + } + // Initial status UI update, send how many blocks in total to scan _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( @@ -597,17 +589,16 @@ class ElectrumWorker { final req = ElectrumTweaksSubscribe( height: syncHeight, - count: 1, + count: getCountPerRequest(syncHeight), historicalMode: false, ); - final stream = await _electrumClient!.subscribe(req); + _scanningStream = await scanningClient!.subscribe(req); void listenFn(Map event, ElectrumTweaksSubscribe req) { final response = req.onResponse(event); - if (_stopScanRequested || response == null) { - _stopScanRequested = false; - _isScanning = false; + + if (response == null || _scanningStream == null) { return; } @@ -623,10 +614,10 @@ class ElectrumWorker { final nextHeight = syncHeight + 1; if (nextHeight <= scanData.chainTip) { - final nextStream = _electrumClient!.subscribe( + final nextStream = scanningClient!.subscribe( ElectrumTweaksSubscribe( height: nextHeight, - count: 1, + count: getCountPerRequest(nextHeight), historicalMode: false, ), ); @@ -710,6 +701,7 @@ class ElectrumWorker { receivingOutputAddress, labelIndex: 1, // TODO: get actual index/label isUsed: true, + // TODO: use right wallet spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd( BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), ), @@ -753,24 +745,27 @@ class ElectrumWorker { ), ); - stream?.close(); + _scanningStream?.close(); + _scanningStream = null; return; } } - stream?.listen((event) => listenFn(event, req)); - _isScanning = false; + _scanningStream?.listen((event) => listenFn(event, req)); } Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { - _sendResponse(ElectrumWorkerGetVersionResponse( - result: (await _electrumClient!.request( + _sendResponse( + ElectrumWorkerGetVersionResponse( + result: await _electrumClient!.request( ElectrumVersion( clientName: "", - protocolVersion: ["1.4"], + protocolVersion: "1.4", ), - )), - id: request.id)); + ), + id: request.id, + ), + ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart index c51670cdcd..d84dce6670 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -9,6 +9,7 @@ class ScanData { final Map labels; final List labelIndexes; final bool isSingleScan; + final bool shouldSwitchNodes; ScanData({ required this.silentPaymentsWallets, @@ -19,6 +20,7 @@ class ScanData { required this.labels, required this.labelIndexes, required this.isSingleScan, + required this.shouldSwitchNodes, }); factory ScanData.fromHeight(ScanData scanData, int newHeight) { @@ -31,6 +33,7 @@ class ScanData { labels: scanData.labels, labelIndexes: scanData.labelIndexes, isSingleScan: scanData.isSingleScan, + shouldSwitchNodes: scanData.shouldSwitchNodes, ); } @@ -44,6 +47,7 @@ class ScanData { 'labels': labels, 'labelIndexes': labelIndexes, 'isSingleScan': isSingleScan, + 'shouldSwitchNodes': shouldSwitchNodes, }; } @@ -60,6 +64,7 @@ class ScanData { labels: json['labels'] as Map, labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(), isSingleScan: json['isSingleScan'] as bool, + shouldSwitchNodes: json['shouldSwitchNodes'] as bool, ); } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 3eb560ba7f..0c402e7dd2 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -288,6 +288,11 @@ class CWBitcoin extends Bitcoin { return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType); } + @override + bool isReceiveOptionSP(ReceivePageOption option) { + return option.value == BitcoinReceivePageOption.silent_payments.value; + } + @override bool hasSelectedSilentPayments(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; @@ -610,7 +615,7 @@ class CWBitcoin extends Bitcoin { @override @computed bool getScanningActive(Object wallet) { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return bitcoinWallet.silentPaymentsScanningActive; } @@ -620,6 +625,12 @@ class CWBitcoin extends Bitcoin { bitcoinWallet.setSilentPaymentsScanning(active); } + @override + Future allowToSwitchNodesForScanning(Object wallet, bool allow) async { + final bitcoinWallet = wallet as BitcoinWallet; + bitcoinWallet.allowedToSwitchNodesForScanning = allow; + } + @override bool isTestnet(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/di.dart b/lib/di.dart index 19d3e8b8fc..a98f474b27 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -472,10 +472,14 @@ Future setup({ getIt.get(), type: type)); - getIt.registerFactory(() => WalletAddressListViewModel( + getIt.registerFactoryParam( + (ReceivePageOption? addressType, _) => WalletAddressListViewModel( appStore: getIt.get(), yatStore: getIt.get(), - fiatConversionStore: getIt.get())); + fiatConversionStore: getIt.get(), + addressType: addressType, + ), + ); getIt.registerFactory(() => BalanceViewModel( appStore: getIt.get(), @@ -704,12 +708,26 @@ Future setup({ getIt.get(param1: pageOption)); }); - getIt.registerFactory( - () => ReceivePage(addressListViewModel: getIt.get())); - getIt.registerFactory(() => AddressPage( - addressListViewModel: getIt.get(), + getIt.registerFactoryParam( + (ReceivePageOption? addressType, _) => ReceivePage( + addressListViewModel: getIt.get( + param1: addressType, + ), + ), + ); + + getIt.registerFactoryParam( + (ReceivePageOption? addressType, _) => AddressPage( + addressListViewModel: getIt.get( + param1: addressType, + ), dashboardViewModel: getIt.get(), - receiveOptionViewModel: getIt.get())); + receiveOptionViewModel: getIt.get( + param1: addressType, + ), + addressType: addressType, + ), + ); getIt.registerFactoryParam( (WalletAddressListItem? item, _) => @@ -904,11 +922,11 @@ Future setup({ getIt.registerFactory(() => WalletKeysViewModel(getIt.get())); getIt.registerFactory(() => WalletKeysPage(getIt.get())); - + getIt.registerFactory(() => AnimatedURModel(getIt.get())); - getIt.registerFactoryParam((String urQr, _) => - AnimatedURPage(getIt.get(), urQr: urQr)); + getIt.registerFactoryParam( + (String urQr, _) => AnimatedURPage(getIt.get(), urQr: urQr)); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); @@ -1004,8 +1022,8 @@ Future setup({ )); getIt.registerFactory(() => MeldBuyProvider( - wallet: getIt.get().wallet!, - )); + wallet: getIt.get().wallet!, + )); getIt.registerFactoryParam((title, uri) => WebViewPage(title, uri)); @@ -1207,16 +1225,15 @@ Future setup({ final items = args.first as List; final pickAnOption = args[1] as void Function(SelectableOption option)?; final confirmOption = args[2] as void Function(BuildContext contex)?; - return BuyOptionsPage( - items: items, pickAnOption: pickAnOption, confirmOption: confirmOption); + return BuyOptionsPage(items: items, pickAnOption: pickAnOption, confirmOption: confirmOption); }); - getIt.registerFactoryParam, void>((List args, _) { + getIt + .registerFactoryParam, void>((List args, _) { final items = args.first as List; final pickAnOption = args[1] as void Function(SelectableOption option)?; - return PaymentMethodOptionsPage( - items: items, pickAnOption: pickAnOption); + return PaymentMethodOptionsPage(items: items, pickAnOption: pickAnOption); }); getIt.registerFactory(() { @@ -1300,9 +1317,8 @@ Future setup({ getIt.registerFactory( () => CakePayService(getIt.get(), getIt.get())); - getIt.registerFactory( - () => CakePayCardsListViewModel(cakePayService: getIt.get(), - settingsStore: getIt.get())); + getIt.registerFactory(() => CakePayCardsListViewModel( + cakePayService: getIt.get(), settingsStore: getIt.get())); getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); diff --git a/lib/router.dart b/lib/router.dart index 1382da28fd..c461966a62 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -124,6 +124,7 @@ import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/nano_account.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/wallet_info.dart'; @@ -376,11 +377,21 @@ Route createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get()); case Routes.receive: - return CupertinoPageRoute(builder: (_) => getIt.get()); + final args = settings.arguments as Map?; + final addressType = args?['addressType'] as ReceivePageOption?; + + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: addressType), + ); case Routes.addressPage: + final args = settings.arguments as Map?; + final addressType = args?['addressType'] as ReceivePageOption?; + return CupertinoPageRoute( - fullscreenDialog: true, builder: (_) => getIt.get()); + fullscreenDialog: true, + builder: (_) => getIt.get(param1: addressType), + ); case Routes.transactionDetails: return CupertinoPageRoute( @@ -588,7 +599,8 @@ Route createRoute(RouteSettings settings) { case Routes.paymentMethodOptionsPage: final args = settings.arguments as List; - return MaterialPageRoute(builder: (_) => getIt.get(param1: args)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: args)); case Routes.buyWebView: final args = settings.arguments as List; @@ -751,7 +763,8 @@ Route createRoute(RouteSettings settings) { return MaterialPageRoute(builder: (_) => getIt.get()); case Routes.urqrAnimatedPage: - return MaterialPageRoute(builder: (_) => getIt.get(param1: settings.arguments)); + return MaterialPageRoute( + builder: (_) => getIt.get(param1: settings.arguments)); case Routes.homeSettings: return CupertinoPageRoute( diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index c81bca4844..f39cd6a6ed 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -32,9 +32,11 @@ class AddressPage extends BasePage { required this.addressListViewModel, required this.dashboardViewModel, required this.receiveOptionViewModel, + ReceivePageOption? addressType, }) : _cryptoAmountFocus = FocusNode(), _formKey = GlobalKey(), - _amountController = TextEditingController() { + _amountController = TextEditingController(), + _addressType = addressType { _amountController.addListener(() { if (_formKey.currentState!.validate()) { addressListViewModel.changeAmount( @@ -49,6 +51,7 @@ class AddressPage extends BasePage { final ReceiveOptionViewModel receiveOptionViewModel; final TextEditingController _amountController; final GlobalKey _formKey; + ReceivePageOption? _addressType; final FocusNode _cryptoAmountFocus; @@ -190,7 +193,11 @@ class AddressPage extends BasePage { if (addressListViewModel.hasAddressList) { return SelectButton( text: addressListViewModel.buttonTitle, - onTap: () => Navigator.of(context).pushNamed(Routes.receive), + onTap: () => Navigator.pushNamed( + context, + Routes.receive, + arguments: {'addressType': _addressType}, + ), textColor: Theme.of(context).extension()!.textColor, color: Theme.of(context).extension()!.syncedBackgroundColor, borderColor: Theme.of(context).extension()!.cardBorderColor, diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index b16a7b090b..ca7688b228 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:auto_size_text/auto_size_text.dart'; @@ -354,12 +355,108 @@ class CryptoBalanceWidget extends StatelessWidget { ], ), ), - Observer( - builder: (_) => StandardSwitch( - value: dashboardViewModel.silentPaymentsScanningActive, - onTaped: () => _toggleSilentPaymentsScanning(context), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).receive, + child: OutlinedButton( + onPressed: () { + Navigator.pushNamed( + context, + Routes.addressPage, + arguments: { + 'addressType': bitcoin! + .getBitcoinReceivePageOptions() + .where( + (option) => option.value == "Silent Payments", + ) + .first + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).receive, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), ), - ) + ), + SizedBox(width: 24), + Expanded( + child: Semantics( + label: S.of(context).scan, + child: OutlinedButton( + onPressed: () => _toggleSilentPaymentsScanning(context), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Observer( + builder: (_) => StandardSwitch( + value: + dashboardViewModel.silentPaymentsScanningActive, + onTaped: () => + _toggleSilentPaymentsScanning(context), + ), + ), + SizedBox(width: 8), + Text( + S.of(context).scan, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), ], ), ], @@ -466,29 +563,40 @@ class CryptoBalanceWidget extends StatelessWidget { Future _toggleSilentPaymentsScanning(BuildContext context) async { final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; final newValue = !isSilentPaymentsScanningActive; - + final willScan = newValue == true; dashboardViewModel.silentPaymentsScanningActive = newValue; - final needsToSwitch = !isSilentPaymentsScanningActive && - await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false; + if (willScan) { + late bool isElectrsSPEnabled; + try { + isElectrsSPEnabled = await bitcoin! + .getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) + .timeout(const Duration(seconds: 3)); + } on TimeoutException { + isElectrsSPEnabled = false; + } - if (needsToSwitch) { - return showPopUp( + final needsToSwitch = isElectrsSPEnabled == false; + if (needsToSwitch) { + return showPopUp( context: context, builder: (BuildContext context) => AlertWithTwoActions( - alertTitle: S.of(context).change_current_node_title, - alertContent: S.of(context).confirm_silent_payments_switch_node, - rightButtonText: S.of(context).confirm, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - dashboardViewModel.setSilentPaymentsScanning(newValue); - Navigator.of(context).pop(); - }, - actionLeftButton: () { - dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; - Navigator.of(context).pop(); - }, - )); + alertTitle: S.of(context).change_current_node_title, + alertContent: S.of(context).confirm_silent_payments_switch_node, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + dashboardViewModel.allowSilentPaymentsScanning(true); + dashboardViewModel.setSilentPaymentsScanning(true); + Navigator.of(context).pop(); + }, + actionLeftButton: () { + dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; + Navigator.of(context).pop(); + }, + ), + ); + } } return dashboardViewModel.setSilentPaymentsScanning(newValue); @@ -1045,10 +1153,9 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400 - .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400 - .withAlpha(50), width: 0), + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: + BorderSide(color: Colors.grey.shade400.withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), @@ -1104,10 +1211,9 @@ class BalanceRowWidget extends StatelessWidget { ); }, style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400 - .withAlpha(50), - side: BorderSide(color: Colors.grey.shade400 - .withAlpha(50), width: 0), + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: + BorderSide(color: Colors.grey.shade400.withAlpha(50), width: 0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 2c1c213c13..a12552712f 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -1,5 +1,6 @@ +import 'dart:async'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; @@ -35,7 +36,8 @@ class RescanPage extends BasePage { isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, - hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now + hasDatePicker: + !_rescanViewModel.isMwebScan, // disable date picker for mweb for now toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, @@ -69,24 +71,32 @@ class RescanPage extends BasePage { Navigator.of(context).pop(); - final needsToSwitch = - await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + late bool isElectrsSPEnabled; + try { + isElectrsSPEnabled = await bitcoin! + .getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) + .timeout(const Duration(seconds: 3)); + } on TimeoutException { + isElectrsSPEnabled = false; + } + final needsToSwitch = isElectrsSPEnabled == false; if (needsToSwitch) { return showPopUp( - context: navigatorKey.currentState!.context, - builder: (BuildContext _dialogContext) => AlertWithTwoActions( - alertTitle: S.of(_dialogContext).change_current_node_title, - alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, - rightButtonText: S.of(_dialogContext).confirm, - leftButtonText: S.of(_dialogContext).cancel, - actionRightButton: () async { - Navigator.of(_dialogContext).pop(); + context: context, + builder: (BuildContext _dialogContext) => AlertWithTwoActions( + alertTitle: S.of(_dialogContext).change_current_node_title, + alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, + rightButtonText: S.of(_dialogContext).confirm, + leftButtonText: S.of(_dialogContext).cancel, + actionRightButton: () async { + Navigator.of(_dialogContext).pop(); - _rescanViewModel.rescanCurrentWallet(restoreHeight: height); - }, - actionLeftButton: () => Navigator.of(_dialogContext).pop(), - )); + _rescanViewModel.rescanCurrentWallet(restoreHeight: height); + }, + actionLeftButton: () => Navigator.of(_dialogContext).pop(), + ), + ); } _rescanViewModel.rescanCurrentWallet(restoreHeight: height); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 808657f66c..413bbbdaf9 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -428,7 +428,8 @@ abstract class DashboardViewModelBase with Store { // to not cause work duplication, this will do the job as well, it will be slightly less precise // about what happened - but still enough. // if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0", - if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") && !wallet.isHardwareWallet) + if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("") && + !wallet.isHardwareWallet) "private view key is 0", // if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0", if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) @@ -454,6 +455,13 @@ abstract class DashboardViewModelBase with Store { @observable bool silentPaymentsScanningActive = false; + @action + void allowSilentPaymentsScanning(bool allow) { + if (hasSilentPayments) { + bitcoin!.allowToSwitchNodesForScanning(wallet, allow); + } + } + @action void setSilentPaymentsScanning(bool active) { silentPaymentsScanningActive = active; 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 d263b2a11b..9d8136d75e 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 @@ -25,6 +25,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_i import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; +import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -209,6 +210,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo required AppStore appStore, required this.yatStore, required this.fiatConversionStore, + ReceivePageOption? addressType, }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), @@ -216,6 +218,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo .contains(appStore.wallet!.type), amount = '', _settingsStore = appStore.settingsStore, + _addressType = addressType, super(appStore: appStore) { _init(); } @@ -234,6 +237,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final FiatConversionStore fiatConversionStore; final SettingsStore _settingsStore; + final ReceivePageOption? _addressType; double? _fiatRate; String _rawAmount = ''; @@ -264,8 +268,19 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType get type => wallet.type; @computed - WalletAddressListItem get address => - WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + WalletAddressListItem get address { + if (_addressType != null) { + final shouldForceSP = _addressType != null && bitcoin!.isReceiveOptionSP(_addressType!); + if (shouldForceSP) { + return WalletAddressListItem( + address: bitcoin!.getSilentPaymentAddresses(wallet).first.address, + isPrimary: true, + ); + } + } + + return WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false); + } @computed PaymentURI get uri { @@ -354,7 +369,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (isElectrumWallet) { - if (bitcoin!.hasSelectedSilentPayments(wallet)) { + final hasSelectedSP = bitcoin!.hasSelectedSilentPayments(wallet); + final shouldForceSP = _addressType != null && bitcoin!.isReceiveOptionSP(_addressType!); + + if (hasSelectedSP || shouldForceSP) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { final isPrimary = address.id == 0; diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 69c7f73c76..aa228308ba 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "حفظ كلمة المرور الاحتياطية", "save_to_downloads": "ﺕﻼﻳﺰﻨﺘﻟﺍ ﻲﻓ ﻆﻔﺣ", "saved_the_trade_id": "لقد تم حفظ معرف العملية", + "scan": "مسح", "scan_one_block": "مسح كتلة واحدة", "scan_qr_code": "امسح رمز QR ضوئيًا", "scan_qr_code_to_get_address": "امسح ال QR للحصول على العنوان", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index ca1b17ec61..e43d7e1b8b 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Запазване на паролата за възстановяване", "save_to_downloads": "Запазване в Изтегляния", "saved_the_trade_id": "Запазих trade ID-то", + "scan": "Сканиране", "scan_one_block": "Сканирайте един блок", "scan_qr_code": "Сканирайте QR кода, за да получите адреса", "scan_qr_code_to_get_address": "Сканирайте QR кода, за да получите адреса", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index f5e86ecdbc..89160858a2 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Uložit heslo pro zálohy", "save_to_downloads": "Uložit do Stažených souborů", "saved_the_trade_id": "Uložil jsem si ID transakce (trade ID)", + "scan": "Skenovat", "scan_one_block": "Prohledejte jeden blok", "scan_qr_code": "Naskenujte QR kód pro získání adresy", "scan_qr_code_to_get_address": "Prohledejte QR kód a získejte adresu", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d33aa8cc1e..267bcd1cd4 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "Sicherungskennwort speichern", "save_to_downloads": "Unter „Downloads“ speichern", "saved_the_trade_id": "Ich habe die Handels-ID gespeichert", + "scan": "Scan", "scan_one_block": "Einen Block scannen", "scan_qr_code": "QR-Code scannen", "scan_qr_code_to_get_address": "Scannen Sie den QR-Code, um die Adresse zu erhalten", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 3669bd64d5..876213c953 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Save backup password", "save_to_downloads": "Save to Downloads", "saved_the_trade_id": "I've saved the trade ID", + "scan": "Scan", "scan_one_block": "Scan one block", "scan_qr_code": "Scan QR code", "scan_qr_code_to_get_address": "Scan the QR code to get the address", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index abdf69b44a..913925fb17 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "Guardar contraseña de respaldo", "save_to_downloads": "Guardar en Descargas", "saved_the_trade_id": "He salvado comercial ID", + "scan": "Escanear", "scan_one_block": "Escanear un bloque", "scan_qr_code": "Escanear código QR", "scan_qr_code_to_get_address": "Escanea el código QR para obtener la dirección", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 0661582f3d..8232c9b7f3 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Enregistrer le mot de passe de sauvegarde", "save_to_downloads": "Enregistrer dans les téléchargements", "saved_the_trade_id": "J'ai sauvegardé l'ID d'échange", + "scan": "Balayage", "scan_one_block": "Scanner un bloc", "scan_qr_code": "Scannez le QR code", "scan_qr_code_to_get_address": "Scannez le QR code pour obtenir l'adresse", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 76a73ca8e6..9a237eea15 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -596,6 +596,7 @@ "save_backup_password_alert": "Ajiye kalmar sirri ta ajiya", "save_to_downloads": "Ajiye zuwa Zazzagewa", "saved_the_trade_id": "Na ajiye ID na ciniki", + "scan": "Scan", "scan_one_block": "Duba toshe daya", "scan_qr_code": "Gani QR kodin", "scan_qr_code_to_get_address": "Duba lambar QR don samun adireshin", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index ee0f77da78..4845c1ce70 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -596,6 +596,7 @@ "save_backup_password_alert": "बैकअप पासवर्ड सेव करें", "save_to_downloads": "डाउनलोड में सहेजें", "saved_the_trade_id": "मैंने व्यापार बचा लिया है ID", + "scan": "स्कैन", "scan_one_block": "एक ब्लॉक को स्कैन करना", "scan_qr_code": "स्कैन क्यू आर कोड", "scan_qr_code_to_get_address": "पता प्राप्त करने के लिए QR कोड स्कैन करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 9ce575ec10..0da2b29907 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Spremi lozinku za sigurnosnu kopiju", "save_to_downloads": "Spremi u Preuzimanja", "saved_the_trade_id": "Spremio/la sam transakcijski ID", + "scan": "Skenirati", "scan_one_block": "Skenirajte jedan blok", "scan_qr_code": "Skenirajte QR kod", "scan_qr_code_to_get_address": "Skeniraj QR kod za dobivanje adrese", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 2d3c336e33..c1faf5c153 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Պահպանել կրկնօրինակի գաղտնաբառը", "save_to_downloads": "Պահպանել ներբեռնումներում", "saved_the_trade_id": "Ես պահպանել եմ առևտրի ID-ն", + "scan": "Սկանավորել", "scan_one_block": "Սկանավորել մեկ բլոկ", "scan_qr_code": "Սկանավորել QR կոդ", "scan_qr_code_to_get_address": "Սկանավորել QR կոդը հասցեն ստանալու համար", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5b1c1c993c..14199976b9 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -597,6 +597,7 @@ "save_backup_password_alert": "Simpan kata sandi cadangan", "save_to_downloads": "Simpan ke Unduhan", "saved_the_trade_id": "Saya telah menyimpan ID perdagangan", + "scan": "Pindai", "scan_one_block": "Pindai satu blok", "scan_qr_code": "Scan kode QR untuk mendapatkan alamat", "scan_qr_code_to_get_address": "Pindai kode QR untuk mendapatkan alamat", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 9daafca74f..c11a9edd8b 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -596,6 +596,7 @@ "save_backup_password_alert": "Salva password Backup", "save_to_downloads": "Salva in Download", "saved_the_trade_id": "Ho salvato l'ID dello scambio", + "scan": "Scansione", "scan_one_block": "Scansionare un blocco", "scan_qr_code": "Scansiona il codice QR", "scan_qr_code_to_get_address": "Scansiona il codice QR per ottenere l'indirizzo", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 31882a7fb4..f6fcfee59b 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "バックアップパスワードを保存する", "save_to_downloads": "ダウンロードに保存", "saved_the_trade_id": "取引IDを保存しました", + "scan": "スキャン", "scan_one_block": "1つのブロックをスキャンします", "scan_qr_code": "QRコードをスキャン", "scan_qr_code_to_get_address": "QRコードをスキャンして住所を取得します", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 905b1fb8e0..e34b9b08fa 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "백업 비밀번호 저장", "save_to_downloads": "다운로드에 저장", "saved_the_trade_id": "거래 ID를 저장했습니다", + "scan": "주사", "scan_one_block": "하나의 블록을 스캔하십시오", "scan_qr_code": "QR 코드 스캔", "scan_qr_code_to_get_address": "QR 코드를 스캔하여 주소를 얻습니다.", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 52df239642..219f9618be 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "အရန်စကားဝှက်ကို သိမ်းဆည်းပါ။", "save_to_downloads": "ဒေါင်းလုဒ်များထံ သိမ်းဆည်းပါ။", "saved_the_trade_id": "ကုန်သွယ်မှု ID ကို သိမ်းဆည်းပြီးပါပြီ။", + "scan": "စကင်ဖတ်", "scan_one_block": "တစ်ကွက်ကိုစကင်ဖတ်စစ်ဆေးပါ", "scan_qr_code": "QR ကုဒ်ကို စကင်န်ဖတ်ပါ။", "scan_qr_code_to_get_address": "လိပ်စာရယူရန် QR ကုဒ်ကို စကင်န်ဖတ်ပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 0b9d1e7ad9..d0d54c70bf 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Bewaar back-upwachtwoord", "save_to_downloads": "Opslaan in downloads", "saved_the_trade_id": "Ik heb de ruil-ID opgeslagen", + "scan": "Scannen", "scan_one_block": "Scan een blok", "scan_qr_code": "Scan QR-code", "scan_qr_code_to_get_address": "Scan de QR-code om het adres te krijgen", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 4243df066a..c1507a85a9 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Zapisz hasło kopii zapasowej", "save_to_downloads": "Zapisz w Pobranych", "saved_the_trade_id": "Zapisałem ID", + "scan": "Skandować", "scan_one_block": "Zeskanuj jeden blok", "scan_qr_code": "Skanowania QR code", "scan_qr_code_to_get_address": "Zeskanuj kod QR, aby uzyskać adres", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8699a106ab..f5a2e10e77 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -596,6 +596,7 @@ "save_backup_password_alert": "Salvar senha de backup", "save_to_downloads": "Salvar em Downloads", "saved_the_trade_id": "ID da troca salvo", + "scan": "Scan", "scan_one_block": "Escanear um bloco", "scan_qr_code": "Escanear código QR", "scan_qr_code_to_get_address": "Digitalize o código QR para obter o endereço", @@ -956,4 +957,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 0f1974f29d..e50ebcb5d7 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "Сохранить пароль резервной копии", "save_to_downloads": "Сохранить в загрузках", "saved_the_trade_id": "Я сохранил ID сделки", + "scan": "Сканирование", "scan_one_block": "Сканируйте один блок", "scan_qr_code": "Сканировать QR-код", "scan_qr_code_to_get_address": "Отсканируйте QR-код для получения адреса", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 96dcb91b41..d44ae11089 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "บันทึกรหัสผ่านสำรอง", "save_to_downloads": "บันทึกลงดาวน์โหลด", "saved_the_trade_id": "ฉันได้บันทึก ID ของการซื้อขายแล้ว", + "scan": "สแกน", "scan_one_block": "สแกนหนึ่งบล็อก", "scan_qr_code": "สแกนรหัส QR", "scan_qr_code_to_get_address": "สแกน QR code เพื่อรับที่อยู่", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7c5ff15d30..7d8ff4b0bc 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "I-save ang backup na password", "save_to_downloads": "I-save sa mga Pag-download", "saved_the_trade_id": "Nai-save ko na ang trade ID", + "scan": "I -scan", "scan_one_block": "I-scan ang isang bloke", "scan_qr_code": "I-scan ang QR code", "scan_qr_code_to_get_address": "I-scan ang QR code upang makuha ang address", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 53e288ae5c..df3dba146d 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "Yedek parolasını kaydet", "save_to_downloads": "İndirilenlere Kaydet", "saved_the_trade_id": "Takas ID'imi kaydettim", + "scan": "Taramak", "scan_one_block": "Bir bloğu tara", "scan_qr_code": "QR kodunu tarayın", "scan_qr_code_to_get_address": "Adresi getirmek için QR kodunu tara", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 3bcdf74ff2..35caa6ffcf 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "Зберегти пароль резервної копії", "save_to_downloads": "Зберегти до завантажень", "saved_the_trade_id": "Я зберіг ID операції", + "scan": "Сканувати", "scan_one_block": "Сканувати один блок", "scan_qr_code": "Відскануйте QR-код", "scan_qr_code_to_get_address": "Скануйте QR-код для одержання адреси", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2742e21bb9..fb07eec668 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -596,6 +596,7 @@ "save_backup_password_alert": "بیک اپ پاس ورڈ محفوظ کریں۔", "save_to_downloads": "۔ﮟﯾﺮﮐ ﻅﻮﻔﺤﻣ ﮟﯿﻣ ﺯﮈﻮﻟ ﻥﺅﺍﮈ", "saved_the_trade_id": "میں نے تجارتی ID محفوظ کر لی ہے۔", + "scan": "اسکین", "scan_one_block": "ایک بلاک اسکین کریں", "scan_qr_code": "پتہ حاصل کرنے کے لیے QR کوڈ اسکین کریں۔", "scan_qr_code_to_get_address": "پتہ حاصل کرنے کے لئے QR کوڈ کو اسکین کریں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a805809c7f..7ed592bf94 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -593,6 +593,7 @@ "save_backup_password_alert": "Lưu mật khẩu sao lưu", "save_to_downloads": "Lưu vào Tải xuống", "saved_the_trade_id": "Tôi đã lưu ID giao dịch", + "scan": "Quét", "scan_one_block": "Quét một khối", "scan_qr_code": "Quét mã QR", "scan_qr_code_to_get_address": "Quét mã QR để nhận địa chỉ", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index e4dc693635..3c71e8400f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -595,6 +595,7 @@ "save_backup_password_alert": "Pamọ́ ọ̀rọ̀ aṣínà ti ẹ̀dà", "save_to_downloads": "Fipamọ si Awọn igbasilẹ", "saved_the_trade_id": "Mo ti pamọ́ àmì ìdánimọ̀ pàṣípààrọ̀", + "scan": "Ọlọjẹ", "scan_one_block": "Ọlọjẹ ọkan bulọki", "scan_qr_code": "Yan QR koodu", "scan_qr_code_to_get_address": "Ṣayẹwo koodu QR naa lati gba adirẹsi naa", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 90b67252cf..da9b004edd 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -594,6 +594,7 @@ "save_backup_password_alert": "保存备份密码", "save_to_downloads": "保存到下载", "saved_the_trade_id": "我已经保存了交易编号", + "scan": "扫描", "scan_one_block": "扫描一个街区", "scan_qr_code": "扫描二维码", "scan_qr_code_to_get_address": "扫描二维码获取地址", diff --git a/tool/configure.dart b/tool/configure.dart index 9254fe3115..10ceaf0b1a 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -227,11 +227,13 @@ abstract class Bitcoin { List getBitcoinReceivePageOptions(); List getLitecoinReceivePageOptions(); BitcoinAddressType getBitcoinAddressType(ReceivePageOption option); + bool isReceiveOptionSP(ReceivePageOption option); bool hasSelectedSilentPayments(Object wallet); bool isBitcoinReceivePageOption(ReceivePageOption option); BitcoinAddressType getOptionToType(ReceivePageOption option); bool hasTaprootInput(PendingTransaction pendingTransaction); bool getScanningActive(Object wallet); + Future allowToSwitchNodesForScanning(Object wallet, bool allow); Future setScanningActive(Object wallet, bool active); bool isTestnet(Object wallet); From d30c85251e3a89754c2a66f05e009bf11dfb1346 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Sun, 24 Nov 2024 20:26:31 -0300 Subject: [PATCH 24/64] chore: build --- cw_bitcoin/lib/electrum_worker/electrum_worker.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 14b1bab7f4..5b1cea5aa5 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -14,6 +14,8 @@ 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:rxdart/rxdart.dart'; import 'package:sp_scanner/sp_scanner.dart'; From 95bb566d09a7146c3b0b7b9a3183c1e969da6ee8 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 27 Nov 2024 19:01:10 -0300 Subject: [PATCH 25/64] a --- cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- cw_bitcoin/lib/electrum_wallet.dart | 1 + cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- .../lib/electrum_worker/electrum_worker.dart | 118 +++++++++++------- .../electrum_worker/methods/connection.dart | 6 + cw_bitcoin/lib/litecoin_wallet.dart | 70 ++++++++--- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 36 +++--- cw_bitcoin/pubspec.lock | 52 ++++---- cw_bitcoin/pubspec.yaml | 16 +-- cw_bitcoin_cash/pubspec.yaml | 12 +- cw_tron/pubspec.yaml | 12 +- lib/bitcoin/cw_bitcoin.dart | 15 +-- lib/view_model/wallet_restore_view_model.dart | 4 + pubspec_base.yaml | 4 +- 14 files changed, 207 insertions(+), 151 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 399cdd0770..a97845a407 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -84,7 +84,11 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { throw ArgumentError('either scriptHash or network must be provided'); } - this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); + try { + this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); + } catch (_) { + this.scriptHash = ''; + } } factory BitcoinAddressRecord.fromJSON(String jsonSource) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 60ad226351..70eea7020b 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -424,6 +424,7 @@ abstract class ElectrumWalletBase uri: node.uri, useSSL: node.useSSL ?? false, network: network, + walletType: walletInfo.type, ).toJson(), ); } else { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 64b49ba5b0..f7f768d31a 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -414,11 +414,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) async { - final count = (isChange + final count = isChange ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; - final startIndex = (isChange ? receiveAddresses : changeAddresses) + final startIndex = (isChange ? changeAddresses : receiveAddresses) .where((addr) => addr.derivationType == derivationType && addr.addressType == addressType) .length; diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 5b1cea5aa5..055b9f246a 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -23,9 +23,11 @@ import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; ElectrumApiProvider? _electrumClient; - BasedUtxoNetwork? _network; BehaviorSubject>? _scanningStream; + BasedUtxoNetwork? _network; + WalletType? _walletType; + ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) : _electrumClient = electrumClient; @@ -134,6 +136,7 @@ class ElectrumWorker { Future _handleConnect(ElectrumWorkerConnectionRequest request) async { _network = request.network; + _walletType = request.walletType; try { _electrumClient = await ElectrumApiProvider.connect( @@ -198,6 +201,10 @@ class ElectrumWorker { // 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 { + if (status == null) { + return; + } + print("status: $status"); _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( @@ -213,6 +220,10 @@ class ElectrumWorker { final addresses = result.addresses; await Future.wait(addresses.map((addressRecord) async { + if (addressRecord.scriptHash.isEmpty) { + return; + } + final history = await _electrumClient!.request(ElectrumScriptHashGetHistory( scriptHash: addressRecord.scriptHash, )); @@ -313,6 +324,10 @@ class ElectrumWorker { final balanceFutures = >>[]; for (final scripthash in request.scripthashes) { + if (scripthash.isEmpty) { + continue; + } + final balanceFuture = _electrumClient!.request( ElectrumGetScriptHashBalance(scriptHash: scripthash), ); @@ -347,9 +362,15 @@ class ElectrumWorker { final unspents = >{}; await Future.wait(request.scripthashes.map((scriptHash) async { - final scriptHashUnspents = await _electrumClient!.request( - ElectrumScriptHashListUnspent(scriptHash: scriptHash), - ); + if (scriptHash.isEmpty) { + return; + } + + final scriptHashUnspents = await _electrumClient! + .request( + ElectrumScriptHashListUnspent(scriptHash: scriptHash), + ) + .timeout(const Duration(seconds: 3)); if (scriptHashUnspents.isNotEmpty) { unspents[scriptHash] = scriptHashUnspents; @@ -389,65 +410,76 @@ class ElectrumWorker { int? height; bool? isDateValidated; - final transactionHex = await _electrumClient!.request( - ElectrumGetTransactionHex(transactionHash: hash), + final transactionVerbose = await _electrumClient!.request( + ElectrumGetTransactionVerbose(transactionHash: hash), ); + String transactionHex; - 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; + if (transactionVerbose.isNotEmpty) { + transactionHex = transactionVerbose['hex'] as String; + time = transactionVerbose['time'] as int?; + confirmations = transactionVerbose['confirmations'] as int?; + } else { + transactionHex = await _electrumClient!.request( + ElectrumGetTransactionHex(transactionHash: hash), + ); - final blockHash = await http + if (getTime && _walletType == WalletType.bitcoin) { + if (mempoolAPIEnabled) { + try { + // TODO: mempool api class + final txVerbose = await http .get( Uri.parse( - "https://mempool.cakewallet.com/api/v1/block-height/$height", + "https://mempool.cakewallet.com/api/v1/tx/$hash/status", ), ) .timeout(const Duration(seconds: 5)); - if (blockHash.statusCode == 200 && blockHash.body.isNotEmpty) { - final blockResponse = await http + 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/${blockHash.body}", + "https://mempool.cakewallet.com/api/v1/block-height/$height", ), ) .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; + 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 (_) {} + } 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; - } + 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; } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/connection.dart b/cw_bitcoin/lib/electrum_worker/methods/connection.dart index 4ff27665cc..c6c11f4c0f 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/connection.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/connection.dart @@ -5,12 +5,14 @@ class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { required this.uri, required this.network, required this.useSSL, + required this.walletType, this.id, }); final Uri uri; final bool useSSL; final BasedUtxoNetwork network; + final WalletType walletType; final int? id; @override @@ -23,6 +25,9 @@ class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { network: BasedUtxoNetwork.values.firstWhere( (e) => e.toString() == json['network'] as String, ), + walletType: WalletType.values.firstWhere( + (e) => e.toString() == json['walletType'] as String, + ), useSSL: json['useSSL'] as bool, id: json['id'] as int?, ); @@ -34,6 +39,7 @@ class ElectrumWorkerConnectionRequest implements ElectrumWorkerRequest { 'method': method, 'uri': uri.toString(), 'network': network.toString(), + 'walletType': walletType.toString(), 'useSSL': useSSL, }; } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 2fa17e35c1..dc6d215541 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -6,6 +6,7 @@ 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_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; @@ -58,7 +59,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required WalletInfo walletInfo, required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, - Uint8List? seedBytes, + List? seedBytes, String? mnemonic, String? xpub, String? passphrase, @@ -155,20 +156,61 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, 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 LitecoinWallet( mnemonic: mnemonic, password: password, @@ -232,7 +274,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? ELECTRUM_PATH; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; - Uint8List? seedBytes = null; + List? seedBytes = null; final mnemonic = keysData.mnemonic; final passphrase = keysData.passphrase; diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index db5e95694f..140e283f2c 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -117,26 +117,26 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with await ensureMwebAddressUpToIndexExists(20); return; } + } - @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, - ); + @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: hdWallets[derivationType]!, + derivationInfo: derivationInfo, + isChange: isChange, + index: index, + ); } @override diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 8a73546dcb..f5be536a8f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -86,20 +86,16 @@ packages: bitcoin_base: dependency: "direct overridden" description: - path: "." - ref: cake-update-v15 - resolved-ref: "49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f" - url: "https://github.com/cake-tech/bitcoin_base.git" - source: git + path: "/home/rafael/Working/bitcoin_base" + relative: false + source: path version: "4.7.0" blockchain_utils: dependency: "direct main" description: - path: "." - ref: cake-update-v3 - resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" - url: "https://github.com/cake-tech/blockchain_utils" - source: git + path: "/home/rafael/Working/blockchain_utils" + relative: false + source: path version: "3.3.0" bluez: dependency: transitive @@ -415,10 +411,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" url: "https://pub.dev" source: hosted - version: "0.3.0+2" + version: "0.3.3" googleapis_auth: dependency: transitive description: @@ -471,10 +467,10 @@ packages: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http2: dependency: transitive description: @@ -849,10 +845,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: @@ -917,11 +913,9 @@ packages: sp_scanner: dependency: "direct main" description: - path: "." - ref: cake-update-v3 - resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b" - url: "https://github.com/cake-tech/sp_scanner.git" - source: git + path: "/home/rafael/Working/sp_scanner" + relative: false + source: path version: "0.0.1" stack_trace: dependency: transitive @@ -1047,18 +1041,26 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "3.0.1" xdg_directories: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index aca94997e8..3dc5571019 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,16 +27,12 @@ dependencies: rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils cw_mweb: path: ../cw_mweb grpc: ^3.2.4 sp_scanner: - git: - url: https://github.com/cake-tech/sp_scanner.git - ref: cake-update-v3 + path: /home/rafael/Working/sp_scanner bech32: git: url: https://github.com/cake-tech/bech32.git @@ -62,13 +58,9 @@ dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v15 + path: /home/rafael/Working/bitcoin_base blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9947b1b277..6d442a7368 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -26,9 +26,7 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils dev_dependencies: flutter_test: @@ -40,13 +38,9 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v15 + path: /home/rafael/Working/bitcoin_base blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 9da2217bb7..17e0caa8ad 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -16,13 +16,9 @@ dependencies: cw_evm: path: ../cw_evm on_chain: - git: - url: https://github.com/cake-tech/on_chain.git - ref: cake-update-v3 + path: /home/rafael/Working/On_chain blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -37,9 +33,7 @@ dev_dependencies: dependency_overrides: blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + path: /home/rafael/Working/blockchain_utils flutter: # assets: diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 0c402e7dd2..793e711419 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -372,8 +372,6 @@ class CWBitcoin extends Bitcoin { ); } - oldList.addAll(bitcoin!.getOldSPDerivationInfos()); - return oldList; } @@ -385,24 +383,13 @@ class CWBitcoin extends Bitcoin { }) async { final list = []; - late BasedUtxoNetwork network; - switch (node.type) { - case WalletType.litecoin: - network = LitecoinNetwork.mainnet; - break; - case WalletType.bitcoin: - default: - network = BitcoinNetwork.mainnet; - break; - } - var electrumSeedBytes; try { electrumSeedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); } catch (e) { print("electrum_v2 seed error: $e"); - if (passphrase != null && passphrase.isEmpty) { + if (passphrase == null || passphrase.isEmpty) { try { // TODO: language pick electrumSeedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 4c17998f41..c8c3f59812 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -268,6 +268,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { // is restoring? = add old used derivations final oldList = bitcoin!.getOldDerivationInfos(list); + if (walletType == WalletType.bitcoin) { + oldList.addAll(bitcoin!.getOldSPDerivationInfos()); + } + return oldList; case WalletType.nano: String? mnemonic = credentials['seed'] as String?; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index f947095dc6..6c731ec4dc 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -142,9 +142,7 @@ dependency_overrides: flutter_secure_storage_platform_interface: 1.0.2 protobuf: ^3.1.0 bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v15 + path: /home/rafael/Working/bitcoin_base ffi: 2.1.0 flutter_icons: From a2bbd6a4711fc766f58b4658d4355fef40f4cd50 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 28 Nov 2024 13:11:44 -0300 Subject: [PATCH 26/64] fix: change addr --- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index f7f768d31a..5ff99d4f8e 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -181,7 +181,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final address = changeAddresses.firstWhere( // TODO: feature to choose change type - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh), + (addressRecord) => _isUnusedChangeAddressByType(addressRecord, SegwitAddresType.p2wpkh), ); return address; } @@ -568,6 +568,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.addressType == type; + bool _isUnusedChangeAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + return addr.isChange && !addr.isUsed && addr.addressType == type; + } + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { return !addr.isChange && !addr.isUsed && addr.addressType == type; } From 143e5d74297ddf9985ce7b474fe6f27ba3261b02 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 28 Nov 2024 15:37:15 -0300 Subject: [PATCH 27/64] Revert 95bb566d09a7146c3b0b7b9a3183c1e969da6ee8 --- cw_bitcoin/pubspec.lock | 52 +++++++++++++++++------------------- cw_bitcoin/pubspec.yaml | 16 ++++++++--- cw_bitcoin_cash/pubspec.yaml | 12 ++++++--- cw_tron/pubspec.yaml | 12 ++++++--- pubspec_base.yaml | 4 ++- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index f5be536a8f..8a73546dcb 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -86,16 +86,20 @@ packages: bitcoin_base: dependency: "direct overridden" description: - path: "/home/rafael/Working/bitcoin_base" - relative: false - source: path + path: "." + 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: "/home/rafael/Working/blockchain_utils" - relative: false - source: path + path: "." + ref: cake-update-v3 + resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" + url: "https://github.com/cake-tech/blockchain_utils" + source: git version: "3.3.0" bluez: dependency: transitive @@ -411,10 +415,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.0+2" googleapis_auth: dependency: transitive description: @@ -467,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: @@ -845,10 +849,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.2.2" shared_preferences_windows: dependency: transitive description: @@ -913,9 +917,11 @@ packages: sp_scanner: dependency: "direct main" description: - path: "/home/rafael/Working/sp_scanner" - relative: false - source: path + path: "." + ref: cake-update-v3 + resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b" + url: "https://github.com/cake-tech/sp_scanner.git" + source: git version: "0.0.1" stack_trace: dependency: transitive @@ -1041,26 +1047,18 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.4.3" xdg_directories: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 3dc5571019..aca94997e8 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,12 +27,16 @@ dependencies: rxdart: ^0.28.0 cryptography: ^2.0.5 blockchain_utils: - path: /home/rafael/Working/blockchain_utils + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 cw_mweb: path: ../cw_mweb grpc: ^3.2.4 sp_scanner: - path: /home/rafael/Working/sp_scanner + git: + url: https://github.com/cake-tech/sp_scanner.git + ref: cake-update-v3 bech32: git: url: https://github.com/cake-tech/bech32.git @@ -58,9 +62,13 @@ dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 blockchain_utils: - path: /home/rafael/Working/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/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 6d442a7368..9947b1b277 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -26,7 +26,9 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data blockchain_utils: - path: /home/rafael/Working/blockchain_utils + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 dev_dependencies: flutter_test: @@ -38,9 +40,13 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 blockchain_utils: - path: /home/rafael/Working/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_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 17e0caa8ad..9da2217bb7 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -16,9 +16,13 @@ dependencies: cw_evm: path: ../cw_evm on_chain: - path: /home/rafael/Working/On_chain + git: + url: https://github.com/cake-tech/on_chain.git + ref: cake-update-v3 blockchain_utils: - path: /home/rafael/Working/blockchain_utils + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -33,7 +37,9 @@ dev_dependencies: dependency_overrides: blockchain_utils: - path: /home/rafael/Working/blockchain_utils + git: + url: https://github.com/cake-tech/blockchain_utils + ref: cake-update-v3 flutter: # assets: diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 6c731ec4dc..f947095dc6 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -142,7 +142,9 @@ dependency_overrides: flutter_secure_storage_platform_interface: 1.0.2 protobuf: ^3.1.0 bitcoin_base: - path: /home/rafael/Working/bitcoin_base + git: + url: https://github.com/cake-tech/bitcoin_base.git + ref: cake-update-v15 ffi: 2.1.0 flutter_icons: From 8264d387be6f4ea24906b123159d4e56d225c084 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 9 Dec 2024 13:18:21 -0300 Subject: [PATCH 28/64] fix: null check --- cw_bitcoin/lib/bitcoin_wallet.dart | 12 ++++++ cw_bitcoin/lib/electrum_wallet.dart | 60 +++++++++++------------------ 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 20a399a880..a7e442e9df 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -287,6 +287,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { hdWallets[CWBitcoinDerivationType.old_electrum] = hdWallets[CWBitcoinDerivationType.electrum]!; } + + 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( diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 19199302ad..6630d43cfc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1229,19 +1229,6 @@ abstract class ElectrumWalletBase @action Future addCoinInfo(BitcoinUnspent coin) async { - final newInfo = UnspentCoinsInfo( - walletId: id, - hash: coin.hash, - isFrozen: coin.isFrozen, - isSending: coin.isSending, - noteRaw: coin.note, - address: coin.bitcoinAddressRecord.address, - value: coin.value, - vout: coin.vout, - isChange: coin.isChange, - isSilentPayment: coin.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord, - ); - // Check if the coin is already in the unspentCoinsInfo for the wallet final existingCoinInfo = unspentCoinsInfo.values .firstWhereOrNull((element) => element.walletId == walletInfo.id && element == coin); @@ -1257,7 +1244,7 @@ abstract class ElectrumWalletBase value: coin.value, vout: coin.vout, isChange: coin.isChange, - isSilentPayment: coin is BitcoinSilentPaymentsUnspent, + isSilentPayment: coin.address is BitcoinReceivedSPAddressRecord, ); await unspentCoinsInfo.add(newInfo); @@ -1423,12 +1410,15 @@ abstract class ElectrumWalletBase final bundle = await getTransactionExpanded(hash: txId); final outputs = bundle.originalTransaction.outputs; - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); + final ownAddresses = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); - // look for a change address in the outputs - final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any((element) => - element.address == - BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network))); + final receiverAmount = outputs + .where( + (output) => !ownAddresses.contains( + BitcoinAddressUtils.addressFromOutputScript(output.scriptPubKey, network), + ), + ) + .fold(0, (sum, output) => sum + output.amount.toInt()); if (receiverAmount == 0) { throw Exception("Receiver output not found."); @@ -1474,7 +1464,7 @@ abstract class ElectrumWalletBase final outTransaction = inputTransaction.outputs[vout]; final address = BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network); - // allInputsAmount += outTransaction.amount.toInt(); + allInputsAmount += outTransaction.amount.toInt(); final addressRecord = walletAddresses.allAddresses.firstWhere((element) => element.address == address); @@ -1566,24 +1556,20 @@ abstract class ElectrumWalletBase for (final utxo in unusedUtxos) { final address = RegexUtils.addressTypeFromStr(utxo.address, network); - final privkey = generateECPrivate( - hd: utxo.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: utxo.bitcoinAddressRecord.index, - network: network, - ); + final privkey = ECPrivate.fromBip32(bip32: bip32); privateKeys.add(privkey); - utxos.add(UtxoWithAddress( - utxo: BitcoinUtxo( - txHash: utxo.hash, - value: BigInt.from(utxo.value), - vout: utxo.vout, - scriptType: _getScriptType(address)), - ownerDetails: - UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), - )); + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utxo.hash, + value: BigInt.from(utxo.value), + vout: utxo.vout, + scriptType: BitcoinAddressUtils.getScriptType(address)), + ownerDetails: + UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), + ), + ); allInputsAmount += utxo.value; remainingFee -= utxo.value; @@ -1636,7 +1622,7 @@ abstract class ElectrumWalletBase } // Identify all change outputs - final changeAddresses = walletAddresses.allAddresses.where((element) => element.isChange); + final changeAddresses = walletAddresses.changeAddresses; final List changeOutputs = outputs .where((output) => changeAddresses .any((element) => element.address == output.address.toAddress(network))) From 183fd89470452712d7d3cdbd492a6d819794f154 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 9 Dec 2024 14:00:27 -0300 Subject: [PATCH 29/64] chore: deps --- cw_bitcoin/pubspec.lock | 36 ++++++++++++++++++++++-------------- cw_bitcoin/pubspec.yaml | 2 +- cw_bitcoin_cash/pubspec.yaml | 2 +- cw_core/pubspec.lock | 4 ++-- cw_monero/pubspec.lock | 4 ++-- pubspec_base.yaml | 2 +- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 7932cf87a3..48c4b6a86a 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -88,8 +88,8 @@ packages: description: path: "." ref: cake-update-v15 - resolved-ref: "49db5748d2edc73c0c8213e11ab6a39fa3a7ff7f" - url: "https://github.com/cake-tech/bitcoin_base.git" + resolved-ref: "82306ae21ea247ba400b9dc3823631d69ae45699" + url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" blockchain_utils: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" url: "https://pub.dev" source: hosted - version: "0.3.0+2" + version: "0.3.3" googleapis_auth: dependency: transitive description: @@ -471,10 +471,10 @@ packages: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http2: dependency: transitive description: @@ -849,10 +849,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: @@ -1031,10 +1031,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: @@ -1047,18 +1047,26 @@ packages: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "3.0.1" xdg_directories: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index aca94997e8..9c7c41420c 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -63,7 +63,7 @@ dependency_overrides: protobuf: ^3.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base.git + url: https://github.com/cake-tech/bitcoin_base ref: cake-update-v15 blockchain_utils: git: diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9947b1b277..f6264564c4 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -41,7 +41,7 @@ dependency_overrides: watcher: ^1.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base.git + url: https://github.com/cake-tech/bitcoin_base ref: cake-update-v15 blockchain_utils: git: diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index c12839a19d..44ef15a417 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -722,10 +722,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index d8a3a4ff82..f4be439b7f 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -829,10 +829,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/pubspec_base.yaml b/pubspec_base.yaml index f947095dc6..789bb3db5b 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -143,7 +143,7 @@ dependency_overrides: protobuf: ^3.1.0 bitcoin_base: git: - url: https://github.com/cake-tech/bitcoin_base.git + url: https://github.com/cake-tech/bitcoin_base ref: cake-update-v15 ffi: 2.1.0 From f702a19965c50664d05f26b0d971a0878c9d9032 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 9 Dec 2024 17:20:38 -0300 Subject: [PATCH 30/64] chore: printVs --- .../lib/bitcoin_hardware_wallet_service.dart | 1 - cw_bitcoin/lib/bitcoin_wallet.dart | 15 ++++++++------- cw_bitcoin/lib/electrum_transaction_info.dart | 9 +++++---- .../lib/electrum_worker/electrum_worker.dart | 15 ++++++++------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index 3f1a7a4c24..15ab4f6c13 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -5,7 +5,6 @@ import 'package:blockchain_utils/blockchain_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'; -import 'package:cw_core/utils/print_verbose.dart'; class BitcoinHardwareWalletService { BitcoinHardwareWalletService(this.ledgerConnection); diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index a7e442e9df..1846f225c4 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -21,6 +21,7 @@ import 'package:cw_core/crypto_currency.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/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; // import 'package:cw_core/wallet_type.dart'; @@ -134,14 +135,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { - print("electrum_v2 seed error: $e"); + printV("electrum_v2 seed error: $e"); try { seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { - print("electrum_v1 seed error: $e"); + printV("electrum_v1 seed error: $e"); } } @@ -265,14 +266,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { - print("electrum_v2 seed error: $e"); + printV("electrum_v2 seed error: $e"); try { seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { - print("electrum_v1 seed error: $e"); + printV("electrum_v1 seed error: $e"); } } @@ -629,7 +630,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // .toList(), // ); - // print("registered: $registered"); + // printV("registered: $registered"); } @action @@ -657,7 +658,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { switch (workerMethod) { case ElectrumRequestMethods.tweaksSubscribeMethod: if (workerError != null) { - print(messageJson); + printV(messageJson); // _onConnectionStatusChange(ConnectionStatus.failed); break; } @@ -827,7 +828,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // return historiesWithDetails; // } catch (e) { - // print("fetchTransactions $e"); + // printV("fetchTransactions $e"); // return {}; // } } diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b5e4bede55..43e462f7c7 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -6,6 +6,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; @@ -173,10 +174,10 @@ class ElectrumTransactionInfo extends TransactionInfo { } } } catch (e) { - print(bundle.originalTransaction.txId()); - print("original: ${bundle.originalTransaction}"); - print("bundle.inputs: ${bundle.originalTransaction.inputs}"); - print("ins: ${bundle.ins}"); + printV(bundle.originalTransaction.txId()); + printV("original: ${bundle.originalTransaction}"); + printV("bundle.inputs: ${bundle.originalTransaction.inputs}"); + printV("ins: ${bundle.ins}"); rethrow; } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 055b9f246a..326fcf64ad 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -15,6 +15,7 @@ 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/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart' as http; import 'package:rxdart/rxdart.dart'; @@ -49,7 +50,7 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { - print("Worker: received message: $message"); + printV("Worker: received message: $message"); try { Map messageJson; @@ -107,7 +108,7 @@ class ElectrumWorker { ); break; case ElectrumWorkerMethods.stopScanningMethod: - print("Worker: received message: $message"); + printV("Worker: received message: $message"); await _handleStopScanning( ElectrumWorkerStopScanningRequest.fromJson(messageJson), ); @@ -205,7 +206,7 @@ class ElectrumWorker { return; } - print("status: $status"); + printV("status: $status"); _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( result: {address: req.onResponse(status)}, @@ -756,13 +757,13 @@ class ElectrumWorker { return; } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); + printV(stacktrace); + printV(e.toString()); } } } catch (e, stacktrace) { - print(stacktrace); - print(e.toString()); + printV(stacktrace); + printV(e.toString()); } syncHeight = tweakHeight; From 51590a668db61e17e33df34684c4c5dbacd416dc Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 19 Dec 2024 13:18:58 -0300 Subject: [PATCH 31/64] feat: tx broadcast, error handling, remove electrum.dart --- cw_bitcoin/lib/bitcoin_address_record.dart | 38 +- cw_bitcoin/lib/bitcoin_wallet.dart | 597 ++++++++++++++++- cw_bitcoin/lib/electrum.dart | 621 ------------------ cw_bitcoin/lib/electrum_transaction_info.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 140 +--- .../lib/electrum_worker/electrum_worker.dart | 18 +- cw_bitcoin/lib/litecoin_wallet.dart | 2 - .../lib/pending_bitcoin_transaction.dart | 66 +- .../src/pending_bitcoin_cash_transaction.dart | 76 ++- res/values/strings_pt.arb | 4 +- 10 files changed, 730 insertions(+), 834 deletions(-) delete mode 100644 cw_bitcoin/lib/electrum.dart diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index a97845a407..f75fee08bf 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; abstract class BaseBitcoinAddressRecord { @@ -84,11 +85,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { throw ArgumentError('either scriptHash or network must be provided'); } - try { - this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); - } catch (_) { - this.scriptHash = ''; - } + this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); } factory BitcoinAddressRecord.fromJSON(String jsonSource) { @@ -211,7 +208,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { } class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { - final ECPrivate spendKey; + final String tweak; BitcoinReceivedSPAddressRecord( super.address, { @@ -220,11 +217,32 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.balance = 0, super.name = '', super.isUsed = false, - required this.spendKey, + required this.tweak, super.addressType = SegwitAddresType.p2tr, super.labelHex, }) : super(isHidden: true); + SilentPaymentOwner getSPWallet( + List silentPaymentsWallets, [ + BasedUtxoNetwork network = BitcoinNetwork.mainnet, + ]) { + final spAddress = silentPaymentsWallets.firstWhere( + (wallet) => wallet.toAddress(network) == this.address, + orElse: () => throw ArgumentError('SP wallet not found'), + ); + + return spAddress; + } + + ECPrivate getSpendKey( + List silentPaymentsWallets, [ + BasedUtxoNetwork network = BitcoinNetwork.mainnet, + ]) { + return getSPWallet(silentPaymentsWallets, network) + .b_spend + .tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(tweak))); + } + factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; @@ -236,9 +254,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { 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), + tweak: decoded['tweak'] as String? ?? '', ); } @@ -252,6 +268,6 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { 'balance': balance, 'type': addressType.toString(), 'labelHex': labelHex, - 'spend_key': spendKey.toString(), + 'tweak': tweak, }); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 1846f225c4..ea805b1687 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -4,9 +4,11 @@ import 'dart:convert'; 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_credentials.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; +import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/pending_bitcoin_transaction.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'; @@ -17,15 +19,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/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; -// import 'package:cw_core/wallet_type.dart'; -// import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; @@ -251,7 +252,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final passphrase = keysData.passphrase; if (mnemonic != null) { - for (final derivation in walletInfo.derivations ?? []) { + final derivations = walletInfo.derivations ?? []; + + for (final derivation in derivations) { if (derivation.description?.contains("SP") ?? false) { continue; } @@ -289,16 +292,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { hdWallets[CWBitcoinDerivationType.electrum]!; } - 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; + if (derivations.isEmpty) { + 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; + } } } @@ -914,4 +919,566 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { super.syncStatusReaction(syncStatus); } } + + @override + Future calcFee({ + required List utxos, + required List outputs, + String? memo, + required int feeRate, + List? inputPrivKeyInfos, + List? vinOutpoints, + }) async => + feeRate * + BitcoinTransactionBuilder.estimateTransactionSize( + utxos: utxos, + outputs: outputs, + network: network, + memo: memo, + inputPrivKeyInfos: inputPrivKeyInfos, + vinOutpoints: vinOutpoints, + ); + + @override + TxCreateUtxoDetails createUTXOS({ + required bool sendAll, + bool paysToSilentPayment = false, + int credentialsAmount = 0, + int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { + List utxos = []; + List vinOutpoints = []; + List inputPrivKeyInfos = []; + final publicKeys = {}; + int allInputsAmount = 0; + bool spendsSilentPayment = false; + bool spendsUnconfirmedTX = false; + + int leftAmount = credentialsAmount; + var availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + return true; + }).toList(); + final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); + + for (int i = 0; i < availableInputs.length; i++) { + final utx = availableInputs[i]; + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; + + if (paysToSilentPayment) { + // Check inputs for shared secret derivation + if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) { + throw BitcoinTransactionSilentPaymentsNotSupported(); + } + } + + allInputsAmount += utx.value; + leftAmount = leftAmount - utx.value; + + final address = RegexUtils.addressTypeFromStr(utx.address, network); + ECPrivate? privkey; + bool? isSilentPayment = false; + + if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).getSpendKey( + (walletAddresses as BitcoinWalletAddresses).silentPaymentWallets, + network, + ); + spendsSilentPayment = true; + isSilentPayment = true; + } else if (!isHardwareWallet) { + 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)); + String pubKeyHex; + + if (privkey != null) { + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + address.type == SegwitAddresType.p2tr, + tweak: !isSilentPayment, + )); + + pubKeyHex = privkey.getPublic().toHex(); + } else { + pubKeyHex = walletAddresses.hdWallet + .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)) + .publicKey + .toHex(); + } + + if (utx.bitcoinAddressRecord is BitcoinAddressRecord) { + final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord) + .derivationInfo + .derivationPath + .toString(); + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + } + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: BitcoinAddressUtils.getScriptType(address), + isSilentPayment: isSilentPayment, + ), + ownerDetails: UtxoAddressDetails( + publicKey: pubKeyHex, + address: address, + ), + ), + ); + + // sendAll continues for all inputs + if (!sendAll) { + bool amountIsAcquired = leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } + } + } + + if (utxos.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + return TxCreateUtxoDetails( + availableInputs: availableInputs, + unconfirmedCoins: unconfirmedCoins, + utxos: utxos, + vinOutpoints: vinOutpoints, + inputPrivKeyInfos: inputPrivKeyInfos, + publicKeys: publicKeys, + allInputsAmount: allInputsAmount, + spendsSilentPayment: spendsSilentPayment, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } + + @override + Future estimateSendAllTx( + List outputs, + int feeRate, { + String? memo, + bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment); + + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change + int amount = utxoDetails.allInputsAmount - fee; + + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); + } + + // Attempting to send less than the dust limit + if (isBelowDust(amount)) { + throw BitcoinTransactionNoDustException(); + } + + if (outputs.length == 1) { + outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + isSendAll: true, + hasChange: false, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } + + @override + Future estimateTxForAmount( + int credentialsAmount, + List outputs, + int feeRate, { + List updatedOutputs = const [], + int? inputsCount, + String? memo, + bool? useUnconfirmed, + bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + // Attempting to send less than the dust limit + if (isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + + final utxoDetails = createUTXOS( + sendAll: false, + credentialsAmount: credentialsAmount, + inputsCount: inputsCount, + paysToSilentPayment: hasSilentPayment, + ); + + final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; + final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX && + utxoDetails.utxos.length == + utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length; + + // How much is being spent - how much is being sent + int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount; + + if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + updatedOutputs: updatedOutputs, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + hasSilentPayment: hasSilentPayment, + ); + } + + throw BitcoinTransactionWrongBalanceException(); + } + + final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, + outputs: updatedOutputs, + ); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); + + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); + int fee = await calcFee( + utxos: utxoDetails.utxos, + // 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, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); + + updatedOutputs.clear(); + updatedOutputs.addAll(temp); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + int amount = credentialsAmount; + final lastOutput = updatedOutputs.last; + final amountLeftForChange = amountLeftForChangeAndFee - fee; + + if (isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + updatedOutputs.removeLast(); + outputs.removeLast(); + + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + feeRate, + updatedOutputs: updatedOutputs, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } else { + // Here, lastOutput already is change, return the amount left without the fee to the user's address. + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } + } + + @override + Future createTransaction(Object credentials) async { + try { + final outputs = []; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final hasMultiDestination = transactionCredentials.outputs.length > 1; + final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; + final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; + + int credentialsAmount = 0; + bool hasSilentPayment = false; + + for (final out in transactionCredentials.outputs) { + final outputAmount = out.formattedCryptoAmount!; + + if (!sendAll && isBelowDust(outputAmount)) { + throw BitcoinTransactionNoDustException(); + } + + if (hasMultiDestination) { + if (out.sendAll) { + throw BitcoinTransactionWrongBalanceException(); + } + } + + credentialsAmount += outputAmount; + + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, network); + final isSilentPayment = address is SilentPaymentAddress; + + if (isSilentPayment) { + hasSilentPayment = true; + } + + if (sendAll) { + // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(0), + isSilentPayment: isSilentPayment, + )); + } else { + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(outputAmount), + isSilentPayment: isSilentPayment, + )); + } + } + + final feeRateInt = transactionCredentials.feeRate != null + ? transactionCredentials.feeRate! + : feeRate(transactionCredentials.priority!); + + EstimatedTxResult estimatedTx; + final updatedOutputs = outputs + .map((e) => BitcoinOutput( + address: e.address, + value: e.value, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(); + + if (sendAll) { + estimatedTx = await estimateSendAllTx( + updatedOutputs, + feeRateInt, + memo: memo, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + estimatedTx = await estimateTxForAmount( + credentialsAmount, + outputs, + feeRateInt, + updatedOutputs: updatedOutputs, + memo: memo, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } + + if (walletInfo.isHardwareWallet) { + final transaction = await buildHardwareWalletTransaction( + utxos: estimatedTx.utxos, + outputs: updatedOutputs, + publicKeys: estimatedTx.publicKeys, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + ); + + return PendingBitcoinTransaction( + transaction, + type, + sendWorker: sendWorker, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + }); + } + + final txb = BitcoinTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: updatedOutputs, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: !estimatedTx.spendsUnconfirmedTX, + ); + + bool hasTaprootInputs = false; + + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + String error = "Cannot find private key."; + + ECPrivateInfo? key; + + if (estimatedTx.inputPrivKeyInfos.isEmpty) { + error += "\nNo private keys generated."; + } else { + error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}"; + + try { + key = estimatedTx.inputPrivKeyInfos.firstWhere((element) { + final elemPubkey = element.privkey.getPublic().toHex(); + if (elemPubkey == publicKey) { + return true; + } else { + error += "\nExpected: $publicKey"; + error += "\nPubkey: $elemPubkey"; + return false; + } + }); + } catch (_) { + throw Exception(error); + } + } + + if (key == null) { + throw Exception(error); + } + + if (utxo.utxo.isP2tr()) { + hasTaprootInputs = true; + return key.privkey.signTapRoot( + txDigest, + sighash: sighash, + tweak: utxo.utxo.isSilentPayment != true, + ); + } else { + return key.privkey.signInput(txDigest, sigHash: sighash); + } + }); + + return PendingBitcoinTransaction( + transaction, + type, + sendWorker: sendWorker, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + hasSilentPayment: hasSilentPayment, + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + if (estimatedTx.spendsSilentPayment) { + transactionHistory.transactions.values.forEach((tx) { + tx.unspents?.removeWhere( + (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); + transactionHistory.addOne(tx); + }); + } + + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + + await updateBalance(); + }); + } catch (e, s) { + print([e, s]); + throw e; + } + } } diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart deleted file mode 100644 index f15a8d5482..0000000000 --- a/cw_bitcoin/lib/electrum.dart +++ /dev/null @@ -1,621 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; - -String jsonrpc( - {required String method, - required List params, - required int id, - double version = 2.0}) => - '{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n'; - -class SocketTask { - SocketTask({required this.isSubscription, this.completer, this.subject}); - - final Completer? completer; - final BehaviorSubject? subject; - final bool isSubscription; -} - -class ElectrumClient { - ElectrumClient() - : _id = 0, - _isConnected = false, - _tasks = {}, - _errors = {}, - unterminatedString = ''; - - static const connectionTimeout = Duration(seconds: 5); - static const aliveTimerDuration = Duration(seconds: 4); - - bool get isConnected => _isConnected; - Socket? socket; - void Function(ConnectionStatus)? onConnectionStatusChange; - int _id; - final Map _tasks; - Map get tasks => _tasks; - final Map _errors; - ConnectionStatus _connectionStatus = ConnectionStatus.disconnected; - bool _isConnected; - Timer? _aliveTimer; - String unterminatedString; - - Uri? uri; - bool? useSSL; - - Future connectToUri(Uri uri, {bool? useSSL}) async { - this.uri = uri; - if (useSSL != null) { - this.useSSL = useSSL; - } - await connect(host: uri.host, port: uri.port); - } - - Future connect({required String host, required int port}) async { - _setConnectionStatus(ConnectionStatus.connecting); - - try { - await socket?.close(); - } catch (_) {} - socket = null; - - try { - if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { - socket = await Socket.connect(host, port, timeout: connectionTimeout); - } else { - socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); - } - } catch (e) { - if (e is HandshakeException) { - useSSL = !(useSSL ?? false); - } - - if (_connectionStatus != ConnectionStatus.connecting) { - _setConnectionStatus(ConnectionStatus.failed); - } - - return; - } - - if (socket == null) { - if (_connectionStatus != ConnectionStatus.connecting) { - _setConnectionStatus(ConnectionStatus.failed); - } - - return; - } - - // use ping to determine actual connection status since we could've just not timed out yet: - // _setConnectionStatus(ConnectionStatus.connected); - - socket!.listen( - (Uint8List event) { - try { - final msg = utf8.decode(event.toList()); - final messagesList = msg.split("\n"); - for (var message in messagesList) { - if (message.isEmpty) { - continue; - } - _parseResponse(message); - } - } catch (e) { - printV("socket.listen: $e"); - } - }, - onError: (Object error) { - final errorMsg = error.toString(); - printV(errorMsg); - unterminatedString = ''; - socket = null; - }, - onDone: () { - printV("SOCKET CLOSED!!!!!"); - unterminatedString = ''; - try { - if (host == socket?.address.host || socket == null) { - _setConnectionStatus(ConnectionStatus.disconnected); - socket?.destroy(); - socket = null; - } - } catch (e) { - printV("onDone: $e"); - } - }, - cancelOnError: true, - ); - - keepAlive(); - } - - void _parseResponse(String message) { - try { - final response = json.decode(message) as Map; - _handleResponse(response); - } on FormatException catch (e) { - final msg = e.message.toLowerCase(); - - if (e.source is String) { - unterminatedString += e.source as String; - } - - if (msg.contains("not a subtype of type")) { - unterminatedString += e.source as String; - return; - } - - if (isJSONStringCorrect(unterminatedString)) { - final response = json.decode(unterminatedString) as Map; - _handleResponse(response); - unterminatedString = ''; - } - } on TypeError catch (e) { - if (!e.toString().contains('Map') && - !e.toString().contains('Map')) { - return; - } - - unterminatedString += message; - - if (isJSONStringCorrect(unterminatedString)) { - final response = json.decode(unterminatedString) as Map; - _handleResponse(response); - // unterminatedString = null; - unterminatedString = ''; - } - } catch (e) { - printV("parse $e"); - } - } - - void keepAlive() { - _aliveTimer?.cancel(); - _aliveTimer = Timer.periodic(aliveTimerDuration, (_) async => ping()); - } - - Future ping() async { - try { - await callWithTimeout(method: 'server.ping'); - _setConnectionStatus(ConnectionStatus.connected); - } catch (_) { - _setConnectionStatus(ConnectionStatus.disconnected); - } - } - - Future> version() => - call(method: 'server.version', params: ["", "1.4"]).then((dynamic result) { - if (result is List) { - return result.map((dynamic val) => val.toString()).toList(); - } - - return []; - }); - - Future> getBalance(String scriptHash) => - call(method: 'blockchain.scripthash.get_balance', params: [scriptHash]) - .then((dynamic result) { - if (result is Map) { - return result; - } - - return {}; - }); - - Future>> getHistory(String scriptHash) => - call(method: 'blockchain.scripthash.get_history', params: [scriptHash]) - .then((dynamic result) { - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - return val; - } - - return {}; - }).toList(); - } - - return []; - }); - - Future>> getListUnspent(String scriptHash) async { - final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]); - - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - return val; - } - - return {}; - }).toList(); - } - - return []; - } - - Future>> getMempool(String scriptHash) => - call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash]) - .then((dynamic result) { - if (result is List) { - return result.map((dynamic val) { - if (val is Map) { - return val; - } - - return {}; - }).toList(); - } - - return []; - }); - - Future getTransaction({required String hash, required bool verbose}) async { - try { - final result = await callWithTimeout( - method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000); - return result; - } on RequestFailedTimeoutException catch (_) { - return {}; - } catch (e) { - return {}; - } - } - - Future> getTransactionVerbose({required String hash}) => - getTransaction(hash: hash, verbose: true).then((dynamic result) { - if (result is Map) { - return result; - } - - return {}; - }); - - Future getTransactionHex({required String hash}) => - getTransaction(hash: hash, verbose: false).then((dynamic result) { - if (result is String) { - return result; - } - - return ''; - }); - - Future broadcastTransaction( - {required String transactionRaw, - BasedUtxoNetwork? network, - Function(int)? idCallback}) async => - call( - method: 'blockchain.transaction.broadcast', - params: [transactionRaw], - idCallback: idCallback) - .then((dynamic result) { - if (result is String) { - return result; - } - - return ''; - }); - - Future> getMerkle({required String hash, required int height}) async => - await call(method: 'blockchain.transaction.get_merkle', params: [hash, height]) - as Map; - - 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}) => - 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]); - - Future estimatefee({required int p}) => - call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) { - if (result is double) { - return result; - } - - if (result is String) { - return double.parse(result); - } - - return 0; - }); - - Future>> feeHistogram() => - call(method: 'mempool.get_fee_histogram').then((dynamic result) { - if (result is List) { - // return result.map((dynamic e) { - // if (e is List) { - // return e.map((dynamic ee) => ee is int ? ee : null).toList(); - // } - - // return null; - // }).toList(); - final histogram = >[]; - for (final e in result) { - if (e is List) { - final eee = []; - for (final ee in e) { - if (ee is int) { - eee.add(ee); - } - } - histogram.add(eee); - } - } - return histogram; - } - - 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: - // { - // "height": 520481, - // "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4" - // } - - Future getCurrentBlockChainTip() async { - try { - final result = await callWithTimeout(method: 'blockchain.headers.subscribe'); - if (result is Map) { - return result["height"] as int; - } - return null; - } on RequestFailedTimeoutException catch (_) { - return null; - } catch (e) { - printV("getCurrentBlockChainTip: ${e.toString()}"); - return null; - } - } - - BehaviorSubject? chainTipSubscribe() { - _id += 1; - return subscribe( - id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe'); - } - - BehaviorSubject? scripthashUpdate(String scripthash) { - _id += 1; - return subscribe( - id: 'blockchain.scripthash.subscribe:$scripthash', - method: 'blockchain.scripthash.subscribe', - params: [scripthash]); - } - - BehaviorSubject? subscribe( - {required String id, required String method, List params = const []}) { - try { - if (socket == null) { - return null; - } - final subscription = BehaviorSubject(); - _regisrySubscription(id, subscription); - socket!.write(jsonrpc(method: method, id: _id, params: params)); - - return subscription; - } catch (e) { - printV("subscribe $e"); - return null; - } - } - - Future call( - {required String method, List params = const [], Function(int)? idCallback}) async { - if (socket == null) { - return null; - } - final completer = Completer(); - _id += 1; - final id = _id; - idCallback?.call(id); - _registryTask(id, completer); - socket!.write(jsonrpc(method: method, id: id, params: params)); - - return completer.future; - } - - Future callWithTimeout( - {required String method, List params = const [], int timeout = 5000}) async { - try { - if (socket == null) { - return null; - } - final completer = Completer(); - _id += 1; - final id = _id; - _registryTask(id, completer); - socket!.write(jsonrpc(method: method, id: id, params: params)); - Timer(Duration(milliseconds: timeout), () { - if (!completer.isCompleted) { - completer.completeError(RequestFailedTimeoutException(method, id)); - } - }); - - return completer.future; - } catch (e) { - printV("callWithTimeout $e"); - rethrow; - } - } - - Future close() async { - _aliveTimer?.cancel(); - try { - await socket?.close(); - socket = null; - } catch (_) {} - onConnectionStatusChange = null; - } - - void _registryTask(int id, Completer completer) => - _tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false); - - void _regisrySubscription(String id, BehaviorSubject subject) => - _tasks[id] = SocketTask(subject: subject, isSubscription: true); - - void _finish(String id, Object? data) { - if (_tasks[id] == null) { - return; - } - - if (!(_tasks[id]?.completer?.isCompleted ?? false)) { - _tasks[id]?.completer!.complete(data); - } - - if (!(_tasks[id]?.isSubscription ?? false)) { - _tasks.remove(id); - } else { - _tasks[id]?.subject?.add(data); - } - } - - void _methodHandler({required String method, required Map request}) { - switch (method) { - case 'blockchain.headers.subscribe': - final params = request['params'] as List; - final id = 'blockchain.headers.subscribe'; - - _tasks[id]?.subject?.add(params.last); - break; - case 'blockchain.scripthash.subscribe': - final params = request['params'] as List; - final scripthash = params.first as String?; - final id = 'blockchain.scripthash.subscribe:$scripthash'; - - _tasks[id]?.subject?.add(params.last); - break; - case 'blockchain.headers.subscribe': - final params = request['params'] as List; - _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; - default: - break; - } - } - - void _setConnectionStatus(ConnectionStatus status) { - onConnectionStatusChange?.call(status); - _connectionStatus = status; - _isConnected = status == ConnectionStatus.connected; - if (!_isConnected) { - try { - socket?.destroy(); - } catch (_) {} - socket = null; - } - } - - void _handleResponse(Map response) { - final method = response['method']; - final id = response['id'] as String?; - final result = response['result']; - - try { - final error = response['error'] as Map?; - if (error != null) { - final errorMessage = error['message'] as String?; - if (errorMessage != null) { - _errors[id!] = errorMessage; - } - } - } catch (_) {} - - try { - final error = response['error'] as String?; - if (error != null) { - _errors[id!] = error; - } - } catch (_) {} - - if (method is String) { - _methodHandler(method: method, request: response); - return; - } - - if (id != null) { - _finish(id, result); - } - } - - String getErrorMessage(int id) => _errors[id.toString()] ?? ''; -} - -// FIXME: move me -bool isJSONStringCorrect(String source) { - try { - json.decode(source); - return true; - } catch (_) { - return false; - } -} - -class RequestFailedTimeoutException implements Exception { - RequestFailedTimeoutException(this.method, this.id); - - final String method; - final int id; -} diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 43e462f7c7..816f302211 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -62,7 +62,7 @@ class ElectrumTransactionInfo extends TransactionInfo { required bool isPending, bool isReplaced = false, required DateTime date, - required int? time, + int? time, bool? isDateValidated, required int confirmations, String? to, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 0d43083520..e5a4cade56 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -417,7 +417,6 @@ abstract class ElectrumWalletBase _workerIsolate = await Isolate.spawn(ElectrumWorker.run, receivePort!.sendPort); _workerSubscription = receivePort!.listen((message) { - printV('Main: received message: $message'); if (message is SendPort) { workerSendPort = message; workerSendPort!.send( @@ -439,13 +438,12 @@ abstract class ElectrumWalletBase } } - int get _dustAmount => 546; + int get _dustAmount => 0; - bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; + bool isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; - TxCreateUtxoDetails _createUTXOS({ + TxCreateUtxoDetails createUTXOS({ required bool sendAll, - required bool paysToSilentPayment, int credentialsAmount = 0, int? inputsCount, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, @@ -455,7 +453,6 @@ abstract class ElectrumWalletBase List inputPrivKeyInfos = []; final publicKeys = {}; int allInputsAmount = 0; - bool spendsSilentPayment = false; bool spendsUnconfirmedTX = false; int leftAmount = credentialsAmount; @@ -483,25 +480,13 @@ abstract class ElectrumWalletBase final utx = availableInputs[i]; if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; - if (paysToSilentPayment) { - // Check inputs for shared secret derivation - if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) { - throw BitcoinTransactionSilentPaymentsNotSupported(); - } - } - allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; final address = RegexUtils.addressTypeFromStr(utx.address, network); ECPrivate? privkey; - bool? isSilentPayment = false; - if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) { - privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey; - spendsSilentPayment = true; - isSilentPayment = true; - } else if (!isHardwareWallet) { + if (!isHardwareWallet) { final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord); final path = addressRecord.derivationInfo.derivationPath .addElem(Bip32KeyIndex( @@ -516,11 +501,7 @@ abstract class ElectrumWalletBase String pubKeyHex; if (privkey != null) { - inputPrivKeyInfos.add(ECPrivateInfo( - privkey, - address.type == SegwitAddresType.p2tr, - tweak: !isSilentPayment, - )); + inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); pubKeyHex = privkey.getPublic().toHex(); } else { @@ -545,7 +526,6 @@ abstract class ElectrumWalletBase value: BigInt.from(utx.value), vout: utx.vout, scriptType: BitcoinAddressUtils.getScriptType(address), - isSilentPayment: isSilentPayment, ), ownerDetails: UtxoAddressDetails( publicKey: pubKeyHex, @@ -575,7 +555,6 @@ abstract class ElectrumWalletBase inputPrivKeyInfos: inputPrivKeyInfos, publicKeys: publicKeys, allInputsAmount: allInputsAmount, - spendsSilentPayment: spendsSilentPayment, spendsUnconfirmedTX: spendsUnconfirmedTX, ); } @@ -584,14 +563,9 @@ abstract class ElectrumWalletBase List outputs, int feeRate, { String? memo, - bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { - final utxoDetails = _createUTXOS( - sendAll: true, - paysToSilentPayment: hasSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, - ); + final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom); int fee = await calcFee( utxos: utxoDetails.utxos, @@ -612,7 +586,7 @@ abstract class ElectrumWalletBase } // Attempting to send less than the dust limit - if (_isBelowDust(amount)) { + if (isBelowDust(amount)) { throw BitcoinTransactionNoDustException(); } @@ -630,31 +604,27 @@ abstract class ElectrumWalletBase hasChange: false, memo: memo, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } Future estimateTxForAmount( int credentialsAmount, List outputs, - List updatedOutputs, int feeRate, { int? inputsCount, String? memo, bool? useUnconfirmed, - bool hasSilentPayment = false, UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { // Attempting to send less than the dust limit - if (_isBelowDust(credentialsAmount)) { + if (isBelowDust(credentialsAmount)) { throw BitcoinTransactionNoDustException(); } - final utxoDetails = _createUTXOS( + final utxoDetails = createUTXOS( sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, - paysToSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); @@ -671,11 +641,9 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, - updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, - hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -685,14 +653,9 @@ abstract class ElectrumWalletBase final changeAddress = await walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, - outputs: updatedOutputs, + outputs: outputs, ); final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); - updatedOutputs.add(BitcoinOutput( - address: address, - value: BigInt.from(amountLeftForChangeAndFee), - isChange: true, - )); outputs.add(BitcoinOutput( address: address, value: BigInt.from(amountLeftForChangeAndFee), @@ -703,34 +666,25 @@ abstract class ElectrumWalletBase utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); - // calcFee updates the silent payment outputs to calculate the tx size accounting - // for taproot addresses, but if more inputs are needed to make up for fees, - // the silent payment outputs need to be recalculated for the new inputs - var temp = outputs.map((output) => output).toList(); int fee = await calcFee( utxos: utxoDetails.utxos, - // 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, + outputs: outputs, memo: memo, feeRate: feeRate, ); - updatedOutputs.clear(); - updatedOutputs.addAll(temp); - if (fee == 0) { throw BitcoinTransactionNoFeeException(); } int amount = credentialsAmount; - final lastOutput = updatedOutputs.last; + final lastOutput = outputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - if (_isBelowDust(amountLeftForChange)) { + if (isBelowDust(amountLeftForChange)) { // If has change that is lower than dust, will end up with tx rejected by network rules // so remove the change amount - updatedOutputs.removeLast(); + outputs.removeLast(); outputs.removeLast(); if (amountLeftForChange < 0) { @@ -738,12 +692,10 @@ abstract class ElectrumWalletBase return estimateTxForAmount( credentialsAmount, outputs, - updatedOutputs, feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { @@ -761,20 +713,12 @@ abstract class ElectrumWalletBase isSendAll: spendingAllCoins, memo: memo, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } else { // Here, lastOutput already is change, return the amount left without the fee to the user's address. - updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( - address: lastOutput.address, - value: BigInt.from(amountLeftForChange), - isSilentPayment: lastOutput.isSilentPayment, - isChange: true, - ); outputs[outputs.length - 1] = BitcoinOutput( address: lastOutput.address, value: BigInt.from(amountLeftForChange), - isSilentPayment: lastOutput.isSilentPayment, isChange: true, ); @@ -788,7 +732,6 @@ abstract class ElectrumWalletBase isSendAll: spendingAllCoins, memo: memo, spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, - spendsSilentPayment: utxoDetails.spendsSilentPayment, ); } } @@ -818,12 +761,11 @@ abstract class ElectrumWalletBase final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; - bool hasSilentPayment = false; for (final out in transactionCredentials.outputs) { final outputAmount = out.formattedCryptoAmount!; - if (!sendAll && _isBelowDust(outputAmount)) { + if (!sendAll && isBelowDust(outputAmount)) { throw BitcoinTransactionNoDustException(); } @@ -836,25 +778,20 @@ abstract class ElectrumWalletBase credentialsAmount += outputAmount; final address = RegexUtils.addressTypeFromStr( - out.isParsedAddress ? out.extractedAddress! : out.address, network); - final isSilentPayment = address is SilentPaymentAddress; - - if (isSilentPayment) { - hasSilentPayment = true; - } + out.isParsedAddress ? out.extractedAddress! : out.address, + network, + ); if (sendAll) { // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent outputs.add(BitcoinOutput( address: address, value: BigInt.from(0), - isSilentPayment: isSilentPayment, )); } else { outputs.add(BitcoinOutput( address: address, value: BigInt.from(outputAmount), - isSilentPayment: isSilentPayment, )); } } @@ -864,31 +801,19 @@ abstract class ElectrumWalletBase : feeRate(transactionCredentials.priority!); EstimatedTxResult estimatedTx; - final updatedOutputs = outputs - .map((e) => BitcoinOutput( - address: e.address, - value: e.value, - isSilentPayment: e.isSilentPayment, - isChange: e.isChange, - )) - .toList(); - if (sendAll) { estimatedTx = await estimateSendAllTx( - updatedOutputs, + outputs, feeRateInt, memo: memo, - hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( credentialsAmount, outputs, - updatedOutputs, feeRateInt, memo: memo, - hasSilentPayment: hasSilentPayment, coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -896,7 +821,7 @@ abstract class ElectrumWalletBase if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: updatedOutputs, + outputs: outputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), network: network, @@ -925,7 +850,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: updatedOutputs, + outputs: outputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -935,7 +860,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: updatedOutputs, + outputs: outputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -974,11 +899,7 @@ abstract class ElectrumWalletBase if (utxo.utxo.isP2tr()) { hasTaprootInputs = true; - return key.privkey.signTapRoot( - txDigest, - sighash: sighash, - tweak: utxo.utxo.isSilentPayment != true, - ); + return key.privkey.signTapRoot(txDigest, sighash: sighash); } else { return key.privkey.signInput(txDigest, sigHash: sighash); } @@ -997,20 +918,14 @@ abstract class ElectrumWalletBase utxos: estimatedTx.utxos, )..addListener((transaction) async { transactionHistory.addOne(transaction); - if (estimatedTx.spendsSilentPayment) { - transactionHistory.transactions.values.forEach((tx) { - tx.unspents?.removeWhere( - (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); - transactionHistory.addOne(tx); - }); - } unspentCoins .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); await updateBalance(); }); - } catch (e) { + } catch (e, s) { + print([e, s]); throw e; } } @@ -1974,7 +1889,7 @@ class EstimatedTxResult { required this.hasChange, required this.isSendAll, this.memo, - required this.spendsSilentPayment, + this.spendsSilentPayment = false, required this.spendsUnconfirmedTX, }); @@ -1985,7 +1900,6 @@ class EstimatedTxResult { final int amount; final bool spendsSilentPayment; - // final bool sendsToSilentPayment; final bool hasChange; final bool isSendAll; final String? memo; @@ -2018,7 +1932,7 @@ class TxCreateUtxoDetails { required this.inputPrivKeyInfos, required this.publicKeys, required this.allInputsAmount, - required this.spendsSilentPayment, + this.spendsSilentPayment = false, required this.spendsUnconfirmedTX, }); } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 326fcf64ad..758934e034 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -382,11 +382,20 @@ class ElectrumWorker { } Future _handleBroadcast(ElectrumWorkerBroadcastRequest request) async { + final rpcId = _electrumClient!.id + 1; final txHash = await _electrumClient!.request( ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw), ); - _sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id)); + if (txHash == null) { + final error = (_electrumClient!.rpc as ElectrumSSLService).getError(rpcId); + + if (error?.message != null) { + return _sendError(ElectrumWorkerBroadcastError(error: error!.message, id: request.id)); + } + } else { + _sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id)); + } } Future _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async { @@ -586,6 +595,7 @@ class ElectrumWorker { if (scanData.shouldSwitchNodes) { scanningClient = await ElectrumApiProvider.connect( ElectrumTCPService.connect( + // TODO: ssl Uri.parse("tcp://electrs.cakewallet.com:50001"), ), ); @@ -714,7 +724,6 @@ class ElectrumWorker { date: scanData.network == BitcoinNetwork.mainnet ? getDateByBitcoinHeight(tweakHeight) : DateTime.now(), - time: null, confirmations: scanData.chainTip - tweakHeight + 1, unspents: [], isReceivedSilentPayment: true, @@ -736,10 +745,7 @@ class ElectrumWorker { receivingOutputAddress, labelIndex: 1, // TODO: get actual index/label isUsed: true, - // TODO: use right wallet - spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd( - BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)), - ), + tweak: t_k, txCount: 1, balance: amount, ); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 8f2febb6c9..0a23731bc8 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -559,7 +559,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { direction: TransactionDirection.incoming, isPending: utxo.height == 0, date: date, - time: null, confirmations: confirmations, inputAddresses: [], outputAddresses: [utxo.outputId], @@ -763,7 +762,6 @@ 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: [], diff --git a/cw_bitcoin/lib/pending_bitcoin_transaction.dart b/cw_bitcoin/lib/pending_bitcoin_transaction.dart index 2c0763305f..9155cbce79 100644 --- a/cw_bitcoin/lib/pending_bitcoin_transaction.dart +++ b/cw_bitcoin/lib/pending_bitcoin_transaction.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:grpc/grpc.dart'; @@ -24,6 +26,7 @@ class PendingBitcoinTransaction with PendingTransaction { this.hasTaprootInputs = false, this.isMweb = false, this.utxos = const [], + this.hasSilentPayment = false, }) : _listeners = []; final WalletType type; @@ -59,7 +62,7 @@ class PendingBitcoinTransaction with PendingTransaction { List get outputs => _tx.outputs; - bool get hasSilentPayment => _tx.hasSilentPayment; + bool hasSilentPayment; PendingChange? get change { try { @@ -76,41 +79,41 @@ class PendingBitcoinTransaction with PendingTransaction { final List _listeners; Future _commit() async { - int? callId; - - final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String; - - // if (result.isEmpty) { - // if (callId != null) { - // final error = sendWorker(getErrorMessage(callId!)); + final result = await sendWorker( + ElectrumWorkerBroadcastRequest(transactionRaw: hex), + ) as String; - // 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(); - // } + String? error; + try { + final resultJson = jsonDecode(result) as Map; + error = resultJson["error"] as String; + } catch (_) {} + + if (error != null) { + if (error.contains("dust")) { + if (hasChange) { + throw BitcoinTransactionCommitFailedDustChange(); + } else if (!isSendAll) { + throw BitcoinTransactionCommitFailedDustOutput(); + } else { + throw BitcoinTransactionCommitFailedDustOutputSendAll(); + } + } - // if (error.contains("non-BIP68-final")) { - // throw BitcoinTransactionCommitFailedBIP68Final(); - // } + if (error.contains("bad-txns-vout-negative")) { + throw BitcoinTransactionCommitFailedVoutNegative(); + } - // if (error.contains("min fee not met")) { - // throw BitcoinTransactionCommitFailedLessThanMin(); - // } + if (error.contains("non-BIP68-final")) { + throw BitcoinTransactionCommitFailedBIP68Final(); + } - // throw BitcoinTransactionCommitFailed(errorMessage: error); - // } + if (error.contains("min fee not met")) { + throw BitcoinTransactionCommitFailedLessThanMin(); + } - // throw BitcoinTransactionCommitFailed(); - // } + throw BitcoinTransactionCommitFailed(errorMessage: error); + } } Future _ltcCommit() async { @@ -151,7 +154,6 @@ class PendingBitcoinTransaction with PendingTransaction { inputAddresses: _tx.inputs.map((input) => input.txId).toList(), outputAddresses: outputAddresses, fee: fee, - time: null, ); @override 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 340c1eb642..04deffbba7 100644 --- a/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart +++ b/cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart @@ -1,24 +1,29 @@ +import 'dart:convert'; + +import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart'; +import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; 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: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'; class PendingBitcoinCashTransaction with PendingTransaction { - PendingBitcoinCashTransaction(this._tx, this.type, - {required this.electrumClient, - required this.amount, - required this.fee, - required this.hasChange, - required this.isSendAll}) - : _listeners = []; + PendingBitcoinCashTransaction( + this._tx, + this.type, { + required this.sendWorker, + required this.amount, + required this.fee, + required this.hasChange, + required this.isSendAll, + }) : _listeners = []; final WalletType type; final bitbox.Transaction _tx; - final ElectrumClient electrumClient; + Future Function(ElectrumWorkerRequest) sendWorker; final int amount; final int fee; final bool hasChange; @@ -40,32 +45,40 @@ class PendingBitcoinCashTransaction with PendingTransaction { @override Future commit() async { - int? callId; - - final result = await electrumClient.broadcastTransaction( - transactionRaw: hex, idCallback: (id) => callId = id); - - if (result.isEmpty) { - if (callId != null) { - final error = electrumClient.getErrorMessage(callId!); - - if (error.contains("dust")) { - if (hasChange) { - throw BitcoinTransactionCommitFailedDustChange(); - } else if (!isSendAll) { - throw BitcoinTransactionCommitFailedDustOutput(); - } else { - throw BitcoinTransactionCommitFailedDustOutputSendAll(); - } + final result = await sendWorker( + ElectrumWorkerBroadcastRequest(transactionRaw: hex), + ) as String; + + String? error; + try { + final resultJson = jsonDecode(result) as Map; + error = resultJson["error"] as String; + } catch (_) {} + + if (error != null) { + 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(); - } - throw BitcoinTransactionCommitFailed(errorMessage: error); + if (error.contains("bad-txns-vout-negative")) { + throw BitcoinTransactionCommitFailedVoutNegative(); + } + + if (error.contains("non-BIP68-final")) { + throw BitcoinTransactionCommitFailedBIP68Final(); + } + + if (error.contains("min fee not met")) { + throw BitcoinTransactionCommitFailedLessThanMin(); } - throw BitcoinTransactionCommitFailed(); + throw BitcoinTransactionCommitFailed(errorMessage: error); } _listeners.forEach((listener) => listener(transactionInfo())); @@ -86,6 +99,7 @@ class PendingBitcoinCashTransaction with PendingTransaction { fee: fee, isReplaced: false, ); + @override Future commitUR() { throw UnimplementedError(); diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index c28c4110bc..dde04bb1c8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -695,7 +695,7 @@ "sent": "Enviada", "service_health_disabled": "O Boletim de Saúde de Serviço está desativado", "service_health_disabled_message": "Esta é a página do Boletim de Saúde de Serviço, você pode ativar esta página em Configurações -> Privacidade", - "set_a_pin": "Defina um pino", + "set_a_pin": "Defina um pin", "settings": "Configurações", "settings_all": "Tudo", "settings_allow_biometrical_authentication": "Permitir autenticação biométrica", @@ -990,4 +990,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} \ No newline at end of file +} From 1646a67117e69a9f5b28aed768775b1c3fa50a03 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 20 Dec 2024 20:18:32 -0300 Subject: [PATCH 32/64] fix: scan --- .../lib/electrum_worker/electrum_worker.dart | 186 +++++++++--------- cw_bitcoin/pubspec.yaml | 2 +- 2 files changed, 96 insertions(+), 92 deletions(-) diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 758934e034..37dbe30f0b 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -3,11 +3,9 @@ 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'; @@ -17,7 +15,6 @@ import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:http/http.dart' as http; import 'package:rxdart/rxdart.dart'; import 'package:sp_scanner/sp_scanner.dart'; @@ -108,7 +105,6 @@ class ElectrumWorker { ); break; case ElectrumWorkerMethods.stopScanningMethod: - printV("Worker: received message: $message"); await _handleStopScanning( ElectrumWorkerStopScanningRequest.fromJson(messageJson), ); @@ -437,49 +433,10 @@ class ElectrumWorker { if (getTime && _walletType == WalletType.bitcoin) { 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; - } - } - } - } + final dates = await getTxDate(hash, _network!, date: date); + time = dates.time; + height = dates.height; + isDateValidated = dates.isDateValidated; } catch (_) {} } } @@ -604,15 +561,12 @@ class ElectrumWorker { 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, - ); - }, + (wallet) => Receiver( + wallet.b_scan.toHex(), + wallet.B_spend.toHex(), + scanData.network == BitcoinNetwork.testnet, + scanData.labelIndexes, + ), ); int getCountPerRequest(int syncHeight) { @@ -640,7 +594,7 @@ class ElectrumWorker { _scanningStream = await scanningClient!.subscribe(req); - void listenFn(Map event, ElectrumTweaksSubscribe req) { + void listenFn(Map event, ElectrumTweaksSubscribe req) async { final response = req.onResponse(event); if (response == null || _scanningStream == null) { @@ -691,24 +645,29 @@ class ElectrumWorker { final tweak = tweakData.tweak; try { - // scanOutputs called from rust here final addToWallet = {}; receivers.forEach((receiver) { + // scanOutputs called from rust here final scanResult = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); - addToWallet.addAll(scanResult); + if (scanResult.isEmpty) { + return; + } + + if (addToWallet[receiver.BSpend] == null) { + addToWallet[receiver.BSpend] = scanResult; + } else { + addToWallet[receiver.BSpend].addAll(scanResult); + } }); - // final addToWallet = scanOutputs( - // outputPubkeys.keys.toList(), - // tweak, - // receivers.last, - // ); + print("ADDTO WALLET: $addToWallet"); if (addToWallet.isEmpty) { // no results tx, continue to next tx continue; } + print(scanData.labels); // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) final txInfo = ElectrumTransactionInfo( @@ -720,40 +679,41 @@ class ElectrumWorker { direction: TransactionDirection.incoming, isPending: false, isReplaced: false, - // TODO: tx time mempool api - date: scanData.network == BitcoinNetwork.mainnet - ? getDateByBitcoinHeight(tweakHeight) - : DateTime.now(), + date: DateTime.fromMillisecondsSinceEpoch( + (await getTxDate(txid, scanData.network)).time! * 1000, + ), 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, - tweak: t_k, - txCount: 1, - balance: amount, - ); + addToWallet.forEach((BSpend, result) { + result.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, + tweak: t_k, + txCount: 1, + balance: amount, + ); - final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); + final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); - txInfo.unspents!.add(unspent); - txInfo.amount += unspent.value; + txInfo.unspents!.add(unspent); + txInfo.amount += unspent.value; + }); }); }); @@ -947,3 +907,47 @@ class ScanNode { ScanNode(this.uri, this.useSSL); } + +class DateResult { + final int? time; + final int? height; + final bool? isDateValidated; + + DateResult({this.time, this.height, this.isDateValidated}); +} + +Future getTxDate( + String txid, + BasedUtxoNetwork network, { + DateTime? date, +}) async { + int? time; + int? height; + bool? isDateValidated; + + final mempoolApi = ApiProvider.fromMempool( + network, + baseUrl: "http://mempool.cakewallet.com:8999/api/v1", + ); + + try { + final txVerbose = await mempoolApi.getTransaction(txid); + + final status = txVerbose.status; + height = status.blockHeight; + + if (height != null) { + final blockHash = await mempoolApi.getBlockHeight(height); + final block = await mempoolApi.getBlock(blockHash); + + time = int.parse(block['timestamp'].toString()); + + if (date != null) { + final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000); + isDateValidated = newDate == date; + } + } + } catch (_) {} + + return DateResult(time: time, height: height, isDateValidated: isDateValidated); +} diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9c7c41420c..9b606066ad 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: sp_scanner: git: url: https://github.com/cake-tech/sp_scanner.git - ref: cake-update-v3 + ref: cake-update-v4 bech32: git: url: https://github.com/cake-tech/bech32.git From a07be3d8637095cd1a8b0a279176b60595c40652 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 20 Dec 2024 20:22:45 -0300 Subject: [PATCH 33/64] chore: prints --- cw_bitcoin/lib/bitcoin_wallet.dart | 3 +-- cw_bitcoin/lib/electrum_wallet.dart | 3 +-- cw_bitcoin/lib/electrum_worker/electrum_worker.dart | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ea805b1687..caa617dae9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1476,8 +1476,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { await updateBalance(); }); - } catch (e, s) { - print([e, s]); + } catch (e) { throw e; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 210fad7860..8ea6a583c9 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -926,8 +926,7 @@ abstract class ElectrumWalletBase await updateBalance(); await updateAllUnspents(); }); - } catch (e, s) { - print([e, s]); + } catch (e) { throw e; } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 37dbe30f0b..2c031a8675 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -662,12 +662,10 @@ class ElectrumWorker { } }); - print("ADDTO WALLET: $addToWallet"); if (addToWallet.isEmpty) { // no results tx, continue to next tx continue; } - print(scanData.labels); // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) final txInfo = ElectrumTransactionInfo( From fb5aa9dc6d716acf24254a6cedfb8a87e11c1fa5 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 26 Dec 2024 18:55:25 -0300 Subject: [PATCH 34/64] feat: fee estimation, review comments --- cw_bitcoin/lib/bitcoin_address_record.dart | 8 +- .../lib/bitcoin_transaction_credentials.dart | 10 +- .../lib/bitcoin_transaction_priority.dart | 69 +-- cw_bitcoin/lib/bitcoin_wallet.dart | 35 +- cw_bitcoin/lib/electrum_wallet.dart | 338 +++++++------ cw_bitcoin/lib/electrum_wallet_addresses.dart | 5 +- .../lib/electrum_worker/electrum_worker.dart | 4 +- cw_bitcoin/lib/litecoin_wallet.dart | 449 +++++++++++++++++- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 7 +- .../lib/src/bitcoin_cash_wallet.dart | 25 +- cw_core/lib/transaction_priority.dart | 4 + cw_core/lib/wallet_base.dart | 5 +- cw_evm/lib/evm_chain_wallet.dart | 3 +- cw_haven/lib/haven_wallet.dart | 2 +- cw_monero/lib/monero_wallet.dart | 182 +++---- cw_nano/lib/nano_wallet.dart | 3 +- cw_solana/lib/solana_wallet.dart | 3 +- cw_tron/lib/tron_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet.dart | 10 +- lib/bitcoin/cw_bitcoin.dart | 89 +++- lib/view_model/send/output.dart | 29 +- .../transaction_details_view_model.dart | 40 +- 22 files changed, 905 insertions(+), 417 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index f75fee08bf..a659354787 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -81,11 +81,13 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { String? scriptHash, BasedUtxoNetwork? network, }) { - if (scriptHash == null && network == null) { + if (scriptHash != null) { + this.scriptHash = scriptHash; + } else if (network != null) { + this.scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); + } else { throw ArgumentError('either scriptHash or network must be provided'); } - - this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!); } factory BitcoinAddressRecord.fromJSON(String jsonSource) { diff --git a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart index f6d769735b..d2f6fbd843 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_credentials.dart @@ -1,14 +1,10 @@ -import 'package:cw_core/output_info.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/output_info.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 TransactionPriority? priority; diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 26a4c2f626..660988de56 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -1,25 +1,25 @@ import 'package:cw_core/transaction_priority.dart'; -class BitcoinTransactionPriority extends TransactionPriority { - const BitcoinTransactionPriority({required super.title, required super.raw}); +class BitcoinAPITransactionPriority extends TransactionPriority { + const BitcoinAPITransactionPriority({required super.title, required super.raw}); // Unimportant: the lowest possible, confirms when it confirms no matter how long it takes - static const BitcoinTransactionPriority unimportant = - BitcoinTransactionPriority(title: 'Unimportant', raw: 0); + static const BitcoinAPITransactionPriority unimportant = + BitcoinAPITransactionPriority(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); + static const BitcoinAPITransactionPriority normal = + BitcoinAPITransactionPriority(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); + static const BitcoinAPITransactionPriority elevated = + BitcoinAPITransactionPriority(title: 'Elevated', raw: 2); // Priority: high fee, expected in the next block (about 10 mins). - static const BitcoinTransactionPriority priority = - BitcoinTransactionPriority(title: 'Priority', raw: 3); + static const BitcoinAPITransactionPriority priority = + BitcoinAPITransactionPriority(title: 'Priority', raw: 3); // Custom: any fee, user defined - static const BitcoinTransactionPriority custom = - BitcoinTransactionPriority(title: 'Custom', raw: 4); + static const BitcoinAPITransactionPriority custom = + BitcoinAPITransactionPriority(title: 'Custom', raw: 4); - static BitcoinTransactionPriority deserialize({required int raw}) { + static BitcoinAPITransactionPriority deserialize({required int raw}) { switch (raw) { case 0: return unimportant; @@ -32,7 +32,7 @@ class BitcoinTransactionPriority extends TransactionPriority { case 4: return custom; default: - throw Exception('Unexpected token: $raw for TransactionPriority deserialize'); + throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); } } @@ -41,19 +41,19 @@ class BitcoinTransactionPriority extends TransactionPriority { var label = ''; switch (this) { - case BitcoinTransactionPriority.unimportant: + case BitcoinAPITransactionPriority.unimportant: label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; break; - case BitcoinTransactionPriority.normal: + case BitcoinAPITransactionPriority.normal: label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; break; - case BitcoinTransactionPriority.elevated: + case BitcoinAPITransactionPriority.elevated: label = 'Elevated'; break; // S.current.transaction_priority_fast; - case BitcoinTransactionPriority.priority: + case BitcoinAPITransactionPriority.priority: label = 'Priority'; break; // S.current.transaction_priority_fast; - case BitcoinTransactionPriority.custom: + case BitcoinAPITransactionPriority.custom: label = 'Custom'; break; default: @@ -65,7 +65,7 @@ class BitcoinTransactionPriority extends TransactionPriority { String labelWithRate(int rate, int? customRate) { final rateValue = this == custom ? customRate ??= 0 : rate; - return '${toString()} ($rateValue ${units}/byte)'; + return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)'; } } @@ -127,7 +127,7 @@ class ElectrumTransactionPriority extends TransactionPriority { String labelWithRate(int rate, int? customRate) { final rateValue = this == custom ? customRate ??= 0 : rate; - return '${toString()} ($rateValue ${units}/byte)'; + return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)'; } } @@ -145,8 +145,8 @@ class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { String get units => 'satoshi'; } -class BitcoinTransactionPriorities implements TransactionPriorities { - const BitcoinTransactionPriorities({ +class BitcoinAPITransactionPriorities implements TransactionPriorities { + const BitcoinAPITransactionPriorities({ required this.unimportant, required this.normal, required this.elevated, @@ -163,15 +163,15 @@ class BitcoinTransactionPriorities implements TransactionPriorities { @override int operator [](TransactionPriority type) { switch (type) { - case BitcoinTransactionPriority.unimportant: + case BitcoinAPITransactionPriority.unimportant: return unimportant; - case BitcoinTransactionPriority.normal: + case BitcoinAPITransactionPriority.normal: return normal; - case BitcoinTransactionPriority.elevated: + case BitcoinAPITransactionPriority.elevated: return elevated; - case BitcoinTransactionPriority.priority: + case BitcoinAPITransactionPriority.priority: return priority; - case BitcoinTransactionPriority.custom: + case BitcoinAPITransactionPriority.custom: return custom; default: throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); @@ -182,7 +182,7 @@ class BitcoinTransactionPriorities implements TransactionPriorities { String labelWithRate(TransactionPriority priorityType, [int? rate]) { late int rateValue; - if (priorityType == BitcoinTransactionPriority.custom) { + if (priorityType == BitcoinAPITransactionPriority.custom) { if (rate == null) { throw Exception('Rate must be provided for custom transaction priority'); } @@ -191,7 +191,7 @@ class BitcoinTransactionPriorities implements TransactionPriorities { rateValue = this[priorityType]; } - return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)'; + return '${priorityType.toString()} (${rateValue} ${priorityType.getUnits(rateValue)}/byte)'; } @override @@ -205,8 +205,8 @@ class BitcoinTransactionPriorities implements TransactionPriorities { }; } - static BitcoinTransactionPriorities fromJson(Map json) { - return BitcoinTransactionPriorities( + static BitcoinAPITransactionPriorities fromJson(Map json) { + return BitcoinAPITransactionPriorities( unimportant: json['unimportant'] as int, normal: json['normal'] as int, elevated: json['elevated'] as int, @@ -247,7 +247,8 @@ class ElectrumTransactionPriorities implements TransactionPriorities { @override String labelWithRate(TransactionPriority priorityType, [int? rate]) { - return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)'; + final rateValue = this[priorityType]; + return '${priorityType.toString()} ($rateValue ${priorityType.getUnits(rateValue)}/byte)'; } factory ElectrumTransactionPriorities.fromList(List list) { @@ -286,7 +287,7 @@ class ElectrumTransactionPriorities implements TransactionPriorities { TransactionPriorities deserializeTransactionPriorities(Map json) { if (json.containsKey('unimportant')) { - return BitcoinTransactionPriorities.fromJson(json); + return BitcoinAPITransactionPriorities.fromJson(json); } else if (json.containsKey('slow')) { return ElectrumTransactionPriorities.fromJson(json); } else { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index caa617dae9..ad49c3b7c7 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -22,7 +22,6 @@ import 'package:cw_core/crypto_currency.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/unspent_coin_type.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -412,7 +411,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, - required BasedUtxoNetwork network, required List utxos, required Map publicKeys, String? memo, @@ -921,14 +919,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - Future calcFee({ + int calcFee({ required List utxos, required List outputs, String? memo, required int feeRate, List? inputPrivKeyInfos, List? vinOutpoints, - }) async => + }) => feeRate * BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxos, @@ -945,7 +943,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { bool paysToSilentPayment = false, int credentialsAmount = 0, int? inputsCount, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -957,6 +954,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int leftAmount = credentialsAmount; var availableInputs = unspentCoins.where((utx) { + // TODO: unspent coin isSending not toggled if (!utx.isSending || utx.isFrozen) { return false; } @@ -1074,7 +1072,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int feeRate, { String? memo, bool hasSilentPayment = false, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment); @@ -1122,19 +1119,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - Future estimateTxForAmount( + EstimatedTxResult estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { - List updatedOutputs = const [], + List? updatedOutputs, int? inputsCount, String? memo, bool? useUnconfirmed, bool hasSilentPayment = false, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, - }) async { + bool isFakeTx = false, + }) { + if (updatedOutputs == null) { + updatedOutputs = outputs.map((output) => output).toList(); + } + // Attempting to send less than the dust limit - if (isBelowDust(credentialsAmount)) { + if (!isFakeTx && isBelowDust(credentialsAmount)) { throw BitcoinTransactionNoDustException(); } @@ -1163,13 +1164,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { inputsCount: utxoDetails.utxos.length + 1, memo: memo, hasSilentPayment: hasSilentPayment, + isFakeTx: isFakeTx, ); } throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress( + final changeAddress = walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, outputs: updatedOutputs, ); @@ -1194,7 +1196,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // for taproot addresses, but if more inputs are needed to make up for fees, // the silent payment outputs need to be recalculated for the new inputs var temp = outputs.map((output) => output).toList(); - int fee = await calcFee( + int fee = calcFee( utxos: utxoDetails.utxos, // Always take only not updated bitcoin outputs here so for every estimation // the SP outputs are re-generated to the proper taproot addresses @@ -1216,7 +1218,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final lastOutput = updatedOutputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - if (isBelowDust(amountLeftForChange)) { + if (!isFakeTx && isBelowDust(amountLeftForChange)) { // If has change that is lower than dust, will end up with tx rejected by network rules // so remove the change amount updatedOutputs.removeLast(); @@ -1233,6 +1235,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, hasSilentPayment: hasSilentPayment, + isFakeTx: isFakeTx, ); } else { throw BitcoinTransactionWrongBalanceException(); @@ -1289,7 +1292,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final hasMultiDestination = transactionCredentials.outputs.length > 1; final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; final memo = transactionCredentials.outputs.first.memo; - final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; int credentialsAmount = 0; bool hasSilentPayment = false; @@ -1353,7 +1355,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { feeRateInt, memo: memo, hasSilentPayment: hasSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, ); } else { estimatedTx = await estimateTxForAmount( @@ -1363,7 +1364,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { updatedOutputs: updatedOutputs, memo: memo, hasSilentPayment: hasSilentPayment, - coinTypeToSpendFrom: coinTypeToSpendFrom, ); } @@ -1373,7 +1373,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { outputs: updatedOutputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), - network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 8ea6a583c9..85b36f0f72 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -35,7 +35,6 @@ 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/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; @@ -201,9 +200,6 @@ abstract class ElectrumWalletBase return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } - int estimatedTransactionSize(int inputsCount, int outputsCounts) => - inputsCount * 68 + outputsCounts * 34 + 10; - static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { switch (network) { case LitecoinNetwork.mainnet: @@ -244,19 +240,15 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - List get addressesSet => walletAddresses.allAddresses - .where((element) => element.addressType != SegwitAddresType.mweb) - .map((addr) => addr.address) - .toList(); + List get addressesSet => + walletAddresses.allAddresses.map((addr) => addr.address).toList(); List get scriptHashes => walletAddresses.addressesByReceiveType - .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => (addr as BitcoinAddressRecord).scriptHash) .toList(); List get publicScriptHashes => walletAddresses.allAddresses .where((addr) => !addr.isChange) - .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) .map((addr) => addr.scriptHash) .toList(); @@ -298,8 +290,8 @@ abstract class ElectrumWalletBase TransactionPriorities? feeRates; int feeRate(TransactionPriority priority) { - if (priority is ElectrumTransactionPriority && feeRates is BitcoinTransactionPriorities) { - final rates = feeRates as BitcoinTransactionPriorities; + if (priority is ElectrumTransactionPriority && feeRates is BitcoinAPITransactionPriorities) { + final rates = feeRates as BitcoinAPITransactionPriorities; switch (priority) { case ElectrumTransactionPriority.slow: @@ -446,7 +438,6 @@ abstract class ElectrumWalletBase required bool sendAll, int credentialsAmount = 0, int? inputsCount, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) { List utxos = []; List vinOutpoints = []; @@ -461,21 +452,10 @@ abstract class ElectrumWalletBase return false; } - switch (coinTypeToSpendFrom) { - case UnspentCoinType.mweb: - return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; - case UnspentCoinType.nonMweb: - return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; - case UnspentCoinType.any: - return true; - } + return true; }).toList(); 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.addressType == SegwitAddresType.mweb ? -1 : 1); - for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; @@ -563,9 +543,8 @@ abstract class ElectrumWalletBase List outputs, int feeRate, { String? memo, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, }) async { - final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom); + final utxoDetails = createUTXOS(sendAll: true); int fee = await calcFee( utxos: utxoDetails.utxos, @@ -607,17 +586,17 @@ abstract class ElectrumWalletBase ); } - Future estimateTxForAmount( + EstimatedTxResult estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { int? inputsCount, String? memo, bool? useUnconfirmed, - UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, - }) async { + bool isFakeTx = false, + }) { // Attempting to send less than the dust limit - if (isBelowDust(credentialsAmount)) { + if (!isFakeTx && isBelowDust(credentialsAmount)) { throw BitcoinTransactionNoDustException(); } @@ -625,7 +604,6 @@ abstract class ElectrumWalletBase sendAll: false, credentialsAmount: credentialsAmount, inputsCount: inputsCount, - coinTypeToSpendFrom: coinTypeToSpendFrom, ); final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; @@ -644,14 +622,14 @@ abstract class ElectrumWalletBase feeRate, inputsCount: utxoDetails.utxos.length + 1, memo: memo, - coinTypeToSpendFrom: coinTypeToSpendFrom, + isFakeTx: isFakeTx, ); } throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress( + final changeAddress = walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, outputs: outputs, ); @@ -666,7 +644,7 @@ abstract class ElectrumWalletBase utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); - int fee = await calcFee( + int fee = calcFee( utxos: utxoDetails.utxos, outputs: outputs, memo: memo, @@ -681,7 +659,7 @@ abstract class ElectrumWalletBase final lastOutput = outputs.last; final amountLeftForChange = amountLeftForChangeAndFee - fee; - if (isBelowDust(amountLeftForChange)) { + if (!isFakeTx && isBelowDust(amountLeftForChange)) { // If has change that is lower than dust, will end up with tx rejected by network rules // so remove the change amount outputs.removeLast(); @@ -696,7 +674,7 @@ abstract class ElectrumWalletBase inputsCount: utxoDetails.utxos.length + 1, memo: memo, useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, - coinTypeToSpendFrom: coinTypeToSpendFrom, + isFakeTx: isFakeTx, ); } else { throw BitcoinTransactionWrongBalanceException(); @@ -736,12 +714,12 @@ abstract class ElectrumWalletBase } } - Future calcFee({ + int calcFee({ required List utxos, required List outputs, String? memo, required int feeRate, - }) async => + }) => feeRate * BitcoinTransactionBuilder.estimateTransactionSize( utxos: utxos, @@ -750,81 +728,94 @@ abstract class ElectrumWalletBase memo: memo, ); - @override - Future createTransaction(Object credentials) async { - try { - final outputs = []; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final hasMultiDestination = transactionCredentials.outputs.length > 1; - final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; - final memo = transactionCredentials.outputs.first.memo; - final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; + CreateTxData getCreateTxDataFromCredentials(Object credentials) { + final outputs = []; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final hasMultiDestination = transactionCredentials.outputs.length > 1; + final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; + final memo = transactionCredentials.outputs.first.memo; - int credentialsAmount = 0; + int credentialsAmount = 0; - for (final out in transactionCredentials.outputs) { - final outputAmount = out.formattedCryptoAmount!; + for (final out in transactionCredentials.outputs) { + final outputAmount = out.formattedCryptoAmount!; - if (!sendAll && isBelowDust(outputAmount)) { - throw BitcoinTransactionNoDustException(); - } + if (!sendAll && isBelowDust(outputAmount)) { + throw BitcoinTransactionNoDustException(); + } - if (hasMultiDestination) { - if (out.sendAll) { - throw BitcoinTransactionWrongBalanceException(); - } + if (hasMultiDestination) { + if (out.sendAll) { + throw BitcoinTransactionWrongBalanceException(); } + } - credentialsAmount += outputAmount; + credentialsAmount += outputAmount; - final address = RegexUtils.addressTypeFromStr( - out.isParsedAddress ? out.extractedAddress! : out.address, - network, - ); + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, + network, + ); - if (sendAll) { - // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent - outputs.add(BitcoinOutput( + if (sendAll) { + outputs.add( + BitcoinOutput( address: address, + // Send all: The value of the single existing output will be updated + // after estimating the Tx size and deducting the fee from the total to be sent value: BigInt.from(0), - )); - } else { - outputs.add(BitcoinOutput( + ), + ); + } else { + outputs.add( + BitcoinOutput( address: address, value: BigInt.from(outputAmount), - )); - } + ), + ); } + } + + final feeRateInt = transactionCredentials.feeRate != null + ? transactionCredentials.feeRate! + : feeRate(transactionCredentials.priority!); - final feeRateInt = transactionCredentials.feeRate != null - ? transactionCredentials.feeRate! - : feeRate(transactionCredentials.priority!); + return CreateTxData( + sendAll: sendAll, + amount: credentialsAmount, + outputs: outputs, + feeRate: feeRateInt, + memo: memo, + ); + } + + @override + Future createTransaction(Object credentials) async { + try { + final data = getCreateTxDataFromCredentials(credentials); EstimatedTxResult estimatedTx; - if (sendAll) { + if (data.sendAll) { estimatedTx = await estimateSendAllTx( - outputs, - feeRateInt, - memo: memo, - coinTypeToSpendFrom: coinTypeToSpendFrom, + data.outputs, + data.feeRate, + memo: data.memo, ); } else { estimatedTx = await estimateTxForAmount( - credentialsAmount, - outputs, - feeRateInt, - memo: memo, - coinTypeToSpendFrom: coinTypeToSpendFrom, + data.amount, + data.outputs, + data.feeRate, + memo: data.memo, ); } if (walletInfo.isHardwareWallet) { final transaction = await buildHardwareWalletTransaction( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: data.outputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), - network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, @@ -836,7 +827,7 @@ abstract class ElectrumWalletBase sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), + feeRate: data.feeRate.toString(), hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot @@ -851,7 +842,7 @@ abstract class ElectrumWalletBase if (network is BitcoinCashNetwork) { txb = ForkedTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: data.outputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -861,7 +852,7 @@ abstract class ElectrumWalletBase } else { txb = BitcoinTransactionBuilder( utxos: estimatedTx.utxos, - outputs: outputs, + outputs: data.outputs, fee: BigInt.from(estimatedTx.fee), network: network, memo: estimatedTx.memo, @@ -912,7 +903,7 @@ abstract class ElectrumWalletBase sendWorker: sendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, - feeRate: feeRateInt.toString(), + feeRate: data.feeRate.toString(), hasChange: estimatedTx.hasChange, isSendAll: estimatedTx.isSendAll, hasTaprootInputs: hasTaprootInputs, @@ -936,11 +927,10 @@ abstract class ElectrumWalletBase Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, - required BasedUtxoNetwork network, required List utxos, required Map publicKeys, String? memo, - bool enableRBF = false, + bool enableRBF = true, BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async => @@ -961,62 +951,120 @@ abstract class ElectrumWalletBase 'unspents': unspentCoins.map((e) => e.toJson()).toList(), }); - int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount, - {int? size}) => - feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); + int estimatedTransactionSize({ + required List inputTypes, + required List outputTypes, + String? memo, + bool enableRBF = true, + }) => + BitcoinTransactionBuilder.estimateTransactionSizeFromTypes( + inputTypes: inputTypes, + outputTypes: outputTypes, + network: network, + memo: memo, + enableRBF: enableRBF, + ); - int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) => - feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount)); + int feeAmountForPriority( + TransactionPriority priority, { + required List inputTypes, + required List outputTypes, + String? memo, + bool enableRBF = true, + }) => + feeRate(priority) * + estimatedTransactionSize( + inputTypes: inputTypes, + outputTypes: outputTypes, + memo: memo, + enableRBF: enableRBF, + ); - @override - int calculateEstimatedFee(TransactionPriority? priority, int? amount, - {int? outputsCount, int? size}) { - if (priority is BitcoinTransactionPriority) { - return calculateEstimatedFeeWithFeeRate( - feeRate(priority), - amount, - outputsCount: outputsCount, - size: size, + int feeAmountWithFeeRate( + int feeRate, { + required List inputTypes, + required List outputTypes, + String? memo, + bool enableRBF = true, + }) => + feeRate * + estimatedTransactionSize( + inputTypes: inputTypes, + outputTypes: outputTypes, + memo: memo, + enableRBF: enableRBF, ); - } - return 0; + @override + int estimatedFeeForOutputsWithPriority({ + required TransactionPriority priority, + List outputAddresses = const [], + String? memo, + bool enableRBF = true, + }) { + return estimatedFeeForOutputsWithFeeRate( + feeRate: feeRate(priority), + outputAddresses: outputAddresses, + memo: memo, + enableRBF: enableRBF, + ); } - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { - if (size != null) { - return feeAmountWithFeeRate(feeRate, 0, 0, size: size); - } - - int inputsCount = 0; - - if (amount != null) { - int totalValue = 0; - - for (final input in unspentCoins) { - if (totalValue >= amount) { + int estimatedFeeForOutputsWithFeeRate({ + required int feeRate, + required List outputAddresses, + String? memo, + bool enableRBF = true, + }) { + final fakePublicKey = ECPrivate.random().getPublic(); + final fakeOutputs = []; + final outputTypes = + outputAddresses.map((e) => BitcoinAddressUtils.addressTypeFromStr(e, network)).toList(); + + for (final outputType in outputTypes) { + late BitcoinBaseAddress address; + switch (outputType) { + case P2pkhAddressType.p2pkh: + address = fakePublicKey.toP2pkhAddress(); break; - } - - if (input.isSending) { - totalValue += input.value; - inputsCount += 1; - } + case P2shAddressType.p2pkInP2sh: + address = fakePublicKey.toP2pkhInP2sh(); + break; + case SegwitAddresType.p2wpkh: + address = fakePublicKey.toP2wpkhAddress(); + break; + case P2shAddressType.p2pkhInP2sh: + address = fakePublicKey.toP2pkhInP2sh(); + break; + case SegwitAddresType.p2wsh: + address = fakePublicKey.toP2wshAddress(); + break; + case SegwitAddresType.p2tr: + address = fakePublicKey.toTaprootAddress(); + break; + default: + throw const FormatException('Invalid output type'); } - if (totalValue < amount) return 0; - } else { - for (final input in unspentCoins) { - if (input.isSending) { - inputsCount += 1; - } - } + fakeOutputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); } - // If send all, then we have no change value - final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); + final estimatedFakeTx = estimateTxForAmount( + 0, + fakeOutputs, + feeRate, + memo: memo, + isFakeTx: true, + ); + final inputTypes = estimatedFakeTx.utxos.map((e) => e.ownerDetails.address.type).toList(); - return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount); + return feeAmountWithFeeRate( + feeRate, + inputTypes: inputTypes, + outputTypes: outputTypes, + memo: memo, + enableRBF: enableRBF, + ); } @override @@ -1175,8 +1223,6 @@ abstract class ElectrumWalletBase unspentCoinsInfo.values.where((record) => record.walletId == id); for (final element in currentWalletUnspentCoins) { - if (RegexUtils.addressTypeFromStr(element.address, network) is MwebAddress) continue; - final existUnspentCoins = unspentCoins.where((coin) => element == coin); if (existUnspentCoins.isEmpty) { @@ -1976,3 +2022,19 @@ class BitcoinUnspentCoins extends ObservableSet { }).toList(); } } + +class CreateTxData { + final int amount; + final int feeRate; + final List outputs; + final bool sendAll; + final String? memo; + + CreateTxData({ + required this.amount, + required this.feeRate, + required this.outputs, + required this.sendAll, + required this.memo, + }); +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index f1a051432d..fc3530a04d 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -3,7 +3,6 @@ import 'dart:io' show Platform; 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/electrum_wallet.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -174,11 +173,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future getChangeAddress({ + BitcoinAddressRecord getChangeAddress({ List? inputs, List? outputs, bool isPegIn = false, - }) async { + }) { updateChangeAddresses(); final address = changeAddresses.firstWhere( diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 2c031a8675..6a4c92f6cc 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -476,7 +476,7 @@ class ElectrumWorker { try { final recommendedFees = await ApiProvider.fromMempool( _network!, - baseUrl: "http://mempool.cakewallet.com:8999/api", + baseUrl: "http://mempool.cakewallet.com:8999/api/v1", ).getRecommendedFeeRate(); final unimportantFee = recommendedFees.economyFee!.satoshis; @@ -498,7 +498,7 @@ class ElectrumWorker { _sendResponse( ElectrumWorkerGetFeesResponse( - result: BitcoinTransactionPriorities( + result: BitcoinAPITransactionPriorities( unimportant: unimportantFee, normal: normalFee, elevated: elevatedFee, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index b06ca87105..a3a8d70adc 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,9 +7,11 @@ 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_bitcoin/exceptions.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/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; @@ -518,8 +520,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // reset coin balances and txCount to 0: unspentCoins.forEach((coin) { - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance = 0; + coin.bitcoinAddressRecord.balance = 0; coin.bitcoinAddressRecord.txCount = 0; }); @@ -930,8 +931,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { coin.isFrozen = coinInfo.isFrozen; coin.isSending = coinInfo.isSending; coin.note = coinInfo.note; - if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord) - coin.bitcoinAddressRecord.balance += coinInfo.value; + coin.bitcoinAddressRecord.balance += coinInfo.value; } else { super.addCoinInfo(coin); } @@ -1024,7 +1024,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // 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); @@ -1075,7 +1074,304 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } @override - Future calcFee({ + TxCreateUtxoDetails createUTXOS({ + required bool sendAll, + int credentialsAmount = 0, + int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { + List utxos = []; + List vinOutpoints = []; + List inputPrivKeyInfos = []; + final publicKeys = {}; + int allInputsAmount = 0; + bool spendsUnconfirmedTX = false; + + int leftAmount = credentialsAmount; + var availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); + 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.addressType == SegwitAddresType.mweb ? -1 : 1); + + for (int i = 0; i < availableInputs.length; i++) { + final utx = availableInputs[i]; + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; + + allInputsAmount += utx.value; + leftAmount = leftAmount - utx.value; + + final address = RegexUtils.addressTypeFromStr(utx.address, network); + ECPrivate? privkey; + + if (!isHardwareWallet) { + 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)); + String pubKeyHex; + + if (privkey != null) { + inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); + + pubKeyHex = privkey.getPublic().toHex(); + } else { + pubKeyHex = walletAddresses.hdWallet + .childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)) + .publicKey + .toHex(); + } + + if (utx.bitcoinAddressRecord is BitcoinAddressRecord) { + final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord) + .derivationInfo + .derivationPath + .toString(); + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + } + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: BitcoinAddressUtils.getScriptType(address), + ), + ownerDetails: UtxoAddressDetails( + publicKey: pubKeyHex, + address: address, + ), + ), + ); + + // sendAll continues for all inputs + if (!sendAll) { + bool amountIsAcquired = leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } + } + } + + if (utxos.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + return TxCreateUtxoDetails( + availableInputs: availableInputs, + unconfirmedCoins: unconfirmedCoins, + utxos: utxos, + vinOutpoints: vinOutpoints, + inputPrivKeyInfos: inputPrivKeyInfos, + publicKeys: publicKeys, + allInputsAmount: allInputsAmount, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } + + Future estimateSendAllTxMweb( + List outputs, + int feeRate, { + String? memo, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom); + + int fee = await calcFeeMweb( + utxos: utxoDetails.utxos, + outputs: outputs, + memo: memo, + feeRate: feeRate, + ); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change + int amount = utxoDetails.allInputsAmount - fee; + + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); + } + + // Attempting to send less than the dust limit + if (isBelowDust(amount)) { + throw BitcoinTransactionNoDustException(); + } + + if (outputs.length == 1) { + outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + isSendAll: true, + hasChange: false, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + ); + } + + Future estimateTxForAmountMweb( + int credentialsAmount, + List outputs, + int feeRate, { + int? inputsCount, + String? memo, + bool? useUnconfirmed, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + // Attempting to send less than the dust limit + if (isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + + final utxoDetails = createUTXOS( + sendAll: false, + credentialsAmount: credentialsAmount, + inputsCount: inputsCount, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + + final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; + final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX && + utxoDetails.utxos.length == + utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length; + + // How much is being spent - how much is being sent + int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount; + + if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmountMweb( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } + + throw BitcoinTransactionWrongBalanceException(); + } + + final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, + outputs: outputs, + ); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); + + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + + int fee = await calcFeeMweb( + utxos: utxoDetails.utxos, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: outputs, + memo: memo, + feeRate: feeRate, + ); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + int amount = credentialsAmount; + final lastOutput = outputs.last; + final amountLeftForChange = amountLeftForChangeAndFee - fee; + + if (isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + outputs.removeLast(); + outputs.removeLast(); + + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmountMweb( + credentialsAmount, + outputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + ); + } else { + // Here, lastOutput already is change, return the amount left without the fee to the user's address. + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isChange: true, + ); + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + ); + } + } + + Future calcFeeMweb({ required List utxos, required List outputs, String? memo, @@ -1085,7 +1381,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final paysToMweb = outputs .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); if (!spendsMweb && !paysToMweb) { - return await super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate); + return super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate); } if (!mwebEnabled) { @@ -1158,8 +1454,132 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future createTransaction(Object credentials) async { + final transactionCredentials = credentials as BitcoinTransactionCredentials; + try { - var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + late PendingBitcoinTransaction tx; + + try { + final data = getCreateTxDataFromCredentials(credentials); + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; + + EstimatedTxResult estimatedTx; + if (data.sendAll) { + estimatedTx = await estimateSendAllTxMweb( + data.outputs, + data.feeRate, + memo: data.memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + estimatedTx = await estimateTxForAmountMweb( + data.amount, + data.outputs, + data.feeRate, + memo: data.memo, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } + + if (walletInfo.isHardwareWallet) { + final transaction = await buildHardwareWalletTransaction( + utxos: estimatedTx.utxos, + outputs: data.outputs, + publicKeys: estimatedTx.publicKeys, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + ); + + tx = PendingBitcoinTransaction( + transaction, + type, + sendWorker: sendWorker, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: data.feeRate.toString(), + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + await updateAllUnspents(); + }); + } else { + final txb = BitcoinTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: data.outputs, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: !estimatedTx.spendsUnconfirmedTX, + ); + + bool hasTaprootInputs = false; + + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + String error = "Cannot find private key."; + + ECPrivateInfo? key; + + if (estimatedTx.inputPrivKeyInfos.isEmpty) { + error += "\nNo private keys generated."; + } else { + error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}"; + + key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) { + final elemPubkey = element.privkey.getPublic().toHex(); + if (elemPubkey == publicKey) { + return true; + } else { + error += "\nExpected: $publicKey"; + error += "\nPubkey: $elemPubkey"; + return false; + } + }); + } + + if (key == null) { + throw Exception(error); + } + + if (utxo.utxo.isP2tr()) { + hasTaprootInputs = true; + return key.privkey.signTapRoot(txDigest, sighash: sighash); + } else { + return key.privkey.signInput(txDigest, sigHash: sighash); + } + }); + + tx = PendingBitcoinTransaction( + transaction, + type, + sendWorker: sendWorker, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: data.feeRate.toString(), + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + + await updateBalance(); + await updateAllUnspents(); + }); + } + } catch (e) { + throw e; + } + tx.isMweb = mwebEnabled; if (!mwebEnabled) { @@ -1178,9 +1598,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { )); final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx)); - // check if the transaction doesn't contain any mweb inputs or outputs: - final transactionCredentials = credentials as BitcoinTransactionCredentials; - bool hasMwebInput = false; bool hasMwebOutput = false; bool hasRegularOutput = false; @@ -1249,12 +1666,10 @@ 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 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 key = ECPrivate.fromBip32( + bip32: bip32.derive((utxo.bitcoinAddressRecord as BitcoinAddressRecord) + .derivationInfo + .derivationPath)); final digest = tx2.getTransactionSegwitDigit( txInIndex: e.key, script: key.getPublic().toP2pkhAddress().toScriptPubKey(), diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 482f18e303..68ec634edc 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -163,11 +163,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - Future getChangeAddress({ + BitcoinAddressRecord 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) { @@ -209,7 +209,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } if (mwebEnabled) { - await ensureMwebAddressUpToIndexExists(1); + // TODO: + // await ensureMwebAddressUpToIndexExists(1); return BitcoinAddressRecord( mwebAddrs[0], index: 0, diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index af39965b95..8b591180ea 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -184,27 +184,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw), ); - int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) { - int inputsCount = 0; - int totalValue = 0; - - for (final input in unspentCoins) { - if (input.isSending) { - inputsCount++; - totalValue += input.value; - } - if (amount != null && totalValue >= amount) { - break; - } - } - - if (amount != null && totalValue < amount) return 0; - - final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); - - return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount); - } - @override int feeRate(TransactionPriority priority) { if (priority is ElectrumTransactionPriority) { @@ -242,12 +221,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } @override - Future calcFee({ + int calcFee({ required List utxos, required List outputs, String? memo, required int feeRate, - }) async => + }) => feeRate * ForkedTransactionBuilder.estimateTransactionSize( utxos: utxos, diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index 35282f49e4..78589abf46 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -4,6 +4,10 @@ abstract class TransactionPriority extends EnumerableItem with Serializable const TransactionPriority({required super.title, required super.raw}); String get units => ''; + String getUnits(int rate) { + return rate == 1 ? units : '${units}s'; + } + String toString() { return title; } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 16c794a25f..bebf268fea 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -24,6 +24,9 @@ abstract class WalletBase walletInfo.type; + bool get isElectrum => + type == WalletType.bitcoin || type == WalletType.litecoin || type == WalletType.bitcoinCash; + CryptoCurrency get currency => currencyForWalletType(type, isTestnet: isTestnet); String get id => walletInfo.id; @@ -71,7 +74,7 @@ abstract class WalletBase createTransaction(Object credentials); - int calculateEstimatedFee(TransactionPriority priority, int? amount); + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}); // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index dca16539c7..f0dc9c2733 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -29,7 +29,6 @@ import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_ledger_credentials.dart'; -import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -188,7 +187,7 @@ abstract class EVMChainWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { { try { if (priority is EVMChainTransactionPriority) { diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 6c372d3443..b12b5304a2 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -219,7 +219,7 @@ abstract class HavenWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { // FIXME: hardcoded value; if (priority is MoneroTransactionPriority) { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 21d5b6d4b1..1e6e5189f9 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -7,7 +7,7 @@ import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/monero_amount_format.dart'; +// import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/monero_balance.dart'; import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/monero_wallet_keys.dart'; @@ -28,7 +28,7 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history; import 'package:cw_monero/api/wallet.dart' as monero_wallet; import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; -import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +// import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; import 'package:cw_monero/ledger.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/monero_transaction_history.dart'; @@ -50,8 +50,8 @@ const MIN_RESTORE_HEIGHT = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase extends WalletBase with Store { +abstract class MoneroWalletBase + extends WalletBase with Store { MoneroWalletBase( {required WalletInfo walletInfo, required Box unspentCoinsInfo, @@ -72,16 +72,13 @@ abstract class MoneroWalletBase extends WalletBase walletAddresses.account, (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) return; - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)) + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) }); _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); _askForUpdateTransactionHistory(); @@ -131,8 +128,7 @@ abstract class MoneroWalletBase extends WalletBase - transactionHistory.transactions.values.firstOrNull?.height; + int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height; monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -145,13 +141,11 @@ abstract class MoneroWalletBase extends WalletBase init() async { await walletAddresses.init(); - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance( - accountIndex: walletAddresses.account!.id), - unlockedBalance: monero_wallet.getUnlockedBalance( - accountIndex: walletAddresses.account!.id)) + fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) }); _setListeners(); await updateTransactions(); @@ -160,15 +154,14 @@ abstract class MoneroWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); // update transaction details after restore - walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id ?? 0); } @override @@ -271,9 +264,9 @@ abstract class MoneroWalletBase extends WalletBase[]; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; - final unlockedBalance = monero_wallet.getUnlockedBalance( - accountIndex: walletAddresses.account!.id); - var allInputsAmount = 0; + final unlockedBalance = + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + // var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -287,55 +280,44 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw MoneroTransactionCreationException( - 'You do not have enough XMR to send this amount.'); + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - final int totalAmount = outputs.fold( - 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - final estimatedFee = - calculateEstimatedFee(_credentials.priority, totalAmount); + // final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority); if (unlockedBalance < totalAmount) { - throw MoneroTransactionCreationException( - 'You do not have enough XMR to send this amount.'); + throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); } - if (inputs.isEmpty) MoneroTransactionCreationException( - 'No inputs selected'); + if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected'); final moneroOutputs = outputs.map((output) { - final outputAddress = - output.isParsedAddress ? output.extractedAddress : output.address; + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; return MoneroOutput( - address: outputAddress!, - amount: output.cryptoAmount!.replaceAll(',', '.')); + address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( - outputs: moneroOutputs, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id, - preferredInputs: inputs); + pendingTransactionDescription = await transaction_history.createTransactionMultDest( + outputs: moneroOutputs, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } else { final output = outputs.first; - final address = - output.isParsedAddress ? output.extractedAddress : output.address; - final amount = - output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); - final formattedAmount = - output.sendAll ? null : output.formattedCryptoAmount; + final address = output.isParsedAddress ? output.extractedAddress : output.address; + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + // final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; // if ((formattedAmount != null && unlockedBalance < formattedAmount) || // (formattedAmount == null && unlockedBalance <= 0)) { @@ -345,17 +327,14 @@ abstract class MoneroWalletBase extends WalletBase changePassword(String password) async => - monero_wallet.setPasswordSync(password); + Future changePassword(String password) async => monero_wallet.setPasswordSync(password); Future getNodeHeight() async => monero_wallet.getNodeHeight(); @@ -512,7 +486,8 @@ abstract class MoneroWalletBase extends WalletBase _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values.where( - (element) => - element.walletId.contains(id) && - element.accountIndex == walletAddresses.account!.id); + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = unspentCoins - .where((coin) => element.keyImage!.contains(coin.keyImage!)); + final existUnspentCoins = + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -610,15 +583,13 @@ abstract class MoneroWalletBase extends WalletBase - monero_wallet.getAddress( - accountIndex: accountIndex, addressIndex: addressIndex); + monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { transaction_history.refreshTransactions(); return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) - .fold>( - {}, + .fold>({}, (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; @@ -647,15 +618,12 @@ abstract class MoneroWalletBase extends WalletBase> _getAllTransactionsOfAccount(int? accountIndex) async => - (await transaction_history - .getAllTransactions()) + (await transaction_history.getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, row.blockheight, - row.isSpend - ? TransactionDirection.outgoing - : TransactionDirection.incoming, + row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming, row.timeStamp, row.isPending, row.amount, @@ -704,8 +672,7 @@ abstract class MoneroWalletBase extends WalletBase _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); - int _getFullBalance() => - monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + // int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); - int _getUnlockedBalance() => monero_wallet.getUnlockedBalance( - accountIndex: walletAddresses.account!.id); + int _getUnlockedBalance() => + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); int _getFrozenBalance() { var frozenBalance = 0; for (var coin in unspentCoinsInfo.values.where((element) => - element.walletId == id && - element.accountIndex == walletAddresses.account!.id)) { + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value; } return frozenBalance; @@ -827,8 +788,7 @@ abstract class MoneroWalletBase extends WalletBase 0; // always 0 :) + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => + 0; // always 0 :) @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 33a2e7df41..f02be11b9a 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -33,7 +33,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:solana/base58.dart'; import 'package:solana/metaplex.dart' as metaplex; import 'package:solana/solana.dart'; -import 'package:solana/src/crypto/ed25519_hd_keypair.dart'; part 'solana_wallet.g.dart'; @@ -174,7 +173,7 @@ abstract class SolanaWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0; @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index cfa80f0d34..623c378f52 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -211,7 +211,7 @@ abstract class TronWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0; @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index c4c79af11a..87cd47df7f 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -51,7 +51,9 @@ abstract class WowneroWalletBase extends WalletBase with Store { WowneroWalletBase( - {required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password}) + {required WalletInfo walletInfo, + required Box unspentCoinsInfo, + required String password}) : balance = ObservableMap.of({ CryptoCurrency.wow: WowneroBalance( fullBalance: wownero_wallet.getFullBalance(accountIndex: 0), @@ -259,7 +261,7 @@ abstract class WowneroWalletBase final int totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); + final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority); if (unlockedBalance < totalAmount) { throw WowneroTransactionCreationException( 'You do not have enough WOW to send this amount.'); @@ -295,7 +297,7 @@ abstract class WowneroWalletBase 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } - final estimatedFee = calculateEstimatedFee(_credentials.priority, formattedAmount); + final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority); if (!spendAllCoins && ((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) || formattedAmount == null)) { @@ -314,7 +316,7 @@ abstract class WowneroWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { + int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { // FIXME: hardcoded value; if (priority is MoneroTransactionPriority) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 92c5143250..ab3b5ad5a9 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -472,31 +472,83 @@ class CWBitcoin extends Bitcoin { } @override - int getFeeAmountForPriority( - Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, - {int? size}) { + int getFeeAmountForOutputsWithFeeRate( + Object wallet, { + required int feeRate, + required List inputAddresses, + required List outputAddresses, + String? memo, + bool enableRBF = true, + }) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeAmountWithFeeRate( + feeRate, + inputTypes: inputAddresses + .map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network)) + .toList(), + outputTypes: outputAddresses + .map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network)) + .toList(), + memo: memo, + enableRBF: enableRBF, + ); + } + + @override + int getFeeAmountForOutputsWithPriority( + Object wallet, { + required TransactionPriority priority, + required List inputAddresses, + required List outputAddresses, + String? memo, + bool enableRBF = true, + }) { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.feeAmountForPriority( - priority as ElectrumTransactionPriority, inputsCount, outputsCount); + priority, + inputTypes: inputAddresses + .map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network)) + .toList(), + outputTypes: outputAddresses + .map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network)) + .toList(), + memo: memo, + enableRBF: enableRBF, + ); } @override - int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, - {int? outputsCount, int? size}) { + int estimatedFeeForOutputsWithPriority( + Object wallet, { + required TransactionPriority priority, + required String outputAddress, + String? memo, + bool enableRBF = true, + }) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.calculateEstimatedFeeWithFeeRate( - feeRate, - amount, - outputsCount: outputsCount, - size: size, + return bitcoinWallet.estimatedFeeForOutputsWithPriority( + priority: priority, + outputAddresses: [outputAddress], + memo: memo, + enableRBF: enableRBF, ); } @override - int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, - {int? size}) { + int estimatedFeeForOutputWithFeeRate( + Object wallet, { + required int feeRate, + required String outputAddress, + String? memo, + bool enableRBF = true, + }) { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.feeAmountWithFeeRate(feeRate, inputsCount, outputsCount, size: size); + return bitcoinWallet.estimatedFeeForOutputsWithFeeRate( + feeRate: feeRate, + outputAddresses: [outputAddress], + memo: memo, + enableRBF: enableRBF, + ); } @override @@ -505,7 +557,7 @@ class CWBitcoin extends Bitcoin { final feeRates = electrumWallet.feeRates; final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities ? ElectrumTransactionPriority.fast - : BitcoinTransactionPriority.priority; + : BitcoinAPITransactionPriority.priority; return (electrumWallet.feeRate(maxFee) * 10).round(); } @@ -640,7 +692,12 @@ class CWBitcoin extends Bitcoin { Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}) async { if (bitcoinMempoolAPIEnabled ?? false) { try { - return await getBitcoinHeightByDateAPI(date: date); + final mempoolApi = ApiProvider.fromMempool( + BitcoinNetwork.mainnet, + baseUrl: "http://mempool.cakewallet.com:8999/api/v1", + ); + + return (await mempoolApi.getBlockTimestamp(date))["height"] as int; } catch (_) {} } return await getBitcoinHeightByDate(date: date); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index f977ef003f..be7328b361 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -144,22 +144,29 @@ abstract class OutputBase with Store { return solana!.getEstimateFees(_wallet) ?? 0.0; } - int? fee = _wallet.calculateEstimatedFee( - _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); - - if (_wallet.type == WalletType.bitcoin) { - if (_settingsStore.priority[_wallet.type] == - bitcoin!.getBitcoinTransactionPriorityCustom()) { - fee = bitcoin!.getEstimatedFeeWithFeeRate( - _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); + final transactionPriority = _settingsStore.priority[_wallet.type]!; + + if (_wallet.isElectrum) { + late int fee; + + if (transactionPriority == bitcoin!.getBitcoinTransactionPriorityCustom()) { + fee = bitcoin!.estimatedFeeForOutputWithFeeRate( + _wallet, + feeRate: _settingsStore.customBitcoinFeeRate, + outputAddress: address, + ); + } else { + fee = bitcoin!.estimatedFeeForOutputsWithPriority( + _wallet, + priority: transactionPriority, + outputAddress: address, + ); } return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } - if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) { - return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); - } + final fee = _wallet.estimatedFeeForOutputsWithPriority(priority: transactionPriority); if (_wallet.type == WalletType.monero) { return monero!.formatterMoneroAmountToDouble(amount: fee); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 9ec542361a..10ab425f1f 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -542,15 +542,13 @@ abstract class TransactionDetailsViewModelBase with Store { void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) { transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium(); - final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true) - ? 1 - : transactionInfo.inputAddresses!.length; - final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true) - ? 1 - : transactionInfo.outputAddresses!.length; - newFee = bitcoin!.getFeeAmountForPriority( - wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount); + newFee = bitcoin!.getFeeAmountForOutputsWithPriority( + wallet, + priority: bitcoin!.getBitcoinTransactionPriorityMedium(), + inputAddresses: transactionInfo.inputAddresses!, + outputAddresses: transactionInfo.outputAddresses!, + ); RBFListItems.add( StandartListItem( @@ -677,17 +675,21 @@ abstract class TransactionDetailsViewModelBase with Store { } String setNewFee({double? value, required TransactionPriority priority}) { - newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null - ? bitcoin!.feeAmountWithFeeRate( - wallet, - value.round(), - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1) - : bitcoin!.getFeeAmountForPriority( - wallet, - priority, - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + if (priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null) { + newFee = bitcoin!.getFeeAmountForOutputsWithFeeRate( + wallet, + feeRate: value.round(), + inputAddresses: transactionInfo.inputAddresses!, + outputAddresses: transactionInfo.outputAddresses!, + ); + } else { + newFee = bitcoin!.getFeeAmountForOutputsWithPriority( + wallet, + priority: priority, + inputAddresses: transactionInfo.inputAddresses!, + outputAddresses: transactionInfo.outputAddresses!, + ); + } return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } From cf9d64b6374ece6fb779d588fa56cf389c7c65c4 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 26 Dec 2024 19:27:56 -0300 Subject: [PATCH 35/64] chore: build --- cw_bitcoin/pubspec.lock | 4 ++-- cw_haven/pubspec.lock | 4 ++-- cw_monero/pubspec.lock | 4 ++-- cw_nano/pubspec.lock | 4 ++-- cw_wownero/pubspec.lock | 4 ++-- tool/configure.dart | 34 ++++++++++++++++++++++++++++++---- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 48c4b6a86a..66d164f69f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -918,8 +918,8 @@ packages: dependency: "direct main" description: path: "." - ref: cake-update-v3 - resolved-ref: "2c21e53fd652e0aee1ee5fcd891376c10334237b" + ref: cake-update-v4 + resolved-ref: bae6ecb9cd10b80e6c496dc963c26de2aee9751c url: "https://github.com/cake-tech/sp_scanner.git" source: git version: "0.0.1" diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock index b6cae9f397..cb5d3e2c35 100644 --- a/cw_haven/pubspec.lock +++ b/cw_haven/pubspec.lock @@ -716,10 +716,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 24be1c0dd0..9aa076a565 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -829,10 +829,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index f426d96dce..f4d5c00f87 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -874,10 +874,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/cw_wownero/pubspec.lock b/cw_wownero/pubspec.lock index 1e16fa089f..532bb236b0 100644 --- a/cw_wownero/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -757,10 +757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: "direct overridden" description: diff --git a/tool/configure.dart b/tool/configure.dart index 112dbb5a7d..2cd99c5df4 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -242,10 +242,36 @@ abstract class Bitcoin { Future canReplaceByFee(Object wallet, Object tx); int getTransactionVSize(Object wallet, String txHex); Future isChangeSufficientForFee(Object wallet, String txId, String newFee); - int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size}); - int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount, - {int? outputsCount, int? size}); - int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount, {int? size}); + int getFeeAmountForOutputsWithFeeRate( + Object wallet, { + required int feeRate, + required List inputAddresses, + required List outputAddresses, + String? memo, + bool enableRBF = true, + }); + int getFeeAmountForOutputsWithPriority( + Object wallet, { + required TransactionPriority priority, + required List inputAddresses, + required List outputAddresses, + String? memo, + bool enableRBF = true, + }); + int estimatedFeeForOutputsWithPriority( + Object wallet, { + required TransactionPriority priority, + required String outputAddress, + String? memo, + bool enableRBF = true, + }); + int estimatedFeeForOutputWithFeeRate( + Object wallet, { + required int feeRate, + required String outputAddress, + String? memo, + bool enableRBF = true, + }); Future registerSilentPaymentsKey(Object wallet, bool active); Future checkIfMempoolAPIIsEnabled(Object wallet); Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); From bcf9aaebfb4a58d10d75cc54dce0952f20570e7b Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 30 Dec 2024 10:32:52 -0300 Subject: [PATCH 36/64] refactor: reviews --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 11 +- cw_bitcoin/lib/electrum_transaction_info.dart | 29 ++-- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 5 +- .../electrum_worker_params.dart | 2 - cw_core/lib/get_height_by_date.dart | 1 + .../pages/balance/crypto_balance_widget.dart | 154 +++++++++++++++--- 6 files changed, 151 insertions(+), 51 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 2f2f87084a..b9744f4246 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -34,6 +34,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final ObservableList silentPaymentAddresses; final ObservableList receivedSPAddresses; + List get usableSilentPaymentAddresses => silentPaymentAddresses + .where((addressRecord) => + addressRecord.addressType != SegwitAddresType.p2tr && + addressRecord.derivationPath == BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND) + .toList(); + @observable List silentPaymentWallets = []; @@ -240,10 +246,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { if (addressPageType == SilentPaymentsAddresType.p2sp) { - final currentSPLabelIndex = silentPaymentAddresses - .where((addressRecord) => addressRecord.addressType != SegwitAddresType.p2tr) - .length - - 1; + final currentSPLabelIndex = usableSilentPaymentAddresses.length - 1; final address = BitcoinSilentPaymentAddressRecord( silentPaymentWallet!.toLabeledSilentPaymentAddress(currentSPLabelIndex).toString(), diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 816f302211..9c1fcd58b5 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -160,25 +160,18 @@ class ElectrumTransactionInfo extends TransactionInfo { List inputAddresses = []; List outputAddresses = []; - 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)); - } + 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) { - printV(bundle.originalTransaction.txId()); - printV("original: ${bundle.originalTransaction}"); - printV("bundle.inputs: ${bundle.originalTransaction.inputs}"); - printV("ins: ${bundle.ins}"); - rethrow; } final receivedAmounts = []; diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 829f10de39..5be10e8949 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -131,10 +131,7 @@ class ElectrumWalletSnapshot { mwebAddresses: mwebAddresses, alwaysScan: alwaysScan, unspentCoins: (data['unspent_coins'] as List) - .map((e) => BitcoinUnspent.fromJSON( - null, - e as Map, - )) + .map((e) => BitcoinUnspent.fromJSON(null, e as Map)) .toList(), ); } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart index ea3c0b1994..ca96f29fe5 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -1,5 +1,3 @@ -// import 'dart:convert'; - import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; abstract class ElectrumWorkerRequest { diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index 1d3c3f4be0..e17b744816 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -246,6 +246,7 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-12": 872708, "2024-11": 868345, "2024-10": 863584, "2024-09": 859317, diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index 0bdf388d3a..56ac8bbb76 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; @@ -255,12 +257,107 @@ class CryptoBalanceWidget extends StatelessWidget { ], ), ), - Observer( - builder: (_) => StandardSwitch( - value: dashboardViewModel.silentPaymentsScanningActive, - onTaped: () => _toggleSilentPaymentsScanning(context), + ], + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Semantics( + label: S.of(context).receive, + child: OutlinedButton( + onPressed: () { + Navigator.pushNamed( + context, + Routes.addressPage, + arguments: { + 'addressType': bitcoin! + .getBitcoinReceivePageOptions() + .where( + (option) => option.value == "Silent Payments", + ) + .first + }, + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + height: 30, + width: 30, + 'assets/images/received.png', + color: Theme.of(context) + .extension()! + .balanceAmountColor, + ), + const SizedBox(width: 8), + Text( + S.of(context).receive, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), ), - ) + ), + SizedBox(width: 24), + Expanded( + child: Semantics( + label: S.of(context).scan, + child: OutlinedButton( + onPressed: () => _toggleSilentPaymentsScanning(context), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.grey.shade400.withAlpha(50), + side: BorderSide( + color: Colors.grey.shade400.withAlpha(50), width: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Observer( + builder: (_) => StandardSwitch( + value: + dashboardViewModel.silentPaymentsScanningActive, + onTaped: () => _toggleSilentPaymentsScanning(context), + ), + ), + SizedBox(width: 8), + Text( + S.of(context).scan, + style: TextStyle( + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ], + ), + ), + ), + ), + ), ], ), ], @@ -364,29 +461,40 @@ class CryptoBalanceWidget extends StatelessWidget { Future _toggleSilentPaymentsScanning(BuildContext context) async { final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; final newValue = !isSilentPaymentsScanningActive; - + final willScan = newValue == true; dashboardViewModel.silentPaymentsScanningActive = newValue; - final needsToSwitch = !isSilentPaymentsScanningActive && - await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false; + if (willScan) { + late bool isElectrsSPEnabled; + try { + isElectrsSPEnabled = await bitcoin! + .getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) + .timeout(const Duration(seconds: 3)); + } on TimeoutException { + isElectrsSPEnabled = false; + } - if (needsToSwitch) { - return showPopUp( + final needsToSwitch = isElectrsSPEnabled == false; + if (needsToSwitch) { + return showPopUp( context: context, builder: (BuildContext context) => AlertWithTwoActions( - alertTitle: S.of(context).change_current_node_title, - alertContent: S.of(context).confirm_silent_payments_switch_node, - rightButtonText: S.of(context).confirm, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - dashboardViewModel.setSilentPaymentsScanning(newValue); - Navigator.of(context).pop(); - }, - actionLeftButton: () { - dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; - Navigator.of(context).pop(); - }, - )); + alertTitle: S.of(context).change_current_node_title, + alertContent: S.of(context).confirm_silent_payments_switch_node, + rightButtonText: S.of(context).confirm, + leftButtonText: S.of(context).cancel, + actionRightButton: () { + dashboardViewModel.allowSilentPaymentsScanning(true); + dashboardViewModel.setSilentPaymentsScanning(true); + Navigator.of(context).pop(); + }, + actionLeftButton: () { + dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; + Navigator.of(context).pop(); + }, + ), + ); + } } return dashboardViewModel.setSilentPaymentsScanning(newValue); From 6019370a1a42460390ee845e2667d8a35a740bfe Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 2 Jan 2025 12:51:23 -0300 Subject: [PATCH 37/64] refactor: reviews --- .../lib/electrum_worker/electrum_worker.dart | 1 + .../screens/dashboard/pages/balance_page.dart | 1270 ----------------- lib/src/screens/rescan/rescan_page.dart | 1 + 3 files changed, 2 insertions(+), 1270 deletions(-) delete mode 100644 lib/src/screens/dashboard/pages/balance_page.dart diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 6a4c92f6cc..ba4b482aae 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -579,6 +579,7 @@ class ElectrumWorker { } // Initial status UI update, send how many blocks in total to scan + // TODO: isSingleScan : dont update restoreHeight _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( height: syncHeight, diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart deleted file mode 100644 index 7565412b07..0000000000 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ /dev/null @@ -1,1270 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/dashboard/pages/nft_listing_page.dart'; -import 'package:cake_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; -import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; -import 'package:cake_wallet/src/widgets/introducing_card.dart'; -import 'package:cake_wallet/src/widgets/standard_switch.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; -import 'package:cake_wallet/utils/feature_flag.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/unspent_coin_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class BalancePage extends StatelessWidget { - BalancePage({ - required this.dashboardViewModel, - required this.settingsStore, - required this.nftViewModel, - }); - - final DashboardViewModel dashboardViewModel; - final NFTViewModel nftViewModel; - final SettingsStore settingsStore; - - @override - Widget build(BuildContext context) { - return Observer( - builder: (context) { - final isEVMCompatible = isEVMCompatibleChain(dashboardViewModel.type); - return DefaultTabController( - length: isEVMCompatible ? 2 : 1, - child: Column( - children: [ - if (isEVMCompatible) - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 8), - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - physics: NeverScrollableScrollPhysics(), - labelStyle: TextStyle( - fontSize: 18, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: - Theme.of(context).extension()!.pageTitleTextColor, - height: 1, - ), - unselectedLabelStyle: TextStyle( - fontSize: 18, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: - Theme.of(context).extension()!.pageTitleTextColor, - height: 1, - ), - labelColor: - Theme.of(context).extension()!.pageTitleTextColor, - dividerColor: Colors.transparent, - indicatorColor: - Theme.of(context).extension()!.pageTitleTextColor, - unselectedLabelColor: Theme.of(context) - .extension()! - .pageTitleTextColor - .withOpacity(0.5), - tabAlignment: TabAlignment.start, - tabs: [ - Tab(text: 'My Crypto'), - Tab(text: 'My NFTs'), - ], - ), - ), - ), - Expanded( - child: TabBarView( - physics: NeverScrollableScrollPhysics(), - children: [ - CryptoBalanceWidget(dashboardViewModel: dashboardViewModel), - if (isEVMCompatible) NFTListingPage(nftViewModel: nftViewModel) - ], - ), - ), - ], - ), - ); - }, - ); - } -} - -class CryptoBalanceWidget extends StatelessWidget { - const CryptoBalanceWidget({ - super.key, - required this.dashboardViewModel, - }); - - final DashboardViewModel dashboardViewModel; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Observer( - builder: (_) { - if (dashboardViewModel.getMoneroError != null) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DashBoardRoundedCardWidget( - title: "Invalid monero bindings", - subTitle: dashboardViewModel.getMoneroError.toString(), - onTap: () {}, - ), - ); - } - return Container(); - }, - ), - Observer( - builder: (_) { - if (dashboardViewModel.getWowneroError != null) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: DashBoardRoundedCardWidget( - title: "Invalid wownero bindings", - subTitle: dashboardViewModel.getWowneroError.toString(), - onTap: () {}, - )); - } - return Container(); - }, - ), - Observer( - builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts - ? HomeScreenAccountWidget( - walletName: dashboardViewModel.name, - accountName: dashboardViewModel.subname) - : Column( - children: [ - SizedBox(height: 16), - Container( - margin: const EdgeInsets.only(left: 24, bottom: 16), - child: Observer( - builder: (_) { - return Row( - children: [ - Text( - dashboardViewModel.balanceViewModel.asset, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - if (dashboardViewModel.wallet.isHardwareWallet) - Padding( - padding: const EdgeInsets.all(8.0), - child: Image.asset( - 'assets/images/hardware_wallet/ledger_nano_x.png', - width: 24, - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - ), - ), - if (dashboardViewModel - .balanceViewModel.isHomeScreenSettingsEnabled) - InkWell( - onTap: () => Navigator.pushNamed( - context, Routes.homeSettings, - arguments: dashboardViewModel.balanceViewModel), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Image.asset( - 'assets/images/home_screen_settings_icon.png', - color: Theme.of(context) - .extension()! - .pageTitleTextColor, - ), - ), - ), - ], - ); - }, - ), - ), - ], - )), - Observer( - builder: (_) { - if (dashboardViewModel.balanceViewModel.isShowCard && - FeatureFlag.isCakePayEnabled) { - return IntroducingCard( - title: S.of(context).introducing_cake_pay, - subTitle: S.of(context).cake_pay_learn_more, - borderColor: Theme.of(context).extension()!.cardBorderColor, - closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); - } - return Container(); - }, - ), - Observer(builder: (_) { - if (!dashboardViewModel.showRepWarning) { - return const SizedBox(); - } - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - title: S.of(context).rep_warning, - subTitle: S.of(context).rep_warning_sub, - onTap: () => Navigator.of(context).pushNamed(Routes.changeRep), - onClose: () { - dashboardViewModel.settingsStore.shouldShowRepWarning = false; - }, - ), - ); - }), - Observer( - builder: (_) { - return ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), - itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, - itemBuilder: (__, index) { - final balance = - dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); - return Observer(builder: (_) { - return BalanceRowWidget( - dashboardViewModel: dashboardViewModel, - availableBalanceLabel: - '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', - availableBalance: balance.availableBalance, - availableFiatBalance: balance.fiatAvailableBalance, - additionalBalanceLabel: - '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', - additionalBalance: balance.additionalBalance, - additionalFiatBalance: balance.fiatAdditionalBalance, - frozenBalance: balance.frozenBalance, - frozenFiatBalance: balance.fiatFrozenBalance, - currency: balance.asset, - hasAdditionalBalance: - dashboardViewModel.balanceViewModel.hasAdditionalBalance, - hasSecondAdditionalBalance: - dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance, - hasSecondAvailableBalance: - dashboardViewModel.balanceViewModel.hasSecondAvailableBalance, - secondAdditionalBalance: balance.secondAdditionalBalance, - secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance, - secondAvailableBalance: balance.secondAvailableBalance, - secondAvailableFiatBalance: balance.fiatSecondAvailableBalance, - secondAdditionalBalanceLabel: - '${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}', - secondAvailableBalanceLabel: - '${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}', - isTestnet: dashboardViewModel.isTestnet, - ); - }); - }, - ); - }, - ), - Observer(builder: (context) { - return Column( - children: [ - if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: "This wallet has encountered an issue", - subTitle: "Here are the things that you should note:\n - " + - dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") + - "\n\nPlease restart your wallet and if it doesn't help contact our support.", - onTap: () {}, - )) - ], - if (dashboardViewModel.showSilentPaymentsCard) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: S.of(context).silent_payments, - subTitle: S.of(context).enable_silent_payments_scanning, - hint: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/bitcoin#silent-payments"), - mode: LaunchMode.externalApplication, - ), - child: Row( - children: [ - Text( - S.of(context).what_is_silent_payments, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], - ), - ), - ], - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Semantics( - label: S.of(context).receive, - child: OutlinedButton( - onPressed: () { - Navigator.pushNamed( - context, - Routes.addressPage, - arguments: { - 'addressType': bitcoin! - .getBitcoinReceivePageOptions() - .where( - (option) => option.value == "Silent Payments", - ) - .first - }, - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400.withAlpha(50), - side: BorderSide( - color: Colors.grey.shade400.withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - height: 30, - width: 30, - 'assets/images/received.png', - color: Theme.of(context) - .extension()! - .balanceAmountColor, - ), - const SizedBox(width: 8), - Text( - S.of(context).receive, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - SizedBox(width: 24), - Expanded( - child: Semantics( - label: S.of(context).scan, - child: OutlinedButton( - onPressed: () => _toggleSilentPaymentsScanning(context), - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400.withAlpha(50), - side: BorderSide( - color: Colors.grey.shade400.withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Observer( - builder: (_) => StandardSwitch( - value: - dashboardViewModel.silentPaymentsScanningActive, - onTaped: () => - _toggleSilentPaymentsScanning(context), - ), - ), - SizedBox(width: 8), - Text( - S.of(context).scan, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ], - ), - onTap: () => _toggleSilentPaymentsScanning(context), - icon: Icon( - Icons.lock, - color: - Theme.of(context).extension()!.pageTitleTextColor, - size: 50, - ), - ), - ), - ], - if (dashboardViewModel.showMwebCard) ...[ - SizedBox(height: 10), - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: DashBoardRoundedCardWidget( - customBorder: 30, - title: S.of(context).litecoin_mweb, - subTitle: S.of(context).litecoin_mweb_description, - hint: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - softWrap: true, - ), - ), - SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () => _enableMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - child: Text( - S.of(context).enable, - maxLines: 1, - ), - ), - ), - ], - ), - ], - ), - onTap: () => {}, - icon: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, - ), - ), - ), - ), - ], - ], - ); - }), - ], - ), - ); - } - - Future _toggleSilentPaymentsScanning(BuildContext context) async { - final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive; - final newValue = !isSilentPaymentsScanningActive; - final willScan = newValue == true; - dashboardViewModel.silentPaymentsScanningActive = newValue; - - if (willScan) { - late bool isElectrsSPEnabled; - try { - isElectrsSPEnabled = await bitcoin! - .getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) - .timeout(const Duration(seconds: 3)); - } on TimeoutException { - isElectrsSPEnabled = false; - } - - final needsToSwitch = isElectrsSPEnabled == false; - if (needsToSwitch) { - return showPopUp( - context: context, - builder: (BuildContext context) => AlertWithTwoActions( - alertTitle: S.of(context).change_current_node_title, - alertContent: S.of(context).confirm_silent_payments_switch_node, - rightButtonText: S.of(context).confirm, - leftButtonText: S.of(context).cancel, - actionRightButton: () { - dashboardViewModel.allowSilentPaymentsScanning(true); - dashboardViewModel.setSilentPaymentsScanning(true); - Navigator.of(context).pop(); - }, - actionLeftButton: () { - dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive; - Navigator.of(context).pop(); - }, - ), - ); - } - } - - return dashboardViewModel.setSilentPaymentsScanning(newValue); - } - - Future _enableMweb(BuildContext context) async { - if (!dashboardViewModel.hasEnabledMwebBefore) { - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).litecoin_mweb_warning, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - )); - } - dashboardViewModel.setMwebEnabled(); - } - - Future _dismissMweb(BuildContext context) async { - await showPopUp( - context: context, - builder: (BuildContext context) => AlertWithOneAction( - alertTitle: S.of(context).alert_notice, - alertContent: S.of(context).litecoin_mweb_enable_later, - buttonText: S.of(context).understand, - buttonAction: () { - Navigator.of(context).pop(); - }, - )); - dashboardViewModel.dismissMweb(); - } -} - -class BalanceRowWidget extends StatelessWidget { - BalanceRowWidget({ - required this.availableBalanceLabel, - required this.availableBalance, - required this.availableFiatBalance, - required this.additionalBalanceLabel, - required this.additionalBalance, - required this.additionalFiatBalance, - required this.secondAvailableBalanceLabel, - required this.secondAvailableBalance, - required this.secondAvailableFiatBalance, - required this.secondAdditionalBalanceLabel, - required this.secondAdditionalBalance, - required this.secondAdditionalFiatBalance, - required this.frozenBalance, - required this.frozenFiatBalance, - required this.currency, - required this.hasAdditionalBalance, - required this.hasSecondAvailableBalance, - required this.hasSecondAdditionalBalance, - required this.isTestnet, - required this.dashboardViewModel, - super.key, - }); - - final String availableBalanceLabel; - final String availableBalance; - final String availableFiatBalance; - final String additionalBalanceLabel; - final String additionalBalance; - final String additionalFiatBalance; - final String secondAvailableBalanceLabel; - final String secondAvailableBalance; - final String secondAvailableFiatBalance; - final String secondAdditionalBalanceLabel; - final String secondAdditionalBalance; - final String secondAdditionalFiatBalance; - final String frozenBalance; - final String frozenFiatBalance; - final CryptoCurrency currency; - final bool hasAdditionalBalance; - final bool hasSecondAvailableBalance; - final bool hasSecondAdditionalBalance; - final bool isTestnet; - final DashboardViewModel dashboardViewModel; - - // void _showBalanceDescription(BuildContext context) { - // showPopUp( - // context: context, - // builder: (_) => - // InformationPage(information: S.of(context).available_balance_description), - // ); - // } - - @override - Widget build(BuildContext context) { - return Column(children: [ - Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, - ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => _showBalanceDescription( - context, S.of(context).available_balance_description) - : null, - child: Row( - children: [ - Semantics( - hint: 'Double tap to see more information', - container: true, - child: Text('${availableBalanceLabel}', - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1)), - ), - if (hasAdditionalBalance) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ), - ], - ), - ), - SizedBox(height: 6), - AutoSizeText(availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .balanceAmountColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.start), - SizedBox(height: 6), - if (isTestnet) - Text(S.of(context).testnet_coins_no_value, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1)), - if (!isTestnet) - Text('${availableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.textColor, - height: 1)), - - ], - ), - - SizedBox( - width: min(MediaQuery.of(context).size.width * 0.2, 100), - child: Center( - child: Column( - children: [ - CakeImageWidget( - imageUrl: currency.iconPath, - height: 40, - width: 40, - displayOnError: Container( - height: 30.0, - width: 30.0, - child: Center( - child: Text( - currency.title.substring(0, min(currency.title.length, 2)), - style: TextStyle(fontSize: 11), - ), - ), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.grey.shade400, - ), - ), - ), - const SizedBox(height: 10), - Text( - currency.title, - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: - Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ), - ], - ), - ), - if (frozenBalance.isNotEmpty) - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: hasAdditionalBalance - ? () => _showBalanceDescription( - context, S.of(context).unavailable_balance_description) - : null, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 26), - Row( - children: [ - Text( - S.of(context).unavailable_balance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ), - ], - ), - SizedBox(height: 8), - AutoSizeText( - frozenBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.balanceAmountColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - frozenFiatBalance, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ), - if (hasAdditionalBalance) - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, - height: 1, - ), - ), - ], - ), - ), - ], - ), - ), - ), - if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[ - SizedBox(height: 10), - Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: Theme.of(context).extension()!.cardBorderColor, - width: 1, - ), - color: Theme.of(context).extension()!.syncedBackgroundColor, - ), - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), - child: Stack( - children: [ - if (currency == CryptoCurrency.ltc) - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container( - padding: EdgeInsets.only(right: 16, top: 0), - child: Column( - children: [ - Container( - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Theme.of(context) - .extension()! - .assetTitleColor, - size: 40, - ), - ), - const SizedBox(height: 10), - Text( - 'MWEB', - style: TextStyle( - fontSize: 15, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - ), - ], - ), - ), - ], - ), - if (hasSecondAvailableBalance) - GestureDetector( - onTap: () => dashboardViewModel.balanceViewModel.switchBalanceValue(), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => launchUrl( - Uri.parse( - "https://docs.cakewallet.com/cryptos/litecoin.html#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Row( - children: [ - Text( - '${secondAvailableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .extension()! - .labelTextColor), - ) - ], - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAvailableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 6), - if (!isTestnet) - Text( - '${secondAvailableFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .textColor, - height: 1, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - Container( - margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16), - child: Stack( - children: [ - if (hasSecondAdditionalBalance) - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 24), - Text( - '${secondAdditionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, - height: 1, - ), - ), - SizedBox(height: 8), - AutoSizeText( - secondAdditionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .assetTitleColor, - height: 1, - ), - maxLines: 1, - textAlign: TextAlign.center, - ), - SizedBox(height: 4), - if (!isTestnet) - Text( - '${secondAdditionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .textColor, - height: 1, - ), - ), - ], - ), - ], - ), - ], - ), - ), - IntrinsicHeight( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Semantics( - label: S.of(context).litecoin_mweb_pegin, - child: OutlinedButton( - onPressed: () { - final mwebAddress = - bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((mwebAddress?.isNotEmpty ?? false)) { - paymentRequest = - PaymentRequest.fromUri(Uri.parse("litecoin:${mwebAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.nonMweb, - }, - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400.withAlpha(50), - side: - BorderSide(color: Colors.grey.shade400.withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - height: 30, - width: 30, - 'assets/images/received.png', - color: Theme.of(context) - .extension()! - .balanceAmountColor, - ), - const SizedBox(width: 8), - Text( - S.of(context).litecoin_mweb_pegin, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - SizedBox(width: 24), - Expanded( - child: Semantics( - label: S.of(context).litecoin_mweb_pegout, - child: OutlinedButton( - onPressed: () { - final litecoinAddress = - bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet); - PaymentRequest? paymentRequest = null; - if ((litecoinAddress?.isNotEmpty ?? false)) { - paymentRequest = PaymentRequest.fromUri( - Uri.parse("litecoin:${litecoinAddress}")); - } - Navigator.pushNamed( - context, - Routes.send, - arguments: { - 'paymentRequest': paymentRequest, - 'coinTypeToSpendFrom': UnspentCoinType.mweb, - }, - ); - }, - style: OutlinedButton.styleFrom( - backgroundColor: Colors.grey.shade400.withAlpha(50), - side: - BorderSide(color: Colors.grey.shade400.withAlpha(50), width: 0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - child: Container( - padding: EdgeInsets.symmetric(vertical: 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - height: 30, - width: 30, - 'assets/images/upload.png', - color: Theme.of(context) - .extension()! - .balanceAmountColor, - ), - const SizedBox(width: 8), - Text( - S.of(context).litecoin_mweb_pegout, - style: TextStyle( - color: Theme.of(context) - .extension()! - .textColor, - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ), - SizedBox(height: 16), - ], - ), - ), - ), - ], - ]); - } - - void _showBalanceDescription(BuildContext context, String content) { - showPopUp(context: context, builder: (_) => InformationPage(information: content)); - } -} diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index a12552712f..7207514ab8 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -66,6 +66,7 @@ class RescanPage extends BasePage { ); } + // TODO: common with crypto balance widget.dart Future _toggleSilentPaymentsScanning(BuildContext context) async { final height = _blockchainHeightWidgetKey.currentState!.height; From c21d163830e8a9de347d7e7811892b89bbe32399 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 7 Jan 2025 19:26:48 -0300 Subject: [PATCH 38/64] refactor: reviewing [skip ci] --- cw_bitcoin/lib/bitcoin_address_record.dart | 92 +++++-- cw_bitcoin/lib/bitcoin_wallet.dart | 11 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 79 +++--- cw_bitcoin/lib/electrum_wallet.dart | 45 ++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 243 ++++-------------- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 4 +- cw_bitcoin/lib/litecoin_wallet.dart | 128 ++------- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 105 ++++++-- .../lib/src/bitcoin_cash_wallet.dart | 8 +- lib/bitcoin/cw_bitcoin.dart | 15 +- tool/configure.dart | 1 + 11 files changed, 303 insertions(+), 428 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index a659354787..3034e190de 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -13,7 +13,7 @@ abstract class BaseBitcoinAddressRecord { int balance = 0, String name = '', bool isUsed = false, - required this.addressType, + required this.type, bool? isHidden, }) : _txCount = txCount, _balance = balance, @@ -57,27 +57,27 @@ abstract class BaseBitcoinAddressRecord { int get hashCode => address.hashCode; - BitcoinAddressType addressType; + BitcoinAddressType type; String toJSON(); } class BitcoinAddressRecord extends BaseBitcoinAddressRecord { final BitcoinDerivationInfo derivationInfo; - final CWBitcoinDerivationType derivationType; + final CWBitcoinDerivationType cwDerivationType; BitcoinAddressRecord( super.address, { required super.index, required this.derivationInfo, - required this.derivationType, + required this.cwDerivationType, super.isHidden, super.isChange = false, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, - required super.addressType, + required super.type, String? scriptHash, BasedUtxoNetwork? network, }) { @@ -99,14 +99,14 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { derivationInfo: BitcoinDerivationInfo.fromJSON( decoded['derivationInfo'] as Map, ), - derivationType: CWBitcoinDerivationType.values[decoded['derivationType'] as int], + cwDerivationType: 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, - addressType: decoded['type'] != null && decoded['type'] != '' + type: decoded['type'] != null && decoded['type'] != '' ? BitcoinAddressType.values .firstWhere((type) => type.toString() == decoded['type'] as String) : SegwitAddresType.p2wpkh, @@ -121,19 +121,19 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { 'address': address, 'index': index, 'derivationInfo': derivationInfo.toJSON(), - 'derivationType': derivationType.index, + 'derivationType': cwDerivationType.index, 'isHidden': isHidden, 'isChange': isChange, 'isUsed': isUsed, 'txCount': txCount, 'name': name, 'balance': balance, - 'type': addressType.toString(), + 'type': type.toString(), 'scriptHash': scriptHash, }); @override - operator ==(Object other) { + bool operator ==(Object other) { if (identical(this, other)) return true; return other is BitcoinAddressRecord && @@ -141,8 +141,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { other.index == index && other.derivationInfo == derivationInfo && other.scriptHash == scriptHash && - other.addressType == addressType && - other.derivationType == derivationType; + other.type == type && + other.cwDerivationType == cwDerivationType; } @override @@ -151,8 +151,8 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { index.hashCode ^ derivationInfo.hashCode ^ scriptHash.hashCode ^ - addressType.hashCode ^ - derivationType.hashCode; + type.hashCode ^ + cwDerivationType.hashCode; } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { @@ -170,7 +170,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { super.balance = 0, super.name = '', super.isUsed = false, - super.addressType = SilentPaymentsAddresType.p2sp, + super.type = SilentPaymentsAddresType.p2sp, super.isHidden, this.labelHex, }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) { @@ -204,7 +204,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { 'txCount': txCount, 'name': name, 'balance': balance, - 'type': addressType.toString(), + 'type': type.toString(), 'labelHex': labelHex, }); } @@ -220,7 +220,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.name = '', super.isUsed = false, required this.tweak, - super.addressType = SegwitAddresType.p2tr, + super.type = SegwitAddresType.p2tr, super.labelHex, }) : super(isHidden: true); @@ -268,8 +268,64 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { 'txCount': txCount, 'name': name, 'balance': balance, - 'type': addressType.toString(), + 'type': type.toString(), 'labelHex': labelHex, 'tweak': tweak, }); } + +class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord { + LitecoinMWEBAddressRecord( + super.address, { + required super.index, + super.isHidden, + super.isChange = false, + super.txCount = 0, + super.balance = 0, + super.name = '', + super.isUsed = false, + BasedUtxoNetwork? network, + super.type = SegwitAddresType.mweb, + }); + + factory LitecoinMWEBAddressRecord.fromJSON(String jsonSource) { + final decoded = json.decode(jsonSource) as Map; + + return LitecoinMWEBAddressRecord( + decoded['address'] as String, + index: decoded['index'] 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, + ); + } + + @override + String toJSON() => json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isChange': isChange, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + }); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is BitcoinAddressRecord && + other.address == address && + other.index == index && + other.type == type; + } + + @override + int get hashCode => address.hashCode ^ index.hashCode ^ type.hashCode; +} diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index ad49c3b7c7..aace2e2f6a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -788,8 +788,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { transactionHistoryIds: transactionHistory.transactions.keys.toList(), labels: walletAddresses.labels, labelIndexes: walletAddresses.silentPaymentAddresses - .where((addr) => - addr.addressType == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) + .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.labelIndex >= 1) .map((addr) => addr.labelIndex) .toList(), isSingleScan: doSingleScan ?? false, @@ -836,6 +835,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // } } + @override + int get dustAmount => network == BitcoinNetwork.testnet ? 0 : 546; + @override @action Future updateTransactions([List? addresses]) async { @@ -968,7 +970,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (paysToSilentPayment) { // Check inputs for shared secret derivation - if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) { + if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { throw BitcoinTransactionSilentPaymentsNotSupported(); } } @@ -1188,7 +1190,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )); // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets - final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); + final changeDerivationPath = + (changeAddress as BitcoinAddressRecord).derivationInfo.derivationPath.toString(); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index b9744f4246..c598e47d39 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -36,7 +36,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S List get usableSilentPaymentAddresses => silentPaymentAddresses .where((addressRecord) => - addressRecord.addressType != SegwitAddresType.p2tr && + addressRecord.type != SegwitAddresType.p2tr && addressRecord.derivationPath == BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND) .toList(); @@ -48,13 +48,13 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override Future init() async { - await generateInitialAddresses(addressType: SegwitAddresType.p2wpkh); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); if (!isHardwareWallet) { - await generateInitialAddresses(addressType: P2pkhAddressType.p2pkh); - await generateInitialAddresses(addressType: P2shAddressType.p2wpkhInP2sh); - await generateInitialAddresses(addressType: SegwitAddresType.p2tr); - await generateInitialAddresses(addressType: SegwitAddresType.p2wsh); + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + await generateInitialAddresses(type: SegwitAddresType.p2tr); + await generateInitialAddresses(type: SegwitAddresType.p2wsh); } if (silentPaymentAddresses.length == 0) { @@ -90,7 +90,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S oldSilentPaymentWallet.toString(), labelIndex: 1, name: "", - addressType: SilentPaymentsAddresType.p2sp, + type: SilentPaymentsAddresType.p2sp, derivationPath: oldSpendPath.toString(), isHidden: true, ), @@ -99,7 +99,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S name: "", labelIndex: 0, labelHex: BytesUtils.toHexString(oldSilentPaymentWallet.generateLabel(0)), - addressType: SilentPaymentsAddresType.p2sp, + type: SilentPaymentsAddresType.p2sp, derivationPath: oldSpendPath.toString(), isHidden: true, ), @@ -112,14 +112,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S silentPaymentWallet.toString(), labelIndex: 1, name: "", - addressType: SilentPaymentsAddresType.p2sp, + type: SilentPaymentsAddresType.p2sp, ), BitcoinSilentPaymentAddressRecord( silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), name: "", labelIndex: 0, labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), - addressType: SilentPaymentsAddresType.p2sp, + type: SilentPaymentsAddresType.p2sp, ), ]); } @@ -253,11 +253,11 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S labelIndex: currentSPLabelIndex, name: label, labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(currentSPLabelIndex)), - addressType: SilentPaymentsAddresType.p2sp, + type: SilentPaymentsAddresType.p2sp, ); silentPaymentAddresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesByMatch()); + Future.delayed(Duration.zero, () => updateAddressesOnReceiveScreen()); return address; } @@ -265,25 +265,25 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return super.generateNewAddress(label: label); } - @override - @action - void addBitcoinAddressTypes() { - super.addBitcoinAddressTypes(); - - silentPaymentAddresses.forEach((addressRecord) { - if (addressRecord.addressType != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { - return; - } - - 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 addBitcoinAddressTypes() { + // super.addBitcoinAddressTypes(); + + // silentPaymentAddresses.forEach((addressRecord) { + // if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { + // return; + // } + + // 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 @@ -309,14 +309,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override @action - void updateAddressesByMatch() { + void updateAddressesOnReceiveScreen() { if (addressPageType == SilentPaymentsAddresType.p2sp) { - addressesByReceiveType.clear(); - addressesByReceiveType.addAll(silentPaymentAddresses); + addressesOnReceiveScreen.clear(); + addressesOnReceiveScreen.addAll(silentPaymentAddresses); return; } - super.updateAddressesByMatch(); + super.updateAddressesOnReceiveScreen(); } @action @@ -325,7 +325,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S addressesSet.addAll(addresses); this.silentPaymentAddresses.clear(); this.silentPaymentAddresses.addAll(addressesSet); - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); } @action @@ -334,17 +334,16 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S addressesSet.addAll(addresses); this.receivedSPAddresses.clear(); this.receivedSPAddresses.addAll(addressesSet); - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); } @action void deleteSilentPaymentAddress(String address) { final addressRecord = silentPaymentAddresses.firstWhere((addressRecord) => - addressRecord.addressType == SilentPaymentsAddresType.p2sp && - addressRecord.address == address); + addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address); silentPaymentAddresses.remove(addressRecord); - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); } Map get labels { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 85b36f0f72..a83877ba30 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -243,7 +243,7 @@ abstract class ElectrumWalletBase List get addressesSet => walletAddresses.allAddresses.map((addr) => addr.address).toList(); - List get scriptHashes => walletAddresses.addressesByReceiveType + List get scriptHashes => walletAddresses.addressesOnReceiveScreen .map((addr) => (addr as BitcoinAddressRecord).scriptHash) .toList(); @@ -430,9 +430,9 @@ abstract class ElectrumWalletBase } } - int get _dustAmount => 0; + int get dustAmount => 546; - bool isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; + bool isBelowDust(int amount) => amount <= dustAmount; TxCreateUtxoDetails createUTXOS({ required bool sendAll, @@ -640,7 +640,8 @@ abstract class ElectrumWalletBase isChange: true, )); - final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); + final changeDerivationPath = + (changeAddress as BitcoinAddressRecord).derivationInfo.derivationPath.toString(); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); @@ -930,7 +931,7 @@ abstract class ElectrumWalletBase required List utxos, required Map publicKeys, String? memo, - bool enableRBF = true, + bool enableRBF = false, BitcoinOrdering inputOrdering = BitcoinOrdering.bip69, BitcoinOrdering outputOrdering = BitcoinOrdering.bip69, }) async => @@ -1295,8 +1296,8 @@ abstract class ElectrumWalletBase final addressList = (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( (element) => - element.addressType == addressRecord.addressType && - element.derivationType == addressRecord.derivationType); + element.type == addressRecord.type && + element.cwDerivationType == addressRecord.cwDerivationType); final totalAddresses = addressList.length; final gapLimit = (isChange @@ -1311,28 +1312,28 @@ abstract class ElectrumWalletBase final hasUsedAddressesUnderGap = addressRecord.index >= totalAddresses - gapLimit; - if (hasUsedAddressesUnderGap && lastDiscoveredType != addressRecord.addressType) { - lastDiscoveredType = addressRecord.addressType; + if (hasUsedAddressesUnderGap && lastDiscoveredType != addressRecord.type) { + lastDiscoveredType = addressRecord.type; // 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, + derivationType: addressRecord.cwDerivationType, + addressType: addressRecord.type, derivationInfo: BitcoinAddressUtils.getDerivationFromType( - addressRecord.addressType, + addressRecord.type, isElectrum: [ CWBitcoinDerivationType.electrum, CWBitcoinDerivationType.old_electrum, - ].contains(addressRecord.derivationType), + ].contains(addressRecord.cwDerivationType), ), ); final newAddressList = (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( (element) => - element.addressType == addressRecord.addressType && - element.derivationType == addressRecord.derivationType); + element.type == addressRecord.type && + element.cwDerivationType == addressRecord.cwDerivationType); printV( "discovered ${newAddresses.length} new addresses, new total: ${newAddressList.length}"); @@ -1404,7 +1405,7 @@ abstract class ElectrumWalletBase var currentFee = allInputsAmount - totalOutAmount; int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee; - return totalBalance - receiverAmount - remainingFee >= _dustAmount; + return totalBalance - receiverAmount - remainingFee >= dustAmount; } Future replaceByFee(String hash, int newFee) async { @@ -1496,10 +1497,10 @@ abstract class ElectrumWalletBase if (isChange) { int outputAmount = output.value.toInt(); - if (outputAmount > _dustAmount) { - int deduction = (outputAmount - _dustAmount >= remainingFee) + if (outputAmount > dustAmount) { + int deduction = (outputAmount - dustAmount >= remainingFee) ? remainingFee - : outputAmount - _dustAmount; + : outputAmount - dustAmount; outputs[i] = BitcoinOutput( address: output.address, value: BigInt.from(outputAmount - deduction)); remainingFee -= deduction; @@ -1564,10 +1565,10 @@ abstract class ElectrumWalletBase final output = outputs[i]; int outputAmount = output.value.toInt(); - if (outputAmount > _dustAmount) { - int deduction = (outputAmount - _dustAmount >= remainingFee) + if (outputAmount > dustAmount) { + int deduction = (outputAmount - dustAmount >= remainingFee) ? remainingFee - : outputAmount - _dustAmount; + : outputAmount - dustAmount; outputs[i] = BitcoinOutput( address: output.address, value: BigInt.from(outputAmount - deduction)); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index fc3530a04d..63c75a91d3 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,5 +1,3 @@ -import 'dart:io' show Platform; - import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -7,7 +5,6 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; @@ -28,24 +25,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.network, required this.isHardwareWallet, List? initialAddresses, - List? initialMwebAddresses, BitcoinAddressType? initialAddressPageType, }) : _allAddresses = ObservableList.of(initialAddresses ?? []), - addressesByReceiveType = + addressesOnReceiveScreen = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of( (initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()), - // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type changeAddresses = ObservableList.of( (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh), - mwebAddresses = - ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); } static const defaultReceiveAddressesCount = 22; @@ -53,11 +46,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const gap = 20; final ObservableList _allAddresses; - final ObservableList addressesByReceiveType; + final ObservableList addressesOnReceiveScreen; final ObservableList receiveAddresses; final ObservableList changeAddresses; - // 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 Map hdWallets; @@ -83,6 +74,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _allAddresses.firstWhere((element) => element.address == address); } + @observable + BitcoinAddressType changeAddressType = SegwitAddresType.p2wpkh; + @override @computed String get address { @@ -96,10 +90,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { receiveAddress = generateNewAddress().address; } else { final previousAddressMatchesType = - previousAddressRecord != null && previousAddressRecord!.addressType == addressPageType; + previousAddressRecord != null && previousAddressRecord!.type == addressPageType; if (previousAddressMatchesType && - typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { + typeMatchingReceiveAddresses.first.address != addressesOnReceiveScreen.first.address) { receiveAddress = previousAddressRecord!.address; } else { receiveAddress = typeMatchingReceiveAddresses.first.address; @@ -132,7 +126,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { BitcoinAddressRecord? previousAddressRecord; @computed - int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + int get totalCountOfReceiveAddresses => addressesOnReceiveScreen.fold(0, (acc, addressRecord) { if (!addressRecord.isChange) { return acc + 1; } @@ -140,7 +134,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }); @computed - int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + int get totalCountOfChangeAddresses => addressesOnReceiveScreen.fold(0, (acc, addressRecord) { if (addressRecord.isChange) { return acc + 1; } @@ -149,31 +143,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { - if (walletInfo.type == WalletType.bitcoinCash) { - await generateInitialAddresses(addressType: P2pkhAddressType.p2pkh); - } else if (walletInfo.type == WalletType.litecoin) { - await generateInitialAddresses(addressType: SegwitAddresType.p2wpkh); - if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await generateInitialAddresses(addressType: SegwitAddresType.mweb); - } - } else if (walletInfo.type == WalletType.bitcoin) { - 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); - } - } - - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); await updateAddressesInBox(); } @action - BitcoinAddressRecord getChangeAddress({ + BaseBitcoinAddressRecord getChangeAddress({ List? inputs, List? outputs, bool isPegIn = false, @@ -181,15 +158,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateChangeAddresses(); final address = changeAddresses.firstWhere( - // TODO: feature to choose change type - (addressRecord) => _isUnusedChangeAddressByType(addressRecord, SegwitAddresType.p2wpkh), + (addressRecord) => _isUnusedChangeAddressByType(addressRecord, changeAddressType), ); return address; } @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { - final newAddressIndex = addressesByReceiveType.fold( + final newAddressIndex = addressesOnReceiveScreen.fold( 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType); @@ -204,13 +180,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { index: newAddressIndex, isChange: false, name: label, - addressType: addressPageType, + type: addressPageType, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), - derivationType: CWBitcoinDerivationType.bip39, + cwDerivationType: CWBitcoinDerivationType.bip39, ); _allAddresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesByMatch()); + Future.delayed(Duration.zero, () => updateAddressesOnReceiveScreen()); return address; } @@ -255,115 +231,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { derivationInfo: derivationInfo, ); - @action - void addBitcoinAddressTypes() { - final lastP2wpkh = _allAddresses - .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; - if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH' + ': ${lastP2wpkh.address}'; - } else { - addressesMap[address] = 'Active - P2WPKH' + ': $address'; - } - - final lastP2pkh = _allAddresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); - if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH' + ': ${lastP2pkh.address}'; - } else { - addressesMap[address] = 'Active - P2PKH' + ': $address'; - } - - final lastP2sh = _allAddresses.firstWhere((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, P2shAddressType.p2wpkhInP2sh)); - if (lastP2sh.address != address) { - addressesMap[lastP2sh.address] = 'P2SH' + ': ${lastP2sh.address}'; - } else { - addressesMap[address] = 'Active - P2SH' + ': $address'; - } - - final lastP2tr = _allAddresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2tr)); - if (lastP2tr.address != address) { - addressesMap[lastP2tr.address] = 'P2TR' + ': ${lastP2tr.address}'; - } else { - addressesMap[address] = 'Active - P2TR' + ': $address'; - } - - final lastP2wsh = _allAddresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wsh)); - if (lastP2wsh.address != address) { - addressesMap[lastP2wsh.address] = 'P2WSH' + ': ${lastP2wsh.address}'; - } else { - addressesMap[address] = 'Active - P2WSH' + ': $address'; - } - } - - @action - void addLitecoinAddressTypes() { - final lastP2wpkh = _allAddresses - .where((addressRecord) => - _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; - if (lastP2wpkh.address != address) { - addressesMap[lastP2wpkh.address] = 'P2WPKH' + ': ${lastP2wpkh.address}'; - } else { - addressesMap[address] = 'Active - P2WPKH' + ': $address'; - } - - final lastMweb = _allAddresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); - if (lastMweb.address != address) { - addressesMap[lastMweb.address] = 'MWEB' + ': ${lastMweb.address}'; - } else { - addressesMap[address] = 'Active - MWEB' + ': $address'; - } - } - - @action - void addBitcoinCashAddressTypes() { - final lastP2pkh = _allAddresses.firstWhere( - (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, P2pkhAddressType.p2pkh)); - if (lastP2pkh.address != address) { - addressesMap[lastP2pkh.address] = 'P2PKH' + ': $address'; - } else { - addressesMap[address] = 'Active - P2PKH' + ': $address'; - } - } - @override @action Future updateAddressesInBox() async { - try { - addressesMap.clear(); - addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address'; + addressesMap.clear(); + addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address'; - allAddressesMap.clear(); - _allAddresses.forEach((addressRecord) { - allAddressesMap[addressRecord.address] = addressRecord.name; - }); - - switch (walletInfo.type) { - case WalletType.bitcoin: - addBitcoinAddressTypes(); - break; - case WalletType.litecoin: - addLitecoinAddressTypes(); - break; - case WalletType.bitcoinCash: - addBitcoinCashAddressTypes(); - break; - default: - break; - } - - await saveAddressesInBox(); - } catch (e) { - printV("updateAddresses $e"); - } + allAddressesMap.clear(); + _allAddresses.forEach((addressRecord) { + allAddressesMap[addressRecord.address] = addressRecord.name; + }); } @action @@ -374,11 +251,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { foundAddress = addressRecord; } }); - mwebAddresses.forEach((addressRecord) { - if (addressRecord.address == address) { - foundAddress = addressRecord; - } - }); if (foundAddress != null) { foundAddress!.setNewName(label); @@ -386,26 +258,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - void updateAddressesByMatch() { - addressesByReceiveType.clear(); - addressesByReceiveType.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList()); + void updateAddressesOnReceiveScreen() { + addressesOnReceiveScreen.clear(); + addressesOnReceiveScreen.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList()); } @action void updateReceiveAddresses() { - receiveAddresses.removeRange(0, receiveAddresses.length); - final newAddresses = _allAddresses.where((addressRecord) => !addressRecord.isChange); - receiveAddresses.addAll(newAddresses); + receiveAddresses.clear(); + receiveAddresses.addAll(_allAddresses.where((addressRecord) => !addressRecord.isChange)); } @action void updateChangeAddresses() { - changeAddresses.removeRange(0, changeAddresses.length); - final newAddresses = _allAddresses.where((addressRecord) => - addressRecord.isChange && - (walletInfo.type != WalletType.bitcoin || - addressRecord.addressType == SegwitAddresType.p2wpkh)); - changeAddresses.addAll(newAddresses); + changeAddresses.clear(); + changeAddresses.addAll(_allAddresses.where((addressRecord) => addressRecord.isChange)); } @action @@ -420,7 +287,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; final startIndex = (isChange ? changeAddresses : receiveAddresses) - .where((addr) => addr.derivationType == derivationType && addr.addressType == addressType) + .where((addr) => addr.cwDerivationType == derivationType && addr.type == addressType) .length; final newAddresses = []; @@ -437,10 +304,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { index: i, isChange: isChange, isHidden: OLD_DERIVATION_TYPES.contains(derivationType), - addressType: addressType, + type: addressType, network: network, derivationInfo: derivationInfo, - derivationType: derivationType, + cwDerivationType: derivationType, ); newAddresses.add(address); } @@ -450,14 +317,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - Future generateInitialAddresses({required BitcoinAddressType addressType}) async { - if (_allAddresses.where((addr) => addr.addressType == addressType).isNotEmpty) { + Future generateInitialAddresses({required BitcoinAddressType type}) async { + if (_allAddresses.where((addr) => addr.type == type).isNotEmpty) { return; } 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) { + if (OLD_DERIVATION_TYPES.contains(derivationType) && type == SegwitAddresType.p2wpkh) { continue; } @@ -465,26 +332,26 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { derivationType == CWBitcoinDerivationType.old_electrum; final derivationInfos = walletInfo.derivations?.where( - (element) => element.scriptType == addressType.toString(), + (element) => element.scriptType == type.toString(), ); if (derivationInfos == null || derivationInfos.isEmpty) { final bitcoinDerivationInfo = BitcoinDerivationInfo( derivationType: isElectrum ? BitcoinDerivationType.electrum : BitcoinDerivationType.bip39, derivationPath: walletInfo.derivationInfo!.derivationPath!, - scriptType: addressType, + scriptType: type, ); await discoverNewAddresses( derivationType: derivationType, isChange: false, - addressType: addressType, + addressType: type, derivationInfo: bitcoinDerivationInfo, ); await discoverNewAddresses( derivationType: derivationType, isChange: true, - addressType: addressType, + addressType: type, derivationInfo: bitcoinDerivationInfo, ); continue; @@ -494,19 +361,19 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final bitcoinDerivationInfo = BitcoinDerivationInfo( derivationType: isElectrum ? BitcoinDerivationType.electrum : BitcoinDerivationType.bip39, derivationPath: derivationInfo.derivationPath!, - scriptType: addressType, + scriptType: type, ); await discoverNewAddresses( derivationType: derivationType, isChange: false, - addressType: addressType, + addressType: type, derivationInfo: bitcoinDerivationInfo, ); await discoverNewAddresses( derivationType: derivationType, isChange: true, - addressType: addressType, + addressType: type, derivationInfo: bitcoinDerivationInfo, ); } @@ -519,7 +386,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final index = _allAddresses.indexWhere((element) => element.address == address.address); _allAddresses.replaceRange(index, index + 1, [address]); - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); } @@ -528,7 +395,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void addAddresses(Iterable addresses) { this._allAddresses.addAll(addresses); - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); @@ -545,19 +412,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { .map((addressRecord) => addressRecord.address)); } - @action - void addMwebAddresses(Iterable addresses) { - final addressesSet = this.mwebAddresses.toSet(); - addressesSet.addAll(addresses); - this.mwebAddresses.clear(); - this.mwebAddresses.addAll(addressesSet); - updateAddressesByMatch(); - } - @action Future setAddressType(BitcoinAddressType type) async { _addressPageType = type; - updateAddressesByMatch(); + updateAddressesOnReceiveScreen(); walletInfo.addressPageType = addressPageType.toString(); await walletInfo.save(); } @@ -566,15 +424,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _isAddressByType(addressRecord, addressPageType); } - bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => - addr.addressType == type; + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; bool _isUnusedChangeAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { - return addr.isChange && !addr.isUsed && addr.addressType == type; + return addr.isChange && !addr.isUsed && addr.type == type; } - bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { - return !addr.isChange && !addr.isUsed && addr.addressType == type; + bool isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + return !addr.isChange && !addr.isUsed && addr.type == type; } Map toJson() { diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 5be10e8949..8a71e4c7fe 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -47,7 +47,7 @@ class ElectrumWalletSnapshot { List addresses; List silentAddresses; - List mwebAddresses; + List mwebAddresses; bool alwaysScan; ElectrumBalance balance; @@ -81,7 +81,7 @@ class ElectrumWalletSnapshot { final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; final mwebAddresses = mwebAddressTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .map((addr) => LitecoinMWEBAddressRecord.fromJSON(addr)) .toList(); final alwaysScan = data['alwaysScan'] as bool? ?? false; diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a3a8d70adc..ceb01e4dc8 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -68,7 +68,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, - List? initialMwebAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -113,9 +113,17 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }); reaction((_) => mwebSyncStatus, (status) async { if (mwebSyncStatus is FailedSyncStatus) { - await CwMweb.stop(); + // we failed to connect to mweb, check if we are connected to the litecoin node: await CwMweb.stop(); await Future.delayed(const Duration(seconds: 5)); - startSync(); + + if ((currentChainTip ?? 0) == 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(); + } } else if (mwebSyncStatus is SyncingSyncStatus) { syncStatus = mwebSyncStatus; } else if (mwebSyncStatus is SynchronizingSyncStatus) { @@ -153,7 +161,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? passphrase, String? addressPageType, List? initialAddresses, - List? initialMwebAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -877,106 +885,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { unspentCoins.addAll(mwebUnspentCoins); } - @override - @action - Future> fetchTransactions() async { - throw UnimplementedError(); - // try { - // final Map historiesWithDetails = {}; - - // await Future.wait(LITECOIN_ADDRESS_TYPES - // .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); - - // int confirmed = balance.confirmed; - // int unconfirmed = balance.unconfirmed; - int confirmedMweb = 0; - int unconfirmedMweb = 0; - try { - mwebUtxosBox.values.forEach((utxo) { - bool isConfirmed = utxo.height > 0; - - printV( - "utxo: ${isConfirmed ? "confirmed" : "unconfirmed"} ${utxo.spent ? "spent" : "unspent"} ${utxo.outputId} ${utxo.height} ${utxo.value}"); - - if (isConfirmed) { - confirmedMweb += utxo.value.toInt(); - } - - if (isConfirmed && utxo.spent) { - unconfirmedMweb -= utxo.value.toInt(); - } - - if (!isConfirmed && !utxo.spent) { - unconfirmedMweb += utxo.value.toInt(); - } - }); - } 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; - 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.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 - // @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(); @@ -1095,9 +1003,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: - return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: return true; } @@ -1105,8 +1013,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { 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.addressType == SegwitAddresType.mweb ? -1 : 1); + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? -1 : 1); for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; @@ -1295,7 +1202,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { )); // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets - final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString(); + final changeDerivationPath = + (changeAddress as BitcoinAddressRecord).derivationInfo.derivationPath.toString(); utxoDetails.publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath('', changeDerivationPath); @@ -1487,7 +1395,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputs: data.outputs, publicKeys: estimatedTx.publicKeys, fee: BigInt.from(estimatedTx.fee), - network: network, memo: estimatedTx.memo, outputOrdering: BitcoinOrdering.none, enableRBF: true, @@ -1899,7 +1806,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Future buildHardwareWalletTransaction({ required List outputs, required BigInt fee, - required BasedUtxoNetwork network, required List utxos, required Map publicKeys, String? memo, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 68ec634edc..1daa413317 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -26,14 +26,18 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required this.mwebEnabled, required super.hdWallets, super.initialAddresses, - super.initialMwebAddresses, - }) : super(walletInfo) { + List? initialMwebAddresses, + }) : mwebAddresses = + ObservableList.of((initialMwebAddresses ?? []).toSet()), + super(walletInfo) { for (int i = 0; i < mwebAddresses.length; i++) { mwebAddrs.add(mwebAddresses[i].address); } printV("initialized with ${mwebAddrs.length} mweb addresses"); } + final ObservableList mwebAddresses; + final Bip32Slip10Secp256k1? mwebHd; bool mwebEnabled; int mwebTopUpIndex = 1000; @@ -47,13 +51,13 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override Future init() async { if (!super.isHardwareWallet) await initMwebAddresses(); - await super.init(); - } - @computed - @override - List get allAddresses { - return List.from(super.allAddresses)..addAll(mwebAddresses); + await generateInitialAddresses(type: SegwitAddresType.p2wpkh); + if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { + await generateInitialAddresses(type: SegwitAddresType.mweb); + } + + await super.init(); } Future ensureMwebAddressUpToIndexExists(int index) async { @@ -94,18 +98,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with if (mwebHd == null) return; if (mwebAddresses.length < mwebAddrs.length) { - List addressRecords = mwebAddrs + List addressRecords = mwebAddrs .asMap() .entries .map( - (e) => BitcoinAddressRecord( - e.value, - index: e.key, - addressType: SegwitAddresType.mweb, - network: network, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), - derivationType: CWBitcoinDerivationType.bip39, - ), + (e) => LitecoinMWEBAddressRecord(e.value, index: e.key), ) .toList(); addMwebAddresses(addressRecords); @@ -163,7 +160,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - BitcoinAddressRecord getChangeAddress({ + BaseBitcoinAddressRecord getChangeAddress({ List? inputs, List? outputs, bool isPegIn = false, @@ -211,14 +208,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with if (mwebEnabled) { // TODO: // await ensureMwebAddressUpToIndexExists(1); - return BitcoinAddressRecord( - mwebAddrs[0], - index: 0, - addressType: SegwitAddresType.mweb, - network: network, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(SegwitAddresType.p2wpkh), - derivationType: CWBitcoinDerivationType.bip39, - ); + return LitecoinMWEBAddressRecord(mwebAddrs[0], index: 0); } return super.getChangeAddress(); @@ -228,7 +218,68 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with String get addressForExchange { // don't use mweb addresses for exchange refund address: final addresses = receiveAddresses - .where((element) => element.addressType == SegwitAddresType.p2wpkh && !element.isUsed); + .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } + + @override + Future updateAddressesInBox() async { + super.updateAddressesInBox(); + + final lastP2wpkh = allAddresses + .where( + (addressRecord) => isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastMweb = allAddresses.firstWhere( + (addressRecord) => isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + if (lastMweb.address != address) { + addressesMap[lastMweb.address] = 'MWEB'; + } else { + addressesMap[address] = 'Active - MWEB'; + } + + await saveAddressesInBox(); + } + + @override + @action + void updateAddress(String address, String label) { + BaseBitcoinAddressRecord? foundAddress; + allAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + + if (foundAddress != null) { + foundAddress!.setNewName(label); + + if (foundAddress is BitcoinAddressRecord) { + final index = allAddresses.indexOf(foundAddress! as BitcoinAddressRecord); + allAddresses.remove(foundAddress); + allAddresses.insert(index, foundAddress as BitcoinAddressRecord); + } + } + } + + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesOnReceiveScreen(); + } } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 8b591180ea..6e4b032a4e 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -151,20 +151,20 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { addr.address, index: addr.index, isChange: addr.isChange, - addressType: P2pkhAddressType.p2pkh, + type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), - derivationType: CWBitcoinDerivationType.bip39, + cwDerivationType: CWBitcoinDerivationType.bip39, ); } catch (_) { return BitcoinAddressRecord( AddressUtils.getCashAddrFormat(addr.address), index: addr.index, isChange: addr.isChange, - addressType: P2pkhAddressType.p2pkh, + type: P2pkhAddressType.p2pkh, network: BitcoinCashNetwork.mainnet, derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), - derivationType: CWBitcoinDerivationType.bip39, + cwDerivationType: CWBitcoinDerivationType.bip39, ); } }).toList(), diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 9ac22c6093..ca2fda0783 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -135,7 +135,7 @@ class CWBitcoin extends Bitcoin { @computed List getSubAddresses(Object wallet) { final electrumWallet = wallet as ElectrumWallet; - return electrumWallet.walletAddresses.addressesByReceiveType + return electrumWallet.walletAddresses.addressesOnReceiveScreen .map( (addr) => ElectrumSubAddress( id: addr.index, @@ -216,9 +216,9 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.unspentCoins.where((element) { switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return element.bitcoinAddressRecord.addressType == SegwitAddresType.mweb; + return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; case UnspentCoinType.nonMweb: - return element.bitcoinAddressRecord.addressType != SegwitAddresType.mweb; + return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; case UnspentCoinType.any: return true; } @@ -802,8 +802,9 @@ class CWBitcoin extends Bitcoin { String? getUnusedMwebAddress(Object wallet) { try { final electrumWallet = wallet as ElectrumWallet; - final mwebAddress = - electrumWallet.walletAddresses.mwebAddresses.firstWhere((element) => !element.isUsed); + final mwebAddress = (electrumWallet.walletAddresses as LitecoinWalletAddresses) + .mwebAddresses + .firstWhere((element) => !element.isUsed); return mwebAddress.address; } catch (_) { return null; @@ -813,8 +814,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.addressType == SegwitAddresType.p2wpkh); + final segwitAddress = electrumWallet.walletAddresses.allAddresses + .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); return segwitAddress.address; } catch (_) { return null; diff --git a/tool/configure.dart b/tool/configure.dart index f300a4a834..9547941d98 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -107,6 +107,7 @@ 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/litecoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; From ad607e002ad07756bca41809c0b715301ca7e6ff Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 8 Jan 2025 09:41:21 -0300 Subject: [PATCH 39/64] refactor: reviewing [skip ci] --- cw_bitcoin/lib/bitcoin_wallet.dart | 244 ++++++++---------- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 2 +- cw_bitcoin/lib/electrum_transaction_info.dart | 11 +- cw_bitcoin/lib/electrum_wallet.dart | 4 +- .../lib/electrum_worker/electrum_worker.dart | 9 +- .../lib/electrum_worker/methods/methods.dart | 1 + .../methods/tweaks_subscribe.dart | 30 ++- cw_bitcoin/lib/litecoin_wallet.dart | 150 +++++------ lib/bitcoin/cw_bitcoin.dart | 40 --- lib/view_model/wallet_restore_view_model.dart | 17 +- tool/configure.dart | 2 - 11 files changed, 213 insertions(+), 297 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index aace2e2f6a..2972d468fd 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -116,60 +116,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { int initialSilentAddressIndex = 0, required bool mempoolAPIEnabled, }) async { - List? seedBytes = null; - final Map hdWallets = {}; - - 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) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = - Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("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; - } - } + final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); return BitcoinWallet( mnemonic: mnemonic, @@ -182,14 +129,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - seedBytes: seedBytes, - hdWallets: hdWallets, + seedBytes: walletSeedBytes.seedBytes, + hdWallets: walletSeedBytes.hdWallets, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, mempoolAPIEnabled: mempoolAPIEnabled, - initialUnspentCoins: [], ); } @@ -251,59 +197,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final passphrase = keysData.passphrase; if (mnemonic != null) { - final derivations = walletInfo.derivations ?? []; - - for (final derivation in 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) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = - Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("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]!; - } - - if (derivations.isEmpty) { - 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; - } - } + final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); + seedBytes = walletSeedBytes.seedBytes; + hdWallets.addAll(walletSeedBytes.hdWallets); } return BitcoinWallet( @@ -326,7 +222,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { alwaysScan: alwaysScan, mempoolAPIEnabled: mempoolAPIEnabled, hdWallets: hdWallets, - initialUnspentCoins: snp?.unspentCoins ?? [], + initialUnspentCoins: snp?.unspentCoins, ); } @@ -492,13 +388,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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(); @@ -686,9 +575,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { for (final map in result.transactions!.entries) { final txid = map.key; - final tx = map.value; + final data = map.value; + final tx = data.txInfo; + final unspents = data.unspents; - if (tx.unspents != null) { + if (unspents.isNotEmpty) { final existingTxInfo = transactionHistory.transactions[txid]; final txAlreadyExisted = existingTxInfo != null; @@ -698,19 +589,22 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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)) + final newUnspents = unspents + .where( + (unspent) => !unspentCoins.any((element) => + element.hash.contains(unspent.hash) && + element.vout == unspent.vout && + element.value == unspent.value), + ) .toList(); if (newUnspents.isNotEmpty) { newUnspents.forEach(_updateSilentAddressRecord); - existingTxInfo.unspents ??= []; - existingTxInfo.unspents!.addAll(newUnspents); + unspentCoins.addAll(newUnspents); + unspentCoins.forEach(updateCoin); + + await refreshUnspentCoinsInfo(); final newAmount = newUnspents.length > 1 ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) @@ -727,7 +621,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } else { // else: First time seeing this TX after scanning - tx.unspents!.forEach(_updateSilentAddressRecord); + unspentCoins.forEach(_updateSilentAddressRecord); transactionHistory.addOne(tx); balance[currency]!.confirmed += tx.amount; @@ -843,15 +737,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { 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; - } - }); + // 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 @@ -1467,8 +1361,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { transactionHistory.addOne(transaction); if (estimatedTx.spendsSilentPayment) { transactionHistory.transactions.values.forEach((tx) { - tx.unspents?.removeWhere( - (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); + // tx.unspents?.removeWhere( + // (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); transactionHistory.addOne(tx); }); } @@ -1483,3 +1377,73 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } } + +class WalletSeedBytes { + final List seedBytes; + final Map hdWallets; + + WalletSeedBytes({required this.seedBytes, required this.hdWallets}); + + static Future getSeedBytes( + WalletInfo walletInfo, + String mnemonic, [ + String? passphrase, + ]) async { + List? seedBytes = null; + final Map hdWallets = {}; + + if (walletInfo.isRecovery) { + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { + try { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("bip39 seed error: $e"); + } + + continue; + } + + if (derivation.derivationType == DerivationType.electrum) { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = + Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v1 seed error: $e"); + } + } + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } + } + + 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 WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); + } +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index c598e47d39..ac7c3669b3 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -12,9 +12,9 @@ class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAd abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { + required super.hdWallets, required super.network, required super.isHardwareWallet, - required super.hdWallets, super.initialAddresses, List? initialSilentAddresses, List? initialReceivedSPAddresses, diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 9c1fcd58b5..9d267759ef 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -2,11 +2,9 @@ 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_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/format_amount.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hex/hex.dart'; @@ -46,7 +44,6 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { - List? unspents; bool isReceivedSilentPayment; int? time; @@ -66,7 +63,6 @@ class ElectrumTransactionInfo extends TransactionInfo { bool? isDateValidated, required int confirmations, String? to, - this.unspents, this.isReceivedSilentPayment = false, Map? additionalInfo, }) { @@ -236,7 +232,6 @@ class ElectrumTransactionInfo extends TransactionInfo { factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { final inputAddresses = data['inputAddresses'] as List? ?? []; final outputAddresses = data['outputAddresses'] as List? ?? []; - final unspents = data['unspents'] as List? ?? []; return ElectrumTransactionInfo( type, @@ -254,9 +249,6 @@ class ElectrumTransactionInfo extends TransactionInfo { outputAddresses: outputAddresses.isEmpty ? [] : outputAddresses.map((e) => e.toString()).toList(), to: data['to'] as String?, - unspents: unspents - .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?, @@ -315,7 +307,6 @@ class ElectrumTransactionInfo extends TransactionInfo { m['confirmations'] = confirmations; m['fee'] = fee; m['to'] = to; - m['unspents'] = unspents?.map((e) => e.toJson()).toList() ?? []; m['inputAddresses'] = inputAddresses; m['outputAddresses'] = outputAddresses; m['isReceivedSilentPayment'] = isReceivedSilentPayment; @@ -325,6 +316,6 @@ class ElectrumTransactionInfo extends TransactionInfo { } String toString() { - return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses, additionalInfo: $additionalInfo)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index a83877ba30..31bff6cf22 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -71,7 +71,7 @@ abstract class ElectrumWalletBase CryptoCurrency? currency, this.alwaysScan, required this.mempoolAPIEnabled, - List initialUnspentCoins = const [], + List? initialUnspentCoins, }) : hdWallets = hdWallets ?? { CWBitcoinDerivationType.bip39: getAccountHDWallet( @@ -85,7 +85,7 @@ abstract class ElectrumWalletBase syncStatus = NotConnectedSyncStatus(), _password = password, isEnabledAutoGenerateSubaddress = true, - unspentCoins = BitcoinUnspentCoins.of(initialUnspentCoins), + unspentCoins = BitcoinUnspentCoins.of(initialUnspentCoins ?? []), scripthashesListening = [], balance = ObservableMap.of(currency != null ? { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index ba4b482aae..96b287c3d6 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -682,10 +682,11 @@ class ElectrumWorker { (await getTxDate(txid, scanData.network)).time! * 1000, ), confirmations: scanData.chainTip - tweakHeight + 1, - unspents: [], isReceivedSilentPayment: true, ); + List unspents = []; + addToWallet.forEach((BSpend, result) { result.forEach((label, value) { (value as Map).forEach((output, tweak) { @@ -710,14 +711,16 @@ class ElectrumWorker { final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); - txInfo.unspents!.add(unspent); + unspents.add(unspent); txInfo.amount += unspent.value; }); }); }); _sendResponse(ElectrumWorkerTweaksSubscribeResponse( - result: TweaksSyncResponse(transactions: {txInfo.id: txInfo}), + result: TweaksSyncResponse( + transactions: {txInfo.id: TweakResponseData(txInfo: txInfo, unspents: unspents)}, + ), )); return; diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index 8f23d1d6a3..c117c45d39 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -8,6 +8,7 @@ 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'; +import 'package:cw_bitcoin/bitcoin_unspent.dart'; part 'connection.dart'; part 'headers_subscribe.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart index d84dce6670..d74fa0bd63 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -105,10 +105,36 @@ class ElectrumWorkerTweaksSubscribeError extends ElectrumWorkerErrorResponse { final String method = ElectrumRequestMethods.tweaksSubscribe.method; } +class TweakResponseData { + final ElectrumTransactionInfo txInfo; + final List unspents; + + TweakResponseData({required this.txInfo, required this.unspents}); + + Map toJson() { + return { + 'txInfo': txInfo.toJson(), + 'unspent': unspents.map((e) => e.toJson()).toList(), + }; + } + + static TweakResponseData fromJson(Map json) { + return TweakResponseData( + txInfo: ElectrumTransactionInfo.fromJson( + json['txInfo'] as Map, + WalletType.bitcoin, + ), + unspents: (json['unspent'] as List) + .map((e) => BitcoinUnspent.fromJSON(null, e as Map)) + .toList(), + ); + } +} + class TweaksSyncResponse { int? height; SyncStatus? syncStatus; - Map? transactions = {}; + Map? transactions = {}; TweaksSyncResponse({this.height, this.syncStatus, this.transactions}); @@ -131,7 +157,7 @@ class TweaksSyncResponse { : (json['transactions'] as Map).map( (key, value) => MapEntry( key, - ElectrumTransactionInfo.fromJson(value as Map, WalletType.bitcoin), + TweakResponseData.fromJson(value as Map), ), ), ); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index ceb01e4dc8..628c8b8f8a 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -8,7 +8,6 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_bitcoin/exceptions.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/unspent_coin_type.dart'; @@ -16,12 +15,10 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/node.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; @@ -167,60 +164,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, required bool mempoolAPIEnabled, }) async { - List? seedBytes = null; - final Map hdWallets = {}; - - 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) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = - Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("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; - } - } + final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); return LitecoinWallet( mnemonic: mnemonic, @@ -232,7 +176,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, - seedBytes: seedBytes, + seedBytes: walletSeedBytes.seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -290,18 +234,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final passphrase = keysData.passphrase; if (mnemonic != null) { - 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; - } + final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); + seedBytes = walletSeedBytes.seedBytes; + // hdWallets.addAll(walletSeedBytes.hdWallets); } return LitecoinWallet( @@ -434,19 +369,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 && - tx.unspents != null) { - for (var coin in tx.unspents!) { - final utxo = mwebUtxosBox.get(coin.address); - if (utxo != null) { - printV("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - await mwebUtxosBox.delete(coin.address); - } - } - } - tx.confirmations = confirmations; tx.isPending = false; transactionHistory.addOne(tx); @@ -1850,3 +1772,65 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return BtcTransaction.fromRaw(rawHex); } } + +class WalletSeedBytes { + final List seedBytes; + final Map hdWallets; + + WalletSeedBytes({required this.seedBytes, required this.hdWallets}); + + static Future getSeedBytes( + WalletInfo walletInfo, + String mnemonic, [ + String? passphrase, + ]) async { + List? seedBytes = null; + final Map hdWallets = {}; + + if (walletInfo.isRecovery) { + for (final derivation in walletInfo.derivations ?? []) { + if (derivation.derivationType == DerivationType.bip39) { + try { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("bip39 seed error: $e"); + } + + continue; + } + + if (derivation.derivationType == DerivationType.electrum) { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = + Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v1 seed error: $e"); + } + } + } + } + } + + 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 WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); + } +} diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index ca2fda0783..0716ee9944 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -352,30 +352,6 @@ class CWBitcoin extends Bitcoin { return count; } - @override - 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, - ), - ); - } - - return oldList; - } - @override Future> getDerivationInfosFromMnemonic({ required String mnemonic, @@ -592,22 +568,6 @@ 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 walletAddresses = (wallet as BitcoinWallet).walletAddresses as BitcoinWalletAddresses; diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index c8c3f59812..ba8b359450 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -257,22 +257,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: - String? mnemonic = credentials['seed'] as String?; - String? passphrase = credentials['passphrase'] as String?; - final list = await bitcoin!.getDerivationInfosFromMnemonic( - mnemonic: mnemonic!, + return await bitcoin!.getDerivationInfosFromMnemonic( + mnemonic: credentials['seed'] as String, node: node, - passphrase: passphrase, + passphrase: credentials['passphrase'] as String?, ); - - // is restoring? = add old used derivations - final oldList = bitcoin!.getOldDerivationInfos(list); - - if (walletType == WalletType.bitcoin) { - oldList.addAll(bitcoin!.getOldSPDerivationInfos()); - } - - return oldList; case WalletType.nano: String? mnemonic = credentials['seed'] as String?; String? seedKey = credentials['private_key'] as String?; diff --git a/tool/configure.dart b/tool/configure.dart index 9547941d98..9627bcd864 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -198,7 +198,6 @@ 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, @@ -221,7 +220,6 @@ abstract class Bitcoin { TransactionPriority getLitecoinTransactionPrioritySlow(); Future> compareDerivationMethods( {required String mnemonic, required Node node}); - List getOldDerivationInfos(List list); Future> getDerivationInfosFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); Map> getElectrumDerivations(); From de773b3bb7f0ddca0e90772fdd03e0268b59af18 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 8 Jan 2025 11:33:07 -0300 Subject: [PATCH 40/64] refactor: reduce unneeded [skip ci] --- cw_bitcoin/lib/bitcoin_wallet.dart | 6 +- cw_bitcoin/lib/electrum_wallet.dart | 20 ++- cw_bitcoin/lib/electrum_wallet_addresses.dart | 4 +- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 11 +- .../src/bitcoin_cash_wallet_addresses.dart | 2 +- cw_core/lib/get_height_by_date.dart | 1 + cw_core/lib/wallet_base.dart | 2 +- cw_core/lib/wallet_info.dart | 2 - cw_evm/lib/evm_chain_wallet.dart | 2 +- cw_haven/lib/haven_wallet.dart | 2 +- cw_monero/lib/monero_wallet.dart | 163 +++++++++++------- cw_nano/lib/nano_wallet.dart | 3 +- cw_solana/lib/solana_wallet.dart | 2 +- cw_tron/lib/tron_wallet.dart | 2 +- cw_wownero/lib/wownero_wallet.dart | 10 +- lib/bitcoin/cw_bitcoin.dart | 12 +- lib/src/screens/send/widgets/send_card.dart | 28 +-- lib/view_model/send/output.dart | 12 +- tool/configure.dart | 4 +- 19 files changed, 163 insertions(+), 125 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 2972d468fd..5e51138e27 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1015,7 +1015,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - EstimatedTxResult estimateTxForAmount( + Future estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { @@ -1025,7 +1025,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { bool? useUnconfirmed, bool hasSilentPayment = false, bool isFakeTx = false, - }) { + }) async { if (updatedOutputs == null) { updatedOutputs = outputs.map((output) => output).toList(); } @@ -1067,7 +1067,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = walletAddresses.getChangeAddress( + final changeAddress = await walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, outputs: updatedOutputs, ); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 31bff6cf22..27b16abbb2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -586,7 +586,7 @@ abstract class ElectrumWalletBase ); } - EstimatedTxResult estimateTxForAmount( + Future estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { @@ -594,7 +594,7 @@ abstract class ElectrumWalletBase String? memo, bool? useUnconfirmed, bool isFakeTx = false, - }) { + }) async { // Attempting to send less than the dust limit if (!isFakeTx && isBelowDust(credentialsAmount)) { throw BitcoinTransactionNoDustException(); @@ -629,7 +629,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = walletAddresses.getChangeAddress( + final changeAddress = await walletAddresses.getChangeAddress( inputs: utxoDetails.availableInputs, outputs: outputs, ); @@ -997,12 +997,12 @@ abstract class ElectrumWalletBase ); @override - int estimatedFeeForOutputsWithPriority({ - required TransactionPriority priority, + Future calculateEstimatedFee( + TransactionPriority priority, { List outputAddresses = const [], String? memo, bool enableRBF = true, - }) { + }) async { return estimatedFeeForOutputsWithFeeRate( feeRate: feeRate(priority), outputAddresses: outputAddresses, @@ -1011,12 +1011,14 @@ abstract class ElectrumWalletBase ); } - int estimatedFeeForOutputsWithFeeRate({ + // Estimates the fee for paying to the given outputs + // using the wallet's available unspent coins as inputs + Future estimatedFeeForOutputsWithFeeRate({ required int feeRate, required List outputAddresses, String? memo, bool enableRBF = true, - }) { + }) async { final fakePublicKey = ECPrivate.random().getPublic(); final fakeOutputs = []; final outputTypes = @@ -1050,7 +1052,7 @@ abstract class ElectrumWalletBase fakeOutputs.add(BitcoinOutput(address: address, value: BigInt.from(0))); } - final estimatedFakeTx = estimateTxForAmount( + final estimatedFakeTx = await estimateTxForAmount( 0, fakeOutputs, feeRate, diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 63c75a91d3..946eb94e27 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -150,11 +150,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - BaseBitcoinAddressRecord getChangeAddress({ + Future getChangeAddress({ List? inputs, List? outputs, bool isPegIn = false, - }) { + }) async { updateChangeAddresses(); final address = changeAddresses.firstWhere( diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 1daa413317..0deeb7e39c 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -20,11 +20,11 @@ class LitecoinWalletAddresses = LitecoinWalletAddressesBase with _$LitecoinWalle abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Store { LitecoinWalletAddressesBase( WalletInfo walletInfo, { + required super.hdWallets, required super.network, required super.isHardwareWallet, required this.mwebHd, required this.mwebEnabled, - required super.hdWallets, super.initialAddresses, List? initialMwebAddresses, }) : mwebAddresses = @@ -126,7 +126,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required BitcoinDerivationInfo derivationInfo, }) { if (addressType == SegwitAddresType.mweb) { - return MwebAddress.fromAddress(address: mwebAddrs[0], network: network); + return MwebAddress.fromAddress(address: mwebAddrs[isChange ? index + 1 : 0]); } return P2wpkhAddress.fromDerivation( @@ -160,11 +160,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @action @override - BaseBitcoinAddressRecord getChangeAddress({ + 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) { @@ -206,8 +206,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with } if (mwebEnabled) { - // TODO: - // await ensureMwebAddressUpToIndexExists(1); + await ensureMwebAddressUpToIndexExists(1); return LitecoinMWEBAddressRecord(mwebAddrs[0], index: 0); } 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 5526f96cec..95ef2b5662 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -10,9 +10,9 @@ class BitcoinCashWalletAddresses = BitcoinCashWalletAddressesBase with _$Bitcoin abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinCashWalletAddressesBase( WalletInfo walletInfo, { + required super.hdWallets, required super.network, required super.isHardwareWallet, - required super.hdWallets, super.initialAddresses, super.initialAddressPageType, }) : super(walletInfo); diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index e17b744816..981d61f8c4 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -246,6 +246,7 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2025-01": 877270, "2024-12": 872708, "2024-11": 868345, "2024-10": 863584, diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index bebf268fea..6b12af4722 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -74,7 +74,7 @@ abstract class WalletBase createTransaction(Object credentials); - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}); + Future calculateEstimatedFee(TransactionPriority priority); // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 96e0e94dab..ab674d9b42 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -19,8 +19,6 @@ enum DerivationType { bip39, @HiveField(4) electrum, - @HiveField(5) - old, } @HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID) diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index e1295f1b69..126bd07311 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -187,7 +187,7 @@ abstract class EVMChainWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { + Future calculateEstimatedFee(TransactionPriority priority) async { { try { if (priority is EVMChainTransactionPriority) { diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index b12b5304a2..2c065e9736 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -219,7 +219,7 @@ abstract class HavenWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { + Future calculateEstimatedFee(TransactionPriority priority) async { // FIXME: hardcoded value; if (priority is MoneroTransactionPriority) { diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index be0da3c954..56643f8a77 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -48,8 +48,8 @@ const MIN_RESTORE_HEIGHT = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase - extends WalletBase with Store { +abstract class MoneroWalletBase extends WalletBase with Store { MoneroWalletBase( {required WalletInfo walletInfo, required Box unspentCoinsInfo, @@ -71,13 +71,16 @@ abstract class MoneroWalletBase transactionHistory = MoneroTransactionHistory(); walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { + _onAccountChangeReaction = + reaction((_) => walletAddresses.account, (Account? account) { if (account == null) return; - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: account.id)) }); _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); _askForUpdateTransactionHistory(); @@ -127,7 +130,8 @@ abstract class MoneroWalletBase publicSpendKey: monero_wallet.getPublicSpendKey(), publicViewKey: monero_wallet.getPublicViewKey()); - int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height; + int? get restoreHeight => + transactionHistory.transactions.values.firstOrNull?.height; monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -140,11 +144,13 @@ abstract class MoneroWalletBase Future init() async { await walletAddresses.init(); - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + fullBalance: monero_wallet.getFullBalance( + accountIndex: walletAddresses.account!.id), + unlockedBalance: monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id)) }); _setListeners(); await updateTransactions(); @@ -153,14 +159,15 @@ abstract class MoneroWalletBase monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); if (monero_wallet.getCurrentHeight() <= 1) { - monero_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight); + monero_wallet.setRefreshFromBlockHeight( + height: walletInfo.restoreHeight); } } - _autoSaveTimer = - Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + _autoSaveTimer = Timer.periodic( + Duration(seconds: _autoSaveInterval), (_) async => await save()); // update transaction details after restore - walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id ?? 0); + walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0); } @override @@ -260,9 +267,9 @@ abstract class MoneroWalletBase bool needExportOutputs(int amount) { // viewOnlyBalance - balance that we can spend // TODO(mrcyjanek): remove hasUnknownKeyImages when we cleanup coin control - return (monero.Wallet_viewOnlyBalance(wptr!, accountIndex: walletAddresses.account!.id) < - amount) || - monero.Wallet_hasUnknownKeyImages(wptr!); + return (monero.Wallet_viewOnlyBalance(wptr!, + accountIndex: walletAddresses.account!.id) < amount) || + monero.Wallet_hasUnknownKeyImages(wptr!); } @override @@ -271,8 +278,8 @@ abstract class MoneroWalletBase final inputs = []; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; - final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + final unlockedBalance = monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id); PendingTransactionDescription pendingTransactionDescription; @@ -291,35 +298,44 @@ abstract class MoneroWalletBase } if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); + if (outputs.any( + (item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw MoneroTransactionCreationException( + 'You do not have enough XMR to send this amount.'); } - final int totalAmount = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = outputs.fold( + 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); + throw MoneroTransactionCreationException( + 'You do not have enough XMR to send this amount.'); } - if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected'); + if (inputs.isEmpty) MoneroTransactionCreationException( + 'No inputs selected'); final moneroOutputs = outputs.map((output) { - final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; + final outputAddress = + output.isParsedAddress ? output.extractedAddress : output.address; return MoneroOutput( - address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); + address: outputAddress!, + amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); - pendingTransactionDescription = await transaction_history.createTransactionMultDest( - outputs: moneroOutputs, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id, - preferredInputs: inputs); + pendingTransactionDescription = + await transaction_history.createTransactionMultDest( + outputs: moneroOutputs, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } else { final output = outputs.first; - final address = output.isParsedAddress ? output.extractedAddress : output.address; - final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final address = + output.isParsedAddress ? output.extractedAddress : output.address; + final amount = + output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); // if ((formattedAmount != null && unlockedBalance < formattedAmount) || // (formattedAmount == null && unlockedBalance <= 0)) { @@ -329,13 +345,15 @@ abstract class MoneroWalletBase // 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); // } - if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected'); - pendingTransactionDescription = await transaction_history.createTransaction( - address: address!, - amount: amount, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id, - preferredInputs: inputs); + if (inputs.isEmpty) MoneroTransactionCreationException( + 'No inputs selected'); + pendingTransactionDescription = + await transaction_history.createTransaction( + address: address!, + amount: amount, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } // final status = monero.PendingTransaction_status(pendingTransactionDescription); @@ -344,7 +362,7 @@ abstract class MoneroWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { + Future calculateEstimatedFee(TransactionPriority priority) async { // FIXME: hardcoded value; if (priority is MoneroTransactionPriority) { @@ -402,8 +420,10 @@ abstract class MoneroWalletBase } try { // -- rename the waller folder -- - final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type)); - final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type); + final currentWalletDir = + Directory(await pathForWalletDir(name: name, type: type)); + final newWalletDirPath = + await pathForWalletDir(name: newWalletName, type: type); await currentWalletDir.rename(newWalletDirPath); // -- use new waller folder to rename files with old names still -- @@ -413,7 +433,8 @@ abstract class MoneroWalletBase final currentKeysFile = File('$renamedWalletPath.keys'); final currentAddressListFile = File('$renamedWalletPath.address.txt'); - final newWalletPath = await pathForWallet(name: newWalletName, type: type); + final newWalletPath = + await pathForWallet(name: newWalletName, type: type); if (currentCacheFile.existsSync()) { await currentCacheFile.rename(newWalletPath); @@ -433,7 +454,8 @@ abstract class MoneroWalletBase final currentKeysFile = File('$currentWalletPath.keys'); final currentAddressListFile = File('$currentWalletPath.address.txt'); - final newWalletPath = await pathForWallet(name: newWalletName, type: type); + final newWalletPath = + await pathForWallet(name: newWalletName, type: type); // Copies current wallet files into new wallet name's dir and files if (currentCacheFile.existsSync()) { @@ -452,7 +474,8 @@ abstract class MoneroWalletBase } @override - Future changePassword(String password) async => monero_wallet.setPasswordSync(password); + Future changePassword(String password) async => + monero_wallet.setPasswordSync(password); Future getNodeHeight() async => monero_wallet.getNodeHeight(); @@ -487,8 +510,7 @@ abstract class MoneroWalletBase for (var i = 0; i < coinCount; i++) { final coin = getCoin(i); final coinSpent = monero.CoinsInfo_spent(coin); - if (coinSpent == false && - monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) { + if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) { final unspent = MoneroUnspent( monero.CoinsInfo_address(coin), monero.CoinsInfo_hash(coin), @@ -561,13 +583,15 @@ abstract class MoneroWalletBase Future _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => - element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); + final currentWalletUnspentCoins = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = - unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); + final existUnspentCoins = unspentCoins + .where((coin) => element.keyImage!.contains(coin.keyImage!)); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -584,13 +608,15 @@ abstract class MoneroWalletBase } String getTransactionAddress(int accountIndex, int addressIndex) => - monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); + monero_wallet.getAddress( + accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { transaction_history.refreshTransactions(); return (await _getAllTransactionsOfAccount(walletAddresses.account?.id)) - .fold>({}, + .fold>( + {}, (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; @@ -619,12 +645,15 @@ abstract class MoneroWalletBase monero_wallet.getSubaddressLabel(accountIndex, addressIndex); Future> _getAllTransactionsOfAccount(int? accountIndex) async => - (await transaction_history.getAllTransactions()) + (await transaction_history + .getAllTransactions()) .map( (row) => MoneroTransactionInfo( row.hash, row.blockheight, - row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming, + row.isSpend + ? TransactionDirection.outgoing + : TransactionDirection.incoming, row.timeStamp, row.isPending, row.amount, @@ -673,7 +702,8 @@ abstract class MoneroWalletBase } int _getHeightDistance(DateTime date) { - final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final distance = + DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; final daysTmp = (distance / 86400).round(); final days = daysTmp < 1 ? 1 : daysTmp; @@ -694,20 +724,24 @@ abstract class MoneroWalletBase void _askForUpdateBalance() { final unlockedBalance = _getUnlockedBalance(); - final fullBalance = monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + final fullBalance = monero_wallet.getFullBalance( + accountIndex: walletAddresses.account!.id); final frozenBalance = _getFrozenBalance(); if (balance[currency]!.fullBalance != fullBalance || balance[currency]!.unlockedBalance != unlockedBalance || balance[currency]!.frozenBalance != frozenBalance) { balance[currency] = MoneroBalance( - fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance); + fullBalance: fullBalance, + unlockedBalance: unlockedBalance, + frozenBalance: frozenBalance); } } - Future _askForUpdateTransactionHistory() async => await updateTransactions(); + Future _askForUpdateTransactionHistory() async => + await updateTransactions(); - int _getUnlockedBalance() => - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + int _getUnlockedBalance() => monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id); int _getFrozenBalance() { var frozenBalance = 0; @@ -788,7 +822,8 @@ abstract class MoneroWalletBase } void setLedgerConnection(LedgerConnection connection) { - final dummyWPtr = wptr ?? monero.WalletManager_openWallet(wmPtr, path: '', password: ''); + final dummyWPtr = wptr ?? + monero.WalletManager_openWallet(wmPtr, path: '', password: ''); enableLedgerExchange(dummyWPtr, connection); } } diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index f4c57555ec..ab79e13fbd 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -144,8 +144,7 @@ abstract class NanoWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => - 0; // always 0 :) + Future calculateEstimatedFee(TransactionPriority priority) async => 0; // always 0 :) @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index f02be11b9a..65e0f7537e 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -173,7 +173,7 @@ abstract class SolanaWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0; + Future calculateEstimatedFee(TransactionPriority priority) async => 0; @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 623c378f52..9217603415 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -211,7 +211,7 @@ abstract class TronWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0; + Future calculateEstimatedFee(TransactionPriority priority) async => 0; @override Future changePassword(String password) => throw UnimplementedError("changePassword"); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 87cd47df7f..b5ca752ec9 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -51,9 +51,7 @@ abstract class WowneroWalletBase extends WalletBase with Store { WowneroWalletBase( - {required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required String password}) + {required WalletInfo walletInfo, required Box unspentCoinsInfo, required String password}) : balance = ObservableMap.of({ CryptoCurrency.wow: WowneroBalance( fullBalance: wownero_wallet.getFullBalance(accountIndex: 0), @@ -261,7 +259,7 @@ abstract class WowneroWalletBase final int totalAmount = outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority); + final estimatedFee = await calculateEstimatedFee(_credentials.priority); if (unlockedBalance < totalAmount) { throw WowneroTransactionCreationException( 'You do not have enough WOW to send this amount.'); @@ -297,7 +295,7 @@ abstract class WowneroWalletBase 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } - final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority); + final estimatedFee = await calculateEstimatedFee(_credentials.priority); if (!spendAllCoins && ((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) || formattedAmount == null)) { @@ -316,7 +314,7 @@ abstract class WowneroWalletBase } @override - int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) { + Future calculateEstimatedFee(TransactionPriority priority) async { // FIXME: hardcoded value; if (priority is MoneroTransactionPriority) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 0716ee9944..e07c791374 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -495,16 +495,16 @@ class CWBitcoin extends Bitcoin { } @override - int estimatedFeeForOutputsWithPriority( + Future calculateEstimatedFee( Object wallet, { required TransactionPriority priority, required String outputAddress, String? memo, bool enableRBF = true, - }) { + }) async { final bitcoinWallet = wallet as ElectrumWallet; - return bitcoinWallet.estimatedFeeForOutputsWithPriority( - priority: priority, + return bitcoinWallet.calculateEstimatedFee( + priority, outputAddresses: [outputAddress], memo: memo, enableRBF: enableRBF, @@ -512,13 +512,13 @@ class CWBitcoin extends Bitcoin { } @override - int estimatedFeeForOutputWithFeeRate( + Future estimatedFeeForOutputWithFeeRate( Object wallet, { required int feeRate, required String outputAddress, String? memo, bool enableRBF = true, - }) { + }) async { final bitcoinWallet = wallet as ElectrumWallet; return bitcoinWallet.estimatedFeeForOutputsWithFeeRate( feeRate: feeRate, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0713fb8c45..a93a019e84 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -336,17 +336,23 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin()! - .textFieldHintColor, - ), + : FutureBuilder( + future: output.estimatedFeeFiatAmount, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + '${snapshot.data} ${sendViewModel.fiat.title}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .extension()! + .textFieldHintColor, + ), + ); + } + return CircularProgressIndicator(); + }, ), ), ], diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index be7328b361..0bf23aa7a5 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -128,7 +128,7 @@ abstract class OutputBase with Store { } @computed - double get estimatedFee { + Future get estimatedFee async { try { if (_wallet.type == WalletType.tron) { if (cryptoCurrencyHandler() == CryptoCurrency.trx) { @@ -150,13 +150,13 @@ abstract class OutputBase with Store { late int fee; if (transactionPriority == bitcoin!.getBitcoinTransactionPriorityCustom()) { - fee = bitcoin!.estimatedFeeForOutputWithFeeRate( + fee = await bitcoin!.estimatedFeeForOutputWithFeeRate( _wallet, feeRate: _settingsStore.customBitcoinFeeRate, outputAddress: address, ); } else { - fee = bitcoin!.estimatedFeeForOutputsWithPriority( + fee = await bitcoin!.calculateEstimatedFee( _wallet, priority: transactionPriority, outputAddress: address, @@ -166,7 +166,7 @@ abstract class OutputBase with Store { return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } - final fee = _wallet.estimatedFeeForOutputsWithPriority(priority: transactionPriority); + final fee = await _wallet.calculateEstimatedFee(transactionPriority); if (_wallet.type == WalletType.monero) { return monero!.formatterMoneroAmountToDouble(amount: fee); @@ -195,7 +195,7 @@ abstract class OutputBase with Store { } @computed - String get estimatedFeeFiatAmount { + Future get estimatedFeeFiatAmount async { try { final currency = (isEVMCompatibleChain(_wallet.type) || _wallet.type == WalletType.solana || @@ -203,7 +203,7 @@ abstract class OutputBase with Store { ? _wallet.currency : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); + price: _fiatConversationStore.prices[currency]!, cryptoAmount: await estimatedFee); return fiat; } catch (_) { return '0.00'; diff --git a/tool/configure.dart b/tool/configure.dart index 9627bcd864..8fd9d8fed9 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -258,14 +258,14 @@ abstract class Bitcoin { String? memo, bool enableRBF = true, }); - int estimatedFeeForOutputsWithPriority( + Future calculateEstimatedFee( Object wallet, { required TransactionPriority priority, required String outputAddress, String? memo, bool enableRBF = true, }); - int estimatedFeeForOutputWithFeeRate( + Future estimatedFeeForOutputWithFeeRate( Object wallet, { required int feeRate, required String outputAddress, From a6c021ed183b0fd762aa2d71831bf4406c80d006 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 8 Jan 2025 18:06:14 -0300 Subject: [PATCH 41/64] feat: fixes & improvements around fee types [skip ci] --- .../lib/bitcoin_transaction_priority.dart | 461 ++++++++++++------ cw_bitcoin/lib/electrum_wallet.dart | 15 - cw_bitcoin/lib/electrum_wallet_addresses.dart | 8 +- .../lib/electrum_worker/electrum_worker.dart | 30 +- cw_bitcoin/lib/litecoin_wallet.dart | 16 - cw_bitcoin/lib/litecoin_wallet_addresses.dart | 63 ++- .../lib/src/bitcoin_cash_wallet.dart | 21 +- .../src/bitcoin_cash_wallet_addresses.dart | 6 + cw_core/lib/transaction_priority.dart | 43 +- .../lib/evm_chain_transaction_priority.dart | 3 +- lib/bitcoin/cw_bitcoin.dart | 38 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 9 +- lib/core/transaction_priority_label.dart | 40 ++ lib/entities/default_settings_migration.dart | 5 +- lib/entities/priority_for_wallet_type.dart | 7 +- lib/src/screens/send/widgets/send_card.dart | 26 +- .../screens/settings/other_settings_page.dart | 36 +- lib/view_model/send/output.dart | 2 +- lib/view_model/send/send_view_model.dart | 24 +- .../settings/other_settings_view_model.dart | 31 +- .../transaction_details_view_model.dart | 10 +- lib/view_model/wallet_creation_vm.dart | 6 + res/values/strings_ar.arb | 7 +- res/values/strings_bg.arb | 7 +- res/values/strings_cs.arb | 7 +- res/values/strings_de.arb | 7 +- res/values/strings_en.arb | 7 +- res/values/strings_es.arb | 7 +- res/values/strings_fr.arb | 7 +- res/values/strings_ha.arb | 7 +- res/values/strings_hi.arb | 9 +- res/values/strings_hr.arb | 7 +- res/values/strings_hy.arb | 7 +- res/values/strings_id.arb | 7 +- res/values/strings_it.arb | 7 +- res/values/strings_ja.arb | 7 +- res/values/strings_ko.arb | 7 +- res/values/strings_my.arb | 7 +- res/values/strings_nl.arb | 7 +- res/values/strings_pl.arb | 7 +- res/values/strings_pt.arb | 9 +- res/values/strings_ru.arb | 7 +- res/values/strings_th.arb | 7 +- res/values/strings_tl.arb | 7 +- res/values/strings_tr.arb | 7 +- res/values/strings_uk.arb | 7 +- res/values/strings_ur.arb | 7 +- res/values/strings_vi.arb | 7 +- res/values/strings_yo.arb | 7 +- res/values/strings_zh.arb | 7 +- tool/configure.dart | 15 +- tool/update_translation.dart | 59 +++ 52 files changed, 842 insertions(+), 332 deletions(-) create mode 100644 lib/core/transaction_priority_label.dart create mode 100644 tool/update_translation.dart diff --git a/cw_bitcoin/lib/bitcoin_transaction_priority.dart b/cw_bitcoin/lib/bitcoin_transaction_priority.dart index 660988de56..b5bc893deb 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_priority.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_priority.dart @@ -3,33 +3,46 @@ import 'package:cw_core/transaction_priority.dart'; class BitcoinAPITransactionPriority extends TransactionPriority { const BitcoinAPITransactionPriority({required super.title, required super.raw}); -// Unimportant: the lowest possible, confirms when it confirms no matter how long it takes - static const BitcoinAPITransactionPriority unimportant = - BitcoinAPITransactionPriority(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 BitcoinAPITransactionPriority normal = - BitcoinAPITransactionPriority(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 BitcoinAPITransactionPriority elevated = - BitcoinAPITransactionPriority(title: 'Elevated', raw: 2); -// Priority: high fee, expected in the next block (about 10 mins). - static const BitcoinAPITransactionPriority priority = - BitcoinAPITransactionPriority(title: 'Priority', raw: 3); -// Custom: any fee, user defined + @override + String get unit => 'sat'; + + static const List all = [ + fastest, + halfHour, + hour, + economy, + minimum, + custom + ]; + +// Minimum: the lowest fee possible to be included in the mempool, confirms whenever without an estimate but could be dropped from the mempool if fees rise more. + static const BitcoinAPITransactionPriority minimum = + BitcoinAPITransactionPriority(title: 'Minimum', raw: 0); +// Economy: in between the minimum and the low fee rates, or 2x the minimum, gives a bigger chance of not being dropped from the mempool + static const BitcoinAPITransactionPriority economy = + BitcoinAPITransactionPriority(title: 'Economy', raw: 1); + static const BitcoinAPITransactionPriority hour = + BitcoinAPITransactionPriority(title: 'Hour', raw: 2); + static const BitcoinAPITransactionPriority halfHour = + BitcoinAPITransactionPriority(title: 'HalfHour', raw: 3); + static const BitcoinAPITransactionPriority fastest = + BitcoinAPITransactionPriority(title: 'Fastest', raw: 4); static const BitcoinAPITransactionPriority custom = - BitcoinAPITransactionPriority(title: 'Custom', raw: 4); + BitcoinAPITransactionPriority(title: 'Custom', raw: 5); static BitcoinAPITransactionPriority deserialize({required int raw}) { switch (raw) { case 0: - return unimportant; + return minimum; case 1: - return normal; + return economy; case 2: - return elevated; + return hour; case 3: - return priority; + return halfHour; case 4: + return fastest; + case 5: return custom; default: throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize'); @@ -38,34 +51,95 @@ class BitcoinAPITransactionPriority extends TransactionPriority { @override String toString() { - var label = ''; - - switch (this) { - case BitcoinAPITransactionPriority.unimportant: - label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; - break; - case BitcoinAPITransactionPriority.normal: - label = 'Normal ~1hr+'; // S.current.transaction_priority_medium; - break; - case BitcoinAPITransactionPriority.elevated: - label = 'Elevated'; - break; // S.current.transaction_priority_fast; - case BitcoinAPITransactionPriority.priority: - label = 'Priority'; - break; // S.current.transaction_priority_fast; + return title; + } + + @override + TransactionPriorityLabel getLabelWithRate(int rate, int? customRate) { + final rateValue = this.title == custom.title ? customRate ??= 0 : rate; + return TransactionPriorityLabel(priority: this, rateValue: rateValue); + } +} + +class BitcoinAPITransactionPriorities + implements TransactionPriorities { + const BitcoinAPITransactionPriorities({ + required this.minimum, + required this.economy, + required this.hour, + required this.halfHour, + required this.fastest, + required this.custom, + }); + + final int minimum; + final int economy; + final int hour; + final int halfHour; + final int fastest; + final int custom; + + @override + int operator [](BitcoinAPITransactionPriority type) { + switch (type) { + case BitcoinAPITransactionPriority.minimum: + return minimum; + case BitcoinAPITransactionPriority.economy: + return economy; + case BitcoinAPITransactionPriority.hour: + return hour; + case BitcoinAPITransactionPriority.halfHour: + return halfHour; + case BitcoinAPITransactionPriority.fastest: + return fastest; case BitcoinAPITransactionPriority.custom: - label = 'Custom'; - break; + return custom; default: - break; + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); + } + } + + TransactionPriorityLabel getLabelWithRate(BitcoinAPITransactionPriority priorityType, + [int? rate]) { + late int rateValue; + + if (priorityType == BitcoinAPITransactionPriority.custom) { + if (rate == null) { + throw Exception('Rate must be provided for custom transaction priority'); + } + rateValue = rate; + } else { + rateValue = this[priorityType]; } - return label; + return TransactionPriorityLabel(priority: priorityType, rateValue: rateValue); } - String labelWithRate(int rate, int? customRate) { - final rateValue = this == custom ? customRate ??= 0 : rate; - return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)'; + String labelWithRate(BitcoinAPITransactionPriority priorityType, [int? rate]) { + return getLabelWithRate(priorityType, rate).toString(); + } + + @override + Map toJson() { + return { + 'minimum': minimum, + 'economy': economy, + 'hour': hour, + 'halfHour': halfHour, + 'fastest': fastest, + 'custom': custom, + }; + } + + static BitcoinAPITransactionPriorities fromJson(Map json) { + return BitcoinAPITransactionPriorities( + minimum: json['minimum'] as int, + economy: json['economy'] as int, + hour: json['hour'] as int, + halfHour: json['halfHour'] as int, + fastest: json['fastest'] as int, + custom: json['custom'] as int, + ); } } @@ -99,35 +173,80 @@ class ElectrumTransactionPriority extends TransactionPriority { } } - String get units => 'sat'; + String get unit => throw UnimplementedError(); @override String toString() { - var label = ''; + return title; + } + + @override + TransactionPriorityLabel getLabelWithRate(int rate, int? customRate) { + final rateValue = this.title == custom.title ? customRate ??= 0 : rate; + return TransactionPriorityLabel(priority: this, rateValue: rateValue); + } - switch (this) { + static ElectrumTransactionPriority fromPriority(TransactionPriority priority) { + if (priority.title == ElectrumTransactionPriority.slow.title) { + return ElectrumTransactionPriority.slow; + } else if (priority.title == ElectrumTransactionPriority.medium.title) { + return ElectrumTransactionPriority.medium; + } else if (priority.title == ElectrumTransactionPriority.fast.title) { + return ElectrumTransactionPriority.fast; + } else if (priority.title == ElectrumTransactionPriority.custom.title) { + return ElectrumTransactionPriority.custom; + } + + throw Exception('Unexpected token: $priority for ElectrumTransactionPriority fromPriority'); + } +} + +class BitcoinElectrumTransactionPriority extends ElectrumTransactionPriority { + const BitcoinElectrumTransactionPriority({required super.title, required super.raw}); + + @override + String get unit => 'sat'; + + static const List all = [fast, medium, slow, custom]; + + static const BitcoinElectrumTransactionPriority slow = + BitcoinElectrumTransactionPriority(title: 'Slow', raw: 0); + static const BitcoinElectrumTransactionPriority medium = + BitcoinElectrumTransactionPriority(title: 'Medium', raw: 1); + static const BitcoinElectrumTransactionPriority fast = + BitcoinElectrumTransactionPriority(title: 'Fast', raw: 2); + static const BitcoinElectrumTransactionPriority custom = + BitcoinElectrumTransactionPriority(title: 'Custom', raw: 3); + + static ElectrumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + case 3: + return custom; + default: + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); + } + } + + static BitcoinElectrumTransactionPriority fromPriority(TransactionPriority priority) { + switch (priority) { case ElectrumTransactionPriority.slow: - label = 'Slow ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs'; - break; + return BitcoinElectrumTransactionPriority.slow; case ElectrumTransactionPriority.medium: - label = 'Medium'; // S.current.transaction_priority_medium; - break; + return BitcoinElectrumTransactionPriority.medium; case ElectrumTransactionPriority.fast: - label = 'Fast'; - break; // S.current.transaction_priority_fast; + return BitcoinElectrumTransactionPriority.fast; case ElectrumTransactionPriority.custom: - label = 'Custom'; - break; + return BitcoinElectrumTransactionPriority.custom; default: - break; + throw Exception( + 'Unexpected token: $priority for BitcoinElectrumTransactionPriority fromPriority'); } - - return label; - } - - String labelWithRate(int rate, int? customRate) { - final rateValue = this == custom ? customRate ??= 0 : rate; - return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)'; } } @@ -135,88 +254,89 @@ class LitecoinTransactionPriority extends ElectrumTransactionPriority { const LitecoinTransactionPriority({required super.title, required super.raw}); @override - String get units => 'lit'; -} + String get unit => 'lit'; -class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { - const BitcoinCashTransactionPriority({required super.title, required super.raw}); - - @override - String get units => 'satoshi'; -} + static const List all = [fast, medium, slow]; -class BitcoinAPITransactionPriorities implements TransactionPriorities { - const BitcoinAPITransactionPriorities({ - required this.unimportant, - required this.normal, - required this.elevated, - required this.priority, - required this.custom, - }); + 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); - final int unimportant; - final int normal; - final int elevated; - final int priority; - final int custom; + static ElectrumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); + } + } - @override - int operator [](TransactionPriority type) { - switch (type) { - case BitcoinAPITransactionPriority.unimportant: - return unimportant; - case BitcoinAPITransactionPriority.normal: - return normal; - case BitcoinAPITransactionPriority.elevated: - return elevated; - case BitcoinAPITransactionPriority.priority: - return priority; - case BitcoinAPITransactionPriority.custom: - return custom; + static LitecoinTransactionPriority fromPriority(TransactionPriority priority) { + switch (priority) { + case ElectrumTransactionPriority.slow: + return LitecoinTransactionPriority.slow; + case ElectrumTransactionPriority.medium: + return LitecoinTransactionPriority.medium; + case ElectrumTransactionPriority.fast: + return LitecoinTransactionPriority.fast; default: - throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); + throw Exception('Unexpected token: $priority for LitecoinTransactionPriority fromPriority'); } } +} + +class BitcoinCashTransactionPriority extends ElectrumTransactionPriority { + const BitcoinCashTransactionPriority({required super.title, required super.raw}); @override - String labelWithRate(TransactionPriority priorityType, [int? rate]) { - late int rateValue; + String get unit => 'satoshi'; - if (priorityType == BitcoinAPITransactionPriority.custom) { - if (rate == null) { - throw Exception('Rate must be provided for custom transaction priority'); - } - rateValue = rate; - } else { - rateValue = this[priorityType]; - } + static const List all = [fast, medium, slow]; - return '${priorityType.toString()} (${rateValue} ${priorityType.getUnits(rateValue)}/byte)'; - } + 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); - @override - Map toJson() { - return { - 'unimportant': unimportant, - 'normal': normal, - 'elevated': elevated, - 'priority': priority, - 'custom': custom, - }; + static ElectrumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for ElectrumTransactionPriority deserialize'); + } } - static BitcoinAPITransactionPriorities fromJson(Map json) { - return BitcoinAPITransactionPriorities( - 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, - ); + static BitcoinCashTransactionPriority fromPriority(TransactionPriority priority) { + switch (priority) { + case ElectrumTransactionPriority.slow: + return BitcoinCashTransactionPriority.slow; + case ElectrumTransactionPriority.medium: + return BitcoinCashTransactionPriority.medium; + case ElectrumTransactionPriority.fast: + return BitcoinCashTransactionPriority.fast; + default: + throw Exception( + 'Unexpected token: $priority for BitcoinCashTransactionPriority fromPriority'); + } } } -class ElectrumTransactionPriorities implements TransactionPriorities { +class ElectrumTransactionPriorities + implements TransactionPriorities { const ElectrumTransactionPriorities({ required this.slow, required this.medium, @@ -230,31 +350,33 @@ class ElectrumTransactionPriorities implements TransactionPriorities { 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: - throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); + int operator [](T type) { + if (type.title == ElectrumTransactionPriority.slow.title) { + return slow; + } else if (type.title == ElectrumTransactionPriority.medium.title) { + return medium; + } else if (type.title == ElectrumTransactionPriority.fast.title) { + return fast; + } else if (type.title == ElectrumTransactionPriority.custom.title) { + return custom; } + + throw Exception('Unexpected token: $type for TransactionPriorities operator[]'); } - @override - String labelWithRate(TransactionPriority priorityType, [int? rate]) { + TransactionPriorityLabel getLabelWithRate(T priorityType, [int? rate]) { final rateValue = this[priorityType]; - return '${priorityType.toString()} ($rateValue ${priorityType.getUnits(rateValue)}/byte)'; + return TransactionPriorityLabel(priority: priorityType, rateValue: rateValue); + } + + String labelWithRate(T priorityType, [int? rate]) { + return getLabelWithRate(priorityType, rate).toString(); } factory ElectrumTransactionPriorities.fromList(List list) { if (list.length != 3) { throw Exception( - 'Unexpected list length: ${list.length} for BitcoinElectrumTransactionPriorities.fromList'); + 'Unexpected list length: ${list.length} for ElectrumTransactionPriorities.fromList'); } return ElectrumTransactionPriorities( @@ -285,8 +407,65 @@ class ElectrumTransactionPriorities implements TransactionPriorities { } } +class BitcoinElectrumTransactionPriorities + extends ElectrumTransactionPriorities { + const BitcoinElectrumTransactionPriorities({ + required super.slow, + required super.medium, + required super.fast, + required super.custom, + }) : super(); + + static BitcoinElectrumTransactionPriorities fromJson(Map json) { + return BitcoinElectrumTransactionPriorities( + slow: json['slow'] as int, + medium: json['medium'] as int, + fast: json['fast'] as int, + custom: json['custom'] as int, + ); + } +} + +class LitecoinTransactionPriorities + extends ElectrumTransactionPriorities { + const LitecoinTransactionPriorities({ + required super.slow, + required super.medium, + required super.fast, + required super.custom, + }) : super(); + + static LitecoinTransactionPriorities fromJson(Map json) { + return LitecoinTransactionPriorities( + slow: json['slow'] as int, + medium: json['medium'] as int, + fast: json['fast'] as int, + custom: json['custom'] as int, + ); + } +} + +class BitcoinCashTransactionPriorities + extends ElectrumTransactionPriorities { + const BitcoinCashTransactionPriorities({ + required super.slow, + required super.medium, + required super.fast, + required super.custom, + }) : super(); + + static BitcoinCashTransactionPriorities fromJson(Map json) { + return BitcoinCashTransactionPriorities( + 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')) { + if (json.containsKey('minimum')) { return BitcoinAPITransactionPriorities.fromJson(json); } else if (json.containsKey('slow')) { return ElectrumTransactionPriorities.fromJson(json); diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 27b16abbb2..4c0820c4fa 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -290,21 +290,6 @@ abstract class ElectrumWalletBase TransactionPriorities? feeRates; int feeRate(TransactionPriority priority) { - if (priority is ElectrumTransactionPriority && feeRates is BitcoinAPITransactionPriorities) { - final rates = feeRates as BitcoinAPITransactionPriorities; - - switch (priority) { - case ElectrumTransactionPriority.slow: - return rates.normal; - case ElectrumTransactionPriority.medium: - return rates.elevated; - case ElectrumTransactionPriority.fast: - return rates.priority; - case ElectrumTransactionPriority.custom: - return rates.custom; - } - } - return feeRates![priority]; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 946eb94e27..6e816d0b81 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -430,8 +430,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return addr.isChange && !addr.isUsed && addr.type == type; } - bool isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { - return !addr.isChange && !addr.isUsed && addr.type == type; + bool isUnusedReceiveAddress(BaseBitcoinAddressRecord addr) { + return !addr.isChange && !addr.isUsed; + } + + bool isUnusedReceiveAddressByType(BaseBitcoinAddressRecord addr, BitcoinAddressType type) { + return isUnusedReceiveAddress(addr) && addr.type == type; } Map toJson() { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 96b287c3d6..28906a2f8d 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -472,26 +472,27 @@ class ElectrumWorker { } Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { - if (request.mempoolAPIEnabled) { + if (request.mempoolAPIEnabled && _walletType == WalletType.bitcoin) { try { final recommendedFees = await ApiProvider.fromMempool( _network!, baseUrl: "http://mempool.cakewallet.com:8999/api/v1", ).getRecommendedFeeRate(); - final unimportantFee = recommendedFees.economyFee!.satoshis; - final normalFee = recommendedFees.low.satoshis; - int elevatedFee = recommendedFees.medium.satoshis; - int priorityFee = recommendedFees.high.satoshis; + final minimum = recommendedFees.minimumFee!.satoshis; + final economy = recommendedFees.economyFee!.satoshis; + final hour = recommendedFees.low.satoshis; + int halfHour = recommendedFees.medium.satoshis; + int fastest = 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++; + if (hour == halfHour) { + halfHour++; } // priority fee should be higher than elevated fee - while (priorityFee <= elevatedFee) { - priorityFee++; + while (fastest <= halfHour) { + fastest++; } // this guarantees that, even if all fees are low and equal, // higher priority fee txs can be consumed when chain fees start surging @@ -499,11 +500,12 @@ class ElectrumWorker { _sendResponse( ElectrumWorkerGetFeesResponse( result: BitcoinAPITransactionPriorities( - unimportant: unimportantFee, - normal: normalFee, - elevated: elevatedFee, - priority: priorityFee, - custom: unimportantFee, + minimum: minimum, + economy: economy, + hour: hour, + halfHour: halfHour, + fastest: fastest, + custom: minimum, ), ), ); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 628c8b8f8a..593339147f 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -887,22 +887,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // ); // } - @override - int feeRate(TransactionPriority priority) { - if (priority is ElectrumTransactionPriority) { - switch (priority) { - case ElectrumTransactionPriority.slow: - return 1; - case ElectrumTransactionPriority.medium: - return 2; - case ElectrumTransactionPriority.fast: - return 3; - } - } - - return 0; - } - @override TxCreateUtxoDetails createUTXOS({ required bool sendAll, diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 0deeb7e39c..59ebb6a298 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -60,6 +60,57 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with await super.init(); } + @action + 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 ? changeAddresses : receiveAddresses) + .where((addr) => addr.cwDerivationType == derivationType && addr.type == addressType) + .length; + + final mwebAddresses = []; + final newAddresses = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final addressString = await getAddressAsync( + derivationType: derivationType, + isChange: isChange, + index: i, + addressType: addressType, + derivationInfo: derivationInfo, + ); + + if (addressType == SegwitAddresType.mweb) { + final address = LitecoinMWEBAddressRecord(addressString, index: i); + mwebAddresses.add(address); + } else { + final address = BitcoinAddressRecord( + addressString, + index: i, + isChange: isChange, + isHidden: OLD_DERIVATION_TYPES.contains(derivationType), + type: addressType, + network: network, + derivationInfo: derivationInfo, + cwDerivationType: derivationType, + ); + + newAddresses.add(address); + } + } + + addAddresses(newAddresses); + addMwebAddresses(mwebAddresses); + return newAddresses; + } + Future ensureMwebAddressUpToIndexExists(int index) async { if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { return null; @@ -225,19 +276,17 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Future updateAddressesInBox() async { super.updateAddressesInBox(); - final lastP2wpkh = allAddresses - .where( - (addressRecord) => isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) - .toList() - .last; + final lastP2wpkh = + allAddresses.where((addressRecord) => isUnusedReceiveAddress(addressRecord)).toList().last; if (lastP2wpkh.address != address) { addressesMap[lastP2wpkh.address] = 'P2WPKH'; } else { addressesMap[address] = 'Active - P2WPKH'; } - final lastMweb = allAddresses.firstWhere( - (addressRecord) => isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + final lastMweb = mwebAddresses.firstWhere( + (addressRecord) => isUnusedReceiveAddress(addressRecord), + ); if (lastMweb.address != address) { addressesMap[lastMweb.address] = 'MWEB'; } else { diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 6e4b032a4e..cdbfc634fb 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -52,7 +52,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, mempoolAPIEnabled: mempoolAPIEnabled, - hdWallets: {CWBitcoinDerivationType.bip39: bitcoinCashHDWallet(seedBytes)}, + hdWallets: {CWBitcoinDerivationType.bip39: Bip32Slip10Secp256k1.fromSeed(seedBytes)}, ) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, @@ -184,22 +184,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw), ); - @override - int feeRate(TransactionPriority priority) { - if (priority is ElectrumTransactionPriority) { - switch (priority) { - case ElectrumTransactionPriority.slow: - return 1; - case ElectrumTransactionPriority.medium: - return 5; - case ElectrumTransactionPriority.fast: - return 10; - } - } - - return 0; - } - @override Future signMessage(String message, {String? address = null}) async { Bip32Slip10Secp256k1 HD = bip32; @@ -234,7 +218,4 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { 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 95ef2b5662..25d2955e0f 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -17,6 +17,12 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi super.initialAddressPageType, }) : super(walletInfo); + @override + Future init() async { + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + await super.init(); + } + @override BitcoinBaseAddress generateAddress({ required CWBitcoinDerivationType derivationType, diff --git a/cw_core/lib/transaction_priority.dart b/cw_core/lib/transaction_priority.dart index 78589abf46..54ba9e23c2 100644 --- a/cw_core/lib/transaction_priority.dart +++ b/cw_core/lib/transaction_priority.dart @@ -3,9 +3,22 @@ import 'package:cw_core/enumerable_item.dart'; abstract class TransactionPriority extends EnumerableItem with Serializable { const TransactionPriority({required super.title, required super.raw}); - String get units => ''; + String get unit => ''; String getUnits(int rate) { - return rate == 1 ? units : '${units}s'; + if (unit.endsWith('s')) { + // example: gas. doesn't become gass + return unit; + } + + return rate == 1 ? unit : '${unit}s'; + } + + TransactionPriorityLabel getLabelWithRate(int rate, int? customRate) { + throw UnimplementedError(); + } + + String labelWithRate(int rate, int? customRate) { + return getLabelWithRate(rate, customRate).toString(); } String toString() { @@ -13,12 +26,32 @@ abstract class TransactionPriority extends EnumerableItem with Serializable } } -abstract class TransactionPriorities { +abstract class TransactionPriorities { const TransactionPriorities(); - int operator [](TransactionPriority type); - String labelWithRate(TransactionPriority type); + int operator [](T type); + String labelWithRate(T type); Map toJson(); factory TransactionPriorities.fromJson(Map json) { throw UnimplementedError(); } } + +class TransactionPriorityLabel { + final TransactionPriority priority; + + final String title; + final String units; + final int rateValue; + + TransactionPriorityLabel({ + required int rateValue, + required this.priority, + String? title, + }) : title = title ?? priority.title, + units = priority.getUnits(rateValue), + rateValue = rateValue; + + String toString() { + return '$title ($rateValue $units/byte)'; + } +} diff --git a/cw_evm/lib/evm_chain_transaction_priority.dart b/cw_evm/lib/evm_chain_transaction_priority.dart index b4ce554908..29efd6c697 100644 --- a/cw_evm/lib/evm_chain_transaction_priority.dart +++ b/cw_evm/lib/evm_chain_transaction_priority.dart @@ -27,7 +27,8 @@ class EVMChainTransactionPriority extends TransactionPriority { } } - String get units => 'gas'; + @override + String get unit => 'gas'; @override String toString() { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index e07c791374..a76bf556f2 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -70,10 +70,23 @@ class CWBitcoin extends Bitcoin { } @override - List getTransactionPriorities() => ElectrumTransactionPriority.all; + List getElectrumTransactionPriorities() => + BitcoinElectrumTransactionPriority.all; @override - List getLitecoinTransactionPriorities() => ElectrumTransactionPriority.all; + List getBitcoinAPITransactionPriorities() => + BitcoinAPITransactionPriority.all; + + @override + List getTransactionPriorities(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.feeRates is ElectrumTransactionPriorities + ? BitcoinElectrumTransactionPriority.all + : BitcoinAPITransactionPriority.all; + } + + @override + List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => @@ -205,9 +218,20 @@ class CWBitcoin extends Bitcoin { BitcoinAmountUtils.stringDoubleToBitcoinAmount(amount); @override - String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, - {int? customRate}) => - (priority as ElectrumTransactionPriority).labelWithRate(rate, customRate); + TransactionPriorityLabel getTransactionPriorityWithLabel( + TransactionPriority priority, + int rate, { + int? customRate, + }) => + priority.getLabelWithRate(rate, customRate); + + @override + String bitcoinTransactionPriorityWithLabel( + TransactionPriority priority, + int rate, { + int? customRate, + }) => + priority.labelWithRate(rate, customRate); @override List getUnspents(Object wallet, @@ -533,8 +557,8 @@ class CWBitcoin extends Bitcoin { final electrumWallet = wallet as ElectrumWallet; final feeRates = electrumWallet.feeRates; final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities - ? ElectrumTransactionPriority.fast - : BitcoinAPITransactionPriority.priority; + ? BitcoinElectrumTransactionPriority.fast + : BitcoinAPITransactionPriority.fastest; return (electrumWallet.feeRate(maxFee) * 10).round(); } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index 0a91313490..a0cb406c2d 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -48,14 +48,15 @@ class CWBitcoinCash extends BitcoinCash { @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => - ElectrumTransactionPriority.deserialize(raw: raw); + BitcoinCashTransactionPriority.deserialize(raw: raw); @override - TransactionPriority getDefaultTransactionPriority() => ElectrumTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; @override - List getTransactionPriorities() => ElectrumTransactionPriority.all; + List getTransactionPriorities() => BitcoinCashTransactionPriority.all; @override - TransactionPriority getBitcoinCashTransactionPrioritySlow() => ElectrumTransactionPriority.slow; + TransactionPriority getBitcoinCashTransactionPrioritySlow() => + BitcoinCashTransactionPriority.slow; } diff --git a/lib/core/transaction_priority_label.dart b/lib/core/transaction_priority_label.dart new file mode 100644 index 0000000000..7365c224ef --- /dev/null +++ b/lib/core/transaction_priority_label.dart @@ -0,0 +1,40 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/transaction_priority.dart'; + +class TransactionPriorityLabelLocalized { + TransactionPriorityLabel label; + + TransactionPriorityLabelLocalized(this.label); + + String toString() { + late String title; + + if (label.title == 'Slow') { + title = S.current.transaction_priority_slow; + } else if (label.title == 'Medium') { + title = S.current.transaction_priority_medium; + } else if (label.title == 'Fast') { + title = S.current.transaction_priority_fast; + } else if (label.title == 'Custom') { + title = S.current.transaction_priority_custom; + } else if (label.title == 'Minimum') { + title = S.current.transaction_priority_minimum; + } else if (label.title == 'Economy') { + title = S.current.transaction_priority_economy; + } else if (label.title == 'Hour') { + title = S.current.transaction_priority_hour; + } else if (label.title == 'HalfHour') { + title = S.current.transaction_priority_half_hour; + } else if (label.title == 'Fastest') { + title = S.current.transaction_priority_fastest; + } else { + title = label.title; + } + + return TransactionPriorityLabel( + title: title, + rateValue: label.rateValue, + priority: label.priority, + ).toString(); + } +} diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 25140f106e..2803885d20 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -617,7 +617,10 @@ Future validateBitcoinSavedTransactionPriority(SharedPreferences sharedPre } final int? savedBitcoinPriority = sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority); - if (!bitcoin!.getTransactionPriorities().any((element) => element.raw == savedBitcoinPriority)) { + if (![ + ...bitcoin!.getElectrumTransactionPriorities(), + ...bitcoin!.getBitcoinAPITransactionPriorities() + ].any((element) => element.raw == savedBitcoinPriority)) { await sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, bitcoin!.getMediumTransactionPriority().serialize()); } diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 5342874940..4035dbfdd4 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,16 +6,17 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -List priorityForWalletType(WalletType type) { - switch (type) { +List priorityForWalletType(WalletBase wallet) { + switch (wallet.type) { case WalletType.monero: return monero!.getTransactionPriorities(); case WalletType.wownero: return wownero!.getTransactionPriorities(); case WalletType.bitcoin: - return bitcoin!.getTransactionPriorities(); + return bitcoin!.getTransactionPriorities(wallet); case WalletType.litecoin: return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.haven: diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index a93a019e84..f6b189ac87 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -322,15 +322,21 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( + future: output.estimatedFee, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text( + '${snapshot.data} ${sendViewModel.currency.toString()}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ); + } + return CircularProgressIndicator(); + }, ), Padding( padding: EdgeInsets.only(top: 5), @@ -523,7 +529,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin pickTransactionPriority(BuildContext context) async { - final items = priorityForWalletType(sendViewModel.walletType); + final items = priorityForWalletType(sendViewModel.wallet); final selectedItem = items.indexOf(sendViewModel.transactionPriority); final customItemIndex = sendViewModel.getCustomPriorityIndex(items); final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin; diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 24f3217985..c9d32d19af 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -34,24 +34,24 @@ class OtherSettingsPage extends BasePage { child: Column( children: [ if (_otherSettingsViewModel.displayTransactionPriority) - _otherSettingsViewModel.walletType == WalletType.bitcoin ? - SettingsPriorityPickerCell( - title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.walletType), - displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority, - selectedItem: _otherSettingsViewModel.transactionPriority, - customItemIndex: _otherSettingsViewModel.customPriorityItemIndex, - onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected, - customValue: _otherSettingsViewModel.customBitcoinFeeRate, - maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(), - ) : - SettingsPickerCell( - title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.walletType), - displayItem: _otherSettingsViewModel.getDisplayPriority, - selectedItem: _otherSettingsViewModel.transactionPriority, - onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, - ), + _otherSettingsViewModel.walletType == WalletType.bitcoin + ? SettingsPriorityPickerCell( + title: S.current.settings_fee_priority, + items: priorityForWalletType(_otherSettingsViewModel.sendViewModel.wallet), + displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority, + selectedItem: _otherSettingsViewModel.transactionPriority, + customItemIndex: _otherSettingsViewModel.customPriorityItemIndex, + onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected, + customValue: _otherSettingsViewModel.customBitcoinFeeRate, + maxValue: _otherSettingsViewModel.maxCustomFeeRate?.toDouble(), + ) + : SettingsPickerCell( + title: S.current.settings_fee_priority, + items: priorityForWalletType(_otherSettingsViewModel.sendViewModel.wallet), + displayItem: _otherSettingsViewModel.getDisplayPriority, + selectedItem: _otherSettingsViewModel.transactionPriority, + onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, + ), if (_otherSettingsViewModel.changeRepresentativeEnabled) SettingsCellWithArrow( title: S.current.change_rep, diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 0bf23aa7a5..28876d4e10 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -149,7 +149,7 @@ abstract class OutputBase with Store { if (_wallet.isElectrum) { late int fee; - if (transactionPriority == bitcoin!.getBitcoinTransactionPriorityCustom()) { + if (transactionPriority.title == bitcoin!.getBitcoinTransactionPriorityCustom().title) { fee = await bitcoin!.estimatedFeeForOutputWithFeeRate( _wallet, feeRate: _settingsStore.customBitcoinFeeRate, diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 7c601e3f62..653a9c6158 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/transaction_priority_label.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; @@ -87,12 +88,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor fiatFromSettings = appStore.settingsStore.fiatCurrency, super(appStore: appStore) { if (wallet.type == WalletType.bitcoin && - _settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) { + _settingsStore.priority[wallet.type]!.title == bitcoinTransactionPriorityCustom.title) { setTransactionPriority(bitcoinTransactionPriorityMedium); } final priority = _settingsStore.priority[wallet.type]; - final priorities = priorityForWalletType(wallet.type); - if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) { + final priorities = priorityForWalletType(wallet); + if (priorities.isNotEmpty && !priorities.contains(priority)) { _settingsStore.priority[wallet.type] = priorities.first; } @@ -204,8 +205,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor int? getCustomPriorityIndex(List priorities) { if (wallet.type == WalletType.bitcoin) { - final customItem = priorities - .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); + final customItem = priorities.firstWhereOrNull( + (element) => element.title == bitcoin!.getBitcoinTransactionPriorityCustom().title, + ); return customItem != null ? priorities.indexOf(customItem) : null; } @@ -595,12 +597,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (walletType == WalletType.bitcoin) { final rate = bitcoin!.getFeeRate(wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue); + return TransactionPriorityLabelLocalized( + bitcoin!.getTransactionPriorityWithLabel(_priority, rate, customRate: customValue), + ).toString(); } if (isElectrumWallet) { final rate = bitcoin!.getFeeRate(wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate); + return TransactionPriorityLabelLocalized( + bitcoin!.getTransactionPriorityWithLabel(_priority, rate), + ).toString(); } return priority.toString(); @@ -731,8 +737,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return S.current.insufficient_funds_for_tx; } - return - '''${S.current.insufficient_funds_for_tx} \n\n''' + return '''${S.current.insufficient_funds_for_tx} \n\n''' '''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n''' '''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n''' '''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})'''; @@ -813,4 +818,3 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return false; } } - diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index c7a5d0b90c..7437ea71ef 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -1,11 +1,8 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/package_info.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; -// import 'package:package_info/package_info.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_history.dart'; @@ -17,18 +14,17 @@ import 'package:mobx/mobx.dart'; part 'other_settings_view_model.g.dart'; -class OtherSettingsViewModel = OtherSettingsViewModelBase - with _$OtherSettingsViewModel; +class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel; abstract class OtherSettingsViewModelBase with Store { OtherSettingsViewModelBase(this._settingsStore, this._wallet, this.sendViewModel) : walletType = _wallet.type, currentVersion = '' { - PackageInfo.fromPlatform().then( - (PackageInfo packageInfo) => currentVersion = packageInfo.version); + PackageInfo.fromPlatform() + .then((PackageInfo packageInfo) => currentVersion = packageInfo.version); final priority = _settingsStore.priority[_wallet.type]; - final priorities = priorityForWalletType(_wallet.type); + final priorities = priorityForWalletType(_wallet); if (!priorities.contains(priority) && priorities.isNotEmpty) { _settingsStore.priority[_wallet.type] = priorities.first; @@ -36,8 +32,7 @@ abstract class OtherSettingsViewModelBase with Store { } final WalletType walletType; - final WalletBase, - TransactionInfo> _wallet; + final WalletBase, TransactionInfo> _wallet; @observable String currentVersion; @@ -63,7 +58,6 @@ abstract class OtherSettingsViewModelBase with Store { @computed bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; - @computed bool get displayTransactionPriority => !(changeRepresentativeEnabled || _wallet.type == WalletType.solana || @@ -89,8 +83,7 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) { final rate = bitcoin!.getFeeRate(_wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, - customRate: customValue); + return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue); } return priority.toString(); @@ -99,8 +92,7 @@ abstract class OtherSettingsViewModelBase with Store { void onDisplayPrioritySelected(TransactionPriority priority) => _settingsStore.priority[walletType] = priority; - void onDisplayBitcoinPrioritySelected( - TransactionPriority priority, double customValue) { + void onDisplayBitcoinPrioritySelected(TransactionPriority priority, double customValue) { if (_wallet.type == WalletType.bitcoin) { _settingsStore.customBitcoinFeeRate = customValue.round(); } @@ -108,13 +100,12 @@ abstract class OtherSettingsViewModelBase with Store { } @computed - double get customBitcoinFeeRate => - _settingsStore.customBitcoinFeeRate.toDouble(); + double get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate.toDouble(); int? get customPriorityItemIndex { - final priorities = priorityForWalletType(walletType); - final customItem = priorities.firstWhereOrNull( - (element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); + final priorities = priorityForWalletType(_wallet); + final customItem = priorities + .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); return customItem != null ? priorities.indexOf(customItem) : null; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 10ab425f1f..1680e4d7a9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -566,10 +566,10 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); } - final priorities = priorityForWalletType(wallet.type); + final priorities = priorityForWalletType(wallet); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); - final customItem = priorities - .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom); + final customItem = priorities.firstWhereOrNull( + (element) => element.title == sendViewModel.bitcoinTransactionPriorityCustom.title); final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); @@ -579,7 +579,7 @@ abstract class TransactionDetailsViewModelBase with Store { title: S.current.estimated_new_fee, value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}', - items: priorityForWalletType(wallet.type), + items: priorities, customValue: settingsStore.customBitcoinFeeRate.toDouble(), maxValue: maxCustomFeeRate, selectedIdx: selectedItem, @@ -675,7 +675,7 @@ abstract class TransactionDetailsViewModelBase with Store { } String setNewFee({double? value, required TransactionPriority priority}) { - if (priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null) { + if (priority.title == bitcoin!.getBitcoinTransactionPriorityCustom().title && value != null) { newFee = bitcoin!.getFeeAmountForOutputsWithFeeRate( wallet, feeRate: value.round(), diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 716913cded..284da1af21 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -152,6 +152,12 @@ abstract class WalletCreationVMBase with Store { ); } return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; + case WalletType.bitcoinCash: + return DerivationInfo( + derivationPath: "m/44'/145'/0'", + description: "Default Bitcoin Cash", + scriptType: "p2pkh", + ); default: return null; } diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 92e3762e80..a4c62ec8d7 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -866,11 +866,16 @@ "transaction_details_title": "تفاصيل المعاملة", "transaction_details_transaction_id": "رقم المعاملة", "transaction_key": "مفتاح العملية", + "transaction_priority_custom": "مخصص", + "transaction_priority_economy": "اقتصاد", "transaction_priority_fast": "سريع", "transaction_priority_fastest": "أسرع", + "transaction_priority_half_hour": "متوسط ​​(~ نصف ساعة)", + "transaction_priority_hour": "منخفض (~ 1 ساعة)", "transaction_priority_medium": "متوسط", + "transaction_priority_minimum": "الحد الأدنى", "transaction_priority_regular": "عادي", - "transaction_priority_slow": "بطيء", + "transaction_priority_slow": "بطيئة ~ 24 ساعة+", "transaction_sent": "تم إرسال المعاملة!", "transaction_sent_notice": "إذا لم تستمر الشاشة بعد دقيقة واحدة ، فتحقق من مستكشف البلوك والبريد الإلكتروني.", "transactions": "المعاملات", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index f8c39ad694..ba95afb008 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Подробности на транзакцията", "transaction_details_transaction_id": "Transaction ID", "transaction_key": "Transaction Key", + "transaction_priority_custom": "Обичай", + "transaction_priority_economy": "Икономика", "transaction_priority_fast": "Бързо", "transaction_priority_fastest": "Най-бързо", + "transaction_priority_half_hour": "Среден (~ Половин час)", + "transaction_priority_hour": "Ниско (~ 1 час)", "transaction_priority_medium": "Средно", + "transaction_priority_minimum": "Минимум", "transaction_priority_regular": "Обичайно", - "transaction_priority_slow": "Бавно", + "transaction_priority_slow": "Бавно ~ 24hrs+", "transaction_sent": "Сумата е изпратена!", "transaction_sent_notice": "Ако процесът продължи повече от 1 минута, проверете някой block explorer и своя имейл.", "transactions": "Транзакции", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 17d6fa3fb7..f79b1c4ed8 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Podrobnosti o transakci", "transaction_details_transaction_id": "ID transakce", "transaction_key": "Klíč transakce", + "transaction_priority_custom": "Zvyk", + "transaction_priority_economy": "Ekonomika", "transaction_priority_fast": "Rychlá", "transaction_priority_fastest": "Nejrychlejší", + "transaction_priority_half_hour": "Střední (~ půl hodiny)", + "transaction_priority_hour": "Nízká (~ 1 hodina)", "transaction_priority_medium": "Střední", + "transaction_priority_minimum": "Minimální", "transaction_priority_regular": "Běžná", - "transaction_priority_slow": "Pomalá", + "transaction_priority_slow": "Pomalé ~ 24 hodin+", "transaction_sent": "Transakce odeslána!", "transaction_sent_notice": "Pokud proces nepokročí během 1 minuty, zkontrolujte block explorer a svůj e-mail.", "transactions": "Transakce", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 472c5f7913..ad0567395e 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -867,11 +867,16 @@ "transaction_details_title": "Transaktionsdetails", "transaction_details_transaction_id": "Transaktions-ID", "transaction_key": "Transaktionsschlüssel", + "transaction_priority_custom": "Brauch", + "transaction_priority_economy": "Wirtschaft", "transaction_priority_fast": "Schnell", "transaction_priority_fastest": "Am schnellsten", + "transaction_priority_half_hour": "Medium (~ halbe Stunde)", + "transaction_priority_hour": "Niedrig (~ 1 Stunde)", "transaction_priority_medium": "Mittel", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Normal", - "transaction_priority_slow": "Langsam", + "transaction_priority_slow": "Langsam ~ 24 Stunden+", "transaction_sent": "Transaktion gesendet!", "transaction_sent_notice": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie einen Block-Explorer und Ihre E-Mail.", "transactions": "Transaktionen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index e05dbe2bae..55f0faf220 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Transaction Details", "transaction_details_transaction_id": "Transaction ID", "transaction_key": "Transaction Key", + "transaction_priority_custom": "Custom", + "transaction_priority_economy": "Economy", "transaction_priority_fast": "Fast", "transaction_priority_fastest": "Fastest", + "transaction_priority_half_hour": "Medium (~half hour)", + "transaction_priority_hour": "Low (~1 hour)", "transaction_priority_medium": "Medium", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Regular", - "transaction_priority_slow": "Slow", + "transaction_priority_slow": "Slow ~24hrs+", "transaction_sent": "Transaction sent!", "transaction_sent_notice": "If the screen doesn’t proceed after 1 minute, check a block explorer and your email.", "transactions": "Transactions", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 6ff923d5c6..520317f396 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -867,11 +867,16 @@ "transaction_details_title": "Detalles de la transacción", "transaction_details_transaction_id": "ID de transacción", "transaction_key": "Clave de transacción", + "transaction_priority_custom": "Costumbre", + "transaction_priority_economy": "Economía", "transaction_priority_fast": "Rápido", "transaction_priority_fastest": "Lo más rápido", + "transaction_priority_half_hour": "Medio (~ media hora)", + "transaction_priority_hour": "Bajo (~ 1 hora)", "transaction_priority_medium": "Medio", + "transaction_priority_minimum": "Mínimo", "transaction_priority_regular": "Regular", - "transaction_priority_slow": "Lento", + "transaction_priority_slow": "Lento ~ 24 horas+", "transaction_sent": "Transacción enviada!", "transaction_sent_notice": "Si la pantalla no continúa después de 1 minuto, revisa un explorador de bloques y tu correo electrónico.", "transactions": "Transacciones", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 021b814478..96d1d239b4 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Détails de transaction", "transaction_details_transaction_id": "ID de transaction", "transaction_key": "Clef de Transaction", + "transaction_priority_custom": "Coutume", + "transaction_priority_economy": "Économie", "transaction_priority_fast": "Rapide", "transaction_priority_fastest": "Le plus rapide", + "transaction_priority_half_hour": "Moyen (~ demi-heure)", + "transaction_priority_hour": "Bas (~ 1 heure)", "transaction_priority_medium": "Moyen", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Normal", - "transaction_priority_slow": "Lent", + "transaction_priority_slow": "Lent ~ 24hrs +", "transaction_sent": "Transaction émise !", "transaction_sent_notice": "Si l'écran ne continue pas après 1 minute, vérifiez un explorateur de blocs et votre e-mail.", "transactions": "Transactions", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 06bb3cced5..2958fd2df2 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -868,11 +868,16 @@ "transaction_details_title": "Bayanai game da aikace-aikacen", "transaction_details_transaction_id": "ID na kasuwanci", "transaction_key": "Aikace-aikacen key", + "transaction_priority_custom": "Al'ada", + "transaction_priority_economy": "Tattalin arzikin ƙasa", "transaction_priority_fast": "sauri", "transaction_priority_fastest": "mafi sauri", + "transaction_priority_half_hour": "Matsakaici (~ rabin sa'a)", + "transaction_priority_hour": "Low (~ 1 awa)", "transaction_priority_medium": "SAURI DA DADI", + "transaction_priority_minimum": "M", "transaction_priority_regular": "SAURI NORMAL", - "transaction_priority_slow": "SAURI DA SAURI", + "transaction_priority_slow": "Jinkirin ~ 24hs +", "transaction_sent": "An aika ciniki!", "transaction_sent_notice": "Idan allon bai ci gaba ba bayan minti 1, duba mai binciken toshewa da imel ɗin ku.", "transactions": "Ma'amaloli", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 6f62c1eba5..cc966f441c 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -501,8 +501,8 @@ "paste": "पेस्ट करें", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", "payment_id": "भुगतान ID: ", - "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "payment_was_received": "आपका भुगतान प्राप्त हुआ था।", + "Payment_was_received": "आपका भुगतान प्राप्त हो गया था।", "pending": " (अपूर्ण)", "percentageOf": "${amount} का", "pin_at_top": "शीर्ष पर ${token} पिन करें", @@ -868,11 +868,16 @@ "transaction_details_title": "लेनदेन का विवरण", "transaction_details_transaction_id": "लेनदेन आईडी", "transaction_key": "लेन-देन की", + "transaction_priority_custom": "रिवाज़", + "transaction_priority_economy": "अर्थव्यवस्था", "transaction_priority_fast": "उपवास", "transaction_priority_fastest": "सबसे तेजी से", + "transaction_priority_half_hour": "मध्यम (~ आधा घंटा)", + "transaction_priority_hour": "कम (~ 1 घंटा)", "transaction_priority_medium": "मध्यम", + "transaction_priority_minimum": "न्यूनतम", "transaction_priority_regular": "नियमित", - "transaction_priority_slow": "धीरे", + "transaction_priority_slow": "धीमी ~ 24hrs+", "transaction_sent": "भेजा गया लेन-देन", "transaction_sent_notice": "अगर 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो ब्लॉक एक्सप्लोरर और अपना ईमेल देखें।", "transactions": "लेन-देन", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 40b58a563f..f7e8c64c15 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Detalji transakcije", "transaction_details_transaction_id": "Transakcijski ID", "transaction_key": "Transakcijski ključ", + "transaction_priority_custom": "Običaj", + "transaction_priority_economy": "Gospodarstvo", "transaction_priority_fast": "Brzo", "transaction_priority_fastest": "Najbrže", + "transaction_priority_half_hour": "Srednji (~ pola sata)", + "transaction_priority_hour": "Nisko (~ 1 sat)", "transaction_priority_medium": "Srednje", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Uobičajeno", - "transaction_priority_slow": "Sporo", + "transaction_priority_slow": "Sporo ~ 24 sata+", "transaction_sent": "Transakcija provedena!", "transaction_sent_notice": "Ako se zaslon ne nastavi nakon 1 minute, provjerite block explorer i svoju e-poštu.", "transactions": "Transakcije", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index d941171157..85dc8f1ce9 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Գործարքի մանրամասներ", "transaction_details_transaction_id": "Գործարքի ID", "transaction_key": "Գործարքի բանալի", + "transaction_priority_custom": "Մաքսատուրք", + "transaction_priority_economy": "Տնտեսություն", "transaction_priority_fast": "Արագ", "transaction_priority_fastest": "Ամենաարագ", + "transaction_priority_half_hour": "Միջին (կես ժամ)", + "transaction_priority_hour": "Low ածր (1 ժամ)", "transaction_priority_medium": "Միջին", + "transaction_priority_minimum": "Նվազագույն", "transaction_priority_regular": "Սովորական", - "transaction_priority_slow": "Դանդաղ", + "transaction_priority_slow": "Դանդաղ ~ 24 ժամ +", "transaction_sent": "Փոխանցումն ուղարկված է", "transaction_sent_notice": "Եթե էկրանը 1 րոպեի ընթացքում չի թարմանում, խնդրում ենք ստուգել բլոկի բացահայտիչը և Ձեր էլ. փոստը", "transactions": "Փոխանցումներ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index d63ac12c38..ea4446e984 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -869,11 +869,16 @@ "transaction_details_title": "Rincian Transaksi", "transaction_details_transaction_id": "ID Transaksi", "transaction_key": "Kunci transaksi", + "transaction_priority_custom": "Kebiasaan", + "transaction_priority_economy": "Ekonomi", "transaction_priority_fast": "Cepat", "transaction_priority_fastest": "Tercepat", + "transaction_priority_half_hour": "Sedang (~ setengah jam)", + "transaction_priority_hour": "Rendah (~ 1 jam)", "transaction_priority_medium": "Sedang", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Normal", - "transaction_priority_slow": "Lambat", + "transaction_priority_slow": "Lambat ~ 24 jam+", "transaction_sent": "Transaksi terkirim!", "transaction_sent_notice": "Jika layar tidak bergerak setelah 1 menit, periksa block explorer dan email Anda.", "transactions": "Transaksi", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8ce918d7e2..cce9f3b55b 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -868,11 +868,16 @@ "transaction_details_title": "Dettagli Transazione", "transaction_details_transaction_id": "ID Transazione", "transaction_key": "Chiave Transazione", + "transaction_priority_custom": "Costume", + "transaction_priority_economy": "Economia", "transaction_priority_fast": "Alta", "transaction_priority_fastest": "Massima", + "transaction_priority_half_hour": "Medio (~ mezz'ora)", + "transaction_priority_hour": "Basso (~ 1 ora)", "transaction_priority_medium": "Media", + "transaction_priority_minimum": "Minimo", "transaction_priority_regular": "Regolare", - "transaction_priority_slow": "Bassa", + "transaction_priority_slow": "Lento ~ 24 ore+", "transaction_sent": "Transazione inviata!", "transaction_sent_notice": "Se lo schermo non procede dopo 1 minuto, controlla un block explorer e la tua email.", "transactions": "Transazioni", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 2d6095a792..d510ed590d 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -867,11 +867,16 @@ "transaction_details_title": "取引の詳細", "transaction_details_transaction_id": "トランザクションID", "transaction_key": "トランザクションキー", + "transaction_priority_custom": "カスタム", + "transaction_priority_economy": "経済", "transaction_priority_fast": "速い", "transaction_priority_fastest": "最速", + "transaction_priority_half_hour": "ミディアム(〜30分)", + "transaction_priority_hour": "低(〜1時間)", "transaction_priority_medium": "中", + "transaction_priority_minimum": "最小", "transaction_priority_regular": "レギュラー", - "transaction_priority_slow": "スロー", + "transaction_priority_slow": "遅い〜24時間+", "transaction_sent": "トランザクションが送信されました!", "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。", "transactions": "取引", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 63a7dd62ec..1f05d79e42 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -867,11 +867,16 @@ "transaction_details_title": "상세 거래 내역", "transaction_details_transaction_id": "트랜잭션 ID", "transaction_key": "거래 키", + "transaction_priority_custom": "관습", + "transaction_priority_economy": "경제", "transaction_priority_fast": "빠른", "transaction_priority_fastest": "가장 빠른", + "transaction_priority_half_hour": "중간 (~ 30 분)", + "transaction_priority_hour": "낮음 (~ 1 시간)", "transaction_priority_medium": "매질", + "transaction_priority_minimum": "최저한의", "transaction_priority_regular": "정규병", - "transaction_priority_slow": "느린", + "transaction_priority_slow": "느린 ~ 24hrs+", "transaction_sent": "거래가 전송되었습니다!", "transaction_sent_notice": "1분 후에도 화면이 진행되지 않으면 블록 익스플로러와 이메일을 확인하세요.", "transactions": "업무", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index adb1fb9abe..ccf588a5f3 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -866,11 +866,16 @@ "transaction_details_title": "ငွေပေးငွေယူအသေးစိတ်", "transaction_details_transaction_id": "ငွေပေးငွေယူ ID", "transaction_key": "ငွေသွင်းငွေထုတ်ကီး", + "transaction_priority_custom": "ထုံးစံဓလေ့", + "transaction_priority_economy": "စီးပါှးရေး", "transaction_priority_fast": "မြန်သည်။", "transaction_priority_fastest": "အမြန်ဆုံး", + "transaction_priority_half_hour": "အလယ်အလတ် (~ နာရီဝက်)", + "transaction_priority_hour": "အနိမ့် (~ 1 နာရီ)", "transaction_priority_medium": "အလယ်အလတ်", + "transaction_priority_minimum": "အနည်းဆုံး", "transaction_priority_regular": "ပုံမှန်အစည်းအဝေး", - "transaction_priority_slow": "နှေးနှေး", + "transaction_priority_slow": "နှေးကွေး ~ 24hrs +", "transaction_sent": "ငွေပေးချေမှု ပို့ပြီးပါပြီ။!", "transaction_sent_notice": "မျက်နှာပြင်သည် ၁ မိနစ်အကြာတွင် ဆက်လက်မလုပ်ဆောင်ပါက၊ ပိတ်ဆို့ရှာဖွေသူနှင့် သင့်အီးမေးလ်ကို စစ်ဆေးပါ။", "transactions": "ငွေပေးငွေယူ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 68de09694a..de83ec5ec6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Transactie details", "transaction_details_transaction_id": "Transactie ID", "transaction_key": "Transactiesleutel", + "transaction_priority_custom": "Aangepast", + "transaction_priority_economy": "Economie", "transaction_priority_fast": "Snel", "transaction_priority_fastest": "Snelste", + "transaction_priority_half_hour": "Medium (~ half uur)", + "transaction_priority_hour": "Laag (~ 1 uur)", "transaction_priority_medium": "Medium", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Regelmatig", - "transaction_priority_slow": "Langzaam", + "transaction_priority_slow": "Langzaam ~ 24 uur++", "transaction_sent": "Transactie verzonden!", "transaction_sent_notice": "Als het scherm na 1 minuut niet verder gaat, controleer dan een blokverkenner en je e-mail.", "transactions": "Transacties", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index c24512fd35..f3b9dd057e 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Szczegóły transakcji", "transaction_details_transaction_id": "ID Trancakcji", "transaction_key": "Klucz transakcji", + "transaction_priority_custom": "Zwyczaj", + "transaction_priority_economy": "Gospodarka", "transaction_priority_fast": "Szybka", "transaction_priority_fastest": "Najszybsza", + "transaction_priority_half_hour": "Medium (~ pół godziny)", + "transaction_priority_hour": "Niski (~ 1 godzina)", "transaction_priority_medium": "Średnia", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Regularna", - "transaction_priority_slow": "Wolna(Zalecane)", + "transaction_priority_slow": "Powolny ~ 24 godziny+", "transaction_sent": "Transakcja wysłana!", "transaction_sent_notice": "Jeśli ekran nie zmieni się po 1 minucie, sprawdź eksplorator bloków i swój e-mail.", "transactions": "Transakcje", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index f804c616a1..8b623d5cc8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -868,11 +868,16 @@ "transaction_details_title": "Detalhes da transação", "transaction_details_transaction_id": "ID da transação", "transaction_key": "Chave de transação", + "transaction_priority_custom": "Personalizado", + "transaction_priority_economy": "Economia", "transaction_priority_fast": "Rápida", "transaction_priority_fastest": "Muito rápida", + "transaction_priority_half_hour": "Médio (~ meia hora)", + "transaction_priority_hour": "Baixo (~ 1 hora)", "transaction_priority_medium": "Média", + "transaction_priority_minimum": "Mínimo", "transaction_priority_regular": "Regular", - "transaction_priority_slow": "Lenta", + "transaction_priority_slow": "Lento ~ 24 horas+", "transaction_sent": "Transação enviada!", "transaction_sent_notice": "Se a tela não prosseguir após 1 minuto, verifique um explorador de blocos e seu e-mail.", "transactions": "Transações", @@ -993,4 +998,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 67e7c0b881..2fd007184b 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -867,11 +867,16 @@ "transaction_details_title": "Детали транзакции", "transaction_details_transaction_id": "ID транзакции", "transaction_key": "Ключ транзакции", + "transaction_priority_custom": "Обычай", + "transaction_priority_economy": "Экономика", "transaction_priority_fast": "Быстрый", "transaction_priority_fastest": "Самый быстрый", + "transaction_priority_half_hour": "Средний (~ полчаса)", + "transaction_priority_hour": "Низкий (~ 1 час)", "transaction_priority_medium": "Средний", + "transaction_priority_minimum": "Минимум", "transaction_priority_regular": "Обычный", - "transaction_priority_slow": "Медленный", + "transaction_priority_slow": "Медленно ~ 24 часа+", "transaction_sent": "Tранзакция отправлена!", "transaction_sent_notice": "Если экран не отображается через 1 минуту, проверьте обозреватель блоков и свою электронную почту.", "transactions": "Транзакции", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 51be995a52..fcd20c47a2 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -866,11 +866,16 @@ "transaction_details_title": "รายละเอียดการทำรายการ", "transaction_details_transaction_id": "ไอดีการทำรายการ", "transaction_key": "รหัสธุรกรรม", + "transaction_priority_custom": "กำหนดเอง", + "transaction_priority_economy": "เศรษฐกิจ", "transaction_priority_fast": "เร็ว", "transaction_priority_fastest": "เร็วที่สุด", + "transaction_priority_half_hour": "ปานกลาง (~ ครึ่งชั่วโมง)", + "transaction_priority_hour": "ต่ำ (~ 1 ชั่วโมง)", "transaction_priority_medium": "ปานกลาง", + "transaction_priority_minimum": "ขั้นต่ำสุด", "transaction_priority_regular": "ปกติ", - "transaction_priority_slow": "ช้า", + "transaction_priority_slow": "ช้า ~ 24 ชั่วโมง+", "transaction_sent": "ธุรกรรมถูกส่ง!", "transaction_sent_notice": "ถ้าหน้าจอไม่ขึ้นหลังจาก 1 นาทีแล้ว ให้ตรวจสอบ block explorer และอีเมลของคุณ", "transactions": "ธุรกรรม", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 844220aaac..cacbf3a35b 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -866,11 +866,16 @@ "transaction_details_title": "Mga detalye ng transaksyon", "transaction_details_transaction_id": "Transaction ID", "transaction_key": "Transaction Key", + "transaction_priority_custom": "Pasadya", + "transaction_priority_economy": "Ekonomiya", "transaction_priority_fast": "Mabilis", "transaction_priority_fastest": "Pinakamabilis", + "transaction_priority_half_hour": "Katamtaman (~ kalahating oras)", + "transaction_priority_hour": "Mababa (~ 1 oras)", "transaction_priority_medium": "Medium", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Regular", - "transaction_priority_slow": "Mabagal", + "transaction_priority_slow": "Mabagal ~ 24hrs+", "transaction_sent": "Ipinadala ang transaksyon!", "transaction_sent_notice": "Kung hindi magpapatuloy ang screen pagkatapos ng 1 minuto, tingnan ang block explorer at ang iyong email.", "transactions": "Mga Transaksyon", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 4f0e53d19a..f62cdef222 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -866,11 +866,16 @@ "transaction_details_title": "İşlem Detayları", "transaction_details_transaction_id": "İşem ID'si", "transaction_key": "İşlem Anahtarı", + "transaction_priority_custom": "Gelenek", + "transaction_priority_economy": "Ekonomi", "transaction_priority_fast": "Hızlı", "transaction_priority_fastest": "En Hızlı", + "transaction_priority_half_hour": "Orta (~ yarım saat)", + "transaction_priority_hour": "Düşük (~ 1 saat)", "transaction_priority_medium": "Orta", + "transaction_priority_minimum": "Minimum", "transaction_priority_regular": "Normal", - "transaction_priority_slow": "Yavaş", + "transaction_priority_slow": "Yavaş ~ 24 saat+", "transaction_sent": "Transfer gönderildi!", "transaction_sent_notice": "Ekran 1 dakika sonra ilerlemezse, blok gezgininden ve e-postanızdan kontrol edin.", "transactions": "İşlemler", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 24337d85c2..fede99baf1 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -867,11 +867,16 @@ "transaction_details_title": "Деталі транзакції", "transaction_details_transaction_id": "ID транзакції", "transaction_key": "Ключ транзакції", + "transaction_priority_custom": "Звичайний", + "transaction_priority_economy": "Економія", "transaction_priority_fast": "Швидкий", "transaction_priority_fastest": "Найшвидший", + "transaction_priority_half_hour": "Середній (~ півгодини)", + "transaction_priority_hour": "Низько (~ 1 година)", "transaction_priority_medium": "Середній", + "transaction_priority_minimum": "Мінімум", "transaction_priority_regular": "Звичайний", - "transaction_priority_slow": "Повільний", + "transaction_priority_slow": "Повільно ~ 24 години+", "transaction_sent": "Tранзакцію відправлено!", "transaction_sent_notice": "Якщо екран не відображається через 1 хвилину, перевірте провідник блоків і свою електронну пошту.", "transactions": "Транзакції", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index dfaae52dcd..94ff8c27d9 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -868,11 +868,16 @@ "transaction_details_title": "لین دین کی تفصیلات", "transaction_details_transaction_id": "ٹرانزیکشن ID", "transaction_key": "لین دین کی کلید", + "transaction_priority_custom": "رواج", + "transaction_priority_economy": "معیشت", "transaction_priority_fast": "تیز", "transaction_priority_fastest": "تیز ترین", + "transaction_priority_half_hour": "میڈیم (آدھا گھنٹہ)", + "transaction_priority_hour": "کم (~ 1 گھنٹہ)", "transaction_priority_medium": "درمیانہ", + "transaction_priority_minimum": "کم سے کم", "transaction_priority_regular": "باقاعدہ", - "transaction_priority_slow": "سست", + "transaction_priority_slow": "سست ~ 24 گھنٹے+", "transaction_sent": "لین دین بھیجا گیا!", "transaction_sent_notice": "اگر اسکرین 1 منٹ کے بعد آگے نہیں بڑھتی ہے، تو بلاک ایکسپلورر اور اپنا ای میل چیک کریں۔", "transactions": "لین دین", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index adcfa4405f..7d759d0843 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -865,11 +865,16 @@ "transaction_details_title": "Chi tiết giao dịch", "transaction_details_transaction_id": "ID giao dịch", "transaction_key": "Khóa giao dịch", + "transaction_priority_custom": "Phong tục", + "transaction_priority_economy": "Kinh tế", "transaction_priority_fast": "Nhanh", "transaction_priority_fastest": "Nhanh nhất", + "transaction_priority_half_hour": "Trung bình (~ nửa giờ)", + "transaction_priority_hour": "Thấp (~ 1 giờ)", "transaction_priority_medium": "Trung bình", + "transaction_priority_minimum": "Tối thiểu", "transaction_priority_regular": "Thông thường", - "transaction_priority_slow": "Chậm", + "transaction_priority_slow": "Chậm ~ 24 giờ+", "transaction_sent": "Giao dịch đã được gửi!", "transaction_sent_notice": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra trình khám phá khối và email của bạn.", "transactions": "Giao dịch", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 01e49999c1..b10963aae2 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -867,11 +867,16 @@ "transaction_details_title": "Àránṣẹ́ ìsọfúnni", "transaction_details_transaction_id": "Àmì ìdánimọ̀ àránṣẹ́", "transaction_key": "Kọ́kọ́rọ́ pàṣípààrọ̀", + "transaction_priority_custom": "Aṣa", + "transaction_priority_economy": "Ọrọ aje", "transaction_priority_fast": "Yára", "transaction_priority_fastest": "Yá jù lọ", + "transaction_priority_half_hour": "Alabọbọ (~ idaji wakati)", + "transaction_priority_hour": "Kekere (~ 1 wakati)", "transaction_priority_medium": "L’áàárín", + "transaction_priority_minimum": "O kere ju", "transaction_priority_regular": "Àjùmọ̀lò", - "transaction_priority_slow": "Díẹ̀", + "transaction_priority_slow": "O lọra ~ 24hrs +", "transaction_sent": "Ẹ ti ránṣẹ́ ẹ̀!", "transaction_sent_notice": "Tí aṣàfihàn kò bá tẹ̀síwájú l'áàárín ìṣẹ́jú kan, ẹ tọ́ olùṣèwádìí àkójọpọ̀ àti ímeèlì yín wò.", "transactions": "Àwọn àránṣẹ́", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index da8d11da5c..211032b0af 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -866,11 +866,16 @@ "transaction_details_title": "交易明细", "transaction_details_transaction_id": "交易编号", "transaction_key": "交易密码", + "transaction_priority_custom": "风俗", + "transaction_priority_economy": "经济", "transaction_priority_fast": "快速", "transaction_priority_fastest": "最快", + "transaction_priority_half_hour": "中(〜半小时)", + "transaction_priority_hour": "低(〜1小时)", "transaction_priority_medium": "中等", + "transaction_priority_minimum": "最低限度", "transaction_priority_regular": "常规", - "transaction_priority_slow": "慢速", + "transaction_priority_slow": "慢〜24小时+", "transaction_sent": "交易已发送", "transaction_sent_notice": "如果屏幕在 1 分钟后没有继续,请检查区块浏览器和您的电子邮件。", "transactions": "交易情况", diff --git a/tool/configure.dart b/tool/configure.dart index 8fd9d8fed9..aaa5be83a6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -175,7 +175,9 @@ abstract class Bitcoin { WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); List getWordList(); Map getWalletKeys(Object wallet); - List getTransactionPriorities(); + List getElectrumTransactionPriorities(); + List getBitcoinAPITransactionPriorities(); + List getTransactionPriorities(Object wallet); List getLitecoinTransactionPriorities(); TransactionPriority deserializeBitcoinTransactionPriority(int raw); TransactionPriority deserializeLitecoinTransactionPriority(int raw); @@ -195,7 +197,16 @@ abstract class Bitcoin { String formatterBitcoinAmountToString({required int amount}); double formatterBitcoinAmountToDouble({required int amount}); int formatterStringDoubleToBitcoinAmount(String amount); - String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate, {int? customRate}); + TransactionPriorityLabel getTransactionPriorityWithLabel( + TransactionPriority priority, + int rate, { + int? customRate, + }); + String bitcoinTransactionPriorityWithLabel( + TransactionPriority priority, + int rate, { + int? customRate, + }); List getUnspents(Object wallet, {UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any}); Future updateUnspents(Object wallet); diff --git a/tool/update_translation.dart b/tool/update_translation.dart new file mode 100644 index 0000000000..11d19e55c4 --- /dev/null +++ b/tool/update_translation.dart @@ -0,0 +1,59 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:cw_core/utils/print_verbose.dart'; + +import 'utils/translation/arb_file_utils.dart'; +import 'utils/translation/translation_constants.dart'; +import 'utils/translation/translation_utils.dart'; + +void main(List args) async { + if (args.length < 2) { + throw Exception( + 'Insufficient arguments!\n\nTry to run `./update_translation.dart "greetings" "New Hello World!"`'); + } + + final name = args.first; + final newText = args[1]; + final force = args.last == "--force"; + + printV('Updating "$name" to: "$newText"'); + + for (var lang in langs) { + final fileName = getArbFileName(lang); + final file = File(fileName); + final arbFile = await readArbFile(file); + + if (!arbFile.containsKey(name)) { + printV('Key "$name" not found in $fileName - skipping'); + continue; + } + + final newTranslation = await getTranslation(newText, lang); + updateStringInArbFile(fileName, name, newTranslation, force: force); + } + + printV('Alphabetizing all files...'); + + for (var lang in langs) { + final fileName = getArbFileName(lang); + alphabetizeArbFile(fileName); + } + + printV('Done!'); +} + +void updateStringInArbFile(String fileName, String key, String value, {bool force = false}) { + final file = File(fileName); + if (!file.existsSync()) { + throw Exception('File $fileName does not exist!'); + } + + final content = jsonDecode(file.readAsStringSync()) as Map; + + if (!content.containsKey(key) && !force) { + throw Exception('Key "$key" not found in $fileName and --force not specified'); + } + + content[key] = value; + file.writeAsStringSync(JsonEncoder.withIndent(' ').convert(content)); +} From a09cb62f468aeac6353b415f10a87c44e449a5f2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 8 Jan 2025 18:23:17 -0300 Subject: [PATCH 42/64] refactor: misc [skip ci] --- .gitignore | 1 + cw_bitcoin/lib/bitcoin_wallet.dart | 44 ------------- .../lib/src/bitcoin_cash_wallet.dart | 4 +- cw_core/lib/wallet_base.dart | 2 +- .../.plugin_symlinks/path_provider_linux | 1 - lib/bitcoin/cw_bitcoin.dart | 6 -- lib/entities/preferences_key.dart | 1 - lib/store/settings_store.dart | 65 ++++++++----------- lib/view_model/send/output.dart | 2 +- .../silent_payments_settings_view_model.dart | 9 --- tool/configure.dart | 1 - 11 files changed, 30 insertions(+), 106 deletions(-) delete mode 120000 cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux diff --git a/.gitignore b/.gitignore index ac0d42742d..54376162eb 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ android/app/.externalNativeBuild/ cw_monero/ios/External/ cw_monero/cw_monero/android/.externalNativeBuild/ cw_monero/cw_monero/android/.cxx/ +cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux # Generated dart files **/*.g.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 5e51138e27..172665741c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -481,50 +481,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { _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(), - // ); - - // printV("registered: $registered"); - } - @action void _updateSilentAddressRecord(BitcoinUnspent unspent) { final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index cdbfc634fb..ba35351df6 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -3,14 +3,12 @@ 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_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'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -57,10 +55,10 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { walletAddresses = BitcoinCashWalletAddresses( walletInfo, initialAddresses: initialAddresses, + hdWallets: hdWallets, network: network, initialAddressPageType: addressPageType, isHardwareWallet: walletInfo.isHardwareWallet, - hdWallets: hdWallets, ); autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 6b12af4722..69a8fdde77 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -24,7 +24,7 @@ abstract class WalletBase walletInfo.type; - bool get isElectrum => + bool get isElectrumBased => type == WalletType.bitcoin || type == WalletType.litecoin || type == WalletType.bitcoinCash; CryptoCurrency get currency => currencyForWalletType(type, isTestnet: isTestnet); diff --git a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux b/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux deleted file mode 120000 index 5dc8fb651e..0000000000 --- a/cw_monero/linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux +++ /dev/null @@ -1 +0,0 @@ -/home/rafael/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ \ No newline at end of file diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a76bf556f2..5b58201dd3 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -661,12 +661,6 @@ 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; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 686619d76c..58a5402786 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -49,7 +49,6 @@ 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/store/settings_store.dart b/lib/store/settings_store.dart index feb58d11ba..e0794fba0f 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -118,7 +118,6 @@ 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, @@ -226,14 +225,11 @@ abstract class SettingsStoreBase with Store { (FiatCurrency fiatCurrency) => sharedPreferences.setString( PreferencesKey.currentFiatCurrencyKey, fiatCurrency.serialize())); - reaction( - (_) => selectedCakePayCountry, - (Country? country) { - if (country != null) { - sharedPreferences.setString( - PreferencesKey.currentCakePayCountry, country.raw); - } - }); + reaction((_) => selectedCakePayCountry, (Country? country) { + if (country != null) { + sharedPreferences.setString(PreferencesKey.currentCakePayCountry, country.raw); + } + }); reaction( (_) => shouldShowYatPopup, @@ -291,9 +287,11 @@ abstract class SettingsStoreBase with Store { }); } - reaction((_) => disableTradeOption, - (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); - + reaction( + (_) => disableTradeOption, + (bool disableTradeOption) => + sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); + reaction( (_) => disableBulletin, (bool disableBulletin) => @@ -305,8 +303,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index)); reaction( - (_) => contactListOrder, - (FilterListOrderType contactListOrder) => + (_) => contactListOrder, + (FilterListOrderType contactListOrder) => sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index)); reaction( @@ -315,8 +313,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending)); reaction( - (_) => contactListAscending, - (bool contactListAscending) => + (_) => contactListAscending, + (bool contactListAscending) => sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending)); reaction( @@ -358,8 +356,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setBool(PreferencesKey.shouldShowMarketPlaceInDashboard, value)); reaction( - (_) => showAddressBookPopupEnabled, - (bool value) => + (_) => showAddressBookPopupEnabled, + (bool value) => sharedPreferences.setBool(PreferencesKey.showAddressBookPopupEnabled, value)); reaction((_) => pinCodeLength, @@ -553,11 +551,6 @@ 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) => @@ -797,9 +790,6 @@ abstract class SettingsStoreBase with Store { @observable bool silentPaymentsAlwaysScan; - @observable - bool silentPaymentsKeyRegistered; - @observable bool mwebAlwaysScan; @@ -862,10 +852,10 @@ abstract class SettingsStoreBase with Store { final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); - final savedCakePayCountryRaw = sharedPreferences.getString(PreferencesKey.currentCakePayCountry); - final currentCakePayCountry = savedCakePayCountryRaw != null - ? Country.deserialize(raw: savedCakePayCountryRaw) - : null; + final savedCakePayCountryRaw = + sharedPreferences.getString(PreferencesKey.currentCakePayCountry); + final currentCakePayCountry = + savedCakePayCountryRaw != null ? Country.deserialize(raw: savedCakePayCountryRaw) : null; TransactionPriority? moneroTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!); @@ -920,12 +910,13 @@ abstract class SettingsStoreBase with Store { final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; - final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; + final disableTradeOption = + sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; final contactListOrder = - FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; final walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; final contactListAscending = @@ -981,8 +972,6 @@ 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; @@ -1259,7 +1248,6 @@ abstract class SettingsStoreBase with Store { customBitcoinFeeRate: customBitcoinFeeRate, silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, - silentPaymentsKeyRegistered: silentPaymentsKeyRegistered, mwebAlwaysScan: mwebAlwaysScan, mwebCardDisplay: mwebCardDisplay, mwebEnabled: mwebEnabled, @@ -1374,13 +1362,14 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; - disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; + disableTradeOption = + sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; contactListOrder = - FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; shouldShowMarketPlaceInDashboard = @@ -1431,8 +1420,6 @@ 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/send/output.dart b/lib/view_model/send/output.dart index 28876d4e10..b2a9a8122e 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -146,7 +146,7 @@ abstract class OutputBase with Store { final transactionPriority = _settingsStore.priority[_wallet.type]!; - if (_wallet.isElectrum) { + if (_wallet.isElectrumBased) { late int fee; if (transactionPriority.title == bitcoin!.getBitcoinTransactionPriorityCustom().title) { 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 d7350e07a5..5d20230d27 100644 --- a/lib/view_model/settings/silent_payments_settings_view_model.dart +++ b/lib/view_model/settings/silent_payments_settings_view_model.dart @@ -20,9 +20,6 @@ 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; @@ -33,10 +30,4 @@ 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/tool/configure.dart b/tool/configure.dart index aaa5be83a6..793f460dad 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -283,7 +283,6 @@ abstract class Bitcoin { String? memo, bool enableRBF = true, }); - Future registerSilentPaymentsKey(Object wallet, bool active); Future checkIfMempoolAPIIsEnabled(Object wallet); Future getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}); int getLitecoinHeightByDate({required DateTime date}); From 1c5be4c4fe04fad641d51b60fa0859e4d7108ab2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 10 Jan 2025 12:03:11 -0300 Subject: [PATCH 43/64] feat: merging bitcoin_base & blockchain_utils --- cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- .../lib/bitcoin_hardware_wallet_service.dart | 18 +- .../lib/bitcoin_receive_page_option.dart | 16 +- cw_bitcoin/lib/bitcoin_wallet.dart | 20 +-- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 20 +-- cw_bitcoin/lib/electrum_wallet.dart | 15 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 6 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 8 +- .../lib/electrum_worker/electrum_worker.dart | 161 ++---------------- .../methods/scripthashes_subscribe.dart | 10 +- .../lib/litecoin_hardware_wallet_service.dart | 22 ++- cw_bitcoin/lib/litecoin_wallet.dart | 30 +--- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 12 +- cw_bitcoin/lib/psbt_transaction_builder.dart | 34 ++-- cw_tron/lib/tron_client.dart | 8 +- cw_tron/lib/tron_http_provider.dart | 36 ++-- cw_tron/lib/tron_wallet.dart | 43 ++--- lib/bitcoin/cw_bitcoin.dart | 18 +- .../settings/silent_payments_settings.dart | 7 - tool/configure.dart | 10 +- 20 files changed, 159 insertions(+), 341 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 3034e190de..3cc87d9284 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -109,7 +109,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { type: decoded['type'] != null && decoded['type'] != '' ? BitcoinAddressType.values .firstWhere((type) => type.toString() == decoded['type'] as String) - : SegwitAddresType.p2wpkh, + : SegwitAddressType.p2wpkh, scriptHash: decoded['scriptHash'] as String?, ); } @@ -220,7 +220,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.name = '', super.isUsed = false, required this.tweak, - super.type = SegwitAddresType.p2tr, + super.type = SegwitAddressType.p2tr, super.labelHex, }) : super(isHidden: true); @@ -285,7 +285,7 @@ class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord { super.name = '', super.isUsed = false, BasedUtxoNetwork? network, - super.type = SegwitAddresType.mweb, + super.type = SegwitAddressType.mweb, }); factory LitecoinMWEBAddressRecord.fromJSON(String jsonSource) { diff --git a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart index 15ab4f6c13..bcc3c394fb 100644 --- a/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_hardware_wallet_service.dart @@ -11,27 +11,25 @@ class BitcoinHardwareWalletService { final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + Future> getAvailableAccounts({int account = 0, int limit = 5}) async { final bitcoinLedgerApp = BitcoinLedgerApp(ledgerConnection); final masterFp = await bitcoinLedgerApp.getMasterFingerprint(); final accounts = []; - final indexRange = List.generate(limit, (i) => i + index); + final accountRange = List.generate(limit, (i) => i + account); - for (final i in indexRange) { + for (final i in accountRange) { final derivationPath = "m/84'/0'/$i'"; final xpub = await bitcoinLedgerApp.getXPubKey(derivationPath: derivationPath); - final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub) - .childKey(Bip32KeyIndex(0)) - .childKey(Bip32KeyIndex(index)); + final changeKey = Bip32KeyIndex(0); + final indexKey = Bip32KeyIndex(0); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub).childKey(changeKey).childKey(indexKey); - final address = ECPublic.fromBip32( - hd.publicKey, - ).toP2wpkhAddress().toAddress(BitcoinNetwork.mainnet); + final address = hd.toECPublic().toP2wpkhAddress(); accounts.add(HardwareAccountData( - address: address, + address: address.toAddress(BitcoinNetwork.mainnet), accountIndex: i, derivationPath: derivationPath, masterFingerprint: masterFp, diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 07083e111a..8331a182db 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -36,9 +36,9 @@ class BitcoinReceivePageOption implements ReceivePageOption { BitcoinAddressType toType() { switch (this) { case BitcoinReceivePageOption.p2tr: - return SegwitAddresType.p2tr; + return SegwitAddressType.p2tr; case BitcoinReceivePageOption.p2wsh: - return SegwitAddresType.p2wsh; + return SegwitAddressType.p2wsh; case BitcoinReceivePageOption.p2pkh: return P2pkhAddressType.p2pkh; case BitcoinReceivePageOption.p2sh: @@ -46,20 +46,20 @@ class BitcoinReceivePageOption implements ReceivePageOption { case BitcoinReceivePageOption.silent_payments: return SilentPaymentsAddresType.p2sp; case BitcoinReceivePageOption.mweb: - return SegwitAddresType.mweb; + return SegwitAddressType.mweb; case BitcoinReceivePageOption.p2wpkh: default: - return SegwitAddresType.p2wpkh; + return SegwitAddressType.p2wpkh; } } factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) { switch (type) { - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return BitcoinReceivePageOption.p2tr; - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return BitcoinReceivePageOption.p2wsh; - case SegwitAddresType.mweb: + case SegwitAddressType.mweb: return BitcoinReceivePageOption.mweb; case P2pkhAddressType.p2pkh: return BitcoinReceivePageOption.p2pkh; @@ -67,7 +67,7 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2sh; case SilentPaymentsAddresType.p2sp: return BitcoinReceivePageOption.silent_payments; - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 172665741c..83bbf2ed72 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -820,7 +820,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (paysToSilentPayment) { // Check inputs for shared secret derivation - if (utx.bitcoinAddressRecord.type == SegwitAddresType.p2wsh) { + if (utx.bitcoinAddressRecord.type == SegwitAddressType.p2wsh) { throw BitcoinTransactionSilentPaymentsNotSupported(); } } @@ -856,7 +856,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (privkey != null) { inputPrivKeyInfos.add(ECPrivateInfo( privkey, - address.type == SegwitAddresType.p2tr, + address.type == SegwitAddressType.p2tr, tweak: !isSilentPayment, )); @@ -1289,7 +1289,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { throw Exception(error); } - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { hasTaprootInputs = true; return key.privkey.signTapRoot( txDigest, @@ -1345,7 +1345,7 @@ class WalletSeedBytes { String mnemonic, [ String? passphrase, ]) async { - List? seedBytes = null; + late List seedBytes; final Map hdWallets = {}; if (walletInfo.isRecovery) { @@ -1388,18 +1388,6 @@ class WalletSeedBytes { } } - 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 WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index ac7c3669b3..b72dc839f0 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -36,7 +36,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S List get usableSilentPaymentAddresses => silentPaymentAddresses .where((addressRecord) => - addressRecord.type != SegwitAddresType.p2tr && + addressRecord.type != SegwitAddressType.p2tr && addressRecord.derivationPath == BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND) .toList(); @@ -48,13 +48,13 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override Future init() async { - await generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await generateInitialAddresses(type: SegwitAddressType.p2wpkh); if (!isHardwareWallet) { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await generateInitialAddresses(type: SegwitAddresType.p2tr); - await generateInitialAddresses(type: SegwitAddresType.p2wsh); + await generateInitialAddresses(type: SegwitAddressType.p2tr); + await generateInitialAddresses(type: SegwitAddressType.p2wsh); } if (silentPaymentAddresses.length == 0) { @@ -187,13 +187,13 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S // switch (addressType) { // case P2pkhAddressType.p2pkh: // return ECPublic.fromBip32(pub).toP2pkhAddress(); - // case SegwitAddresType.p2tr: + // case SegwitAddressType.p2tr: // return ECPublic.fromBip32(pub).toP2trAddress(); - // case SegwitAddresType.p2wsh: + // case SegwitAddressType.p2wsh: // return ECPublic.fromBip32(pub).toP2wshAddress(); // case P2shAddressType.p2wpkhInP2sh: // return ECPublic.fromBip32(pub).toP2wpkhInP2sh(); - // case SegwitAddresType.p2wpkh: + // case SegwitAddressType.p2wpkh: // return ECPublic.fromBip32(pub).toP2wpkhAddress(); // default: // throw ArgumentError('Invalid address type'); @@ -208,14 +208,14 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S isChange: isChange, index: index, ); - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: return P2trAddress.fromDerivation( bip32: hdWallet, derivationInfo: derivationInfo, isChange: isChange, index: index, ); - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: return P2wshAddress.fromDerivation( bip32: hdWallet, derivationInfo: derivationInfo, @@ -230,7 +230,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S index: index, type: P2shAddressType.p2wpkhInP2sh, ); - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: return P2wpkhAddress.fromDerivation( bip32: hdWallet, derivationInfo: derivationInfo, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4c0820c4fa..8207b3c873 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -14,7 +14,6 @@ import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:collection/collection.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_balance.dart'; @@ -466,7 +465,7 @@ abstract class ElectrumWalletBase String pubKeyHex; if (privkey != null) { - inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); + inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddressType.p2tr)); pubKeyHex = privkey.getPublic().toHex(); } else { @@ -875,7 +874,7 @@ abstract class ElectrumWalletBase throw Exception(error); } - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { hasTaprootInputs = true; return key.privkey.signTapRoot(txDigest, sighash: sighash); } else { @@ -928,7 +927,7 @@ abstract class ElectrumWalletBase 'passphrase': passphrase ?? '', 'walletAddresses': walletAddresses.toJson(), 'address_page_type': walletInfo.addressPageType == null - ? SegwitAddresType.p2wpkh.toString() + ? SegwitAddressType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, @@ -1018,16 +1017,16 @@ abstract class ElectrumWalletBase case P2shAddressType.p2pkInP2sh: address = fakePublicKey.toP2pkhInP2sh(); break; - case SegwitAddresType.p2wpkh: + case SegwitAddressType.p2wpkh: address = fakePublicKey.toP2wpkhAddress(); break; case P2shAddressType.p2pkhInP2sh: address = fakePublicKey.toP2pkhInP2sh(); break; - case SegwitAddresType.p2wsh: + case SegwitAddressType.p2wsh: address = fakePublicKey.toP2wshAddress(); break; - case SegwitAddresType.p2tr: + case SegwitAddressType.p2tr: address = fakePublicKey.toTaprootAddress(); break; default: @@ -1601,7 +1600,7 @@ abstract class ElectrumWalletBase throw Exception("Cannot find private key"); } - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { return key.signTapRoot(txDigest, sighash: sighash); } else { return key.signInput(txDigest, sigHash: sighash); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 6e816d0b81..64efd0f970 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -36,7 +36,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) - : SegwitAddresType.p2wpkh), + : SegwitAddressType.p2wpkh), super(walletInfo) { updateAddressesOnReceiveScreen(); } @@ -75,7 +75,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @observable - BitcoinAddressType changeAddressType = SegwitAddresType.p2wpkh; + BitcoinAddressType changeAddressType = SegwitAddressType.p2wpkh; @override @computed @@ -324,7 +324,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { for (final derivationType in hdWallets.keys) { // p2wpkh has always had the right derivations, skip if creating old derivations - if (OLD_DERIVATION_TYPES.contains(derivationType) && type == SegwitAddresType.p2wpkh) { + if (OLD_DERIVATION_TYPES.contains(derivationType) && type == SegwitAddressType.p2wpkh) { continue; } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 8a71e4c7fe..c21b0bb371 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -88,8 +88,8 @@ class ElectrumWalletSnapshot { final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; - var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var regularAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; + var changeAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; var silentAddressIndex = 0; final derivationType = DerivationType @@ -98,10 +98,10 @@ class ElectrumWalletSnapshot { try { regularAddressIndexByType = { - SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') + SegwitAddressType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') }; changeAddressIndexByType = { - SegwitAddresType.p2wpkh.toString(): + SegwitAddressType.p2wpkh.toString(): int.parse(data['change_address_index'] as String? ?? '0') }; silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 28906a2f8d..c728895ccd 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -20,13 +20,13 @@ import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; - ElectrumApiProvider? _electrumClient; + ElectrumProvider? _electrumClient; BehaviorSubject>? _scanningStream; BasedUtxoNetwork? _network; WalletType? _walletType; - ElectrumWorker._(this.sendPort, {ElectrumApiProvider? electrumClient}) + ElectrumWorker._(this.sendPort, {ElectrumProvider? electrumClient}) : _electrumClient = electrumClient; static void run(SendPort sendPort) { @@ -136,7 +136,7 @@ class ElectrumWorker { _walletType = request.walletType; try { - _electrumClient = await ElectrumApiProvider.connect( + _electrumClient = await ElectrumProvider.connect( request.useSSL ? ElectrumSSLService.connect( request.uri, @@ -161,7 +161,7 @@ class ElectrumWorker { } Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { - final req = ElectrumHeaderSubscribe(); + final req = ElectrumRequestHeaderSubscribe(); final stream = _electrumClient!.subscribe(req); if (stream == null) { @@ -184,9 +184,9 @@ class ElectrumWorker { ) async { await Future.wait(request.scripthashByAddress.entries.map((entry) async { final address = entry.key; - final scripthash = entry.value; + final scripthash = entry.value.toString(); - final req = ElectrumScriptHashSubscribe(scriptHash: scripthash); + final req = ElectrumRequestScriptHashSubscribe(scriptHash: scripthash); final stream = await _electrumClient!.subscribe(req); @@ -221,7 +221,7 @@ class ElectrumWorker { return; } - final history = await _electrumClient!.request(ElectrumScriptHashGetHistory( + final history = await _electrumClient!.request(ElectrumRequestScriptHashGetHistory( scriptHash: addressRecord.scriptHash, )); @@ -326,7 +326,7 @@ class ElectrumWorker { } final balanceFuture = _electrumClient!.request( - ElectrumGetScriptHashBalance(scriptHash: scripthash), + ElectrumRequestGetScriptHashBalance(scriptHash: scripthash), ); balanceFutures.add(balanceFuture); } @@ -365,7 +365,7 @@ class ElectrumWorker { final scriptHashUnspents = await _electrumClient! .request( - ElectrumScriptHashListUnspent(scriptHash: scriptHash), + ElectrumRequestScriptHashListUnspent(scriptHash: scriptHash), ) .timeout(const Duration(seconds: 3)); @@ -380,7 +380,7 @@ class ElectrumWorker { Future _handleBroadcast(ElectrumWorkerBroadcastRequest request) async { final rpcId = _electrumClient!.id + 1; final txHash = await _electrumClient!.request( - ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw), + ElectrumRequestBroadCastTransaction(transactionRaw: request.transactionRaw), ); if (txHash == null) { @@ -417,7 +417,7 @@ class ElectrumWorker { bool? isDateValidated; final transactionVerbose = await _electrumClient!.request( - ElectrumGetTransactionVerbose(transactionHash: hash), + ElectrumRequestGetTransactionVerbose(transactionHash: hash), ); String transactionHex; @@ -427,7 +427,7 @@ class ElectrumWorker { confirmations = transactionVerbose['confirmations'] as int?; } else { transactionHex = await _electrumClient!.request( - ElectrumGetTransactionHex(transactionHash: hash), + ElectrumRequestGetTransactionHex(transactionHash: hash), ); if (getTime && _walletType == WalletType.bitcoin) { @@ -456,7 +456,7 @@ class ElectrumWorker { for (final vin in original.inputs) { final inputTransactionHex = await _electrumClient!.request( // TODO: _getTXHex - ElectrumGetTransactionHex(transactionHash: vin.txId), + ElectrumRequestGetTransactionHex(transactionHash: vin.txId), ); ins.add(BtcTransaction.fromRaw(inputTransactionHex)); @@ -552,7 +552,7 @@ class ElectrumWorker { var scanningClient = _electrumClient; if (scanData.shouldSwitchNodes) { - scanningClient = await ElectrumApiProvider.connect( + scanningClient = await ElectrumProvider.connect( ElectrumTCPService.connect( // TODO: ssl Uri.parse("tcp://electrs.cakewallet.com:50001"), @@ -763,7 +763,7 @@ class ElectrumWorker { _sendResponse( ElectrumWorkerGetVersionResponse( result: await _electrumClient!.request( - ElectrumVersion( + ElectrumRequestVersion( clientName: "", protocolVersion: "1.4", ), @@ -774,137 +774,6 @@ class ElectrumWorker { } } -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; diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart index 31f9abe76d..788314debf 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -6,14 +6,14 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques this.id, }); - final Map scripthashByAddress; + final Map scripthashByAddress; final int? id; @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @override - factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { + factory ElectrumWorkerScripthashesSubscribeRequest.fromJson(Map json) { return ElectrumWorkerScripthashesSubscribeRequest( scripthashByAddress: json['scripthashes'] as Map, id: json['id'] as int?, @@ -37,7 +37,7 @@ class ElectrumWorkerScripthashesSubscribeError extends ElectrumWorkerErrorRespon } class ElectrumWorkerScripthashesSubscribeResponse - extends ElectrumWorkerResponse?, Map?> { + extends ElectrumWorkerResponse?, Map?> { ElectrumWorkerScripthashesSubscribeResponse({ required super.result, super.error, @@ -45,14 +45,14 @@ class ElectrumWorkerScripthashesSubscribeResponse }) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); @override - Map? resultJson(result) { + Map? resultJson(result) { return result; } @override factory ElectrumWorkerScripthashesSubscribeResponse.fromJson(Map json) { return ElectrumWorkerScripthashesSubscribeResponse( - result: json['result'] as Map?, + result: json['result'] as Map?, 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 c53a8713d5..9b0ac1910a 100644 --- a/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_hardware_wallet_service.dart @@ -11,29 +11,27 @@ class LitecoinHardwareWalletService { final LedgerConnection ledgerConnection; - Future> getAvailableAccounts({int index = 0, int limit = 5}) async { + Future> getAvailableAccounts({int account = 0, int limit = 5}) async { final litecoinLedgerApp = LitecoinLedgerApp(ledgerConnection); await litecoinLedgerApp.getVersion(); final accounts = []; - final indexRange = List.generate(limit, (i) => i + index); + final accountRange = List.generate(limit, (i) => i + account); final xpubVersion = Bip44Conf.litecoinMainNet.altKeyNetVer; - for (final i in indexRange) { + for (final i in accountRange) { final derivationPath = "m/84'/2'/$i'"; final xpub = await litecoinLedgerApp.getXPubKey( accountsDerivationPath: derivationPath, xPubVersion: int.parse(hex.encode(xpubVersion.public), radix: 16)); - final bip32 = - Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion).childKey(Bip32KeyIndex(0)); - - final address = P2wpkhAddress.fromDerivation( - bip32: bip32, - derivationInfo: BitcoinDerivationInfos.LITECOIN, - isChange: false, - index: 0, - ); + final changeKey = Bip32KeyIndex(0); + final indexKey = Bip32KeyIndex(0); + final hd = Bip32Slip10Secp256k1.fromExtendedKey(xpub, xpubVersion) + .childKey(changeKey) + .childKey(indexKey); + + final address = hd.toECPublic().toP2wpkhAddress(); accounts.add(HardwareAccountData( address: address.toAddress(LitecoinNetwork.mainnet), diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 593339147f..970b9b0950 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -909,9 +909,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.type == SegwitAddressType.mweb; case UnspentCoinType.nonMweb: - return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + return utx.bitcoinAddressRecord.type != SegwitAddressType.mweb; case UnspentCoinType.any: return true; } @@ -919,7 +919,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { 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.type == SegwitAddressType.mweb ? -1 : 1); for (int i = 0; i < availableInputs.length; i++) { final utx = availableInputs[i]; @@ -946,7 +946,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String pubKeyHex; if (privkey != null) { - inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); + inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddressType.p2tr)); pubKeyHex = privkey.getPublic().toHex(); } else { @@ -1191,9 +1191,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? memo, required int feeRate, }) async { - final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddresType.mweb); + final spendsMweb = utxos.any((utxo) => utxo.utxo.scriptType == SegwitAddressType.mweb); final paysToMweb = outputs - .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb); + .any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddressType.mweb); if (!spendsMweb && !paysToMweb) { return super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate); } @@ -1360,7 +1360,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { throw Exception(error); } - if (utxo.utxo.isP2tr()) { + if (utxo.utxo.isP2tr) { hasTaprootInputs = true; return key.privkey.signTapRoot(txDigest, sighash: sighash); } else { @@ -1437,7 +1437,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // check if mweb inputs are used: for (final utxo in tx.utxos) { - if (utxo.utxo.scriptType == SegwitAddresType.mweb) { + if (utxo.utxo.scriptType == SegwitAddressType.mweb) { hasMwebInput = true; } } @@ -1768,7 +1768,7 @@ class WalletSeedBytes { String mnemonic, [ String? passphrase, ]) async { - List? seedBytes = null; + late List seedBytes; final Map hdWallets = {}; if (walletInfo.isRecovery) { @@ -1803,18 +1803,6 @@ class WalletSeedBytes { } } - 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 WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); } } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 59ebb6a298..fece7bfee0 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -52,9 +52,9 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with Future init() async { if (!super.isHardwareWallet) await initMwebAddresses(); - await generateInitialAddresses(type: SegwitAddresType.p2wpkh); + await generateInitialAddresses(type: SegwitAddressType.p2wpkh); if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) { - await generateInitialAddresses(type: SegwitAddresType.mweb); + await generateInitialAddresses(type: SegwitAddressType.mweb); } await super.init(); @@ -87,7 +87,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with derivationInfo: derivationInfo, ); - if (addressType == SegwitAddresType.mweb) { + if (addressType == SegwitAddressType.mweb) { final address = LitecoinMWEBAddressRecord(addressString, index: i); mwebAddresses.add(address); } else { @@ -176,7 +176,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) { - if (addressType == SegwitAddresType.mweb) { + if (addressType == SegwitAddressType.mweb) { return MwebAddress.fromAddress(address: mwebAddrs[isChange ? index + 1 : 0]); } @@ -196,7 +196,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required BitcoinAddressType addressType, required BitcoinDerivationInfo derivationInfo, }) async { - if (addressType == SegwitAddresType.mweb) { + if (addressType == SegwitAddressType.mweb) { await ensureMwebAddressUpToIndexExists(index); } @@ -268,7 +268,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.type == SegwitAddressType.p2wpkh && !element.isUsed); return addresses.first.address; } diff --git a/cw_bitcoin/lib/psbt_transaction_builder.dart b/cw_bitcoin/lib/psbt_transaction_builder.dart index 8cb9797300..5a3b89c512 100644 --- a/cw_bitcoin/lib/psbt_transaction_builder.dart +++ b/cw_bitcoin/lib/psbt_transaction_builder.dart @@ -9,7 +9,9 @@ class PSBTTransactionBuild { final PsbtV2 psbt = PsbtV2(); PSBTTransactionBuild( - {required List inputs, required List outputs, bool enableRBF = true}) { + {required List inputs, + required List outputs, + bool enableRBF = true}) { psbt.setGlobalTxVersion(2); psbt.setGlobalInputCount(inputs.length); psbt.setGlobalOutputCount(outputs.length); @@ -17,20 +19,20 @@ class PSBTTransactionBuild { for (var i = 0; i < inputs.length; i++) { final input = inputs[i]; - printV(input.utxo.isP2tr()); - printV(input.utxo.isSegwit()); - printV(input.utxo.isP2shSegwit()); + printV(input.utxo.isP2tr); + printV(input.utxo.isSegwit); + printV(input.utxo.isP2shSegwit); - psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); + psbt.setInputPreviousTxId( + i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); psbt.setInputOutputIndex(i, input.utxo.vout); psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff); - - if (input.utxo.isSegwit()) { + if (input.utxo.isSegwit) { setInputSegwit(i, input); - } else if (input.utxo.isP2shSegwit()) { + } else if (input.utxo.isP2shSegwit) { setInputP2shSegwit(i, input); - } else if (input.utxo.isP2tr()) { + } else if (input.utxo.isP2tr) { // ToDo: (Konsti) Handle Taproot Inputs } else { setInputP2pkh(i, input); @@ -49,20 +51,14 @@ class PSBTTransactionBuild { void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) { psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); - psbt.setInputBip32Derivation( - i, - Uint8List.fromList(hex.decode(input.ownerPublicKey)), - input.ownerMasterFingerprint, - BIPPath.fromString(input.ownerDerivationPath).toPathArray()); + psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), + input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); } void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) { psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); - psbt.setInputBip32Derivation( - i, - Uint8List.fromList(hex.decode(input.ownerPublicKey)), - input.ownerMasterFingerprint, - BIPPath.fromString(input.ownerDerivationPath).toPathArray()); + psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), + input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)), Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart index 8eca02af64..bd13e5fd97 100644 --- a/cw_tron/lib/tron_client.dart +++ b/cw_tron/lib/tron_client.dart @@ -235,7 +235,7 @@ class TronClient { String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; String constantAmount = '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets. - final contract = ContractABI.fromJson(trc20Abi, isTron: true); + final contract = ContractABI.fromJson(trc20Abi); final function = contract.functionFromName("transfer"); @@ -405,7 +405,7 @@ class TronClient { String contractAddress, BigInt tronBalance, ) async { - final contract = ContractABI.fromJson(trc20Abi, isTron: true); + final contract = ContractABI.fromJson(trc20Abi); final function = contract.functionFromName("transfer"); @@ -483,7 +483,7 @@ class TronClient { final tokenAddress = TronAddress(contractAddress); - final contract = ContractABI.fromJson(trc20Abi, isTron: true); + final contract = ContractABI.fromJson(trc20Abi); final function = contract.functionFromName("balanceOf"); @@ -510,7 +510,7 @@ class TronClient { final ownerAddress = TronAddress(userAddress); - final contract = ContractABI.fromJson(trc20Abi, isTron: true); + final contract = ContractABI.fromJson(trc20Abi); final name = (await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? ''; diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart index 8a3301f878..ea06ba0978 100644 --- a/cw_tron/lib/tron_http_provider.dart +++ b/cw_tron/lib/tron_http_provider.dart @@ -1,8 +1,5 @@ -import 'dart:convert'; - import 'package:http/http.dart' as http; import 'package:on_chain/tron/tron.dart'; -import '.secrets.g.dart' as secrets; class TronHTTPProvider implements TronServiceProvider { TronHTTPProvider( @@ -10,34 +7,23 @@ class TronHTTPProvider implements TronServiceProvider { http.Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)}) : client = client ?? http.Client(); - @override + final String url; final http.Client client; final Duration defaultRequestTimeout; @override - Future> get(TronRequestDetails params, [Duration? timeout]) async { - final response = await client.get(Uri.parse(params.url(url)), headers: { - 'Content-Type': 'application/json', - if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, - if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, - }).timeout(timeout ?? defaultRequestTimeout); - final data = json.decode(response.body) as Map; - return data; - } - - @override - Future> post(TronRequestDetails params, [Duration? timeout]) async { + Future> doRequest(TronRequestDetails params, + {Duration? timeout}) async { + if (params.type.isPostRequest) { + final response = await client + .post(params.toUri(url), headers: params.headers, body: params.body()) + .timeout(timeout ?? defaultRequestTimeout); + return params.toResponse(response.bodyBytes, response.statusCode); + } final response = await client - .post(Uri.parse(params.url(url)), - headers: { - 'Content-Type': 'application/json', - if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, - if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, - }, - body: params.toRequestBody()) + .get(params.toUri(url), headers: params.headers) .timeout(timeout ?? defaultRequestTimeout); - final data = json.decode(response.body) as Map; - return data; + return params.toResponse(response.bodyBytes, response.statusCode); } } diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart index 9217603415..9c85d55a5a 100644 --- a/cw_tron/lib/tron_wallet.dart +++ b/cw_tron/lib/tron_wallet.dart @@ -31,7 +31,7 @@ import 'package:cw_tron/tron_transaction_info.dart'; import 'package:cw_tron/tron_wallet_addresses.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:on_chain/on_chain.dart'; +import 'package:on_chain/on_chain.dart' as on_chain; part 'tron_wallet.g.dart'; @@ -74,13 +74,13 @@ abstract class TronWalletBase late final Box tronTokensBox; - late final TronPrivateKey _tronPrivateKey; + late final on_chain.TronPrivateKey _tronPrivateKey; - late final TronPublicKey _tronPublicKey; + late final on_chain.TronPublicKey _tronPublicKey; - TronPublicKey get tronPublicKey => _tronPublicKey; + on_chain.TronPublicKey get tronPublicKey => _tronPublicKey; - TronPrivateKey get tronPrivateKey => _tronPrivateKey; + on_chain.TronPrivateKey get tronPrivateKey => _tronPrivateKey; late String _tronAddress; @@ -190,7 +190,7 @@ abstract class TronWalletBase String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; - Future getPrivateKey({ + Future getPrivateKey({ String? mnemonic, String? privateKey, required String password, @@ -198,7 +198,7 @@ abstract class TronWalletBase }) async { assert(mnemonic != null || privateKey != null); - if (privateKey != null) return TronPrivateKey(privateKey); + if (privateKey != null) return on_chain.TronPrivateKey(privateKey); final seed = bip39.mnemonicToSeed(mnemonic!, passphrase: passphrase ?? ''); @@ -207,7 +207,7 @@ abstract class TronWalletBase final childKey = bip44.deriveDefaultPath; - return TronPrivateKey.fromBytes(childKey.privateKey.raw); + return on_chain.TronPrivateKey.fromBytes(childKey.privateKey.raw); } @override @@ -242,10 +242,10 @@ abstract class TronWalletBase Future _getEstimatedFees() async { final nativeFee = await _getNativeTxFee(); - nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee)); + nativeTxEstimatedFee = on_chain.TronHelper.fromSun(BigInt.from(nativeFee)); final trc20Fee = await _getTrc20TxFee(); - trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee)); + trc20EstimatedFee = on_chain.TronHelper.fromSun(BigInt.from(trc20Fee)); log('Native Estimated Fee: $nativeTxEstimatedFee'); log('TRC20 Estimated Fee: $trc20EstimatedFee'); @@ -323,7 +323,7 @@ abstract class TronWalletBase totalAmount = walletBalanceForCurrency; } else { final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); - totalAmount = TronHelper.toSun(totalOriginalAmount.toString()); + totalAmount = on_chain.TronHelper.toSun(totalOriginalAmount.toString()); } if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) { @@ -338,7 +338,7 @@ abstract class TronWalletBase toAddress: tronCredentials.outputs.first.isParsedAddress ? tronCredentials.outputs.first.extractedAddress! : tronCredentials.outputs.first.address, - amount: TronHelper.fromSun(totalAmount), + amount: on_chain.TronHelper.fromSun(totalAmount), currency: transactionCurrency, tronBalance: tronBalance, sendAll: shouldSendAll, @@ -355,9 +355,9 @@ abstract class TronWalletBase final Map result = {}; - final contract = ContractABI.fromJson(trc20Abi, isTron: true); + final contract = on_chain.ContractABI.fromJson(trc20Abi); - final ownerAddress = TronAddress(_tronAddress); + final ownerAddress = on_chain.TronAddress(_tronAddress); for (var transactionModel in transactions) { if (transactionModel.isError) { @@ -371,7 +371,7 @@ abstract class TronWalletBase String? tokenSymbol; if (transactionModel.contractAddress != null) { - final tokenAddress = TronAddress(transactionModel.contractAddress!); + final tokenAddress = on_chain.TronAddress(transactionModel.contractAddress!); tokenSymbol = (await _client.getTokenDetail( contract, @@ -385,9 +385,10 @@ abstract class TronWalletBase result[transactionModel.hash] = TronTransactionInfo( id: transactionModel.hash, tronAmount: transactionModel.amount ?? BigInt.zero, - direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address - ? TransactionDirection.outgoing - : TransactionDirection.incoming, + direction: + on_chain.TronAddress(transactionModel.from!, visible: false).toAddress() == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, blockTime: transactionModel.date, txFee: transactionModel.fee, tokenSymbol: tokenSymbol ?? "TRX", @@ -595,11 +596,13 @@ abstract class TronWalletBase if (address == null) { return false; } - TronPublicKey pubKey = TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!; + on_chain.TronPublicKey pubKey = + on_chain.TronPublicKey.fromPersonalSignature(ascii.encode(message), signature)!; return pubKey.toAddress().toString() == address; } - String getTronBase58AddressFromHex(String hexAddress) => TronAddress(hexAddress).toAddress(); + String getTronBase58AddressFromHex(String hexAddress) => + on_chain.TronAddress(hexAddress).toAddress(); void updateScanProviderUsageState(bool isEnabled) { if (isEnabled) { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 5b58201dd3..a3a3af7996 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -240,9 +240,9 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.unspentCoins.where((element) { switch (coinTypeToSpendFrom) { case UnspentCoinType.mweb: - return element.bitcoinAddressRecord.type == SegwitAddresType.mweb; + return element.bitcoinAddressRecord.type == SegwitAddressType.mweb; case UnspentCoinType.nonMweb: - return element.bitcoinAddressRecord.type != SegwitAddresType.mweb; + return element.bitcoinAddressRecord.type != SegwitAddressType.mweb; case UnspentCoinType.any: return true; } @@ -345,14 +345,14 @@ class CWBitcoin extends Bitcoin { case BitcoinReceivePageOption.p2sh: return P2shAddressType.p2wpkhInP2sh; case BitcoinReceivePageOption.p2tr: - return SegwitAddresType.p2tr; + return SegwitAddressType.p2tr; case BitcoinReceivePageOption.p2wsh: - return SegwitAddresType.p2wsh; + return SegwitAddressType.p2wsh; case BitcoinReceivePageOption.mweb: - return SegwitAddresType.mweb; + return SegwitAddressType.mweb; case BitcoinReceivePageOption.p2wpkh: default: - return SegwitAddresType.p2wpkh; + return SegwitAddressType.p2wpkh; } } @@ -573,7 +573,7 @@ class CWBitcoin extends Bitcoin { {int index = 0, int limit = 5}) async { final hardwareWalletService = BitcoinHardwareWalletService(ledgerVM.connection); try { - return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + return hardwareWalletService.getAvailableAccounts(account: index, limit: limit); } catch (err) { printV(err); throw err; @@ -585,7 +585,7 @@ class CWBitcoin extends Bitcoin { {int index = 0, int limit = 5}) async { final hardwareWalletService = LitecoinHardwareWalletService(ledgerVM.connection); try { - return hardwareWalletService.getAvailableAccounts(index: index, limit: limit); + return hardwareWalletService.getAvailableAccounts(account: index, limit: limit); } catch (err) { printV(err); throw err; @@ -793,7 +793,7 @@ class CWBitcoin extends Bitcoin { try { final electrumWallet = wallet as ElectrumWallet; final segwitAddress = electrumWallet.walletAddresses.allAddresses - .firstWhere((element) => !element.isUsed && element.type == SegwitAddresType.p2wpkh); + .firstWhere((element) => !element.isUsed && element.type == SegwitAddressType.p2wpkh); return segwitAddress.address; } catch (_) { return null; diff --git a/lib/src/screens/settings/silent_payments_settings.dart b/lib/src/screens/settings/silent_payments_settings.dart index ebf952a565..bc0ecece1c 100644 --- a/lib/src/screens/settings/silent_payments_settings.dart +++ b/lib/src/screens/settings/silent_payments_settings.dart @@ -37,13 +37,6 @@ 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/tool/configure.dart b/tool/configure.dart index 793f460dad..da80a7b065 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -125,16 +125,16 @@ import 'package:mobx/mobx.dart'; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ const List BITCOIN_ADDRESS_TYPES = [ - SegwitAddresType.p2wpkh, + SegwitAddressType.p2wpkh, P2pkhAddressType.p2pkh, - SegwitAddresType.p2tr, - SegwitAddresType.p2wsh, + SegwitAddressType.p2tr, + SegwitAddressType.p2wsh, P2shAddressType.p2wpkhInP2sh, ]; const List LITECOIN_ADDRESS_TYPES = [ - SegwitAddresType.p2wpkh, - SegwitAddresType.mweb, + SegwitAddressType.p2wpkh, + SegwitAddressType.mweb, ]; const List BITCOIN_CASH_ADDRESS_TYPES = [ From d7d12f002e02063d1bc317a509e4693b41a1cf20 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 14 Jan 2025 10:13:37 -0300 Subject: [PATCH 44/64] fix: review comments and backward compatibility --- cw_bitcoin/lib/bitcoin_address_record.dart | 43 +++++++---- cw_bitcoin/lib/bitcoin_wallet.dart | 45 ++++++------ cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 73 +++++++++---------- .../lib/electrum_transaction_history.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 3 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 63 ++++++++++------ cw_bitcoin/lib/electrum_wallet_snapshot.dart | 12 ++- .../lib/electrum_worker/electrum_worker.dart | 3 +- cw_bitcoin/lib/litecoin_wallet.dart | 28 ++++++- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 2 +- lib/bitcoin/cw_bitcoin.dart | 4 +- .../wallet_address_list_view_model.dart | 8 +- 12 files changed, 169 insertions(+), 117 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 3cc87d9284..d8c3715ab1 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -156,42 +156,50 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { } class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { - final String derivationPath; + String _derivationPath; + String get derivationPath => _derivationPath; int get labelIndex => index; final String? labelHex; - static bool isChangeAddress(int labelIndex) => labelIndex == 0; + static bool isPrimaryAddress(int labelIndex) => labelIndex == 0; BitcoinSilentPaymentAddressRecord( super.address, { required int labelIndex, - this.derivationPath = BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, + String derivationPath = BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, super.txCount = 0, super.balance = 0, super.name = '', super.isUsed = false, super.type = SilentPaymentsAddresType.p2sp, + required super.isChange, super.isHidden, this.labelHex, - }) : super(index: labelIndex, isChange: isChangeAddress(labelIndex)) { - if (labelIndex != 1 && labelHex == null) { + }) : _derivationPath = derivationPath, + super(index: labelIndex) { + if (labelIndex != 0 && labelHex == null) { throw ArgumentError('label must be provided for silent address index != 1'); } + + if (labelIndex != 0) { + _derivationPath = _derivationPath.replaceAll(RegExp(r'\d\/?$'), '$labelIndex'); + } } - factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource, - {BasedUtxoNetwork? network}) { + factory BitcoinSilentPaymentAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, derivationPath: decoded['derivationPath'] as String, - labelIndex: decoded['labelIndex'] as int, + labelIndex: decoded['index'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, name: decoded['name'] as String? ?? '', balance: decoded['balance'] as int? ?? 0, labelHex: decoded['labelHex'] as String?, + isChange: decoded['isChange'] as bool? ?? false, + isHidden: decoded['isHidden'] as bool?, ); } @@ -199,13 +207,15 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { String toJSON() => json.encode({ 'address': address, 'derivationPath': derivationPath, - 'labelIndex': labelIndex, + 'index': labelIndex, 'isUsed': isUsed, 'txCount': txCount, 'name': name, 'balance': balance, 'type': type.toString(), 'labelHex': labelHex, + 'isChange': isChange, + 'isHidden': isHidden, }); } @@ -220,9 +230,9 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.name = '', super.isUsed = false, required this.tweak, - super.type = SegwitAddressType.p2tr, + required super.isChange, super.labelHex, - }) : super(isHidden: true); + }) : super(isHidden: true, type: SegwitAddressType.p2tr); SilentPaymentOwner getSPWallet( List silentPaymentsWallets, [ @@ -245,7 +255,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { .tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(tweak))); } - factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinReceivedSPAddressRecord( @@ -256,14 +266,15 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { name: decoded['name'] as String? ?? '', balance: decoded['balance'] as int? ?? 0, labelHex: decoded['label'] as String?, - tweak: decoded['tweak'] as String? ?? '', + tweak: decoded['tweak'] as String? ?? decoded['silent_payment_tweak'] as String? ?? '', + isChange: decoded['isChange'] as bool? ?? false, ); } @override String toJSON() => json.encode({ 'address': address, - 'labelIndex': labelIndex, + 'index': labelIndex, 'isUsed': isUsed, 'txCount': txCount, 'name': name, @@ -271,6 +282,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { 'type': type.toString(), 'labelHex': labelHex, 'tweak': tweak, + 'isChange': isChange, }); } @@ -285,8 +297,7 @@ class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord { super.name = '', super.isUsed = false, BasedUtxoNetwork? network, - super.type = SegwitAddressType.mweb, - }); + }) : super(type: SegwitAddressType.mweb); factory LitecoinMWEBAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 83bbf2ed72..d65530d93b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -1348,37 +1348,40 @@ class WalletSeedBytes { late List seedBytes; final Map hdWallets = {}; - if (walletInfo.isRecovery) { - for (final derivation in walletInfo.derivations ?? []) { - if (derivation.derivationType == DerivationType.bip39) { - try { - seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("bip39 seed error: $e"); - } + for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { + if (derivation.description?.contains("SP") ?? false) { + continue; + } - continue; + if (derivation.derivationType == DerivationType.bip39) { + try { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("bip39 seed error: $e"); } - if (derivation.derivationType == DerivationType.electrum) { + continue; + } + + if (derivation.derivationType == DerivationType.electrum) { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v2 seed error: $e"); + try { - seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = - Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("electrum_v1 seed error: $e"); - } + printV("electrum_v1 seed error: $e"); } } } + } + if (walletInfo.isRecovery) { if (hdWallets[CWBitcoinDerivationType.bip39] != null) { hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index b72dc839f0..96ee29fd66 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -9,6 +9,8 @@ part 'bitcoin_wallet_addresses.g.dart'; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; +const OLD_SP_SPEND_PATH = "m/352'/1'/0'/0'/0"; + abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { @@ -34,12 +36,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final ObservableList silentPaymentAddresses; final ObservableList receivedSPAddresses; - List get usableSilentPaymentAddresses => silentPaymentAddresses - .where((addressRecord) => - addressRecord.type != SegwitAddressType.p2tr && - addressRecord.derivationPath == BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND) - .toList(); - @observable List silentPaymentWallets = []; @@ -50,49 +46,34 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future init() async { await generateInitialAddresses(type: SegwitAddressType.p2wpkh); + silentPaymentAddresses.clear(); + if (!isHardwareWallet) { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); await generateInitialAddresses(type: SegwitAddressType.p2tr); await generateInitialAddresses(type: SegwitAddressType.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; + if (walletInfo.isRecovery) { + final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); + final oldSpendPath = Bip32PathParser.parse("m/352'/1'/0'/0'/0"); - 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, + final oldSilentPaymentWallet = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate(hdWallet.derive(oldScanPath).privateKey), + b_spend: ECPrivate(hdWallet.derive(oldSpendPath).privateKey), ); - silentPaymentWallets.add(oldSilentPaymentWallet); + silentPaymentWallets.add(oldSilentPaymentWallet); silentPaymentAddresses.addAll( [ BitcoinSilentPaymentAddressRecord( oldSilentPaymentWallet.toString(), - labelIndex: 1, + labelIndex: 0, name: "", type: SilentPaymentsAddresType.p2sp, derivationPath: oldSpendPath.toString(), isHidden: true, + isChange: false, ), BitcoinSilentPaymentAddressRecord( oldSilentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), @@ -102,6 +83,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S type: SilentPaymentsAddresType.p2sp, derivationPath: oldSpendPath.toString(), isHidden: true, + isChange: true, ), ], ); @@ -109,10 +91,11 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S silentPaymentAddresses.addAll([ BitcoinSilentPaymentAddressRecord( - silentPaymentWallet.toString(), - labelIndex: 1, + silentPaymentWallet!.toString(), + labelIndex: 0, name: "", type: SilentPaymentsAddresType.p2sp, + isChange: false, ), BitcoinSilentPaymentAddressRecord( silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), @@ -120,8 +103,11 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S labelIndex: 0, labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), type: SilentPaymentsAddresType.p2sp, + isChange: true, ), ]); + + updateHiddenAddresses(); } await updateAddressesInBox(); @@ -246,18 +232,25 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { if (addressPageType == SilentPaymentsAddresType.p2sp) { - final currentSPLabelIndex = usableSilentPaymentAddresses.length - 1; + final usableSilentPaymentAddresses = silentPaymentAddresses + .where((a) => + a.type != SegwitAddressType.p2tr && + a.derivationPath != OLD_SP_SPEND_PATH && + a.isChange == false) + .toList(); + final nextSPLabelIndex = usableSilentPaymentAddresses.length; final address = BitcoinSilentPaymentAddressRecord( - silentPaymentWallet!.toLabeledSilentPaymentAddress(currentSPLabelIndex).toString(), - labelIndex: currentSPLabelIndex, + silentPaymentWallet!.toLabeledSilentPaymentAddress(nextSPLabelIndex).toString(), + labelIndex: nextSPLabelIndex, name: label, - labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(currentSPLabelIndex)), + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(nextSPLabelIndex)), type: SilentPaymentsAddresType.p2sp, + isChange: false, ); silentPaymentAddresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesOnReceiveScreen()); + updateAddressesOnReceiveScreen(); return address; } @@ -367,7 +360,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @action void updateHiddenAddresses() { super.updateHiddenAddresses(); - this.hiddenAddresses.addAll(silentPaymentAddresses + hiddenAddresses.addAll(silentPaymentAddresses .where((addressRecord) => addressRecord.isHidden) .map((addressRecord) => addressRecord.address)); } diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index c70757fe56..75e9d13896 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -89,7 +89,7 @@ abstract class ElectrumTransactionHistoryBase _height = content['height'] as int; } catch (e) { - printV(e); + // printV(e); } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 8207b3c873..23721f92a2 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -134,7 +134,7 @@ abstract class ElectrumWalletBase @action Future handleWorkerResponse(dynamic message) async { - printV('Main: received message: $message'); + // printV('Main: received message: $message'); Map messageJson; if (message is String) { @@ -1314,6 +1314,7 @@ abstract class ElectrumWalletBase ].contains(addressRecord.cwDerivationType), ), ); + walletAddresses.updateAdresses(newAddresses); final newAddressList = (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 64efd0f970..deb4518add 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -141,6 +141,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return acc; }); + CWBitcoinDerivationType getHDWalletType() { + if (hdWallets.containsKey(CWBitcoinDerivationType.bip39)) { + return CWBitcoinDerivationType.bip39; + } else if (hdWallets.containsKey(CWBitcoinDerivationType.electrum)) { + return CWBitcoinDerivationType.electrum; + } else { + return hdWallets.keys.first; + } + } + @override Future init() async { updateAddressesOnReceiveScreen(); @@ -166,12 +176,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { final newAddressIndex = addressesOnReceiveScreen.fold( - 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc); + 0, + (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc, + ); final derivationInfo = BitcoinAddressUtils.getDerivationFromType(addressPageType); final address = BitcoinAddressRecord( getAddress( - derivationType: CWBitcoinDerivationType.bip39, + derivationType: getHDWalletType(), isChange: false, index: newAddressIndex, addressType: addressPageType, @@ -183,10 +195,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { type: addressPageType, network: network, derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), - cwDerivationType: CWBitcoinDerivationType.bip39, + cwDerivationType: getHDWalletType(), ); - _allAddresses.add(address); - Future.delayed(Duration.zero, () => updateAddressesOnReceiveScreen()); + addAddresses([address]); return address; } @@ -303,7 +314,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ), index: i, isChange: isChange, - isHidden: OLD_DERIVATION_TYPES.contains(derivationType), + isHidden: OLD_DERIVATION_TYPES.contains(derivationType) || isChange, type: addressType, network: network, derivationInfo: derivationInfo, @@ -312,16 +323,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { newAddresses.add(address); } - addAddresses(newAddresses); return newAddresses; } @action Future generateInitialAddresses({required BitcoinAddressType type}) async { - if (_allAddresses.where((addr) => addr.type == type).isNotEmpty) { - return; - } - for (final derivationType in hdWallets.keys) { // p2wpkh has always had the right derivations, skip if creating old derivations if (OLD_DERIVATION_TYPES.contains(derivationType) && type == SegwitAddressType.p2wpkh) { @@ -342,18 +348,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { scriptType: type, ); - await discoverNewAddresses( + final newReceiveAddresses = await discoverNewAddresses( derivationType: derivationType, isChange: false, addressType: type, derivationInfo: bitcoinDerivationInfo, ); - await discoverNewAddresses( + updateAdresses(newReceiveAddresses); + + final newChangeAddresses = await discoverNewAddresses( derivationType: derivationType, isChange: true, addressType: type, derivationInfo: bitcoinDerivationInfo, ); + updateAdresses(newChangeAddresses); + continue; } @@ -364,28 +374,39 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { scriptType: type, ); - await discoverNewAddresses( + final newReceiveAddresses = await discoverNewAddresses( derivationType: derivationType, isChange: false, addressType: type, derivationInfo: bitcoinDerivationInfo, ); - await discoverNewAddresses( + updateAdresses(newReceiveAddresses); + + final newChangeAddresses = await discoverNewAddresses( derivationType: derivationType, isChange: true, addressType: type, derivationInfo: bitcoinDerivationInfo, ); + updateAdresses(newChangeAddresses); } } } @action - void updateAdresses(Iterable addresses) { - for (final address in addresses) { + void updateAdresses(Iterable newAddresses) { + final replacedAddresses = newAddresses.toList(); + for (final address in newAddresses) { final index = _allAddresses.indexWhere((element) => element.address == address.address); - _allAddresses.replaceRange(index, index + 1, [address]); + if (index >= 0) { + _allAddresses.replaceRange(index, index + 1, [address]); + replacedAddresses.remove(address); + } + } + if (replacedAddresses.isNotEmpty) { + _allAddresses.addAll(replacedAddresses); + } else { updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); @@ -395,13 +416,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void addAddresses(Iterable addresses) { this._allAddresses.addAll(addresses); + + updateHiddenAddresses(); updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); - - this.hiddenAddresses.addAll(addresses - .where((addressRecord) => addressRecord.isHidden) - .map((addressRecord) => addressRecord.address)); } @action diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index c21b0bb371..adeb57f01d 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -73,10 +73,14 @@ class ElectrumWalletSnapshot { .toList(); final silentAddressesTmp = data['silent_addresses'] as List? ?? []; - final silentAddresses = silentAddressesTmp - .whereType() - .map((addr) => BitcoinSilentPaymentAddressRecord.fromJSON(addr, network: network)) - .toList(); + final silentAddresses = silentAddressesTmp.whereType().map((j) { + final decoded = json.decode(jsonSource) as Map; + if (decoded['tweak'] != null || decoded['silent_payment_tweak'] != null) { + return BitcoinReceivedSPAddressRecord.fromJSON(j); + } else { + return BitcoinSilentPaymentAddressRecord.fromJSON(j); + } + }).toList(); final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; final mwebAddresses = mwebAddressTmp diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index c728895ccd..e290a940eb 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -47,7 +47,7 @@ class ElectrumWorker { } void handleMessage(dynamic message) async { - printV("Worker: received message: $message"); + // printV("Worker: received message: $message"); try { Map messageJson; @@ -705,6 +705,7 @@ class ElectrumWorker { final receivedAddressRecord = BitcoinReceivedSPAddressRecord( receivingOutputAddress, labelIndex: 1, // TODO: get actual index/label + isChange: false, // and if change or not isUsed: true, tweak: t_k, txCount: 1, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 970b9b0950..47498a2498 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -19,7 +19,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.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_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; @@ -33,7 +32,6 @@ 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_bitcoin/litecoin_wallet_addresses.dart'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:flutter/foundation.dart'; @@ -1771,8 +1769,8 @@ class WalletSeedBytes { late List seedBytes; final Map hdWallets = {}; - if (walletInfo.isRecovery) { - for (final derivation in walletInfo.derivations ?? []) { + if (walletInfo.derivations != null) { + for (final derivation in walletInfo.derivations!) { if (derivation.derivationType == DerivationType.bip39) { try { seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); @@ -1801,6 +1799,28 @@ class WalletSeedBytes { } } } + } 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; + } + } + + if (walletInfo.isRecovery) { + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } } return WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index fece7bfee0..a5313fab38 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -95,7 +95,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with addressString, index: i, isChange: isChange, - isHidden: OLD_DERIVATION_TYPES.contains(derivationType), + isHidden: OLD_DERIVATION_TYPES.contains(derivationType) || isChange, type: addressType, network: network, derivationInfo: derivationInfo, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a3a3af7996..f941c196ed 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -600,9 +600,7 @@ class CWBitcoin extends Bitcoin { id: addr.index, name: addr.name, address: addr.address, - derivationPath: Bip32PathParser.parse(addr.derivationPath) - .addElem(Bip32KeyIndex(addr.index)) - .toString(), + derivationPath: addr.derivationPath, txCount: addr.txCount, balance: addr.balance, isChange: addr.isChange, 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 9d8136d75e..08de415454 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 @@ -270,7 +270,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed WalletAddressListItem get address { if (_addressType != null) { - final shouldForceSP = _addressType != null && bitcoin!.isReceiveOptionSP(_addressType!); + final shouldForceSP = bitcoin!.isReceiveOptionSP(_addressType); if (shouldForceSP) { return WalletAddressListItem( address: bitcoin!.getSilentPaymentAddresses(wallet).first.address, @@ -369,8 +369,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } if (isElectrumWallet) { - final hasSelectedSP = bitcoin!.hasSelectedSilentPayments(wallet); - final shouldForceSP = _addressType != null && bitcoin!.isReceiveOptionSP(_addressType!); + final isBitcoinWallet = wallet.type == WalletType.bitcoin; + final hasSelectedSP = isBitcoinWallet && bitcoin!.hasSelectedSilentPayments(wallet); + final shouldForceSP = + isBitcoinWallet && _addressType != null && bitcoin!.isReceiveOptionSP(_addressType); if (hasSelectedSP || shouldForceSP) { final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) { From 73fe865e7b1a54b439b7fc6145c3140657167a4a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 14 Jan 2025 10:59:35 -0300 Subject: [PATCH 45/64] chore: deps --- cw_bitcoin/pubspec.yaml | 6 +++--- cw_bitcoin_cash/pubspec.yaml | 4 ++-- cw_tron/pubspec.yaml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 9b606066ad..f1884cda83 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-v3 + ref: cake-update-v4 cw_mweb: path: ../cw_mweb grpc: ^3.2.4 sp_scanner: git: url: https://github.com/cake-tech/sp_scanner.git - ref: cake-update-v4 + ref: sp_v4.0.0 bech32: git: url: https://github.com/cake-tech/bech32.git @@ -68,7 +68,7 @@ dependency_overrides: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + ref: cake-update-v4 pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index f6264564c4..583bf44b5a 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-v3 + ref: cake-update-v4 dev_dependencies: flutter_test: @@ -46,7 +46,7 @@ dependency_overrides: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + ref: cake-update-v4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml index 9da2217bb7..67faede53f 100644 --- a/cw_tron/pubspec.yaml +++ b/cw_tron/pubspec.yaml @@ -18,11 +18,11 @@ dependencies: on_chain: git: url: https://github.com/cake-tech/on_chain.git - ref: cake-update-v3 + ref: cake-update-v4 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + ref: cake-update-v4 mobx: ^2.3.0+1 bip39: ^1.0.6 hive: ^2.2.3 @@ -39,7 +39,7 @@ dependency_overrides: blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v3 + ref: cake-update-v4 flutter: # assets: From 91f0f8734694434356746d9440df913001d648e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?rafael=20x=C9=B1r?= Date: Tue, 14 Jan 2025 11:43:59 -0300 Subject: [PATCH 46/64] Update pubspec.yaml --- cw_bitcoin/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index f1884cda83..4d018084e2 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: sp_scanner: git: url: https://github.com/cake-tech/sp_scanner.git - ref: sp_v4.0.0 + ref: cake-update-v4 bech32: git: url: https://github.com/cake-tech/bech32.git From 83ba77052cd9cd83cb24d0d90ca5c7f2cfa55223 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 14 Jan 2025 17:57:59 -0300 Subject: [PATCH 47/64] refactor: reviewing [skip ci] --- cw_bitcoin/lib/bitcoin_address_record.dart | 100 +++++++-------- cw_bitcoin/lib/bitcoin_unspent.dart | 11 +- cw_bitcoin/lib/bitcoin_wallet.dart | 121 ++++++------------ cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 64 ++++++++- .../bitcoin_wallet_creation_credentials.dart | 33 ++--- cw_bitcoin/lib/bitcoin_wallet_service.dart | 13 +- cw_bitcoin/lib/electrum_wallet.dart | 18 +-- .../lib/electrum_worker/methods/get_fees.dart | 5 +- .../electrum_worker/methods/get_history.dart | 2 +- .../methods/get_tx_expanded.dart | 2 +- cw_bitcoin/lib/litecoin_wallet.dart | 80 +----------- cw_bitcoin/lib/litecoin_wallet_service.dart | 13 +- cw_bitcoin/lib/wallet_seed_bytes.dart | 59 +++++++++ cw_bitcoin/pubspec.lock | 12 +- cw_bitcoin/pubspec.yaml | 2 +- .../lib/src/bitcoin_cash_wallet.dart | 6 - .../lib/src/bitcoin_cash_wallet_service.dart | 12 +- lib/bitcoin/cw_bitcoin.dart | 6 +- lib/bitcoin_cash/cw_bitcoin_cash.dart | 2 - lib/di.dart | 7 +- tool/configure.dart | 3 - 21 files changed, 244 insertions(+), 327 deletions(-) create mode 100644 cw_bitcoin/lib/wallet_seed_bytes.dart diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index d8c3715ab1..e82a4eb8e0 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -59,7 +59,34 @@ abstract class BaseBitcoinAddressRecord { BitcoinAddressType type; - String toJSON(); + String toJSON() => json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isChange': isChange, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + 'runtimeType': runtimeType.toString(), + }); + + static BaseBitcoinAddressRecord fromJSON(String jsonSource) { + final decoded = json.decode(jsonSource) as Map; + + if (decoded['runtimeType'] == 'BitcoinAddressRecord') { + return BitcoinAddressRecord.fromJSON(jsonSource); + } else if (decoded['runtimeType'] == 'BitcoinSilentPaymentAddressRecord') { + return BitcoinSilentPaymentAddressRecord.fromJSON(jsonSource); + } else if (decoded['runtimeType'] == 'BitcoinReceivedSPAddressRecord') { + return BitcoinReceivedSPAddressRecord.fromJSON(jsonSource); + } else if (decoded['runtimeType'] == 'LitecoinMWEBAddressRecord') { + return LitecoinMWEBAddressRecord.fromJSON(jsonSource); + } else { + throw ArgumentError('Unknown runtimeType'); + } + } } class BitcoinAddressRecord extends BaseBitcoinAddressRecord { @@ -117,20 +144,13 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { late String scriptHash; @override - String toJSON() => json.encode({ - 'address': address, - 'index': index, - 'derivationInfo': derivationInfo.toJSON(), - 'derivationType': cwDerivationType.index, - 'isHidden': isHidden, - 'isChange': isChange, - 'isUsed': isUsed, - 'txCount': txCount, - 'name': name, - 'balance': balance, - 'type': type.toString(), - 'scriptHash': scriptHash, - }); + String toJSON() { + final m = json.decode(super.toJSON()) as Map; + m['derivationInfo'] = derivationInfo.toJSON(); + m['derivationType'] = cwDerivationType.index; + m['scriptHash'] = scriptHash; + return json.encode(m); + } @override bool operator ==(Object other) { @@ -204,19 +224,13 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { } @override - String toJSON() => json.encode({ - 'address': address, - 'derivationPath': derivationPath, - 'index': labelIndex, - 'isUsed': isUsed, - 'txCount': txCount, - 'name': name, - 'balance': balance, - 'type': type.toString(), - 'labelHex': labelHex, - 'isChange': isChange, - 'isHidden': isHidden, - }); + String toJSON() { + final m = json.decode(super.toJSON()) as Map; + m['derivationPath'] = _derivationPath; + m['index'] = labelIndex; + m['labelHex'] = labelHex; + return json.encode(m); + } } class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { @@ -272,18 +286,11 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { } @override - String toJSON() => json.encode({ - 'address': address, - 'index': labelIndex, - 'isUsed': isUsed, - 'txCount': txCount, - 'name': name, - 'balance': balance, - 'type': type.toString(), - 'labelHex': labelHex, - 'tweak': tweak, - 'isChange': isChange, - }); + String toJSON() { + final m = json.decode(super.toJSON()) as Map; + m['tweak'] = tweak; + return json.encode(m); + } } class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord { @@ -314,19 +321,6 @@ class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord { ); } - @override - String toJSON() => json.encode({ - 'address': address, - 'index': index, - 'isHidden': isHidden, - 'isChange': isChange, - 'isUsed': isUsed, - 'txCount': txCount, - 'name': name, - 'balance': balance, - 'type': type.toString(), - }); - @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 618ce8f0f0..0135e1d74b 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -11,16 +11,8 @@ class BitcoinUnspent extends Unspent { 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)), + address ?? BaseBitcoinAddressRecord.fromJSON(json['address_record'] as String), json['tx_hash'] as String, int.parse(json['value'].toString()), int.parse(json['tx_pos'].toString()), @@ -30,7 +22,6 @@ class BitcoinUnspent extends Unspent { Map toJson() { final json = { 'address_record': bitcoinAddressRecord.toJSON(), - 'address_runtimetype': bitcoinAddressRecord.runtimeType.toString(), 'tx_hash': hash, 'value': value, 'tx_pos': vout, diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index d65530d93b..d2165c2fcb 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -12,6 +12,7 @@ import 'package:cw_bitcoin/psbt_transaction_builder.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_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; @@ -61,7 +62,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { List? initialSilentAddresses, int initialSilentAddressIndex = 0, bool? alwaysScan, - required bool mempoolAPIEnabled, super.hdWallets, super.initialUnspentCoins, }) : super( @@ -83,7 +83,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, ) { walletAddresses = BitcoinWalletAddresses( walletInfo, @@ -114,7 +113,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, - required bool mempoolAPIEnabled, }) async { final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); @@ -135,7 +133,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, networkParam: network, - mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -146,7 +143,6 @@ 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!) @@ -220,7 +216,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, hdWallets: hdWallets, initialUnspentCoins: snp?.unspentCoins, ); @@ -358,6 +353,43 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return super.signMessage(message, address: address); } + Future get mempoolAPIEnabled async { + bool isMempoolAPIEnabled = (await sharedPrefs.future).getBool("use_mempool_fee_api") ?? true; + return isMempoolAPIEnabled; + } + + @override + Future updateFeeRates() async { + workerSendPort!.send( + ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: await mempoolAPIEnabled).toJson(), + ); + } + + @override + Future getTransactionExpanded({required String hash}) async { + return await sendWorker( + ElectrumWorkerTxExpandedRequest( + txHash: hash, + currentChainTip: currentChainTip!, + mempoolAPIEnabled: await mempoolAPIEnabled, + ), + ) as ElectrumTransactionBundle; + } + + @override + 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 ?? -1, + network: network, + mempoolAPIEnabled: await mempoolAPIEnabled, + ).toJson()); + } + @action Future setSilentPaymentsScanning(bool active) async { silentPaymentsScanningActive = active; @@ -688,22 +720,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override int get dustAmount => network == BitcoinNetwork.testnet ? 0 : 546; - @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(); @@ -1333,64 +1349,3 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } } - -class WalletSeedBytes { - final List seedBytes; - final Map hdWallets; - - WalletSeedBytes({required this.seedBytes, required this.hdWallets}); - - static Future getSeedBytes( - WalletInfo walletInfo, - String mnemonic, [ - String? passphrase, - ]) async { - late List seedBytes; - final Map hdWallets = {}; - - for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { - if (derivation.description?.contains("SP") ?? false) { - continue; - } - - if (derivation.derivationType == DerivationType.bip39) { - try { - seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("bip39 seed error: $e"); - } - - continue; - } - - if (derivation.derivationType == DerivationType.electrum) { - try { - seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("electrum_v1 seed error: $e"); - } - } - } - } - - if (walletInfo.isRecovery) { - if (hdWallets[CWBitcoinDerivationType.bip39] != null) { - hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; - } - if (hdWallets[CWBitcoinDerivationType.electrum] != null) { - hdWallets[CWBitcoinDerivationType.old_electrum] = - hdWallets[CWBitcoinDerivationType.electrum]!; - } - } - - return WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); - } -} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 96ee29fd66..e472cad1d4 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -46,8 +46,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future init() async { await generateInitialAddresses(type: SegwitAddressType.p2wpkh); - silentPaymentAddresses.clear(); - if (!isHardwareWallet) { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); @@ -113,6 +111,68 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S await updateAddressesInBox(); } + @action + Future generateSilentPaymentAddresses({required BitcoinAddressType type}) async { + final hasOldSPAddresses = silentPaymentAddresses.any((address) => + address.type == SilentPaymentsAddresType.p2sp && + address.derivationPath == OLD_SP_SPEND_PATH); + + if (walletInfo.isRecovery) { + final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); + final oldSpendPath = Bip32PathParser.parse("m/352'/1'/0'/0'/0"); + + final oldSilentPaymentWallet = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate(hdWallet.derive(oldScanPath).privateKey), + b_spend: ECPrivate(hdWallet.derive(oldSpendPath).privateKey), + ); + + silentPaymentWallets.add(oldSilentPaymentWallet); + silentPaymentAddresses.addAll( + [ + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toString(), + labelIndex: 0, + name: "", + type: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + isChange: false, + ), + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(oldSilentPaymentWallet.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + isChange: true, + ), + ], + ); + } + + silentPaymentAddresses.addAll([ + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toString(), + labelIndex: 0, + name: "", + type: SilentPaymentsAddresType.p2sp, + isChange: false, + ), + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + isChange: true, + ), + ]); + + updateHiddenAddresses(); + } + @override @computed String get address { diff --git a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart index bab72b6251..d1af1301f8 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart @@ -4,37 +4,26 @@ import 'package:cw_core/wallet_info.dart'; class BitcoinNewWalletCredentials extends WalletCredentials { BitcoinNewWalletCredentials({ - required String name, - WalletInfo? walletInfo, - String? password, - String? passphrase, + required super.name, + super.walletInfo, + super.password, + super.passphrase, this.mnemonic, - String? parentAddress, - }) : super( - name: name, - walletInfo: walletInfo, - password: password, - passphrase: passphrase, - parentAddress: parentAddress, - ); + super.parentAddress, + }); final String? mnemonic; } class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials { BitcoinRestoreWalletFromSeedCredentials({ - required String name, - required String password, + required super.name, + required super.password, required this.mnemonic, required super.derivations, - WalletInfo? walletInfo, - String? passphrase, - }) : super( - name: name, - password: password, - passphrase: passphrase, - walletInfo: walletInfo, - ); + super.walletInfo, + super.passphrase, + }); final String mnemonic; } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index e2b3d6690b..904751cdee 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -24,13 +24,11 @@ class BitcoinWalletService extends WalletService< this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect, - this.mempoolAPIEnabled, ); final Box walletInfoSource; final Box unspentCoinsInfoSource; final bool alwaysScan; - final bool mempoolAPIEnabled; final bool isDirect; @override @@ -62,7 +60,6 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); @@ -86,7 +83,6 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -100,7 +96,6 @@ class BitcoinWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); await wallet.init(); @@ -115,8 +110,9 @@ class BitcoinWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); - final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( - (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values + .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) + .toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); @@ -135,7 +131,6 @@ class BitcoinWalletService extends WalletService< walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, encryptionFileUtils: encryptionFileUtilsFor(isDirect), ); @@ -164,7 +159,6 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -190,7 +184,6 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, network: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 23721f92a2..d9dac3fc36 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -24,7 +24,6 @@ import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.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'; import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -69,7 +68,6 @@ abstract class ElectrumWalletBase ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, - required this.mempoolAPIEnabled, List? initialUnspentCoins, }) : hdWallets = hdWallets ?? { @@ -209,7 +207,6 @@ abstract class ElectrumWalletBase } bool? alwaysScan; - bool mempoolAPIEnabled; bool _updatingHistories = false; final Map hdWallets; @@ -358,11 +355,8 @@ abstract class ElectrumWalletBase _onError?.call(error); } - @action Future updateFeeRates() async { - workerSendPort!.send( - ElectrumWorkerGetFeesRequest(mempoolAPIEnabled: mempoolAPIEnabled).toJson(), - ); + workerSendPort!.send(ElectrumWorkerGetFeesRequest().toJson()); } @action @@ -1635,11 +1629,7 @@ abstract class ElectrumWalletBase Future getTransactionExpanded({required String hash}) async { return await sendWorker( - ElectrumWorkerTxExpandedRequest( - txHash: hash, - currentChainTip: currentChainTip!, - mempoolAPIEnabled: mempoolAPIEnabled, - ), + ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!), ) as ElectrumTransactionBundle; } @@ -1663,7 +1653,6 @@ abstract class ElectrumWalletBase throw UnimplementedError(); } - @action Future updateTransactions([List? addresses]) async { workerSendPort!.send(ElectrumWorkerGetHistoryRequest( addresses: walletAddresses.allAddresses.toList(), @@ -1671,9 +1660,8 @@ abstract class ElectrumWalletBase 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()), + chainTip: currentChainTip ?? -1, network: network, - mempoolAPIEnabled: mempoolAPIEnabled, ).toJson()); } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart index 1892e2cb75..68e5f07013 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -1,10 +1,7 @@ part of 'methods.dart'; class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { - ElectrumWorkerGetFeesRequest({ - required this.mempoolAPIEnabled, - this.id, - }); + ElectrumWorkerGetFeesRequest({this.mempoolAPIEnabled = false, this.id}); final bool mempoolAPIEnabled; final int? id; diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart index 021ed6899e..b5b32a7320 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -7,7 +7,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { required this.walletType, required this.chainTip, required this.network, - required this.mempoolAPIEnabled, + this.mempoolAPIEnabled = false, this.id, }); diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart index 1824a0686e..07b49db6fb 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -4,7 +4,7 @@ class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { ElectrumWorkerTxExpandedRequest({ required this.txHash, required this.currentChainTip, - required this.mempoolAPIEnabled, + this.mempoolAPIEnabled = false, this.id, }); diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 47498a2498..a51e8478fa 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -6,8 +6,8 @@ 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_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -69,7 +69,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int? initialMwebHeight, bool? alwaysScan, - required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, password: password, @@ -83,7 +82,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, alwaysScan: alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, ) { if (seedBytes != null) { mwebHd = @@ -160,7 +158,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, - required bool mempoolAPIEnabled, }) async { final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); @@ -178,7 +175,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, - mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -188,7 +184,6 @@ 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); @@ -253,7 +248,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, - mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -1754,75 +1748,3 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return BtcTransaction.fromRaw(rawHex); } } - -class WalletSeedBytes { - final List seedBytes; - final Map hdWallets; - - WalletSeedBytes({required this.seedBytes, required this.hdWallets}); - - static Future getSeedBytes( - WalletInfo walletInfo, - String mnemonic, [ - String? passphrase, - ]) async { - late List seedBytes; - final Map hdWallets = {}; - - if (walletInfo.derivations != null) { - for (final derivation in walletInfo.derivations!) { - if (derivation.derivationType == DerivationType.bip39) { - try { - seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("bip39 seed error: $e"); - } - - continue; - } - - if (derivation.derivationType == DerivationType.electrum) { - try { - seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("electrum_v2 seed error: $e"); - - try { - seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = - Bip32Slip10Secp256k1.fromSeed(seedBytes); - } catch (e) { - printV("electrum_v1 seed error: $e"); - } - } - } - } - } 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; - } - } - - if (walletInfo.isRecovery) { - if (hdWallets[CWBitcoinDerivationType.bip39] != null) { - hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; - } - if (hdWallets[CWBitcoinDerivationType.electrum] != null) { - hdWallets[CWBitcoinDerivationType.old_electrum] = - hdWallets[CWBitcoinDerivationType.electrum]!; - } - } - - return WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); - } -} diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index d71f146efc..8deebc1db0 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -27,14 +27,12 @@ class LitecoinWalletService extends WalletService< 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; @@ -61,7 +59,6 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -86,7 +83,6 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); saveBackup(name); @@ -100,7 +96,6 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; @@ -135,8 +130,9 @@ class LitecoinWalletService extends WalletService< } } - final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( - (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values + .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) + .toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); @@ -156,7 +152,6 @@ class LitecoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, alwaysScan: alwaysScan, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await currentWallet.renameWalletFiles(newName); @@ -183,7 +178,6 @@ class LitecoinWalletService extends WalletService< walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -209,7 +203,6 @@ 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/wallet_seed_bytes.dart b/cw_bitcoin/lib/wallet_seed_bytes.dart new file mode 100644 index 0000000000..34aadc7e70 --- /dev/null +++ b/cw_bitcoin/lib/wallet_seed_bytes.dart @@ -0,0 +1,59 @@ +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_info.dart'; + +class WalletSeedBytes { + final List seedBytes; + final Map hdWallets; + + WalletSeedBytes({required this.seedBytes, required this.hdWallets}); + + static Future getSeedBytes( + WalletInfo walletInfo, + String mnemonic, [ + String? passphrase, + ]) async { + late List seedBytes; + final Map hdWallets = {}; + + for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { + if (derivation.derivationType == DerivationType.bip39) { + try { + seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("bip39 seed error: $e"); + } + + continue; + } + + if (derivation.derivationType == DerivationType.electrum) { + try { + seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v2 seed error: $e"); + + try { + seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } catch (e) { + printV("electrum_v1 seed error: $e"); + } + } + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } + if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } + + return WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); + } +} diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 66d164f69f..7af05b4959 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -88,19 +88,19 @@ packages: description: path: "." ref: cake-update-v15 - resolved-ref: "82306ae21ea247ba400b9dc3823631d69ae45699" + resolved-ref: "10bb92c563e2e05041f4215ae20d834db6c9b2cf" url: "https://github.com/cake-tech/bitcoin_base" source: git - version: "4.7.0" + version: "5.0.0" blockchain_utils: dependency: "direct main" description: path: "." - ref: cake-update-v3 - resolved-ref: "9b64c43bcfe129e7f01300a63607fde083dd0357" + ref: cake-update-v4 + resolved-ref: "7bf4c263900a81fcddbd7797169f0e2fbd3a9f46" url: "https://github.com/cake-tech/blockchain_utils" source: git - version: "3.3.0" + version: "4.0.0" bluez: dependency: transitive description: @@ -919,7 +919,7 @@ packages: description: path: "." ref: cake-update-v4 - resolved-ref: bae6ecb9cd10b80e6c496dc963c26de2aee9751c + resolved-ref: "888bd27c3c4495c890580ebaaba6771b4f40eff6" url: "https://github.com/cake-tech/sp_scanner.git" source: git version: "0.0.1" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index f1884cda83..4d018084e2 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: sp_scanner: git: url: https://github.com/cake-tech/sp_scanner.git - ref: sp_v4.0.0 + ref: cake-update-v4 bech32: git: url: https://github.com/cake-tech/bech32.git diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index ba35351df6..6d1ae59f08 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -36,7 +36,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, - required bool mempoolAPIEnabled, }) : super( mnemonic: mnemonic, password: password, @@ -49,7 +48,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.bch, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, - mempoolAPIEnabled: mempoolAPIEnabled, hdWallets: {CWBitcoinDerivationType.bip39: Bip32Slip10Secp256k1.fromSeed(seedBytes)}, ) { walletAddresses = BitcoinCashWalletAddresses( @@ -80,7 +78,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, - required bool mempoolAPIEnabled, }) async { return BitcoinCashWallet( mnemonic: mnemonic, @@ -95,7 +92,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: passphrase, - mempoolAPIEnabled: mempoolAPIEnabled, ); } @@ -105,7 +101,6 @@ 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); @@ -173,7 +168,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, - mempoolAPIEnabled: mempoolAPIEnabled, ); } 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 34807679c9..7528181151 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_service.dart @@ -22,13 +22,11 @@ class BitcoinCashWalletService extends WalletService< 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; @@ -48,7 +46,6 @@ class BitcoinCashWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), passphrase: credentials.passphrase, - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); @@ -68,7 +65,6 @@ class BitcoinCashWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); saveBackup(name); @@ -81,7 +77,6 @@ class BitcoinCashWalletService extends WalletService< walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.init(); return wallet; @@ -95,8 +90,9 @@ class BitcoinCashWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); - final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( - (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values + .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) + .toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); @@ -115,7 +111,6 @@ class BitcoinCashWalletService extends WalletService< walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), - mempoolAPIEnabled: mempoolAPIEnabled, ); await currentWallet.renameWalletFiles(newName); @@ -154,7 +149,6 @@ class BitcoinCashWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), passphrase: credentials.passphrase, - mempoolAPIEnabled: mempoolAPIEnabled, ); await wallet.save(); await wallet.init(); diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index f941c196ed..942c1f7d37 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -259,14 +259,12 @@ class CWBitcoin extends Bitcoin { Box unspentCoinSource, bool alwaysScan, bool isDirect, - bool mempoolAPIEnabled, ) { return BitcoinWalletService( walletInfoSource, unspentCoinSource, alwaysScan, isDirect, - mempoolAPIEnabled, ); } @@ -275,14 +273,12 @@ class CWBitcoin extends Bitcoin { Box unspentCoinSource, bool alwaysScan, bool isDirect, - bool mempoolAPIEnabled, ) { return LitecoinWalletService( walletInfoSource, unspentCoinSource, alwaysScan, isDirect, - mempoolAPIEnabled, ); } @@ -661,7 +657,7 @@ class CWBitcoin extends Bitcoin { @override Future checkIfMempoolAPIIsEnabled(Object wallet) async { - final bitcoinWallet = wallet as ElectrumWallet; + final bitcoinWallet = wallet as BitcoinWallet; return await bitcoinWallet.mempoolAPIEnabled; } diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index a0cb406c2d..d8b4cad5ea 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -9,13 +9,11 @@ class CWBitcoinCash extends BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect, - bool mempoolAPIEnabled, ) { return BitcoinCashWalletService( walletInfoSource, unspentCoinSource, isDirect, - mempoolAPIEnabled, ); } diff --git a/lib/di.dart b/lib/di.dart index 723878d8e9..07657326f2 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1069,7 +1069,6 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().silentPaymentsAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, - getIt.get().useMempoolFeeAPI, ); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService( @@ -1077,7 +1076,6 @@ Future setup({ _unspentCoinsInfoSource, getIt.get().mwebAlwaysScan, SettingsStoreBase.walletPasswordDirectInput, - getIt.get().useMempoolFeeAPI, ); case WalletType.ethereum: return ethereum!.createEthereumWalletService( @@ -1087,7 +1085,6 @@ Future setup({ _walletInfoSource, _unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput, - getIt.get().useMempoolFeeAPI, ); case WalletType.nano: case WalletType.banano: @@ -1185,7 +1182,7 @@ Future setup({ (seedPhraseLength, _) => PreSeedPage(seedPhraseLength)); getIt.registerFactoryParam( - (content, _) => TransactionSuccessPage(content: content)); + (content, _) => TransactionSuccessPage(content: content)); getIt.registerFactoryParam((trade, _) => TradeDetailsViewModel( @@ -1422,7 +1419,7 @@ Future setup({ getIt.registerFactory(() => SignViewModel(getIt.get().wallet!)); - getIt.registerFactory(() => SeedVerificationPage(getIt.get())); + getIt.registerFactory(() => SeedVerificationPage(getIt.get())); _isSetupFinished = true; } diff --git a/tool/configure.dart b/tool/configure.dart index da80a7b065..615e698f25 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -215,14 +215,12 @@ abstract class Bitcoin { Box unspentCoinSource, bool alwaysScan, bool isDirect, - bool mempoolAPIEnabled, ); WalletService createLitecoinWalletService( Box walletInfoSource, Box unspentCoinSource, bool alwaysScan, bool isDirect, - bool mempoolAPIEnabled, ); TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityCustom(); @@ -1130,7 +1128,6 @@ abstract class BitcoinCash { Box walletInfoSource, Box unspentCoinSource, bool isDirect, - bool mempoolAPIEnabled, ); WalletCredentials createBitcoinCashNewWalletCredentials( From 7a18a89641269d086304cfafded4d1021bba3acf Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 14 Jan 2025 18:39:49 -0300 Subject: [PATCH 48/64] refactor: minor unneeded [skip ci] --- .../lib/electrum_transaction_history.dart | 5 ++- cw_bitcoin/lib/litecoin_wallet_service.dart | 12 +++---- .../lib/src/bitcoin_cash_wallet_service.dart | 34 ++++++++----------- cw_core/lib/wallet_keys_file.dart | 5 +-- lib/bitcoin_cash/cw_bitcoin_cash.dart | 16 ++------- 5 files changed, 26 insertions(+), 46 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 75e9d13896..d096d0e7b4 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -4,9 +4,12 @@ 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/utils/print_verbose.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'; @@ -89,7 +92,7 @@ abstract class ElectrumTransactionHistoryBase _height = content['height'] as int; } catch (e) { - // printV(e); + printV(e); } } diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 8deebc1db0..89ae384d49 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -23,11 +23,7 @@ class LitecoinWalletService extends WalletService< BitcoinRestoreWalletFromWIFCredentials, BitcoinRestoreWalletFromHardware> { LitecoinWalletService( - this.walletInfoSource, - this.unspentCoinsInfoSource, - this.alwaysScan, - this.isDirect, - ); + this.walletInfoSource, this.unspentCoinsInfoSource, this.alwaysScan, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; @@ -72,6 +68,7 @@ class LitecoinWalletService extends WalletService< @override Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; @@ -130,9 +127,8 @@ class LitecoinWalletService extends WalletService< } } - final unspentCoinsToDelete = unspentCoinsInfoSource.values - .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) - .toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); 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 7528181151..931893ef8d 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,7 @@ class BitcoinCashWalletService extends WalletService< BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> { - BitcoinCashWalletService( - this.walletInfoSource, - this.unspentCoinsInfoSource, - this.isDirect, - ); + BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, this.isDirect); final Box walletInfoSource; final Box unspentCoinsInfoSource; @@ -90,9 +86,8 @@ class BitcoinCashWalletService extends WalletService< .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; await walletInfoSource.delete(walletInfo.key); - final unspentCoinsToDelete = unspentCoinsInfoSource.values - .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) - .toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( + (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); @@ -106,12 +101,11 @@ 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)); await currentWallet.renameWalletFiles(newName); await saveBackup(newName); @@ -143,12 +137,12 @@ 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 ); await wallet.save(); await wallet.init(); diff --git a/cw_core/lib/wallet_keys_file.dart b/cw_core/lib/wallet_keys_file.dart index ff680f9e10..638cdc39d1 100644 --- a/cw_core/lib/wallet_keys_file.dart +++ b/cw_core/lib/wallet_keys_file.dart @@ -27,10 +27,7 @@ mixin WalletKeysFile walletInfoSource, - Box unspentCoinSource, - bool isDirect, - ) { - return BitcoinCashWalletService( - walletInfoSource, - unspentCoinSource, - isDirect, - ); + Box walletInfoSource, Box unspentCoinSource, bool isDirect) { + return BitcoinCashWalletService(walletInfoSource, unspentCoinSource, isDirect); } @override @@ -37,10 +30,7 @@ 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); From a2bf1800de7ef7fbc524344889c8291b5ad25e67 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 15 Jan 2025 17:52:31 -0300 Subject: [PATCH 49/64] feat: batch requests --- cw_bitcoin/lib/bitcoin_wallet.dart | 28 +-- cw_bitcoin/lib/bitcoin_wallet_snapshot.dart | 89 +++++++++ cw_bitcoin/lib/electrum_wallet.dart | 134 ++++++------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 10 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 32 --- .../lib/electrum_worker/electrum_worker.dart | 185 ++++++++++-------- cw_bitcoin/lib/litecoin_wallet.dart | 16 +- cw_bitcoin/lib/litecoin_wallet_snapshot.dart | 77 ++++++++ .../lib/src/bitcoin_cash_wallet.dart | 17 +- 9 files changed, 377 insertions(+), 211 deletions(-) create mode 100644 cw_bitcoin/lib/bitcoin_wallet_snapshot.dart create mode 100644 cw_bitcoin/lib/litecoin_wallet_snapshot.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index d2165c2fcb..079d45c00a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -5,6 +5,7 @@ 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_credentials.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_snapshot.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; @@ -18,7 +19,6 @@ import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; 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/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; @@ -150,10 +150,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); - ElectrumWalletSnapshot? snp = null; + BitcoinWalletSnapshot? snp = null; try { - snp = await ElectrumWalletSnapshot.load( + snp = await BitcoinWalletSnapshot.load( encryptionFileUtils, name, walletInfo.type, @@ -237,7 +237,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } if (node!.isElectrs == null) { - final version = await sendWorker(ElectrumWorkerGetVersionRequest()); + final version = await waitSendWorker(ElectrumWorkerGetVersionRequest()); if (version is List && version.isNotEmpty) { final server = version[0]; @@ -269,7 +269,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { if (node!.supportsSilentPayments == null) { try { - final workerResponse = (await sendWorker(ElectrumWorkerCheckTweaksRequest())) as String; + final workerResponse = (await waitSendWorker(ElectrumWorkerCheckTweaksRequest())) as String; final tweaksResponse = ElectrumWorkerCheckTweaksResponse.fromJson( json.decode(workerResponse) as Map, ); @@ -367,7 +367,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override Future getTransactionExpanded({required String hash}) async { - return await sendWorker( + return await waitSendWorker( ElectrumWorkerTxExpandedRequest( txHash: hash, currentChainTip: currentChainTip!, @@ -377,8 +377,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - Future updateTransactions([List? addresses]) async { - workerSendPort!.send(ElectrumWorkerGetHistoryRequest( + Future getUpdateTransactionsRequest([ + List? addresses, + ]) async { + return ElectrumWorkerGetHistoryRequest( addresses: walletAddresses.allAddresses.toList(), storedTxs: transactionHistory.transactions.values.toList(), walletType: type, @@ -387,7 +389,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { chainTip: currentChainTip ?? -1, network: network, mempoolAPIEnabled: await mempoolAPIEnabled, - ).toJson()); + ); } @action @@ -410,7 +412,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { _setListeners(walletInfo.restoreHeight); } } else if (syncStatus is! SyncedSyncStatus) { - await sendWorker(ElectrumWorkerStopScanningRequest()); + await waitSendWorker(ElectrumWorkerStopScanningRequest()); await startSync(); } } @@ -863,7 +865,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { )) .addElem(Bip32KeyIndex(addressRecord.index)); - privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); + privkey = ECPrivate.fromBip32(bip32: hdWallet.derive(path)); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -1250,7 +1252,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), @@ -1320,7 +1322,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { return PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: feeRateInt.toString(), diff --git a/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart new file mode 100644 index 0000000000..499a97e034 --- /dev/null +++ b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart @@ -0,0 +1,89 @@ +import 'dart:convert'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_type.dart'; + +class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { + BitcoinWalletSnapshot({ + required super.name, + required super.type, + required super.password, + required super.mnemonic, + required super.xpub, + required super.addresses, + required super.balance, + required super.regularAddressIndex, + required super.changeAddressIndex, + required super.addressPageType, + required this.silentAddressIndex, + required this.silentAddresses, + required this.alwaysScan, + required super.unspentCoins, + super.passphrase, + super.derivationType, + super.derivationPath, + }) : super(); + + List silentAddresses; + bool alwaysScan; + int silentAddressIndex; + + static Future load( + EncryptionFileUtils encryptionFileUtils, + String name, + WalletType type, + String password, + BasedUtxoNetwork network, + ) async { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + + final ElectrumWalletSnapshot electrumWalletSnapshot = await ElectrumWalletSnapshot.load( + encryptionFileUtils, + name, + type, + password, + network, + ); + + final silentAddressesTmp = data['silent_addresses'] as List? ?? []; + final silentAddresses = silentAddressesTmp.whereType().map((j) { + final decoded = json.decode(jsonSource) as Map; + if (decoded['tweak'] != null || decoded['silent_payment_tweak'] != null) { + return BitcoinReceivedSPAddressRecord.fromJSON(j); + } else { + return BitcoinSilentPaymentAddressRecord.fromJSON(j); + } + }).toList(); + final alwaysScan = data['alwaysScan'] as bool? ?? false; + var silentAddressIndex = 0; + + try { + silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); + } catch (_) {} + + return BitcoinWalletSnapshot( + name: name, + type: type, + password: password, + passphrase: electrumWalletSnapshot.passphrase, + mnemonic: electrumWalletSnapshot.mnemonic, + xpub: electrumWalletSnapshot.xpub, + addresses: electrumWalletSnapshot.addresses, + regularAddressIndex: electrumWalletSnapshot.regularAddressIndex, + balance: electrumWalletSnapshot.balance, + changeAddressIndex: electrumWalletSnapshot.changeAddressIndex, + addressPageType: electrumWalletSnapshot.addressPageType, + derivationType: electrumWalletSnapshot.derivationType, + derivationPath: electrumWalletSnapshot.derivationPath, + unspentCoins: electrumWalletSnapshot.unspentCoins, + silentAddressIndex: silentAddressIndex, + silentAddresses: silentAddresses, + alwaysScan: alwaysScan, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index d9dac3fc36..6a5177f5dc 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -111,7 +111,7 @@ abstract class ElectrumWalletBase } // Sends a request to the worker and returns a future that completes when the worker responds - Future sendWorker(ElectrumWorkerRequest request) { + Future waitSendWorker(ElectrumWorkerRequest request) { final messageId = ++_messageId; final completer = Completer(); @@ -183,20 +183,39 @@ abstract class ElectrumWalletBase } } - static Bip32Slip10Secp256k1 getAccountHDWallet(CryptoCurrency? currency, BasedUtxoNetwork network, - List? seedBytes, String? xpub, DerivationInfo? derivationInfo) { + static Bip32Slip10Secp256k1 getAccountHDWallet( + CryptoCurrency? currency, + BasedUtxoNetwork network, + List? seedBytes, + String? xpub, + DerivationInfo? derivationInfo, + ) { if (seedBytes == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); } if (seedBytes != null) { - return Bip32Slip10Secp256k1.fromSeed(seedBytes); + return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)); } return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } + int estimatedTransactionSize({ + required List inputTypes, + required List outputTypes, + String? memo, + bool enableRBF = true, + }) => + BitcoinTransactionBuilder.estimateTransactionSizeFromTypes( + inputTypes: inputTypes, + outputTypes: outputTypes, + network: network, + memo: memo, + enableRBF: enableRBF, + ); + static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { switch (network) { case LitecoinNetwork.mainnet: @@ -210,7 +229,7 @@ abstract class ElectrumWalletBase bool _updatingHistories = false; final Map hdWallets; - Bip32Slip10Secp256k1 get bip32 => walletAddresses.hdWallet; + Bip32Slip10Secp256k1 get hdWallet => walletAddresses.hdWallet; final String? _mnemonic; final EncryptionFileUtils encryptionFileUtils; @@ -222,7 +241,6 @@ abstract class ElectrumWalletBase @observable bool isEnabledAutoGenerateSubaddress; - ApiProvider? apiProvider; Box unspentCoinsInfo; @override @@ -236,19 +254,7 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - List get addressesSet => - walletAddresses.allAddresses.map((addr) => addr.address).toList(); - - List get scriptHashes => walletAddresses.addressesOnReceiveScreen - .map((addr) => (addr as BitcoinAddressRecord).scriptHash) - .toList(); - - List get publicScriptHashes => walletAddresses.allAddresses - .where((addr) => !addr.isChange) - .map((addr) => addr.scriptHash) - .toList(); - - String get xpub => bip32.publicKey.toExtended; + String get xpub => hdWallet.publicKey.toExtended; @override String? get seed => _mnemonic; @@ -274,9 +280,9 @@ abstract class ElectrumWalletBase @override BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: WifEncoder.encode(bip32.privateKey.raw, netVer: network.wifNetVer), - privateKey: bip32.privateKey.toHex(), - publicKey: bip32.publicKey.toHex(), + wif: WifEncoder.encode(hdWallet.privateKey.raw, netVer: network.wifNetVer), + privateKey: hdWallet.privateKey.toHex(), + publicKey: hdWallet.publicKey.toHex(), ); String _password; @@ -320,28 +326,19 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) - await sendWorker(ElectrumWorkerHeadersSubscribeRequest()); - - _syncedTimes = 0; + await waitSendWorker(ElectrumWorkerHeadersSubscribeRequest()); // 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 updateTransactions(null, true); // INFO: THIRD: Get the full wallet's balance with all addresses considered - await updateBalance(); - - // INFO: FOURTH: Finish getting unspent coins for all the addresses - await updateAllUnspents(); + await updateBalance(true); - // INFO: FIFTH: Get the latest recommended fee rates and start update timer + // INFO: FOURTH: Get the latest recommended fee rates and start update timer await updateFeeRates(); _updateFeeRateTimer ??= Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates()); - if (_syncedTimes == 3) { - syncStatus = SyncedSyncStatus(); - } - await save(); } catch (e, stacktrace) { printV(stacktrace); @@ -452,7 +449,7 @@ abstract class ElectrumWalletBase )) .addElem(Bip32KeyIndex(addressRecord.index)); - privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); + privkey = ECPrivate.fromBip32(bip32: hdWallet.derive(path)); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -803,7 +800,7 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: data.feeRate.toString(), @@ -879,7 +876,7 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: data.feeRate.toString(), @@ -926,24 +923,9 @@ abstract class ElectrumWalletBase 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, - 'alwaysScan': alwaysScan, 'unspents': unspentCoins.map((e) => e.toJson()).toList(), }); - int estimatedTransactionSize({ - required List inputTypes, - required List outputTypes, - String? memo, - bool enableRBF = true, - }) => - BitcoinTransactionBuilder.estimateTransactionSizeFromTypes( - inputTypes: inputTypes, - outputTypes: outputTypes, - network: network, - memo: memo, - enableRBF: enableRBF, - ); - int feeAmountForPriority( TransactionPriority priority, { required List inputTypes, @@ -1316,7 +1298,7 @@ abstract class ElectrumWalletBase element.type == addressRecord.type && element.cwDerivationType == addressRecord.cwDerivationType); printV( - "discovered ${newAddresses.length} new addresses, new total: ${newAddressList.length}"); + "discovered ${newAddresses.length} new ${isChange ? "change" : "receive"} addresses, new total: ${newAddressList.length}"); if (newAddresses.isNotEmpty) { // Update the transactions for the new discovered addresses @@ -1419,7 +1401,7 @@ abstract class ElectrumWalletBase )) .addElem(Bip32KeyIndex(addressRecord.index)); - final privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); + final privkey = ECPrivate.fromBip32(bip32: hdWallet.derive(path)); privateKeys.add(privkey); @@ -1500,7 +1482,7 @@ abstract class ElectrumWalletBase for (final utxo in unusedUtxos) { final address = RegexUtils.addressTypeFromStr(utxo.address, network); - final privkey = ECPrivate.fromBip32(bip32: bip32); + final privkey = ECPrivate.fromBip32(bip32: hdWallet); privateKeys.add(privkey); utxos.add( @@ -1605,7 +1587,7 @@ abstract class ElectrumWalletBase return PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: sendingAmount, fee: newFee, hasChange: changeOutputs.isNotEmpty, @@ -1628,7 +1610,7 @@ abstract class ElectrumWalletBase } Future getTransactionExpanded({required String hash}) async { - return await sendWorker( + return await waitSendWorker( ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!), ) as ElectrumTransactionBundle; } @@ -1653,8 +1635,10 @@ abstract class ElectrumWalletBase throw UnimplementedError(); } - Future updateTransactions([List? addresses]) async { - workerSendPort!.send(ElectrumWorkerGetHistoryRequest( + Future getUpdateTransactionsRequest([ + List? addresses, + ]) async { + return ElectrumWorkerGetHistoryRequest( addresses: walletAddresses.allAddresses.toList(), storedTxs: transactionHistory.transactions.values.toList(), walletType: type, @@ -1662,7 +1646,15 @@ abstract class ElectrumWalletBase // with confirmations as 0 but will be auto fixed on onHeadersResponse chainTip: currentChainTip ?? -1, network: network, - ).toJson()); + ); + } + + Future updateTransactions([List? addresses, bool? wait]) async { + if (wait == true) { + return waitSendWorker(await getUpdateTransactionsRequest()); + } else { + workerSendPort!.send((await getUpdateTransactionsRequest()).toJson()); + } } @action @@ -1710,10 +1702,20 @@ abstract class ElectrumWalletBase } @action - Future updateBalance() async { - workerSendPort!.send(ElectrumWorkerGetBalanceRequest( - scripthashes: walletAddresses.allScriptHashes, - ).toJson()); + Future updateBalance([bool? wait]) async { + if (wait == true) { + return waitSendWorker( + ElectrumWorkerGetBalanceRequest( + scripthashes: walletAddresses.allScriptHashes, + ), + ); + } else { + workerSendPort!.send( + ElectrumWorkerGetBalanceRequest( + scripthashes: walletAddresses.allScriptHashes, + ).toJson(), + ); + } } @override @@ -1728,7 +1730,7 @@ abstract class ElectrumWalletBase ) .addElem(Bip32KeyIndex(record.index)); - final priv = ECPrivate.fromHex(bip32.derive(path).privateKey.toHex()); + final priv = ECPrivate.fromHex(hdWallet.derive(path).privateKey.toHex()); final hexEncoded = priv.signMessage(StringUtils.encode(message)); final decodedSig = hex.decode(hexEncoded); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index deb4518add..08e52f2301 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -457,10 +457,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return isUnusedReceiveAddress(addr) && addr.type == type; } - Map toJson() { - return { - 'allAddresses': _allAddresses.map((address) => address.toJSON()).toList(), - 'addressPageType': addressPageType.toString(), - }; - } + Map toJson() => { + '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 adeb57f01d..8edffd6e1a 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -20,10 +20,6 @@ class ElectrumWalletSnapshot { required this.regularAddressIndex, required this.changeAddressIndex, required this.addressPageType, - required this.silentAddresses, - required this.silentAddressIndex, - required this.mwebAddresses, - required this.alwaysScan, required this.unspentCoins, this.passphrase, this.derivationType, @@ -46,14 +42,10 @@ class ElectrumWalletSnapshot { String? passphrase; List addresses; - List silentAddresses; - List mwebAddresses; - bool alwaysScan; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; - int silentAddressIndex; DerivationType? derivationType; String? derivationPath; @@ -72,29 +64,10 @@ class ElectrumWalletSnapshot { .map((addr) => BitcoinAddressRecord.fromJSON(addr)) .toList(); - final silentAddressesTmp = data['silent_addresses'] as List? ?? []; - final silentAddresses = silentAddressesTmp.whereType().map((j) { - final decoded = json.decode(jsonSource) as Map; - if (decoded['tweak'] != null || decoded['silent_payment_tweak'] != null) { - return BitcoinReceivedSPAddressRecord.fromJSON(j); - } else { - return BitcoinSilentPaymentAddressRecord.fromJSON(j); - } - }).toList(); - - final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; - final mwebAddresses = mwebAddressTmp - .whereType() - .map((addr) => LitecoinMWEBAddressRecord.fromJSON(addr)) - .toList(); - - final alwaysScan = data['alwaysScan'] as bool? ?? false; - final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; - var silentAddressIndex = 0; final derivationType = DerivationType .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; @@ -108,7 +81,6 @@ class ElectrumWalletSnapshot { SegwitAddressType.p2wpkh.toString(): int.parse(data['change_address_index'] as String? ?? '0') }; - silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); } catch (_) { try { regularAddressIndexByType = data["account_index"] as Map? ?? {}; @@ -130,10 +102,6 @@ class ElectrumWalletSnapshot { addressPageType: data['address_page_type'] as String?, derivationType: derivationType, derivationPath: derivationPath, - silentAddresses: silentAddresses, - 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 index e290a940eb..f8971c1306 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -212,78 +212,100 @@ class ElectrumWorker { })); } - Future _handleGetHistory(ElectrumWorkerGetHistoryRequest result) async { - final Map histories = {}; - final addresses = result.addresses; - - await Future.wait(addresses.map((addressRecord) async { - if (addressRecord.scriptHash.isEmpty) { - return; + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest request) async { + final histories = {}; + final scripthashes = []; + final addresses = []; + request.addresses.forEach((addr) { + addr.txCount = 0; + + if (addr.scriptHash.isNotEmpty) { + scripthashes.add(addr.scriptHash); + addresses.add(addr.address); } + }); - final history = await _electrumClient!.request(ElectrumRequestScriptHashGetHistory( - scriptHash: addressRecord.scriptHash, - )); + final historyBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestScriptHashGetHistory( + scriptHashes: scripthashes, + ), + ); - if (history.isNotEmpty) { - addressRecord.setAsUsed(); - addressRecord.txCount = history.length; + await Future.wait(historyBatches.map((result) async { + final history = result.result; + if (history.isEmpty) { + return; + } - await Future.wait(history.map((transaction) async { - final txid = transaction['tx_hash'] as String; - final height = transaction['height'] as int; - ElectrumTransactionInfo? tx; + 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); + try { + // Exception thrown on null, handled on catch + tx = request.storedTxs.firstWhere((tx) => tx.id == txid); - if (height > 0) { - tx.height = height; + 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, - ); + // the tx's block itself is the first confirmation so add 1 + tx.confirmations = request.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 == null || (tx.isDateValidated != true && request.mempoolAPIEnabled)) { + tx = ElectrumTransactionInfo.fromElectrumBundle( + await _getTransactionExpanded( + hash: txid, + currentChainTip: request.chainTip, + mempoolAPIEnabled: request.mempoolAPIEnabled, + getTime: true, + confirmations: tx?.confirmations, + date: tx?.date, + ), + request.walletType, + request.network, + addresses: addresses.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, - ); - } - })); - } + request.addresses.forEach( + (addr) { + final usedAddress = (tx!.outputAddresses?.contains(addr.address) ?? false) || + (tx.inputAddresses?.contains(addr.address) ?? false); + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; + + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(tx); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [tx], + walletType: request.walletType, + ); + } + } + }, + ); + })); })); - _sendResponse(ElectrumWorkerGetHistoryResponse( - result: histories.values.toList(), - id: result.id, - )); + if (histories.isNotEmpty) { + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: request.id, + ), + ); + } } // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { @@ -318,25 +340,18 @@ class ElectrumWorker { // } Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { - final balanceFutures = >>[]; - - for (final scripthash in request.scripthashes) { - if (scripthash.isEmpty) { - continue; - } - - final balanceFuture = _electrumClient!.request( - ElectrumRequestGetScriptHashBalance(scriptHash: scripthash), - ); - balanceFutures.add(balanceFuture); - } + final balances = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetScriptHashBalance( + scriptHashes: request.scripthashes.where((s) => s.isNotEmpty).toList(), + ), + ); var totalConfirmed = 0; var totalUnconfirmed = 0; - final balances = await Future.wait(balanceFutures); - - for (final balance in balances) { + for (final result in balances) { + final balance = result.result; + print("balance: $balance"); final confirmed = balance['confirmed'] as int? ?? 0; final unconfirmed = balance['unconfirmed'] as int? ?? 0; totalConfirmed += confirmed; @@ -429,16 +444,16 @@ class ElectrumWorker { transactionHex = await _electrumClient!.request( ElectrumRequestGetTransactionHex(transactionHash: hash), ); + } - if (getTime && _walletType == WalletType.bitcoin) { - if (mempoolAPIEnabled) { - try { - final dates = await getTxDate(hash, _network!, date: date); - time = dates.time; - height = dates.height; - isDateValidated = dates.isDateValidated; - } catch (_) {} - } + if (getTime && _walletType == WalletType.bitcoin) { + if (mempoolAPIEnabled) { + try { + final dates = await getTxDate(hash, _network!, date: date); + time = dates.time; + height = dates.height; + isDateValidated = dates.isDateValidated; + } catch (_) {} } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a51e8478fa..a632b639bc 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -7,6 +7,7 @@ import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_wallet_snapshot.dart'; import 'package:cw_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; @@ -30,7 +31,6 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/unspent_coins_info.dart'; 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_bitcoin/litecoin_wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; @@ -188,10 +188,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { }) async { final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); - ElectrumWalletSnapshot? snp = null; + LitecoinWalletSnapshot? snp = null; try { - snp = await ElectrumWalletSnapshot.load( + snp = await LitecoinWalletSnapshot.load( encryptionFileUtils, name, walletInfo.type, @@ -931,7 +931,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { )) .addElem(Bip32KeyIndex(addressRecord.index)); - privkey = ECPrivate.fromBip32(bip32: bip32.derive(path)); + privkey = ECPrivate.fromBip32(bip32: hdWallet.derive(path)); } vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); @@ -1301,7 +1301,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx = PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: data.feeRate.toString(), @@ -1363,7 +1363,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { tx = PendingBitcoinTransaction( transaction, type, - sendWorker: sendWorker, + sendWorker: waitSendWorker, amount: estimatedTx.amount, fee: estimatedTx.fee, feeRate: data.feeRate.toString(), @@ -1472,7 +1472,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final utxo = unspentCoins .firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex); final key = ECPrivate.fromBip32( - bip32: bip32.derive((utxo.bitcoinAddressRecord as BitcoinAddressRecord) + bip32: hdWallet.derive((utxo.bitcoinAddressRecord as BitcoinAddressRecord) .derivationInfo .derivationPath)); final digest = tx2.getTransactionSegwitDigit( @@ -1562,7 +1562,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future signMessage(String message, {String? address = null}) async { - Bip32Slip10Secp256k1 HD = bip32; + Bip32Slip10Secp256k1 HD = hdWallet; final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address); diff --git a/cw_bitcoin/lib/litecoin_wallet_snapshot.dart b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart new file mode 100644 index 0000000000..1a07f75da3 --- /dev/null +++ b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_type.dart'; + +class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { + LitecoinWalletSnapshot({ + required super.name, + required super.type, + required super.password, + required super.mnemonic, + required super.xpub, + required super.addresses, + required super.balance, + required super.regularAddressIndex, + required super.changeAddressIndex, + required super.addressPageType, + required this.mwebAddresses, + required this.alwaysScan, + required super.unspentCoins, + super.passphrase, + super.derivationType, + super.derivationPath, + }) : super(); + + List mwebAddresses; + bool alwaysScan; + + static Future load( + EncryptionFileUtils encryptionFileUtils, + String name, + WalletType type, + String password, + BasedUtxoNetwork network, + ) async { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await encryptionFileUtils.read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + + final ElectrumWalletSnapshot electrumWalletSnapshot = await ElectrumWalletSnapshot.load( + encryptionFileUtils, + name, + type, + password, + network, + ); + + final mwebAddressTmp = data['mweb_addresses'] as List? ?? []; + final mwebAddresses = mwebAddressTmp + .whereType() + .map((addr) => LitecoinMWEBAddressRecord.fromJSON(addr)) + .toList(); + final alwaysScan = data['alwaysScan'] as bool? ?? false; + + return LitecoinWalletSnapshot( + name: name, + type: type, + password: password, + passphrase: electrumWalletSnapshot.passphrase, + mnemonic: electrumWalletSnapshot.mnemonic, + xpub: electrumWalletSnapshot.xpub, + addresses: electrumWalletSnapshot.addresses, + regularAddressIndex: electrumWalletSnapshot.regularAddressIndex, + balance: electrumWalletSnapshot.balance, + changeAddressIndex: electrumWalletSnapshot.changeAddressIndex, + addressPageType: electrumWalletSnapshot.addressPageType, + derivationType: electrumWalletSnapshot.derivationType, + derivationPath: electrumWalletSnapshot.derivationPath, + unspentCoins: electrumWalletSnapshot.unspentCoins, + mwebAddresses: mwebAddresses, + alwaysScan: alwaysScan, + ); + } +} diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 6d1ae59f08..7799c1aa8f 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -66,6 +66,21 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override BitcoinCashNetwork get network => BitcoinCashNetwork.mainnet; + @override + int estimatedTransactionSize({ + required List inputTypes, + required List outputTypes, + String? memo, + bool enableRBF = true, + }) => + ForkedTransactionBuilder.estimateTransactionSizeFromTypes( + inputTypes: inputTypes, + outputTypes: outputTypes, + network: network, + memo: memo, + enableRBF: enableRBF, + ); + static Future create({ required String mnemonic, required String password, @@ -178,7 +193,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { @override Future signMessage(String message, {String? address = null}) async { - Bip32Slip10Secp256k1 HD = bip32; + Bip32Slip10Secp256k1 HD = hdWallet; final record = walletAddresses.allAddresses.firstWhere((element) => element.address == address); From f1345fadc809aa1a2b91c312f4a623801ed907a2 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 15 Jan 2025 17:54:50 -0300 Subject: [PATCH 50/64] chore: print --- cw_bitcoin/lib/electrum_worker/electrum_worker.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index f8971c1306..f1e22dec45 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -351,7 +351,6 @@ class ElectrumWorker { for (final result in balances) { final balance = result.result; - print("balance: $balance"); final confirmed = balance['confirmed'] as int? ?? 0; final unconfirmed = balance['unconfirmed'] as int? ?? 0; totalConfirmed += confirmed; From f3e96c67d64cace683e028fe695ffe1a7a4c2281 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 15 Jan 2025 17:59:02 -0300 Subject: [PATCH 51/64] chore: temp fix --- lib/view_model/hardware_wallet/ledger_view_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 4c084c778b..5210137e39 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -112,8 +112,8 @@ abstract class LedgerViewModelBase with Store { : ledgerPlusUSB; if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = ledger.deviceStateChanges - .listen(_connectionChangeListener); + // _connectionChangeSubscription = ledger.deviceStateChanges + // .listen(_connectionChangeListener); } _connection = await ledger.connect(device); From 7ef28df3ab8193a55fcb6d232296b65281e182d1 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 15 Jan 2025 17:59:31 -0300 Subject: [PATCH 52/64] Revert "chore: temp fix" This reverts commit f3e96c67d64cace683e028fe695ffe1a7a4c2281. --- lib/view_model/hardware_wallet/ledger_view_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 5210137e39..4c084c778b 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -112,8 +112,8 @@ abstract class LedgerViewModelBase with Store { : ledgerPlusUSB; if (_connectionChangeSubscription == null) { - // _connectionChangeSubscription = ledger.deviceStateChanges - // .listen(_connectionChangeListener); + _connectionChangeSubscription = ledger.deviceStateChanges + .listen(_connectionChangeListener); } _connection = await ledger.connect(device); From c3fdc5ec990fe4920417aef6806a8b2428282024 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 16 Jan 2025 13:43:34 -0300 Subject: [PATCH 53/64] feat: improve batch request even further, make initial faster --- cw_bitcoin/lib/bitcoin_wallet.dart | 4 +- cw_bitcoin/lib/electrum_transaction_info.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 52 +-- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 3 + .../lib/electrum_worker/electrum_worker.dart | 441 +++++++++++++++--- cw_bitcoin/lib/litecoin_wallet.dart | 2 + .../lib/src/bitcoin_cash_wallet.dart | 2 + 7 files changed, 409 insertions(+), 97 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 079d45c00a..482f5c9933 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -64,6 +64,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { bool? alwaysScan, super.hdWallets, super.initialUnspentCoins, + super.didInitialSync, }) : super( mnemonic: mnemonic, passphrase: passphrase, @@ -218,6 +219,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { alwaysScan: alwaysScan, hdWallets: hdWallets, initialUnspentCoins: snp?.unspentCoins, + didInitialSync: snp?.didInitialSync, ); } @@ -381,7 +383,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { List? addresses, ]) async { return ElectrumWorkerGetHistoryRequest( - addresses: walletAddresses.allAddresses.toList(), + addresses: 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 diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 9d267759ef..6651c1b971 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -236,7 +236,7 @@ class ElectrumTransactionInfo extends TransactionInfo { return ElectrumTransactionInfo( type, id: data['id'] as String, - height: data['height'] as int, + height: data['height'] as int?, amount: data['amount'] as int, fee: data['fee'] as int, direction: parseTransactionDirectionFromInt(data['direction'] as int), diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 6a5177f5dc..da9542853e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -69,6 +69,7 @@ abstract class ElectrumWalletBase CryptoCurrency? currency, this.alwaysScan, List? initialUnspentCoins, + bool? didInitialSync, }) : hdWallets = hdWallets ?? { CWBitcoinDerivationType.bip39: getAccountHDWallet( @@ -97,6 +98,7 @@ abstract class ElectrumWalletBase this.unspentCoinsInfo = unspentCoinsInfo, this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, + _didInitialSync = didInitialSync ?? false, super(walletInfo) { this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory( @@ -226,7 +228,6 @@ abstract class ElectrumWalletBase } bool? alwaysScan; - bool _updatingHistories = false; final Map hdWallets; Bip32Slip10Secp256k1 get hdWallet => walletAddresses.hdWallet; @@ -299,8 +300,7 @@ abstract class ElectrumWalletBase List scripthashesListening; bool _chainTipListenerOn = false; - // TODO: improve this - int _syncedTimes = 0; + bool _didInitialSync; void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; @@ -331,9 +331,16 @@ abstract class ElectrumWalletBase // 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(null, true); + if (!_didInitialSync && transactionHistory.transactions.isNotEmpty) { + await updateTransactions(null, true); + _didInitialSync = true; + } + // INFO: THIRD: Get the full wallet's balance with all addresses considered await updateBalance(true); + syncStatus = SyncedSyncStatus(); + // INFO: FOURTH: Get the latest recommended fee rates and start update timer await updateFeeRates(); _updateFeeRateTimer ??= @@ -924,6 +931,7 @@ abstract class ElectrumWalletBase 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, 'unspents': unspentCoins.map((e) => e.toJson()).toList(), + 'didInitialSync': _didInitialSync, }); int feeAmountForPriority( @@ -1148,11 +1156,6 @@ abstract class ElectrumWalletBase unspentCoins.forEach(updateCoin); await refreshUnspentCoinsInfo(); - - _syncedTimes++; - if (_syncedTimes == 3) { - syncStatus = SyncedSyncStatus(); - } } @action @@ -1233,22 +1236,10 @@ abstract class ElectrumWalletBase @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) { + await Future.wait(histories.map((addressHistory) async { final txs = addressHistory.txs; if (txs.isNotEmpty) { @@ -1306,19 +1297,13 @@ abstract class ElectrumWalletBase } } } - } + })); if (addressesWithHistory.isNotEmpty) { walletAddresses.updateAdresses(addressesWithHistory); } walletAddresses.updateHiddenAddresses(); - _updatingHistories = false; - - _syncedTimes++; - if (_syncedTimes == 3) { - syncStatus = SyncedSyncStatus(); - } } Future canReplaceByFee(ElectrumTransactionInfo tx) async { @@ -1639,7 +1624,7 @@ abstract class ElectrumWalletBase List? addresses, ]) async { return ElectrumWorkerGetHistoryRequest( - addresses: walletAddresses.allAddresses.toList(), + addresses: 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 @@ -1651,9 +1636,9 @@ abstract class ElectrumWalletBase Future updateTransactions([List? addresses, bool? wait]) async { if (wait == true) { - return waitSendWorker(await getUpdateTransactionsRequest()); + return waitSendWorker(await getUpdateTransactionsRequest(addresses)); } else { - workerSendPort!.send((await getUpdateTransactionsRequest()).toJson()); + workerSendPort!.send((await getUpdateTransactionsRequest(addresses)).toJson()); } } @@ -1694,11 +1679,6 @@ abstract class ElectrumWalletBase unconfirmed: totalUnconfirmed, frozen: totalFrozen, ); - - _syncedTimes++; - if (_syncedTimes == 3) { - syncStatus = SyncedSyncStatus(); - } } @action diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 8edffd6e1a..a395f0724c 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -24,6 +24,7 @@ class ElectrumWalletSnapshot { this.passphrase, this.derivationType, this.derivationPath, + this.didInitialSync, }); final String name; @@ -48,6 +49,7 @@ class ElectrumWalletSnapshot { Map changeAddressIndex; DerivationType? derivationType; String? derivationPath; + bool? didInitialSync; static Future load(EncryptionFileUtils encryptionFileUtils, String name, WalletType type, String password, BasedUtxoNetwork network) async { @@ -105,6 +107,7 @@ class ElectrumWalletSnapshot { unspentCoins: (data['unspent_coins'] as List) .map((e) => BitcoinUnspent.fromJSON(null, e as Map)) .toList(), + didInitialSync: data['didInitialSync'] as bool?, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index f1e22dec45..25016dd060 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_priority.dart'; @@ -212,8 +213,93 @@ class ElectrumWorker { })); } + Future _handleGetInitialHistory(ElectrumWorkerGetHistoryRequest request) async { + var histories = {}; + final scripthashes = []; + final addresses = []; + request.addresses.forEach((addr) { + addr.txCount = 0; + + if (addr.scriptHash.isNotEmpty) { + scripthashes.add(addr.scriptHash); + addresses.add(addr.address); + } + }); + + final historyBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestScriptHashGetHistory( + scriptHashes: scripthashes, + ), + ); + + final transactionIdsForHeights = {}; + + for (final batch in historyBatches) { + final history = batch.result; + if (history.isEmpty) { + continue; + } + + history.forEach((tx) { + transactionIdsForHeights[tx['tx_hash'] as String] = tx['height'] as int; + }); + } + + if (transactionIdsForHeights.isNotEmpty) { + final transactions = await _getInitialBatchTransactionsExpanded( + hashesForHeights: transactionIdsForHeights, + currentChainTip: request.chainTip, + ); + + transactions.forEach((tx) { + final txInfo = ElectrumTransactionInfo.fromElectrumBundle( + tx, + request.walletType, + request.network, + addresses: addresses.toSet(), + // height: height, + ); + + request.addresses.forEach( + (addr) { + final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || + (txInfo.inputAddresses?.contains(addr.address) ?? false); + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; + + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(txInfo); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ); + } + } + }, + ); + }); + + if (histories.isNotEmpty) { + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: request.id, + ), + ); + } + } + } + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest request) async { - final histories = {}; + if (request.storedTxs.isEmpty) { + return await _handleGetInitialHistory(request); + } + + var histories = {}; final scripthashes = []; final addresses = []; request.addresses.forEach((addr) { @@ -231,19 +317,21 @@ class ElectrumWorker { ), ); + final transactionIdsHeights = {}; + await Future.wait(historyBatches.map((result) async { final history = result.result; if (history.isEmpty) { return; } - await Future.wait(history.map((transaction) async { + for (final transaction in history) { final txid = transaction['tx_hash'] as String; final height = transaction['height'] as int; ElectrumTransactionInfo? tx; try { - // Exception thrown on null, handled on catch + // Exception thrown if non existing, handled on null condition below trycatch tx = request.storedTxs.firstWhere((tx) => tx.id == txid); if (height > 0) { @@ -258,45 +346,61 @@ class ElectrumWorker { // 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 == null || (tx.isDateValidated != true && request.mempoolAPIEnabled)) { - tx = ElectrumTransactionInfo.fromElectrumBundle( - await _getTransactionExpanded( - hash: txid, - currentChainTip: request.chainTip, - mempoolAPIEnabled: request.mempoolAPIEnabled, - getTime: true, - confirmations: tx?.confirmations, - date: tx?.date, - ), - request.walletType, - request.network, - addresses: addresses.toSet(), - height: height, - ); + transactionIdsHeights[txid] = TxToFetch(height: height, tx: tx); } + } + })); - request.addresses.forEach( - (addr) { - final usedAddress = (tx!.outputAddresses?.contains(addr.address) ?? false) || - (tx.inputAddresses?.contains(addr.address) ?? false); - if (usedAddress == true) { - addr.setAsUsed(); - addr.txCount++; + final txInfos = [...request.storedTxs]; - final addressHistories = histories[addr.address]; - if (addressHistories != null) { - addressHistories.txs.add(tx); - } else { - histories[addr.address] = AddressHistoriesResponse( - addressRecord: addr, - txs: [tx], - walletType: request.walletType, - ); - } - } - }, + if (transactionIdsHeights.isNotEmpty) { + final transactions = await _getBatchTransactionsExpanded( + txsForHeights: transactionIdsHeights, + currentChainTip: request.chainTip, + mempoolAPIEnabled: request.mempoolAPIEnabled, + getTime: true, + ); + + transactions.entries.forEach((result) { + final hash = result.key; + final txBundle = result.value; + + final txInfo = ElectrumTransactionInfo.fromElectrumBundle( + txBundle, + request.walletType, + request.network, + addresses: addresses.toSet(), + height: transactionIdsHeights[hash]?.height, ); - })); - })); + + txInfos.add(txInfo); + }); + } + + txInfos.forEach((txInfo) { + request.addresses.forEach( + (addr) { + final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || + (txInfo.inputAddresses?.contains(addr.address) ?? false); + + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; + + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(txInfo); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ); + } + } + }, + ); + }); if (histories.isNotEmpty) { _sendResponse( @@ -418,6 +522,197 @@ class ElectrumWorker { _sendResponse(ElectrumWorkerTxExpandedResponse(expandedTx: tx, id: request.id)); } + Future> _getInitialBatchTransactionsExpanded({ + required Map hashesForHeights, + required int currentChainTip, + }) async { + final hashes = hashesForHeights.keys.toList(); + + final transactionVerboseBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), + ); + List transactionHexes = []; + List emptyVerboseTxs = []; + + transactionVerboseBatches.forEach((batch) { + final txVerbose = batch.result; + if (txVerbose.isEmpty) { + emptyVerboseTxs.add( + (batch.request.paramsById[batch.id] as List).first as String, + ); + } else { + transactionHexes.add(txVerbose['hex'] as String); + } + }); + + if (emptyVerboseTxs.isNotEmpty) { + transactionHexes.addAll((await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), + )) + .map((result) => result.result) + .toList()); + } + + final dates = {}; + + if (_walletType == WalletType.bitcoin) { + for (final hash in hashes) { + try { + final date = getDateByBitcoinHeight(hashesForHeights[hash]!); + dates[hash] = date; + } catch (_) {} + } + } + + final bundles = []; + final insHashes = []; + + for (final txHex in transactionHexes) { + final original = BtcTransaction.fromRaw(txHex); + insHashes.addAll(original.inputs.map((e) => e.txId)); + } + + final inputTransactionHexBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex( + transactionHashes: insHashes, + ), + ); + + for (final txHex in transactionHexes) { + final original = BtcTransaction.fromRaw(txHex); + final ins = []; + + for (final input in original.inputs) { + try { + final inputTransactionHex = inputTransactionHexBatches + .firstWhere( + (batch) => + (batch.request.paramsById[batch.id] as List).first == input.txId, + ) + .result; + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } catch (_) {} + } + + final date = dates[original.txId]; + final height = hashesForHeights[original.txId] ?? 0; + final tip = currentChainTip; + + bundles.add( + ElectrumTransactionBundle( + original, + ins: ins, + time: date?.millisecondsSinceEpoch, + confirmations: tip - height + 1, + ), + ); + } + + return bundles; + } + + Future> _getBatchTransactionsExpanded({ + required Map txsForHeights, + required int currentChainTip, + required bool mempoolAPIEnabled, + bool getTime = false, + }) async { + final hashes = txsForHeights.keys.toList(); + + final transactionVerboseBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), + ); + List transactionHexes = []; + List emptyVerboseTxs = []; + + transactionVerboseBatches.forEach((batch) { + final txVerbose = batch.result; + if (txVerbose.isEmpty) { + emptyVerboseTxs.add( + (batch.request.paramsById[batch.id] as List).first as String, + ); + } else { + transactionHexes.add(txVerbose['hex'] as String); + } + }); + + if (emptyVerboseTxs.isNotEmpty) { + transactionHexes.addAll((await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), + )) + .map((result) => result.result) + .toList()); + } + + final datesByHashes = {}; + + if (_walletType == WalletType.bitcoin) { + await Future.wait(hashes.map((hash) async { + try { + if (getTime && _walletType == WalletType.bitcoin && mempoolAPIEnabled) { + final tx = txsForHeights[hash]?.tx; + + try { + final dates = await getTxDate( + hash, + _network!, + currentChainTip, + date: tx?.date, + confirmations: tx?.confirmations, + ); + datesByHashes[hash] = dates; + } catch (_) {} + } + } catch (_) {} + })); + } + + final bundles = {}; + final insHashes = []; + + for (final txHex in transactionHexes) { + final original = BtcTransaction.fromRaw(txHex); + insHashes.addAll(original.inputs.map((e) => e.txId)); + } + + final inputTransactionHexBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex( + transactionHashes: insHashes, + ), + ); + + for (final txHex in transactionHexes) { + final original = BtcTransaction.fromRaw(txHex); + final ins = []; + + for (final input in original.inputs) { + try { + final inputTransactionHex = inputTransactionHexBatches + .firstWhere( + (batch) => + (batch.request.paramsById[batch.id] as List).first == input.txId, + ) + .result; + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } catch (_) {} + } + + final dates = datesByHashes[original.txId()]; + + bundles[original.txId()] = ElectrumTransactionBundle( + original, + ins: ins, + time: dates?.time, + confirmations: dates?.confirmations ?? 0, + isDateValidated: dates?.isDateValidated, + ); + } + + return bundles; + } + Future _getTransactionExpanded({ required String hash, required int currentChainTip, @@ -427,8 +722,7 @@ class ElectrumWorker { DateTime? date, }) async { int? time; - int? height; - bool? isDateValidated; + DateResult? dates; final transactionVerbose = await _electrumClient!.request( ElectrumRequestGetTransactionVerbose(transactionHash: hash), @@ -448,22 +742,17 @@ class ElectrumWorker { if (getTime && _walletType == WalletType.bitcoin) { if (mempoolAPIEnabled) { try { - final dates = await getTxDate(hash, _network!, date: date); - time = dates.time; - height = dates.height; - isDateValidated = dates.isDateValidated; + dates = await getTxDate( + hash, + _network!, + currentChainTip, + confirmations: confirmations, + date: 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 = []; @@ -479,9 +768,9 @@ class ElectrumWorker { return ElectrumTransactionBundle( original, ins: ins, - time: time, - confirmations: confirmations ?? 0, - isDateValidated: isDateValidated, + time: time ?? dates?.time, + confirmations: confirmations ?? dates?.confirmations ?? 0, + isDateValidated: dates?.isDateValidated, ); } @@ -695,7 +984,13 @@ class ElectrumWorker { isPending: false, isReplaced: false, date: DateTime.fromMillisecondsSinceEpoch( - (await getTxDate(txid, scanData.network)).time! * 1000, + (await getTxDate( + txid, + scanData.network, + scanData.chainTip, + )) + .time! * + 1000, ), confirmations: scanData.chainTip - tweakHeight + 1, isReceivedSilentPayment: true, @@ -799,14 +1094,22 @@ class ScanNode { class DateResult { final int? time; final int? height; + final int? confirmations; final bool? isDateValidated; - DateResult({this.time, this.height, this.isDateValidated}); + DateResult({ + this.time, + this.height, + this.isDateValidated, + this.confirmations, + }); } Future getTxDate( String txid, - BasedUtxoNetwork network, { + BasedUtxoNetwork network, + int currentChainTip, { + int? confirmations, DateTime? date, }) async { int? time; @@ -837,5 +1140,25 @@ Future getTxDate( } } catch (_) {} - return DateResult(time: time, height: height, isDateValidated: isDateValidated); + 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; + } + } + + return DateResult( + time: time, + height: height, + isDateValidated: isDateValidated, + confirmations: confirmations, + ); +} + +class TxToFetch { + final ElectrumTransactionInfo? tx; + final int height; + + TxToFetch({required this.height, this.tx}); } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a632b639bc..5a972a9860 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -69,6 +69,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int? initialMwebHeight, bool? alwaysScan, + super.didInitialSync, }) : super( mnemonic: mnemonic, password: password, @@ -248,6 +249,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, + didInitialSync: snp?.didInitialSync, ); } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 7799c1aa8f..1115cff092 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -36,6 +36,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + super.didInitialSync, }) : super( mnemonic: mnemonic, password: password, @@ -183,6 +184,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, + didInitialSync: snp?.didInitialSync, ); } From e3058c9ae2e21ecdc9ea37c707281db4493031e8 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 16 Jan 2025 15:56:02 -0300 Subject: [PATCH 54/64] feat(batch): fix initial sync + date update --- cw_bitcoin/lib/electrum_wallet.dart | 44 ++++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index da9542853e..1da72c11f9 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -329,11 +329,17 @@ abstract class ElectrumWalletBase await waitSendWorker(ElectrumWorkerHeadersSubscribeRequest()); // 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 + + final hadTxs = transactionHistory.transactions.isNotEmpty; + final isFirstSync = !_didInitialSync; + await updateTransactions(null, true); + _didInitialSync = true; + final nowHasTxs = transactionHistory.transactions.isNotEmpty; - if (!_didInitialSync && transactionHistory.transactions.isNotEmpty) { + final shouldUpdateDates = !hadTxs && isFirstSync && nowHasTxs; + if (shouldUpdateDates) { await updateTransactions(null, true); - _didInitialSync = true; } // INFO: THIRD: Get the full wallet's balance with all addresses considered @@ -1237,6 +1243,7 @@ abstract class ElectrumWalletBase @action Future onHistoriesResponse(List histories) async { final addressesWithHistory = []; + final newAddresses = []; BitcoinAddressType? lastDiscoveredType; await Future.wait(histories.map((addressHistory) async { @@ -1269,16 +1276,18 @@ abstract class ElectrumWalletBase lastDiscoveredType = addressRecord.type; // Discover new addresses for the same address type until the gap limit is respected - final newAddresses = await walletAddresses.discoverNewAddresses( - isChange: isChange, - derivationType: addressRecord.cwDerivationType, - addressType: addressRecord.type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType( - addressRecord.type, - isElectrum: [ - CWBitcoinDerivationType.electrum, - CWBitcoinDerivationType.old_electrum, - ].contains(addressRecord.cwDerivationType), + newAddresses.addAll( + await walletAddresses.discoverNewAddresses( + isChange: isChange, + derivationType: addressRecord.cwDerivationType, + addressType: addressRecord.type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType( + addressRecord.type, + isElectrum: [ + CWBitcoinDerivationType.electrum, + CWBitcoinDerivationType.old_electrum, + ].contains(addressRecord.cwDerivationType), + ), ), ); walletAddresses.updateAdresses(newAddresses); @@ -1290,15 +1299,16 @@ abstract class ElectrumWalletBase element.cwDerivationType == addressRecord.cwDerivationType); printV( "discovered ${newAddresses.length} new ${isChange ? "change" : "receive"} addresses, new total: ${newAddressList.length}"); - - if (newAddresses.isNotEmpty) { - // Update the transactions for the new discovered addresses - await updateTransactions(newAddresses); - } } } })); + // if the initial sync has been done, update the new discovered addresses + // if not, will update all again on startSync, with the new ones as well, to update dates + if (newAddresses.isNotEmpty && !_didInitialSync) { + await updateTransactions(newAddresses, true); + } + if (addressesWithHistory.isNotEmpty) { walletAddresses.updateAdresses(addressesWithHistory); } From 06762e7abc391c744cb075d44c6b1c212d189c3f Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 21 Jan 2025 14:02:01 -0300 Subject: [PATCH 55/64] feat: batch in sequences, misc reviews --- cw_bitcoin/lib/bitcoin_wallet.dart | 54 +-- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 11 +- cw_bitcoin/lib/electrum_transaction_info.dart | 36 +- cw_bitcoin/lib/electrum_wallet.dart | 224 +++++----- .../lib/electrum_worker/electrum_worker.dart | 403 ++++++++++-------- .../electrum_worker_params.dart | 11 +- .../electrum_worker/methods/broadcast.dart | 17 +- .../methods/check_tweaks_method.dart | 19 +- .../electrum_worker/methods/connection.dart | 7 + .../electrum_worker/methods/get_balance.dart | 17 +- .../lib/electrum_worker/methods/get_fees.dart | 17 +- .../electrum_worker/methods/get_history.dart | 7 + .../methods/get_tx_expanded.dart | 7 + .../methods/headers_subscribe.dart | 15 +- .../electrum_worker/methods/list_unspent.dart | 17 +- .../methods/scripthashes_subscribe.dart | 12 +- .../methods/stop_scanning.dart | 19 +- .../methods/tweaks_subscribe.dart | 12 +- .../lib/electrum_worker/methods/version.dart | 19 +- cw_bitcoin/lib/litecoin_wallet.dart | 43 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 6 +- cw_bitcoin/lib/wallet_seed_bytes.dart | 50 ++- .../lib/src/bitcoin_cash_wallet.dart | 28 +- cw_tron/lib/tron_http_provider.dart | 25 +- .../screens/dashboard/pages/address_page.dart | 56 +-- lib/store/settings_store.dart | 51 ++- 26 files changed, 678 insertions(+), 505 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 482f5c9933..b6d3de7370 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -12,8 +12,6 @@ import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/psbt_transaction_builder.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_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; @@ -45,45 +43,32 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { bool allowedToSwitchNodesForScanning = false; BitcoinWalletBase({ - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required EncryptionFileUtils encryptionFileUtils, - List? seedBytes, - String? mnemonic, - String? xpub, + required super.password, + required super.walletInfo, + required super.unspentCoinsInfo, + required super.encryptionFileUtils, + super.mnemonic, + super.xpub, String? addressPageType, BasedUtxoNetwork? networkParam, List? initialAddresses, - ElectrumBalance? initialBalance, + super.initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, - String? passphrase, + super.passphrase, List? initialSilentAddresses, int initialSilentAddressIndex = 0, - bool? alwaysScan, - super.hdWallets, + super.alwaysScan, super.initialUnspentCoins, super.didInitialSync, }) : super( - mnemonic: mnemonic, - passphrase: passphrase, - xpub: xpub, - password: password, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, network: networkParam == null ? BitcoinNetwork.mainnet : networkParam == BitcoinNetwork.mainnet ? BitcoinNetwork.mainnet : BitcoinNetwork.testnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - encryptionFileUtils: encryptionFileUtils, currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, - alwaysScan: alwaysScan, ) { walletAddresses = BitcoinWalletAddresses( walletInfo, @@ -115,8 +100,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, }) async { - final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); - return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", @@ -128,8 +111,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - seedBytes: walletSeedBytes.seedBytes, - hdWallets: walletSeedBytes.hdWallets, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -188,22 +169,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; - List? seedBytes = null; - final Map hdWallets = {}; - final mnemonic = keysData.mnemonic; - final passphrase = keysData.passphrase; - - if (mnemonic != null) { - final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); - seedBytes = walletSeedBytes.seedBytes; - hdWallets.addAll(walletSeedBytes.hdWallets); - } - return BitcoinWallet( - mnemonic: mnemonic, + mnemonic: keysData.mnemonic, xpub: keysData.xPub, password: password, - passphrase: passphrase, + passphrase: keysData.passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp?.addresses, @@ -211,13 +181,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, initialBalance: snp?.balance, encryptionFileUtils: encryptionFileUtils, - seedBytes: seedBytes, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, - hdWallets: hdWallets, initialUnspentCoins: snp?.unspentCoins, didInitialSync: snp?.didInitialSync, ); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index e472cad1d4..e5f301e5eb 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -9,8 +9,6 @@ part 'bitcoin_wallet_addresses.g.dart'; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; -const OLD_SP_SPEND_PATH = "m/352'/1'/0'/0'/0"; - abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { BitcoinWalletAddressesBase( WalletInfo walletInfo, { @@ -31,6 +29,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S silentPaymentWallets = [silentPaymentWallet!]; } + static const OLD_SP_SPEND_PATH = "m/352'/1'/0'/0'/0"; + static const BITCOIN_ADDRESS_TYPES = [ + SegwitAddressType.p2wpkh, + P2pkhAddressType.p2pkh, + SegwitAddressType.p2tr, + SegwitAddressType.p2wsh, + P2shAddressType.p2wpkhInP2sh, + ]; + @observable SilentPaymentOwner? silentPaymentWallet; final ObservableList silentPaymentAddresses; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 6651c1b971..2f01c605fd 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -46,6 +46,8 @@ class ElectrumTransactionBundle { class ElectrumTransactionInfo extends TransactionInfo { bool isReceivedSilentPayment; int? time; + List? ins; + BtcTransaction? original; ElectrumTransactionInfo( this.type, { @@ -65,6 +67,8 @@ class ElectrumTransactionInfo extends TransactionInfo { String? to, this.isReceivedSilentPayment = false, Map? additionalInfo, + this.ins, + this.original, }) { this.id = id; this.height = height; @@ -156,17 +160,19 @@ 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( - BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) { - direction = TransactionDirection.outgoing; - inputAddresses.add( - BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network), - ); + if (bundle.ins.length > 0) { + 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), + ); + } } } @@ -226,6 +232,8 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: bundle.confirmations, time: bundle.time, isDateValidated: bundle.isDateValidated, + ins: bundle.ins, + original: bundle.originalTransaction, ); } @@ -253,6 +261,10 @@ class ElectrumTransactionInfo extends TransactionInfo { time: data['time'] as int?, isDateValidated: data['isDateValidated'] as bool?, additionalInfo: data['additionalInfo'] as Map?, + ins: + (data['ins'] as List?)?.map((e) => BtcTransaction.fromRaw(e as String)).toList(), + original: + data['original'] != null ? BtcTransaction.fromRaw(data['original'] as String) : null, ); } @@ -312,6 +324,8 @@ class ElectrumTransactionInfo extends TransactionInfo { m['isReceivedSilentPayment'] = isReceivedSilentPayment; m['additionalInfo'] = additionalInfo; m['isDateValidated'] = isDateValidated; + m['ins'] = ins?.map((e) => e.toHex()).toList(); + m['original'] = original?.toHex(); return m; } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 1da72c11f9..fe00a41f3b 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -8,6 +8,7 @@ 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:cw_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; @@ -56,30 +57,18 @@ abstract class ElectrumWalletBase ElectrumWalletBase({ required String password, required WalletInfo walletInfo, - required Box unspentCoinsInfo, + required this.unspentCoinsInfo, required this.network, required this.encryptionFileUtils, - Map? hdWallets, String? xpub, String? mnemonic, - List? seedBytes, this.passphrase, - List? initialAddresses, ElectrumBalance? initialBalance, CryptoCurrency? currency, this.alwaysScan, List? initialUnspentCoins, bool? didInitialSync, - }) : hdWallets = hdWallets ?? - { - CWBitcoinDerivationType.bip39: getAccountHDWallet( - currency, - network, - seedBytes, - xpub, - walletInfo.derivationInfo, - ) - }, + }) : _xpub = xpub, syncStatus = NotConnectedSyncStatus(), _password = password, isEnabledAutoGenerateSubaddress = true, @@ -95,12 +84,12 @@ abstract class ElectrumWalletBase ) } : {}), - this.unspentCoinsInfo = unspentCoinsInfo, this.isTestnet = !network.isMainnet, this._mnemonic = mnemonic, _didInitialSync = didInitialSync ?? false, super(walletInfo) { - this.walletInfo = walletInfo; + getAccountHDWallets(); + transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, password: password, @@ -112,8 +101,27 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } + void getAccountHDWallets() { + if (_mnemonic == null && _xpub == null) { + throw Exception( + "To create a Wallet you need either a seed or an xpub. This should not happen"); + } + + late WalletSeedData walletSeedData; + if (_mnemonic != null) { + walletSeedData = WalletSeedData.fromMnemonic(walletInfo, _mnemonic!, passphrase); + } else { + walletSeedData = WalletSeedData.fromXpub(walletInfo, _xpub!, network); + } + + _hdWallets = walletSeedData.hdWallets; + } + // Sends a request to the worker and returns a future that completes when the worker responds - Future waitSendWorker(ElectrumWorkerRequest request) { + Future waitSendWorker( + ElectrumWorkerRequest request, [ + Duration timeout = const Duration(seconds: 30), + ]) { final messageId = ++_messageId; final completer = Completer(); @@ -146,11 +154,7 @@ abstract class ElectrumWalletBase 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); - } + final completed = messageJson['completed'] as bool?; switch (workerMethod) { case ElectrumWorkerMethods.connectionMethod: @@ -183,25 +187,13 @@ abstract class ElectrumWalletBase onFeesResponse(response.result); break; } - } - static Bip32Slip10Secp256k1 getAccountHDWallet( - CryptoCurrency? currency, - BasedUtxoNetwork network, - List? seedBytes, - String? xpub, - DerivationInfo? derivationInfo, - ) { - if (seedBytes == null && xpub == null) { - throw Exception( - "To create a Wallet you need either a seed or an xpub. This should not happen"); - } + final shouldComplete = workerError != null || completed == true; - if (seedBytes != null) { - return Bip32Slip10Secp256k1.fromSeed(seedBytes, getKeyNetVersion(network)); + if (shouldComplete && responseId != null && _responseCompleters.containsKey(responseId)) { + _responseCompleters[responseId]!.complete(message); + _responseCompleters.remove(responseId); } - - return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network)); } int estimatedTransactionSize({ @@ -218,18 +210,10 @@ abstract class ElectrumWalletBase enableRBF: enableRBF, ); - static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) { - switch (network) { - case LitecoinNetwork.mainnet: - return Bip44Conf.litecoinMainNet.altKeyNetVer; - default: - return null; - } - } - bool? alwaysScan; - final Map hdWallets; + late Map _hdWallets; + Map get hdWallets => _hdWallets; Bip32Slip10Secp256k1 get hdWallet => walletAddresses.hdWallet; final String? _mnemonic; @@ -255,7 +239,8 @@ abstract class ElectrumWalletBase @observable SyncStatus syncStatus; - String get xpub => hdWallet.publicKey.toExtended; + String? _xpub; + String get xpub => _xpub ?? hdWallet.publicKey.toExtended; @override String? get seed => _mnemonic; @@ -292,9 +277,7 @@ abstract class ElectrumWalletBase @observable TransactionPriorities? feeRates; - int feeRate(TransactionPriority priority) { - return feeRates![priority]; - } + int feeRate(TransactionPriority priority) => feeRates![priority]; @observable List scripthashesListening; @@ -1241,79 +1224,96 @@ abstract class ElectrumWalletBase } @action - Future onHistoriesResponse(List histories) async { - final addressesWithHistory = []; + Future checkAddressesGap() async { final newAddresses = []; - BitcoinAddressType? lastDiscoveredType; + final discoveredTypes = []; - await Future.wait(histories.map((addressHistory) async { - final txs = addressHistory.txs; + await Future.forEach(walletAddresses.allAddresses, (BitcoinAddressRecord addressRecord) async { + final isChange = addressRecord.isChange; - if (txs.isNotEmpty) { - final addressRecord = addressHistory.addressRecord; - final isChange = addressRecord.isChange; + final matchingAddressList = + (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( + (element) => + element.type == addressRecord.type && + element.cwDerivationType == addressRecord.cwDerivationType, + ); + final totalMatchingAddresses = matchingAddressList.length; + + final matchingGapLimit = (isChange + ? ElectrumWalletAddressesBase.defaultChangeAddressesCount + : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + + final isAddressUsedAboveGap = + addressRecord.index >= totalMatchingAddresses - matchingGapLimit; + + if (isAddressUsedAboveGap && !discoveredTypes.contains(addressRecord.type)) { + discoveredTypes.add(addressRecord.type); + + newAddresses.addAll( + await walletAddresses.discoverNewAddresses( + isChange: isChange, + derivationType: addressRecord.cwDerivationType, + addressType: addressRecord.type, + derivationInfo: BitcoinAddressUtils.getDerivationFromType( + addressRecord.type, + isElectrum: [ + CWBitcoinDerivationType.electrum, + CWBitcoinDerivationType.old_electrum, + ].contains(addressRecord.cwDerivationType), + ), + ), + ); + walletAddresses.updateAdresses(newAddresses); - final addressList = + final newMatchingAddressList = (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( - (element) => - element.type == addressRecord.type && - element.cwDerivationType == addressRecord.cwDerivationType); - final totalAddresses = addressList.length; + (element) => + element.type == addressRecord.type && + element.cwDerivationType == addressRecord.cwDerivationType, + ); + printV( + "discovered ${newAddresses.length} new ${isChange ? "change" : "receive"} addresses"); + printV( + "Of type ${addressRecord.type} and derivation type ${addressRecord.cwDerivationType}, new total: ${newMatchingAddressList.length}"); + } + }); - final gapLimit = (isChange - ? ElectrumWalletAddressesBase.defaultChangeAddressesCount - : ElectrumWalletAddressesBase.defaultReceiveAddressesCount); + // if the initial sync has been done, fetch histories for the new discovered addresses + // if not, will update all again during startSync, with the new ones as well, to update dates + if (newAddresses.isNotEmpty && !_didInitialSync) { + await updateTransactions(newAddresses, true); + } - addressesWithHistory.add(addressRecord); + walletAddresses.updateHiddenAddresses(); - for (final tx in txs) { - transactionHistory.addOne(tx); - } + await save(); + } - final hasUsedAddressesUnderGap = addressRecord.index >= totalAddresses - gapLimit; - - if (hasUsedAddressesUnderGap && lastDiscoveredType != addressRecord.type) { - lastDiscoveredType = addressRecord.type; - - // Discover new addresses for the same address type until the gap limit is respected - newAddresses.addAll( - await walletAddresses.discoverNewAddresses( - isChange: isChange, - derivationType: addressRecord.cwDerivationType, - addressType: addressRecord.type, - derivationInfo: BitcoinAddressUtils.getDerivationFromType( - addressRecord.type, - isElectrum: [ - CWBitcoinDerivationType.electrum, - CWBitcoinDerivationType.old_electrum, - ].contains(addressRecord.cwDerivationType), - ), - ), - ); - walletAddresses.updateAdresses(newAddresses); - - final newAddressList = - (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( - (element) => - element.type == addressRecord.type && - element.cwDerivationType == addressRecord.cwDerivationType); - printV( - "discovered ${newAddresses.length} new ${isChange ? "change" : "receive"} addresses, new total: ${newAddressList.length}"); + @action + Future onHistoriesResponse(List histories) async { + if (histories.isNotEmpty) { + final addressesWithHistory = []; + + await Future.wait(histories.map((addressHistory) async { + final txs = addressHistory.txs; + + if (txs.isNotEmpty) { + addressesWithHistory.add(addressHistory.addressRecord); + + for (final tx in txs) { + transactionHistory.addOne(tx); + } } - } - })); + })); - // if the initial sync has been done, update the new discovered addresses - // if not, will update all again on startSync, with the new ones as well, to update dates - if (newAddresses.isNotEmpty && !_didInitialSync) { - await updateTransactions(newAddresses, true); - } + if (addressesWithHistory.isNotEmpty) { + walletAddresses.updateAdresses(addressesWithHistory); + } - if (addressesWithHistory.isNotEmpty) { - walletAddresses.updateAdresses(addressesWithHistory); + await save(); + } else { + checkAddressesGap(); } - - walletAddresses.updateHiddenAddresses(); } Future canReplaceByFee(ElectrumTransactionInfo tx) async { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 25016dd060..3f41b49c63 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -183,7 +183,8 @@ class ElectrumWorker { Future _handleScriphashesSubscribe( ElectrumWorkerScripthashesSubscribeRequest request, ) async { - await Future.wait(request.scripthashByAddress.entries.map((entry) async { + await Future.forEach(request.scripthashByAddress.entries, + (MapEntry entry) async { final address = entry.key; final scripthash = entry.value.toString(); @@ -210,7 +211,7 @@ class ElectrumWorker { id: request.id, )); }); - })); + }); } Future _handleGetInitialHistory(ElectrumWorkerGetHistoryRequest request) async { @@ -246,24 +247,61 @@ class ElectrumWorker { } if (transactionIdsForHeights.isNotEmpty) { - final transactions = await _getInitialBatchTransactionsExpanded( - hashesForHeights: transactionIdsForHeights, - currentChainTip: request.chainTip, + final transactionsVerbose = await _getBatchTransactionVerbose( + hashes: transactionIdsForHeights.keys.toList(), ); - transactions.forEach((tx) { + Map transactionHexes = {}; + + if (transactionsVerbose.isEmpty) { + transactionHexes = await _getBatchTransactionHex( + hashes: transactionIdsForHeights.keys.toList(), + ); + } else { + transactionsVerbose.values.forEach((e) { + transactionHexes[e['txid'] as String] = e['hex'] as String; + }); + } + + for (final transactionIdHeight in transactionHexes.entries) { + final hash = transactionIdHeight.key; + final hex = transactionIdHeight.value; + + final transactionVerbose = transactionsVerbose[hash]; + + late ElectrumTransactionBundle txBundle; + + // this is the initial tx history update, so ins will be filled later one by one, + // and time and confirmations will be updated if needed again + if (transactionVerbose != null) { + txBundle = ElectrumTransactionBundle( + BtcTransaction.fromRaw(hex), + ins: [], + time: transactionVerbose['time'] as int?, + confirmations: (transactionVerbose['confirmations'] as int?) ?? 1, + isDateValidated: (transactionVerbose['time'] as int?) != null, + ); + } else { + txBundle = ElectrumTransactionBundle( + BtcTransaction.fromRaw(hex), + ins: [], + confirmations: 1, + ); + } + final txInfo = ElectrumTransactionInfo.fromElectrumBundle( - tx, + txBundle, request.walletType, request.network, addresses: addresses.toSet(), - // height: height, + height: transactionIdsForHeights[hash], ); request.addresses.forEach( (addr) { final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || (txInfo.inputAddresses?.contains(addr.address) ?? false); + if (usedAddress == true) { addr.setAsUsed(); addr.txCount++; @@ -277,29 +315,43 @@ class ElectrumWorker { txs: [txInfo], walletType: request.walletType, ); + + return _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [ + AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ) + ], + id: request.id, + completed: false, + ), + ); } } }, ); - }); - - if (histories.isNotEmpty) { - _sendResponse( - ElectrumWorkerGetHistoryResponse( - result: histories.values.toList(), - id: request.id, - ), - ); } } + + return _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [], + id: request.id, + completed: true, + ), + ); } Future _handleGetHistory(ElectrumWorkerGetHistoryRequest request) async { if (request.storedTxs.isEmpty) { + // _handleGetInitialHistory only gets enough data to update the UI initially, + // then _handleGetHistory will be used to validate and update the dates, confirmations, and ins return await _handleGetInitialHistory(request); } - var histories = {}; final scripthashes = []; final addresses = []; request.addresses.forEach((addr) { @@ -317,12 +369,12 @@ class ElectrumWorker { ), ); - final transactionIdsHeights = {}; + final transactionsByIds = {}; - await Future.wait(historyBatches.map((result) async { + for (final result in historyBatches) { final history = result.result; if (history.isEmpty) { - return; + continue; } for (final transaction in history) { @@ -344,72 +396,129 @@ class ElectrumWorker { } 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 == null || (tx.isDateValidated != true && request.mempoolAPIEnabled)) { - transactionIdsHeights[txid] = TxToFetch(height: height, tx: tx); + // since sometimes the mempool api returns the wrong date at first + if (tx == null || + tx.original == null || + // TODO: use mempool api or tx verbose + // (tx.isDateValidated != true && request.mempoolAPIEnabled)) { + (tx.isDateValidated != true)) { + transactionsByIds[txid] = TxToFetch(height: height, tx: tx); } } - })); - - final txInfos = [...request.storedTxs]; + } - if (transactionIdsHeights.isNotEmpty) { - final transactions = await _getBatchTransactionsExpanded( - txsForHeights: transactionIdsHeights, - currentChainTip: request.chainTip, - mempoolAPIEnabled: request.mempoolAPIEnabled, - getTime: true, + if (transactionsByIds.isNotEmpty) { + final transactionsVerbose = await _getBatchTransactionVerbose( + hashes: transactionsByIds.keys.toList(), ); - transactions.entries.forEach((result) { - final hash = result.key; - final txBundle = result.value; + Map transactionHexes = {}; + + if (transactionsVerbose.isEmpty) { + transactionHexes = await _getBatchTransactionHex( + hashes: transactionsByIds.keys.toList(), + ); + } else { + transactionsVerbose.values.forEach((e) { + transactionHexes[e['txid'] as String] = e['hex'] as String; + }); + } + + await Future.forEach(transactionsByIds.entries, (MapEntry entry) async { + final hash = entry.key; + final txToFetch = entry.value; + final storedTx = txToFetch.tx; + final txVerbose = transactionsVerbose[hash]; + final txHex = transactionHexes[hash]!; + final original = + storedTx?.original ?? BtcTransaction.fromRaw((txVerbose?["hex"] as String?) ?? txHex); + + DateResult? date; + + if (txVerbose != null) { + date = DateResult( + time: txVerbose['time'] as int?, + confirmations: txVerbose['confirmations'] as int?, + isDateValidated: true, + ); + } else if (request.mempoolAPIEnabled) { + try { + date = await getTxDate( + hash, + _network!, + request.chainTip, + confirmations: storedTx?.confirmations, + date: storedTx?.date, + ); + } catch (_) {} + } + + final ins = []; + + final inputTransactionHexes = await _getBatchTransactionHex( + hashes: original.inputs.map((e) => e.txId).toList(), + ); + + for (final vin in original.inputs) { + final inputTransactionHex = inputTransactionHexes[vin.txId]!; + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } final txInfo = ElectrumTransactionInfo.fromElectrumBundle( - txBundle, + ElectrumTransactionBundle( + original, + ins: ins, + time: date?.time, + confirmations: date?.confirmations ?? 0, + isDateValidated: date?.isDateValidated, + ), request.walletType, request.network, addresses: addresses.toSet(), - height: transactionIdsHeights[hash]?.height, + height: transactionsByIds[hash]?.height, ); - txInfos.add(txInfo); - }); - } + var histories = {}; + request.addresses.forEach( + (addr) { + final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || + (txInfo.inputAddresses?.contains(addr.address) ?? false); + + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; - txInfos.forEach((txInfo) { - request.addresses.forEach( - (addr) { - final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || - (txInfo.inputAddresses?.contains(addr.address) ?? false); - - if (usedAddress == true) { - addr.setAsUsed(); - addr.txCount++; - - final addressHistories = histories[addr.address]; - if (addressHistories != null) { - addressHistories.txs.add(txInfo); - } else { - histories[addr.address] = AddressHistoriesResponse( - addressRecord: addr, - txs: [txInfo], - walletType: request.walletType, - ); + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(txInfo); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ); + } } - } - }, - ); - }); + }, + ); - if (histories.isNotEmpty) { - _sendResponse( - ElectrumWorkerGetHistoryResponse( - result: histories.values.toList(), - id: request.id, - ), - ); + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: request.id, + completed: false, + ), + ); + }); } + + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [], + id: request.id, + completed: true, + ), + ); } // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { @@ -476,7 +585,7 @@ class ElectrumWorker { Future _handleListUnspent(ElectrumWorkerListUnspentRequest request) async { final unspents = >{}; - await Future.wait(request.scripthashes.map((scriptHash) async { + await Future.forEach(request.scripthashes, (String scriptHash) async { if (scriptHash.isEmpty) { return; } @@ -490,7 +599,7 @@ class ElectrumWorker { if (scriptHashUnspents.isNotEmpty) { unspents[scriptHash] = scriptHashUnspents; } - })); + }); _sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id)); } @@ -572,11 +681,28 @@ class ElectrumWorker { insHashes.addAll(original.inputs.map((e) => e.txId)); } - final inputTransactionHexBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex( - transactionHashes: insHashes, - ), - ); + final inputTransactionHexBatches = >[]; + + try { + inputTransactionHexBatches.addAll( + await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex( + transactionHashes: insHashes, + ), + Duration(seconds: 10), + ), + ); + } catch (_) {} + + await Future.forEach(insHashes, (String hash) async { + inputTransactionHexBatches.addAll( + await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex( + transactionHashes: [hash], + ), + ), + ); + }); for (final txHex in transactionHexes) { final original = BtcTransaction.fromRaw(txHex); @@ -612,105 +738,22 @@ class ElectrumWorker { return bundles; } - Future> _getBatchTransactionsExpanded({ - required Map txsForHeights, - required int currentChainTip, - required bool mempoolAPIEnabled, - bool getTime = false, + Future>> _getBatchTransactionVerbose({ + required List hashes, }) async { - final hashes = txsForHeights.keys.toList(); - - final transactionVerboseBatches = await _electrumClient!.batchRequest( + final transactionHexBatches = await _electrumClient!.batchRequest( ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), ); - List transactionHexes = []; - List emptyVerboseTxs = []; - - transactionVerboseBatches.forEach((batch) { - final txVerbose = batch.result; - if (txVerbose.isEmpty) { - emptyVerboseTxs.add( - (batch.request.paramsById[batch.id] as List).first as String, - ); - } else { - transactionHexes.add(txVerbose['hex'] as String); - } - }); - - if (emptyVerboseTxs.isNotEmpty) { - transactionHexes.addAll((await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), - )) - .map((result) => result.result) - .toList()); - } - - final datesByHashes = {}; - - if (_walletType == WalletType.bitcoin) { - await Future.wait(hashes.map((hash) async { - try { - if (getTime && _walletType == WalletType.bitcoin && mempoolAPIEnabled) { - final tx = txsForHeights[hash]?.tx; - - try { - final dates = await getTxDate( - hash, - _network!, - currentChainTip, - date: tx?.date, - confirmations: tx?.confirmations, - ); - datesByHashes[hash] = dates; - } catch (_) {} - } - } catch (_) {} - })); - } - - final bundles = {}; - final insHashes = []; - - for (final txHex in transactionHexes) { - final original = BtcTransaction.fromRaw(txHex); - insHashes.addAll(original.inputs.map((e) => e.txId)); - } - - final inputTransactionHexBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex( - transactionHashes: insHashes, - ), - ); - - for (final txHex in transactionHexes) { - final original = BtcTransaction.fromRaw(txHex); - final ins = []; - - for (final input in original.inputs) { - try { - final inputTransactionHex = inputTransactionHexBatches - .firstWhere( - (batch) => - (batch.request.paramsById[batch.id] as List).first == input.txId, - ) - .result; - ins.add(BtcTransaction.fromRaw(inputTransactionHex)); - } catch (_) {} - } + final transactionsVerbose = >{}; - final dates = datesByHashes[original.txId()]; - - bundles[original.txId()] = ElectrumTransactionBundle( - original, - ins: ins, - time: dates?.time, - confirmations: dates?.confirmations ?? 0, - isDateValidated: dates?.isDateValidated, - ); - } + transactionHexBatches.forEach((result) { + final hash = result.request.paramsById[result.id]!.first as String; + final hex = result.result; + transactionsVerbose[hash] = hex; + }); - return bundles; + return transactionsVerbose; } Future _getTransactionExpanded({ @@ -774,6 +817,24 @@ class ElectrumWorker { ); } + Future> _getBatchTransactionHex({ + required List hashes, + }) async { + final transactionHexBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), + ); + + final transactionHexes = {}; + + transactionHexBatches.forEach((result) { + final hash = result.request.paramsById[result.id]!.first as String; + final hex = result.result; + transactionHexes[hash] = hex; + }); + + return transactionHexes; + } + Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { if (request.mempoolAPIEnabled && _walletType == WalletType.bitcoin) { try { diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart index ca96f29fe5..a91005e982 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_params.dart @@ -3,6 +3,7 @@ import 'package:cw_bitcoin/electrum_worker/electrum_worker_methods.dart'; abstract class ElectrumWorkerRequest { abstract final String method; abstract final int? id; + abstract final bool completed; Map toJson(); ElectrumWorkerRequest.fromJson(Map json); @@ -14,12 +15,14 @@ class ElectrumWorkerResponse { required this.result, this.error, this.id, + this.completed = true, }); final String method; final RESULT result; final String? error; final int? id; + final bool completed; RESPONSE resultJson(RESULT result) { throw UnimplementedError(); @@ -30,7 +33,13 @@ class ElectrumWorkerResponse { } Map toJson() { - return {'method': method, 'result': resultJson(result), 'error': error, 'id': id}; + return { + 'method': method, + 'result': resultJson(result), + 'error': error, + 'id': id, + 'completed': completed, + }; } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart index f295fa24a5..68fe3604f0 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/broadcast.dart @@ -1,10 +1,15 @@ part of 'methods.dart'; class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest { - ElectrumWorkerBroadcastRequest({required this.transactionRaw, this.id}); + ElectrumWorkerBroadcastRequest({ + required this.transactionRaw, + this.id, + this.completed = false, + }); final String transactionRaw; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.broadcast.method; @@ -14,12 +19,18 @@ class ElectrumWorkerBroadcastRequest implements ElectrumWorkerRequest { return ElectrumWorkerBroadcastRequest( transactionRaw: json['transactionRaw'] as String, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'transactionRaw': transactionRaw}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'transactionRaw': transactionRaw, + }; } } @@ -38,6 +49,7 @@ class ElectrumWorkerBroadcastResponse extends ElectrumWorkerResponse json) { - return ElectrumWorkerCheckTweaksRequest(id: json['id'] as int?); + return ElectrumWorkerCheckTweaksRequest( + id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, + ); } @override Map toJson() { - return {'method': method, 'id': id}; + return { + 'method': method, + 'id': id, + 'completed': completed, + }; } } @@ -31,6 +42,7 @@ class ElectrumWorkerCheckTweaksResponse extends ElectrumWorkerResponse toJson() { return { 'method': method, + 'id': id, + 'completed': completed, 'uri': uri.toString(), 'network': network.toString(), 'walletType': walletType.toString(), @@ -60,6 +65,7 @@ class ElectrumWorkerConnectionResponse extends ElectrumWorkerResponse scripthashes; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.getBalance.method; @@ -14,12 +19,18 @@ class ElectrumWorkerGetBalanceRequest implements ElectrumWorkerRequest { return ElectrumWorkerGetBalanceRequest( scripthashes: (json['scripthashes'] as List).toSet(), id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'scripthashes': scripthashes.toList()}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'scripthashes': scripthashes.toList(), + }; } } @@ -39,6 +50,7 @@ class ElectrumWorkerGetBalanceResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.getBalance.method); @override @@ -56,6 +68,7 @@ class ElectrumWorkerGetBalanceResponse ), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart index 68e5f07013..37c93edc43 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_fees.dart @@ -1,10 +1,15 @@ part of 'methods.dart'; class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { - ElectrumWorkerGetFeesRequest({this.mempoolAPIEnabled = false, this.id}); + ElectrumWorkerGetFeesRequest({ + this.mempoolAPIEnabled = false, + this.id, + this.completed = false, + }); final bool mempoolAPIEnabled; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.estimateFee.method; @@ -14,12 +19,18 @@ class ElectrumWorkerGetFeesRequest implements ElectrumWorkerRequest { return ElectrumWorkerGetFeesRequest( mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'mempoolAPIEnabled': mempoolAPIEnabled}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'mempoolAPIEnabled': mempoolAPIEnabled, + }; } } @@ -39,6 +50,7 @@ class ElectrumWorkerGetFeesResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.estimateFee.method); @override @@ -54,6 +66,7 @@ class ElectrumWorkerGetFeesResponse : deserializeTransactionPriorities(json['result'] as Map), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart index b5b32a7320..0aa1e24859 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_history.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_history.dart @@ -9,6 +9,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { required this.network, this.mempoolAPIEnabled = false, this.id, + this.completed = false, }); final List addresses; @@ -18,6 +19,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { final BasedUtxoNetwork network; final bool mempoolAPIEnabled; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.getHistory.method; @@ -38,6 +40,7 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { network: BasedUtxoNetwork.fromName(json['network'] as String), mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @@ -45,6 +48,8 @@ class ElectrumWorkerGetHistoryRequest implements ElectrumWorkerRequest { Map toJson() { return { 'method': method, + 'id': id, + 'completed': completed, 'addresses': addresses.map((e) => e.toJSON()).toList(), 'storedTxIds': storedTxs.map((e) => e.toJson()).toList(), 'walletType': walletType.index, @@ -100,6 +105,7 @@ class ElectrumWorkerGetHistoryResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.getHistory.method); @override @@ -115,6 +121,7 @@ class ElectrumWorkerGetHistoryResponse .toList(), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart index 07b49db6fb..907230acc2 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_expanded.dart @@ -6,12 +6,14 @@ class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { required this.currentChainTip, this.mempoolAPIEnabled = false, this.id, + this.completed = false, }); final String txHash; final int currentChainTip; final bool mempoolAPIEnabled; final int? id; + final bool completed; @override final String method = ElectrumWorkerMethods.txHash.method; @@ -23,6 +25,7 @@ class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { currentChainTip: json['currentChainTip'] as int, mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @@ -30,6 +33,8 @@ class ElectrumWorkerTxExpandedRequest implements ElectrumWorkerRequest { Map toJson() { return { 'method': method, + 'id': id, + 'completed': completed, 'txHash': txHash, 'currentChainTip': currentChainTip, 'mempoolAPIEnabled': mempoolAPIEnabled, @@ -53,6 +58,7 @@ class ElectrumWorkerTxExpandedResponse required ElectrumTransactionBundle expandedTx, super.error, super.id, + super.completed, }) : super(result: expandedTx, method: ElectrumWorkerMethods.txHash.method); @override @@ -66,6 +72,7 @@ class ElectrumWorkerTxExpandedResponse expandedTx: ElectrumTransactionBundle.fromJson(json['result'] as Map), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart index de02f5d249..eccc2717ac 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/headers_subscribe.dart @@ -1,22 +1,31 @@ part of 'methods.dart'; class ElectrumWorkerHeadersSubscribeRequest implements ElectrumWorkerRequest { - ElectrumWorkerHeadersSubscribeRequest({this.id}); + ElectrumWorkerHeadersSubscribeRequest({ + this.id, + this.completed = false, + }); @override final String method = ElectrumRequestMethods.headersSubscribe.method; final int? id; + final bool completed; @override factory ElectrumWorkerHeadersSubscribeRequest.fromJson(Map json) { return ElectrumWorkerHeadersSubscribeRequest( id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method}; + return { + 'method': method, + 'id': id, + 'completed': completed, + }; } } @@ -36,6 +45,7 @@ class ElectrumWorkerHeadersSubscribeResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.headersSubscribe.method); @override @@ -49,6 +59,7 @@ class ElectrumWorkerHeadersSubscribeResponse result: ElectrumHeaderResponse.fromJson(json['result'] as Map), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart index 66d1b1a68c..3013a07cc8 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/list_unspent.dart @@ -1,10 +1,15 @@ part of 'methods.dart'; class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest { - ElectrumWorkerListUnspentRequest({required this.scripthashes, this.id}); + ElectrumWorkerListUnspentRequest({ + required this.scripthashes, + this.id, + this.completed = false, + }); final List scripthashes; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.listunspent.method; @@ -14,12 +19,18 @@ class ElectrumWorkerListUnspentRequest implements ElectrumWorkerRequest { return ElectrumWorkerListUnspentRequest( scripthashes: json['scripthashes'] as List, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'scripthashes': scripthashes}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'scripthashes': scripthashes, + }; } } @@ -39,6 +50,7 @@ class ElectrumWorkerListUnspentResponse required Map> utxos, super.error, super.id, + super.completed, }) : super(result: utxos, method: ElectrumRequestMethods.listunspent.method); @override @@ -55,6 +67,7 @@ class ElectrumWorkerListUnspentResponse ), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart index 788314debf..bc02d04d6a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/scripthashes_subscribe.dart @@ -4,10 +4,12 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques ElectrumWorkerScripthashesSubscribeRequest({ required this.scripthashByAddress, this.id, + this.completed = false, }); final Map scripthashByAddress; final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.scriptHashSubscribe.method; @@ -17,12 +19,18 @@ class ElectrumWorkerScripthashesSubscribeRequest implements ElectrumWorkerReques return ElectrumWorkerScripthashesSubscribeRequest( scripthashByAddress: json['scripthashes'] as Map, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'scripthashes': scripthashByAddress}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'scripthashes': scripthashByAddress, + }; } } @@ -42,6 +50,7 @@ class ElectrumWorkerScripthashesSubscribeResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.scriptHashSubscribe.method); @override @@ -55,6 +64,7 @@ class ElectrumWorkerScripthashesSubscribeResponse result: json['result'] as Map?, error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart index a84a171b57..2870102e81 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/stop_scanning.dart @@ -1,21 +1,32 @@ part of 'methods.dart'; class ElectrumWorkerStopScanningRequest implements ElectrumWorkerRequest { - ElectrumWorkerStopScanningRequest({this.id}); + ElectrumWorkerStopScanningRequest({ + this.id, + this.completed = false, + }); final int? id; + final bool completed; @override final String method = ElectrumWorkerMethods.stopScanning.method; @override factory ElectrumWorkerStopScanningRequest.fromJson(Map json) { - return ElectrumWorkerStopScanningRequest(id: json['id'] as int?); + return ElectrumWorkerStopScanningRequest( + id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, + ); } @override Map toJson() { - return {'method': method, 'id': id}; + return { + 'method': method, + 'id': id, + 'completed': completed, + }; } } @@ -31,6 +42,7 @@ class ElectrumWorkerStopScanningResponse extends ElectrumWorkerResponse), id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } @override Map toJson() { - return {'method': method, 'scanData': scanData.toJson()}; + return { + 'method': method, + 'id': id, + 'completed': completed, + 'scanData': scanData.toJson(), + }; } } @@ -170,6 +178,7 @@ class ElectrumWorkerTweaksSubscribeResponse required super.result, super.error, super.id, + super.completed, }) : super(method: ElectrumRequestMethods.tweaksSubscribe.method); @override @@ -183,6 +192,7 @@ class ElectrumWorkerTweaksSubscribeResponse result: TweaksSyncResponse.fromJson(json['result'] as Map), error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/version.dart b/cw_bitcoin/lib/electrum_worker/methods/version.dart index 0f3f814d37..2c20aab36d 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/version.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/version.dart @@ -1,21 +1,32 @@ part of 'methods.dart'; class ElectrumWorkerGetVersionRequest implements ElectrumWorkerRequest { - ElectrumWorkerGetVersionRequest({this.id}); + ElectrumWorkerGetVersionRequest({ + this.id, + this.completed = false, + }); final int? id; + final bool completed; @override final String method = ElectrumRequestMethods.version.method; @override factory ElectrumWorkerGetVersionRequest.fromJson(Map json) { - return ElectrumWorkerGetVersionRequest(id: json['id'] as int?); + return ElectrumWorkerGetVersionRequest( + id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, + ); } @override Map toJson() { - return {'method': method}; + return { + 'method': method, + 'id': id, + 'completed': completed, + }; } } @@ -34,6 +45,7 @@ class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse, error: json['error'] as String?, id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 5a972a9860..92ea34bea2 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -8,7 +8,6 @@ import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/litecoin_wallet_snapshot.dart'; -import 'package:cw_bitcoin/wallet_seed_bytes.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -53,36 +52,26 @@ class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; abstract class LitecoinWalletBase extends ElectrumWallet with Store { LitecoinWalletBase({ - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required EncryptionFileUtils encryptionFileUtils, + required super.password, + required super.walletInfo, + required super.unspentCoinsInfo, + required super.encryptionFileUtils, List? seedBytes, - String? mnemonic, - String? xpub, + super.mnemonic, + super.xpub, String? passphrase, String? addressPageType, List? initialAddresses, List? initialMwebAddresses, - ElectrumBalance? initialBalance, + super.initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, int? initialMwebHeight, - bool? alwaysScan, + super.alwaysScan, super.didInitialSync, }) : super( - mnemonic: mnemonic, - password: password, - xpub: xpub, - walletInfo: walletInfo, - unspentCoinsInfo: unspentCoinsInfo, network: LitecoinNetwork.mainnet, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - seedBytes: seedBytes, - encryptionFileUtils: encryptionFileUtils, currency: CryptoCurrency.ltc, - alwaysScan: alwaysScan, ) { if (seedBytes != null) { mwebHd = @@ -160,8 +149,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, }) async { - final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); - return LitecoinWallet( mnemonic: mnemonic, password: password, @@ -172,7 +159,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, - seedBytes: walletSeedBytes.seedBytes, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: addressPageType, @@ -223,16 +209,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? ELECTRUM_PATH; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; - List? seedBytes = null; - final mnemonic = keysData.mnemonic; - final passphrase = keysData.passphrase; - - if (mnemonic != null) { - final walletSeedBytes = await WalletSeedBytes.getSeedBytes(walletInfo, mnemonic, passphrase); - seedBytes = walletSeedBytes.seedBytes; - // hdWallets.addAll(walletSeedBytes.hdWallets); - } - return LitecoinWallet( mnemonic: keysData.mnemonic, xpub: keysData.xPub, @@ -242,8 +218,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { initialAddresses: snp?.addresses, initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, - seedBytes: seedBytes, - passphrase: passphrase, + passphrase: keysData.passphrase, encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 89ae384d49..0abf5552dc 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -68,7 +68,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()))!; @@ -127,8 +126,9 @@ class LitecoinWalletService extends WalletService< } } - final unspentCoinsToDelete = unspentCoinsInfoSource.values.where( - (unspentCoin) => unspentCoin.walletId == walletInfo.id).toList(); + final unspentCoinsToDelete = unspentCoinsInfoSource.values + .where((unspentCoin) => unspentCoin.walletId == walletInfo.id) + .toList(); final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList(); diff --git a/cw_bitcoin/lib/wallet_seed_bytes.dart b/cw_bitcoin/lib/wallet_seed_bytes.dart index 34aadc7e70..4f5d8542e2 100644 --- a/cw_bitcoin/lib/wallet_seed_bytes.dart +++ b/cw_bitcoin/lib/wallet_seed_bytes.dart @@ -1,26 +1,21 @@ +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_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; -class WalletSeedBytes { - final List seedBytes; +class WalletSeedData { final Map hdWallets; - WalletSeedBytes({required this.seedBytes, required this.hdWallets}); + WalletSeedData({required this.hdWallets}); - static Future getSeedBytes( - WalletInfo walletInfo, - String mnemonic, [ - String? passphrase, - ]) async { - late List seedBytes; + static WalletSeedData fromMnemonic(WalletInfo walletInfo, String mnemonic, [String? passphrase]) { final Map hdWallets = {}; for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { if (derivation.derivationType == DerivationType.bip39) { try { - seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); + final seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { printV("bip39 seed error: $e"); @@ -30,19 +25,48 @@ class WalletSeedBytes { } if (derivation.derivationType == DerivationType.electrum) { + late List seedBytes; + try { seedBytes = ElectrumV2SeedGenerator.generateFromString(mnemonic, passphrase); - hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { printV("electrum_v2 seed error: $e"); try { seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); - hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); } catch (e) { printV("electrum_v1 seed error: $e"); } } + + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromSeed(seedBytes); + } + } + + if (hdWallets[CWBitcoinDerivationType.bip39] != null) { + hdWallets[CWBitcoinDerivationType.old_bip39] = hdWallets[CWBitcoinDerivationType.bip39]!; + } else if (hdWallets[CWBitcoinDerivationType.electrum] != null) { + hdWallets[CWBitcoinDerivationType.old_electrum] = + hdWallets[CWBitcoinDerivationType.electrum]!; + } + + return WalletSeedData(hdWallets: hdWallets); + } + + static WalletSeedData fromXpub(WalletInfo walletInfo, String xpub, BasedUtxoNetwork network) { + final Map hdWallets = {}; + + for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { + if (derivation.derivationType == DerivationType.bip39) { + hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromExtendedKey( + xpub, + BitcoinAddressUtils.getKeyNetVersion(network), + ); + } else if (derivation.derivationType == DerivationType.electrum) { + hdWallets[CWBitcoinDerivationType.electrum] = Bip32Slip10Secp256k1.fromExtendedKey( + xpub, + BitcoinAddressUtils.getKeyNetVersion(network), + ); } } @@ -54,6 +78,6 @@ class WalletSeedBytes { hdWallets[CWBitcoinDerivationType.electrum]!; } - return WalletSeedBytes(seedBytes: seedBytes, hdWallets: hdWallets); + return WalletSeedData(hdWallets: hdWallets); } } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 1115cff092..a0a8ab25c5 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -2,7 +2,6 @@ import 'package:bitbox/bitbox.dart' as bitbox; 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_mnemonics_bip39.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -24,32 +23,21 @@ class BitcoinCashWallet = BitcoinCashWalletBase with _$BitcoinCashWallet; abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { BitcoinCashWalletBase({ - required String mnemonic, - required String password, - required WalletInfo walletInfo, - required Box unspentCoinsInfo, - required Uint8List seedBytes, - required EncryptionFileUtils encryptionFileUtils, - String? passphrase, + required super.mnemonic, + required super.password, + required super.walletInfo, + required super.unspentCoinsInfo, + required super.encryptionFileUtils, + super.passphrase, BitcoinAddressType? addressPageType, List? initialAddresses, - ElectrumBalance? initialBalance, + super.initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, super.didInitialSync, }) : 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, - hdWallets: {CWBitcoinDerivationType.bip39: Bip32Slip10Secp256k1.fromSeed(seedBytes)}, ) { walletAddresses = BitcoinCashWalletAddresses( walletInfo, @@ -102,7 +90,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, initialBalance: initialBalance, - seedBytes: MnemonicBip39.toSeed(mnemonic, passphrase: passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, @@ -178,7 +165,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { } }).toList(), initialBalance: snp?.balance, - seedBytes: await MnemonicBip39.toSeed(keysData.mnemonic!, passphrase: keysData.passphrase), encryptionFileUtils: encryptionFileUtils, initialRegularAddressIndex: snp?.regularAddressIndex, initialChangeAddressIndex: snp?.changeAddressIndex, diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart index ea06ba0978..1aa7e80fe5 100644 --- a/cw_tron/lib/tron_http_provider.dart +++ b/cw_tron/lib/tron_http_provider.dart @@ -1,4 +1,5 @@ import 'package:http/http.dart' as http; +import '.secrets.g.dart' as secrets; import 'package:on_chain/tron/tron.dart'; class TronHTTPProvider implements TronServiceProvider { @@ -15,14 +16,28 @@ class TronHTTPProvider implements TronServiceProvider { @override Future> doRequest(TronRequestDetails params, {Duration? timeout}) async { - if (params.type.isPostRequest) { - final response = await client - .post(params.toUri(url), headers: params.headers, body: params.body()) - .timeout(timeout ?? defaultRequestTimeout); + if (!params.type.isPostRequest) { + final response = await client.get( + params.toUri(url), + headers: { + 'Content-Type': 'application/json', + if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, + }, + ).timeout(timeout ?? defaultRequestTimeout); return params.toResponse(response.bodyBytes, response.statusCode); } + final response = await client - .get(params.toUri(url), headers: params.headers) + .post( + params.toUri(url), + headers: { + 'Content-Type': 'application/json', + if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, + }, + body: params.body, + ) .timeout(timeout ?? defaultRequestTimeout); return params.toResponse(response.bodyBytes, response.statusCode); } diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index f39cd6a6ed..184d0a1ddc 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -75,52 +75,24 @@ class AddressPage extends BasePage { bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI; - 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, - ), + 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), ), + 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, - ), - ), - ), - ), - ), - ), - ], + ), ); } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 0430e46df6..318be637e5 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -226,11 +226,14 @@ abstract class SettingsStoreBase with Store { (FiatCurrency fiatCurrency) => sharedPreferences.setString( PreferencesKey.currentFiatCurrencyKey, fiatCurrency.serialize())); - reaction((_) => selectedCakePayCountry, (Country? country) { - if (country != null) { - sharedPreferences.setString(PreferencesKey.currentCakePayCountry, country.raw); - } - }); + reaction( + (_) => selectedCakePayCountry, + (Country? country) { + if (country != null) { + sharedPreferences.setString( + PreferencesKey.currentCakePayCountry, country.raw); + } + }); reaction( (_) => shouldShowYatPopup, @@ -288,11 +291,9 @@ abstract class SettingsStoreBase with Store { }); } - reaction( - (_) => disableTradeOption, - (bool disableTradeOption) => - sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); - + reaction((_) => disableTradeOption, + (bool disableTradeOption) => sharedPreferences.setBool(PreferencesKey.disableTradeOption, disableTradeOption)); + reaction( (_) => disableBulletin, (bool disableBulletin) => @@ -304,8 +305,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setInt(PreferencesKey.walletListOrder, walletListOrder.index)); reaction( - (_) => contactListOrder, - (FilterListOrderType contactListOrder) => + (_) => contactListOrder, + (FilterListOrderType contactListOrder) => sharedPreferences.setInt(PreferencesKey.contactListOrder, contactListOrder.index)); reaction( @@ -314,8 +315,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setBool(PreferencesKey.walletListAscending, walletListAscending)); reaction( - (_) => contactListAscending, - (bool contactListAscending) => + (_) => contactListAscending, + (bool contactListAscending) => sharedPreferences.setBool(PreferencesKey.contactListAscending, contactListAscending)); reaction( @@ -357,8 +358,8 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setBool(PreferencesKey.shouldShowMarketPlaceInDashboard, value)); reaction( - (_) => showAddressBookPopupEnabled, - (bool value) => + (_) => showAddressBookPopupEnabled, + (bool value) => sharedPreferences.setBool(PreferencesKey.showAddressBookPopupEnabled, value)); reaction((_) => pinCodeLength, @@ -860,10 +861,10 @@ abstract class SettingsStoreBase with Store { final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); - final savedCakePayCountryRaw = - sharedPreferences.getString(PreferencesKey.currentCakePayCountry); - final currentCakePayCountry = - savedCakePayCountryRaw != null ? Country.deserialize(raw: savedCakePayCountryRaw) : null; + final savedCakePayCountryRaw = sharedPreferences.getString(PreferencesKey.currentCakePayCountry); + final currentCakePayCountry = savedCakePayCountryRaw != null + ? Country.deserialize(raw: savedCakePayCountryRaw) + : null; TransactionPriority? moneroTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!); @@ -918,13 +919,12 @@ abstract class SettingsStoreBase with Store { final shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? false; final isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? false; - final disableTradeOption = - sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; + final disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? false; final disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? false; final walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; final contactListOrder = - FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; final walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; final contactListAscending = @@ -1372,14 +1372,13 @@ abstract class SettingsStoreBase with Store { numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; - disableTradeOption = - sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; + disableTradeOption = sharedPreferences.getBool(PreferencesKey.disableTradeOption) ?? disableTradeOption; disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin; walletListOrder = FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; contactListOrder = - FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; + FilterListOrderType.values[sharedPreferences.getInt(PreferencesKey.contactListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; contactListAscending = sharedPreferences.getBool(PreferencesKey.contactListAscending) ?? true; shouldShowMarketPlaceInDashboard = From aa5cb8ded9c9098f3548c8504599ef426d1c3580 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 22 Jan 2025 20:12:41 -0300 Subject: [PATCH 56/64] feat: subscription batch, addresses & scripthashes --- cw_bitcoin/lib/bitcoin_wallet.dart | 143 +++- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 208 ++++-- cw_bitcoin/lib/bitcoin_wallet_service.dart | 8 + cw_bitcoin/lib/bitcoin_wallet_snapshot.dart | 33 +- cw_bitcoin/lib/electrum_wallet.dart | 183 +++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 109 ++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 52 +- .../lib/electrum_worker/electrum_worker.dart | 707 +++++++++++++++--- .../electrum_worker/server_capability.dart | 72 ++ cw_bitcoin/lib/litecoin_wallet.dart | 122 +-- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 5 +- cw_bitcoin/lib/litecoin_wallet_service.dart | 8 + cw_bitcoin/lib/litecoin_wallet_snapshot.dart | 10 +- cw_bitcoin/lib/wallet_seed_bytes.dart | 16 +- .../lib/src/bitcoin_cash_wallet.dart | 81 +- .../src/bitcoin_cash_wallet_addresses.dart | 45 ++ lib/bitcoin/cw_bitcoin.dart | 30 +- lib/view_model/wallet_creation_vm.dart | 1 + 18 files changed, 1351 insertions(+), 482 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/server_capability.dart diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index b6d3de7370..14eb5f6b31 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -29,6 +29,7 @@ import 'package:hive/hive.dart'; import 'package:ledger_bitcoin/ledger_bitcoin.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:mobx/mobx.dart'; +import 'package:collection/collection.dart'; part 'bitcoin_wallet.g.dart'; @@ -47,20 +48,16 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required super.walletInfo, required super.unspentCoinsInfo, required super.encryptionFileUtils, + required super.hdWallets, super.mnemonic, super.xpub, - String? addressPageType, BasedUtxoNetwork? networkParam, - List? initialAddresses, super.initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, super.passphrase, - List? initialSilentAddresses, - int initialSilentAddressIndex = 0, super.alwaysScan, super.initialUnspentCoins, super.didInitialSync, + Map? walletAddressesSnapshot, }) : super( network: networkParam == null ? BitcoinNetwork.mainnet @@ -70,14 +67,22 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { currency: networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc, ) { - walletAddresses = BitcoinWalletAddresses( - walletInfo, - initialAddresses: initialAddresses, - initialSilentAddresses: initialSilentAddresses, - network: networkParam ?? network, - isHardwareWallet: walletInfo.isHardwareWallet, - hdWallets: hdWallets, - ); + if (walletAddressesSnapshot != null) { + walletAddresses = BitcoinWalletAddressesBase.fromJson( + walletAddressesSnapshot, + walletInfo, + network: network, + isHardwareWallet: isHardwareWallet, + hdWallets: hdWallets, + ); + } else { + this.walletAddresses = BitcoinWalletAddresses( + walletInfo, + network: networkParam ?? network, + isHardwareWallet: isHardwareWallet, + hdWallets: hdWallets, + ); + } autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; @@ -91,7 +96,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, String? passphrase, - String? addressPageType, BasedUtxoNetwork? network, List? initialAddresses, List? initialSilentAddresses, @@ -100,21 +104,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { Map? initialChangeAddressIndex, int initialSilentAddressIndex = 0, }) async { + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: network ?? BitcoinNetwork.mainnet, + mnemonic: mnemonic, + passphrase: passphrase, + ); + return BitcoinWallet( mnemonic: mnemonic, passphrase: passphrase ?? "", password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialSilentAddresses: initialSilentAddresses, - initialSilentAddressIndex: initialSilentAddressIndex, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - addressPageType: addressPageType, networkParam: network, + hdWallets: hdWallets, ); } @@ -169,6 +175,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: network, + mnemonic: keysData.mnemonic, + passphrase: keysData.passphrase, + xpub: keysData.xPub, + ); + return BitcoinWallet( mnemonic: keysData.mnemonic, xpub: keysData.xPub, @@ -176,18 +190,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { passphrase: keysData.passphrase, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses, - initialSilentAddresses: snp?.silentAddresses, - initialSilentAddressIndex: snp?.silentAddressIndex ?? 0, initialBalance: snp?.balance, encryptionFileUtils: encryptionFileUtils, - initialRegularAddressIndex: snp?.regularAddressIndex, - initialChangeAddressIndex: snp?.changeAddressIndex, - addressPageType: snp?.addressPageType, networkParam: network, alwaysScan: alwaysScan, initialUnspentCoins: snp?.unspentCoins, didInitialSync: snp?.didInitialSync, + walletAddressesSnapshot: snp?.walletAddressesSnapshot, + hdWallets: hdWallets, ); } @@ -466,6 +476,31 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } + @override + @action + Future addCoinInfo(BitcoinUnspent coin) async { + // Check if the coin is already in the unspentCoinsInfo for the wallet + final existingCoinInfo = unspentCoinsInfo.values + .firstWhereOrNull((element) => element.walletId == walletInfo.id && element == coin); + + if (existingCoinInfo == null) { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.bitcoinAddressRecord.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + isSilentPayment: coin.address is BitcoinReceivedSPAddressRecord, + ); + + await unspentCoinsInfo.add(newInfo); + } + } + @action @override Future startSync() async { @@ -778,7 +813,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); @override - TxCreateUtxoDetails createUTXOS({ + BitcoinTxCreateUtxoDetails createUTXOS({ required bool sendAll, bool paysToSilentPayment = false, int credentialsAmount = 0, @@ -893,7 +928,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { throw BitcoinTransactionNoInputsException(); } - return TxCreateUtxoDetails( + return BitcoinTxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -907,7 +942,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - Future estimateSendAllTx( + Future estimateSendAllTx( List outputs, int feeRate, { String? memo, @@ -944,7 +979,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } - return EstimatedTxResult( + return BitcoinEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -959,7 +994,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @override - Future estimateTxForAmount( + Future estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { @@ -1011,10 +1046,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress( - inputs: utxoDetails.availableInputs, - outputs: updatedOutputs, - ); + final changeAddress = await walletAddresses.getChangeAddress(); final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); updatedOutputs.add(BitcoinOutput( address: address, @@ -1083,7 +1115,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - return EstimatedTxResult( + return BitcoinEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -1110,7 +1142,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { isChange: true, ); - return EstimatedTxResult( + return BitcoinEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -1180,7 +1212,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ? transactionCredentials.feeRate! : feeRate(transactionCredentials.priority!); - EstimatedTxResult estimatedTx; + BitcoinEstimatedTx estimatedTx; final updatedOutputs = outputs .map((e) => BitcoinOutput( address: e.address, @@ -1321,3 +1353,36 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } } + +class BitcoinEstimatedTx extends ElectrumEstimatedTx { + BitcoinEstimatedTx({ + required super.utxos, + required super.inputPrivKeyInfos, + required super.publicKeys, + required super.fee, + required super.amount, + required super.hasChange, + required super.isSendAll, + required super.spendsUnconfirmedTX, + super.memo, + this.spendsSilentPayment = false, + }); + + final bool spendsSilentPayment; +} + +class BitcoinTxCreateUtxoDetails extends ElectrumTxCreateUtxoDetails { + final bool spendsSilentPayment; + + BitcoinTxCreateUtxoDetails({ + required super.availableInputs, + required super.unconfirmedCoins, + required super.utxos, + required super.vinOutpoints, + required super.inputPrivKeyInfos, + required super.publicKeys, + required super.allInputsAmount, + required super.spendsUnconfirmedTX, + this.spendsSilentPayment = false, + }); +} diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index e5f301e5eb..6f3063f837 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -51,78 +53,89 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override Future init() async { - await generateInitialAddresses(type: SegwitAddressType.p2wpkh); + // TODO: if restored from snapshot - if (!isHardwareWallet) { - await generateInitialAddresses(type: P2pkhAddressType.p2pkh); - await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); - await generateInitialAddresses(type: SegwitAddressType.p2tr); - await generateInitialAddresses(type: SegwitAddressType.p2wsh); - - if (walletInfo.isRecovery) { - final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); - final oldSpendPath = Bip32PathParser.parse("m/352'/1'/0'/0'/0"); - - final oldSilentPaymentWallet = SilentPaymentOwner.fromPrivateKeys( - b_scan: ECPrivate(hdWallet.derive(oldScanPath).privateKey), - b_spend: ECPrivate(hdWallet.derive(oldSpendPath).privateKey), - ); + if (allAddresses.where((address) => address.type == SegwitAddressType.p2wpkh).isEmpty) { + await generateInitialAddresses(type: SegwitAddressType.p2wpkh); + } - silentPaymentWallets.add(oldSilentPaymentWallet); - silentPaymentAddresses.addAll( - [ - BitcoinSilentPaymentAddressRecord( - oldSilentPaymentWallet.toString(), - labelIndex: 0, - name: "", - type: SilentPaymentsAddresType.p2sp, - derivationPath: oldSpendPath.toString(), - isHidden: true, - isChange: false, - ), - BitcoinSilentPaymentAddressRecord( - oldSilentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), - name: "", - labelIndex: 0, - labelHex: BytesUtils.toHexString(oldSilentPaymentWallet.generateLabel(0)), - type: SilentPaymentsAddresType.p2sp, - derivationPath: oldSpendPath.toString(), - isHidden: true, - isChange: true, - ), - ], - ); + if (!isHardwareWallet) { + if (allAddresses.where((address) => address.type == P2pkhAddressType.p2pkh).isEmpty) + await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + + if (allAddresses.where((address) => address.type == P2shAddressType.p2wpkhInP2sh).isEmpty) + await generateInitialAddresses(type: P2shAddressType.p2wpkhInP2sh); + + if (allAddresses.where((address) => address.type == SegwitAddressType.p2tr).isEmpty) + await generateInitialAddresses(type: SegwitAddressType.p2tr); + + if (allAddresses.where((address) => address.type == SegwitAddressType.p2wsh).isEmpty) + await generateInitialAddresses(type: SegwitAddressType.p2wsh); + + if (silentPaymentAddresses.isEmpty) { + if (walletInfo.isRecovery) { + final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); + final oldSpendPath = Bip32PathParser.parse("m/352'/1'/0'/0'/0"); + + final oldSilentPaymentWallet = SilentPaymentOwner.fromPrivateKeys( + b_scan: ECPrivate(hdWallet.derive(oldScanPath).privateKey), + b_spend: ECPrivate(hdWallet.derive(oldSpendPath).privateKey), + ); + + silentPaymentWallets.add(oldSilentPaymentWallet); + silentPaymentAddresses.addAll( + [ + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toString(), + labelIndex: 0, + name: "", + type: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + isChange: false, + ), + BitcoinSilentPaymentAddressRecord( + oldSilentPaymentWallet.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(oldSilentPaymentWallet.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + derivationPath: oldSpendPath.toString(), + isHidden: true, + isChange: true, + ), + ], + ); + } + + silentPaymentAddresses.addAll([ + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toString(), + labelIndex: 0, + name: "", + type: SilentPaymentsAddresType.p2sp, + isChange: false, + ), + BitcoinSilentPaymentAddressRecord( + silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), + name: "", + labelIndex: 0, + labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), + type: SilentPaymentsAddresType.p2sp, + isChange: true, + ), + ]); } - - silentPaymentAddresses.addAll([ - BitcoinSilentPaymentAddressRecord( - silentPaymentWallet!.toString(), - labelIndex: 0, - name: "", - type: SilentPaymentsAddresType.p2sp, - isChange: false, - ), - BitcoinSilentPaymentAddressRecord( - silentPaymentWallet!.toLabeledSilentPaymentAddress(0).toString(), - name: "", - labelIndex: 0, - labelHex: BytesUtils.toHexString(silentPaymentWallet!.generateLabel(0)), - type: SilentPaymentsAddresType.p2sp, - isChange: true, - ), - ]); - - updateHiddenAddresses(); } - await updateAddressesInBox(); + super.init(); } @action Future generateSilentPaymentAddresses({required BitcoinAddressType type}) async { - final hasOldSPAddresses = silentPaymentAddresses.any((address) => - address.type == SilentPaymentsAddresType.p2sp && - address.derivationPath == OLD_SP_SPEND_PATH); + // final hasOldSPAddresses = silentPaymentAddresses.any((address) => + // address.type == SilentPaymentsAddresType.p2sp && + // address.derivationPath == OLD_SP_SPEND_PATH); if (walletInfo.isRecovery) { final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); @@ -437,6 +450,75 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S json['silentPaymentAddresses'] = silentPaymentAddresses.map((address) => address.toJSON()).toList(); json['receivedSPAddresses'] = receivedSPAddresses.map((address) => address.toJSON()).toList(); + // json['silentAddressIndex'] = silentAddressIndex.toString(); return json; } + + static Map fromSnapshot(Map data) { + final electrumSnapshot = ElectrumWalletAddressesBase.fromSnapshot(data); + + final silentAddressesTmp = data['silent_addresses'] as List? ?? []; + final silentPaymentAddresses = []; + final receivedSPAddresses = []; + + silentAddressesTmp.whereType().forEach((j) { + final decoded = json.decode(j) as Map; + if (decoded['tweak'] != null || decoded['silent_payment_tweak'] != null) { + silentPaymentAddresses.add(BitcoinReceivedSPAddressRecord.fromJSON(j)); + } else { + receivedSPAddresses.add(BitcoinSilentPaymentAddressRecord.fromJSON(j)); + } + }); + var silentAddressIndex = 0; + + try { + silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); + } catch (_) {} + + return { + 'allAddresses': electrumSnapshot["addresses"], + 'addressPageType': data['address_page_type'] as String?, + 'receiveAddressIndexByType': electrumSnapshot["receiveAddressIndexByType"], + 'changeAddressIndexByType': electrumSnapshot["changeAddressIndexByType"], + 'silentPaymentAddresses': silentPaymentAddresses, + 'receivedSPAddresses': receivedSPAddresses, + 'silentAddressIndex': silentAddressIndex, + }; + } + + static BitcoinWalletAddressesBase fromJson( + Map json, + WalletInfo walletInfo, { + required Map hdWallets, + required BasedUtxoNetwork network, + required bool isHardwareWallet, + List? initialAddresses, + List? initialSilentAddresses, + List? initialReceivedSPAddresses, + }) { + initialAddresses ??= (json['allAddresses'] as List) + .map((record) => BitcoinAddressRecord.fromJSON(record as String)) + .toList(); + + initialSilentAddresses ??= (json['silentPaymentAddresses'] as List) + .map( + (address) => BitcoinSilentPaymentAddressRecord.fromJSON(address as String), + ) + .toList(); + initialReceivedSPAddresses ??= (json['receivedSPAddresses'] as List) + .map( + (address) => BitcoinReceivedSPAddressRecord.fromJSON(address as String), + ) + .toList(); + + return BitcoinWalletAddresses( + walletInfo, + hdWallets: hdWallets, + network: network, + isHardwareWallet: isHardwareWallet, + initialAddresses: initialAddresses, + initialSilentAddresses: initialSilentAddresses, + initialReceivedSPAddresses: initialReceivedSPAddresses, + ); + } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 904751cdee..80895f2ac1 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -3,6 +3,7 @@ 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/bitcoin_wallet_creation_credentials.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; @@ -152,6 +153,12 @@ class BitcoinWalletService extends WalletService< credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: credentials.walletInfo!, + network: network, + xpub: credentials.hwAccountData.xpub, + ); + final wallet = await BitcoinWallet( password: credentials.password!, xpub: credentials.hwAccountData.xpub, @@ -159,6 +166,7 @@ class BitcoinWalletService extends WalletService< unspentCoinsInfo: unspentCoinsInfoSource, networkParam: network, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + hdWallets: hdWallets, ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart index 499a97e034..68dab90e77 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:cw_bitcoin/bitcoin_address_record.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -13,23 +13,16 @@ class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { required super.password, required super.mnemonic, required super.xpub, - required super.addresses, required super.balance, - required super.regularAddressIndex, - required super.changeAddressIndex, - required super.addressPageType, - required this.silentAddressIndex, - required this.silentAddresses, required this.alwaysScan, required super.unspentCoins, + required super.walletAddressesSnapshot, super.passphrase, super.derivationType, super.derivationPath, }) : super(); - List silentAddresses; bool alwaysScan; - int silentAddressIndex; static Future load( EncryptionFileUtils encryptionFileUtils, @@ -50,21 +43,10 @@ class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { network, ); - final silentAddressesTmp = data['silent_addresses'] as List? ?? []; - final silentAddresses = silentAddressesTmp.whereType().map((j) { - final decoded = json.decode(jsonSource) as Map; - if (decoded['tweak'] != null || decoded['silent_payment_tweak'] != null) { - return BitcoinReceivedSPAddressRecord.fromJSON(j); - } else { - return BitcoinSilentPaymentAddressRecord.fromJSON(j); - } - }).toList(); final alwaysScan = data['alwaysScan'] as bool? ?? false; - var silentAddressIndex = 0; - try { - silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); - } catch (_) {} + final walletAddressesSnapshot = data['walletAddresses'] as Map? ?? + BitcoinWalletAddressesBase.fromSnapshot(data); return BitcoinWalletSnapshot( name: name, @@ -73,17 +55,12 @@ class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { passphrase: electrumWalletSnapshot.passphrase, mnemonic: electrumWalletSnapshot.mnemonic, xpub: electrumWalletSnapshot.xpub, - addresses: electrumWalletSnapshot.addresses, - regularAddressIndex: electrumWalletSnapshot.regularAddressIndex, balance: electrumWalletSnapshot.balance, - changeAddressIndex: electrumWalletSnapshot.changeAddressIndex, - addressPageType: electrumWalletSnapshot.addressPageType, derivationType: electrumWalletSnapshot.derivationType, derivationPath: electrumWalletSnapshot.derivationPath, unspentCoins: electrumWalletSnapshot.unspentCoins, - silentAddressIndex: silentAddressIndex, - silentAddresses: silentAddresses, alwaysScan: alwaysScan, + walletAddressesSnapshot: walletAddressesSnapshot, ); } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index fe00a41f3b..d395e6671f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -54,12 +54,15 @@ abstract class ElectrumWalletBase final Map _errorCompleters = {}; int _messageId = 0; + static const String SIGN_MESSAGE_PREFIX = '\x18Bitcoin Signed Message:\n'; + ElectrumWalletBase({ required String password, required WalletInfo walletInfo, required this.unspentCoinsInfo, required this.network, required this.encryptionFileUtils, + required this.hdWallets, String? xpub, String? mnemonic, this.passphrase, @@ -72,7 +75,7 @@ abstract class ElectrumWalletBase syncStatus = NotConnectedSyncStatus(), _password = password, isEnabledAutoGenerateSubaddress = true, - unspentCoins = BitcoinUnspentCoins.of(initialUnspentCoins ?? []), + unspentCoins = ElectrumUnspentCoins.of(initialUnspentCoins ?? []), scripthashesListening = [], balance = ObservableMap.of(currency != null ? { @@ -88,8 +91,6 @@ abstract class ElectrumWalletBase this._mnemonic = mnemonic, _didInitialSync = didInitialSync ?? false, super(walletInfo) { - getAccountHDWallets(); - transactionHistory = ElectrumTransactionHistory( walletInfo: walletInfo, password: password, @@ -101,20 +102,26 @@ abstract class ElectrumWalletBase sharedPrefs.complete(SharedPreferences.getInstance()); } - void getAccountHDWallets() { - if (_mnemonic == null && _xpub == null) { + static Future> getAccountHDWallets({ + required WalletInfo walletInfo, + required BasedUtxoNetwork network, + String? mnemonic, + String? passphrase, + String? xpub, + }) async { + if (mnemonic == null && xpub == null) { throw Exception( "To create a Wallet you need either a seed or an xpub. This should not happen"); } late WalletSeedData walletSeedData; - if (_mnemonic != null) { - walletSeedData = WalletSeedData.fromMnemonic(walletInfo, _mnemonic!, passphrase); + if (mnemonic != null) { + walletSeedData = await WalletSeedData.fromMnemonic(walletInfo, mnemonic, passphrase); } else { - walletSeedData = WalletSeedData.fromXpub(walletInfo, _xpub!, network); + walletSeedData = WalletSeedData.fromXpub(walletInfo, xpub!, network); } - _hdWallets = walletSeedData.hdWallets; + return walletSeedData.hdWallets; } // Sends a request to the worker and returns a future that completes when the worker responds @@ -170,6 +177,10 @@ abstract class ElectrumWalletBase final response = ElectrumWorkerHeadersSubscribeResponse.fromJson(messageJson); await onHeadersResponse(response.result); break; + case ElectrumRequestMethods.scripthashesSubscribeMethod: + final response = ElectrumWorkerScripthashesSubscribeResponse.fromJson(messageJson); + await onScripthashesStatusResponse(response.result); + break; case ElectrumRequestMethods.getBalanceMethod: final response = ElectrumWorkerGetBalanceResponse.fromJson(messageJson); onBalanceResponse(response.result); @@ -212,9 +223,9 @@ abstract class ElectrumWalletBase bool? alwaysScan; - late Map _hdWallets; - Map get hdWallets => _hdWallets; - Bip32Slip10Secp256k1 get hdWallet => walletAddresses.hdWallet; + final Map hdWallets; + Bip32Slip10Secp256k1 get hdWallet => + hdWallets[CWBitcoinDerivationType.bip39] ?? hdWallets[CWBitcoinDerivationType.electrum]!; final String? _mnemonic; final EncryptionFileUtils encryptionFileUtils; @@ -272,7 +283,7 @@ abstract class ElectrumWalletBase ); String _password; - BitcoinUnspentCoins unspentCoins; + ElectrumUnspentCoins unspentCoins; @observable TransactionPriorities? feeRates; @@ -282,6 +293,10 @@ abstract class ElectrumWalletBase @observable List scripthashesListening; + // READ: https://electrumx.readthedocs.io/en/latest/protocol-basics.html#status + @observable + List scripthashesWithStatus = []; + bool _chainTipListenerOn = false; bool _didInitialSync; @@ -309,24 +324,29 @@ abstract class ElectrumWalletBase syncStatus = SynchronizingSyncStatus(); // INFO: FIRST (always): Call subscribe for headers, wait for completion to update currentChainTip (needed for other methods) - await waitSendWorker(ElectrumWorkerHeadersSubscribeRequest()); + await subscribeForHeaders(true); + await subscribeForStatuses(true); + + final addressesWithHistory = walletAddresses.allAddresses + .where((e) => scripthashesWithStatus.contains(e.scriptHash)) + .toList(); // 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 final hadTxs = transactionHistory.transactions.isNotEmpty; final isFirstSync = !_didInitialSync; - await updateTransactions(null, true); + await updateTransactions(addressesWithHistory, isFirstSync); _didInitialSync = true; final nowHasTxs = transactionHistory.transactions.isNotEmpty; final shouldUpdateDates = !hadTxs && isFirstSync && nowHasTxs; if (shouldUpdateDates) { - await updateTransactions(null, true); + await updateTransactions(addressesWithHistory, true); } // INFO: THIRD: Get the full wallet's balance with all addresses considered - await updateBalance(true); + await updateBalance(scripthashesWithStatus.toSet(), true); syncStatus = SyncedSyncStatus(); @@ -405,7 +425,7 @@ abstract class ElectrumWalletBase bool isBelowDust(int amount) => amount <= dustAmount; - TxCreateUtxoDetails createUTXOS({ + ElectrumTxCreateUtxoDetails createUTXOS({ required bool sendAll, int credentialsAmount = 0, int? inputsCount, @@ -498,7 +518,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionNoInputsException(); } - return TxCreateUtxoDetails( + return ElectrumTxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -510,7 +530,7 @@ abstract class ElectrumWalletBase ); } - Future estimateSendAllTx( + Future estimateSendAllTx( List outputs, int feeRate, { String? memo, @@ -544,7 +564,7 @@ abstract class ElectrumWalletBase outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -557,7 +577,7 @@ abstract class ElectrumWalletBase ); } - Future estimateTxForAmount( + Future estimateTxForAmount( int credentialsAmount, List outputs, int feeRate, { @@ -600,10 +620,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress( - inputs: utxoDetails.availableInputs, - outputs: outputs, - ); + final changeAddress = await walletAddresses.getChangeAddress(); final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); outputs.add(BitcoinOutput( address: address, @@ -635,7 +652,6 @@ abstract class ElectrumWalletBase // If has change that is lower than dust, will end up with tx rejected by network rules // so remove the change amount outputs.removeLast(); - outputs.removeLast(); if (amountLeftForChange < 0) { if (!spendingAllCoins) { @@ -653,7 +669,7 @@ abstract class ElectrumWalletBase } } - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -672,7 +688,7 @@ abstract class ElectrumWalletBase isChange: true, ); - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -766,7 +782,7 @@ abstract class ElectrumWalletBase try { final data = getCreateTxDataFromCredentials(credentials); - EstimatedTxResult estimatedTx; + ElectrumEstimatedTx estimatedTx; if (data.sendAll) { estimatedTx = await estimateSendAllTx( data.outputs, @@ -919,7 +935,7 @@ abstract class ElectrumWalletBase 'balance': balance[currency]?.toJSON(), 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, 'derivationPath': walletInfo.derivationInfo?.derivationPath, - 'unspents': unspentCoins.map((e) => e.toJson()).toList(), + 'unspent_coins': unspentCoins.map((e) => e.toJson()).toList(), 'didInitialSync': _didInitialSync, }); @@ -1132,6 +1148,7 @@ abstract class ElectrumWalletBase await Future.wait(unspent.map((unspent) async { final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson()); coin.isChange = addressRecord.isChange; + // TODO: \/ final tx = await fetchTransactionInfo(hash: coin.hash); if (tx != null) { coin.confirmations = tx.confirmations; @@ -1164,13 +1181,13 @@ abstract class ElectrumWalletBase value: coin.value, vout: coin.vout, isChange: coin.isChange, - isSilentPayment: coin.address is BitcoinReceivedSPAddressRecord, ); await unspentCoinsInfo.add(newInfo); } } + @action Future refreshUnspentCoinsInfo() async { try { final List keys = []; @@ -1193,33 +1210,55 @@ abstract class ElectrumWalletBase } } + @action + Future onScripthashesStatusResponse(Map? result) async { + if (result != null) { + for (final entry in result.entries) { + final address = entry.key; + + final scripthash = walletAddresses.allAddresses + .firstWhereOrNull((element) => element.address == address) + ?.scriptHash; + + if (scripthash != null) { + scripthashesWithStatus.add(scripthash); + } + } + } + } + @action Future onHeadersResponse(ElectrumHeaderResponse response) async { currentChainTip = response.height; - bool updated = false; + bool anyTxWasUpdated = false; transactionHistory.transactions.values.forEach((tx) { if (tx.height != null && tx.height! > 0) { - final newConfirmations = currentChainTip! - tx.height! + 1; + final newConfirmationsValue = currentChainTip! - tx.height! + 1; - if (tx.confirmations != newConfirmations) { - tx.confirmations = newConfirmations; + if (tx.confirmations != newConfirmationsValue) { + tx.confirmations = newConfirmationsValue; tx.isPending = tx.confirmations == 0; - updated = true; + anyTxWasUpdated = true; } } }); - if (updated) { + if (anyTxWasUpdated) { await save(); } } @action - Future subscribeForHeaders() async { + Future subscribeForHeaders([bool? wait]) async { if (_chainTipListenerOn) return; - workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); + if (wait == true) { + await waitSendWorker(ElectrumWorkerHeadersSubscribeRequest()); + } else { + workerSendPort!.send(ElectrumWorkerHeadersSubscribeRequest().toJson()); + } + _chainTipListenerOn = true; } @@ -1312,7 +1351,7 @@ abstract class ElectrumWalletBase await save(); } else { - checkAddressesGap(); + // checkAddressesGap(); } } @@ -1653,21 +1692,25 @@ abstract class ElectrumWalletBase } @action - Future subscribeForUpdates([Iterable? unsubscribedScriptHashes]) async { - unsubscribedScriptHashes ??= walletAddresses.allScriptHashes.where( - (sh) => !scripthashesListening.contains(sh), - ); - + Future subscribeForStatuses([bool? wait]) async { Map scripthashByAddress = {}; walletAddresses.allAddresses.forEach((addressRecord) { scripthashByAddress[addressRecord.address] = addressRecord.scriptHash; }); - workerSendPort!.send( - ElectrumWorkerScripthashesSubscribeRequest( - scripthashByAddress: scripthashByAddress, - ).toJson(), - ); + if (wait == true) { + await waitSendWorker( + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + ), + ); + } else { + workerSendPort!.send( + ElectrumWorkerScripthashesSubscribeRequest( + scripthashByAddress: scripthashByAddress, + ).toJson(), + ); + } scripthashesListening.addAll(scripthashByAddress.values); } @@ -1692,18 +1735,16 @@ abstract class ElectrumWalletBase } @action - Future updateBalance([bool? wait]) async { + Future updateBalance([Set? scripthashes, bool? wait]) async { + scripthashes ??= walletAddresses.allScriptHashes.toSet(); + if (wait == true) { return waitSendWorker( - ElectrumWorkerGetBalanceRequest( - scripthashes: walletAddresses.allScriptHashes, - ), + ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes), ); } else { workerSendPort!.send( - ElectrumWorkerGetBalanceRequest( - scripthashes: walletAddresses.allScriptHashes, - ).toJson(), + ElectrumWorkerGetBalanceRequest(scripthashes: scripthashes).toJson(), ); } } @@ -1722,7 +1763,8 @@ abstract class ElectrumWalletBase final priv = ECPrivate.fromHex(hdWallet.derive(path).privateKey.toHex()); - final hexEncoded = priv.signMessage(StringUtils.encode(message)); + final hexEncoded = + priv.signMessage(StringUtils.encode(message), messagePrefix: SIGN_MESSAGE_PREFIX); final decodedSig = hex.decode(hexEncoded); return base64Encode(decodedSig); } @@ -1746,9 +1788,8 @@ abstract class ElectrumWalletBase "signature must be 64 bytes without recover-id or 65 bytes with recover-id"); } - String messagePrefix = '\x18Bitcoin Signed Message:\n'; final messageHash = QuickCrypto.sha256Hash( - BitcoinSignerUtils.magicMessage(StringUtils.encode(message), messagePrefix)); + BitcoinSignerUtils.magicMessage(StringUtils.encode(message), SIGN_MESSAGE_PREFIX)); List correctSignature = sigDecodedBytes.length == 65 ? sigDecodedBytes.sublist(1) : List.from(sigDecodedBytes); @@ -1895,8 +1936,8 @@ abstract class ElectrumWalletBase } } -class EstimatedTxResult { - EstimatedTxResult({ +class ElectrumEstimatedTx { + ElectrumEstimatedTx({ required this.utxos, required this.inputPrivKeyInfos, required this.publicKeys, @@ -1905,7 +1946,6 @@ class EstimatedTxResult { required this.hasChange, required this.isSendAll, this.memo, - this.spendsSilentPayment = false, required this.spendsUnconfirmedTX, }); @@ -1914,7 +1954,6 @@ class EstimatedTxResult { final Map publicKeys; // PubKey to derivationPath final int fee; final int amount; - final bool spendsSilentPayment; final bool hasChange; final bool isSendAll; @@ -1929,7 +1968,7 @@ class PublicKeyWithDerivationPath { final String publicKey; } -class TxCreateUtxoDetails { +class ElectrumTxCreateUtxoDetails { final List availableInputs; final List unconfirmedCoins; final List utxos; @@ -1937,10 +1976,9 @@ class TxCreateUtxoDetails { final List inputPrivKeyInfos; final Map publicKeys; // PubKey to derivationPath final int allInputsAmount; - final bool spendsSilentPayment; final bool spendsUnconfirmedTX; - TxCreateUtxoDetails({ + ElectrumTxCreateUtxoDetails({ required this.availableInputs, required this.unconfirmedCoins, required this.utxos, @@ -1948,16 +1986,15 @@ class TxCreateUtxoDetails { required this.inputPrivKeyInfos, required this.publicKeys, required this.allInputsAmount, - this.spendsSilentPayment = false, required this.spendsUnconfirmedTX, }); } -class BitcoinUnspentCoins extends ObservableSet { - BitcoinUnspentCoins() : super(); +class ElectrumUnspentCoins extends ObservableSet { + ElectrumUnspentCoins() : super(); - static BitcoinUnspentCoins of(Iterable unspentCoins) { - final coins = BitcoinUnspentCoins(); + static ElectrumUnspentCoins of(Iterable unspentCoins) { + final coins = ElectrumUnspentCoins(); coins.addAll(unspentCoins); return coins; } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 08e52f2301..842a74d88c 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -2,7 +2,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_core/utils/print_verbose.dart'; -import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -25,6 +24,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { required this.network, required this.isHardwareWallet, List? initialAddresses, + Map? initialRegularAddressIndex, + Map? initialChangeAddressIndex, BitcoinAddressType? initialAddressPageType, }) : _allAddresses = ObservableList.of(initialAddresses ?? []), addressesOnReceiveScreen = @@ -33,13 +34,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { (initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()), changeAddresses = ObservableList.of( (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), + currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, + currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? (walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddressType.p2wpkh), - super(walletInfo) { - updateAddressesOnReceiveScreen(); - } + super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -80,10 +81,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override @computed String get address { - String receiveAddress; + String receiveAddress = ""; - final typeMatchingReceiveAddresses = - receiveAddresses.where(_isAddressPageTypeMatch).where((addr) => !addr.isUsed); + final typeMatchingReceiveAddresses = addressesOnReceiveScreen.where((addr) => !addr.isUsed); if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || typeMatchingReceiveAddresses.isEmpty) { @@ -92,11 +92,13 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final previousAddressMatchesType = previousAddressRecord != null && previousAddressRecord!.type == addressPageType; - if (previousAddressMatchesType && - typeMatchingReceiveAddresses.first.address != addressesOnReceiveScreen.first.address) { - receiveAddress = previousAddressRecord!.address; - } else { - receiveAddress = typeMatchingReceiveAddresses.first.address; + if (typeMatchingReceiveAddresses.isNotEmpty) { + if (previousAddressMatchesType && + typeMatchingReceiveAddresses.first.address != addressesOnReceiveScreen.first.address) { + receiveAddress = previousAddressRecord!.address; + } else { + receiveAddress = typeMatchingReceiveAddresses.first.address; + } } } @@ -122,6 +124,22 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override String get primaryAddress => _allAddresses.first.address; + 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; + @observable BitcoinAddressRecord? previousAddressRecord; @@ -156,15 +174,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesOnReceiveScreen(); updateReceiveAddresses(); updateChangeAddresses(); + updateHiddenAddresses(); await updateAddressesInBox(); } @action - Future getChangeAddress({ - List? inputs, - List? outputs, - bool isPegIn = false, - }) async { + Future getChangeAddress() async { updateChangeAddresses(); final address = changeAddresses.firstWhere( @@ -173,7 +188,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { final newAddressIndex = addressesOnReceiveScreen.fold( 0, @@ -197,7 +211,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { derivationInfo: BitcoinAddressUtils.getDerivationFromType(addressPageType), cwDerivationType: getHDWalletType(), ); - addAddresses([address]); return address; } @@ -460,5 +473,63 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Map toJson() => { 'allAddresses': _allAddresses.map((address) => address.toJSON()).toList(), 'addressPageType': addressPageType.toString(), + // 'receiveAddressIndexByType': receiveAddressIndexByType, + // 'changeAddressIndexByType': changeAddressIndexByType, }; + + static Map fromSnapshot(Map data) { + final addressesTmp = data['addresses'] as List? ?? []; + final addresses = addressesTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .toList(); + + var receiveAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; + var changeAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; + + try { + receiveAddressIndexByType = { + SegwitAddressType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') + }; + changeAddressIndexByType = { + SegwitAddressType.p2wpkh.toString(): + int.parse(data['change_address_index'] as String? ?? '0') + }; + } catch (_) { + try { + receiveAddressIndexByType = data["account_index"] as Map? ?? {}; + changeAddressIndexByType = data["change_address_index"] as Map? ?? {}; + } catch (_) {} + } + + return { + 'allAddresses': addresses, + 'addressPageType': data['address_page_type'] as String?, + 'receiveAddressIndexByType': receiveAddressIndexByType, + 'changeAddressIndexByType': changeAddressIndexByType, + }; + } + + static ElectrumWalletAddressesBase fromJson( + Map json, + WalletInfo walletInfo, { + required Map hdWallets, + required BasedUtxoNetwork network, + required bool isHardwareWallet, + List? initialAddresses, + List? initialSilentAddresses, + List? initialReceivedSPAddresses, + }) { + initialAddresses ??= (json['allAddresses'] as List) + .map((record) => BitcoinAddressRecord.fromJSON(record as String)) + .toList(); + + return ElectrumWalletAddresses( + walletInfo, + hdWallets: hdWallets, + network: network, + isHardwareWallet: isHardwareWallet, + initialAddresses: initialAddresses, + ); + } } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index a395f0724c..4fcd5d2bea 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -1,8 +1,8 @@ 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_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; @@ -15,12 +15,9 @@ class ElectrumWalletSnapshot { required this.password, required this.mnemonic, required this.xpub, - required this.addresses, required this.balance, - required this.regularAddressIndex, - required this.changeAddressIndex, - required this.addressPageType, required this.unspentCoins, + required this.walletAddressesSnapshot, this.passphrase, this.derivationType, this.derivationPath, @@ -30,7 +27,6 @@ class ElectrumWalletSnapshot { final String name; final String password; final WalletType type; - final String? addressPageType; List unspentCoins; @deprecated @@ -42,17 +38,20 @@ class ElectrumWalletSnapshot { @deprecated String? passphrase; - List addresses; - ElectrumBalance balance; - Map regularAddressIndex; - Map changeAddressIndex; DerivationType? derivationType; String? derivationPath; bool? didInitialSync; - static Future load(EncryptionFileUtils encryptionFileUtils, String name, - WalletType type, String password, BasedUtxoNetwork network) async { + Map? walletAddressesSnapshot; + + static Future load( + EncryptionFileUtils encryptionFileUtils, + String name, + WalletType type, + String password, + BasedUtxoNetwork network, + ) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -60,35 +59,15 @@ class ElectrumWalletSnapshot { final xpub = data['xpub'] as String?; final passphrase = data['passphrase'] as String? ?? ''; - final addressesTmp = data['addresses'] as List? ?? []; - final addresses = addressesTmp - .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr)) - .toList(); - final balance = ElectrumBalance.fromJSON(data['balance'] as String?) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); - var regularAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; - var changeAddressIndexByType = {SegwitAddressType.p2wpkh.toString(): 0}; final derivationType = DerivationType .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; final derivationPath = data['derivationPath'] as String? ?? ELECTRUM_PATH; - try { - regularAddressIndexByType = { - SegwitAddressType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0') - }; - changeAddressIndexByType = { - SegwitAddressType.p2wpkh.toString(): - int.parse(data['change_address_index'] as String? ?? '0') - }; - } catch (_) { - try { - regularAddressIndexByType = data["account_index"] as Map? ?? {}; - changeAddressIndexByType = data["change_address_index"] as Map? ?? {}; - } catch (_) {} - } + final walletAddressesSnapshot = data['walletAddresses'] as Map? ?? + ElectrumWalletAddressesBase.fromSnapshot(data); return ElectrumWalletSnapshot( name: name, @@ -97,17 +76,14 @@ class ElectrumWalletSnapshot { passphrase: passphrase, mnemonic: mnemonic, xpub: xpub, - addresses: addresses, balance: balance, - regularAddressIndex: regularAddressIndexByType, - changeAddressIndex: changeAddressIndexByType, - addressPageType: data['address_page_type'] as String?, derivationType: derivationType, derivationPath: derivationPath, unspentCoins: (data['unspent_coins'] as List) .map((e) => BitcoinUnspent.fromJSON(null, e as Map)) .toList(), didInitialSync: data['didInitialSync'] as bool?, + walletAddressesSnapshot: walletAddressesSnapshot, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 3f41b49c63..c5252cafd9 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; +import 'package:cw_bitcoin/electrum_worker/server_capability.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -22,6 +23,7 @@ import 'package:sp_scanner/sp_scanner.dart'; class ElectrumWorker { final SendPort sendPort; ElectrumProvider? _electrumClient; + ServerCapability? _serverCapability; BehaviorSubject>? _scanningStream; BasedUtxoNetwork? _network; @@ -136,31 +138,61 @@ class ElectrumWorker { _network = request.network; _walletType = request.walletType; + bool initialConnection = true; + bool needsToConnect = false; + try { _electrumClient = await ElectrumProvider.connect( request.useSSL ? ElectrumSSLService.connect( request.uri, onConnectionStatusChange: (status) { - _sendResponse( - ElectrumWorkerConnectionResponse(status: status, id: request.id), - ); + if (status == ConnectionStatus.connected && initialConnection) { + needsToConnect = true; + } else { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + } }, ) : ElectrumTCPService.connect( request.uri, onConnectionStatusChange: (status) { - _sendResponse( - ElectrumWorkerConnectionResponse(status: status, id: request.id), - ); + if (status == ConnectionStatus.connected && initialConnection) { + needsToConnect = true; + } else { + _sendResponse( + ElectrumWorkerConnectionResponse(status: status, id: request.id), + ); + } }, ), ); + + if (needsToConnect) { + final version = await _electrumClient!.request( + ElectrumRequestVersion(clientName: "", protocolVersion: "1.4"), + ); + + _serverCapability = ServerCapability.fromVersion(version); + + _sendResponse( + ElectrumWorkerConnectionResponse( + status: ConnectionStatus.connected, + id: request.id, + ), + ); + + initialConnection = false; + needsToConnect = false; + } } catch (e) { _sendError(ElectrumWorkerConnectionError(error: e.toString())); } } + // Subscribe to new blocks Future _handleHeadersSubscribe(ElectrumWorkerHeadersSubscribeRequest request) async { final req = ElectrumRequestHeaderSubscribe(); @@ -183,38 +215,98 @@ class ElectrumWorker { Future _handleScriphashesSubscribe( ElectrumWorkerScripthashesSubscribeRequest request, ) async { - await Future.forEach(request.scripthashByAddress.entries, - (MapEntry entry) async { - final address = entry.key; - final scripthash = entry.value.toString(); + if (_serverCapability!.supportsBatching) { + try { + final req = ElectrumBatchRequestScriptHashSubscribe( + scriptHashes: request.scripthashByAddress.values.toList() as List, + ); - final req = ElectrumRequestScriptHashSubscribe(scriptHash: scripthash); + final streams = await _electrumClient!.batchSubscribe(req); - final stream = await _electrumClient!.subscribe(req); + if (streams != null) { + int i = 0; - if (stream == null) { - _sendError(ElectrumWorkerScripthashesSubscribeError(error: 'Failed to subscribe')); - return; + await Future.wait(streams.map((stream) async { + stream.subscription.listen((status) { + final batch = req.onResponse(status, stream.params); + final result = batch.result; + + final scriptHash = batch.paramForRequest!.first as String; + final address = request.scripthashByAddress.entries + .firstWhere( + (entry) => entry.value == scriptHash, + ) + .key; + + if (result != null) { + _sendResponse( + ElectrumWorkerScripthashesSubscribeResponse( + result: {address: result}, + id: request.id, + completed: false, + ), + ); + } + + i++; + + if (i == request.scripthashByAddress.length) { + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: null}, + id: request.id, + completed: true, + )); + } + }); + })); + } else { + _serverCapability!.supportsBatching = false; + } + } catch (_) { + _serverCapability!.supportsBatching = false; } + } - // 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 { - if (status == null) { + if (_serverCapability!.supportsBatching == false) { + int i = 0; + await Future.wait(request.scripthashByAddress.entries.map((entry) async { + final address = entry.key; + final scripthash = entry.value.toString(); + + final req = ElectrumRequestScriptHashSubscribe(scriptHash: scripthash); + + final stream = await _electrumClient!.subscribe(req); + + if (stream == null) { + _sendError(ElectrumWorkerScripthashesSubscribeError(error: 'Failed to subscribe')); return; } - printV("status: $status"); + // 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 { + if (status != null) { + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: req.onResponse(status)}, + id: request.id, + completed: false, + )); + } + i++; - _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( - result: {address: req.onResponse(status)}, - id: request.id, - )); - }); - }); + if (i == request.scripthashByAddress.length) { + _sendResponse(ElectrumWorkerScripthashesSubscribeResponse( + result: {address: null}, + id: request.id, + completed: true, + )); + } + }); + })); + } } - Future _handleGetInitialHistory(ElectrumWorkerGetHistoryRequest request) async { + Future _handleGetBatchInitialHistory(ElectrumWorkerGetHistoryRequest request) async { var histories = {}; final scripthashes = []; final addresses = []; @@ -227,11 +319,17 @@ class ElectrumWorker { } }); - final historyBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestScriptHashGetHistory( - scriptHashes: scripthashes, - ), - ); + late List>>> historyBatches; + try { + historyBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestScriptHashGetHistory( + scriptHashes: scripthashes, + ), + ); + } catch (_) { + _serverCapability!.supportsBatching = false; + return _handleGetInitialHistory(request); + } final transactionIdsForHeights = {}; @@ -345,13 +443,130 @@ class ElectrumWorker { ); } - Future _handleGetHistory(ElectrumWorkerGetHistoryRequest request) async { - if (request.storedTxs.isEmpty) { - // _handleGetInitialHistory only gets enough data to update the UI initially, - // then _handleGetHistory will be used to validate and update the dates, confirmations, and ins - return await _handleGetInitialHistory(request); - } + Future _handleGetInitialHistory(ElectrumWorkerGetHistoryRequest request) async { + var histories = {}; + final scripthashes = []; + final addresses = []; + request.addresses.forEach((addr) { + addr.txCount = 0; + + if (addr.scriptHash.isNotEmpty) { + scripthashes.add(addr.scriptHash); + addresses.add(addr.address); + } + }); + await Future.wait(scripthashes.map((scripthash) async { + final history = await _electrumClient!.request( + ElectrumRequestScriptHashGetHistory(scriptHash: scripthash), + ); + + if (history.isEmpty) { + return; + } + + final transactionIdsForHeights = {}; + + history.forEach((tx) { + transactionIdsForHeights[tx['tx_hash'] as String] = tx['height'] as int; + }); + + if (transactionIdsForHeights.isNotEmpty) { + await Future.wait(transactionIdsForHeights.keys.toList().map((hash) async { + final transactionVerbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose(transactionHash: hash), + ); + + late String transactionHex; + + if (transactionVerbose.isEmpty) { + transactionHex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex( + transactionHash: hash, + ), + ); + } else { + transactionHex = transactionVerbose['hex'] as String; + } + + late ElectrumTransactionBundle txBundle; + + // this is the initial tx history update, so ins will be filled later one by one, + // and time and confirmations will be updated if needed again + if (transactionVerbose.isNotEmpty) { + txBundle = ElectrumTransactionBundle( + BtcTransaction.fromRaw(transactionHex), + ins: [], + time: transactionVerbose['time'] as int?, + confirmations: (transactionVerbose['confirmations'] as int?) ?? 1, + isDateValidated: (transactionVerbose['time'] as int?) != null, + ); + } else { + txBundle = ElectrumTransactionBundle( + BtcTransaction.fromRaw(transactionHex), + ins: [], + confirmations: 1, + ); + } + + final txInfo = ElectrumTransactionInfo.fromElectrumBundle( + txBundle, + request.walletType, + request.network, + addresses: addresses.toSet(), + height: transactionIdsForHeights[hash], + ); + + request.addresses.forEach( + (addr) { + final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || + (txInfo.inputAddresses?.contains(addr.address) ?? false); + + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; + + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(txInfo); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ); + + return _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [ + AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ) + ], + id: request.id, + completed: false, + ), + ); + } + } + }, + ); + })); + } + + return _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [], + id: request.id, + completed: true, + ), + ); + })); + } + + Future _handleGetBatchHistory(ElectrumWorkerGetHistoryRequest request) async { final scripthashes = []; final addresses = []; request.addresses.forEach((addr) { @@ -363,16 +578,22 @@ class ElectrumWorker { } }); - final historyBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestScriptHashGetHistory( - scriptHashes: scripthashes, - ), - ); + late List>>> historyBatches; + try { + historyBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestScriptHashGetHistory( + scriptHashes: scripthashes, + ), + ); + } catch (_) { + _serverCapability!.supportsBatching = false; + return _handleGetHistory(request); + } final transactionsByIds = {}; - for (final result in historyBatches) { - final history = result.result; + for (final batch in historyBatches) { + final history = batch.result; if (history.isEmpty) { continue; } @@ -521,6 +742,196 @@ class ElectrumWorker { ); } + Future _handleGetHistory(ElectrumWorkerGetHistoryRequest request) async { + if (request.storedTxs.isEmpty) { + // _handleGetInitialHistory only gets enough data to update the UI initially, + // then _handleGetHistory will be used to validate and update the dates, confirmations, and ins + if (_serverCapability!.supportsBatching) { + return await _handleGetBatchInitialHistory(request); + } else { + return await _handleGetInitialHistory(request); + } + } + + if (_serverCapability!.supportsBatching) { + return await _handleGetBatchHistory(request); + } + + final scripthashes = []; + final addresses = []; + request.addresses.forEach((addr) { + addr.txCount = 0; + + if (addr.scriptHash.isNotEmpty) { + scripthashes.add(addr.scriptHash); + addresses.add(addr.address); + } + }); + + await Future.wait(scripthashes.map((scripthash) async { + final history = await _electrumClient!.request( + ElectrumRequestScriptHashGetHistory(scriptHash: scripthash), + ); + + final transactionsByIds = {}; + + if (history.isEmpty) { + return; + } + + for (final transaction in history) { + final txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + ElectrumTransactionInfo? tx; + + try { + // Exception thrown if non existing, handled on null condition below trycatch + tx = request.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 = request.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 + final canValidateDate = request.mempoolAPIEnabled || _serverCapability!.supportsTxVerbose; + if (tx == null || + tx.original == null || + (tx.isDateValidated != true && canValidateDate) || + tx.time == null) { + transactionsByIds[txid] = TxToFetch(height: height, tx: tx); + } + } + + if (transactionsByIds.isNotEmpty) { + await Future.wait(transactionsByIds.keys.toList().map((hash) async { + late String txHex; + Map? txVerbose; + + if (_serverCapability!.supportsTxVerbose) { + txVerbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose( + transactionHash: hash, + ), + ); + } + + if (txVerbose?.isEmpty ?? true) { + txHex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex(transactionHash: hash), + ); + } else { + txHex = txVerbose!['hex'] as String; + } + + await Future.forEach(transactionsByIds.entries, + (MapEntry entry) async { + final hash = entry.key; + final txToFetch = entry.value; + final storedTx = txToFetch.tx; + final original = storedTx?.original ?? BtcTransaction.fromRaw(txHex); + + DateResult? date; + + if (txVerbose?.isNotEmpty ?? false) { + date = DateResult( + time: txVerbose!['time'] as int?, + confirmations: txVerbose['confirmations'] as int?, + isDateValidated: true, + ); + } else if (request.mempoolAPIEnabled) { + try { + date = await getTxDate( + hash, + _network!, + request.chainTip, + confirmations: storedTx?.confirmations, + date: storedTx?.date, + ); + } catch (_) {} + } + + final inputTransactionHexes = {}; + + await Future.wait(original.inputs.map((e) => e.txId).toList().map((inHash) async { + final hex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex(transactionHash: inHash), + ); + + inputTransactionHexes[inHash] = hex; + })); + + final ins = []; + + for (final vin in original.inputs) { + final inputTransactionHex = inputTransactionHexes[vin.txId]!; + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } + + final txInfo = ElectrumTransactionInfo.fromElectrumBundle( + ElectrumTransactionBundle( + original, + ins: ins, + time: date?.time, + confirmations: date?.confirmations ?? 0, + isDateValidated: date?.isDateValidated, + ), + request.walletType, + request.network, + addresses: addresses.toSet(), + height: transactionsByIds[hash]?.height, + ); + + var histories = {}; + request.addresses.forEach( + (addr) { + final usedAddress = (txInfo.outputAddresses?.contains(addr.address) ?? false) || + (txInfo.inputAddresses?.contains(addr.address) ?? false); + + if (usedAddress == true) { + addr.setAsUsed(); + addr.txCount++; + + final addressHistories = histories[addr.address]; + if (addressHistories != null) { + addressHistories.txs.add(txInfo); + } else { + histories[addr.address] = AddressHistoriesResponse( + addressRecord: addr, + txs: [txInfo], + walletType: request.walletType, + ); + } + } + }, + ); + + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: histories.values.toList(), + id: request.id, + completed: false, + ), + ); + }); + })); + } + + _sendResponse( + ElectrumWorkerGetHistoryResponse( + result: [], + id: request.id, + completed: true, + ), + ); + })); + } + // Future _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { // final balanceFutures = >>[]; @@ -553,17 +964,33 @@ class ElectrumWorker { // } Future _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { - final balances = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetScriptHashBalance( - scriptHashes: request.scripthashes.where((s) => s.isNotEmpty).toList(), - ), - ); + final scripthashes = request.scripthashes.where((s) => s.isNotEmpty).toList(); + final balanceResults = >[]; + + if (_serverCapability!.supportsBatching) { + try { + balanceResults.addAll((await _electrumClient!.batchRequest( + ElectrumBatchRequestGetScriptHashBalance(scriptHashes: scripthashes), + )) + .map((e) => e.result) + .toList()); + } catch (_) { + _serverCapability!.supportsBatching = false; + } + } else { + await Future.wait(scripthashes.map((scripthash) async { + final history = await _electrumClient!.request( + ElectrumRequestGetScriptHashBalance(scriptHash: scripthash), + ); + + balanceResults.add(history); + })); + } var totalConfirmed = 0; var totalUnconfirmed = 0; - for (final result in balances) { - final balance = result.result; + for (final balance in balanceResults) { final confirmed = balance['confirmed'] as int? ?? 0; final unconfirmed = balance['unconfirmed'] as int? ?? 0; totalConfirmed += confirmed; @@ -636,30 +1063,65 @@ class ElectrumWorker { required int currentChainTip, }) async { final hashes = hashesForHeights.keys.toList(); - - final transactionVerboseBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), - ); + final txVerboseResults = >[]; List transactionHexes = []; List emptyVerboseTxs = []; - transactionVerboseBatches.forEach((batch) { - final txVerbose = batch.result; - if (txVerbose.isEmpty) { - emptyVerboseTxs.add( - (batch.request.paramsById[batch.id] as List).first as String, + if (_serverCapability!.supportsBatching) { + try { + final transactionVerboseBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), ); - } else { - transactionHexes.add(txVerbose['hex'] as String); + + txVerboseResults.addAll(transactionVerboseBatches.map((e) => e.result).toList()); + + transactionVerboseBatches.forEach((batch) { + final txVerbose = batch.result; + if (txVerbose.isEmpty) { + emptyVerboseTxs.add(batch.paramForRequest!.first as String); + } else { + transactionHexes.add(txVerbose['hex'] as String); + } + }); + } catch (_) { + _serverCapability!.supportsBatching = false; } - }); + } else { + await Future.wait(hashes.map((hash) async { + final history = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose(transactionHash: hash), + ); + + txVerboseResults.add(history); + + if (history.isEmpty) { + emptyVerboseTxs.add(hash); + } else { + transactionHexes.add(history['hex'] as String); + } + })); + } if (emptyVerboseTxs.isNotEmpty) { - transactionHexes.addAll((await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), - )) - .map((result) => result.result) - .toList()); + if (_serverCapability!.supportsBatching) { + try { + transactionHexes.addAll((await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), + )) + .map((e) => e.result) + .toList()); + } catch (_) { + _serverCapability!.supportsBatching = false; + } + } else { + await Future.wait(hashes.map((hash) async { + final history = await _electrumClient!.request( + ElectrumRequestGetTransactionHex(transactionHash: hash), + ); + + transactionHexes.add(history); + })); + } } final dates = {}; @@ -681,28 +1143,7 @@ class ElectrumWorker { insHashes.addAll(original.inputs.map((e) => e.txId)); } - final inputTransactionHexBatches = >[]; - - try { - inputTransactionHexBatches.addAll( - await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex( - transactionHashes: insHashes, - ), - Duration(seconds: 10), - ), - ); - } catch (_) {} - - await Future.forEach(insHashes, (String hash) async { - inputTransactionHexBatches.addAll( - await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex( - transactionHashes: [hash], - ), - ), - ); - }); + final inputTransactionHexById = await _getBatchTransactionHex(hashes: insHashes); for (final txHex in transactionHexes) { final original = BtcTransaction.fromRaw(txHex); @@ -710,13 +1151,7 @@ class ElectrumWorker { for (final input in original.inputs) { try { - final inputTransactionHex = inputTransactionHexBatches - .firstWhere( - (batch) => - (batch.request.paramsById[batch.id] as List).first == input.txId, - ) - .result; - + final inputTransactionHex = inputTransactionHexById[input.txId]!; ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } catch (_) {} } @@ -741,19 +1176,37 @@ class ElectrumWorker { Future>> _getBatchTransactionVerbose({ required List hashes, }) async { - final transactionHexBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionVerbose(transactionHashes: hashes), - ); + final txVerboseById = >{}; - final transactionsVerbose = >{}; + if (_serverCapability!.supportsBatching) { + try { + final inputTransactionHexBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionVerbose( + transactionHashes: hashes, + ), + ); - transactionHexBatches.forEach((result) { - final hash = result.request.paramsById[result.id]!.first as String; - final hex = result.result; - transactionsVerbose[hash] = hex; - }); + inputTransactionHexBatches.forEach((batch) { + final hash = batch.paramForRequest!.first as String; + final verbose = batch.result; + txVerboseById[hash] = verbose; + }); + } catch (_) { + _serverCapability!.supportsBatching = false; + } + } else { + await Future.wait(hashes.map((hash) async { + final verbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose( + transactionHash: hash, + ), + ); + + txVerboseById[hash] = verbose; + })); + } - return transactionsVerbose; + return txVerboseById; } Future _getTransactionExpanded({ @@ -820,19 +1273,37 @@ class ElectrumWorker { Future> _getBatchTransactionHex({ required List hashes, }) async { - final transactionHexBatches = await _electrumClient!.batchRequest( - ElectrumBatchRequestGetTransactionHex(transactionHashes: hashes), - ); + final inputTransactionHexById = {}; + + if (_serverCapability!.supportsBatching) { + try { + final inputTransactionHexBatches = await _electrumClient!.batchRequest( + ElectrumBatchRequestGetTransactionHex( + transactionHashes: hashes, + ), + ); - final transactionHexes = {}; + inputTransactionHexBatches.forEach((batch) { + final hash = batch.paramForRequest!.first as String; + final hex = batch.result; + inputTransactionHexById[hash] = hex; + }); + } catch (_) { + _serverCapability!.supportsBatching = false; + } + } else { + await Future.wait(hashes.map((hash) async { + final hex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex( + transactionHash: hash, + ), + ); - transactionHexBatches.forEach((result) { - final hash = result.request.paramsById[result.id]!.first as String; - final hex = result.result; - transactionHexes[hash] = hex; - }); + inputTransactionHexById[hash] = hex; + })); + } - return transactionHexes; + return inputTransactionHexById; } Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { diff --git a/cw_bitcoin/lib/electrum_worker/server_capability.dart b/cw_bitcoin/lib/electrum_worker/server_capability.dart new file mode 100644 index 0000000000..038c85755d --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/server_capability.dart @@ -0,0 +1,72 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; + +class ServerCapability { + static const ELECTRS_MIN_BATCHING_VERSION = ElectrumVersion(0, 9, 0); + static const FULCRUM_MIN_BATCHING_VERSION = ElectrumVersion(1, 6, 0); + static const MEMPOOL_ELECTRS_MIN_BATCHING_VERSION = ElectrumVersion(3, 1, 0); + + bool supportsBatching; + bool supportsTxVerbose; + + ServerCapability({required this.supportsBatching, required this.supportsTxVerbose}); + + static ServerCapability fromVersion(List serverVersion) { + if (serverVersion.isNotEmpty) { + final server = serverVersion.first.toLowerCase(); + + if (server.contains('electrumx')) { + return ServerCapability(supportsBatching: true, supportsTxVerbose: true); + } + + if (server.startsWith('electrs/')) { + var electrsVersion = server.substring('electrs/'.length); + final dashIndex = electrsVersion.indexOf('-'); + if (dashIndex > -1) { + electrsVersion = electrsVersion.substring(0, dashIndex); + } + + try { + final version = ElectrumVersion.fromStr(electrsVersion); + if (version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) { + return ServerCapability(supportsBatching: true, supportsTxVerbose: false); + } + } catch (e) { + // ignore version parsing errors + } + + return ServerCapability(supportsBatching: false, supportsTxVerbose: false); + } + + if (server.startsWith('fulcrum')) { + final fulcrumVersion = server.substring('fulcrum'.length).trim(); + + try { + final version = ElectrumVersion.fromStr(fulcrumVersion); + if (version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) { + return ServerCapability(supportsBatching: true, supportsTxVerbose: true); + } + } catch (e) {} + } + + if (server.startsWith('mempool-electrs')) { + var mempoolElectrsVersion = server.substring('mempool-electrs'.length).trim(); + final dashIndex = mempoolElectrsVersion.indexOf('-'); + + if (dashIndex > -1) { + mempoolElectrsVersion = mempoolElectrsVersion.substring(0, dashIndex); + } + + try { + final version = ElectrumVersion.fromStr(mempoolElectrsVersion); + if (version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0) { + return ServerCapability(supportsBatching: true, supportsTxVerbose: false); + } + } catch (e) { + // ignore version parsing errors + } + } + } + + return ServerCapability(supportsBatching: false, supportsTxVerbose: false); + } +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 92ea34bea2..d9714147a1 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -56,41 +56,39 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required super.walletInfo, required super.unspentCoinsInfo, required super.encryptionFileUtils, - List? seedBytes, + required super.hdWallets, super.mnemonic, super.xpub, String? passphrase, - String? addressPageType, - List? initialAddresses, - List? initialMwebAddresses, super.initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, int? initialMwebHeight, super.alwaysScan, super.didInitialSync, + Map? walletAddressesSnapshot, }) : super( network: LitecoinNetwork.mainnet, currency: CryptoCurrency.ltc, ) { - if (seedBytes != null) { - mwebHd = - Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1; - mwebEnabled = alwaysScan ?? false; + mwebEnabled = alwaysScan ?? false; + + if (walletAddressesSnapshot != null) { + // walletAddresses = LitecoinWalletAddresses.fromJson( + // walletAddressesSnapshot, + // walletInfo, + // network: network, + // isHardwareWallet: isHardwareWallet, + // hdWallets: hdWallets, + // ); } else { - mwebHd = null; - mwebEnabled = false; + walletAddresses = LitecoinWalletAddresses( + walletInfo, + network: network, + mwebEnabled: mwebEnabled, + isHardwareWallet: walletInfo.isHardwareWallet, + hdWallets: hdWallets, + ); } - walletAddresses = LitecoinWalletAddresses( - walletInfo, - initialAddresses: initialAddresses, - initialMwebAddresses: initialMwebAddresses, - network: network, - mwebHd: mwebHd, - mwebEnabled: mwebEnabled, - isHardwareWallet: walletInfo.isHardwareWallet, - hdWallets: hdWallets, - ); + autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); @@ -120,7 +118,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } }); } - late final Bip32Slip10Secp256k1? mwebHd; + + @override + LitecoinNetwork get network => LitecoinNetwork.mainnet; + + Bip32Slip10Secp256k1? get mwebHd => (walletAddresses as LitecoinWalletAddresses).mwebHd; + late final Box mwebUtxosBox; Timer? _syncTimer; Timer? _feeRatesTimer; @@ -142,26 +145,28 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required Box unspentCoinsInfo, required EncryptionFileUtils encryptionFileUtils, String? passphrase, - String? addressPageType, List? initialAddresses, List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, }) async { + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: LitecoinNetwork.mainnet, + mnemonic: mnemonic, + passphrase: passphrase, + ); + return LitecoinWallet( mnemonic: mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, - initialMwebAddresses: initialMwebAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, passphrase: passphrase, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, - addressPageType: addressPageType, + hdWallets: hdWallets, ); } @@ -209,22 +214,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { walletInfo.derivationInfo!.derivationPath ??= snp?.derivationPath ?? ELECTRUM_PATH; walletInfo.derivationInfo!.derivationType ??= snp?.derivationType ?? DerivationType.electrum; + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: LitecoinNetwork.mainnet, + mnemonic: keysData.mnemonic, + passphrase: keysData.passphrase, + xpub: keysData.xPub, + ); + return LitecoinWallet( mnemonic: keysData.mnemonic, xpub: keysData.xPub, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses, - initialMwebAddresses: snp?.mwebAddresses, initialBalance: snp?.balance, passphrase: keysData.passphrase, encryptionFileUtils: encryptionFileUtils, - initialRegularAddressIndex: snp?.regularAddressIndex, - initialChangeAddressIndex: snp?.changeAddressIndex, - addressPageType: snp?.addressPageType, alwaysScan: snp?.alwaysScan, didInitialSync: snp?.didInitialSync, + hdWallets: hdWallets, ); } @@ -268,7 +277,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { try { mwebSyncStatus = SynchronizingSyncStatus(); try { - await subscribeForUpdates([]); + await subscribeForStatuses(); } catch (e) { printV("failed to subcribe for updates: $e"); } @@ -857,7 +866,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { // } @override - TxCreateUtxoDetails createUTXOS({ + ElectrumTxCreateUtxoDetails createUTXOS({ required bool sendAll, int credentialsAmount = 0, int? inputsCount, @@ -961,7 +970,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { throw BitcoinTransactionNoInputsException(); } - return TxCreateUtxoDetails( + return ElectrumTxCreateUtxoDetails( availableInputs: availableInputs, unconfirmedCoins: unconfirmedCoins, utxos: utxos, @@ -973,7 +982,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - Future estimateSendAllTxMweb( + Future estimateSendAllTxMweb( List outputs, int feeRate, { String? memo, @@ -1008,7 +1017,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); } - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -1021,7 +1030,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { ); } - Future estimateTxForAmountMweb( + Future estimateTxForAmountMweb( int credentialsAmount, List outputs, int feeRate, { @@ -1065,7 +1074,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { throw BitcoinTransactionWrongBalanceException(); } - final changeAddress = await walletAddresses.getChangeAddress( + final changeAddress = await (walletAddresses as LitecoinWalletAddresses).getChangeAddress( inputs: utxoDetails.availableInputs, outputs: outputs, ); @@ -1121,7 +1130,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -1140,7 +1149,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { isChange: true, ); - return EstimatedTxResult( + return ElectrumEstimatedTx( utxos: utxoDetails.utxos, inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, publicKeys: utxoDetails.publicKeys, @@ -1246,7 +1255,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final data = getCreateTxDataFromCredentials(credentials); final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; - EstimatedTxResult estimatedTx; + ElectrumEstimatedTx estimatedTx; if (data.sendAll) { estimatedTx = await estimateSendAllTxMweb( data.outputs, @@ -1498,6 +1507,31 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } } + @action + Future refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((record) => record.walletId == id); + + for (final element in currentWalletUnspentCoins) { + if (RegexUtils.addressTypeFromStr(element.address, network) is MwebAddress) continue; + + final existUnspentCoins = unspentCoins.where((coin) => element == coin); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + printV("refreshUnspentCoinsInfo $e"); + } + } + @override Future save() async { await super.save(); diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index a5313fab38..02dce4d5db 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -23,13 +23,14 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with required super.hdWallets, required super.network, required super.isHardwareWallet, - required this.mwebHd, required this.mwebEnabled, super.initialAddresses, List? initialMwebAddresses, }) : mwebAddresses = ObservableList.of((initialMwebAddresses ?? []).toSet()), super(walletInfo) { + mwebHd = hdWallet.derivePath("m/1000'") as Bip32Slip10Secp256k1; + for (int i = 0; i < mwebAddresses.length; i++) { mwebAddrs.add(mwebAddresses[i].address); } @@ -38,7 +39,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with final ObservableList mwebAddresses; - final Bip32Slip10Secp256k1? mwebHd; + late final Bip32Slip10Secp256k1? mwebHd; bool mwebEnabled; int mwebTopUpIndex = 1000; List mwebAddrs = []; diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 0abf5552dc..76d0fce2a1 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_mnemonics_bip39.dart'; +import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/mnemonic_is_incorrect_exception.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -168,12 +169,19 @@ class LitecoinWalletService extends WalletService< credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath; + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: credentials.walletInfo!, + network: network, + xpub: credentials.hwAccountData.xpub, + ); + final wallet = await LitecoinWallet( password: credentials.password!, xpub: credentials.hwAccountData.xpub, walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource, encryptionFileUtils: encryptionFileUtilsFor(isDirect), + hdWallets: hdWallets, ); await wallet.save(); await wallet.init(); diff --git a/cw_bitcoin/lib/litecoin_wallet_snapshot.dart b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart index 1a07f75da3..c6e31624d2 100644 --- a/cw_bitcoin/lib/litecoin_wallet_snapshot.dart +++ b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart @@ -13,14 +13,11 @@ class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { required super.password, required super.mnemonic, required super.xpub, - required super.addresses, required super.balance, - required super.regularAddressIndex, - required super.changeAddressIndex, - required super.addressPageType, required this.mwebAddresses, required this.alwaysScan, required super.unspentCoins, + required super.walletAddressesSnapshot, super.passphrase, super.derivationType, super.derivationPath, @@ -62,16 +59,13 @@ class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { passphrase: electrumWalletSnapshot.passphrase, mnemonic: electrumWalletSnapshot.mnemonic, xpub: electrumWalletSnapshot.xpub, - addresses: electrumWalletSnapshot.addresses, - regularAddressIndex: electrumWalletSnapshot.regularAddressIndex, balance: electrumWalletSnapshot.balance, - changeAddressIndex: electrumWalletSnapshot.changeAddressIndex, - addressPageType: electrumWalletSnapshot.addressPageType, derivationType: electrumWalletSnapshot.derivationType, derivationPath: electrumWalletSnapshot.derivationPath, unspentCoins: electrumWalletSnapshot.unspentCoins, mwebAddresses: mwebAddresses, alwaysScan: alwaysScan, + walletAddressesSnapshot: electrumWalletSnapshot.walletAddressesSnapshot, ); } } diff --git a/cw_bitcoin/lib/wallet_seed_bytes.dart b/cw_bitcoin/lib/wallet_seed_bytes.dart index 4f5d8542e2..d4e6d9ec0a 100644 --- a/cw_bitcoin/lib/wallet_seed_bytes.dart +++ b/cw_bitcoin/lib/wallet_seed_bytes.dart @@ -1,6 +1,7 @@ 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/bitcoin_mnemonic.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; @@ -9,11 +10,13 @@ class WalletSeedData { WalletSeedData({required this.hdWallets}); - static WalletSeedData fromMnemonic(WalletInfo walletInfo, String mnemonic, [String? passphrase]) { + static Future fromMnemonic(WalletInfo walletInfo, String mnemonic, + [String? passphrase]) async { final Map hdWallets = {}; for (final derivation in walletInfo.derivations ?? [walletInfo.derivationInfo!]) { - if (derivation.derivationType == DerivationType.bip39) { + if (derivation.derivationType == DerivationType.bip39 && + !hdWallets.containsKey(CWBitcoinDerivationType.bip39)) { try { final seedBytes = Bip39SeedGenerator.generateFromString(mnemonic, passphrase); hdWallets[CWBitcoinDerivationType.bip39] = Bip32Slip10Secp256k1.fromSeed(seedBytes); @@ -24,7 +27,8 @@ class WalletSeedData { continue; } - if (derivation.derivationType == DerivationType.electrum) { + if (derivation.derivationType == DerivationType.electrum && + !hdWallets.containsKey(CWBitcoinDerivationType.electrum)) { late List seedBytes; try { @@ -36,6 +40,12 @@ class WalletSeedData { seedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); } catch (e) { printV("electrum_v1 seed error: $e"); + + try { + seedBytes = await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + } catch (e) { + printV("old electrum seed error: $e"); + } } } diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index a0a8ab25c5..7402beac92 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,10 +1,8 @@ import 'package:bitbox/bitbox.dart' as bitbox; 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/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'; @@ -28,25 +26,33 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required super.walletInfo, required super.unspentCoinsInfo, required super.encryptionFileUtils, + required super.hdWallets, super.passphrase, BitcoinAddressType? addressPageType, - List? initialAddresses, super.initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, super.didInitialSync, + Map? walletAddressesSnapshot, }) : super( network: BitcoinCashNetwork.mainnet, currency: CryptoCurrency.bch, ) { - walletAddresses = BitcoinCashWalletAddresses( - walletInfo, - initialAddresses: initialAddresses, - hdWallets: hdWallets, - network: network, - initialAddressPageType: addressPageType, - isHardwareWallet: walletInfo.isHardwareWallet, - ); + if (walletAddressesSnapshot != null) { + walletAddresses = BitcoinCashWalletAddressesBase.fromJson( + walletAddressesSnapshot, + walletInfo, + network: network, + isHardwareWallet: isHardwareWallet, + hdWallets: hdWallets, + ); + } else { + this.walletAddresses = BitcoinCashWalletAddresses( + walletInfo, + network: network, + isHardwareWallet: isHardwareWallet, + hdWallets: hdWallets, + ); + } + autorun((_) { this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; }); @@ -78,23 +84,25 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, - List? initialAddresses, ElectrumBalance? initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, }) async { + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: BitcoinCashNetwork.mainnet, + mnemonic: mnemonic, + passphrase: passphrase, + ); + return BitcoinCashWallet( mnemonic: mnemonic, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: initialAddresses, initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, - initialRegularAddressIndex: initialRegularAddressIndex, - initialChangeAddressIndex: initialChangeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: passphrase, + hdWallets: hdWallets, ); } @@ -135,42 +143,25 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { ); } + final hdWallets = await ElectrumWalletBase.getAccountHDWallets( + walletInfo: walletInfo, + network: BitcoinCashNetwork.mainnet, + mnemonic: keysData.mnemonic, + passphrase: keysData.passphrase, + xpub: keysData.xPub, + ); + return BitcoinCashWallet( mnemonic: keysData.mnemonic!, password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses.map((addr) { - try { - BitcoinCashAddress(addr.address); - return BitcoinAddressRecord( - addr.address, - index: addr.index, - isChange: addr.isChange, - type: P2pkhAddressType.p2pkh, - network: BitcoinCashNetwork.mainnet, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), - cwDerivationType: CWBitcoinDerivationType.bip39, - ); - } catch (_) { - return BitcoinAddressRecord( - AddressUtils.getCashAddrFormat(addr.address), - index: addr.index, - isChange: addr.isChange, - type: P2pkhAddressType.p2pkh, - network: BitcoinCashNetwork.mainnet, - derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), - cwDerivationType: CWBitcoinDerivationType.bip39, - ); - } - }).toList(), initialBalance: snp?.balance, encryptionFileUtils: encryptionFileUtils, - initialRegularAddressIndex: snp?.regularAddressIndex, - initialChangeAddressIndex: snp?.changeAddressIndex, addressPageType: P2pkhAddressType.p2pkh, passphrase: keysData.passphrase, didInitialSync: snp?.didInitialSync, + hdWallets: hdWallets, ); } 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 25d2955e0f..d6bf07588c 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -1,5 +1,8 @@ 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/electrum_wallet_addresses.dart'; +import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; @@ -37,4 +40,46 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi isChange: isChange, index: index, ); + + static BitcoinCashWalletAddressesBase fromJson( + Map json, + WalletInfo walletInfo, { + required Map hdWallets, + required BasedUtxoNetwork network, + required bool isHardwareWallet, + List? initialAddresses, + }) { + initialAddresses ??= (json['allAddresses'] as List).map((addr) { + try { + BitcoinCashAddress(addr.address); + return BitcoinAddressRecord( + addr.address, + index: addr.index, + isChange: addr.isChange, + type: P2pkhAddressType.p2pkh, + network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + cwDerivationType: CWBitcoinDerivationType.bip39, + ); + } catch (_) { + return BitcoinAddressRecord( + AddressUtils.getCashAddrFormat(addr.address), + index: addr.index, + isChange: addr.isChange, + type: P2pkhAddressType.p2pkh, + network: BitcoinCashNetwork.mainnet, + derivationInfo: BitcoinAddressUtils.getDerivationFromType(P2pkhAddressType.p2pkh), + cwDerivationType: CWBitcoinDerivationType.bip39, + ); + } + }).toList(); + + return BitcoinCashWalletAddresses( + walletInfo, + hdWallets: hdWallets, + network: network, + isHardwareWallet: isHardwareWallet, + initialAddresses: initialAddresses, + ); + } } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 942c1f7d37..40dc828efe 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -89,8 +89,13 @@ class CWBitcoin extends Bitcoin { List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; @override - TransactionPriority deserializeBitcoinTransactionPriority(int raw) => - ElectrumTransactionPriority.deserialize(raw: raw); + TransactionPriority deserializeBitcoinTransactionPriority(int raw) { + try { + return ElectrumTransactionPriority.deserialize(raw: raw); + } catch (_) { + return BitcoinAPITransactionPriority.deserialize(raw: raw); + } + } @override TransactionPriority deserializeLitecoinTransactionPriority(int raw) => @@ -389,6 +394,7 @@ class CWBitcoin extends Bitcoin { if (passphrase == null || passphrase.isEmpty) { try { // TODO: language pick +// electrumSeedBytes = ElectrumV1SeedGenerator(mnemonic).generate(); } catch (e) { printV("electrum_v1 seed error: $e"); @@ -429,6 +435,25 @@ class CWBitcoin extends Bitcoin { } } + if (electrumSeedBytes == null && bip39SeedBytes == null) { + try { + final oldElectrumSeedBytes = + await mnemonicToSeedBytes(mnemonic, passphrase: passphrase ?? ""); + + for (final addressType in BITCOIN_ADDRESS_TYPES) { + list.add( + DerivationInfo( + derivationType: DerivationType.electrum, + derivationPath: "m/0'", + scriptType: addressType.value, + ), + ); + } + } catch (e) { + printV("old electrum seed error: $e"); + } + } + return list; } @@ -767,6 +792,7 @@ class CWBitcoin extends Bitcoin { } } + // // TODO: this could be improved: return inputAddressesContainMweb || outputAddressesContainMweb; } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 284da1af21..a27b95fb7b 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -154,6 +154,7 @@ abstract class WalletCreationVMBase with Store { return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; case WalletType.bitcoinCash: return DerivationInfo( + derivationType: DerivationType.bip39, derivationPath: "m/44'/145'/0'", description: "Default Bitcoin Cash", scriptType: "p2pkh", From 3cc7fba130576937cf1211f4583532eacdc30c10 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Thu, 23 Jan 2025 09:55:57 -0300 Subject: [PATCH 57/64] fix(worker): server capability for tx verbose --- .../lib/electrum_worker/electrum_worker.dart | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index c5252cafd9..7f63b50321 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -345,18 +345,21 @@ class ElectrumWorker { } if (transactionIdsForHeights.isNotEmpty) { - final transactionsVerbose = await _getBatchTransactionVerbose( - hashes: transactionIdsForHeights.keys.toList(), - ); + Map>? transactionsVerbose; + if (_serverCapability!.supportsTxVerbose) { + transactionsVerbose = await _getBatchTransactionVerbose( + hashes: transactionIdsForHeights.keys.toList(), + ); + } Map transactionHexes = {}; - if (transactionsVerbose.isEmpty) { + if (transactionsVerbose?.isEmpty ?? true) { transactionHexes = await _getBatchTransactionHex( hashes: transactionIdsForHeights.keys.toList(), ); } else { - transactionsVerbose.values.forEach((e) { + transactionsVerbose!.values.forEach((e) { transactionHexes[e['txid'] as String] = e['hex'] as String; }); } @@ -365,7 +368,7 @@ class ElectrumWorker { final hash = transactionIdHeight.key; final hex = transactionIdHeight.value; - final transactionVerbose = transactionsVerbose[hash]; + final transactionVerbose = transactionsVerbose?[hash]; late ElectrumTransactionBundle txBundle; @@ -473,37 +476,40 @@ class ElectrumWorker { if (transactionIdsForHeights.isNotEmpty) { await Future.wait(transactionIdsForHeights.keys.toList().map((hash) async { - final transactionVerbose = await _electrumClient!.request( - ElectrumRequestGetTransactionVerbose(transactionHash: hash), - ); - - late String transactionHex; + late String txHex; + Map? txVerbose; - if (transactionVerbose.isEmpty) { - transactionHex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex( + if (_serverCapability!.supportsTxVerbose) { + txVerbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose( transactionHash: hash, ), ); + } + + if (txVerbose?.isEmpty ?? true) { + txHex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex(transactionHash: hash), + ); } else { - transactionHex = transactionVerbose['hex'] as String; + txHex = txVerbose!['hex'] as String; } late ElectrumTransactionBundle txBundle; // this is the initial tx history update, so ins will be filled later one by one, // and time and confirmations will be updated if needed again - if (transactionVerbose.isNotEmpty) { + if (txVerbose?.isNotEmpty ?? false) { txBundle = ElectrumTransactionBundle( - BtcTransaction.fromRaw(transactionHex), + BtcTransaction.fromRaw(txHex), ins: [], - time: transactionVerbose['time'] as int?, - confirmations: (transactionVerbose['confirmations'] as int?) ?? 1, - isDateValidated: (transactionVerbose['time'] as int?) != null, + time: txVerbose!['time'] as int?, + confirmations: (txVerbose['confirmations'] as int?) ?? 1, + isDateValidated: (txVerbose['time'] as int?) != null, ); } else { txBundle = ElectrumTransactionBundle( - BtcTransaction.fromRaw(transactionHex), + BtcTransaction.fromRaw(txHex), ins: [], confirmations: 1, ); @@ -629,18 +635,22 @@ class ElectrumWorker { } if (transactionsByIds.isNotEmpty) { - final transactionsVerbose = await _getBatchTransactionVerbose( - hashes: transactionsByIds.keys.toList(), - ); + Map>? transactionsVerbose; + + if (_serverCapability!.supportsTxVerbose) { + transactionsVerbose = await _getBatchTransactionVerbose( + hashes: transactionsByIds.keys.toList(), + ); + } Map transactionHexes = {}; - if (transactionsVerbose.isEmpty) { + if (transactionsVerbose?.isEmpty ?? true) { transactionHexes = await _getBatchTransactionHex( hashes: transactionsByIds.keys.toList(), ); } else { - transactionsVerbose.values.forEach((e) { + transactionsVerbose!.values.forEach((e) { transactionHexes[e['txid'] as String] = e['hex'] as String; }); } @@ -649,7 +659,7 @@ class ElectrumWorker { final hash = entry.key; final txToFetch = entry.value; final storedTx = txToFetch.tx; - final txVerbose = transactionsVerbose[hash]; + final txVerbose = transactionsVerbose?[hash]; final txHex = transactionHexes[hash]!; final original = storedTx?.original ?? BtcTransaction.fromRaw((txVerbose?["hex"] as String?) ?? txHex); @@ -1088,16 +1098,20 @@ class ElectrumWorker { } } else { await Future.wait(hashes.map((hash) async { - final history = await _electrumClient!.request( - ElectrumRequestGetTransactionVerbose(transactionHash: hash), - ); + Map? txVerbose; - txVerboseResults.add(history); + if (_serverCapability!.supportsTxVerbose) { + txVerbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose( + transactionHash: hash, + ), + ); + } - if (history.isEmpty) { + if (txVerbose?.isEmpty ?? true) { emptyVerboseTxs.add(hash); } else { - transactionHexes.add(history['hex'] as String); + transactionHexes.add(txVerbose!['hex'] as String); } })); } @@ -1220,13 +1234,17 @@ class ElectrumWorker { int? time; DateResult? dates; - final transactionVerbose = await _electrumClient!.request( - ElectrumRequestGetTransactionVerbose(transactionHash: hash), - ); + Map? transactionVerbose; + if (_serverCapability!.supportsTxVerbose) { + transactionVerbose = await _electrumClient!.request( + ElectrumRequestGetTransactionVerbose(transactionHash: hash), + ); + } + String transactionHex; - if (transactionVerbose.isNotEmpty) { - transactionHex = transactionVerbose['hex'] as String; + if (transactionVerbose?.isNotEmpty ?? false) { + transactionHex = transactionVerbose!['hex'] as String; time = transactionVerbose['time'] as int?; confirmations = transactionVerbose['confirmations'] as int?; } else { From 7da30d85b743b0f5f5f97b5454cd917b369d236e Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Thu, 23 Jan 2025 21:45:03 +0200 Subject: [PATCH 58/64] fallback to electrum get fees if mempool api failed --- cw_bitcoin/lib/bitcoin_address_record.dart | 9 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 6 +- .../lib/electrum_worker/electrum_worker.dart | 8 +- cw_bitcoin/pubspec.lock | 138 +++++++++--------- cw_bitcoin/pubspec.yaml | 12 +- cw_bitcoin_cash/pubspec.yaml | 10 +- cw_core/pubspec.lock | 4 +- 7 files changed, 96 insertions(+), 91 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index e82a4eb8e0..c1bbd43f47 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; abstract class BaseBitcoinAddressRecord { @@ -27,8 +28,10 @@ abstract class BaseBitcoinAddressRecord { final String address; bool _isHidden; + bool get isHidden => _isHidden; final bool _isChange; + bool get isChange => _isChange; final int index; int _txCount; @@ -126,6 +129,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { derivationInfo: BitcoinDerivationInfo.fromJSON( decoded['derivationInfo'] as Map, ), + // TODO: make nullable maybe? cwDerivationType: CWBitcoinDerivationType.values[decoded['derivationType'] as int], isHidden: decoded['isHidden'] as bool? ?? false, isChange: decoded['isChange'] as bool? ?? false, @@ -177,7 +181,9 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { String _derivationPath; + String get derivationPath => _derivationPath; + int get labelIndex => index; final String? labelHex; @@ -211,7 +217,8 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, - derivationPath: decoded['derivationPath'] as String, + derivationPath: + decoded['derivationPath'] as String? ?? BitcoinWalletAddressesBase.OLD_SP_SPEND_PATH, labelIndex: decoded['index'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 4fcd5d2bea..98356277b8 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -79,9 +79,9 @@ class ElectrumWalletSnapshot { balance: balance, derivationType: derivationType, derivationPath: derivationPath, - unspentCoins: (data['unspent_coins'] as List) - .map((e) => BitcoinUnspent.fromJSON(null, e as Map)) - .toList(), + unspentCoins: (data['unspent_coins'] as List?) + ?.map((e) => BitcoinUnspent.fromJSON(null, e as Map)) + .toList() ?? [], didInitialSync: data['didInitialSync'] as bool?, walletAddressesSnapshot: walletAddressesSnapshot, ); diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index 7f63b50321..b106c430c0 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -1363,7 +1363,13 @@ class ElectrumWorker { ), ); } catch (e) { - _sendError(ElectrumWorkerGetFeesError(error: e.toString())); + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: ElectrumTransactionPriorities.fromList( + await _electrumClient!.getFeeRates(), + ), + ), + ); } } else { _sendResponse( diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 7af05b4959..81024d5a01 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "1.5.8" async: dependency: transitive description: @@ -84,16 +84,16 @@ packages: source: git version: "1.0.1" bitcoin_base: - dependency: "direct overridden" + dependency: "direct main" description: path: "." ref: cake-update-v15 - resolved-ref: "10bb92c563e2e05041f4215ae20d834db6c9b2cf" + resolved-ref: db0856e1f69f148c8cfc5e8861e1c777ce8f85db url: "https://github.com/cake-tech/bitcoin_base" source: git version: "5.0.0" blockchain_utils: - dependency: "direct main" + dependency: "direct overridden" description: path: "." ref: cake-update-v4 @@ -185,10 +185,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.3" cake_backup: dependency: transitive description: @@ -218,10 +218,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -234,10 +234,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: @@ -312,10 +312,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" encrypt: dependency: transitive description: @@ -373,10 +373,10 @@ packages: dependency: "direct main" description: name: flutter_mobx - sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -386,10 +386,10 @@ packages: dependency: transitive description: name: flutter_web_bluetooth - sha256: fcd03e2e5f82edcedcbc940f1b6a0635a50757374183254f447640886c53208e + sha256: "1363831def5eed1e1064d1eca04e8ccb35446e8f758579c3c519e156b77926da" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "1.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -407,10 +407,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_identity_services_web: dependency: transitive description: @@ -471,26 +471,26 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http2: dependency: transitive description: name: http2 - sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -560,7 +560,7 @@ packages: description: path: "packages/ledger-bitcoin" ref: HEAD - resolved-ref: "07cd61ef76a2a017b6d5ef233396740163265457" + resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" @@ -568,16 +568,16 @@ packages: dependency: "direct main" description: name: ledger_flutter_plus - sha256: c7b04008553193dbca7e17b430768eecc372a72b0ff3625b5e7fc5e5c8d3231b + sha256: "58e550ef7f4e20801c1333847befb0d64592b9765a839d4463524bb6d674967c" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.5.0" ledger_litecoin: dependency: "direct main" description: path: "packages/ledger-litecoin" ref: HEAD - resolved-ref: "3dee36713e6ebec9dceb59b9ccae7f243a53ea9e" + resolved-ref: e93254f3ff3f996fb91f65a1e7ceffb9f510b4c8 url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.2" @@ -633,10 +633,10 @@ packages: dependency: "direct main" description: name: mobx - sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.5.0" mobx_codegen: dependency: "direct dev" description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" path: dependency: transitive description: @@ -673,26 +673,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -777,18 +777,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" quiver: dependency: transitive description: @@ -809,26 +809,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -873,10 +873,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -943,10 +943,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -975,10 +975,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tuple: dependency: transitive description: @@ -999,10 +999,10 @@ packages: dependency: transitive description: name: universal_ble - sha256: "0dfbd6b64bff3ad61ed7a895c232530d9614e9b01ab261a74433a43267edb7f3" + sha256: c7e5156b9bbb0f9662f07e987d8d6f2083727fa8c221b19a7ecc2f3a6e8f33c3 url: "https://pub.dev" source: hosted - version: "0.12.0" + version: "0.14.0" universal_platform: dependency: transitive description: @@ -1031,18 +1031,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -1063,10 +1063,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" xdg_directories: dependency: transitive description: @@ -1087,18 +1087,18 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yaml_edit: dependency: transitive description: name: yaml_edit - sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 4d018084e2..1e1c8f3a0e 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -20,16 +20,16 @@ dependencies: shared_preferences: ^2.0.15 cw_core: path: ../cw_core + bitcoin_base: + git: + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v15 bitbox: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data rxdart: ^0.28.0 cryptography: ^2.0.5 - blockchain_utils: - git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v4 cw_mweb: path: ../cw_mweb grpc: ^3.2.4 @@ -61,10 +61,6 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v15 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 583bf44b5a..346cfb8dc0 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -25,10 +25,10 @@ dependencies: git: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data - blockchain_utils: + bitcoin_base: git: - url: https://github.com/cake-tech/blockchain_utils - ref: cake-update-v4 + url: https://github.com/cake-tech/bitcoin_base + ref: cake-update-v15 dev_dependencies: flutter_test: @@ -39,10 +39,6 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 - bitcoin_base: - git: - url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v15 blockchain_utils: git: url: https://github.com/cake-tech/blockchain_utils diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 44ef15a417..c12839a19d 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -722,10 +722,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: "direct overridden" description: From 309dca9ac943e5a5954c1da3f490347fcaba2e3a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 24 Jan 2025 13:04:36 -0300 Subject: [PATCH 59/64] feat: misc reviews --- cw_bitcoin/lib/bitcoin_address_record.dart | 49 +++-- cw_bitcoin/lib/bitcoin_unspent.dart | 15 +- cw_bitcoin/lib/bitcoin_wallet.dart | 57 +++--- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 28 +-- cw_bitcoin/lib/bitcoin_wallet_snapshot.dart | 7 +- cw_bitcoin/lib/electrum_transaction_info.dart | 7 +- cw_bitcoin/lib/electrum_wallet.dart | 38 ++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 116 +++++------ cw_bitcoin/lib/electrum_wallet_snapshot.dart | 13 +- .../lib/electrum_worker/electrum_worker.dart | 140 +++++++------ .../electrum_worker_methods.dart | 2 + .../electrum_worker/methods/get_tx_hex.dart | 77 ++++++++ .../lib/electrum_worker/methods/methods.dart | 1 + .../methods/tweaks_subscribe.dart | 10 +- .../lib/electrum_worker/methods/version.dart | 8 +- .../electrum_worker/server_capability.dart | 43 +++- cw_bitcoin/lib/litecoin_wallet.dart | 186 ++++++++++-------- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 66 ++++++- cw_bitcoin/lib/litecoin_wallet_snapshot.dart | 7 +- .../lib/src/bitcoin_cash_wallet.dart | 2 +- .../src/bitcoin_cash_wallet_addresses.dart | 3 + lib/bitcoin/cw_bitcoin.dart | 5 +- lib/entities/priority_for_wallet_type.dart | 2 +- lib/src/screens/receive/receive_page.dart | 28 ++- lib/src/screens/send/widgets/send_card.dart | 2 +- .../screens/settings/other_settings_page.dart | 4 +- lib/view_model/send/send_view_model.dart | 2 +- .../settings/other_settings_view_model.dart | 4 +- .../transaction_details_view_model.dart | 2 +- 29 files changed, 577 insertions(+), 347 deletions(-) create mode 100644 cw_bitcoin/lib/electrum_worker/methods/get_tx_hex.dart diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index c1bbd43f47..855b33c8f8 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -4,6 +4,7 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; abstract class BaseBitcoinAddressRecord { BaseBitcoinAddressRecord( @@ -20,16 +21,15 @@ abstract class BaseBitcoinAddressRecord { _balance = balance, _name = name, _isUsed = isUsed, - _isHidden = isHidden ?? isChange, + 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; @@ -53,7 +53,7 @@ abstract class BaseBitcoinAddressRecord { void setAsUsed() { _isUsed = true; - _isHidden = true; + isHidden = true; } void setNewName(String label) => _name = label; @@ -75,11 +75,15 @@ abstract class BaseBitcoinAddressRecord { 'runtimeType': runtimeType.toString(), }); - static BaseBitcoinAddressRecord fromJSON(String jsonSource) { + static BaseBitcoinAddressRecord fromJSON( + String jsonSource, [ + DerivationInfo? derivationInfo, + BasedUtxoNetwork? network, + ]) { final decoded = json.decode(jsonSource) as Map; if (decoded['runtimeType'] == 'BitcoinAddressRecord') { - return BitcoinAddressRecord.fromJSON(jsonSource); + return BitcoinAddressRecord.fromJSON(jsonSource, derivationInfo, network); } else if (decoded['runtimeType'] == 'BitcoinSilentPaymentAddressRecord') { return BitcoinSilentPaymentAddressRecord.fromJSON(jsonSource); } else if (decoded['runtimeType'] == 'BitcoinReceivedSPAddressRecord') { @@ -87,7 +91,7 @@ abstract class BaseBitcoinAddressRecord { } else if (decoded['runtimeType'] == 'LitecoinMWEBAddressRecord') { return LitecoinMWEBAddressRecord.fromJSON(jsonSource); } else { - throw ArgumentError('Unknown runtimeType'); + return BitcoinAddressRecord.fromJSON(jsonSource, derivationInfo, network); } } } @@ -120,17 +124,34 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord { } } - factory BitcoinAddressRecord.fromJSON(String jsonSource) { + factory BitcoinAddressRecord.fromJSON( + String jsonSource, [ + DerivationInfo? derivationInfo, + BasedUtxoNetwork? network, + ]) { final decoded = json.decode(jsonSource) as Map; + final derivationInfoSnp = decoded['derivationInfo'] as Map?; + final derivationTypeSnp = decoded['derivationType'] as int?; + final cwDerivationType = derivationTypeSnp != null + ? CWBitcoinDerivationType.values[derivationTypeSnp] + : derivationInfo!.derivationType == DerivationType.bip39 + ? CWBitcoinDerivationType.old_bip39 + : CWBitcoinDerivationType.old_electrum; return BitcoinAddressRecord( decoded['address'] as String, index: decoded['index'] as int, - derivationInfo: BitcoinDerivationInfo.fromJSON( - decoded['derivationInfo'] as Map, - ), - // TODO: make nullable maybe? - cwDerivationType: CWBitcoinDerivationType.values[decoded['derivationType'] as int], + derivationInfo: derivationInfoSnp == null + ? [CWBitcoinDerivationType.bip39, CWBitcoinDerivationType.old_bip39] + .contains(cwDerivationType) + ? BitcoinDerivationInfo.fromDerivationAndAddress( + BitcoinDerivationType.bip39, + decoded['address'] as String, + network!, + ) + : BitcoinDerivationInfos.ELECTRUM + : BitcoinDerivationInfo.fromJSON(derivationInfoSnp), + cwDerivationType: cwDerivationType, isHidden: decoded['isHidden'] as bool? ?? false, isChange: decoded['isChange'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false, @@ -218,7 +239,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { return BitcoinSilentPaymentAddressRecord( decoded['address'] as String, derivationPath: - decoded['derivationPath'] as String? ?? BitcoinWalletAddressesBase.OLD_SP_SPEND_PATH, + (decoded['derivationPath'] as String?) ?? BitcoinWalletAddressesBase.OLD_SP_PATH, labelIndex: decoded['index'] as int, isUsed: decoded['isUsed'] as bool? ?? false, txCount: decoded['txCount'] as int? ?? 0, diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 0135e1d74b..3efe022651 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,6 +1,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:cw_core/wallet_info.dart'; class BitcoinUnspent extends Unspent { BitcoinUnspent(BaseBitcoinAddressRecord addressRecord, String hash, int value, int vout) @@ -10,9 +11,19 @@ class BitcoinUnspent extends Unspent { factory BitcoinUnspent.fromUTXO(BaseBitcoinAddressRecord address, ElectrumUtxo utxo) => BitcoinUnspent(address, utxo.txId, utxo.value.toInt(), utxo.vout); - factory BitcoinUnspent.fromJSON(BaseBitcoinAddressRecord? address, Map json) { + factory BitcoinUnspent.fromJSON( + BaseBitcoinAddressRecord? address, + Map json, [ + DerivationInfo? derivationInfo, + BasedUtxoNetwork? network, + ]) { return BitcoinUnspent( - address ?? BaseBitcoinAddressRecord.fromJSON(json['address_record'] as String), + address ?? + BaseBitcoinAddressRecord.fromJSON( + json['address_record'] as String, + derivationInfo, + network, + ), json['tx_hash'] as String, int.parse(json['value'].toString()), int.parse(json['tx_pos'].toString()), diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 14eb5f6b31..254ce9db27 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -144,7 +144,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { snp = await BitcoinWalletSnapshot.load( encryptionFileUtils, name, - walletInfo.type, + walletInfo, password, network, ); @@ -209,27 +209,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final isNamedElectrs = node?.uri.host.contains("electrs") ?? false; if (isNamedElectrs) { node!.isElectrs = true; + node!.save(); + return true; } final isNamedFulcrum = node!.uri.host.contains("fulcrum"); if (isNamedFulcrum) { node!.isElectrs = false; + node!.save(); + return false; } - if (node!.isElectrs == null) { - final version = await waitSendWorker(ElectrumWorkerGetVersionRequest()); + final version = await waitSendWorker(ElectrumWorkerGetVersionRequest()); - if (version is List && version.isNotEmpty) { - final server = version[0]; + if (version is List && version.isNotEmpty) { + final server = version[0]; - if (server.toLowerCase().contains('electrs')) { - node!.isElectrs = true; - } - } else if (version is String && version.toLowerCase().contains('electrs')) { + if (server.toLowerCase().contains('electrs')) { node!.isElectrs = true; - } else { - node!.isElectrs = false; } + } else if (version is String && version.toLowerCase().contains('electrs')) { + node!.isElectrs = true; + } else { + node!.isElectrs = false; } node!.save(); @@ -293,8 +295,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { final psbtReadyInputs = []; for (final utxo in utxos) { - final rawTx = - (await getTransactionExpanded(hash: utxo.utxo.txHash)).originalTransaction.toHex(); + final rawTx = await getTransactionHex(hash: utxo.utxo.txHash); final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!; psbtReadyInputs.add(PSBTReadyUtxoWithAddress( @@ -373,7 +374,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } @action - Future setSilentPaymentsScanning(bool active) async { + Future setSilentPaymentsScanning(bool active, [int? height, bool? doSingleScan]) async { silentPaymentsScanningActive = active; final nodeSupportsSilentPayments = await getNodeSupportsSilentPayments(); final isAllowedToScan = nodeSupportsSilentPayments || allowedToSwitchNodesForScanning; @@ -382,14 +383,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { syncStatus = AttemptingScanSyncStatus(); final tip = currentChainTip!; + final beginHeight = height ?? walletInfo.restoreHeight; - if (tip == walletInfo.restoreHeight) { + if (tip == beginHeight) { syncStatus = SyncedTipSyncStatus(tip); return; } - if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight); + if (tip > beginHeight) { + _requestTweakScanning(beginHeight, doSingleScan: doSingleScan); } } else if (syncStatus is! SyncedSyncStatus) { await waitSendWorker(ElectrumWorkerStopScanningRequest()); @@ -501,23 +503,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } } - @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); + setSilentPaymentsScanning(true, height, doSingleScan); } @action @@ -645,14 +634,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { } final height = result.height; - if (height != null) { + if (height != null && result.wasSingleBlock == false) { await walletInfo.updateRestoreHeight(height); } } } @action - Future _setListeners(int height, {bool? doSingleScan}) async { + Future _requestTweakScanning(int height, {bool? doSingleScan}) async { if (currentChainTip == null) { throw Exception("currentChainTip is null"); } @@ -763,7 +752,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { // New headers received, start scanning if (alwaysScan == true && syncStatus is SyncedSyncStatus) { - _setListeners(walletInfo.restoreHeight); + _requestTweakScanning(walletInfo.restoreHeight); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 6f3063f837..52696a14af 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -31,7 +31,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S silentPaymentWallets = [silentPaymentWallet!]; } - static const OLD_SP_SPEND_PATH = "m/352'/1'/0'/0'/0"; + static const OLD_SP_PATH = "m/352'/1'/0'/#'/0"; static const BITCOIN_ADDRESS_TYPES = [ SegwitAddressType.p2wpkh, P2pkhAddressType.p2pkh, @@ -74,8 +74,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S if (silentPaymentAddresses.isEmpty) { if (walletInfo.isRecovery) { - final oldScanPath = Bip32PathParser.parse("m/352'/1'/0'/1'/0"); - final oldSpendPath = Bip32PathParser.parse("m/352'/1'/0'/0'/0"); + final oldScanPath = Bip32PathParser.parse(OLD_SP_PATH.replaceFirst("#", "1")); + final oldSpendPath = Bip32PathParser.parse(OLD_SP_PATH.replaceFirst("#", "0")); final oldSilentPaymentWallet = SilentPaymentOwner.fromPrivateKeys( b_scan: ECPrivate(hdWallet.derive(oldScanPath).privateKey), @@ -315,7 +315,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S final usableSilentPaymentAddresses = silentPaymentAddresses .where((a) => a.type != SegwitAddressType.p2tr && - a.derivationPath != OLD_SP_SPEND_PATH && + a.derivationPath != OLD_SP_PATH && a.isChange == false) .toList(); final nextSPLabelIndex = usableSilentPaymentAddresses.length; @@ -330,7 +330,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S ); silentPaymentAddresses.add(address); - updateAddressesOnReceiveScreen(); + updateAddressesByType(); return address; } @@ -382,14 +382,17 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override @action - void updateAddressesOnReceiveScreen() { + void updateAddressesByType() { if (addressPageType == SilentPaymentsAddresType.p2sp) { - addressesOnReceiveScreen.clear(); - addressesOnReceiveScreen.addAll(silentPaymentAddresses); + receiveAddressesByType.clear(); + receiveAddressesByType[SilentPaymentsAddresType.p2sp] = silentPaymentAddresses + .where((addressRecord) => + addressRecord.type == SilentPaymentsAddresType.p2sp && !addressRecord.isChange) + .toList(); return; } - super.updateAddressesOnReceiveScreen(); + super.updateAddressesByType(); } @action @@ -398,7 +401,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S addressesSet.addAll(addresses); this.silentPaymentAddresses.clear(); this.silentPaymentAddresses.addAll(addressesSet); - updateAddressesOnReceiveScreen(); + updateAddressesByType(); } @action @@ -407,7 +410,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S addressesSet.addAll(addresses); this.receivedSPAddresses.clear(); this.receivedSPAddresses.addAll(addressesSet); - updateAddressesOnReceiveScreen(); + updateAddressesByType(); } @action @@ -416,7 +419,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S addressRecord.type == SilentPaymentsAddresType.p2sp && addressRecord.address == address); silentPaymentAddresses.remove(addressRecord); - updateAddressesOnReceiveScreen(); + updateAddressesByType(); } Map get labels { @@ -492,6 +495,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required Map hdWallets, required BasedUtxoNetwork network, required bool isHardwareWallet, + // TODO: make it used List? initialAddresses, List? initialSilentAddresses, List? initialReceivedSPAddresses, diff --git a/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart index 68dab90e77..b722ac74c9 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_snapshot.dart @@ -4,7 +4,7 @@ import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/wallet_info.dart'; class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { BitcoinWalletSnapshot({ @@ -27,10 +27,11 @@ class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { static Future load( EncryptionFileUtils encryptionFileUtils, String name, - WalletType type, + WalletInfo walletInfo, String password, BasedUtxoNetwork network, ) async { + final type = walletInfo.type; final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -38,7 +39,7 @@ class BitcoinWalletSnapshot extends ElectrumWalletSnapshot { final ElectrumWalletSnapshot electrumWalletSnapshot = await ElectrumWalletSnapshot.load( encryptionFileUtils, name, - type, + walletInfo, password, network, ); diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 2f01c605fd..d336967eeb 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -160,6 +160,7 @@ class ElectrumTransactionInfo extends TransactionInfo { List inputAddresses = []; List outputAddresses = []; + final sentAmounts = []; if (bundle.ins.length > 0) { for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) { final input = bundle.originalTransaction.inputs[i]; @@ -167,11 +168,13 @@ class ElectrumTransactionInfo extends TransactionInfo { final outTransaction = inputTransaction.outputs[input.txIndex]; inputAmount += outTransaction.amount.toInt(); if (addresses.contains( - BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network))) { + BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network), + )) { direction = TransactionDirection.outgoing; inputAddresses.add( BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network), ); + sentAmounts.add(outTransaction.amount.toInt()); } } } @@ -214,6 +217,8 @@ class ElectrumTransactionInfo extends TransactionInfo { // Self-send direction = TransactionDirection.incoming; amount = receivedAmounts.reduce((a, b) => a + b); + } else if (sentAmounts.length > 0) { + amount = sentAmounts.reduce((a, b) => a + b); } final fee = inputAmount - totalOutAmount; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index d395e6671f..10668e8ae6 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -186,8 +186,7 @@ abstract class ElectrumWalletBase onBalanceResponse(response.result); break; case ElectrumRequestMethods.getHistoryMethod: - final response = ElectrumWorkerGetHistoryResponse.fromJson(messageJson); - onHistoriesResponse(response.result); + onHistoriesResponse(ElectrumWorkerGetHistoryResponse.fromJson(messageJson)); break; case ElectrumRequestMethods.listunspentMethod: final response = ElectrumWorkerListUnspentResponse.fromJson(messageJson); @@ -1270,12 +1269,10 @@ abstract class ElectrumWalletBase await Future.forEach(walletAddresses.allAddresses, (BitcoinAddressRecord addressRecord) async { final isChange = addressRecord.isChange; - final matchingAddressList = - (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( - (element) => - element.type == addressRecord.type && - element.cwDerivationType == addressRecord.cwDerivationType, - ); + final matchingAddressList = walletAddresses + .getAddressesByType(addressRecord.type, isChange) + .where((element) => + (element as BitcoinAddressRecord).cwDerivationType == addressRecord.cwDerivationType); final totalMatchingAddresses = matchingAddressList.length; final matchingGapLimit = (isChange @@ -1305,11 +1302,11 @@ abstract class ElectrumWalletBase walletAddresses.updateAdresses(newAddresses); final newMatchingAddressList = - (isChange ? walletAddresses.changeAddresses : walletAddresses.receiveAddresses).where( - (element) => - element.type == addressRecord.type && - element.cwDerivationType == addressRecord.cwDerivationType, - ); + walletAddresses.getAddressesByType(addressRecord.type, isChange).where( + (element) => + element.type == addressRecord.type && + (element as BitcoinAddressRecord) == addressRecord.cwDerivationType, + ); printV( "discovered ${newAddresses.length} new ${isChange ? "change" : "receive"} addresses"); printV( @@ -1329,7 +1326,8 @@ abstract class ElectrumWalletBase } @action - Future onHistoriesResponse(List histories) async { + Future onHistoriesResponse(ElectrumWorkerGetHistoryResponse response) async { + final histories = response.result; if (histories.isNotEmpty) { final addressesWithHistory = []; @@ -1350,8 +1348,8 @@ abstract class ElectrumWalletBase } await save(); - } else { - // checkAddressesGap(); + } else if (response.completed) { + checkAddressesGap(); } } @@ -1582,7 +1580,7 @@ abstract class ElectrumWalletBase } // Identify all change outputs - final changeAddresses = walletAddresses.changeAddresses; + final changeAddresses = walletAddresses.allChangeAddresses; final List changeOutputs = outputs .where((output) => changeAddresses .any((element) => element.address == output.address.toAddress(network))) @@ -1643,6 +1641,12 @@ abstract class ElectrumWalletBase } } + Future getTransactionHex({required String hash}) async { + return await waitSendWorker( + ElectrumWorkerTxHexRequest(txHash: hash, currentChainTip: currentChainTip!), + ) as String; + } + Future getTransactionExpanded({required String hash}) async { return await waitSendWorker( ElectrumWorkerTxExpandedRequest(txHash: hash, currentChainTip: currentChainTip!), diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 842a74d88c..d9a2fbe6fe 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -28,12 +28,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { Map? initialChangeAddressIndex, BitcoinAddressType? initialAddressPageType, }) : _allAddresses = ObservableList.of(initialAddresses ?? []), - addressesOnReceiveScreen = - ObservableList.of(([]).toSet()), - receiveAddresses = ObservableList.of( - (initialAddresses ?? []).where((addressRecord) => !addressRecord.isChange).toSet()), - changeAddresses = ObservableList.of( - (initialAddresses ?? []).where((addressRecord) => addressRecord.isChange).toSet()), currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {}, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {}, _addressPageType = initialAddressPageType ?? @@ -46,10 +40,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static const defaultChangeAddressesCount = 17; static const gap = 20; + final walletAddressTypes = []; + final ObservableList _allAddresses; - final ObservableList addressesOnReceiveScreen; - final ObservableList receiveAddresses; - final ObservableList changeAddresses; + @observable + Map> receiveAddressesByType = {}; + @observable + Map> changeAddressesByType = {}; + final BasedUtxoNetwork network; final Map hdWallets; @@ -61,6 +59,24 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable late BitcoinAddressType _addressPageType; + @computed + List get allChangeAddresses => + _allAddresses.where((addr) => addr.isChange).toList(); + + @computed + List get selectedReceiveAddresses => + receiveAddressesByType[_addressPageType]!; + + @computed + List get selectedChangeAddresses => + receiveAddressesByType[_addressPageType]!; + + List getAddressesByType( + BitcoinAddressType type, [ + bool isChange = false, + ]) => + isChange ? changeAddressesByType[type]! : receiveAddressesByType[type]!; + @computed BitcoinAddressType get addressPageType => _addressPageType; @@ -75,6 +91,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return _allAddresses.firstWhere((element) => element.address == address); } + // TODO: toggle to switch @observable BitcoinAddressType changeAddressType = SegwitAddressType.p2wpkh; @@ -83,9 +100,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { String get address { String receiveAddress = ""; - final typeMatchingReceiveAddresses = addressesOnReceiveScreen.where((addr) => !addr.isUsed); + final typeMatchingReceiveAddresses = selectedReceiveAddresses.where( + (addressRecord) => !addressRecord.isUsed, + ); - if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || + if ((isEnabledAutoGenerateSubaddress && selectedReceiveAddresses.isEmpty) || typeMatchingReceiveAddresses.isEmpty) { receiveAddress = generateNewAddress().address; } else { @@ -94,7 +113,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (typeMatchingReceiveAddresses.isNotEmpty) { if (previousAddressMatchesType && - typeMatchingReceiveAddresses.first.address != addressesOnReceiveScreen.first.address) { + typeMatchingReceiveAddresses.first.address != selectedReceiveAddresses.first.address) { receiveAddress = previousAddressRecord!.address; } else { receiveAddress = typeMatchingReceiveAddresses.first.address; @@ -143,22 +162,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @observable BitcoinAddressRecord? previousAddressRecord; - @computed - int get totalCountOfReceiveAddresses => addressesOnReceiveScreen.fold(0, (acc, addressRecord) { - if (!addressRecord.isChange) { - return acc + 1; - } - return acc; - }); - - @computed - int get totalCountOfChangeAddresses => addressesOnReceiveScreen.fold(0, (acc, addressRecord) { - if (addressRecord.isChange) { - return acc + 1; - } - return acc; - }); - CWBitcoinDerivationType getHDWalletType() { if (hdWallets.containsKey(CWBitcoinDerivationType.bip39)) { return CWBitcoinDerivationType.bip39; @@ -171,25 +174,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { - updateAddressesOnReceiveScreen(); - updateReceiveAddresses(); - updateChangeAddresses(); + updateAddressesByType(); updateHiddenAddresses(); await updateAddressesInBox(); } @action Future getChangeAddress() async { - updateChangeAddresses(); - - final address = changeAddresses.firstWhere( - (addressRecord) => _isUnusedChangeAddressByType(addressRecord, changeAddressType), + final address = selectedChangeAddresses.firstWhere( + (addr) => _isUnusedChangeAddressByType(addr, changeAddressType), ); return address; } BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { - final newAddressIndex = addressesOnReceiveScreen.fold( + final newAddressIndex = selectedReceiveAddresses.fold( 0, (int acc, addressRecord) => addressRecord.isChange == false ? acc + 1 : acc, ); @@ -282,21 +281,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } @action - void updateAddressesOnReceiveScreen() { - addressesOnReceiveScreen.clear(); - addressesOnReceiveScreen.addAll(_allAddresses.where(_isAddressPageTypeMatch).toList()); - } - - @action - void updateReceiveAddresses() { - receiveAddresses.clear(); - receiveAddresses.addAll(_allAddresses.where((addressRecord) => !addressRecord.isChange)); - } - - @action - void updateChangeAddresses() { - changeAddresses.clear(); - changeAddresses.addAll(_allAddresses.where((addressRecord) => addressRecord.isChange)); + void updateAddressesByType() { + receiveAddressesByType.clear(); + walletAddressTypes.forEach((type) { + receiveAddressesByType[type] = + _allAddresses.where((addr) => _isAddressByType(addr, type)).toList(); + changeAddressesByType[type] = + _allAddresses.where((addr) => _isAddressByType(addr, type)).toList(); + }); } @action @@ -310,8 +302,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; - final startIndex = (isChange ? changeAddresses : receiveAddresses) - .where((addr) => addr.cwDerivationType == derivationType && addr.type == addressType) + final startIndex = (isChange ? selectedChangeAddresses : selectedReceiveAddresses) + .where((addr) => + (addr as BitcoinAddressRecord).cwDerivationType == derivationType && + addr.type == addressType) .length; final newAddresses = []; @@ -420,9 +414,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (replacedAddresses.isNotEmpty) { _allAddresses.addAll(replacedAddresses); } else { - updateAddressesOnReceiveScreen(); - updateReceiveAddresses(); - updateChangeAddresses(); + updateAddressesByType(); } } @@ -431,9 +423,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { this._allAddresses.addAll(addresses); updateHiddenAddresses(); - updateAddressesOnReceiveScreen(); - updateReceiveAddresses(); - updateChangeAddresses(); + updateAddressesByType(); } @action @@ -447,18 +437,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action Future setAddressType(BitcoinAddressType type) async { _addressPageType = type; - updateAddressesOnReceiveScreen(); + updateAddressesByType(); walletInfo.addressPageType = addressPageType.toString(); await walletInfo.save(); } - bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) { - return _isAddressByType(addressRecord, addressPageType); - } - bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; - bool _isUnusedChangeAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) { + bool _isUnusedChangeAddressByType(BaseBitcoinAddressRecord addr, BitcoinAddressType type) { return addr.isChange && !addr.isUsed && addr.type == type; } diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 98356277b8..401772a47d 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -48,10 +48,11 @@ class ElectrumWalletSnapshot { static Future load( EncryptionFileUtils encryptionFileUtils, String name, - WalletType type, + WalletInfo walletInfo, String password, BasedUtxoNetwork network, ) async { + final type = walletInfo.type; final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -80,8 +81,14 @@ class ElectrumWalletSnapshot { derivationType: derivationType, derivationPath: derivationPath, unspentCoins: (data['unspent_coins'] as List?) - ?.map((e) => BitcoinUnspent.fromJSON(null, e as Map)) - .toList() ?? [], + ?.map((e) => BitcoinUnspent.fromJSON( + null, + e as Map, + walletInfo.derivationInfo!, + network, + )) + .toList() ?? + [], didInitialSync: data['didInitialSync'] as bool?, walletAddressesSnapshot: walletAddressesSnapshot, ); diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index b106c430c0..d194366c1c 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -24,6 +24,7 @@ class ElectrumWorker { final SendPort sendPort; ElectrumProvider? _electrumClient; ServerCapability? _serverCapability; + String? get version => _serverCapability?.version; BehaviorSubject>? _scanningStream; BasedUtxoNetwork? _network; @@ -72,6 +73,11 @@ class ElectrumWorker { ElectrumWorkerTxExpandedRequest.fromJson(messageJson), ); break; + case ElectrumWorkerMethods.txHexMethod: + await _handleGetTxHex( + ElectrumWorkerTxHexRequest.fromJson(messageJson), + ); + break; case ElectrumRequestMethods.headersSubscribeMethod: await _handleHeadersSubscribe( ElectrumWorkerHeadersSubscribeRequest.fromJson(messageJson), @@ -257,6 +263,8 @@ class ElectrumWorker { completed: true, )); } + }, onError: () { + _serverCapability!.supportsBatching = false; }); })); } else { @@ -488,9 +496,7 @@ class ElectrumWorker { } if (txVerbose?.isEmpty ?? true) { - txHex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex(transactionHash: hash), - ); + txHex = await _getTransactionHex(hash: hash); } else { txHex = txVerbose!['hex'] as String; } @@ -624,11 +630,11 @@ class ElectrumWorker { // 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 + final canValidateDate = request.mempoolAPIEnabled || _serverCapability!.supportsTxVerbose; if (tx == null || tx.original == null || - // TODO: use mempool api or tx verbose - // (tx.isDateValidated != true && request.mempoolAPIEnabled)) { - (tx.isDateValidated != true)) { + (tx.isDateValidated != true && canValidateDate) || + tx.time == null) { transactionsByIds[txid] = TxToFetch(height: height, tx: tx); } } @@ -832,9 +838,7 @@ class ElectrumWorker { } if (txVerbose?.isEmpty ?? true) { - txHex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex(transactionHash: hash), - ); + txHex = await _getTransactionHex(hash: hash); } else { txHex = txVerbose!['hex'] as String; } @@ -869,10 +873,7 @@ class ElectrumWorker { final inputTransactionHexes = {}; await Future.wait(original.inputs.map((e) => e.txId).toList().map((inHash) async { - final hex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex(transactionHash: inHash), - ); - + final hex = await _getTransactionHex(hash: inHash); inputTransactionHexes[inHash] = hex; })); @@ -1058,6 +1059,11 @@ class ElectrumWorker { } } + Future _handleGetTxHex(ElectrumWorkerTxHexRequest request) async { + final hex = await _getTransactionHex(hash: request.txHash); + _sendResponse(ElectrumWorkerTxHexResponse(hex: hex, id: request.id)); + } + Future _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async { final tx = await _getTransactionExpanded( hash: request.txHash, @@ -1129,11 +1135,8 @@ class ElectrumWorker { } } else { await Future.wait(hashes.map((hash) async { - final history = await _electrumClient!.request( - ElectrumRequestGetTransactionHex(transactionHash: hash), - ); - - transactionHexes.add(history); + final hex = await _getTransactionHex(hash: hash); + transactionHexes.add(hex); })); } } @@ -1248,9 +1251,7 @@ class ElectrumWorker { time = transactionVerbose['time'] as int?; confirmations = transactionVerbose['confirmations'] as int?; } else { - transactionHex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex(transactionHash: hash), - ); + transactionHex = await _getTransactionHex(hash: hash); } if (getTime && _walletType == WalletType.bitcoin) { @@ -1271,11 +1272,7 @@ class ElectrumWorker { final ins = []; for (final vin in original.inputs) { - final inputTransactionHex = await _electrumClient!.request( - // TODO: _getTXHex - ElectrumRequestGetTransactionHex(transactionHash: vin.txId), - ); - + final inputTransactionHex = await _getTransactionHex(hash: vin.txId); ins.add(BtcTransaction.fromRaw(inputTransactionHex)); } @@ -1311,12 +1308,7 @@ class ElectrumWorker { } } else { await Future.wait(hashes.map((hash) async { - final hex = await _electrumClient!.request( - ElectrumRequestGetTransactionHex( - transactionHash: hash, - ), - ); - + final hex = await _getTransactionHex(hash: hash); inputTransactionHexById[hash] = hex; })); } @@ -1324,6 +1316,14 @@ class ElectrumWorker { return inputTransactionHexById; } + Future _getTransactionHex({required String hash}) async { + final hex = await _electrumClient!.request( + ElectrumRequestGetTransactionHex(transactionHash: hash), + ); + + return hex; + } + Future _handleGetFeeRates(ElectrumWorkerGetFeesRequest request) async { if (request.mempoolAPIEnabled && _walletType == WalletType.bitcoin) { try { @@ -1350,7 +1350,7 @@ class ElectrumWorker { // this guarantees that, even if all fees are low and equal, // higher priority fee txs can be consumed when chain fees start surging - _sendResponse( + return _sendResponse( ElectrumWorkerGetFeesResponse( result: BitcoinAPITransactionPriorities( minimum: minimum, @@ -1362,24 +1362,17 @@ class ElectrumWorker { ), ), ); - } catch (e) { - _sendResponse( - ElectrumWorkerGetFeesResponse( - result: ElectrumTransactionPriorities.fromList( - await _electrumClient!.getFeeRates(), - ), - ), - ); - } - } else { - _sendResponse( - ElectrumWorkerGetFeesResponse( - result: ElectrumTransactionPriorities.fromList( - await _electrumClient!.getFeeRates(), - ), - ), - ); + } catch (_) {} } + + // If the above didn't run or failed, fallback to Electrum fees anyway + _sendResponse( + ElectrumWorkerGetFeesResponse( + result: ElectrumTransactionPriorities.fromList( + await _electrumClient!.getFeeRates(), + ), + ), + ); } Future _handleCheckTweaks(ElectrumWorkerCheckTweaksRequest request) async { @@ -1440,11 +1433,11 @@ class ElectrumWorker { } // Initial status UI update, send how many blocks in total to scan - // TODO: isSingleScan : dont update restoreHeight _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( height: syncHeight, syncStatus: StartingScanSyncStatus(syncHeight), + wasSingleBlock: scanData.isSingleScan, ), )); @@ -1493,7 +1486,11 @@ class ElectrumWorker { ? SyncingSyncStatus(1, 0) : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); _sendResponse(ElectrumWorkerTweaksSubscribeResponse( - result: TweaksSyncResponse(height: syncHeight, syncStatus: syncingStatus), + result: TweaksSyncResponse( + height: syncHeight, + syncStatus: syncingStatus, + wasSingleBlock: scanData.isSingleScan, + ), )); final tweakHeight = response.block; @@ -1509,20 +1506,21 @@ class ElectrumWorker { try { final addToWallet = {}; - receivers.forEach((receiver) { - // scanOutputs called from rust here - final scanResult = scanOutputs(outputPubkeys.keys.toList(), tweak, receiver); + // receivers.forEach((receiver) { + // scanOutputs called from rust here + final receiver = receivers.first; + final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); - if (scanResult.isEmpty) { - return; - } + if (scanResult.isEmpty) { + continue; + } - if (addToWallet[receiver.BSpend] == null) { - addToWallet[receiver.BSpend] = scanResult; - } else { - addToWallet[receiver.BSpend].addAll(scanResult); - } - }); + if (addToWallet[receiver.BSpend] == null) { + addToWallet[receiver.BSpend] = scanResult; + } else { + addToWallet[receiver.BSpend].addAll(scanResult); + } + // }); if (addToWallet.isEmpty) { // no results tx, continue to next tx @@ -1588,6 +1586,7 @@ class ElectrumWorker { _sendResponse(ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( transactions: {txInfo.id: TweakResponseData(txInfo: txInfo, unspents: unspents)}, + wasSingleBlock: scanData.isSingleScan, ), )); @@ -1604,7 +1603,7 @@ class ElectrumWorker { syncHeight = tweakHeight; - if (tweakHeight >= scanData.chainTip || scanData.isSingleScan) { + if ((tweakHeight >= scanData.chainTip) || scanData.isSingleScan) { _sendResponse( ElectrumWorkerTweaksSubscribeResponse( result: TweaksSyncResponse( @@ -1612,6 +1611,7 @@ class ElectrumWorker { syncStatus: scanData.isSingleScan ? SyncedSyncStatus() : SyncedTipSyncStatus(scanData.chainTip), + wasSingleBlock: scanData.isSingleScan, ), ), ); @@ -1627,15 +1627,7 @@ class ElectrumWorker { Future _handleGetVersion(ElectrumWorkerGetVersionRequest request) async { _sendResponse( - ElectrumWorkerGetVersionResponse( - result: await _electrumClient!.request( - ElectrumRequestVersion( - clientName: "", - protocolVersion: "1.4", - ), - ), - id: request.id, - ), + ElectrumWorkerGetVersionResponse(result: version!, id: request.id), ); } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart index 4d9c85a47b..47c66c46eb 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker_methods.dart @@ -5,12 +5,14 @@ class ElectrumWorkerMethods { static const String connectionMethod = "connection"; static const String unknownMethod = "unknown"; static const String txHashMethod = "txHash"; + static const String txHexMethod = "txHex"; 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 txHex = ElectrumWorkerMethods._(txHexMethod); static const ElectrumWorkerMethods checkTweaks = ElectrumWorkerMethods._(checkTweaksMethod); static const ElectrumWorkerMethods stopScanning = ElectrumWorkerMethods._(stopScanningMethod); diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_tx_hex.dart b/cw_bitcoin/lib/electrum_worker/methods/get_tx_hex.dart new file mode 100644 index 0000000000..ef31a7b4f6 --- /dev/null +++ b/cw_bitcoin/lib/electrum_worker/methods/get_tx_hex.dart @@ -0,0 +1,77 @@ +part of 'methods.dart'; + +class ElectrumWorkerTxHexRequest implements ElectrumWorkerRequest { + ElectrumWorkerTxHexRequest({ + required this.txHash, + required this.currentChainTip, + this.mempoolAPIEnabled = false, + this.id, + this.completed = false, + }); + + final String txHash; + final int currentChainTip; + final bool mempoolAPIEnabled; + final int? id; + final bool completed; + + @override + final String method = ElectrumWorkerMethods.txHex.method; + + @override + factory ElectrumWorkerTxHexRequest.fromJson(Map json) { + return ElectrumWorkerTxHexRequest( + txHash: json['txHash'] as String, + currentChainTip: json['currentChainTip'] as int, + mempoolAPIEnabled: json['mempoolAPIEnabled'] as bool, + id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, + ); + } + + @override + Map toJson() { + return { + 'method': method, + 'id': id, + 'completed': completed, + 'txHash': txHash, + 'currentChainTip': currentChainTip, + 'mempoolAPIEnabled': mempoolAPIEnabled, + }; + } +} + +class ElectrumWorkerTxHexError extends ElectrumWorkerErrorResponse { + ElectrumWorkerTxHexError({ + required String error, + super.id, + }) : super(error: error); + + @override + String get method => ElectrumWorkerMethods.txHex.method; +} + +class ElectrumWorkerTxHexResponse extends ElectrumWorkerResponse { + ElectrumWorkerTxHexResponse({ + required String hex, + super.error, + super.id, + super.completed, + }) : super(result: hex, method: ElectrumWorkerMethods.txHex.method); + + @override + String resultJson(result) { + return result; + } + + @override + factory ElectrumWorkerTxHexResponse.fromJson(Map json) { + return ElectrumWorkerTxHexResponse( + hex: json['result'] as String, + error: json['error'] as String?, + id: json['id'] as int?, + completed: json['completed'] as bool? ?? false, + ); + } +} diff --git a/cw_bitcoin/lib/electrum_worker/methods/methods.dart b/cw_bitcoin/lib/electrum_worker/methods/methods.dart index c117c45d39..2e53556ef3 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/methods.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/methods.dart @@ -16,6 +16,7 @@ part 'scripthashes_subscribe.dart'; part 'get_balance.dart'; part 'get_history.dart'; part 'get_tx_expanded.dart'; +part 'get_tx_hex.dart'; part 'broadcast.dart'; part 'list_unspent.dart'; part 'tweaks_subscribe.dart'; diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart index 3eeb015e72..03294a2818 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -143,14 +143,21 @@ class TweaksSyncResponse { int? height; SyncStatus? syncStatus; Map? transactions = {}; + final bool wasSingleBlock; - TweaksSyncResponse({this.height, this.syncStatus, this.transactions}); + TweaksSyncResponse({ + required this.wasSingleBlock, + 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())), + 'wasSingleBlock': wasSingleBlock, }; } @@ -168,6 +175,7 @@ class TweaksSyncResponse { TweakResponseData.fromJson(value as Map), ), ), + wasSingleBlock: json['wasSingleBlock'] as bool? ?? false, ); } } diff --git a/cw_bitcoin/lib/electrum_worker/methods/version.dart b/cw_bitcoin/lib/electrum_worker/methods/version.dart index 2c20aab36d..b6c3cc9a80 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/version.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/version.dart @@ -40,7 +40,7 @@ class ElectrumWorkerGetVersionError extends ElectrumWorkerErrorResponse { String get method => ElectrumRequestMethods.version.method; } -class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse, List> { +class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse { ElectrumWorkerGetVersionResponse({ required super.result, super.error, @@ -49,14 +49,14 @@ class ElectrumWorkerGetVersionResponse extends ElectrumWorkerResponse resultJson(result) { - return result; + String resultJson(result) { + return result.toString(); } @override factory ElectrumWorkerGetVersionResponse.fromJson(Map json) { return ElectrumWorkerGetVersionResponse( - result: json['result'] as List, + result: json['result'] as String, error: json['error'] as String?, id: json['id'] as int?, completed: json['completed'] as bool? ?? false, diff --git a/cw_bitcoin/lib/electrum_worker/server_capability.dart b/cw_bitcoin/lib/electrum_worker/server_capability.dart index 038c85755d..ac395a2bf5 100644 --- a/cw_bitcoin/lib/electrum_worker/server_capability.dart +++ b/cw_bitcoin/lib/electrum_worker/server_capability.dart @@ -7,15 +7,24 @@ class ServerCapability { bool supportsBatching; bool supportsTxVerbose; + String version; - ServerCapability({required this.supportsBatching, required this.supportsTxVerbose}); + ServerCapability({ + required this.supportsBatching, + required this.supportsTxVerbose, + required this.version, + }); static ServerCapability fromVersion(List serverVersion) { if (serverVersion.isNotEmpty) { final server = serverVersion.first.toLowerCase(); if (server.contains('electrumx')) { - return ServerCapability(supportsBatching: true, supportsTxVerbose: true); + return ServerCapability( + supportsBatching: true, + supportsTxVerbose: true, + version: server, + ); } if (server.startsWith('electrs/')) { @@ -28,13 +37,21 @@ class ServerCapability { try { final version = ElectrumVersion.fromStr(electrsVersion); if (version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) { - return ServerCapability(supportsBatching: true, supportsTxVerbose: false); + return ServerCapability( + supportsBatching: true, + supportsTxVerbose: false, + version: server, + ); } } catch (e) { // ignore version parsing errors } - return ServerCapability(supportsBatching: false, supportsTxVerbose: false); + return ServerCapability( + supportsBatching: false, + supportsTxVerbose: false, + version: server, + ); } if (server.startsWith('fulcrum')) { @@ -43,7 +60,11 @@ class ServerCapability { try { final version = ElectrumVersion.fromStr(fulcrumVersion); if (version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) { - return ServerCapability(supportsBatching: true, supportsTxVerbose: true); + return ServerCapability( + supportsBatching: true, + supportsTxVerbose: true, + version: server, + ); } } catch (e) {} } @@ -59,7 +80,11 @@ class ServerCapability { try { final version = ElectrumVersion.fromStr(mempoolElectrsVersion); if (version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0) { - return ServerCapability(supportsBatching: true, supportsTxVerbose: false); + return ServerCapability( + supportsBatching: true, + supportsTxVerbose: false, + version: server, + ); } } catch (e) { // ignore version parsing errors @@ -67,6 +92,10 @@ class ServerCapability { } } - return ServerCapability(supportsBatching: false, supportsTxVerbose: false); + return ServerCapability( + supportsBatching: false, + supportsTxVerbose: false, + version: "unknown", + ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index d9714147a1..8d1dfe926f 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -72,13 +72,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mwebEnabled = alwaysScan ?? false; if (walletAddressesSnapshot != null) { - // walletAddresses = LitecoinWalletAddresses.fromJson( - // walletAddressesSnapshot, - // walletInfo, - // network: network, - // isHardwareWallet: isHardwareWallet, - // hdWallets: hdWallets, - // ); + walletAddresses = LitecoinWalletAddressesBase.fromJson( + walletAddressesSnapshot, + walletInfo, + network: network, + isHardwareWallet: isHardwareWallet, + hdWallets: hdWallets, + ); } else { walletAddresses = LitecoinWalletAddresses( walletInfo, @@ -186,7 +186,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { snp = await LitecoinWalletSnapshot.load( encryptionFileUtils, name, - walletInfo.type, + walletInfo, password, LitecoinNetwork.mainnet, ); @@ -347,6 +347,19 @@ 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) { + for (var coin in unspentCoins) { + if (tx.inputAddresses?.contains(coin.address) ?? false) { + final utxo = mwebUtxosBox.get(coin.address); + if (utxo != null) { + printV("deleting utxo ${coin.address} @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + await mwebUtxosBox.delete(coin.address); + } + } + } + } + tx.confirmations = confirmations; tx.isPending = false; transactionHistory.addOne(tx); @@ -786,84 +799,85 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { } // @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; - // 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, - // ); - // } + Future updateBalance([Set? scripthashes, bool? wait]) async { + await super.updateBalance(scripthashes, true); + final balance = this.balance[currency]!; + + if (!mwebEnabled) { + return; + } + + // 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; + 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++; + } + } + + this.balance[currency] = ElectrumBalance( + confirmed: confirmed, + unconfirmed: unconfirmed, + frozen: balance.frozen, + secondConfirmed: confirmedMweb, + secondUnconfirmed: unconfirmedMweb, + ); + } @override ElectrumTxCreateUtxoDetails createUTXOS({ diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 02dce4d5db..1faa8eeb5a 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -72,8 +72,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with ? ElectrumWalletAddressesBase.defaultChangeAddressesCount : ElectrumWalletAddressesBase.defaultReceiveAddressesCount; - final startIndex = (isChange ? changeAddresses : receiveAddresses) - .where((addr) => addr.cwDerivationType == derivationType && addr.type == addressType) + final startIndex = getAddressesByType(addressType, isChange) + .where((addr) => (addr as BitcoinAddressRecord).cwDerivationType == derivationType) .length; final mwebAddresses = []; @@ -268,7 +268,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with @override String get addressForExchange { // don't use mweb addresses for exchange refund address: - final addresses = receiveAddresses + final addresses = selectedReceiveAddresses .where((element) => element.type == SegwitAddressType.p2wpkh && !element.isUsed); return addresses.first.address; } @@ -329,6 +329,64 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with addressesSet.addAll(addresses); this.mwebAddresses.clear(); this.mwebAddresses.addAll(addressesSet); - updateAddressesOnReceiveScreen(); + updateAddressesByType(); + } + + Map toJson() { + final json = super.toJson(); + json['mwebAddresses'] = mwebAddresses.map((address) => address.toJSON()).toList(); + // json['mwebAddressIndex'] = + return json; + } + + static Map fromSnapshot(Map data) { + final electrumSnapshot = ElectrumWalletAddressesBase.fromSnapshot(data); + + final mwebAddresses = data['mweb_addresses'] as List? ?? + [].map((e) => LitecoinMWEBAddressRecord.fromJSON(e as String)).toList(); + + // var mwebAddressIndex = 0; + + // try { + // mwebAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); + // } catch (_) {} + + return { + 'allAddresses': electrumSnapshot["addresses"], + 'addressPageType': data['address_page_type'] as String?, + 'receiveAddressIndexByType': electrumSnapshot["receiveAddressIndexByType"], + 'changeAddressIndexByType': electrumSnapshot["changeAddressIndexByType"], + 'mwebAddresses': mwebAddresses, + }; + } + + static LitecoinWalletAddressesBase fromJson( + Map json, + WalletInfo walletInfo, { + required Map hdWallets, + required BasedUtxoNetwork network, + required bool isHardwareWallet, + List? initialAddresses, + List? initialMwebAddresses, + }) { + initialAddresses ??= (json['allAddresses'] as List) + .map((record) => BitcoinAddressRecord.fromJSON(record as String)) + .toList(); + + initialMwebAddresses ??= (json['mwebAddresses'] as List) + .map( + (address) => LitecoinMWEBAddressRecord.fromJSON(address as String), + ) + .toList(); + + return LitecoinWalletAddresses( + walletInfo, + hdWallets: hdWallets, + network: network, + isHardwareWallet: isHardwareWallet, + initialAddresses: initialAddresses, + initialMwebAddresses: initialMwebAddresses, + mwebEnabled: true, // TODO + ); } } diff --git a/cw_bitcoin/lib/litecoin_wallet_snapshot.dart b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart index c6e31624d2..b051e6b4bc 100644 --- a/cw_bitcoin/lib/litecoin_wallet_snapshot.dart +++ b/cw_bitcoin/lib/litecoin_wallet_snapshot.dart @@ -4,7 +4,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_wallet_snapshot.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/wallet_info.dart'; class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { LitecoinWalletSnapshot({ @@ -29,10 +29,11 @@ class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { static Future load( EncryptionFileUtils encryptionFileUtils, String name, - WalletType type, + WalletInfo walletInfo, String password, BasedUtxoNetwork network, ) async { + final type = walletInfo.type; final path = await pathForWallet(name: name, type: type); final jsonSource = await encryptionFileUtils.read(path: path, password: password); final data = json.decode(jsonSource) as Map; @@ -40,7 +41,7 @@ class LitecoinWalletSnapshot extends ElectrumWalletSnapshot { final ElectrumWalletSnapshot electrumWalletSnapshot = await ElectrumWalletSnapshot.load( encryptionFileUtils, name, - type, + walletInfo, password, network, ); diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 7402beac92..91ac7a90ea 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -121,7 +121,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { snp = await ElectrumWalletSnapshot.load( encryptionFileUtils, name, - walletInfo.type, + walletInfo, password, BitcoinCashNetwork.mainnet, ); 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 d6bf07588c..f534128bf4 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -23,6 +23,9 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi @override Future init() async { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); + allAddresses.forEach((addr) { + print(addr.address); + }); await super.init(); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 40dc828efe..46b8d0d6f1 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -153,7 +153,10 @@ class CWBitcoin extends Bitcoin { @computed List getSubAddresses(Object wallet) { final electrumWallet = wallet as ElectrumWallet; - return electrumWallet.walletAddresses.addressesOnReceiveScreen + return [ + ...electrumWallet.walletAddresses.selectedReceiveAddresses, + ...electrumWallet.walletAddresses.selectedChangeAddresses + ] .map( (addr) => ElectrumSubAddress( id: addr.index, diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 4035dbfdd4..671d8c699a 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -9,7 +9,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -List priorityForWalletType(WalletBase wallet) { +List priorityForWallet(WalletBase wallet) { switch (wallet.type) { case WalletType.monero: return monero!.getTransactionPriorities(); diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index bae9a972a3..7e3c2b5553 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -1,13 +1,27 @@ +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'; @@ -102,13 +116,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/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index f6b189ac87..af13a25a42 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -529,7 +529,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin pickTransactionPriority(BuildContext context) async { - final items = priorityForWalletType(sendViewModel.wallet); + final items = priorityForWallet(sendViewModel.wallet); final selectedItem = items.indexOf(sendViewModel.transactionPriority); final customItemIndex = sendViewModel.getCustomPriorityIndex(items); final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin; diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index c9d32d19af..50d294d5a4 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -37,7 +37,7 @@ class OtherSettingsPage extends BasePage { _otherSettingsViewModel.walletType == WalletType.bitcoin ? SettingsPriorityPickerCell( title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.sendViewModel.wallet), + items: priorityForWallet(_otherSettingsViewModel.sendViewModel.wallet), displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority, selectedItem: _otherSettingsViewModel.transactionPriority, customItemIndex: _otherSettingsViewModel.customPriorityItemIndex, @@ -47,7 +47,7 @@ class OtherSettingsPage extends BasePage { ) : SettingsPickerCell( title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.sendViewModel.wallet), + items: priorityForWallet(_otherSettingsViewModel.sendViewModel.wallet), displayItem: _otherSettingsViewModel.getDisplayPriority, selectedItem: _otherSettingsViewModel.transactionPriority, onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 653a9c6158..59c995bade 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -92,7 +92,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor setTransactionPriority(bitcoinTransactionPriorityMedium); } final priority = _settingsStore.priority[wallet.type]; - final priorities = priorityForWalletType(wallet); + final priorities = priorityForWallet(wallet); if (priorities.isNotEmpty && !priorities.contains(priority)) { _settingsStore.priority[wallet.type] = priorities.first; } diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 7437ea71ef..949abb8263 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -24,7 +24,7 @@ abstract class OtherSettingsViewModelBase with Store { .then((PackageInfo packageInfo) => currentVersion = packageInfo.version); final priority = _settingsStore.priority[_wallet.type]; - final priorities = priorityForWalletType(_wallet); + final priorities = priorityForWallet(_wallet); if (!priorities.contains(priority) && priorities.isNotEmpty) { _settingsStore.priority[_wallet.type] = priorities.first; @@ -103,7 +103,7 @@ abstract class OtherSettingsViewModelBase with Store { double get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate.toDouble(); int? get customPriorityItemIndex { - final priorities = priorityForWalletType(_wallet); + final priorities = priorityForWallet(_wallet); final customItem = priorities .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); return customItem != null ? priorities.indexOf(customItem) : null; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 1680e4d7a9..4fa5b4742e 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -566,7 +566,7 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte')); } - final priorities = priorityForWalletType(wallet); + final priorities = priorityForWallet(wallet); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); final customItem = priorities.firstWhereOrNull( (element) => element.title == sendViewModel.bitcoinTransactionPriorityCustom.title); From be1b964967cf0a0e8a4711e5bacacd1cd6ea8846 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 24 Jan 2025 17:37:57 -0300 Subject: [PATCH 60/64] fix: address list --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 5 +++++ cw_bitcoin/lib/electrum_wallet.dart | 2 +- cw_bitcoin/lib/electrum_wallet_addresses.dart | 7 +++---- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 5 +++++ cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart | 5 +++++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 52696a14af..5f0fd99dee 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -31,6 +31,9 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S silentPaymentWallets = [silentPaymentWallet!]; } + @override + final walletAddressTypes = BITCOIN_ADDRESS_TYPES; + static const OLD_SP_PATH = "m/352'/1'/0'/#'/0"; static const BITCOIN_ADDRESS_TYPES = [ SegwitAddressType.p2wpkh, @@ -53,6 +56,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override Future init() async { + super.init(); + // TODO: if restored from snapshot if (allAddresses.where((address) => address.type == SegwitAddressType.p2wpkh).isEmpty) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 10668e8ae6..f92f6d0b13 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1349,7 +1349,7 @@ abstract class ElectrumWalletBase await save(); } else if (response.completed) { - checkAddressesGap(); + // checkAddressesGap(); } } diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index d9a2fbe6fe..cfd9fd82b0 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -65,17 +65,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @computed List get selectedReceiveAddresses => - receiveAddressesByType[_addressPageType]!; + receiveAddressesByType[_addressPageType] ?? []; @computed List get selectedChangeAddresses => - receiveAddressesByType[_addressPageType]!; + changeAddressesByType[_addressPageType] ?? []; List getAddressesByType( BitcoinAddressType type, [ bool isChange = false, ]) => - isChange ? changeAddressesByType[type]! : receiveAddressesByType[type]!; + (isChange ? changeAddressesByType[type] : receiveAddressesByType[type]) ?? []; @computed BitcoinAddressType get addressPageType => _addressPageType; @@ -282,7 +282,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action void updateAddressesByType() { - receiveAddressesByType.clear(); walletAddressTypes.forEach((type) { receiveAddressesByType[type] = _allAddresses.where((addr) => _isAddressByType(addr, type)).toList(); diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index 1faa8eeb5a..7daab74d67 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -37,6 +37,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with printV("initialized with ${mwebAddrs.length} mweb addresses"); } + @override + final walletAddressTypes = LITECOIN_ADDRESS_TYPES; + + static const LITECOIN_ADDRESS_TYPES = [SegwitAddressType.p2wpkh]; + final ObservableList mwebAddresses; late final Bip32Slip10Secp256k1? mwebHd; 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 f534128bf4..5f22d408b9 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -20,6 +20,11 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi super.initialAddressPageType, }) : super(walletInfo); + @override + final walletAddressTypes = BITCOIN_CASH_ADDRESS_TYPES; + + static const BITCOIN_CASH_ADDRESS_TYPES = [P2pkhAddressType.p2pkh]; + @override Future init() async { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); From e7cae899b35a5ac7e239effba157c0f92d506cb3 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 27 Jan 2025 10:43:15 -0300 Subject: [PATCH 61/64] chore: merge --- cw_bitcoin/lib/litecoin_wallet.dart | 5 +---- cw_bitcoin/lib/litecoin_wallet_addresses.dart | 9 ++------- cw_zano/lib/zano_wallet.dart | 4 ++-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index dcb2177366..14fc7d141b 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1090,7 +1090,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { final changeAddress = await (walletAddresses as LitecoinWalletAddresses).getChangeAddress( inputs: utxoDetails.availableInputs, - outputs: updatedOutputs, + outputs: outputs, coinTypeToSpendFrom: coinTypeToSpendFrom, ); final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); @@ -1200,11 +1200,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return await super.calcFee( utxos: utxos, outputs: outputs, - network: network, memo: memo, feeRate: feeRate, - inputPrivKeyInfos: inputPrivKeyInfos, - vinOutpoints: vinOutpoints, ); } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index a6c79c1745..051ed9cf59 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -263,13 +263,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with if (mwebEnabled) { await ensureMwebAddressUpToIndexExists(1); - updateChangeAddresses(); - return BitcoinAddressRecord( - mwebAddrs[0], - index: 0, - type: SegwitAddresType.mweb, - network: network, - ); + updateAddressesByType(); + return LitecoinMWEBAddressRecord(mwebAddrs[0], index: 0, network: network); } return super.getChangeAddress(); diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index bc9eb6188c..31f4857480 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -117,7 +117,7 @@ abstract class ZanoWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, [int? amount = null]) => + Future calculateEstimatedFee(TransactionPriority priority) async => getCurrentTxFee(priority); @override @@ -205,7 +205,7 @@ abstract class ZanoWalletBase final hasMultiDestination = outputs.length > 1; final unlockedBalanceZano = balance[CryptoCurrency.zano]?.unlocked ?? BigInt.zero; final unlockedBalanceCurrency = balance[credentials.currency]?.unlocked ?? BigInt.zero; - final fee = BigInt.from(calculateEstimatedFee(credentials.priority)); + final fee = BigInt.from(await calculateEstimatedFee(credentials.priority)); late BigInt totalAmount; void checkForEnoughBalances() { if (isZano) { From 5ae8b448cb5ae91ef4d729111e3cc1a31ddf4593 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 27 Jan 2025 10:23:57 -0300 Subject: [PATCH 62/64] chore: prints --- cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart | 3 --- 1 file changed, 3 deletions(-) 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 5f22d408b9..d7ec18bd18 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -28,9 +28,6 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi @override Future init() async { await generateInitialAddresses(type: P2pkhAddressType.p2pkh); - allAddresses.forEach((addr) { - print(addr.address); - }); await super.init(); } From dacbe83e75e0aafd0fe44316e3efb7f5e0272408 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 27 Jan 2025 20:08:06 -0300 Subject: [PATCH 63/64] fix: scan & address book --- cw_bitcoin/lib/bitcoin_address_record.dart | 10 +- cw_bitcoin/lib/bitcoin_wallet.dart | 31 +++-- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 130 ++++++++++-------- cw_bitcoin/lib/electrum_wallet.dart | 28 ++-- cw_bitcoin/lib/electrum_wallet_addresses.dart | 24 ++-- .../lib/electrum_worker/electrum_worker.dart | 62 +++++---- .../methods/tweaks_subscribe.dart | 4 +- cw_bitcoin/lib/litecoin_wallet.dart | 4 +- lib/bitcoin/cw_bitcoin.dart | 2 +- 9 files changed, 168 insertions(+), 127 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index 855b33c8f8..fde0e739a7 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -263,6 +263,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { final String tweak; + final String spAddress; BitcoinReceivedSPAddressRecord( super.address, { @@ -272,6 +273,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { super.name = '', super.isUsed = false, required this.tweak, + required this.spAddress, required super.isChange, super.labelHex, }) : super(isHidden: true, type: SegwitAddressType.p2tr); @@ -280,12 +282,10 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { List silentPaymentsWallets, [ BasedUtxoNetwork network = BitcoinNetwork.mainnet, ]) { - final spAddress = silentPaymentsWallets.firstWhere( - (wallet) => wallet.toAddress(network) == this.address, + return silentPaymentsWallets.firstWhere( + (wallet) => wallet.toAddress(network) == spAddress, orElse: () => throw ArgumentError('SP wallet not found'), ); - - return spAddress; } ECPrivate getSpendKey( @@ -310,6 +310,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { labelHex: decoded['label'] as String?, tweak: decoded['tweak'] as String? ?? decoded['silent_payment_tweak'] as String? ?? '', isChange: decoded['isChange'] as bool? ?? false, + spAddress: decoded['spAddress'] as String? ?? '', ); } @@ -317,6 +318,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord { String toJSON() { final m = json.decode(super.toJSON()) as Map; m['tweak'] = tweak; + m['spAddress'] = spAddress; return json.encode(m); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 254ce9db27..097d39cbd7 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -15,7 +15,6 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; -import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -97,12 +96,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { required EncryptionFileUtils encryptionFileUtils, String? passphrase, BasedUtxoNetwork? network, - List? initialAddresses, - List? initialSilentAddresses, - ElectrumBalance? initialBalance, - Map? initialRegularAddressIndex, - Map? initialChangeAddressIndex, - int initialSilentAddressIndex = 0, }) async { final hdWallets = await ElectrumWalletBase.getAccountHDWallets( walletInfo: walletInfo, @@ -117,7 +110,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialBalance: initialBalance, encryptionFileUtils: encryptionFileUtils, networkParam: network, hdWallets: hdWallets, @@ -401,12 +393,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { @override @action - Future updateAllUnspents() async { - List updatedUnspentCoins = []; - - unspentCoins.addAll(updatedUnspentCoins); - - await super.updateAllUnspents(); + Future updateAllUnspents([Set? scripthashes, bool? wait]) async { + await super.updateAllUnspents(scripthashes, wait); final walletAddresses = this.walletAddresses as BitcoinWalletAddresses; @@ -1341,6 +1329,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { throw e; } } + + @override + @action + Future onUnspentResponse(Map> unspents) async { + final silentPaymentUnspents = unspentCoins + .where((utxo) => + utxo.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord || + utxo.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) + .toList(); + + unspentCoins.clear(); + unspentCoins.addAll(silentPaymentUnspents); + + super.onUnspentResponse(unspents); + } } class BitcoinEstimatedTx extends ElectrumEstimatedTx { diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 5f0fd99dee..03b9c4dc04 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -249,27 +249,31 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S }) { 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 SegwitAddressType.p2tr: - // return ECPublic.fromBip32(pub).toP2trAddress(); - // case SegwitAddressType.p2wsh: - // return ECPublic.fromBip32(pub).toP2wshAddress(); - // case P2shAddressType.p2wpkhInP2sh: - // return ECPublic.fromBip32(pub).toP2wpkhInP2sh(); - // case SegwitAddressType.p2wpkh: - // return ECPublic.fromBip32(pub).toP2wpkhAddress(); - // default: - // throw ArgumentError('Invalid address type'); - // } - // } + if (OLD_DERIVATION_TYPES.contains(derivationType)) { + final isElectrum = [CWBitcoinDerivationType.electrum, CWBitcoinDerivationType.old_electrum] + .contains(derivationType); + + final oldPath = (isElectrum ? BitcoinDerivationInfos.ELECTRUM : BitcoinDerivationInfos.BIP84) + .derivationPath + .addElem(Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(isChange))) + .addElem(Bip32KeyIndex(index)); + final oldPub = hdWallet.derive(oldPath).publicKey; + + switch (addressType) { + case P2pkhAddressType.p2pkh: + return ECPublic.fromBip32(oldPub).toP2pkhAddress(); + case SegwitAddressType.p2tr: + return ECPublic.fromBip32(oldPub).toP2trAddress(); + case SegwitAddressType.p2wsh: + return ECPublic.fromBip32(oldPub).toP2wshAddress(); + case P2shAddressType.p2wpkhInP2sh: + return ECPublic.fromBip32(oldPub).toP2wpkhInP2sh(); + case SegwitAddressType.p2wpkh: + return ECPublic.fromBip32(oldPub).toP2wpkhAddress(); + default: + throw ArgumentError('Invalid address type'); + } + } switch (addressType) { case P2pkhAddressType.p2pkh: @@ -343,25 +347,46 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S return super.generateNewAddress(label: label); } - // @override - // @action - // void addBitcoinAddressTypes() { - // super.addBitcoinAddressTypes(); - - // silentPaymentAddresses.forEach((addressRecord) { - // if (addressRecord.type != SilentPaymentsAddresType.p2sp || addressRecord.isChange) { - // return; - // } - - // 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 + Future updateAddressesInBox() async { + super.updateAddressesInBox(); + + receiveAddressesByType.entries.forEach((e) { + final type = e.key; + final values = e.value; + bool isFirstUnused = true; + + values.forEach((addr) { + allAddressesMap[addr.address] = addr.name; + + if (!addr.isHidden && !addr.isChange && isFirstUnused && !addr.isUsed) { + if (type == SilentPaymentsAddresType.p2sp) { + final addressString = + '${addr.address.substring(0, 9 + 5)}...${addr.address.substring(addr.address.length - 9, addr.address.length)}'; + + if (addr.address != address) { + addressesMap[addr.address] = addr.name.isEmpty + ? "Silent Payments" + ': $addressString' + : "Silent Payments - " + addr.name + ': $addressString'; + } else { + addressesMap[address] = 'Active - Silent Payments' + ': $addressString'; + } + } else { + isFirstUnused = false; + + if (addr.address != address) { + addressesMap[addr.address] = type.value.toUpperCase(); + } else { + addressesMap[address] = 'Active - ${type.value.toUpperCase()}'; + } + } + } + }); + }); + + await saveAddressesInBox(); + } @override @action @@ -388,14 +413,10 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @override @action void updateAddressesByType() { - if (addressPageType == SilentPaymentsAddresType.p2sp) { - receiveAddressesByType.clear(); - receiveAddressesByType[SilentPaymentsAddresType.p2sp] = silentPaymentAddresses - .where((addressRecord) => - addressRecord.type == SilentPaymentsAddresType.p2sp && !addressRecord.isChange) - .toList(); - return; - } + receiveAddressesByType[SilentPaymentsAddresType.p2sp] = silentPaymentAddresses + .where((addressRecord) => + addressRecord.type == SilentPaymentsAddresType.p2sp && !addressRecord.isChange) + .toList(); super.updateAddressesByType(); } @@ -427,18 +448,15 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S updateAddressesByType(); } - Map get labels { - final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); - final labels = {}; + Map get labels { + 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; + if (silentPaymentTweak != null) { + labels[silentPaymentTweak] = silentAddressRecord.labelIndex; } } return labels; diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index f92f6d0b13..40fd992e9d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -347,6 +347,8 @@ abstract class ElectrumWalletBase // INFO: THIRD: Get the full wallet's balance with all addresses considered await updateBalance(scripthashesWithStatus.toSet(), true); + await updateAllUnspents(scripthashesWithStatus.toSet(), true); + syncStatus = SyncedSyncStatus(); // INFO: FOURTH: Get the latest recommended fee rates and start update timer @@ -1100,12 +1102,20 @@ abstract class ElectrumWalletBase } @action - Future updateAllUnspents() async { - workerSendPort!.send( - ElectrumWorkerListUnspentRequest( - scripthashes: walletAddresses.allScriptHashes.toList(), - ).toJson(), - ); + Future updateAllUnspents([Set? scripthashes, bool? wait]) async { + scripthashes ??= walletAddresses.allScriptHashes; + + if (wait == true) { + await waitSendWorker(ElectrumWorkerListUnspentRequest( + scripthashes: scripthashes.toList(), + )); + } else { + workerSendPort!.send( + ElectrumWorkerListUnspentRequest( + scripthashes: scripthashes.toList(), + ).toJson(), + ); + } } @action @@ -1316,9 +1326,9 @@ abstract class ElectrumWalletBase // if the initial sync has been done, fetch histories for the new discovered addresses // if not, will update all again during startSync, with the new ones as well, to update dates - if (newAddresses.isNotEmpty && !_didInitialSync) { - await updateTransactions(newAddresses, true); - } + // if (newAddresses.isNotEmpty && !_didInitialSync) { + // await updateTransactions(newAddresses, true); + // } walletAddresses.updateHiddenAddresses(); diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index cfd9fd82b0..aef6eed8c4 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -63,13 +63,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List get allChangeAddresses => _allAddresses.where((addr) => addr.isChange).toList(); - @computed - List get selectedReceiveAddresses => - receiveAddressesByType[_addressPageType] ?? []; + @observable + List selectedReceiveAddresses = []; - @computed - List get selectedChangeAddresses => - changeAddressesByType[_addressPageType] ?? []; + @observable + List selectedChangeAddresses = []; List getAddressesByType( BitcoinAddressType type, [ @@ -179,7 +177,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { await updateAddressesInBox(); } - @action Future getChangeAddress() async { final address = selectedChangeAddresses.firstWhere( (addr) => _isUnusedChangeAddressByType(addr, changeAddressType), @@ -264,6 +261,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { _allAddresses.forEach((addressRecord) { allAddressesMap[addressRecord.address] = addressRecord.name; }); + + await saveAddressesInBox(); } @action @@ -288,6 +287,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { changeAddressesByType[type] = _allAddresses.where((addr) => _isAddressByType(addr, type)).toList(); }); + + selectedReceiveAddresses = receiveAddressesByType[addressPageType] ?? []; + selectedChangeAddresses = changeAddressesByType[changeAddressType] ?? []; } @action @@ -360,7 +362,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressType: type, derivationInfo: bitcoinDerivationInfo, ); - updateAdresses(newReceiveAddresses); + addAddresses(newReceiveAddresses); final newChangeAddresses = await discoverNewAddresses( derivationType: derivationType, @@ -368,7 +370,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressType: type, derivationInfo: bitcoinDerivationInfo, ); - updateAdresses(newChangeAddresses); + addAddresses(newChangeAddresses); continue; } @@ -386,7 +388,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressType: type, derivationInfo: bitcoinDerivationInfo, ); - updateAdresses(newReceiveAddresses); + addAddresses(newReceiveAddresses); final newChangeAddresses = await discoverNewAddresses( derivationType: derivationType, @@ -394,7 +396,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { addressType: type, derivationInfo: bitcoinDerivationInfo, ); - updateAdresses(newChangeAddresses); + addAddresses(newChangeAddresses); } } } diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index d194366c1c..185d4ef3ce 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -1506,21 +1506,20 @@ class ElectrumWorker { try { final addToWallet = {}; - // receivers.forEach((receiver) { - // scanOutputs called from rust here - final receiver = receivers.first; - final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); + receivers.forEach((receiver) { + // scanOutputs called from rust here + final scanResult = scanOutputs([outputPubkeys.keys.toList()], tweak, receiver); - if (scanResult.isEmpty) { - continue; - } + if (scanResult.isEmpty) { + return; + } - if (addToWallet[receiver.BSpend] == null) { - addToWallet[receiver.BSpend] = scanResult; - } else { - addToWallet[receiver.BSpend].addAll(scanResult); - } - // }); + if (addToWallet[receiver.BSpend] == null) { + addToWallet[receiver.BSpend] = scanResult; + } else { + addToWallet[receiver.BSpend].addAll(scanResult); + } + }); if (addToWallet.isEmpty) { // no results tx, continue to next tx @@ -1552,27 +1551,34 @@ class ElectrumWorker { List unspents = []; - addToWallet.forEach((BSpend, result) { - result.forEach((label, value) { - (value as Map).forEach((output, tweak) { - final t_k = tweak.toString(); + addToWallet.forEach((BSpend, scanResultPerLabel) { + scanResultPerLabel.forEach((label, scanOutput) { + (scanOutput as Map).forEach((outputPubkey, tweak) { + final t_k = tweak as String; - final receivingOutputAddress = ECPublic.fromHex(output) + final receivingOutputAddress = ECPublic.fromHex(outputPubkey) .toTaprootAddress(tweak: false) .toAddress(scanData.network); - final matchingOutput = outputPubkeys[output]!; + final matchingOutput = outputPubkeys[outputPubkey]!; final amount = matchingOutput.amount; final pos = matchingOutput.vout; + final matchingSPWallet = scanData.silentPaymentsWallets.firstWhere( + (receiver) => receiver.B_spend.toHex() == BSpend.toString(), + ); + + final labelIndex = scanData.labels[label]; + final receivedAddressRecord = BitcoinReceivedSPAddressRecord( receivingOutputAddress, - labelIndex: 1, // TODO: get actual index/label - isChange: false, // and if change or not + labelIndex: labelIndex ?? 0, + isChange: labelIndex == 0, isUsed: true, tweak: t_k, txCount: 1, balance: amount, + spAddress: matchingSPWallet.toAddress(scanData.network), ); final unspent = BitcoinUnspent(receivedAddressRecord, txid, amount, pos); @@ -1583,14 +1589,14 @@ class ElectrumWorker { }); }); - _sendResponse(ElectrumWorkerTweaksSubscribeResponse( - result: TweaksSyncResponse( - transactions: {txInfo.id: TweakResponseData(txInfo: txInfo, unspents: unspents)}, - wasSingleBlock: scanData.isSingleScan, + _sendResponse( + ElectrumWorkerTweaksSubscribeResponse( + result: TweaksSyncResponse( + transactions: {txInfo.id: TweakResponseData(txInfo: txInfo, unspents: unspents)}, + wasSingleBlock: scanData.isSingleScan, + ), ), - )); - - return; + ); } catch (e, stacktrace) { printV(stacktrace); printV(e.toString()); diff --git a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart index 03294a2818..01dd47fd59 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/tweaks_subscribe.dart @@ -6,7 +6,7 @@ class ScanData { final BasedUtxoNetwork network; final int chainTip; final List transactionHistoryIds; - final Map labels; + final Map labels; final List labelIndexes; final bool isSingleScan; final bool shouldSwitchNodes; @@ -61,7 +61,7 @@ class ScanData { chainTip: json['chainTip'] as int, transactionHistoryIds: (json['transactionHistoryIds'] as List).map((e) => e as String).toList(), - labels: json['labels'] as Map, + labels: json['labels'] as Map, labelIndexes: (json['labelIndexes'] as List).map((e) => e as int).toList(), isSingleScan: json['isSingleScan'] as bool, shouldSwitchNodes: json['shouldSwitchNodes'] as bool, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 14fc7d141b..e367180736 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -751,9 +751,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override @action - Future updateAllUnspents() async { + Future updateAllUnspents([Set? scripthashes, bool? wait]) async { if (!mwebEnabled) { - await super.updateAllUnspents(); + await super.updateAllUnspents(scripthashes, wait); return; } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 46b8d0d6f1..97d233dc48 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -143,7 +143,7 @@ class CWBitcoin extends Bitcoin { formattedCryptoAmount: out.formattedCryptoAmount, memo: out.memo)) .toList(), - priority: priority as ElectrumTransactionPriority, + priority: priority, feeRate: bitcoinFeeRate, coinTypeToSpendFrom: coinTypeToSpendFrom, ); From 7ed38b7116c7ebd7d81aaae282df8c49dc1810a0 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 28 Jan 2025 09:40:22 -0300 Subject: [PATCH 64/64] fix: sp label index --- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 03b9c4dc04..14be53dcb0 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -321,12 +321,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S @action BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { if (addressPageType == SilentPaymentsAddresType.p2sp) { - final usableSilentPaymentAddresses = silentPaymentAddresses - .where((a) => - a.type != SegwitAddressType.p2tr && - a.derivationPath != OLD_SP_PATH && - a.isChange == false) - .toList(); + final usableSilentPaymentAddresses = + (receiveAddressesByType[SilentPaymentsAddresType.p2sp] ?? []) + .where((a) => + (a as BitcoinSilentPaymentAddressRecord).derivationPath != + OLD_SP_PATH.replaceFirst("#", "0")) + .toList(); final nextSPLabelIndex = usableSilentPaymentAddresses.length; final address = BitcoinSilentPaymentAddressRecord(