fix: unspents remaining, check spent silent payment during scanning

This commit is contained in:
Rafael Saes 2023-12-11 19:41:48 -03:00
parent c472ef0e07
commit 2d9158d552
6 changed files with 165 additions and 133 deletions

View file

@ -4,7 +4,9 @@ import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) { String addressFromOutput(Uint8List script, bitcoin.NetworkType networkType) {
try { 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 (_) {} } catch (_) {}
try { try {

View file

@ -1,5 +1,4 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; 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/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart';
@ -148,9 +147,10 @@ class ElectrumTransactionInfo extends TransactionInfo {
if (addresses != null) { if (addresses != null) {
tx.outs.forEach((out) { tx.outs.forEach((out) {
try { try {
final p2pkh = final p2pkh = bitcoin.P2pkhAddress(
bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin); scriptPubKey: bitcoin.Script.fromRaw(byteData: out.script),
exist = addresses.contains(p2pkh.data.address); networkType: bitcoin.bitcoin);
exist = addresses.contains(p2pkh.address);
if (exist) { if (exist) {
amount += out.value!; amount += out.value!;
@ -236,4 +236,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{}; m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
return m; 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)';
}
} }

View file

@ -328,9 +328,7 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
} }
amount = output.sendAll || allAmount - credentialsAmount < minAmount amount = output.sendAll ? allAmount : credentialsAmount;
? allAmount
: credentialsAmount;
if (output.sendAll || amount == allAmount) { if (output.sendAll || amount == allAmount) {
fee = allAmountFee; fee = allAmountFee;
@ -360,9 +358,8 @@ abstract class ElectrumWalletBase
List<bitcoin.PrivateKeyInfo> inputPrivKeys = []; List<bitcoin.PrivateKeyInfo> inputPrivKeys = [];
List<bitcoin.Outpoint> outpoints = []; List<bitcoin.Outpoint> outpoints = [];
List<int> amounts = [];
List<int>? amounts; List<Uint8List> scriptPubKeys = [];
List<Uint8List>? scriptPubKeys;
final curve = bitcoin.getSecp256k1(); final curve = bitcoin.getSecp256k1();
@ -371,41 +368,28 @@ abstract class ElectrumWalletBase
leftAmount = leftAmount - utx.value; leftAmount = leftAmount - utx.value;
totalInputAmount += utx.value; totalInputAmount += utx.value;
if (amounts == null) {
amounts = [];
}
amounts.add(utx.value); amounts.add(utx.value);
outpoints.add(bitcoin.Outpoint(txid: utx.hash, index: utx.vout)); outpoints.add(bitcoin.Outpoint(txid: utx.hash, index: utx.vout));
if (utx.bitcoinAddressRecord.silentPaymentTweak != null) { Uint8List? script;
bitcoin.ECPair? keyPair;
// https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending // https://github.com/bitcoin/bips/blob/c55f80c53c98642357712c1839cfdc0551d531c4/bip-0352.mediawiki#user-content-Spending
if (utx.bitcoinAddressRecord.silentPaymentTweak != null) {
final d = bitcoin.PrivateKey.fromHex( final d = bitcoin.PrivateKey.fromHex(
curve, walletAddresses.primarySilentAddress!.spendPrivkey.toCompressedHex()) curve, walletAddresses.primarySilentAddress!.spendPrivkey.toCompressedHex())
.tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!.fromHex.bigint)!; .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); keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex,
bitcoin.ECPair keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex,
compressed: true, network: networkType); compressed: true, network: networkType);
script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType)
final script = p2tr.toScriptPubKey().toBytes(); .scriptPubkey
.toBytes();
txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); } else if ((utx.type == bitcoin.AddressType.p2tr) ||
if (scriptPubKeys == null) {
scriptPubKeys = [];
}
scriptPubKeys.add(script);
continue;
}
if ((utx.type == bitcoin.AddressType.p2tr) ||
bitcoin.P2trAddress.REGEX.hasMatch(utx.address)) { bitcoin.P2trAddress.REGEX.hasMatch(utx.address)) {
bitcoin.ECPair keyPair = generateKeyPair( keyPair = generateKeyPair(
hd: utx.bitcoinAddressRecord.isHidden hd: utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd ? walletAddresses.sideHd
: walletAddresses.mainHd, : walletAddresses.mainHd,
@ -413,30 +397,21 @@ abstract class ElectrumWalletBase
network: networkType); network: networkType);
inputPrivKeys.add(bitcoin.PrivateKeyInfo( inputPrivKeys.add(bitcoin.PrivateKeyInfo(
bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), true));
utx.type == bitcoin.AddressType.p2tr));
final p2tr = bitcoin.P2trAddress(pubkey: keyPair.publicKey.hex, network: networkType); script = bitcoin.P2trAddress(pubkey: keyPair.publicKey.hex, networkType: networkType)
final script = p2tr.toScriptPubKey().toBytes(); .scriptPubkey
.toBytes();
txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); } else {
keyPair = generateKeyPair(
if (scriptPubKeys == null) { hd: utx.bitcoinAddressRecord.isHidden
scriptPubKeys = []; ? walletAddresses.sideHd
} : walletAddresses.mainHd,
scriptPubKeys.add(script);
continue;
}
bitcoin.ECPair keyPair = generateKeyPair(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index, index: utx.bitcoinAddressRecord.index,
network: networkType); network: networkType);
inputPrivKeys.add(bitcoin.PrivateKeyInfo( inputPrivKeys.add(bitcoin.PrivateKeyInfo(
bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), bitcoin.PrivateKey.fromHex(curve, keyPair.privateKey!.hex), false));
utx.type == bitcoin.AddressType.p2tr));
if (utx.isP2wpkh) { if (utx.isP2wpkh) {
final p2wpkh = bitcoin final p2wpkh = bitcoin
@ -449,19 +424,13 @@ abstract class ElectrumWalletBase
network: networkType) network: networkType)
.data; .data;
final script = p2wpkh.output; script = p2wpkh.output;
}
}
txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value);
if (scriptPubKeys == null) {
scriptPubKeys = [];
}
if (script != null) scriptPubKeys.add(script); if (script != null) scriptPubKeys.add(script);
continue;
}
txb.addInput(utx.hash, utx.vout, null, null, keyPair, utx.value);
if (leftAmount <= 0) { if (leftAmount <= 0) {
break; break;
} }
@ -500,21 +469,21 @@ abstract class ElectrumWalletBase
txb.addOutput( txb.addOutput(
bitcoin.P2trAddress( bitcoin.P2trAddress(
program: bitcoin.ECPublic.fromHex(output.$1.toHex()).toTapPoint(), program: bitcoin.ECPublic.fromHex(output.$1.toHex()).toTapPoint(),
network: networkType) networkType: networkType)
.toScriptPubKey() .scriptPubkey
.toBytes(), .toBytes(),
output.$2); output.$2);
}); });
}); });
} }
final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1); final estimatedSize = estimatedTransactionSize(txb.inputs.length, outputs.length + 1);
var feeAmount = 0; var feeAmount = 0;
if (transactionCredentials.feeRate != null) { if (transactionCredentials.feeRate != null) {
feeAmount = transactionCredentials.feeRate! * estimatedSize; feeAmount = transactionCredentials.feeRate! * estimatedSize;
} else { } else {
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize; feeAmount = (feeRate(transactionCredentials.priority!) * estimatedSize).toInt();
} }
final changeValue = totalInputAmount - amount - feeAmount; final changeValue = totalInputAmount - amount - feeAmount;
@ -523,15 +492,25 @@ abstract class ElectrumWalletBase
txb.addOutput(changeAddress, changeValue); txb.addOutput(changeAddress, changeValue);
} }
for (var i = 0; i < inputs.length; i++) { for (var i = 0; i < txb.inputs.length; i++) {
txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: inputs); txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: txb.inputs);
} }
return PendingBitcoinTransaction(txb.build(), type, return PendingBitcoinTransaction(txb.build(), type,
electrumClient: electrumClient, amount: amount, fee: fee, networkType: networkType) electrumClient: electrumClient, amount: amount, fee: fee, networkType: networkType)
..addListener((transaction) async { ..addListener((transaction) async {
transactionHistory.addOne(transaction); 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 updateBalance();
await updateTransactions();
}); });
} catch (e, stacktrace) { } catch (e, stacktrace) {
print(stacktrace); print(stacktrace);
@ -554,6 +533,10 @@ abstract class ElectrumWalletBase
int feeRate(TransactionPriority priority) { int feeRate(TransactionPriority priority) {
try { try {
if (priority is BitcoinTransactionPriority) { if (priority is BitcoinTransactionPriority) {
if (networkType.bech32 == bitcoin.testnet.bech32 &&
priority == BitcoinTransactionPriority.fast) {
return 2;
}
return _feeRates[priority.raw]; return _feeRates[priority.raw];
} }
@ -1121,7 +1104,7 @@ Future<void> startRefresh(ScanData scanData) async {
return; return;
} }
print(["Scanning from height:", syncHeight]); // print(["Scanning from height:", syncHeight]);
try { try {
final networkPath = final networkPath =
@ -1183,13 +1166,20 @@ Future<void> startRefresh(ScanData scanData) async {
break; 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; skip = true;
// print("Skipping, invalid witness"); // print("Skipping, invalid witness");
break; break;
} }
final pubkey = input["witness"][1] as String;
pubkeys.add(pubkey); pubkeys.add(pubkey);
outpoints.add( outpoints.add(
bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int)); bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int));
@ -1220,8 +1210,9 @@ Future<void> startRefresh(ScanData scanData) async {
// break; // break;
// } // }
final p2tr = bitcoin.P2trAddress(program: script.sublist(2).hex); final p2tr = bitcoin.P2trAddress(
final address = p2tr.toAddress(scanData.networkType); program: script.sublist(2).hex, networkType: scanData.networkType);
final address = p2tr.address;
print(["Verifying taproot address:", address]); print(["Verifying taproot address:", address]);
@ -1236,13 +1227,9 @@ Future<void> startRefresh(ScanData scanData) async {
final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints); final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
final curve = bitcoin.getSecp256k1();
final result = bitcoin.scanOutputs( final result = bitcoin.scanOutputs(
bitcoin.PrivateKey.fromHex( scanData.primarySilentAddress.scanPrivkey,
curve, scanData.primarySilentAddress.scanPrivkey.toCompressedHex()), scanData.primarySilentAddress.spendPubkey,
bitcoin.PublicKey.fromHex(
curve, scanData.primarySilentAddress.spendPubkey.toCompressedHex()),
bitcoin.getSumInputPubKeys(pubkeys), bitcoin.getSumInputPubKeys(pubkeys),
outpointHash, outpointHash,
outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(), outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(),
@ -1261,7 +1248,7 @@ Future<void> startRefresh(ScanData scanData) async {
} }
print(result); print(result);
result.forEach((key, value) { result.forEach((key, value) async {
final outpoint = outpointsByP2TRpubkey[key]; final outpoint = outpointsByP2TRpubkey[key];
if (outpoint == null) { if (outpoint == null) {
@ -1272,10 +1259,73 @@ Future<void> startRefresh(ScanData scanData) async {
String? label; String? label;
if (value.length > 1) label = value[1]; 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<dynamic>;
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<dynamic>)) {
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( final unspent = BitcoinUnspent(
BitcoinAddressRecord( BitcoinAddressRecord(
bitcoin.P2trAddress(program: key, network: scanData.networkType) bitcoin.P2trAddress(program: key, networkType: scanData.networkType).address,
.toAddress(scanData.networkType),
index: 0, index: 0,
isHidden: true, isHidden: true,
isUsed: true, isUsed: true,
@ -1294,28 +1344,8 @@ Future<void> startRefresh(ScanData scanData) async {
scanData.sendPort.send(unspent); scanData.sendPort.send(unspent);
// also send tx data for tx history // also send tx data for tx history
scanData.sendPort.send({ txInfo.unspent = unspent;
txid: ElectrumTransactionInfo( scanData.sendPort.send({txid: txInfo});
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,
)
});
}); });
} catch (_) {} } catch (_) {}
} }
@ -1334,6 +1364,7 @@ Future<void> startRefresh(ScanData scanData) async {
print(stacktrace); print(stacktrace);
print(e.toString()); print(e.toString());
scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus()));
break; break;
} }
} }

View file

@ -15,22 +15,16 @@ String generateP2WPKHAddress(
{required bitcoin.HDWallet hd, {required bitcoin.HDWallet hd,
required int index, required int index,
required bitcoin.NetworkType networkType}) => 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( String generateP2PKHAddress(
{required bitcoin.HDWallet hd, {required bitcoin.HDWallet hd,
required int index, required int index,
required bitcoin.NetworkType networkType}) => required bitcoin.NetworkType networkType}) =>
bitcoin bitcoin.P2pkhAddress(pubkey: hd.derive(index).pubKey!, networkType: networkType).address;
.P2PKH(
data: PaymentData(pubkey: Uint8List.fromList(HEX.decode(hd.derive(index).pubKey!))),
network: networkType)
.data
.address!;
String generateP2TRAddress( String generateP2TRAddress(
{required bitcoin.HDWallet hd, {required bitcoin.HDWallet hd,
required int index, required int index,
required bitcoin.NetworkType networkType}) => required bitcoin.NetworkType networkType}) =>
bitcoin.P2trAddress(pubkey: hd.derive(index).pubKey!, network: networkType) bitcoin.P2trAddress(pubkey: hd.derive(index).pubKey!, networkType: networkType).address;
.toAddress(networkType);

View file

@ -296,7 +296,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
state = IsExecutingState(); state = IsExecutingState();
pendingTransaction = await wallet.createTransaction(_credentials()); pendingTransaction = await wallet.createTransaction(_credentials());
state = ExecutedSuccessfullyState(); state = ExecutedSuccessfullyState();
} catch (e) { } catch (e, s) {
print(s);
state = FailureState(e.toString()); state = FailureState(e.toString());
} }
} }
@ -338,7 +339,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
} }
state = TransactionCommitted(); state = TransactionCommitted();
} catch (e) { } catch (e, s) {
print(s);
String translatedError = translateErrorMessage(e.toString(), wallet.type, wallet.currency); String translatedError = translateErrorMessage(e.toString(), wallet.type, wallet.currency);
state = FailureState(translatedError); state = FailureState(translatedError);
} }

View file

@ -61,12 +61,11 @@ abstract class UnspentCoinsListViewModelBase with Store {
UnspentCoinsInfo getUnspentCoinInfo( UnspentCoinsInfo getUnspentCoinInfo(
String hash, String address, int value, int vout, String? keyImage) => String hash, String address, int value, int vout, String? keyImage) =>
_unspentCoinsInfo.values.firstWhere((element) { _unspentCoinsInfo.values.firstWhere((element) {
print([ element.address, address ]);
return element.walletId == wallet.id && return element.walletId == wallet.id &&
element.hash == hash && element.hash == hash &&
element.address == address && element.address == address &&
// element.value == value && element.value == value &&
// element.vout == vout && element.vout == vout &&
element.keyImage == keyImage; element.keyImage == keyImage;
}); });