Skip to content

Commit a4b8411

Browse files
OttoAllmendingerllm-git
andcommitted
feat(wasm-utxo): add comprehensive PSBT input/output building
Add complete API for PSBT construction with full wallet metadata: - Add wallet inputs with script path or key path spending - Add wallet outputs with proper derivation info - Add replay protection inputs with P2SH-P2PK scripts - Support transaction reconstruction from parsed data - Include input sequence number in parsed input data - Add version and lockTime getters to PSBT class Issue: BTC-2893 Co-authored-by: llm-git <[email protected]>
1 parent 8ae5aa0 commit a4b8411

File tree

9 files changed

+1753
-52
lines changed

9 files changed

+1753
-52
lines changed

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

Lines changed: 196 additions & 13 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 = {
@@ -63,10 +64,10 @@ export type AddInputOptions = {
6364
vout: number;
6465
/** Value in satoshis (for witness_utxo) */
6566
value: bigint;
66-
/** Output script of UTXO being spent */
67-
script: Uint8Array;
6867
/** Sequence number (default: 0xFFFFFFFE for RBF) */
6968
sequence?: number;
69+
/** Full previous transaction (for non-segwit strict compliance) */
70+
prevTx?: Uint8Array;
7071
};
7172

7273
export type AddOutputOptions = {
@@ -76,27 +77,68 @@ export type AddOutputOptions = {
7677
value: bigint;
7778
};
7879

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+
79107
export class BitGoPsbt {
80108
private constructor(private wasm: WasmBitGoPsbt) {}
81109

82110
/**
83-
* Create an empty PSBT for the given network
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.
84115
*
85116
* @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)
86118
* @param options - Optional transaction parameters (version, lockTime)
87119
* @returns A new empty BitGoPsbt instance
88120
*
89121
* @example
90122
* ```typescript
91-
* // Create empty PSBT with defaults (version 2, lockTime 0)
92-
* const psbt = BitGoPsbt.createEmpty("bitcoin");
123+
* // Create empty PSBT with wallet keys
124+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys);
93125
*
94126
* // Create with custom version and lockTime
95-
* const psbt = BitGoPsbt.createEmpty("bitcoin", { version: 1, lockTime: 500000 });
127+
* const psbt = BitGoPsbt.createEmpty("bitcoin", walletKeys, { version: 1, lockTime: 500000 });
96128
* ```
97129
*/
98-
static createEmpty(network: NetworkName, options?: CreateEmptyOptions): BitGoPsbt {
99-
const wasm = WasmBitGoPsbt.create_empty(network, options?.version, options?.lockTime);
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+
);
100142
return new BitGoPsbt(wasm);
101143
}
102144

@@ -117,7 +159,8 @@ export class BitGoPsbt {
117159
* This adds a transaction input and corresponding PSBT input metadata.
118160
* The witness_utxo is automatically populated for modern signing compatibility.
119161
*
120-
* @param options - Input options (txid, vout, value, script, sequence)
162+
* @param options - Input options (txid, vout, value, sequence)
163+
* @param script - Output script of the UTXO being spent
121164
* @returns The index of the newly added input
122165
*
123166
* @example
@@ -126,17 +169,17 @@ export class BitGoPsbt {
126169
* txid: "abc123...",
127170
* vout: 0,
128171
* value: 100000n,
129-
* script: outputScript,
130-
* });
172+
* }, outputScript);
131173
* ```
132174
*/
133-
addInput(options: AddInputOptions): number {
175+
addInput(options: AddInputOptions, script: Uint8Array): number {
134176
return this.wasm.add_input(
135177
options.txid,
136178
options.vout,
137179
options.value,
138-
options.script,
180+
script,
139181
options.sequence,
182+
options.prevTx,
140183
);
141184
}
142185

@@ -158,6 +201,130 @@ export class BitGoPsbt {
158201
return this.wasm.add_output(options.script, options.value);
159202
}
160203

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+
161328
/**
162329
* Get the unsigned transaction ID
163330
* @returns The unsigned transaction ID
@@ -166,6 +333,22 @@ export class BitGoPsbt {
166333
return this.wasm.unsigned_txid();
167334
}
168335

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+
169352
/**
170353
* Parse transaction with wallet keys to identify wallet inputs/outputs
171354
* @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)