From 080b874b4ab10a1d717146dbbffc96656e06bdb0 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Mon, 2 Feb 2026 10:28:47 +0100 Subject: [PATCH] feat(wasm-utxo): expose generic PSBT KV accessors and package info at TS layer Add low-level PSBT key-value accessors (global, input, output) at Rust, WASM, and TypeScript layers, enabling consumers to read/write arbitrary unknown or proprietary fields. Also export BitGoKeySubtype enum values and package version info at the TypeScript layer. - Add PsbtAccess trait methods: set/get_{global,input,output}_{unknown,proprietary}_kv - Wire up WasmBitGoPsbt methods: {set,get}_{kv,input_kv,output_kv} - Export BitGoKeySubtype + PsbtKvKey types from fixedScriptWallet/index.ts - Add WasmUtxoNamespace.get_wasm_utxo_version() + getWasmUtxoVersion() TS helper - Remove obsolete set_version_info() method and test - Replace WasmUtxoVersionInfo::to_proprietary_kv/from_proprietary_kv with build_key_value() Issue: BTC-2992 Co-authored-by: llm-git --- .../wasm-utxo/js/descriptorWallet/Psbt.ts | 25 +++ .../js/fixedScriptWallet/BitGoKeySubtype.ts | 32 ++++ .../js/fixedScriptWallet/BitGoPsbt.ts | 31 ++++ .../wasm-utxo/js/fixedScriptWallet/index.ts | 11 +- packages/wasm-utxo/js/index.ts | 6 + packages/wasm-utxo/js/psbt.ts | 7 + .../src/fixed_script_wallet/bitgo_psbt/mod.rs | 40 ----- .../fixed_script_wallet/bitgo_psbt/propkv.rs | 45 ++---- packages/wasm-utxo/src/psbt_ops.rs | 152 +++++++++++++++++- .../src/wasm/fixed_script_wallet/mod.rs | 122 +++++++++++++- packages/wasm-utxo/src/wasm/mod.rs | 2 + packages/wasm-utxo/src/wasm/package_info.rs | 16 ++ packages/wasm-utxo/src/wasm/psbt.rs | 81 ++++++++++ .../wasm-utxo/src/wasm/try_from_js_value.rs | 35 ++++ .../wasm-utxo/src/wasm/try_into_js_value.rs | 9 ++ 15 files changed, 534 insertions(+), 80 deletions(-) create mode 100644 packages/wasm-utxo/js/fixedScriptWallet/BitGoKeySubtype.ts create mode 100644 packages/wasm-utxo/src/wasm/package_info.rs diff --git a/packages/wasm-utxo/js/descriptorWallet/Psbt.ts b/packages/wasm-utxo/js/descriptorWallet/Psbt.ts index a577d714914..e5f6802b68b 100644 --- a/packages/wasm-utxo/js/descriptorWallet/Psbt.ts +++ b/packages/wasm-utxo/js/descriptorWallet/Psbt.ts @@ -8,6 +8,7 @@ import { 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"; @@ -129,6 +130,30 @@ export class Psbt implements IPsbt { 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/BitGoKeySubtype.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoKeySubtype.ts new file mode 100644 index 00000000000..5b2492d6e4d --- /dev/null +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoKeySubtype.ts @@ -0,0 +1,32 @@ +import { FixedScriptWalletNamespace } from "../wasm/wasm_utxo.js"; + +/** + * Subtype constants for BitGo proprietary PSBT key-values. + * Values are loaded from the Rust enum at module init time — no duplication. + * The type shape is declared here for IDE support. + */ +export type BitGoKeySubtypeMap = { + readonly ZecConsensusBranchId: number; + readonly Musig2ParticipantPubKeys: number; + readonly Musig2PubNonce: number; + readonly Musig2PartialSig: number; + readonly PayGoAddressAttestationProof: number; + readonly Bip322Message: number; + readonly WasmUtxoSignedWith: number; +}; + +export const BitGoKeySubtype = + FixedScriptWalletNamespace.get_bitgo_key_subtypes() as BitGoKeySubtypeMap; +export type BitGoKeySubtype = BitGoKeySubtypeMap[keyof BitGoKeySubtypeMap]; + +/** + * A composable PSBT key for use with `setKV` / `getKV` / `setInputKV` / `getInputKV` etc. + * + * - `"unknown"`: stored in the PSBT `unknown` map (raw BIP-174 key-value pair) + * - `"proprietary"`: stored in the PSBT `proprietary` map with an arbitrary prefix + * - `"bitgo"`: stored in the PSBT `proprietary` map with the `BITGO` prefix + */ +export type PsbtKvKey = + | { type: "unknown"; keyType: number; data?: Uint8Array } + | { type: "proprietary"; prefix: Uint8Array; subtype: number; key?: Uint8Array } + | { type: "bitgo"; subtype: number; key?: Uint8Array }; diff --git a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts index 1ef602e6dc8..89fd2318744 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts @@ -14,6 +14,7 @@ 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, @@ -559,6 +560,36 @@ export class BitGoPsbt implements IPsbtWithAddress { 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 diff --git a/packages/wasm-utxo/js/fixedScriptWallet/index.ts b/packages/wasm-utxo/js/fixedScriptWallet/index.ts index 0d430f02071..e4c1ca6172f 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/index.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/index.ts @@ -34,6 +34,8 @@ export { type HydrationUnspent, } from "./BitGoPsbt.js"; +export { BitGoKeySubtype, type PsbtKvKey } from "./BitGoKeySubtype.js"; + // Zcash-specific PSBT subclass export { ZcashBitGoPsbt, @@ -41,15 +43,6 @@ 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 b793e4b966f..c9c1ea1ad22 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -1,4 +1,5 @@ import * as wasm from "./wasm/wasm_utxo.js"; +import { WasmUtxoNamespace } from "./wasm/wasm_utxo.js"; // we need to access the wasm module here, otherwise webpack gets all weird // and forgets to include it in the bundle @@ -21,6 +22,11 @@ export { ECPair } from "./ecpair.js"; export { BIP32 } from "./bip32.js"; export { Dimensions } from "./fixedScriptWallet/Dimensions.js"; +export type WasmUtxoVersionInfo = { version: string; gitHash: string }; +export function getWasmUtxoVersion(): WasmUtxoVersionInfo { + return WasmUtxoNamespace.get_wasm_utxo_version() as WasmUtxoVersionInfo; +} + export { type CoinName, getMainnet, isMainnet, isTestnet, isCoinName } from "./coinName.js"; export type { Triple } from "./triple.js"; export type { AddressFormat } from "./address.js"; diff --git a/packages/wasm-utxo/js/psbt.ts b/packages/wasm-utxo/js/psbt.ts index 8a3f1c3a077..5a0666d8995 100644 --- a/packages/wasm-utxo/js/psbt.ts +++ b/packages/wasm-utxo/js/psbt.ts @@ -1,6 +1,7 @@ import type { PsbtInputData, PsbtOutputData, PsbtOutputDataWithAddress } from "./wasm/wasm_utxo.js"; import type { BIP32 } from "./bip32.js"; import type { ITransactionCommon } from "./transaction.js"; +import type { PsbtKvKey } from "./fixedScriptWallet/BitGoKeySubtype.js"; /** Common interface for PSBT types */ export interface IPsbt extends ITransactionCommon { @@ -17,6 +18,12 @@ export interface IPsbt extends ITransactionCommon addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number; removeInput(index: number): void; removeOutput(index: number): void; + setKV(key: PsbtKvKey, value: Uint8Array): void; + getKV(key: PsbtKvKey): Uint8Array | undefined; + setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void; + getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined; + setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void; + getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined; } /** Extended PSBT with address resolution (no coin parameter needed) */ 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 3941bc99541..bd80fec66da 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 @@ -1380,17 +1380,6 @@ impl BitGoPsbt { ) } - /// Set version information in the PSBT's proprietary fields - /// - /// This embeds the wasm-utxo version and git hash into the PSBT's global - /// proprietary fields, allowing identification of which library version - /// processed the PSBT. - pub fn set_version_info(&mut self) { - let version_info = WasmUtxoVersionInfo::from_build_info(); - let (key, value) = version_info.to_proprietary_kv(); - self.psbt_mut().proprietary.insert(key, value); - } - pub fn finalize_input( &mut self, secp: &secp256k1::Secp256k1, @@ -5169,35 +5158,6 @@ mod tests { assert_eq!(decoded.compute_txid(), extracted_tx.compute_txid()); } - #[test] - fn test_set_version_info() { - use crate::fixed_script_wallet::test_utils::get_test_wallet_keys; - use miniscript::bitcoin::psbt::raw::ProprietaryKey; - - let wallet_keys = - crate::fixed_script_wallet::RootWalletKeys::new(get_test_wallet_keys("doge_1e19")); - - let mut psbt = BitGoPsbt::new(Network::Bitcoin, &wallet_keys, Some(2), Some(0)); - - // Set version info - psbt.set_version_info(); - - // Verify it was set in the proprietary fields - let version_key = ProprietaryKey { - prefix: BITGO.to_vec(), - subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8, - key: vec![], - }; - - assert!(psbt.psbt().proprietary.contains_key(&version_key)); - - // Verify the value is correctly formatted - let value = psbt.psbt().proprietary.get(&version_key).unwrap(); - let version_info = WasmUtxoVersionInfo::from_bytes(value).unwrap(); - assert!(!version_info.version.is_empty()); - assert!(!version_info.git_hash.is_empty()); - } - #[test] fn test_get_global_xpubs() { use crate::fixed_script_wallet::test_utils::get_test_wallet_keys; diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs index c386f336c14..4c48408d957 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs @@ -42,7 +42,7 @@ pub enum ProprietaryKeySubtype { Musig2PartialSig = 0x03, PayGoAddressAttestationProof = 0x04, Bip322Message = 0x05, - WasmUtxoVersion = 0x06, + WasmUtxoSignedWith = 0x06, } impl ProprietaryKeySubtype { @@ -54,7 +54,7 @@ impl ProprietaryKeySubtype { 0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig), 0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof), 0x05 => Some(ProprietaryKeySubtype::Bip322Message), - 0x06 => Some(ProprietaryKeySubtype::WasmUtxoVersion), + 0x06 => Some(ProprietaryKeySubtype::WasmUtxoSignedWith), _ => None, } } @@ -187,25 +187,14 @@ impl WasmUtxoVersionInfo { Ok(Self { version, git_hash }) } - /// Convert to proprietary key-value pair for PSBT global fields - pub fn to_proprietary_kv(&self) -> (ProprietaryKey, Vec) { - let key = ProprietaryKey { - prefix: BITGO.to_vec(), - subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8, - key: vec![], // Empty key data - only one version per PSBT - }; - (key, self.to_bytes()) - } - - /// Create from proprietary key-value pair - pub fn from_proprietary_kv(key: &ProprietaryKey, value: &[u8]) -> Result { - if key.prefix.as_slice() != BITGO { - return Err("Not a BITGO proprietary key".to_string()); - } - if key.subtype != ProprietaryKeySubtype::WasmUtxoVersion as u8 { - return Err("Not a WasmUtxoVersion proprietary key".to_string()); - } - Self::from_bytes(value) + /// Build a (ProprietaryKey, value) pair for per-input "signed-with" storage + pub fn build_key_value() -> (ProprietaryKey, Vec) { + BitGoKeyValue::new( + ProprietaryKeySubtype::WasmUtxoSignedWith, + vec![], + WasmUtxoVersionInfo::from_build_info().to_bytes(), + ) + .to_key_value() } } @@ -333,17 +322,15 @@ mod tests { } #[test] - fn test_version_info_proprietary_kv() { - let version_info = - WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string()); - - let (key, value) = version_info.to_proprietary_kv(); + fn test_version_info_build_key_value() { + let (key, value) = WasmUtxoVersionInfo::build_key_value(); assert_eq!(key.prefix, b"BITGO"); - assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoVersion as u8); + assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoSignedWith as u8); let empty_vec: Vec = vec![]; assert_eq!(key.key, empty_vec); - let deserialized = WasmUtxoVersionInfo::from_proprietary_kv(&key, &value).unwrap(); - assert_eq!(deserialized, version_info); + // The value should round-trip through from_bytes + let info = WasmUtxoVersionInfo::from_bytes(&value).unwrap(); + assert_eq!(info, WasmUtxoVersionInfo::from_build_info()); } } diff --git a/packages/wasm-utxo/src/psbt_ops.rs b/packages/wasm-utxo/src/psbt_ops.rs index 6aad1871afc..171463bd138 100644 --- a/packages/wasm-utxo/src/psbt_ops.rs +++ b/packages/wasm-utxo/src/psbt_ops.rs @@ -1,4 +1,4 @@ -use miniscript::bitcoin::{psbt, Psbt, TxIn, TxOut}; +use miniscript::bitcoin::{psbt, psbt::raw, Psbt, TxIn, TxOut}; /// Shared accessor trait for types that wrap a `Psbt`. /// @@ -27,6 +27,156 @@ pub trait PsbtAccess { fn unsigned_tx_id(&self) -> String { self.psbt().unsigned_tx.compute_txid().to_string() } + + // ------------------------------------------------------------------------- + // Global KV accessors + // ------------------------------------------------------------------------- + + fn set_global_unknown_kv(&mut self, key: raw::Key, value: Vec) { + self.psbt_mut().unknown.insert(key, value); + } + + fn get_global_unknown_kv(&self, key: &raw::Key) -> Option> { + self.psbt().unknown.get(key).cloned() + } + + fn set_global_proprietary_kv(&mut self, key: raw::ProprietaryKey, value: Vec) { + self.psbt_mut().proprietary.insert(key, value); + } + + fn get_global_proprietary_kv(&self, key: &raw::ProprietaryKey) -> Option> { + self.psbt().proprietary.get(key).cloned() + } + + // ------------------------------------------------------------------------- + // Per-input KV accessors + // ------------------------------------------------------------------------- + + fn set_input_unknown_kv( + &mut self, + index: usize, + key: raw::Key, + value: Vec, + ) -> Result<(), String> { + let len = self.psbt().inputs.len(); + if index >= len { + return Err(format!( + "input index {index} out of bounds (have {len} inputs)" + )); + } + self.psbt_mut().inputs[index].unknown.insert(key, value); + Ok(()) + } + + fn get_input_unknown_kv( + &self, + index: usize, + key: &raw::Key, + ) -> Result>, String> { + let len = self.psbt().inputs.len(); + if index >= len { + return Err(format!( + "input index {index} out of bounds (have {len} inputs)" + )); + } + Ok(self.psbt().inputs[index].unknown.get(key).cloned()) + } + + fn set_input_proprietary_kv( + &mut self, + index: usize, + key: raw::ProprietaryKey, + value: Vec, + ) -> Result<(), String> { + let len = self.psbt().inputs.len(); + if index >= len { + return Err(format!( + "input index {index} out of bounds (have {len} inputs)" + )); + } + self.psbt_mut().inputs[index].proprietary.insert(key, value); + Ok(()) + } + + fn get_input_proprietary_kv( + &self, + index: usize, + key: &raw::ProprietaryKey, + ) -> Result>, String> { + let len = self.psbt().inputs.len(); + if index >= len { + return Err(format!( + "input index {index} out of bounds (have {len} inputs)" + )); + } + Ok(self.psbt().inputs[index].proprietary.get(key).cloned()) + } + + // ------------------------------------------------------------------------- + // Per-output KV accessors + // ------------------------------------------------------------------------- + + fn set_output_unknown_kv( + &mut self, + index: usize, + key: raw::Key, + value: Vec, + ) -> Result<(), String> { + let len = self.psbt().outputs.len(); + if index >= len { + return Err(format!( + "output index {index} out of bounds (have {len} outputs)" + )); + } + self.psbt_mut().outputs[index].unknown.insert(key, value); + Ok(()) + } + + fn get_output_unknown_kv( + &self, + index: usize, + key: &raw::Key, + ) -> Result>, String> { + let len = self.psbt().outputs.len(); + if index >= len { + return Err(format!( + "output index {index} out of bounds (have {len} outputs)" + )); + } + Ok(self.psbt().outputs[index].unknown.get(key).cloned()) + } + + fn set_output_proprietary_kv( + &mut self, + index: usize, + key: raw::ProprietaryKey, + value: Vec, + ) -> Result<(), String> { + let len = self.psbt().outputs.len(); + if index >= len { + return Err(format!( + "output index {index} out of bounds (have {len} outputs)" + )); + } + self.psbt_mut().outputs[index] + .proprietary + .insert(key, value); + Ok(()) + } + + fn get_output_proprietary_kv( + &self, + index: usize, + key: &raw::ProprietaryKey, + ) -> Result>, String> { + let len = self.psbt().outputs.len(); + if index >= len { + return Err(format!( + "output index {index} out of bounds (have {len} outputs)" + )); + } + Ok(self.psbt().outputs[index].proprietary.get(key).cloned()) + } } fn check_bounds(index: usize, len: usize, name: &str) -> Result<(), String> { 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 854782cecab..e0aaf6cb177 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -15,7 +15,7 @@ use crate::utxolib_compat::UtxolibNetwork; use crate::wasm::bip32::WasmBIP32; use crate::wasm::ecpair::WasmECPair; use crate::wasm::replay_protection::WasmReplayProtection; -use crate::wasm::try_from_js_value::TryFromJsValue; +use crate::wasm::try_from_js_value::{PsbtKvKey, TryFromJsValue}; use crate::wasm::try_into_js_value::TryIntoJsValue; use crate::wasm::wallet_keys::WasmRootWalletKeys; @@ -244,6 +244,32 @@ impl FixedScriptWalletNamespace { .map_err(|e| WasmUtxoError::new(&e))?; Ok(WasmRootWalletKeys::from_inner(wallet_keys)) } + + /// Returns an object mapping BitGo proprietary key subtype names to their `u8` values. + /// Values are loaded directly from the Rust enum at build time — no duplication in TypeScript. + #[wasm_bindgen] + pub fn get_bitgo_key_subtypes() -> JsValue { + use crate::fixed_script_wallet::bitgo_psbt::ProprietaryKeySubtype as S; + let obj = js_sys::Object::new(); + for (name, val) in [ + ("ZecConsensusBranchId", S::ZecConsensusBranchId as u8), + ( + "Musig2ParticipantPubKeys", + S::Musig2ParticipantPubKeys as u8, + ), + ("Musig2PubNonce", S::Musig2PubNonce as u8), + ("Musig2PartialSig", S::Musig2PartialSig as u8), + ( + "PayGoAddressAttestationProof", + S::PayGoAddressAttestationProof as u8, + ), + ("Bip322Message", S::Bip322Message as u8), + ("WasmUtxoSignedWith", S::WasmUtxoSignedWith as u8), + ] { + js_sys::Reflect::set(&obj, &name.into(), &JsValue::from_f64(val as f64)).unwrap(); + } + obj.into() + } } #[wasm_bindgen] @@ -798,6 +824,100 @@ impl BitGoPsbt { 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, diff --git a/packages/wasm-utxo/src/wasm/mod.rs b/packages/wasm-utxo/src/wasm/mod.rs index 5aeadfd39ae..d236560b8d4 100644 --- a/packages/wasm-utxo/src/wasm/mod.rs +++ b/packages/wasm-utxo/src/wasm/mod.rs @@ -9,6 +9,7 @@ mod inscriptions; mod inspect; mod message; mod miniscript; +mod package_info; mod psbt; mod recursive_tap_tree; mod replay_protection; @@ -28,6 +29,7 @@ pub use fixed_script_wallet::{BitGoPsbt, FixedScriptWalletNamespace, WasmDimensi pub use inscriptions::InscriptionsNamespace; pub use message::MessageNamespace; pub use miniscript::WrapMiniscript; +pub use package_info::WasmUtxoNamespace; pub use psbt::WrapPsbt; pub use replay_protection::WasmReplayProtection; pub use transaction::{WasmTransaction, WasmZcashTransaction}; diff --git a/packages/wasm-utxo/src/wasm/package_info.rs b/packages/wasm-utxo/src/wasm/package_info.rs new file mode 100644 index 00000000000..836cd741dfc --- /dev/null +++ b/packages/wasm-utxo/src/wasm/package_info.rs @@ -0,0 +1,16 @@ +use crate::error::WasmUtxoError; +use crate::wasm::try_into_js_value::TryIntoJsValue; +use wasm_bindgen::prelude::*; + +/// Top-level package info namespace +#[wasm_bindgen] +pub struct WasmUtxoNamespace; + +#[wasm_bindgen] +impl WasmUtxoNamespace { + /// Returns the wasm-utxo build version as `{ version: string, gitHash: string }`. + pub fn get_wasm_utxo_version() -> Result { + use crate::fixed_script_wallet::bitgo_psbt::WasmUtxoVersionInfo; + WasmUtxoVersionInfo::from_build_info().try_to_js_value() + } +} diff --git a/packages/wasm-utxo/src/wasm/psbt.rs b/packages/wasm-utxo/src/wasm/psbt.rs index 62c392515fc..641742c0f49 100644 --- a/packages/wasm-utxo/src/wasm/psbt.rs +++ b/packages/wasm-utxo/src/wasm/psbt.rs @@ -777,6 +777,87 @@ 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) + } + } + .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) + } + } + .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), + } + .map_err(|e| WasmUtxoError::new(&e)) + } } impl crate::psbt_ops::PsbtAccess for WrapPsbt { diff --git a/packages/wasm-utxo/src/wasm/try_from_js_value.rs b/packages/wasm-utxo/src/wasm/try_from_js_value.rs index 0f2828a850e..d8aab945f6a 100644 --- a/packages/wasm-utxo/src/wasm/try_from_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_from_js_value.rs @@ -2,6 +2,7 @@ use std::ops::Deref; use crate::address::utxolib_compat::{CashAddr, UtxolibNetwork}; use crate::error::WasmUtxoError; +use miniscript::bitcoin::psbt::raw; use wasm_bindgen::JsValue; // ============================================================================= @@ -175,6 +176,40 @@ impl TryFromJsValue for crate::inscriptions::TapLeafScript { } } +// ============================================================================= +// PsbtKvKey: composable PSBT key for set_kv / get_kv WASM methods +// ============================================================================= + +/// A PSBT key that can represent either an unknown or proprietary record. +/// The `bitgo` variant is a convenience alias for proprietary with prefix `b"BITGO"`. +pub(crate) enum PsbtKvKey { + Unknown(raw::Key), + Proprietary(raw::ProprietaryKey), +} + +impl TryFromJsValue for PsbtKvKey { + fn try_from_js_value(value: &JsValue) -> Result { + let typ: String = get_field(value, "type")?; + match typ.as_str() { + "unknown" => Ok(PsbtKvKey::Unknown(raw::Key { + type_value: get_field(value, "keyType")?, + key: get_field::>>(value, "data")?.unwrap_or_default(), + })), + "proprietary" => Ok(PsbtKvKey::Proprietary(raw::ProprietaryKey { + prefix: get_field(value, "prefix")?, + subtype: get_field(value, "subtype")?, + key: get_field::>>(value, "key")?.unwrap_or_default(), + })), + "bitgo" => Ok(PsbtKvKey::Proprietary(raw::ProprietaryKey { + prefix: b"BITGO".to_vec(), + subtype: get_field(value, "subtype")?, + key: get_field::>>(value, "key")?.unwrap_or_default(), + })), + _ => Err(WasmUtxoError::new(&format!("Unknown PSBT key type: {typ}"))), + } + } +} + impl TryFromJsValue for crate::networks::Network { fn try_from_js_value(value: &JsValue) -> Result { let network_str = value 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 dab5d55bb35..713f010a7c5 100644 --- a/packages/wasm-utxo/src/wasm/try_into_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_into_js_value.rs @@ -401,6 +401,15 @@ impl TryIntoJsValue for crate::inscriptions::TapLeafScript { } } +impl TryIntoJsValue for crate::fixed_script_wallet::bitgo_psbt::WasmUtxoVersionInfo { + fn try_to_js_value(&self) -> Result { + js_obj!( + "version" => self.version.clone(), + "gitHash" => self.git_hash.clone() + ) + } +} + impl TryIntoJsValue for crate::inscriptions::InscriptionRevealData { fn try_to_js_value(&self) -> Result { js_obj!(