Skip to content

Commit 3f582fc

Browse files
OttoAllmendingerllm-git
andcommitted
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 <llm-git@ttll.de>
1 parent 0a8ad97 commit 3f582fc

File tree

11 files changed

+419
-80
lines changed

11 files changed

+419
-80
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { FixedScriptWalletNamespace } from "../wasm/wasm_utxo.js";
2+
3+
/**
4+
* Subtype constants for BitGo proprietary PSBT key-values.
5+
* Values are loaded from the Rust enum at module init time — no duplication.
6+
* The type shape is declared here for IDE support.
7+
*/
8+
export type BitGoKeySubtypeMap = {
9+
readonly ZecConsensusBranchId: number;
10+
readonly Musig2ParticipantPubKeys: number;
11+
readonly Musig2PubNonce: number;
12+
readonly Musig2PartialSig: number;
13+
readonly PayGoAddressAttestationProof: number;
14+
readonly Bip322Message: number;
15+
readonly WasmUtxoSignedWith: number;
16+
};
17+
18+
export const BitGoKeySubtype =
19+
FixedScriptWalletNamespace.get_bitgo_key_subtypes() as BitGoKeySubtypeMap;
20+
export type BitGoKeySubtype = BitGoKeySubtypeMap[keyof BitGoKeySubtypeMap];
21+
22+
/**
23+
* A composable PSBT key for use with `setKV` / `getKV` / `setInputKV` / `getInputKV` etc.
24+
*
25+
* - `"unknown"`: stored in the PSBT `unknown` map (raw BIP-174 key-value pair)
26+
* - `"proprietary"`: stored in the PSBT `proprietary` map with an arbitrary prefix
27+
* - `"bitgo"`: stored in the PSBT `proprietary` map with the `BITGO` prefix
28+
*/
29+
export type PsbtKvKey =
30+
| { type: "unknown"; keyType: number; data?: Uint8Array }
31+
| { type: "proprietary"; prefix: Uint8Array; subtype: number; key?: Uint8Array }
32+
| { type: "bitgo"; subtype: number; key?: Uint8Array };

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { type ECPairArg, ECPair } from "../ecpair.js";
1414
import type { UtxolibName } from "../utxolibCompat.js";
1515
import type { CoinName } from "../coinName.js";
1616
import type { InputScriptType } from "./scriptType.js";
17+
import type { PsbtKvKey } from "./BitGoKeySubtype.js";
1718
import {
1819
Transaction,
1920
DashTransaction,
@@ -559,6 +560,36 @@ export class BitGoPsbt implements IPsbtWithAddress {
559560
return this._wasm.lock_time();
560561
}
561562

563+
/** Set an arbitrary KV pair on the PSBT global map. */
564+
setKV(key: PsbtKvKey, value: Uint8Array): void {
565+
this._wasm.set_kv(key, value);
566+
}
567+
568+
/** Get a KV value from the PSBT global map. Returns `undefined` if not present. */
569+
getKV(key: PsbtKvKey): Uint8Array | undefined {
570+
return this._wasm.get_kv(key) ?? undefined;
571+
}
572+
573+
/** Set an arbitrary KV pair on a specific PSBT input. */
574+
setInputKV(index: number, key: PsbtKvKey, value: Uint8Array): void {
575+
this._wasm.set_input_kv(index, key, value);
576+
}
577+
578+
/** Get a KV value from a specific PSBT input. Returns `undefined` if not present. */
579+
getInputKV(index: number, key: PsbtKvKey): Uint8Array | undefined {
580+
return this._wasm.get_input_kv(index, key) ?? undefined;
581+
}
582+
583+
/** Set an arbitrary KV pair on a specific PSBT output. */
584+
setOutputKV(index: number, key: PsbtKvKey, value: Uint8Array): void {
585+
this._wasm.set_output_kv(index, key, value);
586+
}
587+
588+
/** Get a KV value from a specific PSBT output. Returns `undefined` if not present. */
589+
getOutputKV(index: number, key: PsbtKvKey): Uint8Array | undefined {
590+
return this._wasm.get_output_kv(index, key) ?? undefined;
591+
}
592+
562593
/**
563594
* Parse transaction with wallet keys to identify wallet inputs/outputs
564595
* @param walletKeys - The wallet keys to use for identification

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,15 @@ export {
3434
type HydrationUnspent,
3535
} from "./BitGoPsbt.js";
3636

37+
export { BitGoKeySubtype, type PsbtKvKey } from "./BitGoKeySubtype.js";
38+
3739
// Zcash-specific PSBT subclass
3840
export {
3941
ZcashBitGoPsbt,
4042
type ZcashNetworkName,
4143
type CreateEmptyZcashOptions,
4244
} from "./ZcashBitGoPsbt.js";
4345

44-
// PSBT introspection types (re-exported for consumer convenience)
45-
export type {
46-
PsbtBip32Derivation,
47-
PsbtInputData,
48-
PsbtOutputData,
49-
PsbtOutputDataWithAddress,
50-
PsbtWitnessUtxo,
51-
} from "../wasm/wasm_utxo.js";
52-
5346
import type { ScriptType } from "./scriptType.js";
5447

5548
/**

packages/wasm-utxo/js/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as wasm from "./wasm/wasm_utxo.js";
2+
import { WasmUtxoNamespace } from "./wasm/wasm_utxo.js";
23

34
// we need to access the wasm module here, otherwise webpack gets all weird
45
// and forgets to include it in the bundle
@@ -21,6 +22,11 @@ export { ECPair } from "./ecpair.js";
2122
export { BIP32 } from "./bip32.js";
2223
export { Dimensions } from "./fixedScriptWallet/Dimensions.js";
2324

25+
export type WasmUtxoVersionInfo = { version: string; gitHash: string };
26+
export function getWasmUtxoVersion(): WasmUtxoVersionInfo {
27+
return WasmUtxoNamespace.get_wasm_utxo_version() as WasmUtxoVersionInfo;
28+
}
29+
2430
export { type CoinName, getMainnet, isMainnet, isTestnet, isCoinName } from "./coinName.js";
2531
export type { Triple } from "./triple.js";
2632
export type { AddressFormat } from "./address.js";

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

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,17 +1380,6 @@ impl BitGoPsbt {
13801380
)
13811381
}
13821382

1383-
/// Set version information in the PSBT's proprietary fields
1384-
///
1385-
/// This embeds the wasm-utxo version and git hash into the PSBT's global
1386-
/// proprietary fields, allowing identification of which library version
1387-
/// processed the PSBT.
1388-
pub fn set_version_info(&mut self) {
1389-
let version_info = WasmUtxoVersionInfo::from_build_info();
1390-
let (key, value) = version_info.to_proprietary_kv();
1391-
self.psbt_mut().proprietary.insert(key, value);
1392-
}
1393-
13941383
pub fn finalize_input<C: secp256k1::Verification>(
13951384
&mut self,
13961385
secp: &secp256k1::Secp256k1<C>,
@@ -5169,35 +5158,6 @@ mod tests {
51695158
assert_eq!(decoded.compute_txid(), extracted_tx.compute_txid());
51705159
}
51715160

5172-
#[test]
5173-
fn test_set_version_info() {
5174-
use crate::fixed_script_wallet::test_utils::get_test_wallet_keys;
5175-
use miniscript::bitcoin::psbt::raw::ProprietaryKey;
5176-
5177-
let wallet_keys =
5178-
crate::fixed_script_wallet::RootWalletKeys::new(get_test_wallet_keys("doge_1e19"));
5179-
5180-
let mut psbt = BitGoPsbt::new(Network::Bitcoin, &wallet_keys, Some(2), Some(0));
5181-
5182-
// Set version info
5183-
psbt.set_version_info();
5184-
5185-
// Verify it was set in the proprietary fields
5186-
let version_key = ProprietaryKey {
5187-
prefix: BITGO.to_vec(),
5188-
subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8,
5189-
key: vec![],
5190-
};
5191-
5192-
assert!(psbt.psbt().proprietary.contains_key(&version_key));
5193-
5194-
// Verify the value is correctly formatted
5195-
let value = psbt.psbt().proprietary.get(&version_key).unwrap();
5196-
let version_info = WasmUtxoVersionInfo::from_bytes(value).unwrap();
5197-
assert!(!version_info.version.is_empty());
5198-
assert!(!version_info.git_hash.is_empty());
5199-
}
5200-
52015161
#[test]
52025162
fn test_get_global_xpubs() {
52035163
use crate::fixed_script_wallet::test_utils::get_test_wallet_keys;

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub enum ProprietaryKeySubtype {
4242
Musig2PartialSig = 0x03,
4343
PayGoAddressAttestationProof = 0x04,
4444
Bip322Message = 0x05,
45-
WasmUtxoVersion = 0x06,
45+
WasmUtxoSignedWith = 0x06,
4646
}
4747

4848
impl ProprietaryKeySubtype {
@@ -54,7 +54,7 @@ impl ProprietaryKeySubtype {
5454
0x03 => Some(ProprietaryKeySubtype::Musig2PartialSig),
5555
0x04 => Some(ProprietaryKeySubtype::PayGoAddressAttestationProof),
5656
0x05 => Some(ProprietaryKeySubtype::Bip322Message),
57-
0x06 => Some(ProprietaryKeySubtype::WasmUtxoVersion),
57+
0x06 => Some(ProprietaryKeySubtype::WasmUtxoSignedWith),
5858
_ => None,
5959
}
6060
}
@@ -187,25 +187,14 @@ impl WasmUtxoVersionInfo {
187187
Ok(Self { version, git_hash })
188188
}
189189

190-
/// Convert to proprietary key-value pair for PSBT global fields
191-
pub fn to_proprietary_kv(&self) -> (ProprietaryKey, Vec<u8>) {
192-
let key = ProprietaryKey {
193-
prefix: BITGO.to_vec(),
194-
subtype: ProprietaryKeySubtype::WasmUtxoVersion as u8,
195-
key: vec![], // Empty key data - only one version per PSBT
196-
};
197-
(key, self.to_bytes())
198-
}
199-
200-
/// Create from proprietary key-value pair
201-
pub fn from_proprietary_kv(key: &ProprietaryKey, value: &[u8]) -> Result<Self, String> {
202-
if key.prefix.as_slice() != BITGO {
203-
return Err("Not a BITGO proprietary key".to_string());
204-
}
205-
if key.subtype != ProprietaryKeySubtype::WasmUtxoVersion as u8 {
206-
return Err("Not a WasmUtxoVersion proprietary key".to_string());
207-
}
208-
Self::from_bytes(value)
190+
/// Build a (ProprietaryKey, value) pair for per-input "signed-with" storage
191+
pub fn build_key_value() -> (ProprietaryKey, Vec<u8>) {
192+
BitGoKeyValue::new(
193+
ProprietaryKeySubtype::WasmUtxoSignedWith,
194+
vec![],
195+
WasmUtxoVersionInfo::from_build_info().to_bytes(),
196+
)
197+
.to_key_value()
209198
}
210199
}
211200

@@ -333,17 +322,15 @@ mod tests {
333322
}
334323

335324
#[test]
336-
fn test_version_info_proprietary_kv() {
337-
let version_info =
338-
WasmUtxoVersionInfo::new("0.0.2".to_string(), "abc123def456".to_string());
339-
340-
let (key, value) = version_info.to_proprietary_kv();
325+
fn test_version_info_build_key_value() {
326+
let (key, value) = WasmUtxoVersionInfo::build_key_value();
341327
assert_eq!(key.prefix, b"BITGO");
342-
assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoVersion as u8);
328+
assert_eq!(key.subtype, ProprietaryKeySubtype::WasmUtxoSignedWith as u8);
343329
let empty_vec: Vec<u8> = vec![];
344330
assert_eq!(key.key, empty_vec);
345331

346-
let deserialized = WasmUtxoVersionInfo::from_proprietary_kv(&key, &value).unwrap();
347-
assert_eq!(deserialized, version_info);
332+
// The value should round-trip through from_bytes
333+
let info = WasmUtxoVersionInfo::from_bytes(&value).unwrap();
334+
assert_eq!(info, WasmUtxoVersionInfo::from_build_info());
348335
}
349336
}

0 commit comments

Comments
 (0)