diff --git a/packages/wasm-utxo/src/wasm/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs index d1806b0..1e78bd1 100644 --- a/packages/wasm-utxo/src/wasm/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -3,9 +3,13 @@ use crate::wasm::descriptor::WrapDescriptorEnum; use crate::wasm::try_into_js_value::TryIntoJsValue; use crate::wasm::WrapDescriptor; use miniscript::bitcoin::bip32::Fingerprint; +use miniscript::bitcoin::locktime::absolute::LockTime; use miniscript::bitcoin::secp256k1::{Secp256k1, Signing}; -use miniscript::bitcoin::{bip32, psbt, PublicKey, XOnlyPublicKey}; -use miniscript::bitcoin::{PrivateKey, Psbt}; +use miniscript::bitcoin::transaction::{Transaction, Version}; +use miniscript::bitcoin::{ + bip32, psbt, Amount, OutPoint, PublicKey, ScriptBuf, Sequence, XOnlyPublicKey, +}; +use miniscript::bitcoin::{PrivateKey, Psbt, TxIn, TxOut, Txid}; use miniscript::descriptor::{SinglePub, SinglePubKey}; use miniscript::psbt::PsbtExt; use miniscript::{DescriptorPublicKey, ToPublicKey}; @@ -78,6 +82,22 @@ pub struct WrapPsbt(Psbt); #[wasm_bindgen()] impl WrapPsbt { + /// Create an empty PSBT + /// + /// # Arguments + /// * `version` - Transaction version (default: 2) + /// * `lock_time` - Transaction lock time (default: 0) + #[wasm_bindgen(constructor)] + pub fn new(version: Option, lock_time: Option) -> WrapPsbt { + let tx = Transaction { + version: Version(version.unwrap_or(2)), + lock_time: LockTime::from_consensus(lock_time.unwrap_or(0)), + input: vec![], + output: vec![], + }; + WrapPsbt(Psbt::from_unsigned_tx(tx).expect("empty transaction should be valid")) + } + pub fn deserialize(psbt: Vec) -> Result { Ok(WrapPsbt(Psbt::deserialize(&psbt).map_err(JsError::from)?)) } @@ -91,6 +111,91 @@ impl WrapPsbt { Clone::clone(self) } + /// Add an input to the PSBT + /// + /// # Arguments + /// * `txid` - Transaction ID (hex string, 32 bytes reversed) + /// * `vout` - Output index being spent + /// * `value` - Value in satoshis of the output being spent + /// * `script` - The scriptPubKey of the output being spent + /// * `sequence` - Sequence number (default: 0xFFFFFFFE for RBF) + /// + /// # Returns + /// The index of the newly added input + #[wasm_bindgen(js_name = addInput)] + pub fn add_input( + &mut self, + txid: &str, + vout: u32, + value: u64, + script: &[u8], + sequence: Option, + ) -> Result { + let txid = + Txid::from_str(txid).map_err(|e| JsError::new(&format!("Invalid txid: {}", e)))?; + let script = ScriptBuf::from_bytes(script.to_vec()); + + let tx_in = TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence(sequence.unwrap_or(0xFFFFFFFE)), + witness: miniscript::bitcoin::Witness::default(), + }; + + let psbt_input = psbt::Input { + witness_utxo: Some(TxOut { + value: Amount::from_sat(value), + script_pubkey: script, + }), + ..Default::default() + }; + + self.0.unsigned_tx.input.push(tx_in); + self.0.inputs.push(psbt_input); + + Ok(self.0.inputs.len() - 1) + } + + /// Add an output to the PSBT + /// + /// # Arguments + /// * `script` - The output script (scriptPubKey) + /// * `value` - Value in satoshis + /// + /// # Returns + /// The index of the newly added output + #[wasm_bindgen(js_name = addOutput)] + pub fn add_output(&mut self, script: &[u8], value: u64) -> usize { + let script = ScriptBuf::from_bytes(script.to_vec()); + + let tx_out = TxOut { + value: Amount::from_sat(value), + script_pubkey: script, + }; + + let psbt_output = psbt::Output::default(); + + self.0.unsigned_tx.output.push(tx_out); + self.0.outputs.push(psbt_output); + + self.0.outputs.len() - 1 + } + + /// Get the unsigned transaction bytes + /// + /// # Returns + /// The serialized unsigned transaction + #[wasm_bindgen(js_name = getUnsignedTx)] + pub fn get_unsigned_tx(&self) -> Vec { + use miniscript::bitcoin::consensus::Encodable; + let mut buf = Vec::new(); + self.0 + .unsigned_tx + .consensus_encode(&mut buf) + .expect("encoding to vec should not fail"); + buf + } + #[wasm_bindgen(js_name = updateInputWithDescriptor)] pub fn update_input_with_descriptor( &mut self, diff --git a/packages/wasm-utxo/test/inscriptions.ts b/packages/wasm-utxo/test/inscriptions.ts index a1b00b2..1b5f60d 100644 --- a/packages/wasm-utxo/test/inscriptions.ts +++ b/packages/wasm-utxo/test/inscriptions.ts @@ -1,8 +1,7 @@ import * as assert from "assert"; -import * as utxolib from "@bitgo/utxo-lib"; import { ECPair } from "../js/ecpair.js"; import { Transaction } from "../js/transaction.js"; -import { address, inscriptions } from "../js/index.js"; +import { address, inscriptions, Psbt } from "../js/index.js"; describe("inscriptions (wasm-utxo)", () => { const contentType = "text/plain"; @@ -133,26 +132,21 @@ describe("inscriptions (wasm-utxo)", () => { describe("signRevealTransaction", () => { // Create a mock commit transaction with a P2TR output function createMockCommitTx(commitOutputScript: Uint8Array): Transaction { - const psbt = new utxolib.Psbt({ network: utxolib.networks.testnet }); + const psbt = new Psbt(); // Add a dummy input - psbt.addInput({ - hash: Buffer.alloc(32), // dummy txid - index: 0, - witnessUtxo: { - script: Buffer.from(commitOutputScript), - value: BigInt(100_000), - }, - }); + psbt.addInput( + "0".repeat(64), // dummy txid (32 zero bytes as hex) + 0, + BigInt(100_000), + commitOutputScript, + ); // Add the commit output - psbt.addOutput({ - script: Buffer.from(commitOutputScript), - value: BigInt(42), - }); + psbt.addOutput(commitOutputScript, BigInt(42)); // Get the unsigned transaction - const txBytes = psbt.data.globalMap.unsignedTx.toBuffer(); + const txBytes = psbt.getUnsignedTx(); return Transaction.fromBytes(txBytes); }