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
196 changes: 196 additions & 0 deletions packages/wasm-utxo/js/descriptorWallet/Psbt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
WrapPsbt as WasmPsbt,
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 { CoinName } from "../coinName.js";
import type { BIP32 } from "../bip32.js";
import { Transaction } from "../transaction.js";

export type SignPsbtResult = {
[inputIndex: number]: [pubkey: string][];
};

export class Psbt implements IPsbt {
private _wasm: WasmPsbt;

constructor(versionOrWasm?: number | WasmPsbt, lockTime?: number) {
if (versionOrWasm instanceof WasmPsbt) {
this._wasm = versionOrWasm;
} else {
this._wasm = new WasmPsbt(versionOrWasm, lockTime);
}
}

/** @internal Access the underlying WASM instance */
get wasm(): WasmPsbt {
return this._wasm;
}

// -- Static / Factory --

static create(version?: number, lockTime?: number): Psbt {
return new Psbt(new WasmPsbt(version, lockTime));
}

static deserialize(bytes: Uint8Array): Psbt {
return new Psbt(WasmPsbt.deserialize(bytes));
}

// -- 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[];
}

// -- IPsbt: mutation --

addInputAtIndex(
index: number,
txid: string,
vout: number,
value: bigint,
script: Uint8Array,
sequence?: number,
): number {
return this._wasm.add_input_at_index(index, txid, vout, value, script, sequence);
}

addInput(
txid: string,
vout: number,
value: bigint,
script: Uint8Array,
sequence?: number,
): number {
return this._wasm.add_input(txid, vout, value, script, sequence);
}

addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number {
return this._wasm.add_output_at_index(index, script, value);
}

addOutput(script: Uint8Array, value: bigint): number {
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);
}

// -- Descriptor updates --

updateInputWithDescriptor(inputIndex: number, descriptor: WrapDescriptor): void {
this._wasm.update_input_with_descriptor(inputIndex, descriptor);
}

updateOutputWithDescriptor(outputIndex: number, descriptor: WrapDescriptor): void {
this._wasm.update_output_with_descriptor(outputIndex, descriptor);
}

// -- Signing --

signWithXprv(xprv: string): SignPsbtResult {
return this._wasm.sign_with_xprv(xprv) as unknown as SignPsbtResult;
}

signWithPrv(prv: Uint8Array): SignPsbtResult {
return this._wasm.sign_with_prv(prv) as unknown as SignPsbtResult;
}

signAll(key: WasmBIP32): SignPsbtResult {
return this._wasm.sign_all(key) as unknown as SignPsbtResult;
}

signAllWithEcpair(key: WasmECPair): SignPsbtResult {
return this._wasm.sign_all_with_ecpair(key) as unknown as SignPsbtResult;
}

// -- Signature introspection --

getPartialSignatures(inputIndex: number): Array<{ pubkey: Uint8Array; signature: Uint8Array }> {
return this._wasm.get_partial_signatures(inputIndex) as Array<{
pubkey: Uint8Array;
signature: Uint8Array;
}>;
}

hasPartialSignatures(inputIndex: number): boolean {
return this._wasm.has_partial_signatures(inputIndex);
}

// -- Validation --

validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean {
return this._wasm.validate_signature_at_input(inputIndex, pubkey);
}

verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean {
return this._wasm.verify_signature_with_key(inputIndex, key);
}

// -- Transaction extraction --

getUnsignedTx(): Uint8Array {
return this._wasm.get_unsigned_tx();
}

finalize(): void {
this._wasm.finalize_mut();
}

extractTransaction(): Transaction {
return Transaction.fromWasm(this._wasm.extract_transaction());
}
}
3 changes: 3 additions & 0 deletions packages/wasm-utxo/js/descriptorWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ export * from "./VirtualSize.js";
// PSBT utilities
export * from "./psbt/index.js";

// PSBT wrapper
export { Psbt, type SignPsbtResult } from "./Psbt.js";

// Pattern matching
export * from "./parse/PatternMatcher.js";
54 changes: 1 addition & 53 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ export type DescriptorPkType = "derivable" | "definite" | "string";

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

export type SignPsbtResult = {
[inputIndex: number]: [pubkey: string][];
};

