From 18914847824e12621e2b225c320279fb3227d5cc Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Tue, 2 Dec 2025 16:34:53 +0100 Subject: [PATCH] feat(wasm-utxo): add previousOutput field to ParsedInput Add OutPoint type and previousOutput field to ParsedInput to expose transaction input's previous output information. This allows clients to access the txid and vout of the input directly from the parsed PSBT. Extended tests to verify the new field structure and data types. Issue: BTC-2786 Co-authored-by: llm-git --- .../js/fixedScriptWallet/BitGoPsbt.ts | 6 ++ .../bitgo_psbt/psbt_wallet_input.rs | 2 + .../wasm-utxo/src/wasm/try_into_js_value.rs | 1 + .../parseTransactionWithWalletKeys.ts | 86 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index 80cb5a2..c2412f4 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -19,7 +19,13 @@ export type InputScriptType = | "p2trMusig2ScriptPath" | "p2trMusig2KeyPath"; +export type OutPoint = { + txid: string; + vout: number; +}; + export type ParsedInput = { + previousOutput: OutPoint; address: string; script: Uint8Array; value: bigint; diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs index 887579c..3312087 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/psbt_wallet_input.rs @@ -565,6 +565,7 @@ impl InputScriptType { /// Parsed input from a PSBT transaction #[derive(Debug, Clone)] pub struct ParsedInput { + pub previous_output: OutPoint, pub address: String, pub script: Vec, pub value: u64, @@ -627,6 +628,7 @@ impl ParsedInput { .map_err(ParseInputError::ScriptTypeDetection)?; Ok(Self { + previous_output: tx_input.previous_output, address, script: output_script.to_bytes(), value: value.to_sat(), diff --git a/packages/wasm-utxo/src/wasm/try_into_js_value.rs b/packages/wasm-utxo/src/wasm/try_into_js_value.rs index 24e7744..7608f54 100644 --- a/packages/wasm-utxo/src/wasm/try_into_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_into_js_value.rs @@ -336,6 +336,7 @@ impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::InputScriptType impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::ParsedInput { fn try_to_js_value(&self) -> Result { js_obj!( + "previousOutput" => js_obj!("txid" => self.previous_output.txid.to_string(), "vout" => self.previous_output.vout)?, "address" => self.address.clone(), "value" => self.value, "scriptId" => self.script_id, diff --git a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts index 46e0d35..2533a9f 100644 --- a/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts +++ b/packages/wasm-utxo/test/fixedScript/parseTransactionWithWalletKeys.ts @@ -83,9 +83,59 @@ describe("parseTransactionWithWalletKeys", function () { // Verify all inputs have addresses and values parsed.inputs.forEach((input, i) => { + // Verify previousOutput structure + assert.ok(input.previousOutput, `Input ${i} should have previousOutput`); + assert.ok( + typeof input.previousOutput === "object", + `Input ${i} previousOutput should be an object`, + ); + assert.ok( + typeof input.previousOutput.txid === "string", + `Input ${i} previousOutput.txid should be string`, + ); + assert.strictEqual( + input.previousOutput.txid.length, + 64, + `Input ${i} previousOutput.txid should be 64 chars (32 bytes hex)`, + ); + assert.ok( + typeof input.previousOutput.vout === "number", + `Input ${i} previousOutput.vout should be number`, + ); + assert.ok( + input.previousOutput.vout >= 0, + `Input ${i} previousOutput.vout should be >= 0`, + ); + + // Verify address assert.ok(input.address, `Input ${i} should have an address`); + assert.ok(typeof input.address === "string", `Input ${i} address should be string`); + + // Verify value assert.ok(typeof input.value === "bigint", `Input ${i} value should be bigint`); assert.ok(input.value > 0n, `Input ${i} value should be > 0`); + + // Verify scriptId structure (can be null for replay protection inputs) + if (input.scriptId !== null) { + assert.ok( + typeof input.scriptId === "object", + `Input ${i} scriptId should be an object when present`, + ); + assert.ok( + typeof input.scriptId.chain === "number", + `Input ${i} scriptId.chain should be number`, + ); + assert.ok( + typeof input.scriptId.index === "number", + `Input ${i} scriptId.index should be number`, + ); + assert.ok(input.scriptId.chain >= 0, `Input ${i} scriptId.chain should be >= 0`); + assert.ok(input.scriptId.index >= 0, `Input ${i} scriptId.index should be >= 0`); + } + + // Verify scriptType is present + assert.ok(input.scriptType, `Input ${i} should have scriptType`); + assert.ok(typeof input.scriptType === "string", `Input ${i} scriptType should be string`); }); // Validate outputs @@ -157,6 +207,42 @@ describe("parseTransactionWithWalletKeys", function () { expectedScriptType, `Input ${i} scriptType should be ${expectedScriptType}, got ${input.scriptType}`, ); + + // Verify previousOutput is present and structured correctly + assert.ok(input.previousOutput, `Input ${i} should have previousOutput`); + assert.ok( + typeof input.previousOutput === "object", + `Input ${i} previousOutput should be an object`, + ); + assert.ok( + typeof input.previousOutput.txid === "string", + `Input ${i} previousOutput.txid should be string`, + ); + assert.strictEqual( + input.previousOutput.txid.length, + 64, + `Input ${i} previousOutput.txid should be 64 chars`, + ); + assert.ok( + typeof input.previousOutput.vout === "number", + `Input ${i} previousOutput.vout should be number`, + ); + + // Verify scriptId structure when present (can be null for replay protection inputs) + if (input.scriptId !== null) { + assert.ok( + typeof input.scriptId === "object", + `Input ${i} scriptId should be an object when present`, + ); + assert.ok( + typeof input.scriptId.chain === "number", + `Input ${i} scriptId.chain should be number`, + ); + assert.ok( + typeof input.scriptId.index === "number", + `Input ${i} scriptId.index should be number`, + ); + } }); });