| 1 | import 'dart:convert'; |
| 2 | import 'dart:typed_data'; |
| 3 | |
| 4 | import 'package:bitcoin_base/bitcoin_base.dart'; |
| 5 | import 'package:convert/convert.dart'; |
| 6 | import 'package:cw_core/output_info.dart'; |
| 7 | import 'package:cw_core/utils/print_verbose.dart'; |
| 8 | import 'package:ledger_bitcoin/psbt.dart'; |
| 9 | |
| 10 | class PSBTTransactionBuild { |
| 11 | final PsbtV2 psbt = PsbtV2(); |
| 12 | |
| 13 | PSBTTransactionBuild( |
| 14 | {required List<PSBTReadyUtxoWithAddress> inputs, |
| 15 | required List<BitcoinBaseOutput> outputs, |
| 16 | required List<OutputInfo> cwOutputs, |
| 17 | bool enableRBF = true, |
| 18 | int locktime = 0}) { |
| 19 | psbt.setGlobalTxVersion(2); |
| 20 | psbt.setGlobalFallbackLocktime(locktime); |
| 21 | psbt.setGlobalInputCount(inputs.length); |
| 22 | psbt.setGlobalOutputCount(outputs.length); |
| 23 | |
| 24 | for (var i = 0; i < inputs.length; i++) { |
| 25 | final input = inputs[i]; |
| 26 | |
| 27 | printV(input.utxo.isP2tr()); |
| 28 | printV(input.utxo.isSegwit()); |
| 29 | printV(input.utxo.isP2shSegwit()); |
| 30 | |
| 31 | psbt.setInputPreviousTxId( |
| 32 | i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList())); |
| 33 | psbt.setInputOutputIndex(i, input.utxo.vout); |
| 34 | psbt.setInputSequence(i, enableRBF ? 0xfffffffd : 0xffffffff); |
| 35 | |
| 36 | if (input.utxo.isSegwit()) { |
| 37 | setInputSegwit(i, input); |
| 38 | } else if (input.utxo.isP2shSegwit()) { |
| 39 | setInputP2shSegwit(i, input); |
| 40 | } else if (input.utxo.isP2tr()) { |
| 41 | // ToDo: (Konsti) Handle Taproot Inputs |
| 42 | } else { |
| 43 | setInputP2pkh(i, input); |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | for (var i = 0; i < outputs.length; i++) { |
| 48 | final output = outputs[i]; |
| 49 | |
| 50 | if (output is BitcoinOutput) { |
| 51 | psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes())); |
| 52 | psbt.setOutputAmount(i, output.value.toInt()); |
| 53 | if (cwOutputs.isNotEmpty) { |
| 54 | try { |
| 55 | final cwOutput = cwOutputs |
| 56 | .where((e) => [e.address, e.extractedAddress] |
| 57 | .map((e) => e?.toLowerCase()) |
| 58 | .contains(output.address.toAddress().toLowerCase())) |
| 59 | .firstOrNull; |
| 60 | if (cwOutput != null && |
| 61 | cwOutput.extra.containsKey('bip353_name') && |
| 62 | cwOutput.extra.containsKey('bip353_proof')) { |
| 63 | final bip353Name = utf8.encode(cwOutput.extra['bip353_name'] as String); |
| 64 | final bip353Proof = base64.decode(cwOutput.extra['bip353_proof'] as String); |
| 65 | |
| 66 | if (bip353Name.length > 255) { |
| 67 | printV('BIP353 name is too long, skipping'); |
| 68 | continue; |
| 69 | } |
| 70 | final proof = Uint8List.fromList([ |
| 71 | bip353Name.length, |
| 72 | ...bip353Name, |
| 73 | ...bip353Proof, |
| 74 | ]); |
| 75 | |
| 76 | psbt.setOutputDNSSECProof(i, proof); |
| 77 | } |
| 78 | } catch (e) { |
| 79 | printV('Error setting DNSSEC proof: $e'); |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) { |
| 87 | psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); |
| 88 | psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), |
| 89 | input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); |
| 90 | } |
| 91 | |
| 92 | void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) { |
| 93 | psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); |
| 94 | psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), |
| 95 | input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); |
| 96 | |
| 97 | psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)), |
| 98 | Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); |
| 99 | } |
| 100 | |
| 101 | void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) { |
| 102 | psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx))); |
| 103 | psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)), |
| 104 | input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray()); |
| 105 | |
| 106 | psbt.setInputRedeemScript( |
| 107 | i, Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); |
| 108 | psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)), |
| 109 | Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes())); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | class PSBTReadyUtxoWithAddress extends UtxoWithAddress { |
| 114 | final String rawTx; |
| 115 | final String ownerDerivationPath; |
| 116 | final Uint8List ownerMasterFingerprint; |
| 117 | final String ownerPublicKey; |
| 118 | |
| 119 | PSBTReadyUtxoWithAddress({ |
| 120 | required super.utxo, |
| 121 | required this.rawTx, |
| 122 | required super.ownerDetails, |
| 123 | required this.ownerDerivationPath, |
| 124 | required this.ownerMasterFingerprint, |
| 125 | required this.ownerPublicKey, |
| 126 | }); |
| 127 | } |