declare module "./wasm/wasm_utxo.js" {
interface WrapDescriptor {
/** These are not the same types of nodes as in the ast module */
Expand Down Expand Up @@ -90,58 +86,10 @@ declare module "./wasm/wasm_utxo.js" {
interface PsbtOutputDataWithAddress extends PsbtOutputData {
address: string;
}

interface WrapPsbt {
// Signing methods (legacy - kept for backwards compatibility)
signWithXprv(this: WrapPsbt, xprv: string): SignPsbtResult;
signWithPrv(this: WrapPsbt, prv: Uint8Array): SignPsbtResult;

// Signing methods (new - using WasmBIP32/WasmECPair)
signAll(this: WrapPsbt, key: WasmBIP32): SignPsbtResult;
signAllWithEcpair(this: WrapPsbt, key: WasmECPair): SignPsbtResult;

// Introspection methods
inputCount(): number;
outputCount(): number;
getInputs(): PsbtInputData[];
getOutputs(): PsbtOutputData[];
getOutputsWithAddress(coin: import("./coinName.js").CoinName): PsbtOutputDataWithAddress[];
getGlobalXpubs(): WasmBIP32[];
getPartialSignatures(inputIndex: number): Array<{
pubkey: Uint8Array;
signature: Uint8Array;
}>;
hasPartialSignatures(inputIndex: number): boolean;

// Validation methods
validateSignatureAtInput(inputIndex: number, pubkey: Uint8Array): boolean;
verifySignatureWithKey(inputIndex: number, key: WasmBIP32): boolean;

// Extraction methods
extractTransaction(): WasmTransaction;

// Mutation methods
addInputAtIndex(
index: number,
txid: string,
vout: number,
value: bigint,
script: Uint8Array,
sequence?: number,
): number;
addOutputAtIndex(index: number, script: Uint8Array, value: bigint): number;
removeInput(index: number): void;
removeOutput(index: number): void;

// Metadata methods
unsignedTxId(): string;
lockTime(): number;
version(): number;
}
}

export { WrapDescriptor as Descriptor } from "./wasm/wasm_utxo.js";
export { WrapMiniscript as Miniscript } from "./wasm/wasm_utxo.js";
export { WrapPsbt as Psbt } from "./wasm/wasm_utxo.js";
export { Psbt } from "./descriptorWallet/Psbt.js";
export { DashTransaction, Transaction, ZcashTransaction } from "./transaction.js";
export { hasPsbtMagic, type IPsbt, type IPsbtWithAddress } from "./psbt.js";
29 changes: 19 additions & 10 deletions packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,23 +1273,15 @@ impl BitGoPsbt {
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
/// to the inner Bitcoin-compatible PSBT structure.
pub fn psbt(&self) -> &Psbt {
match self {
BitGoPsbt::BitcoinLike(ref psbt, _network) => psbt,
BitGoPsbt::Dash(ref dash_psbt, _network) => &dash_psbt.psbt,
BitGoPsbt::Zcash(ref zcash_psbt, _network) => &zcash_psbt.psbt,
}
crate::psbt_ops::PsbtAccess::psbt(self)
}

/// Get a mutable reference to the underlying PSBT
///
/// This works for both BitcoinLike and Zcash PSBTs, returning a reference
/// to the inner Bitcoin-compatible PSBT structure.
pub fn psbt_mut(&mut self) -> &mut Psbt {
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _network) => psbt,
BitGoPsbt::Dash(ref mut dash_psbt, _network) => &mut dash_psbt.psbt,
BitGoPsbt::Zcash(ref mut zcash_psbt, _network) => &mut zcash_psbt.psbt,
}
crate::psbt_ops::PsbtAccess::psbt_mut(self)
}

/// Returns the global xpubs from the PSBT, or None if the PSBT has no global xpubs.
Expand Down Expand Up @@ -3019,6 +3011,23 @@ impl BitGoPsbt {
}
}

impl crate::psbt_ops::PsbtAccess for BitGoPsbt {
fn psbt(&self) -> &Psbt {
match self {
BitGoPsbt::BitcoinLike(ref psbt, _) => psbt,
BitGoPsbt::Dash(ref dash_psbt, _) => &dash_psbt.psbt,
BitGoPsbt::Zcash(ref zcash_psbt, _) => &zcash_psbt.psbt,
}
}
fn psbt_mut(&mut self) -> &mut Psbt {
match self {
BitGoPsbt::BitcoinLike(ref mut psbt, _) => psbt,
BitGoPsbt::Dash(ref mut dash_psbt, _) => &mut dash_psbt.psbt,
BitGoPsbt::Zcash(ref mut zcash_psbt, _) => &mut zcash_psbt.psbt,
}
}
}

/// All 6 orderings of a 3-element array, used to brute-force the
/// [user, backup, bitgo] assignment from an unordered xpub triple.
const XPUB_TRIPLE_PERMUTATIONS: [[usize; 3]; 6] = [
Expand Down
29 changes: 29 additions & 0 deletions packages/wasm-utxo/src/psbt_ops.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
use miniscript::bitcoin::{psbt, Psbt, TxIn, TxOut};

/// Shared accessor trait for types that wrap a `Psbt`.
///
/// Provides default implementations for common introspection methods so that
/// both `WrapPsbt` and `BitGoPsbt` can reuse the same logic.
pub trait PsbtAccess {
fn psbt(&self) -> &Psbt;
fn psbt_mut(&mut self) -> &mut Psbt;

fn input_count(&self) -> usize {
self.psbt().inputs.len()
}

fn output_count(&self) -> usize {
self.psbt().outputs.len()
}

fn version(&self) -> i32 {
self.psbt().unsigned_tx.version.0
}

fn lock_time(&self) -> u32 {
self.psbt().unsigned_tx.lock_time.to_consensus_u32()
}

fn unsigned_tx_id(&self) -> String {
self.psbt().unsigned_tx.compute_txid().to_string()
}
}

fn check_bounds(index: usize, len: usize, name: &str) -> Result<(), String> {
if index > len {
return Err(format!(
Expand Down
Loading
Loading