Skip to content

Commit c953971

Browse files
Merge pull request #77 from BitGo/BTC-2893.add-inputs-outputs
feat(wasm-utxo): implement PSBT creation and manipulation utilities
2 parents 94cf7f5 + e615905 commit c953971

File tree

11 files changed

+2519
-34
lines changed

11 files changed

+2519
-34
lines changed

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

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type ParsedInput = {
3131
value: bigint;
3232
scriptId: ScriptId | null;
3333
scriptType: InputScriptType;
34+
sequence: number;
3435
};
3536

3637
export type ParsedOutput = {
@@ -49,9 +50,98 @@ export type ParsedTransaction = {
4950
virtualSize: number;
5051
};
5152

53+
export type CreateEmptyOptions = {
54+
/** Transaction version (default: 2) */
55+
version?: number;
56+
/** Lock time (default: 0) */
57+
lockTime?: number;
58+
};
59+
60+
export type AddInputOptions = {
61+
/** Previous transaction ID (hex string) */
62+
txid: string;
63+
/** Output index being spent */
64+
vout: number;
65+
/** Value in satoshis (for witness_utxo) */
66+
value: bigint;
67+
/** Sequence number (default: 0xFFFFFFFE for RBF) */
68+
sequence?: number;
69+
/** Full previous transaction (for non-segwit strict compliance) */
70+
prevTx?: Uint8Array;
71+
};
72+
73+
export type AddOutputOptions = {
74+
/** Output script (scriptPubKey) */
75+
script: Uint8Array;
76+
/** Value in satoshis */
77+
value: bigint;
78+
};
79+
80+
/** Key identifier for signing ("user", "backup", or "bitgo") */
81+
export type SignerKey = "user" | "backup" | "bitgo";
82+
83+
/** Specifies signer and cosigner for Taproot inputs */
84+
export type SignPath = {
85+
/** Key that will sign */
86+
signer: SignerKey;
87+
/** Key that will co-sign */
88+
cosigner: SignerKey;
89+
};
90+
91+
export type AddWalletInputOptions = {
92+
/** Script location in wallet (chain + index) */
93+
scriptId: ScriptId;
94+
/** Sign path - required for p2tr/p2trMusig2 (chains 30-41) */
95+
signPath?: SignPath;
96+
};
97+
98+
export type AddWalletOutputOptions = {
99+
/** Chain code (0/1=p2sh, 10/11=p2shP2wsh, 20/21=p2wsh, 30/31=p2tr, 40/41=p2trMusig2) */
100+
chain: number;
101+
/** Derivation index */
102+
index: number;
103+
/** Value in satoshis */
104+
value: bigint;
105+
};
106+
52107
export class BitGoPsbt {
53108
private constructor(private wasm: WasmBitGoPsbt) {}
54109

110+
/**
111+
* Create an empty PSBT for the given network with wallet keys
112+
*
113+
* The wallet keys are used to set global xpubs in the PSBT, which identifies
114+
* the keys that will be used for signing.
115+
*
116+
* @param network - Network name (utxolib name like "bitcoin" or coin name like "btc")
117+
* @param walletKeys - The wallet's root keys (sets global xpubs in the PSBT)
118+
* @param options - Optional transaction parameters (version, lockTime)
119+
* @returns A new empty BitGoPsbt instance
120+
*
121+
* @example
122+
* ```typescript
123+
* // Create empty PSBT with wallet keys
124+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys);
125+
*
126+
* // Create with custom version and lockTime
127+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys, { version: 1, lockTime: 500000 });
128+
* ```
129+
*/
130+
static createEmpty(
131+
network: NetworkName,
132+
walletKeys: WalletKeysArg,
133+
options?: CreateEmptyOptions,
134+
): BitGoPsbt {
135+
const keys = RootWalletKeys.from(walletKeys);
136+
const wasm = WasmBitGoPsbt.create_empty(
137+
network,
138+
keys.wasm,
139+
options?.version,
140+
options?.lockTime,
141+
);
142+
return new BitGoPsbt(wasm);
143+
}
144+
55145
/**
56146
* Deserialize a PSBT from bytes
57147
* @param bytes - The PSBT bytes
@@ -63,6 +153,178 @@ export class BitGoPsbt {
63153
return new BitGoPsbt(wasm);
64154
}
65155

156+
/**
157+
* Add an input to the PSBT
158+
*
159+
* This adds a transaction input and corresponding PSBT input metadata.
160+
* The witness_utxo is automatically populated for modern signing compatibility.
161+
*
162+
* @param options - Input options (txid, vout, value, sequence)
163+
* @param script - Output script of the UTXO being spent
164+
* @returns The index of the newly added input
165+
*
166+
* @example
167+
* ```typescript
168+
* const inputIndex = psbt.addInput({
169+
* txid: "abc123...",
170+
* vout: 0,
171+
* value: 100000n,
172+
* }, outputScript);
173+
* ```
174+
*/
175+
addInput(options: AddInputOptions, script: Uint8Array): number {
176+
return this.wasm.add_input(
177+
options.txid,
178+
options.vout,
179+
options.value,
180+
script,
181+
options.sequence,
182+
options.prevTx,
183+
);
184+
}
185+
186+
/**
187+
* Add an output to the PSBT
188+
*
189+
* @param options - Output options (script, value)
190+
* @returns The index of the newly added output
191+
*
192+
* @example
193+
* ```typescript
194+
* const outputIndex = psbt.addOutput({
195+
* script: outputScript,
196+
* value: 50000n,
197+
* });
198+
* ```
199+
*/
200+
addOutput(options: AddOutputOptions): number {
201+
return this.wasm.add_output(options.script, options.value);
202+
}
203+
204+
/**
205+
* Add a wallet input with full PSBT metadata
206+
*
207+
* This is a higher-level method that adds an input and populates all required
208+
* PSBT fields (scripts, derivation info, etc.) based on the wallet's chain type.
209+
*
210+
* For p2sh/p2shP2wsh/p2wsh: Sets bip32Derivation, witnessScript, redeemScript (signPath not needed)
211+
* For p2tr/p2trMusig2 script path: Sets tapLeafScript, tapBip32Derivation (signPath required)
212+
* For p2trMusig2 key path: Sets tapInternalKey, tapMerkleRoot, tapBip32Derivation, musig2 participants (signPath required)
213+
*
214+
* @param inputOptions - Common input options (txid, vout, value, sequence)
215+
* @param walletKeys - The wallet's root keys
216+
* @param walletOptions - Wallet-specific options (scriptId, signPath, prevTx)
217+
* @returns The index of the newly added input
218+
*
219+
* @example
220+
* ```typescript
221+
* // Add a p2shP2wsh input (signPath not needed)
222+
* const inputIndex = psbt.addWalletInput(
223+
* { txid: "abc123...", vout: 0, value: 100000n },
224+
* walletKeys,
225+
* { scriptId: { chain: 10, index: 0 } }, // p2shP2wsh external
226+
* );
227+
*
228+
* // Add a p2trMusig2 key path input (signPath required)
229+
* const inputIndex = psbt.addWalletInput(
230+
* { txid: "def456...", vout: 1, value: 50000n },
231+
* walletKeys,
232+
* { scriptId: { chain: 40, index: 5 }, signPath: { signer: "user", cosigner: "bitgo" } },
233+
* );
234+
*
235+
* // Add p2trMusig2 with backup key (script path spend)
236+
* const inputIndex = psbt.addWalletInput(
237+
* { txid: "ghi789...", vout: 0, value: 75000n },
238+
* walletKeys,
239+
* { scriptId: { chain: 40, index: 3 }, signPath: { signer: "user", cosigner: "backup" } },
240+
* );
241+
* ```
242+
*/
243+
addWalletInput(
244+
inputOptions: AddInputOptions,
245+
walletKeys: WalletKeysArg,
246+
walletOptions: AddWalletInputOptions,
247+
): number {
248+
const keys = RootWalletKeys.from(walletKeys);
249+
return this.wasm.add_wallet_input(
250+
inputOptions.txid,
251+
inputOptions.vout,
252+
inputOptions.value,
253+
keys.wasm,
254+
walletOptions.scriptId.chain,
255+
walletOptions.scriptId.index,
256+
walletOptions.signPath?.signer,
257+
walletOptions.signPath?.cosigner,
258+
inputOptions.sequence,
259+
inputOptions.prevTx,
260+
);
261+
}
262+
263+
/**
264+
* Add a wallet output with full PSBT metadata
265+
*
266+
* This creates a verifiable wallet output (typically for change) with all required
267+
* PSBT fields (scripts, derivation info) based on the wallet's chain type.
268+
*
269+
* For p2sh/p2shP2wsh/p2wsh: Sets bip32Derivation, witnessScript, redeemScript
270+
* For p2tr/p2trMusig2: Sets tapInternalKey, tapBip32Derivation
271+
*
272+
* @param walletKeys - The wallet's root keys
273+
* @param options - Output options including chain, index, and value
274+
* @returns The index of the newly added output
275+
*
276+
* @example
277+
* ```typescript
278+
* // Add a p2shP2wsh change output
279+
* const outputIndex = psbt.addWalletOutput(walletKeys, {
280+
* chain: 11, // p2shP2wsh internal (change)
281+
* index: 0,
282+
* value: 50000n,
283+
* });
284+
*
285+
* // Add a p2trMusig2 change output
286+
* const outputIndex = psbt.addWalletOutput(walletKeys, {
287+
* chain: 41, // p2trMusig2 internal (change)
288+
* index: 5,
289+
* value: 25000n,
290+
* });
291+
* ```
292+
*/
293+
addWalletOutput(walletKeys: WalletKeysArg, options: AddWalletOutputOptions): number {
294+
const keys = RootWalletKeys.from(walletKeys);
295+
return this.wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm);
296+
}
297+
298+
/**
299+
* Add a replay protection input to the PSBT
300+
*
301+
* Replay protection inputs are P2SH-P2PK inputs used on forked networks to prevent
302+
* transaction replay attacks. They use a simple pubkey script without wallet derivation.
303+
*
304+
* @param inputOptions - Common input options (txid, vout, value, sequence)
305+
* @param key - ECPair containing the public key for the replay protection input
306+
* @returns The index of the newly added input
307+
*
308+
* @example
309+
* ```typescript
310+
* // Add a replay protection input using ECPair
311+
* const inputIndex = psbt.addReplayProtectionInput(
312+
* { txid: "abc123...", vout: 0, value: 1000n },
313+
* replayProtectionKey,
314+
* );
315+
* ```
316+
*/
317+
addReplayProtectionInput(inputOptions: AddInputOptions, key: ECPairArg): number {
318+
const ecpair = ECPair.from(key);
319+
return this.wasm.add_replay_protection_input(
320+
ecpair.wasm,
321+
inputOptions.txid,
322+
inputOptions.vout,
323+
inputOptions.value,
324+
inputOptions.sequence,
325+
);
326+
}
327+
66328
/**
67329
* Get the unsigned transaction ID
68330
* @returns The unsigned transaction ID
@@ -71,6 +333,22 @@ export class BitGoPsbt {
71333
return this.wasm.unsigned_txid();
72334
}
73335

336+
/**
337+
* Get the transaction version
338+
* @returns The transaction version number
339+
*/
340+
get version(): number {
341+
return this.wasm.version();
342+
}
343+
344+
/**
345+
* Get the transaction lock time
346+
* @returns The transaction lock time
347+
*/
348+
get lockTime(): number {
349+
return this.wasm.lock_time();
350+
}
351+
74352
/**
75353
* Parse transaction with wallet keys to identify wallet inputs/outputs
76354
* @param walletKeys - The wallet keys to use for identification

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export {
99
type ParsedInput,
1010
type ParsedOutput,
1111
type ParsedTransaction,
12+
type SignPath,
1213
} from "./BitGoPsbt.js";

0 commit comments

Comments
 (0)