From 023bad0c707cc60b68bb2d4529bdd3382df48327 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Apr 2024 15:48:55 -0600 Subject: [PATCH] WIP: coinlib 2 migration (taproot txns on btc testnet tested working) --- lib/models/signing_data.dart | 25 +++ .../electrumx_interface.dart | 182 ++++++++++++------ 2 files changed, 149 insertions(+), 58 deletions(-) diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart index 0937a2d8b..f510f7845 100644 --- a/lib/models/signing_data.dart +++ b/lib/models/signing_data.dart @@ -11,6 +11,7 @@ import 'dart:typed_data'; import 'package:bitcoindart/bitcoindart.dart'; +// import 'package:coinlib_flutter/coinlib_flutter.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart'; @@ -40,3 +41,27 @@ class SigningData { "}"; } } + +// class SigningData { +// SigningData({ +// required this.derivePathType, +// required this.utxo, +// this.keyPair, +// this.redeemScript, +// }); +// +// final DerivePathType derivePathType; +// final UTXO utxo; +// ({ECPrivateKey privateKey, ECPublicKey publicKey})? keyPair; +// Uint8List? redeemScript; +// +// @override +// String toString() { +// return "SigningData{\n" +// " derivePathType: $derivePathType,\n" +// " utxo: $utxo,\n" +// " keyPair: $keyPair,\n" +// " redeemScript: $redeemScript,\n" +// "}"; +// } +// } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 07e48b7bc..ff8d7e4da 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:typed_data'; import 'package:bip47/src/util.dart'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; @@ -599,7 +600,7 @@ mixin ElectrumXInterface on Bip39HDWallet { // final coinlib.Input input; final pubKey = keys.publicKey.data; - final bitcoindart.PaymentData data; + final bitcoindart.PaymentData? data; switch (sd.derivePathType) { case DerivePathType.bip44: @@ -651,12 +652,20 @@ mixin ElectrumXInterface on Bip39HDWallet { .data; break; + case DerivePathType.bip86: + data = null; + break; + default: throw Exception("DerivePathType unsupported"); } // sd.output = input.script!.compiled; - sd.output = data.output!; + + if (sd.derivePathType != DerivePathType.bip86) { + sd.output = data!.output!; + } + sd.keyPair = bitcoindart.ECPair.fromPrivateKey( keys.privateKey.data, compressed: keys.privateKey.compressed, @@ -680,57 +689,87 @@ mixin ElectrumXInterface on Bip39HDWallet { Logging.instance .log("Starting buildTransaction ----------", level: LogLevel.Info); - // TODO: use coinlib - - // Check if any txData.recipients are taproot/P2TR outputs. - bool hasTaprootOutput = false; - for (final recipient in txData.recipients!) { - if (cryptoCurrency.addressType(address: recipient.address) == - DerivePathType.bip86) { - hasTaprootOutput = true; - } - } - - if (hasTaprootOutput) { - // Use Coinlib to construct taproot transaction. - // TODO [prio=high]: Implement taproot transaction construction. - } - - final txb = bitcoindart.TransactionBuilder( - network: bitcoindart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: bitcoindart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ), - maximumFeeRate: maximumFeerate, - ); - const version = 1; // TODO possibly override this for certain coins? - txb.setVersion(version); - // temp tx data to show in gui while waiting for real data from server final List tempInputs = []; final List tempOutputs = []; + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: 1, // TODO: check if we can use 3 (as is default in coinlib) + inputs: [], + outputs: [], + ); + // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { final txid = utxoSigningData[i].utxo.txid; - txb.addInput( - txid, + + final hash = Uint8List.fromList(txid.fromHex.reversed.toList()); + + final prevOutpoint = coinlib.OutPoint( + hash, utxoSigningData[i].utxo.vout, - null, - utxoSigningData[i].output!, - cryptoCurrency.networkParams.bech32Hrp, ); + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + // publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + sequence: 0xffffffff - 1, + ); + + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + input = coinlib.P2SHMultisigInput( + prevOut: prevOutpoint, + program: coinlib.MultisigProgram.decompile( + utxoSigningData[i].redeemScript!, + ), + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + // publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + sequence: 0xffffffff - 1, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); + tempInputs.add( InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: txb.inputs.first.script?.toHex, + scriptSigHex: input.scriptSig.toHex, scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( @@ -751,12 +790,18 @@ mixin ElectrumXInterface on Bip39HDWallet { // Add transaction output for (var i = 0; i < txData.recipients!.length; i++) { - txb.addOutput( + final address = coinlib.Address.fromString( normalizeAddress(txData.recipients![i].address), - txData.recipients![i].amount.raw.toInt(), - cryptoCurrency.networkParams.bech32Hrp, + cryptoCurrency.networkParams, ); + final output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + + clTx = clTx.addOutput(output); + tempOutputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", @@ -779,13 +824,37 @@ mixin ElectrumXInterface on Bip39HDWallet { try { // Sign the transaction accordingly for (var i = 0; i < utxoSigningData.length; i++) { - txb.sign( - vin: i, - keyPair: utxoSigningData[i].keyPair!, - witnessValue: utxoSigningData[i].utxo.value, - redeemScript: utxoSigningData[i].redeemScript, - overridePrefix: cryptoCurrency.networkParams.bech32Hrp, + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = coinlib.ECPrivateKey( + utxoSigningData[i].keyPair!.privateKey!, + compressed: utxoSigningData[i].keyPair!.compressed, ); + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: coinlib.ECPublicKey( + utxoSigningData[i].keyPair!.publicKey, + ), + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + // key: utxoSigningData[i].keyPair!.privateKey, + key: key, + prevOuts: prevOuts, + ); + + // txb.sign( + // vin: i, + // keyPair: utxoSigningData[i].keyPair!, + // witnessValue: utxoSigningData[i].utxo.value, + // redeemScript: utxoSigningData[i].redeemScript, + // overridePrefix: cryptoCurrency.networkParams.bech32Hrp, + // ); } } catch (e, s) { Logging.instance.log("Caught exception while signing transaction: $e\n$s", @@ -793,22 +862,19 @@ mixin ElectrumXInterface on Bip39HDWallet { rethrow; } - final builtTx = txb.build(cryptoCurrency.networkParams.bech32Hrp); - final vSize = builtTx.virtualSize(); - return txData.copyWith( - raw: builtTx.toHex(), - vSize: vSize, + raw: clTx.toHex(), + vSize: clTx.size, tempTx: TransactionV2( walletId: walletId, blockHash: null, - hash: builtTx.getId(), - txid: builtTx.getId(), + hash: clTx.hashHex, + txid: clTx.txid, height: null, timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(tempInputs), outputs: List.unmodifiable(tempOutputs), - version: version, + version: clTx.version, type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && txData.paynymAccountLite == null