From 2d9158d5522ba9850bd91a6ab2a5285fca064abf Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Mon, 11 Dec 2023 19:41:48 -0300 Subject: [PATCH] fix: unspents remaining, check spent silent payment during scanning --- cw_bitcoin/lib/address_from_output.dart | 4 +- cw_bitcoin/lib/electrum_transaction_info.dart | 12 +- cw_bitcoin/lib/electrum_wallet.dart | 259 ++++++++++-------- cw_bitcoin/lib/utils.dart | 12 +- lib/view_model/send/send_view_model.dart | 6 +- .../unspent_coins_list_view_model.dart | 5 +- 6 files changed, 165 insertions(+), 133 deletions(-) diff --git a/cw_bitcoin/lib/address_from_output.dart b/cw_bitcoin/lib/address_from_output.dart index 1013eca72..2d6266ca2 100644 --- a/cw_bitcoin/lib/address_from_output.dart +++ b/cw_bitcoin/lib/address_from_output.dart @@ -4,7 +4,9 @@ import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { try { - return bitcoin.P2PKH(data: PaymentData(output: script), network: networkType).data.address!; + return bitcoin.P2pkhAddress( + scriptPubKey: bitcoin.Script.fromRaw(byteData: script), networkType: networkType) + .address; } catch (_) {} try { diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 32c7f1a56..9640d6701 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,5 +1,4 @@ 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'; @@ -148,9 +147,10 @@ class ElectrumTransactionInfo extends TransactionInfo { 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); + final p2pkh = bitcoin.P2pkhAddress( + scriptPubKey: bitcoin.Script.fromRaw(byteData: out.script), + networkType: bitcoin.bitcoin); + exist = addresses.contains(p2pkh.address); if (exist) { amount += out.value!; @@ -236,4 +236,8 @@ class ElectrumTransactionInfo extends TransactionInfo { 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 208219fd1..1b349cc00 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -328,9 +328,7 @@ abstract class ElectrumWalletBase throw BitcoinTransactionWrongBalanceException(currency); } - amount = output.sendAll || allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; + amount = output.sendAll ? allAmount : credentialsAmount; if (output.sendAll || amount == allAmount) { fee = allAmountFee; @@ -360,9 +358,8 @@ abstract class ElectrumWalletBase List inputPrivKeys = []; List outpoints = []; - - List? amounts; - List? scriptPubKeys; + List amounts = []; + List scriptPubKeys = []; final curve = bitcoin.getSecp256k1(); @@ -371,41 +368,28 @@ abstract class ElectrumWalletBase leftAmount = leftAmount - utx.value; totalInputAmount += utx.value; - if (amounts == null) { - amounts = []; - } amounts.add(utx.value); - outpoints.add(bitcoin.Outpoint(txid: utx.hash, index: utx.vout)); + Uint8List? script; + bitcoin.ECPair? keyPair; + + // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending if (utx.bitcoinAddressRecord.silentPaymentTweak != null) { - // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending final d = bitcoin.PrivateKey.fromHex( curve, walletAddresses.primarySilentAddress!.spendPrivkey.toCompressedHex()) .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!.fromHex.bigint)!; - inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, utx.type == bitcoin.AddressType.p2tr)); + inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true)); - final p2tr = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), network: networkType); - - bitcoin.ECPair keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex, + keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex, compressed: true, network: networkType); - - final script = p2tr.toScriptPubKey().toBytes(); - - txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); - - if (scriptPubKeys == null) { - scriptPubKeys = []; - } - scriptPubKeys.add(script); - - continue; - } - - if ((utx.type == bitcoin.AddressType.p2tr) || + script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType) + .scriptPubkey + .toBytes(); + } else if ((utx.type == bitcoin.AddressType.p2tr) || bitcoin.P2trAddress.REGEX.hasMatch(utx.address)) { - bitcoin.ECPair keyPair = generateKeyPair( + keyPair = generateKeyPair( hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, @@ -413,54 +397,39 @@ abstract class ElectrumWalletBase network: networkType); inputPrivKeys.add(bitcoin.PrivateKeyInfo( - bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), - utx.type == bitcoin.AddressType.p2tr)); + bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), true)); - final p2tr = bitcoin.P2trAddress(pubkey: keyPair.publicKey.hex, network: networkType); - final script = p2tr.toScriptPubKey().toBytes(); + script = bitcoin.P2trAddress(pubkey: keyPair.publicKey.hex, networkType: networkType) + .scriptPubkey + .toBytes(); + } else { + keyPair = generateKeyPair( + hd: utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index, + network: networkType); - txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); + inputPrivKeys.add(bitcoin.PrivateKeyInfo( + bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), false)); - if (scriptPubKeys == null) { - scriptPubKeys = []; + if (utx.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData( + hd: utx.bitcoinAddressRecord.isHidden + ? walletAddresses.sideHd + : walletAddresses.mainHd, + index: utx.bitcoinAddressRecord.index), + network: networkType) + .data; + + script = p2wpkh.output; } - scriptPubKeys.add(script); - - continue; } - bitcoin.ECPair keyPair = generateKeyPair( - hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, - index: utx.bitcoinAddressRecord.index, - network: networkType); - - inputPrivKeys.add(bitcoin.PrivateKeyInfo( - bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), - utx.type == bitcoin.AddressType.p2tr)); - - if (utx.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData( - hd: utx.bitcoinAddressRecord.isHidden - ? walletAddresses.sideHd - : walletAddresses.mainHd, - index: utx.bitcoinAddressRecord.index), - network: networkType) - .data; - - final script = p2wpkh.output; - txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); - - if (scriptPubKeys == null) { - scriptPubKeys = []; - } - if (script != null) scriptPubKeys.add(script); - - continue; - } - - txb.addInput(utx.hash, utx.vout, null, null, keyPair, utx.value); + txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); + if (script != null) scriptPubKeys.add(script); if (leftAmount <= 0) { break; @@ -500,21 +469,21 @@ abstract class ElectrumWalletBase txb.addOutput( bitcoin.P2trAddress( program: bitcoin.ECPublic.fromHex(output.$1.toHex()).toTapPoint(), - network: networkType) - .toScriptPubKey() + networkType: networkType) + .scriptPubkey .toBytes(), output.$2); }); }); } - final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1); + final estimatedSize = estimatedTransactionSize(txb.inputs.length, outputs.length + 1); var feeAmount = 0; if (transactionCredentials.feeRate != null) { feeAmount = transactionCredentials.feeRate! * estimatedSize; } else { - feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; + feeAmount = (feeRate(transactionCredentials.priority!) * estimatedSize).toInt(); } final changeValue = totalInputAmount - amount - feeAmount; @@ -523,15 +492,25 @@ abstract class ElectrumWalletBase txb.addOutput(changeAddress, changeValue); } - for (var i = 0; i < inputs.length; i++) { - txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: inputs); + for (var i = 0; i < txb.inputs.length; i++) { + txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: txb.inputs); } return PendingBitcoinTransaction(txb.build(), type, electrumClient: electrumClient, amount: amount, fee: fee, networkType: networkType) ..addListener((transaction) async { transactionHistory.addOne(transaction); + for (final input in txb.inputs) { + final unspent = unspentCoins.firstWhereOrNull((utx) => + utx.hash.contains(HEX.encode(input.hash!.reversed.toList())) && + utx.vout == input.index); + if (unspent != null) { + unspentCoins.remove(unspent); + } + } + await updateUnspent(); await updateBalance(); + await updateTransactions(); }); } catch (e, stacktrace) { print(stacktrace); @@ -554,6 +533,10 @@ abstract class ElectrumWalletBase int feeRate(TransactionPriority priority) { try { if (priority is BitcoinTransactionPriority) { + if (networkType.bech32 == bitcoin.testnet.bech32 && + priority == BitcoinTransactionPriority.fast) { + return 2; + } return _feeRates[priority.raw]; } @@ -1121,7 +1104,7 @@ Future startRefresh(ScanData scanData) async { return; } - print(["Scanning from height:", syncHeight]); + // print(["Scanning from height:", syncHeight]); try { final networkPath = @@ -1183,13 +1166,20 @@ Future startRefresh(ScanData scanData) async { break; } - if (input["witness"].length != 2) { + String? pubkey; + if (input["witness"].length == 2) { + pubkey = input["witness"][1] as String; + } else if (input["witness"].length == 1) { + pubkey = + "03" + (input["prevout"]["scriptpubkey"] as String).fromHex.sublist(2).hex; + } + + if (pubkey == null) { skip = true; // print("Skipping, invalid witness"); break; } - final pubkey = input["witness"][1] as String; pubkeys.add(pubkey); outpoints.add( bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int)); @@ -1220,8 +1210,9 @@ Future startRefresh(ScanData scanData) async { // break; // } - final p2tr = bitcoin.P2trAddress(program: script.sublist(2).hex); - final address = p2tr.toAddress(scanData.networkType); + final p2tr = bitcoin.P2trAddress( + program: script.sublist(2).hex, networkType: scanData.networkType); + final address = p2tr.address; print(["Verifying taproot address:", address]); @@ -1236,13 +1227,9 @@ Future startRefresh(ScanData scanData) async { final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); - final curve = bitcoin.getSecp256k1(); - final result = bitcoin.scanOutputs( - bitcoin.PrivateKey.fromHex( - curve, scanData.primarySilentAddress.scanPrivkey.toCompressedHex()), - bitcoin.PublicKey.fromHex( - curve, scanData.primarySilentAddress.spendPubkey.toCompressedHex()), + scanData.primarySilentAddress.scanPrivkey, + scanData.primarySilentAddress.spendPubkey, bitcoin.getSumInputPubKeys(pubkeys), outpointHash, outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(), @@ -1261,7 +1248,7 @@ Future startRefresh(ScanData scanData) async { } print(result); - result.forEach((key, value) { + result.forEach((key, value) async { final outpoint = outpointsByP2TRpubkey[key]; if (outpoint == null) { @@ -1272,10 +1259,73 @@ Future startRefresh(ScanData scanData) async { 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, network: scanData.networkType) - .toAddress(scanData.networkType), + bitcoin.P2trAddress(program: key, networkType: scanData.networkType).address, index: 0, isHidden: true, isUsed: true, @@ -1294,28 +1344,8 @@ Future startRefresh(ScanData scanData) async { scanData.sendPort.send(unspent); // also send tx data for tx history - scanData.sendPort.send({ - txid: 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: unspent, - ) - }); + txInfo.unspent = unspent; + scanData.sendPort.send({txid: txInfo}); }); } catch (_) {} } @@ -1334,6 +1364,7 @@ Future startRefresh(ScanData scanData) async { print(stacktrace); print(e.toString()); + scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus())); break; } } diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index 2c58c08e1..8165e8034 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -15,22 +15,16 @@ String generateP2WPKHAddress( {required bitcoin.HDWallet hd, required int index, required bitcoin.NetworkType networkType}) => - bitcoin.P2wpkhAddress(pubkey: hd.derive(index).pubKey!).toAddress(networkType); + bitcoin.P2wpkhAddress(pubkey: hd.derive(index).pubKey!, networkType: networkType).address; String generateP2PKHAddress( {required bitcoin.HDWallet hd, required int index, required bitcoin.NetworkType networkType}) => - bitcoin - .P2PKH( - data: PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))), - network: networkType) - .data - .address!; + bitcoin.P2pkhAddress(pubkey: hd.derive(index).pubKey!, networkType: networkType).address; String generateP2TRAddress( {required bitcoin.HDWallet hd, required int index, required bitcoin.NetworkType networkType}) => - bitcoin.P2trAddress(pubkey: hd.derive(index).pubKey!, network: networkType) - .toAddress(networkType); + bitcoin.P2trAddress(pubkey: hd.derive(index).pubKey!, networkType: networkType).address; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 885e2efe0..526c5ce8d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -296,7 +296,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = IsExecutingState(); pendingTransaction = await wallet.createTransaction(_credentials()); state = ExecutedSuccessfullyState(); - } catch (e) { + } catch (e, s) { + print(s); state = FailureState(e.toString()); } } @@ -338,7 +339,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } state = TransactionCommitted(); - } catch (e) { + } catch (e, s) { + print(s); String translatedError = translateErrorMessage(e.toString(), wallet.type, wallet.currency); state = FailureState(translatedError); } diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 4607661c3..441551ba2 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -61,12 +61,11 @@ abstract class UnspentCoinsListViewModelBase with Store { UnspentCoinsInfo getUnspentCoinInfo( String hash, String address, int value, int vout, String? keyImage) => _unspentCoinsInfo.values.firstWhere((element) { - print([ element.address, address ]); return element.walletId == wallet.id && element.hash == hash && element.address == address && - // element.value == value && - // element.vout == vout && + element.value == value && + element.vout == vout && element.keyImage == keyImage; });