mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-03 17:40:43 +00:00
fix: bitcoin_base changes, tx history fixes
This commit is contained in:
parent
53724a2901
commit
ff5e4c1d86
2 changed files with 180 additions and 45 deletions
|
@ -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];
|
||||||
|
|
|
@ -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})
|
||||||
|
|
Loading…
Reference in a new issue