From a44bd6b8f9c225e786c80b5ffddf57f26e8853b5 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Tue, 27 Feb 2024 19:51:53 -0300 Subject: [PATCH] fix: scan when switching, fix multiple unspents in same tx --- cw_bitcoin/lib/electrum_transaction_info.dart | 18 ++- cw_bitcoin/lib/electrum_wallet.dart | 153 ++++++++++-------- cw_bitcoin/lib/electrum_wallet_addresses.dart | 11 +- cw_bitcoin/pubspec.lock | 2 +- 4 files changed, 105 insertions(+), 79 deletions(-) diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 5a7f797f9..50896c837 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -19,7 +19,7 @@ class ElectrumTransactionBundle { } class ElectrumTransactionInfo extends TransactionInfo { - BitcoinUnspent? unspent; + List? unspents; ElectrumTransactionInfo(this.type, {required String id, @@ -31,7 +31,7 @@ class ElectrumTransactionInfo extends TransactionInfo { required DateTime date, required int confirmations, String? to, - this.unspent}) { + this.unspents}) { this.id = id; this.height = height; this.amount = amount; @@ -160,10 +160,12 @@ class ElectrumTransactionInfo extends TransactionInfo { 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) + unspents: data['unspent'] != null + ? (data['unspent'] as List) + .map((unspent) => BitcoinUnspent.fromJSON( + BitcoinAddressRecord.fromJSON(unspent['address_record'] as String), + data['unspent'] as Map)) + .toList() : null, ); } @@ -210,11 +212,11 @@ class ElectrumTransactionInfo extends TransactionInfo { m['confirmations'] = confirmations; m['fee'] = fee; m['to'] = to; - m['unspent'] = unspent?.toJson() ?? {}; + m['unspent'] = unspents?.map((e) => e.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)'; + return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)'; } } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index bd63c4097..455213a2e 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -142,6 +142,9 @@ abstract class ElectrumWalletBase @observable bool nodeSupportsSilentPayments = true; + @observable + int? currentChainTip; + @override BitcoinWalletKeys get keys => BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!); @@ -197,30 +200,37 @@ abstract class ElectrumWalletBase syncStatus = UnsupportedSyncStatus(); } - 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(); + for (final map in message.entries) { + final txid = map.key; + final tx = map.value; + + if (tx.unspents != null) { + tx.unspents!.forEach((unspent) => walletAddresses.addSilentAddresses( + [unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord])); + + final existingTxInfo = transactionHistory.transactions[txid]; + if (existingTxInfo != null) { + final newUnspents = tx.unspents! + .where((unspent) => !existingTxInfo.unspents!.any((element) => + element.hash.contains(unspent.hash) && element.vout == unspent.vout)) + .toList(); + + if (newUnspents.isNotEmpty) { + existingTxInfo.unspents ??= []; + existingTxInfo.unspents!.addAll(newUnspents); + existingTxInfo.amount += newUnspents.length > 1 + ? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent) + : newUnspents[0].value; + } + } else { + transactionHistory.addMany(message); + transactionHistory.save(); + } + } + } + + updateUnspent(); } // check if is a SyncStatus type since "is SyncStatus" doesn't work here @@ -236,17 +246,16 @@ abstract class ElectrumWalletBase @override Future startSync() async { try { + syncStatus = AttemptingSyncStatus(); + if (hasSilentPaymentsScanning) { try { await _setInitialHeight(); } catch (_) {} - final currentChainTip = await electrumClient.getCurrentBlockChainTip(); if ((currentChainTip ?? 0) > walletInfo.restoreHeight) { _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip); } - } else { - syncStatus = AttemptingSyncStatus(); } await updateTransactions(); @@ -258,9 +267,9 @@ abstract class ElectrumWalletBase Timer.periodic( const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates()); - // if (!hasSilentPaymentsScanning) { - syncStatus = SyncedSyncStatus(); - // } + if (!hasSilentPaymentsScanning || walletInfo.restoreHeight == currentChainTip) { + syncStatus = SyncedSyncStatus(); + } } catch (e, stacktrace) { print(stacktrace); print(e.toString()); @@ -312,10 +321,22 @@ abstract class ElectrumWalletBase leftAmount = leftAmount - utx.value; final address = _addressTypeFromStr(utx.address, network); - final privkey = generateECPrivate( - hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: utx.bitcoinAddressRecord.index, - network: network); + + ECPrivate? privkey; + if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + privkey = walletAddresses.primarySilentAddress!.b_spend.clone().tweakAdd( + BigintUtils.fromBytes(BytesUtils.fromHexString( + (utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord) + .silentPaymentTweak)), + ); + } else { + privkey = generateECPrivate( + hd: utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: network); + } privateKeys.add(privkey); inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr)); @@ -661,18 +682,19 @@ abstract class ElectrumWalletBase Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { + List updatedUnspentCoins = []; + // 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!); + if (tx.unspents != null) { + if (!unspentCoins.any((utx) => + tx.unspents!.any((element) => utx.hash.contains(element.hash)) && + tx.unspents!.any((element) => utx.vout == element.vout))) { + updatedUnspentCoins.addAll(tx.unspents!); } } }); - List updatedUnspentCoins = []; - final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient @@ -866,7 +888,7 @@ abstract class ElectrumWalletBase final Map historiesWithDetails = {}; final history = await electrumClient - .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)!); + .getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)); if (history.isNotEmpty) { addressRecord.setAsUsed(); @@ -1048,8 +1070,8 @@ abstract class ElectrumWalletBase } if (walletInfo.restoreHeight == 0) { - final currentHeight = await electrumClient.getCurrentBlockChainTip(); - if (currentHeight != null) walletInfo.restoreHeight = currentHeight; + currentChainTip = await electrumClient.getCurrentBlockChainTip(); + if (currentChainTip != null) walletInfo.restoreHeight = currentChainTip!; } } } @@ -1102,13 +1124,18 @@ class SyncResponse { Future startRefresh(ScanData scanData) async { var cachedBlockchainHeight = scanData.chainTip; + Future connect() async { + final electrumClient = scanData.electrumClient; + if (!electrumClient.isConnected) { + final node = scanData.node; + await electrumClient.connectToUri(Uri.parse(node)); + } + return electrumClient; + } + 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)); - } + final electrumClient = await connect(); cachedBlockchainHeight = await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight; @@ -1151,16 +1178,8 @@ Future startRefresh(ScanData scanData) async { return; } - print(["Scanning from height:", syncHeight]); - try { - // Get all the tweaks from the block - final electrumClient = scanData.electrumClient; - if (!electrumClient.isConnected) { - final node = scanData.node; - print(node); - await electrumClient.connectToUri(Uri.parse(node)); - } + final electrumClient = await connect(); List? tweaks; try { @@ -1177,7 +1196,6 @@ Future startRefresh(ScanData scanData) async { for (var i = 0; i < tweaks.length; i++) { try { - // final txid = tweaks.keys.toList()[i]; final details = tweaks[i] as Map; final output_pubkeys = (details["output_pubkeys"] as List); final tweak = details["tweak"].toString(); @@ -1193,9 +1211,7 @@ Future startRefresh(ScanData scanData) async { final result = spb.scanOutputs( scanData.primarySilentAddress.b_scan, scanData.primarySilentAddress.B_spend, - output_pubkeys - .map((p) => ECPublic.fromBytes(BytesUtils.fromHexString(p.toString()).sublist(2))) - .toList(), + output_pubkeys.map((output) => output.toString()).toList(), precomputedLabels: scanData.labels, ); @@ -1206,8 +1222,7 @@ Future startRefresh(ScanData scanData) async { result.forEach((key, value) async { final t_k = value[0]; - final address = - ECPublic.fromHex(key).toTaprootAddress(tweak: false).toAddress(scanData.network); + final address = ECPublic.fromHex(key).toTaprootAddress().toAddress(scanData.network); final listUnspent = await electrumClient.getListUnspentWithAddress(address, scanData.network); @@ -1228,6 +1243,10 @@ Future startRefresh(ScanData scanData) async { } catch (_) {} }); + if (info == null) { + return; + } + // final tweak = value[0]; // String? label; // if (value.length > 1) label = value[1]; @@ -1237,20 +1256,16 @@ Future startRefresh(ScanData scanData) async { WalletType.bitcoin, id: tx.hash, height: syncHeight, - amount: tx.value, + amount: 0, // will be added later via unspent fee: 0, direction: TransactionDirection.incoming, isPending: false, date: DateTime.now(), - confirmations: currentChainTip - syncHeight, + confirmations: currentChainTip - syncHeight - 1, to: scanData.primarySilentAddress.toString(), - unspent: tx, + unspents: [tx], ); - // 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) { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ec2a27498..2bfeeeea0 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -35,7 +35,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { }) : _addresses = ObservableList.of((initialAddresses ?? []).toSet()), primarySilentAddress = silentAddress, addressesByReceiveType = - ObservableList.of(([]).toSet()), + ObservableList.of(([]).toSet()), receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .toSet()), @@ -408,6 +408,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { updateAddressesByMatch(); } + @action + void addSilentAddresses(Iterable addresses) { + final addressesSet = this.silentAddresses.toSet(); + addressesSet.addAll(addresses); + this.silentAddresses.clear(); + this.silentAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + void _validateSideHdAddresses(List addrWithTransactions) { addrWithTransactions.forEach((element) { if (element.address != diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index ac12055b2..3584b0fe3 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -80,7 +80,7 @@ packages: description: path: "." ref: cake-update-v2 - resolved-ref: be980da9ed063da13db3907ca76d534298bb1d40 + resolved-ref: "7634511b15e5a48bd18e3c19f81971628090c04f" url: "https://github.com/cake-tech/bitcoin_base.git" source: git version: "4.0.0"