From 6b795b5ba3f25fc2fe3f4cb64d544e27d146c2c8 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 26 Feb 2024 15:32:54 -0300 Subject: [PATCH] feat: rebase btc-addr-types, migrate to bitcoin_base --- cw_bitcoin/lib/bitcoin_address_record.dart | 6 +- .../lib/bitcoin_receive_page_option.dart | 4 + cw_bitcoin/lib/bitcoin_unspent.dart | 28 +- cw_bitcoin/lib/bitcoin_wallet.dart | 17 +- cw_bitcoin/lib/bitcoin_wallet_addresses.dart | 4 +- cw_bitcoin/lib/electrum.dart | 6 + cw_bitcoin/lib/electrum_transaction_info.dart | 76 +- cw_bitcoin/lib/electrum_wallet.dart | 717 +++++++++++++++++- cw_bitcoin/lib/electrum_wallet_addresses.dart | 85 ++- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 23 +- cw_bitcoin/pubspec.lock | 12 +- cw_bitcoin/pubspec.yaml | 3 +- cw_bitcoin_cash/pubspec.yaml | 2 +- cw_core/lib/sync_status.dart | 14 +- cw_core/lib/unspent_transaction_output.dart | 3 +- howto-build-android.md | 20 +- lib/bitcoin/cw_bitcoin.dart | 5 + lib/core/address_validator.dart | 5 +- lib/core/sync_status_title.dart | 6 +- lib/di.dart | 5 + .../present_receive_option_picker.dart | 128 ++-- lib/src/screens/receive/receive_page.dart | 32 +- lib/src/screens/send/widgets/send_card.dart | 3 +- lib/src/widgets/address_text_field.dart | 6 +- .../dashboard/dashboard_view_model.dart | 5 + lib/view_model/rescan_view_model.dart | 11 +- .../wallet_address_list_view_model.dart | 23 +- model_generator.sh | 19 +- tool/configure.dart | 1 + 29 files changed, 1046 insertions(+), 223 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index d8d908230..2c40ba34c 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -16,6 +16,7 @@ class BitcoinAddressRecord { required this.type, String? scriptHash, required this.network, + this.silentPaymentTweak, }) : _txCount = txCount, _balance = balance, _name = name, @@ -23,7 +24,7 @@ class BitcoinAddressRecord { scriptHash = scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); - factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) { + factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord( @@ -42,6 +43,7 @@ class BitcoinAddressRecord { network: (decoded['network'] as String?) == null ? network : BasedUtxoNetwork.fromName(decoded['network'] as String), + silentPaymentTweak: decoded['silentPaymentTweak'] as String?, ); } @@ -57,6 +59,7 @@ class BitcoinAddressRecord { bool _isUsed; String? scriptHash; BasedUtxoNetwork? network; + final String? silentPaymentTweak; int get txCount => _txCount; @@ -96,5 +99,6 @@ class BitcoinAddressRecord { 'type': type.toString(), 'scriptHash': scriptHash, 'network': network?.value, + 'silentPaymentTweak': silentPaymentTweak, }); } diff --git a/cw_bitcoin/lib/bitcoin_receive_page_option.dart b/cw_bitcoin/lib/bitcoin_receive_page_option.dart index 2e246f532..2b025965b 100644 --- a/cw_bitcoin/lib/bitcoin_receive_page_option.dart +++ b/cw_bitcoin/lib/bitcoin_receive_page_option.dart @@ -8,6 +8,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); + static const silent_payments = BitcoinReceivePageOption._('Silent Payments'); + const BitcoinReceivePageOption._(this.value); final String value; @@ -34,6 +36,8 @@ class BitcoinReceivePageOption implements ReceivePageOption { return BitcoinReceivePageOption.p2pkh; case P2shAddressType.p2wpkhInP2sh: return BitcoinReceivePageOption.p2sh; + case SilentPaymentsAddresType.p2sp: + return BitcoinReceivePageOption.silent_payments; case SegwitAddresType.p2wpkh: default: return BitcoinReceivePageOption.p2wpkh; diff --git a/cw_bitcoin/lib/bitcoin_unspent.dart b/cw_bitcoin/lib/bitcoin_unspent.dart index 52edea091..131f47ab6 100644 --- a/cw_bitcoin/lib/bitcoin_unspent.dart +++ b/cw_bitcoin/lib/bitcoin_unspent.dart @@ -1,14 +1,38 @@ +import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_core/unspent_transaction_output.dart'; class BitcoinUnspent extends Unspent { - BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) + BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout, + {this.silentPaymentTweak, this.type}) : bitcoinAddressRecord = addressRecord, super(addressRecord.address, hash, value, vout, null); factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map json) => BitcoinUnspent( - address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); + address, + json['tx_hash'] as String, + json['value'] as int, + json['tx_pos'] as int, + silentPaymentTweak: json['silent_payment_tweak'] as String?, + type: json['type'] == null + ? null + : BitcoinAddressType.values.firstWhere((e) => e.toString() == json['type']), + ); + + Map toJson() { + final json = { + 'address_record': bitcoinAddressRecord.toJSON(), + 'tx_hash': hash, + 'value': value, + 'tx_pos': vout, + 'silent_payment_tweak': silentPaymentTweak, + 'type': type.toString(), + }; + return json; + } final BitcoinAddressRecord bitcoinAddressRecord; + String? silentPaymentTweak; + BitcoinAddressType? type; } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 3b3e9c636..a018abbd6 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -30,6 +30,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + List? initialSilentAddresses, + int initialSilentAddressIndex = 0, + SilentPaymentOwner? silentAddress, }) : super( mnemonic: mnemonic, password: password, @@ -46,10 +49,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { currency: CryptoCurrency.btc) { walletAddresses = BitcoinWalletAddresses( walletInfo, - electrumClient: electrumClient, initialAddresses: initialAddresses, initialRegularAddressIndex: initialRegularAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + silentAddress: silentAddress, mainHd: hd, sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), network: networkParam ?? network, @@ -67,9 +72,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { String? addressPageType, BasedUtxoNetwork? network, List? initialAddresses, + List? initialSilentAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + int initialSilentAddressIndex = 0, }) async { return BitcoinWallet( mnemonic: mnemonic, @@ -77,6 +84,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: initialAddresses, + initialSilentAddresses: initialSilentAddresses, + initialSilentAddressIndex: initialSilentAddressIndex, + silentAddress: await SilentPaymentOwner.fromMnemonic(mnemonic, + hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'), initialBalance: initialBalance, seedBytes: await mnemonicToSeedBytes(mnemonic), initialRegularAddressIndex: initialRegularAddressIndex, @@ -101,6 +112,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, + initialSilentAddresses: snp.silentAddresses, + initialSilentAddressIndex: snp.silentAddressIndex, + silentAddress: await SilentPaymentOwner.fromMnemonic(snp.mnemonic, + hrp: snp.network == BitcoinNetwork.testnet ? 'tsp' : 'sp'), initialBalance: snp.balance, seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialRegularAddressIndex: snp.regularAddressIndex, diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index f12577492..48960ce3d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -15,10 +15,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S required super.mainHd, required super.sideHd, required super.network, - required super.electrumClient, super.initialAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, + super.initialSilentAddresses, + super.initialSilentAddressIndex = 0, + super.silentAddress, }) : super(walletInfo); @override diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 51a53e285..59c864eb2 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -349,6 +349,12 @@ class ElectrumClient { return null; }); + BehaviorSubject? chainTipUpdate() { + _id += 1; + return subscribe( + id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe'); + } + BehaviorSubject? scripthashUpdate(String scripthash) { _id += 1; return subscribe( diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index cfea0e089..5a7f797f9 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,9 +1,8 @@ import 'package:bitcoin_base/bitcoin_base.dart'; -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; 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'; import 'package:cw_core/format_amount.dart'; @@ -20,6 +19,8 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { + BitcoinUnspent? unspent; + ElectrumTransactionInfo(this.type, {required String id, required int height, @@ -28,7 +29,9 @@ class ElectrumTransactionInfo extends TransactionInfo { required TransactionDirection direction, required bool isPending, required DateTime date, - required int confirmations}) { + required int confirmations, + String? to, + this.unspent}) { this.id = id; this.height = height; this.amount = amount; @@ -37,6 +40,7 @@ class ElectrumTransactionInfo extends TransactionInfo { this.date = date; this.isPending = isPending; this.confirmations = confirmations; + this.to = to; } factory ElectrumTransactionInfo.fromElectrumVerbose(Map obj, WalletType type, @@ -144,50 +148,24 @@ class ElectrumTransactionInfo extends TransactionInfo { confirmations: bundle.confirmations); } - factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, - {List? addresses, required int height, int? timestamp, required int confirmations}) { - final tx = bitcoin.Transaction.fromHex(hex); - var exist = false; - var amount = 0; - - if (addresses != null) { - tx.outs.forEach((out) { - try { - final p2pkh = - bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); - exist = addresses.contains(p2pkh.data.address); - - if (exist) { - amount += out.value!; - } - } catch (_) {} - }); - } - - final date = - timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); - - return ElectrumTransactionInfo(type, - id: tx.getId(), - height: height, - isPending: false, - fee: null, - direction: TransactionDirection.incoming, - amount: amount, - date: date, - confirmations: confirmations); - } - factory ElectrumTransactionInfo.fromJson(Map data, WalletType type) { - return ElectrumTransactionInfo(type, - id: data['id'] as String, - height: data['height'] as int, - amount: data['amount'] as int, - fee: data['fee'] as int, - direction: parseTransactionDirectionFromInt(data['direction'] as int), - date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - isPending: data['isPending'] as bool, - confirmations: data['confirmations'] as int); + return ElectrumTransactionInfo( + type, + id: data['id'] as String, + height: data['height'] as int, + amount: data['amount'] as int, + fee: data['fee'] as int, + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + to: data['to'] as String?, + unspent: data['unspent'] != null + ? BitcoinUnspent.fromJSON( + BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String), + data['unspent'] as Map) + : null, + ); } final WalletType type; @@ -231,6 +209,12 @@ class ElectrumTransactionInfo extends TransactionInfo { m['isPending'] = isPending; m['confirmations'] = confirmations; m['fee'] = fee; + m['to'] = to; + m['unspent'] = unspent?.toJson() ?? {}; return m; } + + String toString() { + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspent)'; + } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 873fe2977..4bef9b748 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'dart:math'; import 'package:bitcoin_base/bitcoin_base.dart'; @@ -143,21 +144,93 @@ abstract class ElectrumWalletBase List unspentCoins; List _feeRates; Map?> _scripthashesUpdateSubject; + BehaviorSubject? _chainTipUpdateSubject; bool _isTransactionUpdating; + // Future? _isolate; void Function(FlutterErrorDetails)? _onError; + Timer? _autoSaveTimer; + static const int _autoSaveInterval = 30; Future init() async { await walletAddresses.init(); await transactionHistory.init(); - await save(); + + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } + // @action + // Future _setListeners(int height, {int? chainTip}) async { + // final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0; + // syncStatus = AttemptingSyncStatus(); + + // if (_isolate != null) { + // final runningIsolate = await _isolate!; + // runningIsolate.kill(priority: Isolate.immediate); + // } + + // final receivePort = ReceivePort(); + // _isolate = Isolate.spawn( + // startRefresh, + // ScanData( + // sendPort: receivePort.sendPort, + // primarySilentAddress: walletAddresses.primarySilentAddress!, + // networkType: networkType, + // height: height, + // chainTip: currentChainTip, + // electrumClient: ElectrumClient(), + // transactionHistoryIds: transactionHistory.transactions.keys.toList(), + // node: electrumClient.uri.toString(), + // labels: walletAddresses.labels, + // )); + + // await for (var message in receivePort) { + // if (message is BitcoinUnspent) { + // if (!unspentCoins.any((utx) => + // utx.hash.contains(message.hash) && + // utx.vout == message.vout && + // utx.address.contains(message.address))) { + // unspentCoins.add(message); + + // if (unspentCoinsInfo.values.any((element) => + // element.walletId.contains(id) && + // element.hash.contains(message.hash) && + // element.address.contains(message.address))) { + // _addCoinInfo(message); + + // await walletInfo.save(); + // await save(); + // } + + // balance[currency] = await _fetchBalances(); + // } + // } + + // if (message is Map) { + // transactionHistory.addMany(message); + // await transactionHistory.save(); + // } + + // // check if is a SyncStatus type since "is SyncStatus" doesn't work here + // if (message is SyncResponse) { + // syncStatus = message.syncStatus; + // walletInfo.restoreHeight = message.height; + // await walletInfo.save(); + // } + // } + // } + @action @override Future startSync() async { try { - syncStatus = AttemptingSyncStatus(); + await _setInitialHeight(); + } catch (_) {} + + try { + rescan(height: walletInfo.restoreHeight); + await updateTransactions(); _subscribeForUpdates(); await updateUnspent(); @@ -187,6 +260,12 @@ abstract class ElectrumWalletBase } }; syncStatus = ConnectedSyncStatus(); + + // final currentChainTip = await electrumClient.getCurrentBlockChainTip(); + + // if ((currentChainTip ?? 0) > walletInfo.restoreHeight) { + // _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip); + // } } catch (e) { print(e.toString()); syncStatus = FailedSyncStatus(); @@ -213,6 +292,124 @@ abstract class ElectrumWalletBase allInputsAmount += utx.value; leftAmount = leftAmount - utx.value; + if (utx.bitcoinAddressRecord.silentPaymentTweak != null) { + // final d = ECPrivate.fromHex(walletAddresses.primarySilentAddress!.spendPrivkey.toHex()) + // .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!)!; + + // inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true)); + // address = bitcoin.P2trAddress(address: utx.address, networkType: networkType); + // keyPairs.add(bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex, + // compressed: true, network: networkType)); + // scriptType = bitcoin.AddressType.p2tr; + // script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType) + // .scriptPubkey + // .toBytes(); + } + + final address = _addressTypeFromStr(utx.address, network); + final privkey = generateECPrivate( + hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: network); + + privateKeys.add(privkey); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + ), + ownerDetails: + UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address), + ), + ); + + bool amountIsAcquired = !sendAll && leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + final allAmountFee = transactionCredentials.feeRate != null + ? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length) + : feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length); + + final allAmount = allInputsAmount - allAmountFee; + + var credentialsAmount = 0; + var amount = 0; + var fee = 0; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); + + if (allAmount - credentialsAmount < minAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = credentialsAmount; + + if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount, + outputsCount: outputs.length + 1); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount, + outputsCount: outputs.length + 1); + } + } else { + final output = outputs.first; + credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0; + + if (credentialsAmount > allAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + amount = output.sendAll || allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + + if (output.sendAll || amount == allAmount) { + fee = allAmountFee; + } else if (transactionCredentials.feeRate != null) { + fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount); + } else { + fee = calculateEstimatedFee(transactionCredentials.priority, amount); + } + } + + if (fee == 0) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final totalAmount = amount + fee; + + if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + + final txb = bitcoin.TransactionBuilder(network: networkType); + final changeAddress = await walletAddresses.getChangeAddress(); + var leftAmount = totalAmount; + var totalInputAmount = 0; + + inputs.clear(); + + for (final utx in unspentCoins) { + if (utx.isSending) { + leftAmount = leftAmount - utx.value; + final address = _addressTypeFromStr(utx.address, network); final privkey = generateECPrivate( hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, @@ -304,23 +501,22 @@ abstract class ElectrumWalletBase } } - return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount); - } + if (SilentPaymentAddress.regex.hasMatch(outputAddress)) { + // final outpointsHash = SilentPayment.hashOutpoints(outpoints); + // final generatedOutputs = SilentPayment.generateMultipleRecipientPubkeys(inputPrivKeys, + // outpointsHash, SilentPaymentDestination.fromAddress(outputAddress, outputAmount!)); - @override - Future createTransaction(Object credentials) async { - try { - final outputs = []; - final outputAddresses = []; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final hasMultiDestination = transactionCredentials.outputs.length > 1; - final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; - - var credentialsAmount = 0; - - for (final out in transactionCredentials.outputs) { - final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address; - final address = _addressTypeFromStr(outputAddress, network); + // generatedOutputs.forEach((recipientSilentAddress, generatedOutput) { + // generatedOutput.forEach((output) { + // outputs.add(BitcoinOutputDetails( + // address: P2trAddress( + // program: ECPublic.fromHex(output.$1.toHex()).toTapPoint(), + // networkType: networkType), + // value: BigInt.from(output.$2), + // )); + // }); + // }); + } outputAddresses.add(address); @@ -392,6 +588,8 @@ abstract class ElectrumWalletBase ? SegwitAddresType.p2wpkh.toString() : walletInfo.addressPageType.toString(), 'balance': balance[currency]?.toJSON(), + 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), + 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), 'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet', }); @@ -498,18 +696,31 @@ abstract class ElectrumWalletBase } @override - Future rescan({required int height}) async => throw UnimplementedError(); + Future rescan({required int height, int? chainTip, ScanData? scanData}) async { + // _setListeners(height); + } @override Future close() async { try { await electrumClient.close(); } catch (_) {} + _autoSaveTimer?.cancel(); } Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { + // Update unspents stored from scanned silent payment transactions + transactionHistory.transactions.values.forEach((tx) { + if (tx.unspent != null) { + if (!unspentCoins + .any((utx) => utx.hash.contains(tx.unspent!.hash) && utx.vout == tx.unspent!.vout)) { + unspentCoins.add(tx.unspent!); + } + } + }); + List updatedUnspentCoins = []; final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); @@ -538,7 +749,7 @@ abstract class ElectrumWalletBase final coinInfoList = unspentCoinsInfo.values.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash) && - element.vout == coin.vout); + element.address.contains(coin.address)); if (coinInfoList.isNotEmpty) { final coinInfo = coinInfoList.first; @@ -555,6 +766,7 @@ abstract class ElectrumWalletBase await _refreshUnspentCoinsInfo(); } + @action Future _addCoinInfo(BitcoinUnspent coin) async { final newInfo = UnspentCoinsInfo( walletId: id, @@ -619,19 +831,17 @@ abstract class ElectrumWalletBase confirmations = verboseTransaction['confirmations'] as int? ?? 0; } - final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex); - final ins = []; + final original = BtcTransaction.fromRaw(transactionHex); + final ins = []; for (final vin in original.inputs) { try { final id = HEX.encode(HEX.decode(vin.txId).reversed.toList()); final txHex = await electrumClient.getTransactionHex(hash: id); - final tx = bitcoin_base.BtcTransaction.fromRaw(txHex); + final tx = BtcTransaction.fromRaw(txHex); ins.add(tx); } catch (_) { - ins.add(bitcoin_base.BtcTransaction.fromRaw( - await electrumClient.getTransactionHex(hash: vin.txId), - )); + ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId))); } } @@ -767,7 +977,7 @@ abstract class ElectrumWalletBase } } - void _subscribeForUpdates() { + void _subscribeForUpdates() async { scriptHashes.forEach((sh) async { await _scripthashesUpdateSubject[sh]?.close(); _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); @@ -786,6 +996,23 @@ abstract class ElectrumWalletBase } }); }); + + await _chainTipUpdateSubject?.close(); + _chainTipUpdateSubject = electrumClient.chainTipUpdate(); + _chainTipUpdateSubject?.listen((_) async { + try { + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + // _setListeners(walletInfo.restoreHeight, chainTip: currentHeight); + } catch (e, s) { + print(e.toString()); + _onError?.call(FlutterErrorDetails( + exception: e, + stack: s, + library: this.runtimeType.toString(), + )); + } + }); } Future _fetchBalances() async { @@ -799,21 +1026,25 @@ abstract class ElectrumWalletBase } var totalFrozen = 0; + var totalConfirmed = 0; + var totalUnconfirmed = 0; + + // Add values from unspent coins that are not fetched by the address list + // i.e. scanned silent payments unspentCoinsInfo.values.forEach((info) { unspentCoins.forEach((element) { if (element.hash == info.hash && - element.vout == info.vout && - info.isFrozen && element.bitcoinAddressRecord.address == info.address && element.value == info.value) { - totalFrozen += element.value; + if (info.isFrozen) totalFrozen += element.value; + if (element.bitcoinAddressRecord.silentPaymentTweak != null) { + totalConfirmed += element.value; + } } }); }); final balances = await Future.wait(balanceFutures); - var totalConfirmed = 0; - var totalUnconfirmed = 0; for (var i = 0; i < balances.length; i++) { final addressRecord = addresses[i]; @@ -860,6 +1091,428 @@ abstract class ElectrumWalletBase final HD = index == null ? hd : hd.derive(index); return base64Encode(HD.signMessage(message)); } + + Future _setInitialHeight() async { + if (walletInfo.isRecovery) { + return; + } + + if (walletInfo.restoreHeight == 0) { + final currentHeight = await electrumClient.getCurrentBlockChainTip(); + if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + } + } +} + +class ScanData { + final SendPort sendPort; + final SilentPaymentReceiver primarySilentAddress; + final int height; + final String node; + final bitcoin.NetworkType networkType; + final int chainTip; + final ElectrumClient electrumClient; + final List transactionHistoryIds; + final Map labels; + + ScanData({ + required this.sendPort, + required this.primarySilentAddress, + required this.height, + required this.node, + required this.networkType, + required this.chainTip, + required this.electrumClient, + required this.transactionHistoryIds, + required this.labels, + }); + + factory ScanData.fromHeight(ScanData scanData, int newHeight) { + return ScanData( + sendPort: scanData.sendPort, + primarySilentAddress: scanData.primarySilentAddress, + height: newHeight, + node: scanData.node, + networkType: scanData.networkType, + chainTip: scanData.chainTip, + transactionHistoryIds: scanData.transactionHistoryIds, + electrumClient: scanData.electrumClient, + labels: scanData.labels, + ); + } +} + +class SyncResponse { + final int height; + final SyncStatus syncStatus; + + SyncResponse(this.height, this.syncStatus); +} + +// Future startRefresh(ScanData scanData) async { +// var cachedBlockchainHeight = scanData.chainTip; + +// Future getNodeHeightOrUpdate(int baseHeight) async { +// if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) { +// final electrumClient = scanData.electrumClient; +// if (!electrumClient.isConnected) { +// final node = scanData.node; +// await electrumClient.connectToUri(Uri.parse(node)); +// } + +// cachedBlockchainHeight = +// await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight; +// } + +// return cachedBlockchainHeight; +// } + +// var lastKnownBlockHeight = 0; +// var initialSyncHeight = 0; + +// var syncHeight = scanData.height; +// var currentChainTip = scanData.chainTip; + +// if (syncHeight <= 0) { +// syncHeight = currentChainTip; +// } + +// if (initialSyncHeight <= 0) { +// initialSyncHeight = syncHeight; +// } + +// if (lastKnownBlockHeight == syncHeight) { +// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); +// return; +// } + +// // Run this until no more blocks left to scan txs. At first this was recursive +// // i.e. re-calling the startRefresh function but this was easier for the above values to retain +// // their initial values +// while (true) { +// lastKnownBlockHeight = syncHeight; + +// final syncingStatus = +// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight); +// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); + +// if (syncingStatus.blocksLeft <= 0) { +// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus())); +// return; +// } + +// // print(["Scanning from height:", syncHeight]); + +// try { +// final networkPath = +// scanData.networkType.network == bitcoin.BtcNetwork.mainnet ? "" : "/testnet"; + +// // This endpoint gets up to 10 latest blocks from the given height +// final tenNewestBlocks = +// (await http.get(Uri.parse("https://blockstream.info$networkPath/api/blocks/$syncHeight"))) +// .body; +// var decodedBlocks = json.decode(tenNewestBlocks) as List; + +// decodedBlocks.sort((a, b) => (a["height"] as int).compareTo(b["height"] as int)); +// decodedBlocks = +// decodedBlocks.where((element) => (element["height"] as int) >= syncHeight).toList(); + +// // for each block, get up to 25 txs +// for (var i = 0; i < decodedBlocks.length; i++) { +// final blockJson = decodedBlocks[i]; +// final blockHash = blockJson["id"]; +// final txCount = blockJson["tx_count"] as int; + +// // print(["Scanning block index:", i, "with tx count:", txCount]); + +// int startIndex = 0; +// // go through each tx in block until no more txs are left +// while (startIndex < txCount) { +// // This endpoint gets up to 25 txs from the given block hash and start index +// final twentyFiveTxs = json.decode((await http.get(Uri.parse( +// "https://blockstream.info$networkPath/api/block/$blockHash/txs/$startIndex"))) +// .body) as List; + +// // print(["Scanning txs index:", startIndex]); + +// // For each tx, apply silent payment filtering and do shared secret calculation when applied +// for (var i = 0; i < twentyFiveTxs.length; i++) { +// try { +// final tx = twentyFiveTxs[i]; +// final txid = tx["txid"] as String; + +// // print(["Scanning tx:", txid]); + +// // TODO: if tx already scanned & stored skip +// // if (scanData.transactionHistoryIds.contains(txid)) { +// // // already scanned tx, continue to next tx +// // pos++; +// // continue; +// // } + +// List pubkeys = []; +// List outpoints = []; + +// bool skip = false; + +// for (var i = 0; i < (tx["vin"] as List).length; i++) { +// final input = tx["vin"][i]; +// final prevout = input["prevout"]; +// final scriptPubkeyType = prevout["scriptpubkey_type"]; +// String? pubkey; + +// if (scriptPubkeyType == "v0_p2wpkh" || scriptPubkeyType == "v1_p2tr") { +// final witness = input["witness"]; +// if (witness == null) { +// skip = true; +// // print("Skipping, no witness"); +// break; +// } + +// if (witness.length == 2) { +// pubkey = witness[1] as String; +// } else if (witness.length == 1) { +// pubkey = "02" + (prevout["scriptpubkey"] as String).fromHex.sublist(2).hex; +// } +// } + +// if (scriptPubkeyType == "p2pkh") { +// pubkey = bitcoin.P2pkhAddress( +// scriptSig: bitcoin.Script.fromRaw(hexData: input["scriptsig"] as String)) +// .pubkey; +// } + +// if (pubkey == null) { +// skip = true; +// // print("Skipping, invalid witness"); +// break; +// } + +// pubkeys.add(pubkey); +// outpoints.add( +// bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int)); +// } + +// if (skip) { +// // skipped tx, continue to next tx +// continue; +// } + +// Map outpointsByP2TRpubkey = {}; +// for (var i = 0; i < (tx["vout"] as List).length; i++) { +// final output = tx["vout"][i]; +// if (output["scriptpubkey_type"] != "v1_p2tr") { +// // print("Skipping, not a v1_p2tr output"); +// continue; +// } + +// final script = (output["scriptpubkey"] as String).fromHex; + +// // final alreadySpentOutput = (await electrumClient.getHistory( +// // scriptHashFromScript(script, networkType: scanData.networkType))) +// // .length > +// // 1; + +// // if (alreadySpentOutput) { +// // print("Skipping, invalid witness"); +// // break; +// // } + +// final p2tr = bitcoin.P2trAddress( +// program: script.sublist(2).hex, networkType: scanData.networkType); +// final address = p2tr.address; + +// print(["Verifying taproot address:", address]); + +// outpointsByP2TRpubkey[script.sublist(2).hex] = +// bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int); +// } + +// if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) { +// // skipped tx, continue to next tx +// continue; +// } + +// final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); + +// final result = bitcoin.scanOutputs( +// scanData.primarySilentAddress.scanPrivkey, +// scanData.primarySilentAddress.spendPubkey, +// bitcoin.getSumInputPubKeys(pubkeys), +// outpointHash, +// outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(), +// labels: scanData.labels, +// ); + +// if (result.isEmpty) { +// // no results tx, continue to next tx +// continue; +// } + +// if (result.length > 1) { +// print("MULTIPLE UNSPENT COINS FOUND!"); +// } else { +// print("UNSPENT COIN FOUND!"); +// } + +// result.forEach((key, value) async { +// final outpoint = outpointsByP2TRpubkey[key]; + +// if (outpoint == null) { +// return; +// } + +// final tweak = value[0]; +// String? label; +// if (value.length > 1) label = value[1]; + +// final txInfo = ElectrumTransactionInfo( +// WalletType.bitcoin, +// id: txid, +// height: syncHeight, +// amount: outpoint.value!, +// fee: 0, +// direction: TransactionDirection.incoming, +// isPending: false, +// date: DateTime.fromMillisecondsSinceEpoch((blockJson["timestamp"] as int) * 1000), +// confirmations: currentChainTip - syncHeight, +// to: bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress( +// scanData.primarySilentAddress.scanPubkey, +// scanData.primarySilentAddress.spendPubkey, +// label != null ? label.fromHex : "0".fromHex, +// hrp: scanData.primarySilentAddress.hrp, +// version: scanData.primarySilentAddress.version) +// .toString(), +// unspent: null, +// ); + +// final status = json.decode((await http +// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$txid/outspends"))) +// .body) as List; + +// bool spent = false; +// for (final s in status) { +// if ((s["spent"] as bool) == true) { +// spent = true; + +// scanData.sendPort.send({txid: txInfo}); + +// final sentTxId = s["txid"] as String; +// final sentTx = json.decode((await http +// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$sentTxId"))) +// .body); + +// int amount = 0; +// for (final out in (sentTx["vout"] as List)) { +// amount += out["value"] as int; +// } + +// final height = s["status"]["block_height"] as int; + +// scanData.sendPort.send({ +// sentTxId: ElectrumTransactionInfo( +// WalletType.bitcoin, +// id: sentTxId, +// height: height, +// amount: amount, +// fee: 0, +// direction: TransactionDirection.outgoing, +// isPending: false, +// date: DateTime.fromMillisecondsSinceEpoch( +// (s["status"]["block_time"] as int) * 1000), +// confirmations: currentChainTip - height, +// ) +// }); +// } +// } + +// if (spent) { +// return; +// } + +// final unspent = BitcoinUnspent( +// BitcoinAddressRecord( +// bitcoin.P2trAddress(program: key, networkType: scanData.networkType).address, +// index: 0, +// isHidden: true, +// isUsed: true, +// silentAddressLabel: null, +// silentPaymentTweak: tweak, +// type: bitcoin.AddressType.p2tr, +// ), +// txid, +// outpoint.value!, +// outpoint.index, +// silentPaymentTweak: tweak, +// type: bitcoin.AddressType.p2tr, +// ); + +// // found utxo for tx, send unspent coin to main isolate +// scanData.sendPort.send(unspent); + +// // also send tx data for tx history +// txInfo.unspent = unspent; +// scanData.sendPort.send({txid: txInfo}); +// }); +// } catch (_) {} +// } + +// // Finished scanning batch of txs in block, add 25 to start index and continue to next block in loop +// startIndex += 25; +// } + +// // Finished scanning block, add 1 to height and continue to next block in loop +// syncHeight += 1; +// currentChainTip = await getNodeHeightOrUpdate(syncHeight); +// scanData.sendPort.send(SyncResponse(syncHeight, +// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight))); +// } +// } catch (e, stacktrace) { +// print(stacktrace); +// print(e.toString()); + +// scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus())); +// break; +// } +// } +// } + +class EstimatedTxResult { + EstimatedTxResult( + {required this.utxos, required this.privateKeys, required this.fee, required this.amount}); + + final List utxos; + final List privateKeys; + final int fee; + final int amount; +} + +BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) { + if (P2pkhAddress.regex.hasMatch(address)) { + return P2pkhAddress.fromAddress(address: address, network: network); + } else if (P2shAddress.regex.hasMatch(address)) { + return P2shAddress.fromAddress(address: address, network: network); + } else if (P2wshAddress.regex.hasMatch(address)) { + return P2wshAddress.fromAddress(address: address, network: network); + } else if (P2trAddress.regex.hasMatch(address)) { + return P2trAddress.fromAddress(address: address, network: network); + } else { + return P2wpkhAddress.fromAddress(address: address, network: network); + } +} + +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 { + return SegwitAddresType.p2wpkh; + } } class EstimateTxParams { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 5880f5a19..23482e4d7 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -2,7 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_bitcoin/bitcoin_address_record.dart'; -import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -25,12 +24,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { WalletInfo walletInfo, { required this.mainHd, required this.sideHd, - required this.electrumClient, required this.network, List? initialAddresses, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, + List? initialSilentAddresses, + int initialSilentAddressIndex = 0, + SilentPaymentOwner? silentAddress, }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), + primarySilentAddress = silentAddress, addressesByReceiveType = ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of((initialAddresses ?? []) @@ -44,6 +46,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { _addressPageType = walletInfo.addressPageType != null ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) : SegwitAddresType.p2wpkh, + silentAddresses = ObservableList.of((initialSilentAddresses ?? []) + .where((addressRecord) => addressRecord.silentPaymentTweak != null) + .toSet()), + currentSilentAddressIndex = initialSilentAddressIndex, super(walletInfo) { updateAddressesByMatch(); } @@ -61,27 +67,57 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { late ObservableList addressesByReceiveType; final ObservableList receiveAddresses; final ObservableList changeAddresses; - final ElectrumClient electrumClient; + final ObservableList silentAddresses; final BasedUtxoNetwork network; final bitcoin.HDWallet mainHd; final bitcoin.HDWallet sideHd; + final SilentPaymentOwner? primarySilentAddress; + @observable BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh; @computed BitcoinAddressType get addressPageType => _addressPageType; + @observable + String? activeSilentAddress; + @computed List get allAddresses => _addresses; @override @computed String get address { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + if (activeSilentAddress != null) { + return activeSilentAddress!; + } + + return primarySilentAddress!.toString(); + } + String receiveAddress; final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch); + if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || + typeMatchingReceiveAddresses.isEmpty) { + receiveAddress = generateNewAddress().address; + } else { + final previousAddressMatchesType = + previousAddressRecord != null && previousAddressRecord!.type == addressPageType; + + if (previousAddressMatchesType && + typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) { + receiveAddress = previousAddressRecord!.address; + } else { + receiveAddress = typeMatchingReceiveAddresses.first.address; + } + final receiveAddress = receiveAddresses.first.address; + + final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch); + if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || typeMatchingReceiveAddresses.isEmpty) { receiveAddress = generateNewAddress().address; @@ -105,6 +141,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override set address(String addr) { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + activeSilentAddress = addr; + return; + } + if (addr.startsWith('bitcoincash:')) { addr = toLegacy(addr); } @@ -134,6 +175,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { void set currentChangeAddressIndex(int index) => currentChangeAddressIndexByType[_addressPageType.toString()] = index; + int currentSilentAddressIndex; + @observable BitcoinAddressRecord? previousAddressRecord; @@ -195,7 +238,43 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } + Map get labels { + final labels = {}; + for (int i = 0; i < silentAddresses.length; i++) { + final silentAddressRecord = silentAddresses[i]; + final silentAddress = + SilentPaymentDestination.fromAddress(silentAddressRecord.address, 0).spendPubkey.toHex(); + + if (silentAddressRecord.silentPaymentTweak != null) + labels[silentAddress] = silentAddressRecord.silentPaymentTweak!; + } + return labels; + } + BitcoinAddressRecord generateNewAddress({String label = ''}) { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + currentSilentAddressIndex += 1; + + final tweak = BigInt.from(currentSilentAddressIndex); + + final address = BitcoinAddressRecord( + SilentPaymentAddress.createLabeledSilentPaymentAddress( + primarySilentAddress!.scanPubkey, primarySilentAddress!.spendPubkey, tweak, + hrp: primarySilentAddress!.hrp, version: primarySilentAddress!.version) + .toString(), + index: currentSilentAddressIndex, + isHidden: false, + name: label, + silentPaymentTweak: tweak.toString(), + network: network, + type: SilentPaymentsAddresType.p2sp, + ); + + silentAddresses.add(address); + + return address; + } + final newAddressIndex = addressesByReceiveType.fold( 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 98c3753db..ceb603f9f 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -13,11 +13,13 @@ class ElectrumWalletSnapshot { required this.password, required this.mnemonic, required this.addresses, + required this.silentAddresses, required this.balance, required this.regularAddressIndex, required this.changeAddressIndex, required this.addressPageType, required this.network, + required this.silentAddressIndex, }); final String name; @@ -28,24 +30,36 @@ class ElectrumWalletSnapshot { String mnemonic; List addresses; + List silentAddresses; ElectrumBalance balance; Map regularAddressIndex; Map changeAddressIndex; + int silentAddressIndex; - static Future load(String name, WalletType type, String password, BasedUtxoNetwork? network) async { + static Future load( + String name, WalletType type, String password, BasedUtxoNetwork? network) async { final path = await pathForWallet(name: name, type: type); final jsonSource = await read(path: path, password: password); final data = json.decode(jsonSource) as Map; - final addressesTmp = data['addresses'] as List? ?? []; final mnemonic = data['mnemonic'] as String; + + final addressesTmp = data['addresses'] as List? ?? []; final addresses = addressesTmp .whereType() - .map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) .toList(); + + final silentAddressesTmp = data['silent_addresses'] as List? ?? []; + final silentAddresses = silentAddressesTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network)) + .toList(); + final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; + var silentAddressIndex = 0; try { regularAddressIndexByType = { @@ -55,6 +69,7 @@ class ElectrumWalletSnapshot { SegwitAddresType.p2wpkh.toString(): int.parse(data['change_address_index'] as String? ?? '0') }; + silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0'); } catch (_) { try { regularAddressIndexByType = data["account_index"] as Map? ?? {}; @@ -68,11 +83,13 @@ class ElectrumWalletSnapshot { password: password, mnemonic: mnemonic, addresses: addresses, + silentAddresses: silentAddresses, balance: balance, regularAddressIndex: regularAddressIndexByType, changeAddressIndex: changeAddressIndexByType, addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(), network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, + silentAddressIndex: silentAddressIndex, ); } } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 25e6f269d..aff28df6e 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -79,8 +79,8 @@ packages: dependency: "direct main" description: path: "." - ref: cake-update-v1 - resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa" + ref: cake-update-v2 + resolved-ref: e4686da77cace5400697de69f7885020297cb900 url: "https://github.com/cake-tech/bitcoin_base.git" source: git version: "4.0.0" @@ -93,14 +93,6 @@ packages: url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.1.0" - blockchain_utils: - dependency: "direct main" - description: - name: blockchain_utils - sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9" - url: "https://pub.dev" - source: hosted - version: "1.6.0" boolean_selector: dependency: transitive description: diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 847b77773..b6acab7f4 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -33,8 +33,7 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v1 - blockchain_utils: ^1.6.0 + ref: cake-update-v2 dev_dependencies: flutter_test: diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index 9c098c0ff..7130b3c58 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: bitcoin_base: git: url: https://github.com/cake-tech/bitcoin_base.git - ref: cake-update-v1 + ref: cake-update-v2 diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 4983967d0..afddc7c7a 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus { @override String toString() => '$blocksLeft'; + + factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) { + final track = chainTip - initialSyncHeight; + final diff = track - (chainTip - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = chainTip - syncHeight; + + // sum 1 because if at the chain tip, will say "0 blocks left" + return SyncingSyncStatus(left + 1, ptc); + } } class SyncedSyncStatus extends SyncStatus { @@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus { class LostConnectionSyncStatus extends SyncStatus { @override double progress() => 1.0; -} \ No newline at end of file + @override + String toString() => 'Reconnecting'; +} diff --git a/cw_core/lib/unspent_transaction_output.dart b/cw_core/lib/unspent_transaction_output.dart index b52daf43c..01b26cdcc 100644 --- a/cw_core/lib/unspent_transaction_output.dart +++ b/cw_core/lib/unspent_transaction_output.dart @@ -16,5 +16,6 @@ class Unspent { bool isFrozen; String note; - bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); + bool get isP2wpkh => + address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc'); } diff --git a/howto-build-android.md b/howto-build-android.md index a2a4e4d9f..c3fe415ee 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -142,27 +142,9 @@ Then we need to generate localization files. `$ flutter packages pub run tool/generate_localization.dart` -Lastly, we will generate mobx models for the project. - -Generate mobx models for `cw_core`: - -`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_monero`: - -`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_bitcoin`: - -`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - -Generate mobx models for `cw_haven`: - -`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..` - Finally build mobx models for the app: -`$ flutter packages pub run build_runner build --delete-conflicting-outputs` +`$ ./model_generator.sh` ### 9. Build! diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index f9c20d45e..891c6298a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -188,4 +188,9 @@ class CWBitcoin extends Bitcoin { @override List getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; + + List getSilentAddresses(Object wallet) { + final bitcoinWallet = wallet as ElectrumWallet; + return bitcoinWallet.walletAddresses.silentAddresses; + } } diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 853762a1c..19cdb1616 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -26,7 +26,7 @@ class AddressValidator extends TextValidator { return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; case CryptoCurrency.btc: - return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; + return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${bitcoin.SilentPaymentAddress.REGEX.pattern}\$'; case CryptoCurrency.nano: return '[0-9a-zA-Z_]'; case CryptoCurrency.banano: @@ -274,7 +274,8 @@ class AddressValidator extends TextValidator { '([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)' '([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)' '([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)' - '([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'; + '([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)' + '|${bitcoin.SilentPaymentAddress.REGEX.pattern}\$'; case CryptoCurrency.ltc: return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 66094de2b..fbb86fa9f 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart'; String syncStatusTitle(SyncStatus syncStatus) { if (syncStatus is SyncingSyncStatus) { - return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); + return syncStatus.blocksLeft == 1 + ? S.current.Block_remaining('${syncStatus.blocksLeft}') + : S.current.Blocks_remaining('${syncStatus.blocksLeft}'); } if (syncStatus is SyncedSyncStatus) { @@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) { } return ''; -} \ No newline at end of file +} diff --git a/lib/di.dart b/lib/di.dart index 473eaed00..d2af49f4b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -229,6 +229,7 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc; import 'buy/dfx/dfx_buy_provider.dart'; import 'core/totp_request_details.dart'; @@ -657,6 +658,10 @@ Future setup({ getIt.registerFactory(() { final wallet = getIt.get().wallet!; + // if ((wallet.type == WalletType.bitcoin && + // wallet.walletAddresses.addressPageType == btc.AddressType.p2sp) || + // wallet.type == WalletType.monero || + // wallet.type == WalletType.haven) { if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } diff --git a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart index 33bceeb5c..64125b145 100644 --- a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart +++ b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart @@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; class PresentReceiveOptionPicker extends StatelessWidget { - PresentReceiveOptionPicker( - {required this.receiveOptionViewModel, required this.color}); + PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color}); final ReceiveOptionViewModel receiveOptionViewModel; final Color color; @@ -43,17 +42,11 @@ class PresentReceiveOptionPicker extends StatelessWidget { Text( S.current.receive, style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - fontFamily: 'Lato', - color: color), + fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color), ), Observer( - builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(), - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w500, - color: color))) + builder: (_) => Text(describeOption(receiveOptionViewModel.selectedReceiveOption), + style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color))) ], ), SizedBox(width: 5), @@ -73,65 +66,68 @@ class PresentReceiveOptionPicker extends StatelessWidget { backgroundColor: Colors.transparent, body: Stack( alignment: AlignmentDirectional.center, - children:[ AlertBackground( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Spacer(), - Container( - margin: EdgeInsets.symmetric(horizontal: 24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Theme.of(context).colorScheme.background, - ), - child: Padding( - padding: const EdgeInsets.only(top: 24, bottom: 24), - child: (ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: receiveOptionViewModel.options.length, - itemBuilder: (_, index) { - final option = receiveOptionViewModel.options[index]; - return InkWell( - onTap: () { - Navigator.pop(popUpContext); + children: [ + AlertBackground( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer(), + Container( + margin: EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.background, + ), + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: (ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: receiveOptionViewModel.options.length, + itemBuilder: (_, index) { + final option = receiveOptionViewModel.options[index]; + return InkWell( + onTap: () { + Navigator.pop(popUpContext); - receiveOptionViewModel.selectReceiveOption(option); - }, - child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24), - child: Observer(builder: (_) { - final value = receiveOptionViewModel.selectedReceiveOption; + receiveOptionViewModel.selectReceiveOption(option); + }, + child: Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: Observer(builder: (_) { + final value = receiveOptionViewModel.selectedReceiveOption; - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(option.toString(), - textAlign: TextAlign.left, - style: textSmall( - color: Theme.of(context).extension()!.titleColor, - ).copyWith( - fontWeight: - value == option ? FontWeight.w800 : FontWeight.w500, - )), - RoundedCheckbox( - value: value == option, - ) - ], - ); - }), - ), - ); - }, - separatorBuilder: (_, index) => SizedBox(height: 30), - )), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(describeOption(option), + textAlign: TextAlign.left, + style: textSmall( + color: Theme.of(context) + .extension()! + .titleColor, + ).copyWith( + fontWeight: + value == option ? FontWeight.w800 : FontWeight.w500, + )), + RoundedCheckbox( + value: value == option, + ) + ], + ); + }), + ), + ); + }, + separatorBuilder: (_, index) => SizedBox(height: 30), + )), + ), ), - ), - Spacer() - ], + Spacer() + ], + ), ), - ), AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40) ], ), diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 75719d123..3f3e546b3 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -67,8 +67,7 @@ class ReceivePage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override Widget trailing(BuildContext context) { @@ -159,7 +158,8 @@ class ReceivePage extends BasePage { trailingIcon: Icon( Icons.arrow_forward_ios, size: 14, - color: Theme.of(context).extension()!.iconsColor, + color: + Theme.of(context).extension()!.iconsColor, )); } @@ -185,11 +185,19 @@ class ReceivePage extends BasePage { final isCurrent = item.address == addressListViewModel.address.address; final backgroundColor = isCurrent - ? Theme.of(context).extension()!.currentTileBackgroundColor - : Theme.of(context).extension()!.tilesBackgroundColor; + ? Theme.of(context) + .extension()! + .currentTileBackgroundColor + : Theme.of(context) + .extension()! + .tilesBackgroundColor; final textColor = isCurrent - ? Theme.of(context).extension()!.currentTileTextColor - : Theme.of(context).extension()!.tilesTextColor; + ? Theme.of(context) + .extension()! + .currentTileTextColor + : Theme.of(context) + .extension()! + .tilesTextColor; return AddressCell.fromItem(item, isCurrent: isCurrent, @@ -211,6 +219,16 @@ class ReceivePage extends BasePage { child: cell, ); })), + if (!addressListViewModel.hasSilentAddresses) + Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 32), + child: Text(S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: + Theme.of(context).extension()!.labelTextColor)), + ), ], ), )) diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 6bd2d81e9..7b3df5175 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -164,7 +165,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _presentQRScanner(BuildContext context) async { bool isCameraPermissionGranted = - await PermissionHandler.checkPermission(Permission.camera, context); + await PermissionHandler.checkPermission(Permission.camera, context); if (!isCameraPermissionGranted) return; final code = await presentQRScanner(); if (code.isEmpty) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index da5eb0373..db932ff33 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -278,6 +278,11 @@ abstract class DashboardViewModelBase with Store { WalletBase, TransactionInfo> wallet; bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; + // bool get hasRescan => + // (wallet.type == WalletType.bitcoin && + // wallet.walletAddresses.addressPageType == bitcoin.AddressType.p2sp) || + // wallet.type == WalletType.monero || + // wallet.type == WalletType.haven; final KeyService keyService; diff --git a/lib/view_model/rescan_view_model.dart b/lib/view_model/rescan_view_model.dart index c973b7b3f..e263f4a12 100644 --- a/lib/view_model/rescan_view_model.dart +++ b/lib/view_model/rescan_view_model.dart @@ -1,4 +1,5 @@ import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'rescan_view_model.g.dart'; @@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none } abstract class RescanViewModelBase with Store { RescanViewModelBase(this._wallet) - : state = RescanWalletState.none, - isButtonEnabled = false; + : state = RescanWalletState.none, + isButtonEnabled = false; final WalletBase _wallet; @@ -23,8 +24,8 @@ abstract class RescanViewModelBase with Store { @action Future rescanCurrentWallet({required int restoreHeight}) async { state = RescanWalletState.rescaning; - await _wallet.rescan(height: restoreHeight); - _wallet.transactionHistory.clear(); + _wallet.rescan(height: restoreHeight); + if (_wallet.type != WalletType.bitcoin) _wallet.transactionHistory.clear(); state = RescanWalletState.none; } -} \ No newline at end of file +} 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 a2aab5251..817b37d7a 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 @@ -183,8 +183,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }) : _baseItems = [], selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), - hasAccounts = - appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, amount = '', _settingsStore = appStore.settingsStore, super(appStore: appStore) { @@ -196,7 +194,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _init(); selectedCurrency = walletTypeToCryptoCurrency(wallet.type); - hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.haven; + _hasAccounts = + hasSilentAddresses || wallet.type == WalletType.monero || wallet.type == WalletType.haven; } static const String _cryptoNumberPattern = '0.00000000'; @@ -365,7 +364,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } @observable - bool hasAccounts; + bool _hasAccounts = false; + + @computed + bool get hasAccounts => _hasAccounts; @computed String get accountLabel { @@ -380,8 +382,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return ''; } + @observable + // ignore: prefer_final_fields + bool? _hasSilentAddresses = null; + + @computed + bool get hasSilentAddresses => _hasSilentAddresses ?? wallet.type == WalletType.bitcoin; + // @computed + // bool get hasSilentAddresses => + // _hasSilentAddresses ?? + // wallet.type == WalletType.bitcoin && + // wallet.walletAddresses.addressPageType == btc.AddressType.p2sp; + @computed bool get hasAddressList => + hasSilentAddresses || wallet.type == WalletType.monero || wallet.type == WalletType.haven || wallet.type == WalletType.bitcoinCash || diff --git a/model_generator.sh b/model_generator.sh index 8a6098621..fa1ea6fac 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,11 +1,10 @@ -cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_ethereum && flutter pub get && cd .. -cd cw_polygon && flutter pub get && cd .. +cd cw_core; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_evm; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_monero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_haven; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_nano; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_bitcoin_cash; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_polygon; flutter pub get; cd .. +cd cw_ethereum; flutter pub get; cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/tool/configure.dart b/tool/configure.dart index fb1647e13..91ea71d1f 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -125,6 +125,7 @@ abstract class Bitcoin { List getAddresses(Object wallet); String getAddress(Object wallet); + List getSilentAddresses(Object wallet); List getSubAddresses(Object wallet);