Skip to content

Commit ca862f7

Browse files
Merge pull request #85 from BitGo/BTC-2908.wasm-utxo-dims
feat(wasm-utxo): add transaction size estimation
2 parents 37e005f + 5e18ade commit ca862f7

File tree

10 files changed

+1029
-28
lines changed

10 files changed

+1029
-28
lines changed

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

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ export type AddWalletOutputOptions = {
105105
};
106106

107107
export class BitGoPsbt {
108-
protected constructor(protected wasm: WasmBitGoPsbt) {}
108+
protected constructor(protected _wasm: WasmBitGoPsbt) {}
109+
110+
/**
111+
* Get the underlying WASM instance
112+
* @internal - for use by other wasm-utxo modules
113+
*/
114+
get wasm(): WasmBitGoPsbt {
115+
return this._wasm;
116+
}
109117

110118
/**
111119
* Create an empty PSBT for the given network with wallet keys
@@ -135,13 +143,13 @@ export class BitGoPsbt {
135143
options?: CreateEmptyOptions,
136144
): BitGoPsbt {
137145
const keys = RootWalletKeys.from(walletKeys);
138-
const wasm = WasmBitGoPsbt.create_empty(
146+
const wasmPsbt = WasmBitGoPsbt.create_empty(
139147
network,
140148
keys.wasm,
141149
options?.version,
142150
options?.lockTime,
143151
);
144-
return new BitGoPsbt(wasm);
152+
return new BitGoPsbt(wasmPsbt);
145153
}
146154

147155
/**
@@ -175,7 +183,7 @@ export class BitGoPsbt {
175183
* ```
176184
*/
177185
addInput(options: AddInputOptions, script: Uint8Array): number {
178-
return this.wasm.add_input(
186+
return this._wasm.add_input(
179187
options.txid,
180188
options.vout,
181189
options.value,
@@ -200,7 +208,7 @@ export class BitGoPsbt {
200208
* ```
201209
*/
202210
addOutput(options: AddOutputOptions): number {
203-
return this.wasm.add_output(options.script, options.value);
211+
return this._wasm.add_output(options.script, options.value);
204212
}
205213

206214
/**
@@ -248,7 +256,7 @@ export class BitGoPsbt {
248256
walletOptions: AddWalletInputOptions,
249257
): number {
250258
const keys = RootWalletKeys.from(walletKeys);
251-
return this.wasm.add_wallet_input(
259+
return this._wasm.add_wallet_input(
252260
inputOptions.txid,
253261
inputOptions.vout,
254262
inputOptions.value,
@@ -294,7 +302,7 @@ export class BitGoPsbt {
294302
*/
295303
addWalletOutput(walletKeys: WalletKeysArg, options: AddWalletOutputOptions): number {
296304
const keys = RootWalletKeys.from(walletKeys);
297-
return this.wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm);
305+
return this._wasm.add_wallet_output(options.chain, options.index, options.value, keys.wasm);
298306
}
299307

300308
/**
@@ -318,7 +326,7 @@ export class BitGoPsbt {
318326
*/
319327
addReplayProtectionInput(inputOptions: AddInputOptions, key: ECPairArg): number {
320328
const ecpair = ECPair.from(key);
321-
return this.wasm.add_replay_protection_input(
329+
return this._wasm.add_replay_protection_input(
322330
ecpair.wasm,
323331
inputOptions.txid,
324332
inputOptions.vout,
@@ -332,23 +340,23 @@ export class BitGoPsbt {
332340
* @returns The unsigned transaction ID
333341
*/
334342
unsignedTxid(): string {
335-
return this.wasm.unsigned_txid();
343+
return this._wasm.unsigned_txid();
336344
}
337345

338346
/**
339347
* Get the transaction version
340348
* @returns The transaction version number
341349
*/
342350
get version(): number {
343-
return this.wasm.version();
351+
return this._wasm.version();
344352
}
345353

346354
/**
347355
* Get the transaction lock time
348356
* @returns The transaction lock time
349357
*/
350358
get lockTime(): number {
351-
return this.wasm.lock_time();
359+
return this._wasm.lock_time();
352360
}
353361

354362
/**
@@ -364,9 +372,9 @@ export class BitGoPsbt {
364372
payGoPubkeys?: ECPairArg[],
365373
): ParsedTransaction {
366374
const keys = RootWalletKeys.from(walletKeys);
367-
const rp = ReplayProtection.from(replayProtection, this.wasm.network());
375+
const rp = ReplayProtection.from(replayProtection, this._wasm.network());
368376
const pubkeys = payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
369-
return this.wasm.parse_transaction_with_wallet_keys(
377+
return this._wasm.parse_transaction_with_wallet_keys(
370378
keys.wasm,
371379
rp.wasm,
372380
pubkeys,
@@ -391,7 +399,7 @@ export class BitGoPsbt {
391399
): ParsedOutput[] {
392400
const keys = RootWalletKeys.from(walletKeys);
393401
const pubkeys = payGoPubkeys?.map((arg) => ECPair.from(arg).wasm);
394-
return this.wasm.parse_outputs_with_wallet_keys(keys.wasm, pubkeys) as ParsedOutput[];
402+
return this._wasm.parse_outputs_with_wallet_keys(keys.wasm, pubkeys) as ParsedOutput[];
395403
}
396404

397405
/**
@@ -406,7 +414,7 @@ export class BitGoPsbt {
406414
* @throws Error if output index is out of bounds or entropy is not 64 bytes
407415
*/
408416
addPayGoAttestation(outputIndex: number, entropy: Uint8Array, signature: Uint8Array): void {
409-
this.wasm.add_paygo_attestation(outputIndex, entropy, signature);
417+
this._wasm.add_paygo_attestation(outputIndex, entropy, signature);
410418
}
411419

412420
/**
@@ -448,12 +456,12 @@ export class BitGoPsbt {
448456
// Try to parse as BIP32Arg first (string or BIP32 instance)
449457
if (typeof key === "string" || ("derive" in key && typeof key.derive === "function")) {
450458
const wasmKey = BIP32.from(key as BIP32Arg).wasm;
451-
return this.wasm.verify_signature_with_xpub(inputIndex, wasmKey);
459+
return this._wasm.verify_signature_with_xpub(inputIndex, wasmKey);
452460
}
453461

454462
// Otherwise it's an ECPairArg (Uint8Array, ECPair, or WasmECPair)
455463
const wasmECPair = ECPair.from(key as ECPairArg).wasm;
456-
return this.wasm.verify_signature_with_pub(inputIndex, wasmECPair);
464+
return this._wasm.verify_signature_with_pub(inputIndex, wasmECPair);
457465
}
458466

459467
/**
@@ -508,11 +516,11 @@ export class BitGoPsbt {
508516
) {
509517
// It's a BIP32Arg
510518
const wasmKey = BIP32.from(key as BIP32Arg);
511-
this.wasm.sign_with_xpriv(inputIndex, wasmKey.wasm);
519+
this._wasm.sign_with_xpriv(inputIndex, wasmKey.wasm);
512520
} else {
513521
// It's an ECPairArg
514522
const wasmKey = ECPair.from(key as ECPairArg);
515-
this.wasm.sign_with_privkey(inputIndex, wasmKey.wasm);
523+
this._wasm.sign_with_privkey(inputIndex, wasmKey.wasm);
516524
}
517525
}
518526

@@ -540,8 +548,8 @@ export class BitGoPsbt {
540548
inputIndex: number,
541549
replayProtection: ReplayProtectionArg,
542550
): boolean {
543-
const rp = ReplayProtection.from(replayProtection, this.wasm.network());
544-
return this.wasm.verify_replay_protection_signature(inputIndex, rp.wasm);
551+
const rp = ReplayProtection.from(replayProtection, this._wasm.network());
552+
return this._wasm.verify_replay_protection_signature(inputIndex, rp.wasm);
545553
}
546554

547555
/**
@@ -550,7 +558,7 @@ export class BitGoPsbt {
550558
* @returns The serialized PSBT as a byte array
551559
*/
552560
serialize(): Uint8Array {
553-
return this.wasm.serialize();
561+
return this._wasm.serialize();
554562
}
555563

556564
/**
@@ -593,7 +601,7 @@ export class BitGoPsbt {
593601
*/
594602
generateMusig2Nonces(key: BIP32Arg, sessionId?: Uint8Array): void {
595603
const wasmKey = BIP32.from(key);
596-
this.wasm.generate_musig2_nonces(wasmKey.wasm, sessionId);
604+
this._wasm.generate_musig2_nonces(wasmKey.wasm, sessionId);
597605
}
598606

599607
/**
@@ -616,7 +624,7 @@ export class BitGoPsbt {
616624
* ```
617625
*/
618626
combineMusig2Nonces(sourcePsbt: BitGoPsbt): void {
619-
this.wasm.combine_musig2_nonces(sourcePsbt.wasm);
627+
this._wasm.combine_musig2_nonces(sourcePsbt.wasm);
620628
}
621629

622630
/**
@@ -625,7 +633,7 @@ export class BitGoPsbt {
625633
* @throws Error if any input failed to finalize
626634
*/
627635
finalizeAllInputs(): void {
628-
this.wasm.finalize_all_inputs();
636+
this._wasm.finalize_all_inputs();
629637
}
630638

631639
/**
@@ -635,6 +643,6 @@ export class BitGoPsbt {
635643
* @throws Error if the PSBT is not fully finalized or extraction fails
636644
*/
637645
extractTransaction(): Uint8Array {
638-
return this.wasm.extract_transaction();
646+
return this._wasm.extract_transaction();
639647
}
640648
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { WasmDimensions } from "../wasm/wasm_utxo.js";
2+
import type { BitGoPsbt, InputScriptType, SignPath } from "./BitGoPsbt.js";
3+
import type { CoinName } from "../coinName.js";
4+
import { toOutputScriptWithCoin } from "../address.js";
5+
6+
type FromInputParams = { chain: number; signPath?: SignPath } | { scriptType: InputScriptType };
7+
8+
/**
9+
* Dimensions class for estimating transaction virtual size.
10+
*
11+
* Tracks weight internally with min/max bounds to handle ECDSA signature variance.
12+
* Schnorr signatures have no variance (always 64 bytes).
13+
*
14+
* This is a thin wrapper over the WASM implementation.
15+
*/
16+
export class Dimensions {
17+
private constructor(private _wasm: WasmDimensions) {}
18+
19+
/**
20+
* Create empty dimensions (zero weight)
21+
*/
22+
static empty(): Dimensions {
23+
return new Dimensions(WasmDimensions.empty());
24+
}
25+
26+
/**
27+
* Create dimensions from a BitGoPsbt
28+
*
29+
* Parses PSBT inputs and outputs to compute weight bounds without
30+
* requiring wallet keys. Input types are detected from BIP32 derivation
31+
* paths stored in the PSBT.
32+
*/
33+
static fromPsbt(psbt: BitGoPsbt): Dimensions {
34+
return new Dimensions(WasmDimensions.from_psbt(psbt.wasm));
35+
}
36+
37+
/**
38+
* Create dimensions for a single input
39+
*
40+
* @param params - Either `{ chain, signPath? }` or `{ scriptType }`
41+
*/
42+
static fromInput(params: FromInputParams): Dimensions {
43+
if ("scriptType" in params) {
44+
return new Dimensions(WasmDimensions.from_input_script_type(params.scriptType));
45+
}
46+
return new Dimensions(
47+
WasmDimensions.from_input(params.chain, params.signPath?.signer, params.signPath?.cosigner),
48+
);
49+
}
50+
51+
/**
52+
* Create dimensions for a single output from script bytes
53+
*/
54+
static fromOutput(script: Uint8Array): Dimensions;
55+
/**
56+
* Create dimensions for a single output from an address
57+
*/
58+
static fromOutput(address: string, network: CoinName): Dimensions;
59+
static fromOutput(scriptOrAddress: Uint8Array | string, network?: CoinName): Dimensions {
60+
if (typeof scriptOrAddress === "string") {
61+
if (network === undefined) {
62+
throw new Error("network is required when passing an address string");
63+
}
64+
const script = toOutputScriptWithCoin(scriptOrAddress, network);
65+
return new Dimensions(WasmDimensions.from_output_script(script));
66+
}
67+
return new Dimensions(WasmDimensions.from_output_script(scriptOrAddress));
68+
}
69+
70+
/**
71+
* Combine with another Dimensions instance
72+
*/
73+
plus(other: Dimensions): Dimensions {
74+
return new Dimensions(this._wasm.plus(other._wasm));
75+
}
76+
77+
/**
78+
* Whether any inputs are segwit (affects overhead calculation)
79+
*/
80+
get hasSegwit(): boolean {
81+
return this._wasm.has_segwit();
82+
}
83+
84+
/**
85+
* Get total weight (min or max)
86+
* @param size - "min" or "max", defaults to "max"
87+
*/
88+
getWeight(size: "min" | "max" = "max"): number {
89+
return this._wasm.get_weight(size);
90+
}
91+
92+
/**
93+
* Get virtual size (min or max)
94+
* @param size - "min" or "max", defaults to "max"
95+
*/
96+
getVSize(size: "min" | "max" = "max"): number {
97+
return this._wasm.get_vsize(size);
98+
}
99+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { RootWalletKeys, type WalletKeysArg, type IWalletKeys } from "./RootWalletKeys.js";
22
export { ReplayProtection, type ReplayProtectionArg } from "./ReplayProtection.js";
33
export { outputScript, address } from "./address.js";
4+
export { Dimensions } from "./Dimensions.js";
45

56
// Bitcoin-like PSBT (for all non-Zcash networks)
67
export {

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export * as ecpair from "./ecpair.js";
1616
// Only the most commonly used classes and types are exported at the top level for convenience
1717
export { ECPair } from "./ecpair.js";
1818
export { BIP32 } from "./bip32.js";
19+
export { Dimensions } from "./fixedScriptWallet/Dimensions.js";
1920

2021
export type { CoinName } from "./coinName.js";
2122
export type { Triple } from "./triple.js";

packages/wasm-utxo/js/transaction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ export class Transaction {
1616
return this._wasm.to_bytes();
1717
}
1818

19+
/**
20+
* Get the virtual size of the transaction
21+
*
22+
* Virtual size accounts for the segwit discount on witness data.
23+
*
24+
* @returns The virtual size in virtual bytes (vbytes)
25+
*/
26+
getVSize(): number {
27+
return this._wasm.get_vsize();
28+
}
29+
1930
/**
2031
* @internal
2132
*/

0 commit comments

Comments
 (0)