Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/wasm-utxo/js/descriptorWallet/Psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoKeySubtype.ts
Original file line number Diff line number Diff line change
@@ -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 };
31 changes: 31 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/BitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 2 additions & 9 deletions packages/wasm-utxo/js/fixedScriptWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,15 @@ export {
type HydrationUnspent,
} from "./BitGoPsbt.js";

export { BitGoKeySubtype, type PsbtKvKey } from "./BitGoKeySubtype.js";

// Zcash-specific PSBT subclass
export {
ZcashBitGoPsbt,
type ZcashNetworkName,
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";

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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";
Expand Down
7 changes: 7 additions & 0 deletions packages/wasm-utxo/js/psbt.ts
Original file line number Diff line number Diff line change
@@ -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<PsbtInputData, PsbtOutputData> {
Expand All @@ -17,6 +18,12 @@ export interface IPsbt extends ITransactionCommon<PsbtInputData, PsbtOutputData>
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) */
Expand Down
40 changes: 0 additions & 40 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<C: secp256k1::Verification>(
&mut self,
secp: &secp256k1::Secp256k1<C>,
Expand Down Expand Up @@ -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;
Expand Down
45 changes: 16 additions & 29 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/propkv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub enum ProprietaryKeySubtype {
Musig2PartialSig = 0x03,
PayGoAddressAttestationProof = 0x04,
Bip322Message = 0x05,
WasmUtxoVersion = 0x06,
WasmUtxoSignedWith = 0x06,
}

impl ProprietaryKeySubtype {
Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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<u8>) {
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<Self, String> {
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<u8>) {
BitGoKeyValue::new(
ProprietaryKeySubtype::WasmUtxoSignedWith,
vec![],
WasmUtxoVersionInfo::from_build_info().to_bytes(),
)
.to_key_value()
}
}

Expand Down Expand Up @@ -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<u8> = 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());
}
}
Loading
Loading