Skip to content

Commit 5489ce8

Browse files
Merge pull request #158 from BitGo/BTC-2650.psbt-introspection
feat(wasm-utxo): add PSBT introspection methods
2 parents b57f2c6 + 412feec commit 5489ce8

6 files changed

Lines changed: 175 additions & 26 deletions

File tree

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { BitGoPsbt as WasmBitGoPsbt } from "../wasm/wasm_utxo.js";
1+
import {
2+
BitGoPsbt as WasmBitGoPsbt,
3+
type PsbtInputData,
4+
type PsbtOutputData,
5+
type PsbtOutputDataWithAddress,
6+
} from "../wasm/wasm_utxo.js";
7+
import type { IPsbtIntrospectionWithAddress } from "../psbt.js";
28
import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js";
39
import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js";
410
import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js";
@@ -109,7 +115,7 @@ export type AddWalletOutputOptions = {
109115
value: bigint;
110116
};
111117

112-
export class BitGoPsbt {
118+
export class BitGoPsbt implements IPsbtIntrospectionWithAddress {
113119
protected constructor(protected _wasm: WasmBitGoPsbt) {}
114120

115121
/**
@@ -806,4 +812,65 @@ export class BitGoPsbt {
806812
getHalfSignedLegacyFormat(): Uint8Array {
807813
return this._wasm.extract_half_signed_legacy_tx();
808814
}
815+
816+
/**
817+
* Get the number of inputs in the PSBT
818+
* @returns The number of inputs
819+
*/
820+
get inputCount(): number {
821+
return this._wasm.input_count();
822+
}
823+
824+
/**
825+
* Get the number of outputs in the PSBT
826+
* @returns The number of outputs
827+
*/
828+
get outputCount(): number {
829+
return this._wasm.output_count();
830+
}
831+
832+
/**
833+
* Get all PSBT inputs as an array
834+
*
835+
* Returns raw PSBT input data including witness_utxo and derivation info.
836+
* For parsed transaction data with address identification, use
837+
* parseTransactionWithWalletKeys() instead.
838+
*
839+
* @returns Array of PsbtInputData objects
840+
*/
841+
getInputs(): PsbtInputData[] {
842+
return this._wasm.get_inputs() as PsbtInputData[];
843+
}
844+
845+
/**
846+
* Get all PSBT outputs as an array
847+
*
848+
* Returns raw PSBT output data without address resolution.
849+
* For output data with addresses, use getOutputsWithAddress().
850+
*
851+
* @returns Array of PsbtOutputData objects
852+
*/
853+
getOutputs(): PsbtOutputData[] {
854+
return this._wasm.get_outputs() as PsbtOutputData[];
855+
}
856+
857+
/**
858+
* Get all PSBT outputs with resolved address strings
859+
*
860+
* Unlike the generic Psbt class which requires a coin parameter,
861+
* BitGoPsbt automatically uses the network it was created with to resolve addresses.
862+
*
863+
* @returns Array of PsbtOutputDataWithAddress objects
864+
*
865+
* @example
866+
* ```typescript
867+
* const outputs = psbt.getOutputsWithAddress();
868+
* for (const output of outputs) {
869+
* console.log(`${output.address}: ${output.value} satoshis`);
870+
* }
871+
* ```
872+
*/
873+
getOutputsWithAddress(): PsbtOutputDataWithAddress[] {
874+
return this._wasm.get_outputs_with_address() as PsbtOutputDataWithAddress[];
875+
}
809876
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ export {
3737
type CreateEmptyZcashOptions,
3838
} from "./ZcashBitGoPsbt.js";
3939

40+
// PSBT introspection types (re-exported for consumer convenience)
41+
export type {
42+
PsbtBip32Derivation,
43+
PsbtInputData,
44+
PsbtOutputData,
45+
PsbtOutputDataWithAddress,
46+
PsbtWitnessUtxo,
47+
} from "../wasm/wasm_utxo.js";
48+
4049
import type { ScriptType } from "./scriptType.js";
4150

4251
/**

packages/wasm-utxo/js/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,8 @@ export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js";
130130
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js";
131131
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js";
132132
export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js";
133-
export { hasPsbtMagic } from "./psbt.js";
133+
export {
134+
hasPsbtMagic,
135+
type IPsbtIntrospection,
136+
type IPsbtIntrospectionWithAddress,
137+
} from "./psbt.js";

packages/wasm-utxo/js/psbt.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
import type { PsbtInputData, PsbtOutputData, PsbtOutputDataWithAddress } from "./wasm/wasm_utxo.js";
2+
3+
/** Common interface for PSBT introspection methods */
4+
export interface IPsbtIntrospection {
5+
readonly inputCount: number;
6+
readonly outputCount: number;
7+
getInputs(): PsbtInputData[];
8+
getOutputs(): PsbtOutputData[];
9+
}
10+
11+
/** Extended introspection with address resolution (no coin parameter needed) */
12+
export interface IPsbtIntrospectionWithAddress extends IPsbtIntrospection {
13+
getOutputsWithAddress(): PsbtOutputDataWithAddress[];
14+
}
15+
116
/** PSBT magic bytes: "psbt" (0x70 0x73 0x62 0x74) followed by separator 0xff */
217
const PSBT_MAGIC = new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff]);
318

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,40 @@ impl BitGoPsbt {
628628
}
629629
}
630630

