From 1956ff43ffb5330df53972c9496ab8972394bb5d Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Wed, 1 Apr 2026 15:31:04 +0200 Subject: [PATCH] refactor(wasm-utxo): extract base classes for PSBT and transaction Introduce `PsbtBase` and `TransactionBase` to eliminate duplication across `Psbt`/`BitGoPsbt` and `Transaction`/`ZcashTransaction`/ `DashTransaction`. Common methods (input/output counts, serialization, KV access) are now implemented once in the base classes. On the Rust side, add a `WasmPsbtOps` trait and macro to generate consistent WASM bindings. Move `remove_input`/`remove_output` into the `PsbtAccess` trait for uniformity. Co-authored-by: llm-git --- .../wasm-utxo/js/descriptorWallet/Psbt.ts | 85 +---- .../js/fixedScriptWallet/BitGoPsbt.ts | 123 +------ packages/wasm-utxo/js/psbtBase.ts | 83 +++++ packages/wasm-utxo/js/transaction.ts | 161 +--------- packages/wasm-utxo/js/transactionBase.ts | 58 ++++ .../src/fixed_script_wallet/bitgo_psbt/mod.rs | 4 +- packages/wasm-utxo/src/psbt_ops.rs | 50 +-- .../src/wasm/fixed_script_wallet/mod.rs | 144 +-------- packages/wasm-utxo/src/wasm/mod.rs | 4 +- packages/wasm-utxo/src/wasm/psbt.rs | 299 +++++++++++------- packages/wasm-utxo/src/wasm/psbt_ops.rs | 119 +++++++ 11 files changed, 503 insertions(+), 627 deletions(-) create mode 100644 packages/wasm-utxo/js/psbtBase.ts create mode 100644 packages/wasm-utxo/js/transactionBase.ts create mode 100644 packages/wasm-utxo/src/wasm/psbt_ops.rs diff --git a/packages/wasm-utxo/js/descriptorWallet/Psbt.ts b/packages/wasm-utxo/js/descriptorWallet/Psbt.ts index e5f6802b68b..dbc06cb7953 100644 --- a/packages/wasm-utxo/js/descriptorWallet/Psbt.ts +++ b/packages/wasm-utxo/js/descriptorWallet/Psbt.ts @@ -3,29 +3,22 @@ import { type WasmBIP32, type WasmECPair, type WrapDescriptor, - type PsbtInputData, - type PsbtOutputData, type PsbtOutputDataWithAddress, } from "../wasm/wasm_utxo.js"; import type { IPsbt } from "../psbt.js"; -import type { PsbtKvKey } from "../fixedScriptWallet/BitGoKeySubtype.js"; import type { CoinName } from "../coinName.js"; -import type { BIP32 } from "../bip32.js"; import { Transaction } from "../transaction.js"; +import { PsbtBase } from "../psbtBase.js"; export type SignPsbtResult = { [inputIndex: number]: [pubkey: string][]; }; -export class Psbt implements IPsbt { - private _wasm: WasmPsbt; - +export class Psbt extends PsbtBase implements IPsbt { constructor(versionOrWasm?: number | WasmPsbt, lockTime?: number) { - if (versionOrWasm instanceof WasmPsbt) { - this._wasm = versionOrWasm; - } else { - this._wasm = new WasmPsbt(versionOrWasm, lockTime); - } + super( + versionOrWasm instanceof WasmPsbt ? versionOrWasm : new WasmPsbt(versionOrWasm, lockTime), + ); } /** @internal Access the underlying WASM instance */ @@ -45,48 +38,12 @@ export class Psbt implements IPsbt { // -- Serialization -- - serialize(): Uint8Array { - return this._wasm.serialize(); - } - clone(): Psbt { return new Psbt(this._wasm.clone()); } // -- IPsbt: introspection -- - inputCount(): number { - return this._wasm.input_count(); - } - - outputCount(): number { - return this._wasm.output_count(); - } - - version(): number { - return this._wasm.version(); - } - - lockTime(): number { - return this._wasm.lock_time(); - } - - unsignedTxId(): string { - return this._wasm.unsigned_tx_id(); - } - - getInputs(): PsbtInputData[] { - return this._wasm.get_inputs() as PsbtInputData[]; - } - - getOutputs(): PsbtOutputData[] { - return this._wasm.get_outputs() as PsbtOutputData[]; - } - - getGlobalXpubs(): BIP32[] { - return this._wasm.get_global_xpubs() as BIP32[]; - } - getOutputsWithAddress(coin: CoinName): PsbtOutputDataWithAddress[] { return this._wasm.get_outputs_with_address(coin) as PsbtOutputDataWithAddress[]; } @@ -122,38 +79,6 @@ export class Psbt implements IPsbt { return this._wasm.add_output(script, value); } - removeInput(index: number): void { - this._wasm.remove_input(index); - } - - removeOutput(index: number): void { - this._wasm.remove_output(index); - } - - setKV(key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_kv(key, value); - } - - getKV(key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_kv(key) ?? undefined; - } - - setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_input_kv(index, key, value); - } - - getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_input_kv(index, key) ?? undefined; - } - - setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_output_kv(index, key, value); - } - - getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_output_kv(index, key) ?? undefined; - } - // -- Descriptor updates -- updateInputWithDescriptor(inputIndex: number, descriptor: WrapDescriptor): void { diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index 89fd2318744..0c4936e0d6d 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -1,12 +1,10 @@ import { BitGoPsbt as WasmBitGoPsbt, FixedScriptWalletNamespace, - WasmBIP32, - type PsbtInputData, - type PsbtOutputData, type PsbtOutputDataWithAddress, } from "../wasm/wasm_utxo.js"; import type { IPsbtWithAddress } from "../psbt.js"; +import { PsbtBase } from "../psbtBase.js"; import { type WalletKeysArg, RootWalletKeys } from "./RootWalletKeys.js"; import { type ReplayProtectionArg, ReplayProtection } from "./ReplayProtection.js"; import { type BIP32Arg, BIP32, isBIP32Arg } from "../bip32.js"; @@ -14,7 +12,6 @@ import { type ECPairArg, ECPair } from "../ecpair.js"; import type { UtxolibName } from "../utxolibCompat.js"; import type { CoinName } from "../coinName.js"; import type { InputScriptType } from "./scriptType.js"; -import type { PsbtKvKey } from "./BitGoKeySubtype.js"; import { Transaction, DashTransaction, @@ -133,8 +130,10 @@ export type HydrationUnspent = { value: bigint; }; -export class BitGoPsbt implements IPsbtWithAddress { - protected constructor(protected _wasm: WasmBitGoPsbt) {} +export class BitGoPsbt extends PsbtBase implements IPsbtWithAddress { + protected constructor(wasm: WasmBitGoPsbt) { + super(wasm); + } /** * Get the underlying WASM instance @@ -532,64 +531,6 @@ export class BitGoPsbt implements IPsbtWithAddress { ); } - removeInput(index: number): void { - this._wasm.remove_input(index); - } - - removeOutput(index: number): void { - this._wasm.remove_output(index); - } - - /** - * Get the unsigned transaction ID - * @returns The unsigned transaction ID - */ - unsignedTxId(): string { - return this._wasm.unsigned_txid(); - } - - /** - * Get the transaction version - * @returns The transaction version number - */ - version(): number { - return this._wasm.version(); - } - - lockTime(): number { - return this._wasm.lock_time(); - } - - /** Set an arbitrary KV pair on the PSBT global map. */ - setKV(key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_kv(key, value); - } - - /** Get a KV value from the PSBT global map. Returns `undefined` if not present. */ - getKV(key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_kv(key) ?? undefined; - } - - /** Set an arbitrary KV pair on a specific PSBT input. */ - setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_input_kv(index, key, value); - } - - /** Get a KV value from a specific PSBT input. Returns `undefined` if not present. */ - getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_input_kv(index, key) ?? undefined; - } - - /** Set an arbitrary KV pair on a specific PSBT output. */ - setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { - this._wasm.set_output_kv(index, key, value); - } - - /** Get a KV value from a specific PSBT output. Returns `undefined` if not present. */ - getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { - return this._wasm.get_output_kv(index, key) ?? undefined; - } - /** * Parse transaction with wallet keys to identify wallet inputs/outputs * @param walletKeys - The wallet keys to use for identification @@ -850,15 +791,6 @@ export class BitGoPsbt implements IPsbtWithAddress { return this._wasm.verify_replay_protection_signature(inputIndex, rp.wasm); } - /** - * Serialize the PSBT to bytes - * - * @returns The serialized PSBT as a byte array - */ - serialize(): Uint8Array { - return this._wasm.serialize(); - } - /** * Generate and store MuSig2 nonces for all MuSig2 inputs * @@ -983,43 +915,6 @@ export class BitGoPsbt implements IPsbtWithAddress { return this._wasm.extract_half_signed_legacy_tx(); } - /** - * Get the number of inputs in the PSBT - * @returns The number of inputs - */ - inputCount(): number { - return this._wasm.input_count(); - } - - 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 * @@ -1039,14 +934,6 @@ export class BitGoPsbt implements IPsbtWithAddress { getOutputsWithAddress(): PsbtOutputDataWithAddress[] { return this._wasm.get_outputs_with_address() as PsbtOutputDataWithAddress[]; } - - /** - * Returns the unordered global xpubs from this PSBT as BIP32 instances. - */ - getGlobalXpubs(): BIP32[] { - const result = this._wasm.get_global_xpubs() as WasmBIP32[]; - return result.map((w) => BIP32.fromWasm(w)); - } } /** diff --git a/packages/wasm-utxo/js/psbtBase.ts b/packages/wasm-utxo/js/psbtBase.ts new file mode 100644 index 00000000000..cd370396b30 --- /dev/null +++ b/packages/wasm-utxo/js/psbtBase.ts @@ -0,0 +1,83 @@ +import type { PsbtInputData, PsbtOutputData, WasmBIP32 } from "./wasm/wasm_utxo.js"; +import { BIP32 } from "./bip32.js"; +import type { PsbtKvKey } from "./fixedScriptWallet/BitGoKeySubtype.js"; + +interface WasmPsbtBase { + input_count(): number; + output_count(): number; + version(): number; + lock_time(): number; + unsigned_tx_id(): string; + serialize(): Uint8Array; + get_inputs(): unknown; + get_outputs(): unknown; + get_global_xpubs(): unknown; + remove_input(index: number): void; + remove_output(index: number): void; + set_kv(key: unknown, value: Uint8Array): void; + get_kv(key: unknown): Uint8Array | null | undefined; + set_input_kv(index: number, key: unknown, value: Uint8Array): void; + get_input_kv(index: number, key: unknown): Uint8Array | null | undefined; + set_output_kv(index: number, key: unknown, value: Uint8Array): void; + get_output_kv(index: number, key: unknown): Uint8Array | null | undefined; +} + +export abstract class PsbtBase { + protected _wasm: W; + + constructor(wasm: W) { + this._wasm = wasm; + } + + inputCount(): number { + return this._wasm.input_count(); + } + outputCount(): number { + return this._wasm.output_count(); + } + version(): number { + return this._wasm.version(); + } + lockTime(): number { + return this._wasm.lock_time(); + } + unsignedTxId(): string { + return this._wasm.unsigned_tx_id(); + } + serialize(): Uint8Array { + return this._wasm.serialize(); + } + getInputs(): PsbtInputData[] { + return this._wasm.get_inputs() as PsbtInputData[]; + } + getOutputs(): PsbtOutputData[] { + return this._wasm.get_outputs() as PsbtOutputData[]; + } + getGlobalXpubs(): BIP32[] { + return (this._wasm.get_global_xpubs() as WasmBIP32[]).map((w) => BIP32.fromWasm(w)); + } + removeInput(index: number): void { + this._wasm.remove_input(index); + } + removeOutput(index: number): void { + this._wasm.remove_output(index); + } + setKV(key: PsbtKvKey, value: Uint8Array): void { + this._wasm.set_kv(key, value); + } + getKV(key: PsbtKvKey): Uint8Array | undefined { + return this._wasm.get_kv(key) ?? undefined; + } + setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { + this._wasm.set_input_kv(index, key, value); + } + getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { + return this._wasm.get_input_kv(index, key) ?? undefined; + } + setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void { + this._wasm.set_output_kv(index, key, value); + } + getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined { + return this._wasm.get_output_kv(index, key) ?? undefined; + } +} diff --git a/packages/wasm-utxo/js/transaction.ts b/packages/wasm-utxo/js/transaction.ts index 7cab71b8e70..26c03e14d86 100644 --- a/packages/wasm-utxo/js/transaction.ts +++ b/packages/wasm-utxo/js/transaction.ts @@ -1,12 +1,7 @@ -import { - WasmDashTransaction, - WasmTransaction, - WasmZcashTransaction, - type TxInputData, - type TxOutputData, - type TxOutputDataWithAddress, -} from "./wasm/wasm_utxo.js"; +import { WasmDashTransaction, WasmTransaction, WasmZcashTransaction } from "./wasm/wasm_utxo.js"; +import type { TxInputData, TxOutputData, TxOutputDataWithAddress } from "./wasm/wasm_utxo.js"; import type { CoinName } from "./coinName.js"; +import { TransactionBase } from "./transactionBase.js"; /** Common read-only interface shared by transactions and PSBTs */ export interface ITransactionCommon { @@ -30,8 +25,10 @@ export interface ITransaction extends ITransactionCommon { + private constructor(wasm: WasmTransaction) { + super(wasm); + } /** * Create an empty transaction (version 1, locktime 0) @@ -72,22 +69,6 @@ export class Transaction implements ITransaction { return this._wasm.add_output(script, value); } - toBytes(): Uint8Array { - return this._wasm.to_bytes(); - } - - /** - * Get the transaction ID (txid) - * - * The txid is the double SHA256 of the transaction bytes (excluding witness - * data for segwit transactions), displayed in reverse byte order as is standard. - * - * @returns The transaction ID as a hex string - */ - getId(): string { - return this._wasm.get_txid(); - } - /** * Get the virtual size of the transaction * @@ -99,34 +80,6 @@ export class Transaction implements ITransaction { return this._wasm.get_vsize(); } - inputCount(): number { - return this._wasm.input_count(); - } - - outputCount(): number { - return this._wasm.output_count(); - } - - version(): number { - return this._wasm.version(); - } - - lockTime(): number { - return this._wasm.lock_time(); - } - - getInputs(): TxInputData[] { - return this._wasm.get_inputs() as TxInputData[]; - } - - getOutputs(): TxOutputData[] { - return this._wasm.get_outputs() as TxOutputData[]; - } - - getOutputsWithAddress(coin: CoinName): TxOutputDataWithAddress[] { - return this._wasm.get_outputs_with_address(coin) as TxOutputDataWithAddress[]; - } - /** @internal */ get wasm(): WasmTransaction { return this._wasm; @@ -138,8 +91,10 @@ export class Transaction implements ITransaction { * * Provides a camelCase, strongly-typed API over the snake_case WASM bindings. */ -export class ZcashTransaction implements ITransaction { - private constructor(private _wasm: WasmZcashTransaction) {} +export class ZcashTransaction extends TransactionBase { + private constructor(wasm: WasmZcashTransaction) { + super(wasm); + } static fromBytes(bytes: Uint8Array): ZcashTransaction { return new ZcashTransaction(WasmZcashTransaction.from_bytes(bytes)); @@ -150,50 +105,6 @@ export class ZcashTransaction implements ITransaction { return new ZcashTransaction(wasm); } - toBytes(): Uint8Array { - return this._wasm.to_bytes(); - } - - /** - * Get the transaction ID (txid) - * - * The txid is the double SHA256 of the full Zcash transaction bytes, - * displayed in reverse byte order as is standard. - * - * @returns The transaction ID as a hex string - */ - getId(): string { - return this._wasm.get_txid(); - } - - inputCount(): number { - return this._wasm.input_count(); - } - - outputCount(): number { - return this._wasm.output_count(); - } - - version(): number { - return this._wasm.version(); - } - - lockTime(): number { - return this._wasm.lock_time(); - } - - getInputs(): TxInputData[] { - return this._wasm.get_inputs() as TxInputData[]; - } - - getOutputs(): TxOutputData[] { - return this._wasm.get_outputs() as TxOutputData[]; - } - - getOutputsWithAddress(coin: CoinName): TxOutputDataWithAddress[] { - return this._wasm.get_outputs_with_address(coin) as TxOutputDataWithAddress[]; - } - /** @internal */ get wasm(): WasmZcashTransaction { return this._wasm; @@ -205,8 +116,10 @@ export class ZcashTransaction implements ITransaction { * * Round-trip only: bytes -> parse -> bytes. */ -export class DashTransaction implements ITransaction { - private constructor(private _wasm: WasmDashTransaction) {} +export class DashTransaction extends TransactionBase { + private constructor(wasm: WasmDashTransaction) { + super(wasm); + } static fromBytes(bytes: Uint8Array): DashTransaction { return new DashTransaction(WasmDashTransaction.from_bytes(bytes)); @@ -217,50 +130,6 @@ export class DashTransaction implements ITransaction { return new DashTransaction(wasm); } - toBytes(): Uint8Array { - return this._wasm.to_bytes(); - } - - /** - * Get the transaction ID (txid) - * - * The txid is the double SHA256 of the full Dash transaction bytes, - * displayed in reverse byte order as is standard. - * - * @returns The transaction ID as a hex string - */ - getId(): string { - return this._wasm.get_txid(); - } - - inputCount(): number { - return this._wasm.input_count(); - } - - outputCount(): number { - return this._wasm.output_count(); - } - - version(): number { - return this._wasm.version(); - } - - lockTime(): number { - return this._wasm.lock_time(); - } - - getInputs(): TxInputData[] { - return this._wasm.get_inputs() as TxInputData[]; - } - - getOutputs(): TxOutputData[] { - return this._wasm.get_outputs() as TxOutputData[]; - } - - getOutputsWithAddress(coin: CoinName): TxOutputDataWithAddress[] { - return this._wasm.get_outputs_with_address(coin) as TxOutputDataWithAddress[]; - } - /** @internal */ get wasm(): WasmDashTransaction { return this._wasm; diff --git a/packages/wasm-utxo/js/transactionBase.ts b/packages/wasm-utxo/js/transactionBase.ts new file mode 100644 index 00000000000..37107763f5b --- /dev/null +++ b/packages/wasm-utxo/js/transactionBase.ts @@ -0,0 +1,58 @@ +import type { TxInputData, TxOutputData, TxOutputDataWithAddress } from "./wasm/wasm_utxo.js"; +import type { CoinName } from "./coinName.js"; +import type { ITransaction } from "./transaction.js"; + +interface WasmTransactionLike { + input_count(): number; + output_count(): number; + version(): number; + lock_time(): number; + to_bytes(): Uint8Array; + get_txid(): string; + get_inputs(): unknown; + get_outputs(): unknown; + get_outputs_with_address(coin: string): unknown; +} + +export abstract class TransactionBase implements ITransaction { + protected _wasm: W; + constructor(wasm: W) { + this._wasm = wasm; + } + + inputCount(): number { + return this._wasm.input_count(); + } + + outputCount(): number { + return this._wasm.output_count(); + } + + version(): number { + return this._wasm.version(); + } + + lockTime(): number { + return this._wasm.lock_time(); + } + + toBytes(): Uint8Array { + return this._wasm.to_bytes(); + } + + getId(): string { + return this._wasm.get_txid(); + } + + getInputs(): TxInputData[] { + return this._wasm.get_inputs() as TxInputData[]; + } + + getOutputs(): TxOutputData[] { + return this._wasm.get_outputs() as TxOutputData[]; + } + + getOutputsWithAddress(coin: CoinName): TxOutputDataWithAddress[] { + return this._wasm.get_outputs_with_address(coin) as TxOutputDataWithAddress[]; + } +} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index bd80fec66da..db1057248ee 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -1119,11 +1119,11 @@ impl BitGoPsbt { } pub fn remove_input(&mut self, index: usize) -> Result<(), String> { - crate::psbt_ops::remove_input(self.psbt_mut(), index) + crate::psbt_ops::PsbtAccess::remove_input(self, index) } pub fn remove_output(&mut self, index: usize) -> Result<(), String> { - crate::psbt_ops::remove_output(self.psbt_mut(), index) + crate::psbt_ops::PsbtAccess::remove_output(self, index) } pub fn network(&self) -> Network { diff --git a/packages/wasm-utxo/src/psbt_ops.rs b/packages/wasm-utxo/src/psbt_ops.rs index 171463bd138..63fa9d2f9b4 100644 --- a/packages/wasm-utxo/src/psbt_ops.rs +++ b/packages/wasm-utxo/src/psbt_ops.rs @@ -177,6 +177,32 @@ pub trait PsbtAccess { } Ok(self.psbt().outputs[index].proprietary.get(key).cloned()) } + + fn remove_input(&mut self, index: usize) -> Result<(), String> { + let psbt = self.psbt_mut(); + if index >= psbt.inputs.len() { + return Err(format!( + "input index {index} out of bounds (have {} inputs)", + psbt.inputs.len() + )); + } + psbt.unsigned_tx.input.remove(index); + psbt.inputs.remove(index); + Ok(()) + } + + fn remove_output(&mut self, index: usize) -> Result<(), String> { + let psbt = self.psbt_mut(); + if index >= psbt.outputs.len() { + return Err(format!( + "output index {index} out of bounds (have {} outputs)", + psbt.outputs.len() + )); + } + psbt.unsigned_tx.output.remove(index); + psbt.outputs.remove(index); + Ok(()) + } } fn check_bounds(index: usize, len: usize, name: &str) -> Result<(), String> { @@ -211,27 +237,3 @@ pub fn insert_output( psbt.outputs.insert(index, psbt_output); Ok(index) } - -pub fn remove_input(psbt: &mut Psbt, index: usize) -> Result<(), String> { - if index >= psbt.inputs.len() { - return Err(format!( - "input index {index} out of bounds (have {} inputs)", - psbt.inputs.len() - )); - } - psbt.unsigned_tx.input.remove(index); - psbt.inputs.remove(index); - Ok(()) -} - -pub fn remove_output(psbt: &mut Psbt, index: usize) -> Result<(), String> { - if index >= psbt.outputs.len() { - return Err(format!( - "output index {index} out of bounds (have {} outputs)", - psbt.outputs.len() - )); - } - psbt.unsigned_tx.output.remove(index); - psbt.outputs.remove(index); - Ok(()) -} 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 e0aaf6cb177..5868a49375d 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -14,8 +14,9 @@ use crate::fixed_script_wallet::{Chain, WalletScripts}; use crate::utxolib_compat::UtxolibNetwork; use crate::wasm::bip32::WasmBIP32; use crate::wasm::ecpair::WasmECPair; +use crate::wasm::psbt_ops::WasmPsbtOps; use crate::wasm::replay_protection::WasmReplayProtection; -use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; +use crate::wasm::try_from_js_value::TryFromJsValue; use crate::wasm::try_into_js_value::TryIntoJsValue; use crate::wasm::wallet_keys::WasmRootWalletKeys; @@ -562,18 +563,6 @@ impl BitGoPsbt { Ok(self.psbt.add_output_with_address(address, value)?) } - pub fn remove_input(&mut self, index: usize) -> Result<(), WasmUtxoError> { - self.psbt - .remove_input(index) - .map_err(|e| WasmUtxoError::new(&e)) - } - - pub fn remove_output(&mut self, index: usize) -> Result<(), WasmUtxoError> { - self.psbt - .remove_output(index) - .map_err(|e| WasmUtxoError::new(&e)) - } - #[allow(clippy::too_many_arguments)] pub fn add_wallet_input_at_index( &mut self, @@ -749,11 +738,6 @@ impl BitGoPsbt { ) } - /// Get the unsigned transaction ID - pub fn unsigned_txid(&self) -> String { - self.psbt.unsigned_txid().to_string() - } - /// Get the network of the PSBT pub fn network(&self) -> String { self.psbt.network().to_string() @@ -772,14 +756,6 @@ impl BitGoPsbt { } } - pub fn version(&self) -> i32 { - crate::psbt_ops::PsbtAccess::version(&self.psbt) - } - - pub fn lock_time(&self) -> u32 { - crate::psbt_ops::PsbtAccess::lock_time(&self.psbt) - } - /// Get the Zcash version group ID (returns None for non-Zcash PSBTs) pub fn version_group_id(&self) -> Option { use crate::fixed_script_wallet::bitgo_psbt::BitGoPsbt as InnerBitGoPsbt; @@ -800,124 +776,10 @@ impl BitGoPsbt { } } - pub fn input_count(&self) -> usize { - crate::psbt_ops::PsbtAccess::input_count(&self.psbt) - } - - pub fn output_count(&self) -> usize { - crate::psbt_ops::PsbtAccess::output_count(&self.psbt) - } - - pub fn get_inputs(&self) -> Result { - crate::wasm::psbt::get_inputs_from_psbt(self.psbt.psbt()) - } - - pub fn get_outputs(&self) -> Result { - crate::wasm::psbt::get_outputs_from_psbt(self.psbt.psbt()) - } - pub fn get_outputs_with_address(&self) -> Result { crate::wasm::psbt::get_outputs_with_address_from_psbt(self.psbt.psbt(), self.psbt.network()) } - pub fn get_global_xpubs(&self) -> JsValue { - crate::wasm::psbt::get_global_xpubs_from_psbt(self.psbt.psbt()) - } - - /// Set an arbitrary KV pair on the PSBT global map. - /// `key` must be `{ type: "unknown", keyType: number, data?: Uint8Array }` or - /// `{ type: "proprietary", prefix: Uint8Array, subtype: number, key?: Uint8Array }` or - /// `{ type: "bitgo", subtype: number, key?: Uint8Array }`. - pub fn set_kv(&mut self, key: JsValue, value: Vec) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::set_global_unknown_kv(&mut self.psbt, k, value), - PsbtKvKey::Proprietary(k) => { - PsbtAccess::set_global_proprietary_kv(&mut self.psbt, k, value) - } - } - Ok(()) - } - - /// Get a KV value from the PSBT global map. Returns `undefined` if not present. - pub fn get_kv(&self, key: JsValue) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - Ok(match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_global_unknown_kv(&self.psbt, &k), - PsbtKvKey::Proprietary(k) => PsbtAccess::get_global_proprietary_kv(&self.psbt, &k), - }) - } - - /// Set an arbitrary KV pair on a specific PSBT input. - pub fn set_input_kv( - &mut self, - index: usize, - key: JsValue, - value: Vec, - ) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => { - PsbtAccess::set_input_unknown_kv(&mut self.psbt, index, k, value) - } - PsbtKvKey::Proprietary(k) => { - PsbtAccess::set_input_proprietary_kv(&mut self.psbt, index, k, value) - } - } - .map_err(|e| WasmUtxoError::new(&e)) - } - - /// Get a KV value from a specific PSBT input. Returns `undefined` if not present. - pub fn get_input_kv( - &self, - index: usize, - key: JsValue, - ) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_input_unknown_kv(&self.psbt, index, &k), - PsbtKvKey::Proprietary(k) => { - PsbtAccess::get_input_proprietary_kv(&self.psbt, index, &k) - } - } - .map_err(|e| WasmUtxoError::new(&e)) - } - - /// Set an arbitrary KV pair on a specific PSBT output. - pub fn set_output_kv( - &mut self, - index: usize, - key: JsValue, - value: Vec, - ) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => { - PsbtAccess::set_output_unknown_kv(&mut self.psbt, index, k, value) - } - PsbtKvKey::Proprietary(k) => { - PsbtAccess::set_output_proprietary_kv(&mut self.psbt, index, k, value) - } - } - .map_err(|e| WasmUtxoError::new(&e)) - } - - /// Get a KV value from a specific PSBT output. Returns `undefined` if not present. - pub fn get_output_kv( - &self, - index: usize, - key: JsValue, - ) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_output_unknown_kv(&self.psbt, index, &k), - PsbtKvKey::Proprietary(k) => { - PsbtAccess::get_output_proprietary_kv(&self.psbt, index, &k) - } - } - .map_err(|e| WasmUtxoError::new(&e)) - } - /// Parse transaction with wallet keys to identify wallet inputs/outputs pub fn parse_transaction_with_wallet_keys( &self, @@ -1887,3 +1749,5 @@ impl BitGoPsbt { .map_err(|e| WasmUtxoError::new(&e)) } } + +impl_wasm_psbt_ops!(BitGoPsbt, psbt); diff --git a/packages/wasm-utxo/src/wasm/mod.rs b/packages/wasm-utxo/src/wasm/mod.rs index d236560b8d4..c47f472cb4c 100644 --- a/packages/wasm-utxo/src/wasm/mod.rs +++ b/packages/wasm-utxo/src/wasm/mod.rs @@ -4,13 +4,15 @@ mod bip322; mod dash_transaction; mod descriptor; mod ecpair; +mod psbt_ops; +#[macro_use] +mod psbt; mod fixed_script_wallet; mod inscriptions; mod inspect; mod message; mod miniscript; mod package_info; -mod psbt; mod recursive_tap_tree; mod replay_protection; mod transaction; diff --git a/packages/wasm-utxo/src/wasm/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs index 641742c0f49..00bf88cd30d 100644 --- a/packages/wasm-utxo/src/wasm/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -3,6 +3,7 @@ use crate::error::WasmUtxoError; use crate::wasm::bip32::WasmBIP32; use crate::wasm::descriptor::WrapDescriptorEnum; use crate::wasm::ecpair::WasmECPair; +use crate::wasm::psbt_ops::WasmPsbtOps; use crate::wasm::try_into_js_value::TryIntoJsValue; use crate::wasm::WrapDescriptor; use miniscript::bitcoin::bip32::Fingerprint; @@ -368,14 +369,6 @@ impl WrapPsbt { .expect("insert at len should never fail") } - pub fn remove_input(&mut self, index: usize) -> Result<(), JsError> { - crate::psbt_ops::remove_input(&mut self.0, index).map_err(|e| JsError::new(&e)) - } - - pub fn remove_output(&mut self, index: usize) -> Result<(), JsError> { - crate::psbt_ops::remove_output(&mut self.0, index).map_err(|e| JsError::new(&e)) - } - /// Get the unsigned transaction bytes /// /// # Returns @@ -626,32 +619,12 @@ impl WrapPsbt { Ok(crate::wasm::transaction::WasmTransaction::from_tx(tx)) } - pub fn input_count(&self) -> usize { - crate::psbt_ops::PsbtAccess::input_count(self) - } - - pub fn output_count(&self) -> usize { - crate::psbt_ops::PsbtAccess::output_count(self) - } - - pub fn get_inputs(&self) -> Result { - get_inputs_from_psbt(&self.0) - } - - pub fn get_outputs(&self) -> Result { - get_outputs_from_psbt(&self.0) - } - 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)))?; get_outputs_with_address_from_psbt(&self.0, network) } - pub fn get_global_xpubs(&self) -> JsValue { - get_global_xpubs_from_psbt(&self.0) - } - pub fn get_partial_signatures(&self, input_index: usize) -> Result { use crate::wasm::try_into_js_value::{collect_partial_signatures, TryIntoJsValue}; @@ -674,18 +647,6 @@ impl WrapPsbt { || input.tap_key_sig.is_some()) } - pub fn unsigned_tx_id(&self) -> String { - crate::psbt_ops::PsbtAccess::unsigned_tx_id(self) - } - - pub fn lock_time(&self) -> u32 { - crate::psbt_ops::PsbtAccess::lock_time(self) - } - - pub fn version(&self) -> i32 { - crate::psbt_ops::PsbtAccess::version(self) - } - pub fn validate_signature_at_input( &self, input_index: usize, @@ -777,89 +738,195 @@ impl WrapPsbt { // No matching signature found Ok(false) } +} - pub fn set_kv(&mut self, key: JsValue, value: Vec) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::set_global_unknown_kv(self, k, value), - PsbtKvKey::Proprietary(k) => PsbtAccess::set_global_proprietary_kv(self, k, value), - } - Ok(()) - } - - pub fn get_kv(&self, key: JsValue) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - Ok(match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_global_unknown_kv(self, &k), - PsbtKvKey::Proprietary(k) => PsbtAccess::get_global_proprietary_kv(self, &k), - }) - } - - pub fn set_input_kv( - &mut self, - index: usize, - key: JsValue, - value: Vec, - ) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::set_input_unknown_kv(self, index, k, value), - PsbtKvKey::Proprietary(k) => { - PsbtAccess::set_input_proprietary_kv(self, index, k, value) +/// Generates a `#[wasm_bindgen]` impl block exposing all shared PSBT delegation methods. +/// +/// - `impl_wasm_psbt_ops!(Type)` — the type itself implements `PsbtAccess` (e.g. `WrapPsbt`) +/// - `impl_wasm_psbt_ops!(Type, field)` — `self.field` implements `PsbtAccess` (e.g. `BitGoPsbt, psbt`) +/// +/// Requires `WasmPsbtOps` to be in scope at the invocation site. +macro_rules! impl_wasm_psbt_ops { + ($type:ty) => { + #[::wasm_bindgen::prelude::wasm_bindgen] + impl $type { + pub fn input_count(&self) -> usize { + self.wasm_input_count() } - } - .map_err(|e| WasmUtxoError::new(&e)) - } - - pub fn get_input_kv( - &self, - index: usize, - key: JsValue, - ) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_input_unknown_kv(self, index, &k), - PsbtKvKey::Proprietary(k) => PsbtAccess::get_input_proprietary_kv(self, index, &k), - } - .map_err(|e| WasmUtxoError::new(&e)) - } - - pub fn set_output_kv( - &mut self, - index: usize, - key: JsValue, - value: Vec, - ) -> Result<(), WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::set_output_unknown_kv(self, index, k, value), - PsbtKvKey::Proprietary(k) => { - PsbtAccess::set_output_proprietary_kv(self, index, k, value) + pub fn output_count(&self) -> usize { + self.wasm_output_count() + } + pub fn version(&self) -> i32 { + self.wasm_version() + } + pub fn lock_time(&self) -> u32 { + self.wasm_lock_time() + } + pub fn unsigned_tx_id(&self) -> String { + self.wasm_unsigned_tx_id() + } + pub fn remove_input( + &mut self, + index: usize, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.wasm_remove_input(index) + } + pub fn remove_output( + &mut self, + index: usize, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.wasm_remove_output(index) + } + pub fn get_inputs( + &self, + ) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> { + self.wasm_get_inputs() + } + pub fn get_outputs( + &self, + ) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> { + self.wasm_get_outputs() + } + pub fn get_global_xpubs(&self) -> ::wasm_bindgen::JsValue { + self.wasm_get_global_xpubs() + } + pub fn set_kv( + &mut self, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.wasm_set_kv(key, value) + } + pub fn get_kv( + &self, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.wasm_get_kv(key) + } + pub fn set_input_kv( + &mut self, + index: usize, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.wasm_set_input_kv(index, key, value) + } + pub fn get_input_kv( + &self, + index: usize, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.wasm_get_input_kv(index, key) + } + pub fn set_output_kv( + &mut self, + index: usize, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.wasm_set_output_kv(index, key, value) + } + pub fn get_output_kv( + &self, + index: usize, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.wasm_get_output_kv(index, key) } } - .map_err(|e| WasmUtxoError::new(&e)) - } - - pub fn get_output_kv( - &self, - index: usize, - key: JsValue, - ) -> Result>, WasmUtxoError> { - use crate::psbt_ops::PsbtAccess; - use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; - match PsbtKvKey::try_from_js_value(&key)? { - PsbtKvKey::Unknown(k) => PsbtAccess::get_output_unknown_kv(self, index, &k), - PsbtKvKey::Proprietary(k) => PsbtAccess::get_output_proprietary_kv(self, index, &k), + }; + ($type:ty, $field:ident) => { + #[::wasm_bindgen::prelude::wasm_bindgen] + impl $type { + pub fn input_count(&self) -> usize { + self.$field.wasm_input_count() + } + pub fn output_count(&self) -> usize { + self.$field.wasm_output_count() + } + pub fn version(&self) -> i32 { + self.$field.wasm_version() + } + pub fn lock_time(&self) -> u32 { + self.$field.wasm_lock_time() + } + pub fn unsigned_tx_id(&self) -> String { + self.$field.wasm_unsigned_tx_id() + } + pub fn remove_input( + &mut self, + index: usize, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.$field.wasm_remove_input(index) + } + pub fn remove_output( + &mut self, + index: usize, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.$field.wasm_remove_output(index) + } + pub fn get_inputs( + &self, + ) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> { + self.$field.wasm_get_inputs() + } + pub fn get_outputs( + &self, + ) -> Result<::wasm_bindgen::JsValue, $crate::error::WasmUtxoError> { + self.$field.wasm_get_outputs() + } + pub fn get_global_xpubs(&self) -> ::wasm_bindgen::JsValue { + self.$field.wasm_get_global_xpubs() + } + pub fn set_kv( + &mut self, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.$field.wasm_set_kv(key, value) + } + pub fn get_kv( + &self, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.$field.wasm_get_kv(key) + } + pub fn set_input_kv( + &mut self, + index: usize, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.$field.wasm_set_input_kv(index, key, value) + } + pub fn get_input_kv( + &self, + index: usize, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.$field.wasm_get_input_kv(index, key) + } + pub fn set_output_kv( + &mut self, + index: usize, + key: ::wasm_bindgen::JsValue, + value: Vec, + ) -> Result<(), $crate::error::WasmUtxoError> { + self.$field.wasm_set_output_kv(index, key, value) + } + pub fn get_output_kv( + &self, + index: usize, + key: ::wasm_bindgen::JsValue, + ) -> Result>, $crate::error::WasmUtxoError> { + self.$field.wasm_get_output_kv(index, key) + } } - .map_err(|e| WasmUtxoError::new(&e)) - } + }; } +impl_wasm_psbt_ops!(WrapPsbt); + impl crate::psbt_ops::PsbtAccess for WrapPsbt { fn psbt(&self) -> &Psbt { &self.0 diff --git a/packages/wasm-utxo/src/wasm/psbt_ops.rs b/packages/wasm-utxo/src/wasm/psbt_ops.rs new file mode 100644 index 00000000000..5294e3d709c --- /dev/null +++ b/packages/wasm-utxo/src/wasm/psbt_ops.rs @@ -0,0 +1,119 @@ +use crate::error::WasmUtxoError; +use crate::psbt_ops::PsbtAccess; +use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; +use wasm_bindgen::JsValue; + +/// WASM-layer trait providing shared method implementations for any `PsbtAccess` implementor. +/// Blanket-impl'd so both `WrapPsbt` and the inner `BitGoPsbt` get these for free. +pub(crate) trait WasmPsbtOps: PsbtAccess { + fn wasm_input_count(&self) -> usize { + PsbtAccess::input_count(self) + } + + fn wasm_output_count(&self) -> usize { + PsbtAccess::output_count(self) + } + + fn wasm_version(&self) -> i32 { + PsbtAccess::version(self) + } + + fn wasm_lock_time(&self) -> u32 { + PsbtAccess::lock_time(self) + } + + fn wasm_unsigned_tx_id(&self) -> String { + PsbtAccess::unsigned_tx_id(self) + } + + fn wasm_remove_input(&mut self, index: usize) -> Result<(), WasmUtxoError> { + PsbtAccess::remove_input(self, index).map_err(|e| WasmUtxoError::new(&e)) + } + + fn wasm_remove_output(&mut self, index: usize) -> Result<(), WasmUtxoError> { + PsbtAccess::remove_output(self, index).map_err(|e| WasmUtxoError::new(&e)) + } + + fn wasm_get_inputs(&self) -> Result { + crate::wasm::psbt::get_inputs_from_psbt(self.psbt()) + } + + fn wasm_get_outputs(&self) -> Result { + crate::wasm::psbt::get_outputs_from_psbt(self.psbt()) + } + + fn wasm_get_global_xpubs(&self) -> JsValue { + crate::wasm::psbt::get_global_xpubs_from_psbt(self.psbt()) + } + + fn wasm_set_kv(&mut self, key: JsValue, value: Vec) -> Result<(), WasmUtxoError> { + match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::set_global_unknown_kv(self, k, value), + PsbtKvKey::Proprietary(k) => PsbtAccess::set_global_proprietary_kv(self, k, value), + } + Ok(()) + } + + fn wasm_get_kv(&self, key: JsValue) -> Result>, WasmUtxoError> { + Ok(match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::get_global_unknown_kv(self, &k), + PsbtKvKey::Proprietary(k) => PsbtAccess::get_global_proprietary_kv(self, &k), + }) + } + + fn wasm_set_input_kv( + &mut self, + index: usize, + key: JsValue, + value: Vec, + ) -> Result<(), WasmUtxoError> { + match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::set_input_unknown_kv(self, index, k, value), + PsbtKvKey::Proprietary(k) => { + PsbtAccess::set_input_proprietary_kv(self, index, k, value) + } + } + .map_err(|e| WasmUtxoError::new(&e)) + } + + fn wasm_get_input_kv( + &self, + index: usize, + key: JsValue, + ) -> Result>, WasmUtxoError> { + match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::get_input_unknown_kv(self, index, &k), + PsbtKvKey::Proprietary(k) => PsbtAccess::get_input_proprietary_kv(self, index, &k), + } + .map_err(|e| WasmUtxoError::new(&e)) + } + + fn wasm_set_output_kv( + &mut self, + index: usize, + key: JsValue, + value: Vec, + ) -> Result<(), WasmUtxoError> { + match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::set_output_unknown_kv(self, index, k, value), + PsbtKvKey::Proprietary(k) => { + PsbtAccess::set_output_proprietary_kv(self, index, k, value) + } + } + .map_err(|e| WasmUtxoError::new(&e)) + } + + fn wasm_get_output_kv( + &self, + index: usize, + key: JsValue, + ) -> Result>, WasmUtxoError> { + match PsbtKvKey::try_from_js_value(&key)? { + PsbtKvKey::Unknown(k) => PsbtAccess::get_output_unknown_kv(self, index, &k), + PsbtKvKey::Proprietary(k) => PsbtAccess::get_output_proprietary_kv(self, index, &k), + } + .map_err(|e| WasmUtxoError::new(&e)) + } +} + +impl WasmPsbtOps for T {}