Skip to content

Commit a39a187

Browse files
Merge pull request #104 from BitGo/BTC-2936.add-descriptor-psbt-addinput-addoutput
feat(wasm-utxo): add basic PSBT creation and management functions
2 parents 1ce52ff + 62d0456 commit a39a187

2 files changed

Lines changed: 117 additions & 18 deletions

File tree

packages/wasm-utxo/src/wasm/psbt.rs

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ use crate::wasm::descriptor::WrapDescriptorEnum;
33
use crate::wasm::try_into_js_value::TryIntoJsValue;
44
use crate::wasm::WrapDescriptor;
55
use miniscript::bitcoin::bip32::Fingerprint;
6+
use miniscript::bitcoin::locktime::absolute::LockTime;
67
use miniscript::bitcoin::secp256k1::{Secp256k1, Signing};
7-
use miniscript::bitcoin::{bip32, psbt, PublicKey, XOnlyPublicKey};
8-
use miniscript::bitcoin::{PrivateKey, Psbt};
8+
use miniscript::bitcoin::transaction::{Transaction, Version};
9+
use miniscript::bitcoin::{
10+
bip32, psbt, Amount, OutPoint, PublicKey, ScriptBuf, Sequence, XOnlyPublicKey,
11+
};
12+
use miniscript::bitcoin::{PrivateKey, Psbt, TxIn, TxOut, Txid};
913
use miniscript::descriptor::{SinglePub, SinglePubKey};
1014
use miniscript::psbt::PsbtExt;
1115
use miniscript::{DescriptorPublicKey, ToPublicKey};
@@ -78,6 +82,22 @@ pub struct WrapPsbt(Psbt);
7882