631+
/// Get the number of inputs in the PSBT
632+
pub fn input_count(&self) -> usize {
633+
self.psbt.psbt().inputs.len()
634+
}
635+
636+
/// Get the number of outputs in the PSBT
637+
pub fn output_count(&self) -> usize {
638+
self.psbt.psbt().outputs.len()
639+
}
640+
641+
/// Get all PSBT inputs as an array of PsbtInputData
642+
///
643+
/// Returns an array with witness_utxo, bip32_derivation, and tap_bip32_derivation
644+
/// for each input.
645+
pub fn get_inputs(&self) -> Result<JsValue, WasmUtxoError> {
646+
crate::wasm::psbt::get_inputs_from_psbt(self.psbt.psbt())
647+
}
648+
649+
/// Get all PSBT outputs as an array of PsbtOutputData
650+
///
651+
/// Returns an array with script, value, bip32_derivation, and tap_bip32_derivation
652+
/// for each output.
653+
pub fn get_outputs(&self) -> Result<JsValue, WasmUtxoError> {
654+
crate::wasm::psbt::get_outputs_from_psbt(self.psbt.psbt())
655+
}
656+
657+
/// Get all PSBT outputs with resolved address strings.
658+
///
659+
/// Unlike the generic WrapPsbt which requires a coin parameter, BitGoPsbt
660+
/// uses the network it was created/deserialized with to resolve addresses.
661+
pub fn get_outputs_with_address(&self) -> Result<JsValue, WasmUtxoError> {
662+
crate::wasm::psbt::get_outputs_with_address_from_psbt(self.psbt.psbt(), self.psbt.network())
663+
}
664+
631665
/// Parse transaction with wallet keys to identify wallet inputs/outputs
632666
pub fn parse_transaction_with_wallet_keys(
633667
&self,

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

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,46 @@ impl PsbtOutputDataWithAddress {
203203
}
204204
}
205205

