fix: bitcoin_base changes, tx history fixes

This commit is contained in:
Rafael Saes 2023-12-13 15:13:44 -03:00
parent 53724a2901
commit ff5e4c1d86
2 changed files with 180 additions and 45 deletions

View file

@ -354,12 +354,13 @@ abstract class ElectrumWalletBase
var leftAmount = totalAmount; var leftAmount = totalAmount;
var totalInputAmount = 0; var totalInputAmount = 0;
final txb = bitcoin.TransactionBuilder(network: networkType, version: 1); final List<bitcoin.UtxoWithOwner> utxos = [];
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<bitcoin.Script> scriptPubKeys = [];
final List<bitcoin.ECPair> keyPairs = [];
final curve = bitcoin.getSecp256k1(); final curve = bitcoin.getSecp256k1();
@ -371,8 +372,9 @@ abstract class ElectrumWalletBase
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));
bitcoin.BitcoinAddress? address;
bitcoin.AddressType scriptType = bitcoin.AddressType.p2pkh;
Uint8List? script; 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) { if (utx.bitcoinAddressRecord.silentPaymentTweak != null) {
@ -381,39 +383,43 @@ abstract class ElectrumWalletBase
.tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!.fromHex.bigint)!; .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!.fromHex.bigint)!;
inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true)); inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true));
address = bitcoin.P2trAddress(address: utx.address, networkType: networkType);
keyPair = bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex, keyPairs.add(bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex,
compressed: true, network: networkType); compressed: true, network: networkType));
scriptType = bitcoin.AddressType.p2tr;
script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType) script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType)
.scriptPubkey .scriptPubkey
.toBytes(); .toBytes();
} else if ((utx.type == bitcoin.AddressType.p2tr) || } else if ((utx.type == bitcoin.AddressType.p2tr) ||
bitcoin.P2trAddress.REGEX.hasMatch(utx.address)) { bitcoin.P2trAddress.REGEX.hasMatch(utx.address)) {
keyPair = generateKeyPair( keyPairs.add(generateKeyPair(
hd: utx.bitcoinAddressRecord.isHidden hd: utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd ? walletAddresses.sideHd
: walletAddresses.mainHd, : 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), true)); bitcoin.PrivateKey.fromHex(curve, keyPairs[i].privateKey!.hex), true));
address = bitcoin.P2trAddress(address: utx.address, networkType: networkType);
script = bitcoin.P2trAddress(pubkey: keyPair.publicKey.hex, networkType: networkType) scriptType = bitcoin.AddressType.p2tr;
script = bitcoin.P2trAddress(pubkey: keyPairs[i].publicKey.hex, networkType: networkType)
.scriptPubkey .scriptPubkey
.toBytes(); .toBytes();
} else { } else {
keyPair = generateKeyPair( keyPairs.add(generateKeyPair(
hd: utx.bitcoinAddressRecord.isHidden hd: utx.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd ? walletAddresses.sideHd
: walletAddresses.mainHd, : 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), false)); bitcoin.PrivateKey.fromHex(curve, keyPairs[i].privateKey!.hex), false));
if (utx.isP2wpkh) { if (utx.isP2wpkh) {
address = bitcoin.P2wpkhAddress(address: utx.address, networkType: networkType);
scriptType = bitcoin.AddressType.p2wpkh;
final p2wpkh = bitcoin final p2wpkh = bitcoin
.P2WPKH( .P2WPKH(
data: generatePaymentData( data: generatePaymentData(
@ -425,18 +431,33 @@ abstract class ElectrumWalletBase
.data; .data;
script = p2wpkh.output; script = p2wpkh.output;
} else {
address = bitcoin.P2pkhAddress(address: utx.address, networkType: networkType);
} }
} }
txb.addInput(utx.hash, utx.vout, null, script, keyPair, utx.value); utxos.add(bitcoin.UtxoWithOwner(
if (script != null) scriptPubKeys.add(script); utxo: bitcoin.BitcoinUtxo(
txHash: utx.hash,
vout: utx.vout,
value: BigInt.from(utx.value),
scriptType: scriptType,
blockHeight: 0,
),
ownerDetails: bitcoin.UtxoOwnerDetails(
publicKey: keyPairs[i].publicKey.hex,
address: address,
),
));
if (script != null) scriptPubKeys.add(bitcoin.Script.fromRaw(byteData: script));
if (leftAmount <= 0) { if (leftAmount <= 0) {
break; break;
} }
} }
if (txb.inputs.isEmpty) { if (utxos.isEmpty) {
throw BitcoinTransactionNoInputsException(); throw BitcoinTransactionNoInputsException();
} }
@ -444,6 +465,8 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
} }
final List<bitcoin.BitcoinOutputDetails> outs = [];
List<bitcoin.SilentPaymentDestination> silentPaymentDestinations = []; List<bitcoin.SilentPaymentDestination> silentPaymentDestinations = [];
outputs.forEach((item) { outputs.forEach((item) {
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
@ -455,7 +478,20 @@ abstract class ElectrumWalletBase
.add(bitcoin.SilentPaymentDestination.fromAddress(outputAddress, outputAmount!)); .add(bitcoin.SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
} else { } else {
// Add all non-silent payment destinations to the transaction // Add all non-silent payment destinations to the transaction
txb.addOutput(addressToOutputScript(outputAddress, networkType), outputAmount!); final address = bitcoin.P2pkhAddress.REGEX.hasMatch(outputAddress)
? bitcoin.P2pkhAddress(address: outputAddress, networkType: networkType)
: bitcoin.P2wpkhAddress.REGEX.hasMatch(outputAddress)
? bitcoin.P2wpkhAddress(address: outputAddress, networkType: networkType)
: bitcoin.P2trAddress.REGEX.hasMatch(outputAddress)
? bitcoin.P2trAddress(address: outputAddress, networkType: networkType)
: null;
if (address != null) {
outs.add(bitcoin.BitcoinOutputDetails(
address: address,
value: BigInt.from(outputAmount!),
));
}
} }
}); });
@ -466,18 +502,21 @@ abstract class ElectrumWalletBase
generatedOutputs.forEach((recipientSilentAddress, generatedOutput) { generatedOutputs.forEach((recipientSilentAddress, generatedOutput) {
generatedOutput.forEach((output) { generatedOutput.forEach((output) {
txb.addOutput( outs.add(bitcoin.BitcoinOutputDetails(
bitcoin.P2trAddress( address: bitcoin.P2trAddress(
program: bitcoin.ECPublic.fromHex(output.$1.toHex()).toTapPoint(), program: bitcoin.ECPublic.fromHex(output.$1.toHex()).toTapPoint(),
networkType: networkType) networkType: networkType),
.scriptPubkey value: BigInt.from(output.$2),
.toBytes(), ));
output.$2);
}); });
}); });
} }
final estimatedSize = estimatedTransactionSize(txb.inputs.length, outputs.length + 1); if (outs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final estimatedSize = estimatedTransactionSize(inputs.length, outputs.length + 1);
var feeAmount = 0; var feeAmount = 0;
if (transactionCredentials.feeRate != null) { if (transactionCredentials.feeRate != null) {
@ -489,21 +528,48 @@ abstract class ElectrumWalletBase
final changeValue = totalInputAmount - amount - feeAmount; final changeValue = totalInputAmount - amount - feeAmount;
if (changeValue > minAmount) { if (changeValue > minAmount) {
txb.addOutput(changeAddress, changeValue); outs.add(bitcoin.BitcoinOutputDetails(
address: bitcoin.P2wpkhAddress(address: changeAddress, networkType: networkType),
value: BigInt.from(changeValue),
));
} }
for (var i = 0; i < txb.inputs.length; i++) { final transactionBuilder = bitcoin.BitcoinTransactionBuilder(
txb.sign(vin: i, amounts: amounts, scriptPubKeys: scriptPubKeys, inputs: txb.inputs); outputs: outs,
} utxos: utxos,
fee: BigInt.from(feeAmount),
network: networkType,
);
return PendingBitcoinTransaction(txb.build(), type, final transaction = transactionBuilder.buildTransaction((trDigest, utxo, publicKey) {
final key = keyPairs.firstWhereOrNull((element) => element.publicKey.hex == publicKey);
if (key == null) {
throw Exception("Cannot find private key");
}
if (utxo.utxo.isP2tr()) {
return key
.signTapRoot(trDigest,
scripts: [
bitcoin.Script(
script: [bitcoin.ECPublic.fromHex(publicKey).toTapPoint(), 'OP_CHECKSIG'])
],
tweak: false)
.hex;
} else {
return key.sign(trDigest).hex;
}
}, scriptPubKeys);
return PendingBtcTransaction(transaction, 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) { for (final input in inputs) {
final unspent = unspentCoins.firstWhereOrNull((utx) => final unspent = unspentCoins.firstWhereOrNull((utx) =>
utx.hash.contains(HEX.encode(input.hash!.reversed.toList())) && utx.hash.contains(HEX.encode(input.hash.fromHex.reversed.toList())) &&
utx.vout == input.index); utx.vout == input.vout);
if (unspent != null) { if (unspent != null) {
unspentCoins.remove(unspent); unspentCoins.remove(unspent);
} }
@ -801,6 +867,13 @@ abstract class ElectrumWalletBase
updatedConf.confirmations = walletInfo.restoreHeight - tx.height; updatedConf.confirmations = walletInfo.restoreHeight - tx.height;
acc[tx.id] = updatedConf; acc[tx.id] = updatedConf;
} }
if (tx.confirmations == 0 && transactionHistory.transactions[tx.id] != null) {
final updatedConf = acc[tx.id]!;
updatedConf.confirmations = transactionHistory.transactions[tx.id]!.confirmations;
updatedConf.isPending = transactionHistory.transactions[tx.id]!.isPending;
acc[tx.id] = updatedConf;
}
return acc; return acc;
}); });
} }
@ -1160,18 +1233,29 @@ Future<void> startRefresh(ScanData scanData) async {
for (var i = 0; i < (tx["vin"] as List<dynamic>).length; i++) { for (var i = 0; i < (tx["vin"] as List<dynamic>).length; i++) {
final input = tx["vin"][i]; final input = tx["vin"][i];
if (input["witness"] == null) { final prevout = input["prevout"];
skip = true; final scriptPubkeyType = prevout["scriptpubkey_type"];
// print("Skipping, no witness"); String? pubkey;
break;
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;
}
} }
String? pubkey; if (scriptPubkeyType == "p2pkh") {
if (input["witness"].length == 2) { pubkey = bitcoin.P2pkhAddress(
pubkey = input["witness"][1] as String; scriptSig: bitcoin.Script.fromRaw(hexData: input["scriptsig"] as String))
} else if (input["witness"].length == 1) { .pubkey;
pubkey =
"03" + (input["prevout"]["scriptpubkey"] as String).fromHex.sublist(2).hex;
} }
if (pubkey == null) { if (pubkey == null) {
@ -1246,7 +1330,6 @@ Future<void> startRefresh(ScanData scanData) async {
} else { } else {
print("UNSPENT COIN FOUND!"); print("UNSPENT COIN FOUND!");
} }
print(result);
result.forEach((key, value) async { result.forEach((key, value) async {
final outpoint = outpointsByP2TRpubkey[key]; final outpoint = outpointsByP2TRpubkey[key];

View file

@ -7,6 +7,58 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
class PendingBtcTransaction with PendingTransaction {
PendingBtcTransaction(this._tx, this.type,
{required this.electrumClient, required this.amount, required this.fee, this.networkType})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
final bitcoin.BtcTransaction _tx;
final ElectrumClient electrumClient;
final int amount;
final int fee;
final bitcoin.NetworkType? networkType;
@override
String get id => _tx.txId();
@override
String get hex => _tx.serialize();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
final result =
await electrumClient.broadcastTransaction(transactionRaw: hex, networkType: networkType);
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
}
_listeners?.forEach((listener) => listener(transactionInfo()));
}
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
id: id,
height: 0,
amount: amount,
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
confirmations: 0,
fee: fee);
}
class PendingBitcoinTransaction with PendingTransaction { class PendingBitcoinTransaction with PendingTransaction {
PendingBitcoinTransaction(this._tx, this.type, PendingBitcoinTransaction(this._tx, this.type,
{required this.electrumClient, required this.amount, required this.fee, this.networkType}) {required this.electrumClient, required this.amount, required this.fee, this.networkType})