dev
dart 244 lines 8.39 KB
Raw
1 import 'dart:typed_data';
2
3 import 'package:bitcoin_base/bitcoin_base.dart';
4 import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:collection/collection.dart';
6 import 'package:cw_bitcoin/bitcoin_address_record.dart';
7 import 'package:cw_bitcoin/bitcoin_unspent.dart';
8 import 'package:cw_bitcoin/bitcoin_wallet.dart';
9 import 'package:cw_bitcoin/utils.dart';
10 import 'package:ledger_bitcoin/psbt.dart';
11 import 'package:ledger_bitcoin/src/utils/buffer_writer.dart';
12
13 extension PsbtSigner on PsbtV2 {
14 Uint8List extractUnsignedTX({bool getSegwit = true}) {
15 final tx = BufferWriter()..writeUInt32(getGlobalTxVersion());
16
17 final isSegwit = getInputWitnessUtxo(0) != null;
18 if (isSegwit && getSegwit) {
19 tx.writeSlice(Uint8List.fromList([0, 1]));
20 }
21
22 final inputCount = getGlobalInputCount();
23 tx.writeVarInt(inputCount);
24
25 for (var i = 0; i < inputCount; i++) {
26 tx
27 ..writeSlice(getInputPreviousTxid(i))
28 ..writeUInt32(getInputOutputIndex(i))
29 ..writeVarSlice(Uint8List(0))
30 ..writeUInt32(getInputSequence(i));
31 }
32
33 final outputCount = getGlobalOutputCount();
34 tx.writeVarInt(outputCount);
35 for (var i = 0; i < outputCount; i++) {
36 tx.writeUInt64(getOutputAmount(i));
37 tx.writeVarSlice(getOutputScript(i));
38 }
39 tx.writeUInt32(getGlobalFallbackLocktime() ?? 0);
40 return tx.buffer();
41 }
42
43 Future<void> signWithUTXO(List<UtxoWithPrivateKey> utxos, UTXOSignerCallBack signer,
44 [UTXOGetterCallBack? getTaprootPair]) async {
45 final raw = BytesUtils.toHexString(extractUnsignedTX(getSegwit: false));
46 final tx = BtcTransaction.fromRaw(raw);
47
48 /// when the transaction is taproot and we must use getTaproot transaction
49 /// digest we need all of inputs amounts and owner script pub keys
50 List<BigInt> taprootAmounts = [];
51 List<Script> taprootScripts = [];
52
53 if (utxos.any((e) => e.utxo.isP2tr())) {
54 for (final input in tx.inputs) {
55 final utxo = utxos
56 .firstWhereOrNull((u) => u.utxo.txHash == input.txId && u.utxo.vout == input.txIndex);
57
58 if (utxo == null) {
59 final trPair = await getTaprootPair!.call(input.txId, input.txIndex);
60 taprootAmounts.add(trPair.value);
61 taprootScripts.add(trPair.script);
62 continue;
63 }
64 taprootAmounts.add(utxo.utxo.value);
65 taprootScripts.add(_findLockingScript(utxo, true));
66 }
67 }
68
69 for (var i = 0; i < tx.inputs.length; i++) {
70 final utxo = utxos.firstWhereOrNull((e) =>
71 e.utxo.txHash == tx.inputs[i].txId &&
72 e.utxo.vout == tx.inputs[i].txIndex); // ToDo: More robust verify
73 if (utxo == null) continue;
74
75 /// We receive the owner's ScriptPubKey
76 final script = _findLockingScript(utxo, false);
77
78 final int sighash = utxo.utxo.isP2tr()
79 ? BitcoinOpCodeConst.TAPROOT_SIGHASH_ALL
80 : BitcoinOpCodeConst.SIGHASH_ALL;
81
82 /// We generate transaction digest for current input
83 final digest =
84 _generateTransactionDigest(script, i, utxo.utxo, tx, taprootAmounts, taprootScripts);
85
86 /// now we need sign the transaction digest
87 final sig = signer(digest, utxo, utxo.privateKey, sighash);
88
89 if (utxo.utxo.isP2tr()) {
90 setInputTapKeySig(i, Uint8List.fromList(BytesUtils.fromHexString(sig)));
91 } else {
92 setInputPartialSig(i, Uint8List.fromList(BytesUtils.fromHexString(utxo.public().toHex())),
93 Uint8List.fromList(BytesUtils.fromHexString(sig)));
94 }
95 }
96 }
97
98 List<int> _generateTransactionDigest(Script scriptPubKeys, int input, BitcoinUtxo utxo,
99 BtcTransaction transaction, List<BigInt> taprootAmounts, List<Script> tapRootPubKeys) {
100 if (utxo.isSegwit()) {
101 if (utxo.isP2tr()) {
102 return transaction.getTransactionTaprootDigset(
103 txIndex: input,
104 scriptPubKeys: tapRootPubKeys,
105 amounts: taprootAmounts,
106 );
107 }
108 return transaction.getTransactionSegwitDigit(
109 txInIndex: input, script: scriptPubKeys, amount: utxo.value);
110 }
111 return transaction.getTransactionDigest(txInIndex: input, script: scriptPubKeys);
112 }
113
114 Script _findLockingScript(UtxoWithAddress utxo, bool isTaproot) {
115 if (utxo.isMultiSig()) {
116 throw Exception("MultiSig is not supported yet");
117 }
118
119 final senderPub = utxo.public();
120 switch (utxo.utxo.scriptType) {
121 case PubKeyAddressType.p2pk:
122 return senderPub.toRedeemScript();
123 case SegwitAddresType.p2wsh:
124 if (isTaproot) {
125 return senderPub.toP2wshAddress().toScriptPubKey();
126 }
127 return senderPub.toP2wshRedeemScript();
128 case P2pkhAddressType.p2pkh:
129 return senderPub.toP2pkhAddress().toScriptPubKey();
130 case SegwitAddresType.p2wpkh:
131 if (isTaproot) {
132 return senderPub.toP2wpkhAddress().toScriptPubKey();
133 }
134 return senderPub.toP2pkhAddress().toScriptPubKey();
135 case SegwitAddresType.p2tr:
136 return senderPub
137 .toTaprootAddress(tweak: utxo.utxo.isSilentPayment != true)
138 .toScriptPubKey();
139 case SegwitAddresType.mweb:
140 return Script(script: []);
141 case P2shAddressType.p2pkhInP2sh:
142 if (isTaproot) {
143 return senderPub.toP2pkhInP2sh().toScriptPubKey();
144 }
145 return senderPub.toP2pkhAddress().toScriptPubKey();
146 case P2shAddressType.p2wpkhInP2sh:
147 if (isTaproot) {
148 return senderPub.toP2wpkhInP2sh().toScriptPubKey();
149 }
150 return senderPub.toP2pkhAddress().toScriptPubKey();
151 case P2shAddressType.p2wshInP2sh:
152 if (isTaproot) {
153 return senderPub.toP2wshInP2sh().toScriptPubKey();
154 }
155 return senderPub.toP2wshRedeemScript();
156 case P2shAddressType.p2pkInP2sh:
157 if (isTaproot) {
158 return senderPub.toP2pkInP2sh().toScriptPubKey();
159 }
160 return senderPub.toRedeemScript();
161 }
162 throw Exception("invalid bitcoin address type");
163 }
164 }
165
166 typedef UTXOSignerCallBack = String Function(
167 List<int> trDigest, UtxoWithAddress utxo, ECPrivate privateKey, int sighash);
168
169 typedef UTXOGetterCallBack = Future<TaprootAmountScriptPair> Function(String txId, int vout);
170
171 class TaprootAmountScriptPair {
172 final BigInt value;
173 final Script script;
174
175 const TaprootAmountScriptPair(this.value, this.script);
176 }
177
178 class UtxoWithPrivateKey extends UtxoWithAddress {
179 final ECPrivate privateKey;
180
181 UtxoWithPrivateKey({
182 required super.utxo,
183 required super.ownerDetails,
184 required this.privateKey,
185 });
186
187 factory UtxoWithPrivateKey.fromUtxo(
188 UtxoWithAddress input, List<ECPrivateInfo> inputPrivateKeyInfos) {
189 ECPrivateInfo? key;
190
191 if (inputPrivateKeyInfos.isEmpty) {
192 throw Exception("No private keys generated.");
193 } else {
194 key = inputPrivateKeyInfos.firstWhereOrNull((element) {
195 final elemPubkey = element.privkey.getPublic().toHex();
196 if (elemPubkey == input.public().toHex()) {
197 return true;
198 } else {
199 return false;
200 }
201 });
202 }
203
204 if (key == null) {
205 throw Exception("${input.utxo.txHash} No Key found");
206 }
207
208 return UtxoWithPrivateKey(
209 utxo: input.utxo, ownerDetails: input.ownerDetails, privateKey: key.privkey);
210 }
211
212 factory UtxoWithPrivateKey.fromUnspent(BitcoinUnspent input, BitcoinWalletBase wallet) {
213 final address = RegexUtils.addressTypeFromStr(input.address, BitcoinNetwork.mainnet);
214
215 final newHd = input.bitcoinAddressRecord.isHidden ? wallet.sideHd : wallet.mainHd;
216
217 ECPrivate privkey;
218 if (input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
219 final unspentAddress = input.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
220 privkey = wallet.walletAddresses.silentAddress!.b_spend.tweakAdd(
221 BigintUtils.fromBytes(
222 BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!),
223 ),
224 );
225 } else {
226 privkey = generateECPrivate(
227 hd: newHd, index: input.bitcoinAddressRecord.index, network: BitcoinNetwork.mainnet);
228 }
229
230 return UtxoWithPrivateKey(
231 utxo: BitcoinUtxo(
232 txHash: input.hash,
233 value: BigInt.from(input.value),
234 vout: input.vout,
235 scriptType: input.bitcoinAddressRecord.type,
236 isSilentPayment: input.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord,
237 ),
238 ownerDetails: UtxoAddressDetails(
239 publicKey: privkey.getPublic().toHex(),
240 address: address,
241 ),
242 privateKey: privkey);
243 }
244 }