206+
// ============================================================================
207+
// Helper functions for PSBT introspection - shared by WrapPsbt and BitGoPsbt
208+
// ============================================================================
209+
210+
/// Get all PSBT inputs as an array of PsbtInputData
211+
pub fn get_inputs_from_psbt(psbt: &Psbt) -> Result<JsValue, WasmUtxoError> {
212+
let inputs: Vec<PsbtInputData> = psbt.inputs.iter().map(PsbtInputData::from).collect();
213+
inputs.try_to_js_value()
214+
}
215+
216+
/// Get all PSBT outputs as an array of PsbtOutputData
217+
pub fn get_outputs_from_psbt(psbt: &Psbt) -> Result<JsValue, WasmUtxoError> {
218+
let outputs: Vec<PsbtOutputData> = psbt
219+
.unsigned_tx
220+
.output
221+
.iter()
222+
.zip(psbt.outputs.iter())
223+
.map(|(tx_out, psbt_out)| PsbtOutputData::from(tx_out, psbt_out))
224+
.collect();
225+
outputs.try_to_js_value()
226+
}
227+
228+
/// Get all PSBT outputs with resolved address strings
229+
pub fn get_outputs_with_address_from_psbt(
230+
psbt: &Psbt,
231+
network: crate::Network,
232+
) -> Result<JsValue, WasmUtxoError> {
233+
let outputs: Vec<PsbtOutputDataWithAddress> = psbt
234+
.unsigned_tx
235+
.output
236+
.iter()
237+
.zip(psbt.outputs.iter())
238+
.map(|(tx_out, psbt_out)| {
239+
let base = PsbtOutputData::from(tx_out, psbt_out);
240+
PsbtOutputDataWithAddress::from(base, network)
241+
})
242+
.collect::<Result<Vec<_>, _>>()?;
243+
outputs.try_to_js_value()
244+
}
245+
206246
#[wasm_bindgen]
207247
pub struct WrapPsbt(Psbt);
208248

@@ -585,8 +625,7 @@ impl WrapPsbt {
585625
/// for each input. This is useful for introspecting the PSBT structure.
586626
#[wasm_bindgen(js_name = getInputs)]
587627
pub fn get_inputs(&self) -> Result<JsValue, WasmUtxoError> {
588-
let inputs: Vec<PsbtInputData> = self.0.inputs.iter().map(PsbtInputData::from).collect();
589-
inputs.try_to_js_value()
628+
get_inputs_from_psbt(&self.0)
590629
}
591630

592631
/// Get all PSBT outputs as an array of PsbtOutputData
@@ -595,15 +634,7 @@ impl WrapPsbt {
595634
/// for each output. This is useful for introspecting the PSBT structure.
596635
#[wasm_bindgen(js_name = getOutputs)]
597636
pub fn get_outputs(&self) -> Result<JsValue, WasmUtxoError> {
598-
let outputs: Vec<PsbtOutputData> = self
599-
.0
600-
.unsigned_tx
601-
.output
602-
.iter()
603-
.zip(self.0.outputs.iter())
604-
.map(|(tx_out, psbt_out)| PsbtOutputData::from(tx_out, psbt_out))
605-
.collect();
606-
outputs.try_to_js_value()
637+
get_outputs_from_psbt(&self.0)
607638
}
608639

609640
/// Get all PSBT outputs with resolved address strings.
@@ -614,18 +645,7 @@ impl WrapPsbt {
614645
pub fn get_outputs_with_address(&self, coin: &str) -> Result<JsValue, WasmUtxoError> {
615646
let network = crate::Network::from_coin_name(coin)
616647
.ok_or_else(|| WasmUtxoError::new(&format!("Unknown coin: {}", coin)))?;
617-
let outputs: Vec<PsbtOutputDataWithAddress> = self
618-
.0
619-
.unsigned_tx
620-
.output
621-
.iter()
622-
.zip(self.0.outputs.iter())
623-
.map(|(tx_out, psbt_out)| {
624-
let base = PsbtOutputData::from(tx_out, psbt_out);
625-
PsbtOutputDataWithAddress::from(base, network)
626-
})
627-
.collect::<Result<Vec<_>, _>>()?;
628-
outputs.try_to_js_value()
648+
get_outputs_with_address_from_psbt(&self.0, network)
629649
}
630650

631651
/// Get partial signatures for an input

0 commit comments

Comments
 (0)