Skip to content

Commit 5716771

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add PSBT creation and manipulation methods
Add methods to create empty PSBTs and manipulate them by adding inputs and outputs. This extends the BitGoPsbt class with: - `createEmpty` static method to create new PSBTs - `addInput` method for adding transaction inputs with witness UTXO data - `addOutput` method for adding transaction outputs Issue: BTC-2893 Co-authored-by: llm-git <[email protected]>
1 parent faba118 commit 5716771

File tree

3 files changed

+272
-0
lines changed

3 files changed

+272
-0
lines changed

packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,57 @@ export type ParsedTransaction = {
4949
virtualSize: number;
5050
};
5151

52+
export type CreateEmptyOptions = {
53+
/** Transaction version (default: 2) */
54+
version?: number;
55+
/** Lock time (default: 0) */
56+
lockTime?: number;
57+
};
58+
59+
export type AddInputOptions = {
60+
/** Previous transaction ID (hex string) */
61+
txid: string;
62+
/** Output index being spent */
63+
vout: number;
64+
/** Value in satoshis (for witness_utxo) */
65+
value: bigint;
66+
/** Output script of UTXO being spent */
67+
script: Uint8Array;
68+
/** Sequence number (default: 0xFFFFFFFE for RBF) */
69+
sequence?: number;
70+
};
71+
72+
export type AddOutputOptions = {
73+
/** Output script (scriptPubKey) */
74+
script: Uint8Array;
75+
/** Value in satoshis */
76+
value: bigint;
77+
};
78+
5279
export class BitGoPsbt {
5380
private constructor(private wasm: WasmBitGoPsbt) {}
5481

82+
/**
83+
* Create an empty PSBT for the given network
84+
*
85+
* @param network - Network name (utxolib name like "bitcoin" or coin name like "btc")
86+
* @param options - Optional transaction parameters (version, lockTime)
87+
* @returns A new empty BitGoPsbt instance
88+
*
89+
* @example
90+
* ```typescript
91+
* // Create empty PSBT with defaults (version 2, lockTime 0)
92+
* const psbt = BitGoPsbt.createEmpty("bitcoin");
93+
*
94+
* // Create with custom version and lockTime
95+
* const psbt = BitGoPsbt.createEmpty("bitcoin", { version: 1, lockTime: 500000 });
96+
* ```
97+
*/
98+
static createEmpty(network: NetworkName, options?: CreateEmptyOptions): BitGoPsbt {
99+
const wasm = WasmBitGoPsbt.create_empty(network, options?.version, options?.lockTime);
100+
return new BitGoPsbt(wasm);
101+
}
102+
55103
/**
56104
* Deserialize a PSBT from bytes
57105
* @param bytes - The PSBT bytes
@@ -63,6 +111,53 @@ export class BitGoPsbt {
63111
return new BitGoPsbt(wasm);
64112
}
65113

114+
/**
115+
* Add an input to the PSBT
116+
*
117+
* This adds a transaction input and corresponding PSBT input metadata.
118+
* The witness_utxo is automatically populated for modern signing compatibility.
119+
*
120+
* @param options - Input options (txid, vout, value, script, sequence)
121+
* @returns The index of the newly added input
122+
*
123+
* @example
124+
* ```typescript
125+
* const inputIndex = psbt.addInput({
126+
* txid: "abc123...",
127+
* vout: 0,
128+
* value: 100000n,
129+
* script: outputScript,
130+
* });
131+
* ```
132+
*/
133+
addInput(options: AddInputOptions): number {
134+
return this.wasm.add_input(
135+
options.txid,
136+
options.vout,
137+
options.value,
138+
options.script,
139+
options.sequence,
140+
);
141+
}
142+
143+
/**
144+
* Add an output to the PSBT
145+
*
146+
* @param options - Output options (script, value)
147+
* @returns The index of the newly added output
148+
*
149+
* @example
150+
* ```typescript
151+
* const outputIndex = psbt.addOutput({
152+
* script: outputScript,
153+
* value: 50000n,
154+
* });
155+
* ```
156+
*/
157+
addOutput(options: AddOutputOptions): number {
158+
return this.wasm.add_output(options.script, options.value);
159+
}
160+
66161
/**
67162
* Get the unsigned transaction ID
68163
* @returns The unsigned transaction ID

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,117 @@ impl BitGoPsbt {
197197
}
198198
}
199199

200+
/// Create an empty PSBT with the given network
201+
///
202+
/// # Arguments
203+
/// * `network` - The network this PSBT is for
204+
/// * `version` - Transaction version (default: 2)
205+
/// * `lock_time` - Lock time (default: 0)
206+
pub fn new(network: Network, version: Option<i32>, lock_time: Option<u32>) -> Self {
207+
use miniscript::bitcoin::{absolute::LockTime, transaction::Version, Transaction};
208+
209+
let tx = Transaction {
210+
version: Version(version.unwrap_or(2)),
211+
lock_time: LockTime::from_consensus(lock_time.unwrap_or(0)),
212+
input: vec![],
213+
output: vec![],
214+
};
215+
216+
let psbt = Psbt::from_unsigned_tx(tx).expect("empty transaction should be valid");
217+
218+
match network {
219+
Network::Zcash | Network::ZcashTestnet => BitGoPsbt::Zcash(
220+
ZcashPsbt {
221+
psbt,
222+
version_group_id: None,
223+
expiry_height: None,
224+
sapling_fields: vec![],
225+
},
226+
network,
227+
),
228+
_ => BitGoPsbt::BitcoinLike(psbt, network),
229+
}
230+
}
231+
232+
/// Add an input to the PSBT
233+
///
234+
/// This adds a transaction input and corresponding PSBT input metadata.
235+
/// The witness_utxo is automatically populated for modern signing compatibility.
236+
///
237+
/// # Arguments
238+
/// * `txid` - The transaction ID of the output being spent
239+
/// * `vout` - The output index being spent
240+
/// * `value` - The value in satoshis of the output being spent
241+
/// * `script` - The output script (scriptPubKey) of the output being spent
242+
/// * `sequence` - Optional sequence number (default: 0xFFFFFFFE for RBF)
243+
///
244+
/// # Returns
245+
/// The index of the newly added input
246+
pub fn add_input(
247+
&mut self,
248+
txid: Txid,
249+
vout: u32,
250+
value: u64,
251+
script: miniscript::bitcoin::ScriptBuf,
252+
sequence: Option<u32>,
253+
) -> usize {
254+
use miniscript::bitcoin::{transaction::Sequence, Amount, OutPoint, TxIn, TxOut};
255+
256+
let psbt = self.psbt_mut();
257+
258+
// Create the transaction input
259+
let tx_in = TxIn {
260+
previous_output: OutPoint { txid, vout },
261+
script_sig: miniscript::bitcoin::ScriptBuf::new(),
262+
sequence: Sequence(sequence.unwrap_or(0xFFFFFFFE)),
263+
witness: miniscript::bitcoin::Witness::default(),
264+
};
265+
266+
// Create the PSBT input with witness_utxo populated
267+
let psbt_input = miniscript::bitcoin::psbt::Input {
268+
witness_utxo: Some(TxOut {
269+
value: Amount::from_sat(value),
270+
script_pubkey: script,
271+
}),
272+
..Default::default()
273+
};
274+
275+
// Add to the PSBT
276+
psbt.unsigned_tx.input.push(tx_in);
277+
psbt.inputs.push(psbt_input);
278+
279+
psbt.inputs.len() - 1
280+
}
281+
282+
/// Add an output to the PSBT
283+
///
284+
/// # Arguments
285+
/// * `script` - The output script (scriptPubKey)
286+
/// * `value` - The value in satoshis
287+
///
288+
/// # Returns
289+
/// The index of the newly added output
290+
pub fn add_output(&mut self, script: miniscript::bitcoin::ScriptBuf, value: u64) -> usize {
291+
use miniscript::bitcoin::{Amount, TxOut};
292+
293+
let psbt = self.psbt_mut();
294+
295+
// Create the transaction output
296+
let tx_out = TxOut {
297+
value: Amount::from_sat(value),
298+
script_pubkey: script,
299+
};
300+
301+
// Create the PSBT output
302+
let psbt_output = miniscript::bitcoin::psbt::Output::default();
303+
304+
// Add to the PSBT
305+
psbt.unsigned_tx.output.push(tx_out);
306+
psbt.outputs.push(psbt_output);
307+
308+
psbt.outputs.len() - 1
309+
}
310+
200311
pub fn network(&self) -> Network {
201312
match self {
202313
BitGoPsbt::BitcoinLike(_, network) => *network,

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,72 @@ impl BitGoPsbt {
105105
})
106106
}
107107

108+
/// Create an empty PSBT for the given network
109+
///
110+
/// # Arguments
111+
/// * `network` - Network name (utxolib or coin name)
112+
/// * `version` - Optional transaction version (default: 2)
113+
/// * `lock_time` - Optional lock time (default: 0)
114+
pub fn create_empty(
115+
network: &str,
116+
version: Option<i32>,
117+
lock_time: Option<u32>,
118+
) -> Result<BitGoPsbt, WasmUtxoError> {
119+
let network = parse_network(network)?;
120+
121+
let psbt = crate::fixed_script_wallet::bitgo_psbt::BitGoPsbt::new(network, version, lock_time);
122+
123+
Ok(BitGoPsbt {
124+
psbt,
125+
first_rounds: HashMap::new(),
126+
})
127+
}
128+
129+
/// Add an input to the PSBT
130+
///
131+
/// # Arguments
132+
/// * `txid` - The transaction ID (hex string) of the output being spent
133+
/// * `vout` - The output index being spent
134+
/// * `value` - The value in satoshis of the output being spent
135+
/// * `script` - The output script (scriptPubKey) of the output being spent
136+
/// * `sequence` - Optional sequence number (default: 0xFFFFFFFE for RBF)
137+
///
138+
/// # Returns
139+
/// The index of the newly added input
140+
pub fn add_input(
141+
&mut self,
142+
txid: &str,
143+
vout: u32,
144+
value: u64,
145+
script: &[u8],
146+
sequence: Option<u32>,
147+
) -> Result<usize, WasmUtxoError> {
148+
use miniscript::bitcoin::{ScriptBuf, Txid};
149+
use std::str::FromStr;
150+
151+
let txid = Txid::from_str(txid)
152+
.map_err(|e| WasmUtxoError::new(&format!("Invalid txid: {}", e)))?;
153+
let script = ScriptBuf::from_bytes(script.to_vec());
154+
155+
Ok(self.psbt.add_input(txid, vout, value, script, sequence))
156+
}
157+
158+
/// Add an output to the PSBT
159+
///
160+
/// # Arguments
161+
/// * `script` - The output script (scriptPubKey)
162+
/// * `value` - The value in satoshis
163+
///
164+
/// # Returns
165+
/// The index of the newly added output
166+
pub fn add_output(&mut self, script: &[u8], value: u64) -> Result<usize, WasmUtxoError> {
167+
use miniscript::bitcoin::ScriptBuf;
168+
169+
let script = ScriptBuf::from_bytes(script.to_vec());
170+
171+
Ok(self.psbt.add_output(script, value))
172+
}
173+
108174
/// Get the unsigned transaction ID
109175
pub fn unsigned_txid(&self) -> String {
110176
self.psbt.unsigned_txid().to_string()

0 commit comments

Comments
 (0)