Skip to content

Commit bd20d68

Browse files
feat(wasm-utxo): add Psbt wrapper class with typed interface
BREAKING CHANGE: Psbt export is now a TypeScript wrapper class instead of the raw WrapPsbt WASM binding. Transaction methods renamed from snake_case to camelCase (get_txid -> getId, get_vsize -> getVSize, to_bytes -> toBytes, from_bytes -> fromBytes). SignPsbtResult type moved to js/descriptorWallet/Psbt.ts. WrapPsbt module augmentation removed from js/index.ts.
1 parent 907e359 commit bd20d68

8 files changed

Lines changed: 279 additions & 157 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
WrapPsbt as WasmPsbt,
3+
type WasmBIP32,
4+
type WasmECPair,
5+
type WrapDescriptor,
6+
type PsbtInputData,
7+
type PsbtOutputData,
8+
type PsbtOutputDataWithAddress,
9+
} from "../wasm/wasm_utxo.js";
10+
import type { IPsbt } from "../psbt.js";
11+
import type { CoinName } from "../coinName.js";
12+
import type { BIP32 } from "../bip32.js";
13+
import { Transaction } from "../transaction.js";
14+
15+
export type SignPsbtResult = {
16+
[inputIndex: number]: [pubkey: string][];
17+
};
18+
19+
export class Psbt implements IPsbt {
20+
private _wasm: WasmPsbt;
21+
22+
constructor(versionOrWasm?: number | WasmPsbt, lockTime?: number) {
23+
if (versionOrWasm instanceof WasmPsbt) {
24+
this._wasm = versionOrWasm;
25+
} else {
26+
this._wasm = new WasmPsbt(versionOrWasm, lockTime);
27+
}
28+
}
29+
30+
/** @internal Access the underlying WASM instance */
31+
get wasm(): WasmPsbt {
32+
return this._wasm;
33+
}
34+
35+
// -- Static / Factory --
36+
37+
static create(version?: number, lockTime?: number): Psbt {
38+
return new Psbt(new WasmPsbt(version, lockTime));
39+
}
40+
41+
static deserialize(bytes: Uint8Array): Psbt {
42+
return new Psbt(WasmPsbt.deserialize(bytes));
43+
}
44+
45+
// -- Serialization --
46+
47+
serialize(): Uint8Array {
48+
return this._wasm.serialize();
49+
}
50+
51+
clone(): Psbt {
52+
return new Psbt(this._wasm.clone());
53+
}
54+
55+
// -- IPsbt: introspection --
56+
57+
inputCount(): number {
58+
return this._wasm.input_count();
59+
}
60+
61+
outputCount(): number {
62+
return this._wasm.output_count();
63+
}
64+
65+
version(): number {
66+
return this._wasm.version();
67+
}
68+
69+
lockTime(): number {
70+
return this._wasm.lock_time();
71+
}
72+
73+
unsignedTxId(): string {
74+
return this._wasm.unsigned_tx_id();
75+
}
76+
77+
getInputs(): PsbtInputData[] {
78+
return this._wasm.get_inputs() as PsbtInputData[];
79+
}
80+
81+
getOutputs(): PsbtOutputData[] {
82+
return this._wasm.get_outputs() as PsbtOutputData[];
83+
}
84+
85+
getGlobalXpubs(): BIP32[] {
86+
return this._wasm.get_global_xpubs() as BIP32[];
87+
}
88+
89+
getOutputsWithAddress(coin: CoinName): PsbtOutputDataWithAddress[] {
90+
return this._wasm.get_outputs_with_address(coin) as PsbtOutputDataWithAddress[];
91+
}
92+
93+
// -- IPsbt: mutation --
94+
95+
addInputAtIndex(
96+
index: number,
97+
txid: string,
98+
vout: number,
99+
value: bigint,
100+
script: Uint8Array,
101+
sequence?: number,
102+
): number {
103+
return this._wasm.add_input_at_index(index, txid, vout, value, script, sequence);
104+
}
105+
106+
addInput(
107+
txid: string,
108+
vout: number,
109+
value: bigint,
110+
script: Uint8Array,
111+
sequence?: number,
112+
): number {
113+
return this._wasm.add_input(txid, vout, value, script, sequence);
114+
}
115+
116+
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number {
117+
return this._wasm.add_output_at_index(index, script, value);
118+
}
119+
120+
addOutput(script: Uint8Array, value: bigint): number {
121+
return this._wasm.add_output(script, value);
122+
}
123+
124+
removeInput(index: number): void {
125+
this._wasm.remove_input(index);
126+
}
127+
128+
removeOutput(index: number): void {
129+
this._wasm.remove_output(index);
130+
}
131+
132+
// -- Descriptor updates --
133+
134+
updateInputWithDescriptor(inputIndex: number, descriptor: WrapDescriptor): void {
135+
this._wasm.update_input_with_descriptor(inputIndex, descriptor);
136+
}
137+
138+
updateOutputWithDescriptor(outputIndex: number, descriptor: WrapDescriptor): void {
139+
this._wasm.update_output_with_descriptor(outputIndex, descriptor);
140+
}
141+
142+
// -- Signing --
143+
144+
signWithXprv(xprv: string): SignPsbtResult {
145+
return this._wasm.sign_with_xprv(xprv) as unknown as SignPsbtResult;
146+
}
147+
148+
signWithPrv(prv: Uint8Array): SignPsbtResult {
149+
return this._wasm.sign_with_prv(prv) as unknown as SignPsbtResult;
150+
}
151+
152+
signAll(key: WasmBIP32): SignPsbtResult {
153+
return this._wasm.sign_all(key) as unknown as SignPsbtResult;
154+
}
155+
156+
signAllWithEcpair(key: WasmECPair): SignPsbtResult {
157+
return this._wasm.sign_all_with_ecpair(key) as unknown as SignPsbtResult;
158+
}
159+
160+
// -- Signature introspection --
161+
162+
getPartialSignatures(inputIndex: number): Array<{ pubkey: Uint8Array; signature: Uint8Array }> {
163+
return this._wasm.get_partial_signatures(inputIndex) as Array<{
164+
pubkey: Uint8Array;
165+
signature: Uint8Array;
166+
}>;
167+
}
168+
169+
hasPartialSignatures(inputIndex: number): boolean {
170+
return this._wasm.has_partial_signatures(inputIndex);
171+
}
172+
173+
// -- Validation --
174+
175+
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean {
176+
return this._wasm.validate_signature_at_input(inputIndex, pubkey);
177+
}
178+
179+
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean {
180+
return this._wasm.verify_signature_with_key(inputIndex, key);
181+
}
182+
183+
// -- Transaction extraction --
184+
185+
getUnsignedTx(): Uint8Array {
186+
return this._wasm.get_unsigned_tx();
187+
}
188+
189+
finalize(): void {
190+
this._wasm.finalize_mut();
191+
}
192+
193+
extractTransaction(): Transaction {
194+
return Transaction.fromWasm(this._wasm.extract_transaction());
195+
}
196+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ export * from "./VirtualSize.js";
4545
// PSBT utilities
4646
export * from "./psbt/index.js";
4747

48+
// PSBT wrapper
49+
export { Psbt, type SignPsbtResult } from "./Psbt.js";
50+
4851
// Pattern matching
4952
export * from "./parse/PatternMatcher.js";

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ export type DescriptorPkType = "derivable" | "definite" | "string";
3232

3333
export type ScriptContext = "tap" | "segwitv0" | "legacy";
3434

35-
export type SignPsbtResult = {
36-
[inputIndex: number]: [pubkey: string][];
37-
};
38-
3935
declare module "./wasm/wasm_utxo.js" {
4036
interface WrapDescriptor {
4137
/** These are not the same types of nodes as in the ast module */
@@ -90,58 +86,10 @@ declare module "./wasm/wasm_utxo.js" {
9086
interface PsbtOutputDataWithAddress extends PsbtOutputData {
9187
address: string;
9288
}
93-
94-
interface WrapPsbt {
95-
// Signing methods (legacy - kept for backwards compatibility)
96-
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
97-
signWithPrv(this: WrapPsbt, prv: Uint8Array): SignPsbtResult;
98-
99-
// Signing methods (new - using WasmBIP32/WasmECPair)
100-
signAll(this: WrapPsbt, key: WasmBIP32): SignPsbtResult;
101-
signAllWithEcpair(this: WrapPsbt, key: WasmECPair): SignPsbtResult;
102-
103-
// Introspection methods
104-
inputCount(): number;
105-
outputCount(): number;
106-
getInputs(): PsbtInputData[];
107-
getOutputs(): PsbtOutputData[];
108-
getOutputsWithAddress(coin: import("./coinName.js").CoinName): PsbtOutputDataWithAddress[];
109-
getGlobalXpubs(): WasmBIP32[];
110-
getPartialSignatures(inputIndex: number): Array<{
111-
pubkey: Uint8Array;
112-
signature: Uint8Array;
113-
}>;
114-
hasPartialSignatures(inputIndex: number): boolean;
115-
116-
// Validation methods
117-
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean;
118-
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean;
119-
120-
// Extraction methods
121-
extractTransaction(): WasmTransaction;
122-
123-
// Mutation methods
124-
addInputAtIndex(
125-
index: number,
126-
txid: string,
127-
vout: number,
128-
value: bigint,
129-
script: Uint8Array,
130-
sequence?: number,
131-
): number;
132-
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
133-
removeInput(index: number): void;
134-
removeOutput(index: number): void;
135-
136-
// Metadata methods
137-
unsignedTxId(): string;
138-
lockTime(): number;
139-
version(): number;
140-
}
14189
}
14290

14391
export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js";
14492
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js";
145-
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js";
93+
export { Psbt } from "./descriptorWallet/Psbt.js";
14694
export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js";
14795
export { hasPsbtMagic, type IPsbt, type IPsbtWithAddress } from "./psbt.js";

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,23 +1273,15 @@ impl BitGoPsbt {
12731273
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
12741274
/// to the inner Bitcoin-compatible PSBT structure.
12751275
pub fn psbt(&self) -> &Psbt {
1276-
match self {
1277-
BitGoPsbt::BitcoinLike(ref psbt, _network) => psbt,
1278-
BitGoPsbt::Dash(ref dash_psbt, _network) => &dash_psbt.psbt,
1279-
BitGoPsbt::Zcash(ref zcash_psbt, _network) => &zcash_psbt.psbt,
1280-
}
1276+
crate::psbt_ops::PsbtAccess::psbt(self)
12811277
}
12821278

12831279
/// Get a mutable reference to the underlying PSBT
12841280
///
12851281
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
12861282
/// to the inner Bitcoin-compatible PSBT structure.
12871283
pub fn psbt_mut(&mut self) -> &mut Psbt {
1288-
match self {
1289-
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => psbt,
1290-
BitGoPsbt::Dash(ref mut dash_psbt, _network) => &mut dash_psbt.psbt,
1291-
BitGoPsbt::Zcash(ref mut zcash_psbt, _network) => &mut zcash_psbt.psbt,
1292-
}
1284+
crate::psbt_ops::PsbtAccess::psbt_mut(self)
12931285
}
12941286

12951287
/// Returns the global xpubs from the PSBT, or None if the PSBT has no global xpubs.
@@ -3019,6 +3011,23 @@ impl BitGoPsbt {
30193011
}
30203012
}
30213013

3014+
impl crate::psbt_ops::PsbtAccess for BitGoPsbt {
3015+
fn psbt(&self) -> &Psbt {
3016+
match self {
3017+
BitGoPsbt::BitcoinLike(ref psbt, _) => psbt,
3018+
BitGoPsbt::Dash(ref dash_psbt, _) => &dash_psbt.psbt,
3019+
BitGoPsbt::Zcash(ref zcash_psbt, _) => &zcash_psbt.psbt,
3020+
}
3021+
}
3022+
fn psbt_mut(&mut self) -> &mut Psbt {
3023+
match self {
3024+
BitGoPsbt::BitcoinLike(ref mut psbt, _) => psbt,
3025+
BitGoPsbt::Dash(ref mut dash_psbt, _) => &mut dash_psbt.psbt,
3026+
BitGoPsbt::Zcash(ref mut zcash_psbt, _) => &mut zcash_psbt.psbt,
3027+
}
3028+
}
3029+
}
3030+
30223031
/// All 6 orderings of a 3-element array, used to brute-force the
30233032
/// [user, backup, bitgo] assignment from an unordered xpub triple.
30243033
const XPUB_TRIPLE_PERMUTATIONS: [[usize; 3]; 6] = [

packages/wasm-utxo/src/psbt_ops.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
use miniscript::bitcoin::{psbt, Psbt, TxIn, TxOut};
22

3+
/// Shared accessor trait for types that wrap a `Psbt`.
4+
///
5+
/// Provides default implementations for common introspection methods so that
6+
/// both `WrapPsbt` and `BitGoPsbt` can reuse the same logic.
7+
pub trait PsbtAccess {
8+
fn psbt(&self) -> &Psbt;
9+
fn psbt_mut(&mut self) -> &mut Psbt;
10+
11+
fn input_count(&self) -> usize {
12+
self.psbt().inputs.len()
13+
}
14+
15+
fn output_count(&self) -> usize {
16+
self.psbt().outputs.len()
17+
}
18+
19+
fn version(&self) -> i32 {
20+
self.psbt().unsigned_tx.version.0
21+
}
22+
23+
fn lock_time(&self) -> u32 {
24+
self.psbt().unsigned_tx.lock_time.to_consensus_u32()
25+
}
26+
27+
fn unsigned_tx_id(&self) -> String {
28+
self.psbt().unsigned_tx.compute_txid().to_string()
29+
}
30+
}
31+
332
fn check_bounds(index: usize, len: usize, name: &str) -> Result<(), String> {
433
if index > len {
534
return Err(format!(

0 commit comments

Comments
 (0)