From 412feec110b0df617f2d35045a25c9d295e05e84 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 13 Feb 2026 10:40:31 +0100 Subject: [PATCH] feat(wasm-utxo): add PSBT introspection methods to BitGoPsbt Implement PSBT introspection interface for BitGoPsbt class with methods to examine inputs and outputs. Add utility interfaces for common PSBT introspection methods and re-export PSBT data types for consumer use. Issue: BTC-2650 Co-authored-by: llm-git --- .../js/fixedScriptWallet/BitGoPsbt.ts | 71 ++++++++++++++++++- .../wasm-utxo/js/fixedScriptWallet/index.ts | 9 +++ packages/wasm-utxo/js/index.ts | 6 +- packages/wasm-utxo/js/psbt.ts | 15 ++++ .../src/wasm/fixed_script_wallet/mod.rs | 34 +++++++++ packages/wasm-utxo/src/wasm/psbt.rs | 66 +++++++++++------ 6 files changed, 175 insertions(+), 26 deletions(-) diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index a8cea65..3e6623c 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -1,4 +1,10 @@ -import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js"; +import { + BitGoPsbt as WasmBitGoPsbt, + type PsbtInputData, + type PsbtOutputData, + type PsbtOutputDataWithAddress, +} from "../wasm/wasm_utxo.js"; +import type { IPsbtIntrospectionWithAddress } from "../psbt.js"; import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js"; import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js"; import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js"; @@ -109,7 +115,7 @@ export type AddWalletOutputOptions = { value: bigint; }; -export class BitGoPsbt { +export class BitGoPsbt implements IPsbtIntrospectionWithAddress { protected constructor(protected _wasm: WasmBitGoPsbt) {} /** @@ -806,4 +812,65 @@ export class BitGoPsbt { getHalfSignedLegacyFormat(): Uint8Array { return this._wasm.extract_half_signed_legacy_tx(); } + + /** + * Get the number of inputs in the PSBT + * @returns The number of inputs + */ + get inputCount(): number { + return this._wasm.input_count(); + } + + /** + * Get the number of outputs in the PSBT + * @returns The number of outputs + */ + get outputCount(): number { + return this._wasm.output_count(); + } + + /** + * Get all PSBT inputs as an array + * + * Returns raw PSBT input data including witness_utxo and derivation info. + * For parsed transaction data with address identification, use + * parseTransactionWithWalletKeys() instead. + * + * @returns Array of PsbtInputData objects + */ + getInputs(): PsbtInputData[] { + return this._wasm.get_inputs() as PsbtInputData[]; + } + + /** + * Get all PSBT outputs as an array + * + * Returns raw PSBT output data without address resolution. + * For output data with addresses, use getOutputsWithAddress(). + * + * @returns Array of PsbtOutputData objects + */ + getOutputs(): PsbtOutputData[] { + return this._wasm.get_outputs() as PsbtOutputData[]; + } + + /** + * Get all PSBT outputs with resolved address strings + * + * Unlike the generic Psbt class which requires a coin parameter, + * BitGoPsbt automatically uses the network it was created with to resolve addresses. + * + * @returns Array of PsbtOutputDataWithAddress objects + * + * @example + * ```typescript + * const outputs = psbt.getOutputsWithAddress(); + * for (const output of outputs) { + * console.log(`${output.address}: ${output.value} satoshis`); + * } + * ``` + */ + getOutputsWithAddress(): PsbtOutputDataWithAddress[] { + return this._wasm.get_outputs_with_address() as PsbtOutputDataWithAddress[]; + } } diff --git a/packages/wasm-utxo/js/fixedScriptWallet/index.ts b/packages/wasm-utxo/js/fixedScriptWallet/index.ts index e1891cc..d4689c4 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/index.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/index.ts @@ -37,6 +37,15 @@ export { type CreateEmptyZcashOptions, } from "./ZcashBitGoPsbt.js"; +// PSBT introspection types (re-exported for consumer convenience) +export type { + PsbtBip32Derivation, + PsbtInputData, + PsbtOutputData, + PsbtOutputDataWithAddress, + PsbtWitnessUtxo, +} from "../wasm/wasm_utxo.js"; + import type { ScriptType } from "./scriptType.js"; /** diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index 8b30e91..85d135c 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -130,4 +130,8 @@ export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js"; export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js"; export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js"; export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js"; -export { hasPsbtMagic } from "./psbt.js"; +export { + hasPsbtMagic, + type IPsbtIntrospection, + type IPsbtIntrospectionWithAddress, +} from "./psbt.js"; diff --git a/packages/wasm-utxo/js/psbt.ts b/packages/wasm-utxo/js/psbt.ts index 98929dd..005bae5 100644 --- a/packages/wasm-utxo/js/psbt.ts +++ b/packages/wasm-utxo/js/psbt.ts @@ -1,3 +1,18 @@ +import type { PsbtInputData, PsbtOutputData, PsbtOutputDataWithAddress } from "./wasm/wasm_utxo.js"; + +/** Common interface for PSBT introspection methods */ +export interface IPsbtIntrospection { + readonly inputCount: number; + readonly outputCount: number; + getInputs(): PsbtInputData[]; + getOutputs(): PsbtOutputData[]; +} + +/** Extended introspection with address resolution (no coin parameter needed) */ +export interface IPsbtIntrospectionWithAddress extends IPsbtIntrospection { + getOutputsWithAddress(): PsbtOutputDataWithAddress[]; +} + /** PSBT magic bytes: "psbt" (0x70 0x73 0x62 0x74) followed by separator 0xff */ const PSBT_MAGIC = new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff]); diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs index 074b11a..b7a926a 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -628,6 +628,40 @@ impl BitGoPsbt { } } + /// Get the number of inputs in the PSBT + pub fn input_count(&self) -> usize { + self.psbt.psbt().inputs.len() + } + + /// Get the number of outputs in the PSBT + pub fn output_count(&self) -> usize { + self.psbt.psbt().outputs.len() + } + + /// Get all PSBT inputs as an array of PsbtInputData + /// + /// Returns an array with witness_utxo, bip32_derivation, and tap_bip32_derivation + /// for each input. + pub fn get_inputs(&self) -> Result { + crate::wasm::psbt::get_inputs_from_psbt(self.psbt.psbt()) + } + + /// Get all PSBT outputs as an array of PsbtOutputData + /// + /// Returns an array with script, value, bip32_derivation, and tap_bip32_derivation + /// for each output. + pub fn get_outputs(&self) -> Result { + crate::wasm::psbt::get_outputs_from_psbt(self.psbt.psbt()) + } + + /// Get all PSBT outputs with resolved address strings. + /// + /// Unlike the generic WrapPsbt which requires a coin parameter, BitGoPsbt + /// uses the network it was created/deserialized with to resolve addresses. + pub fn get_outputs_with_address(&self) -> Result { + crate::wasm::psbt::get_outputs_with_address_from_psbt(self.psbt.psbt(), self.psbt.network()) + } + /// Parse transaction with wallet keys to identify wallet inputs/outputs pub fn parse_transaction_with_wallet_keys( &self, diff --git a/packages/wasm-utxo/src/wasm/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs index f03cbf4..e2f44be 100644 --- a/packages/wasm-utxo/src/wasm/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -203,6 +203,46 @@ impl PsbtOutputDataWithAddress { } } +// ============================================================================ +// Helper functions for PSBT introspection - shared by WrapPsbt and BitGoPsbt +// ============================================================================ + +/// Get all PSBT inputs as an array of PsbtInputData +pub fn get_inputs_from_psbt(psbt: &Psbt) -> Result { + let inputs: Vec = psbt.inputs.iter().map(PsbtInputData::from).collect(); + inputs.try_to_js_value() +} + +/// Get all PSBT outputs as an array of PsbtOutputData +pub fn get_outputs_from_psbt(psbt: &Psbt) -> Result { + let outputs: Vec = psbt + .unsigned_tx + .output + .iter() + .zip(psbt.outputs.iter()) + .map(|(tx_out, psbt_out)| PsbtOutputData::from(tx_out, psbt_out)) + .collect(); + outputs.try_to_js_value() +} + +/// Get all PSBT outputs with resolved address strings +pub fn get_outputs_with_address_from_psbt( + psbt: &Psbt, + network: crate::Network, +) -> Result { + let outputs: Vec = psbt + .unsigned_tx + .output + .iter() + .zip(psbt.outputs.iter()) + .map(|(tx_out, psbt_out)| { + let base = PsbtOutputData::from(tx_out, psbt_out); + PsbtOutputDataWithAddress::from(base, network) + }) + .collect::, _>>()?; + outputs.try_to_js_value() +} + #[wasm_bindgen] pub struct WrapPsbt(Psbt); @@ -585,8 +625,7 @@ impl WrapPsbt { /// for each input. This is useful for introspecting the PSBT structure. #[wasm_bindgen(js_name = getInputs)] pub fn get_inputs(&self) -> Result { - let inputs: Vec = self.0.inputs.iter().map(PsbtInputData::from).collect(); - inputs.try_to_js_value() + get_inputs_from_psbt(&self.0) } /// Get all PSBT outputs as an array of PsbtOutputData @@ -595,15 +634,7 @@ impl WrapPsbt { /// for each output. This is useful for introspecting the PSBT structure. #[wasm_bindgen(js_name = getOutputs)] pub fn get_outputs(&self) -> Result { - let outputs: Vec = self - .0 - .unsigned_tx - .output - .iter() - .zip(self.0.outputs.iter()) - .map(|(tx_out, psbt_out)| PsbtOutputData::from(tx_out, psbt_out)) - .collect(); - outputs.try_to_js_value() + get_outputs_from_psbt(&self.0) } /// Get all PSBT outputs with resolved address strings. @@ -614,18 +645,7 @@ impl WrapPsbt { pub fn get_outputs_with_address(&self, coin: &str) -> Result { let network = crate::Network::from_coin_name(coin) .ok_or_else(|| WasmUtxoError::new(&format!("Unknown coin: {}", coin)))?; - let outputs: Vec = self - .0 - .unsigned_tx - .output - .iter() - .zip(self.0.outputs.iter()) - .map(|(tx_out, psbt_out)| { - let base = PsbtOutputData::from(tx_out, psbt_out); - PsbtOutputDataWithAddress::from(base, network) - }) - .collect::, _>>()?; - outputs.try_to_js_value() + get_outputs_with_address_from_psbt(&self.0, network) } /// Get partial signatures for an input