7983
#[wasm_bindgen()]
8084
impl WrapPsbt {
85+
/// Create an empty PSBT
86+
///
87+
/// # Arguments
88+
/// * `version` - Transaction version (default: 2)
89+
/// * `lock_time` - Transaction lock time (default: 0)
90+
#[wasm_bindgen(constructor)]
91+
pub fn new(version: Option<i32>, lock_time: Option<u32>) -> WrapPsbt {
92+
let tx = Transaction {
93+
version: Version(version.unwrap_or(2)),
94+
lock_time: LockTime::from_consensus(lock_time.unwrap_or(0)),
95+
input: vec![],
96+
output: vec![],
97+
};
98+
WrapPsbt(Psbt::from_unsigned_tx(tx).expect("empty transaction should be valid"))
99+
}
100+
81101
pub fn deserialize(psbt: Vec<u8>) -> Result<WrapPsbt, JsError> {
82102
Ok(WrapPsbt(Psbt::deserialize(&psbt).map_err(JsError::from)?))
83103
}
@@ -91,6 +111,91 @@ impl WrapPsbt {
91111
Clone::clone(self)
92112
}
93113

114+
/// Add an input to the PSBT
115+
///
116+
/// # Arguments
117+
/// * `txid` - Transaction ID (hex string, 32 bytes reversed)
118+
/// * `vout` - Output index being spent
119+
/// * `value` - Value in satoshis of the output being spent
120+
/// * `script` - The scriptPubKey of the output being spent
121+
/// * `sequence` - Sequence number (default: 0xFFFFFFFE for RBF)
122+
///
123+
/// # Returns
124+
/// The index of the newly added input
125+
#[wasm_bindgen(js_name = addInput)]
126+
pub fn add_input(
127+
&mut self,
128+
txid: &str,
129+
vout: u32,
130+
value: u64,
131+
script: &[u8],
132+
sequence: Option<u32>,
133+
) -> Result<usize, JsError> {
134+
let txid =
135+
Txid::from_str(txid).map_err(|e| JsError::new(&format!("Invalid txid: {}", e)))?;
136+
let script = ScriptBuf::from_bytes(script.to_vec());
137+
138+
let tx_in = TxIn {
139+
previous_output: OutPoint { txid, vout },
140+
script_sig: ScriptBuf::new(),
141+
sequence: Sequence(sequence.unwrap_or(0xFFFFFFFE)),
142+
witness: miniscript::bitcoin::Witness::default(),
143+
};
144+
145+
let psbt_input = psbt::Input {
146+
witness_utxo: Some(TxOut {
147+
value: Amount::from_sat(value),
148+
script_pubkey: script,
149+
}),
150+
..Default::default()
151+
};
152+
153+
self.0.unsigned_tx.input.push(tx_in);
154+
self.0.inputs.push(psbt_input);
155+
156+
Ok(self.0.inputs.len() - 1)
157+
}
158+
159+
/// Add an output to the PSBT
160+
///
161+
/// # Arguments
162+
/// * `script` - The output script (scriptPubKey)
163+
/// * `value` - Value in satoshis
164+
///
165+
/// # Returns
166+
/// The index of the newly added output
167+
#[wasm_bindgen(js_name = addOutput)]
168+
pub fn add_output(&mut self, script: &[u8], value: u64) -> usize {
169+
let script = ScriptBuf::from_bytes(script.to_vec());
170+
171+
let tx_out = TxOut {
172+
value: Amount::from_sat(value),
173+
script_pubkey: script,
174+
};
175+
176+
let psbt_output = psbt::Output::default();
177+
178+
self.0.unsigned_tx.output.push(tx_out);
179+
self.0.outputs.push(psbt_output);
180+
181+
self.0.outputs.len() - 1
182+
}
183+
184+
/// Get the unsigned transaction bytes
185+
///
186+
/// # Returns
187+
/// The serialized unsigned transaction
188+
#[wasm_bindgen(js_name = getUnsignedTx)]
189+
pub fn get_unsigned_tx(&self) -> Vec<u8> {
190+
use miniscript::bitcoin::consensus::Encodable;
191+
let mut buf = Vec::new();
192+
self.0
193+
.unsigned_tx
194+
.consensus_encode(&mut buf)
195+
.expect("encoding to vec should not fail");
196+
buf
197+
}
198+
94199
#[wasm_bindgen(js_name = updateInputWithDescriptor)]
95200
pub fn update_input_with_descriptor(
96201
&mut self,

packages/wasm-utxo/test/inscriptions.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as assert from "assert";
2-
import * as utxolib from "@bitgo/utxo-lib";
32
import { ECPair } from "../js/ecpair.js";
43
import { Transaction } from "../js/transaction.js";
5-
import { address, inscriptions } from "../js/index.js";
4+
import { address, inscriptions, Psbt } from "../js/index.js";
65

76
describe("inscriptions (wasm-utxo)", () => {
87
const contentType = "text/plain";
@@ -133,26 +132,21 @@ describe("inscriptions (wasm-utxo)", () => {
133132
describe("signRevealTransaction", () => {
134133
// Create a mock commit transaction with a P2TR output
135134
function createMockCommitTx(commitOutputScript: Uint8Array): Transaction {
136-
const psbt = new utxolib.Psbt({ network: utxolib.networks.testnet });
135+
const psbt = new Psbt();
137136

138137
// Add a dummy input
139-
psbt.addInput({
140-
hash: Buffer.alloc(32), // dummy txid
141-
index: 0,
142-
witnessUtxo: {
143-
script: Buffer.from(commitOutputScript),
144-
value: BigInt(100_000),
145-
},
146-
});
138+
psbt.addInput(
139+
"0".repeat(64), // dummy txid (32 zero bytes as hex)
140+
0,
141+
BigInt(100_000),
142+
commitOutputScript,
143+
);
147144

148145
// Add the commit output
149-
psbt.addOutput({
150-
script: Buffer.from(commitOutputScript),
151-
value: BigInt(42),
152-
});
146+
psbt.addOutput(commitOutputScript, BigInt(42));
153147

154148
// Get the unsigned transaction
155-
const txBytes = psbt.data.globalMap.unsignedTx.toBuffer();
149+
const txBytes = psbt.getUnsignedTx();
156150
return Transaction.fromBytes(txBytes);
157151
}
158152

0 commit comments

Comments
 (0)