From e47846bc7d7d4a2e4c13266a4c615cea7ae23a2a Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Fri, 4 Oct 2024 11:49:49 -0300 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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]!; } }