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
26 changes: 21 additions & 5 deletions packages/wasm-utxo/js/fixedScriptWallet/Dimensions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WasmDimensions } from "../wasm/wasm_utxo.js";
import type { BitGoPsbt, InputScriptType, SignPath } from "./BitGoPsbt.js";
import type { CoinName } from "../coinName.js";
import type { OutputScriptType } from "./scriptType.js";
import { toOutputScriptWithCoin } from "../address.js";

type FromInputParams = { chain: number; signPath?: SignPath } | { scriptType: InputScriptType };
Expand Down Expand Up @@ -56,15 +57,30 @@ export class Dimensions {
* Create dimensions for a single output from an address
*/
static fromOutput(address: string, network: CoinName): Dimensions;
static fromOutput(scriptOrAddress: Uint8Array | string, network?: CoinName): Dimensions {
if (typeof scriptOrAddress === "string") {
/**
* Create dimensions for a single output from script length only
*/
static fromOutput(params: { length: number }): Dimensions;
/**
* Create dimensions for a single output from script type
*/
static fromOutput(params: { scriptType: OutputScriptType }): Dimensions;
static fromOutput(
params: Uint8Array | string | { length: number } | { scriptType: OutputScriptType },
network?: CoinName,
): Dimensions {
if (typeof params === "string") {
if (network === undefined) {
throw new Error("network is required when passing an address string");
}
const script = toOutputScriptWithCoin(scriptOrAddress, network);
return new Dimensions(WasmDimensions.from_output_script(script));
const script = toOutputScriptWithCoin(params, network);
return new Dimensions(WasmDimensions.from_output_script_length(script.length));
}
if (typeof params === "object" && "scriptType" in params) {
return new Dimensions(WasmDimensions.from_output_script_type(params.scriptType));
}
return new Dimensions(WasmDimensions.from_output_script(scriptOrAddress));
// Both Uint8Array and { length: number } have .length
return new Dimensions(WasmDimensions.from_output_script_length(params.length));
}

/**
Expand Down
25 changes: 22 additions & 3 deletions packages/wasm-utxo/src/wasm/fixed_script_wallet/dimensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! This module provides weight-based estimation for transaction fees,
//! tracking min/max bounds to account for ECDSA signature variance.

use std::str::FromStr;

use crate::error::WasmUtxoError;
use crate::fixed_script_wallet::bitgo_psbt::psbt_wallet_input::{
parse_shared_chain_and_index, InputScriptType,
Expand Down Expand Up @@ -435,9 +437,9 @@ impl WasmDimensions {
})
}

/// Create dimensions for a single output from script bytes
pub fn from_output_script(script: &[u8]) -> WasmDimensions {
let weight = compute_output_weight(script.len());
/// Create dimensions for a single output from script length
pub fn from_output_script_length(length: u32) -> WasmDimensions {
let weight = compute_output_weight(length as usize);
WasmDimensions {
input_weight_min: 0,
input_weight_max: 0,
Expand All @@ -446,6 +448,23 @@ impl WasmDimensions {
}
}

/// Create dimensions for a single output from script type string
///
/// # Arguments
/// * `script_type` - One of: "p2sh", "p2shP2wsh", "p2wsh", "p2tr"/"p2trLegacy", "p2trMusig2"
pub fn from_output_script_type(script_type: &str) -> Result<WasmDimensions, WasmUtxoError> {
let parsed = OutputScriptType::from_str(script_type).map_err(|e| WasmUtxoError::new(&e))?;
let length = match parsed {
// P2SH: OP_HASH160 [20 bytes] OP_EQUAL = 23 bytes
OutputScriptType::P2sh | OutputScriptType::P2shP2wsh => 23,
// P2WSH: OP_0 [32 bytes] = 34 bytes
OutputScriptType::P2wsh => 34,
// P2TR: OP_1 [32 bytes] = 34 bytes
OutputScriptType::P2trLegacy | OutputScriptType::P2trMusig2 => 34,
};
Ok(Self::from_output_script_length(length))
}

/// Combine with another Dimensions instance
pub fn plus(&self, other: &WasmDimensions) -> WasmDimensions {
WasmDimensions {
Expand Down
65 changes: 64 additions & 1 deletion packages/wasm-utxo/test/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,73 @@ describe("Dimensions", function () {

it("should throw when address is provided without network", function () {
assert.throws(() => {
// @ts-expect-error - testing runtime error
// String matches { length: number } but implementation detects string and throws
Dimensions.fromOutput("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
}, /network is required/);
});

it("should create dimensions from script length only", function () {
// Compare with actual script
const script = Buffer.alloc(23);
const fromScript = Dimensions.fromOutput(script);
const fromLength = Dimensions.fromOutput({ length: 23 });

assert.strictEqual(fromLength.getWeight(), fromScript.getWeight());
assert.strictEqual(fromLength.getVSize(), fromScript.getVSize());
assert.strictEqual(fromLength.getOutputWeight(), fromScript.getOutputWeight());
});

it("should calculate correct weight for different script lengths", function () {
// p2pkh: 25 bytes -> weight = 4 * (8 + 1 + 25) = 136
const p2pkh = Dimensions.fromOutput({ length: 25 });
assert.strictEqual(p2pkh.getOutputWeight(), 136);

// p2wpkh: 22 bytes -> weight = 4 * (8 + 1 + 22) = 124
const p2wpkh = Dimensions.fromOutput({ length: 22 });
assert.strictEqual(p2wpkh.getOutputWeight(), 124);

// p2tr: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
const p2tr = Dimensions.fromOutput({ length: 34 });
assert.strictEqual(p2tr.getOutputWeight(), 172);
});

it("should create dimensions from script type", function () {
// p2sh/p2shP2wsh: 23 bytes -> weight = 4 * (8 + 1 + 23) = 128
const p2sh = Dimensions.fromOutput({ scriptType: "p2sh" });
assert.strictEqual(p2sh.getOutputWeight(), 128);

const p2shP2wsh = Dimensions.fromOutput({ scriptType: "p2shP2wsh" });
assert.strictEqual(p2shP2wsh.getOutputWeight(), 128);

// p2wsh: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
const p2wsh = Dimensions.fromOutput({ scriptType: "p2wsh" });
assert.strictEqual(p2wsh.getOutputWeight(), 172);

// p2tr/p2trLegacy: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
const p2tr = Dimensions.fromOutput({ scriptType: "p2tr" });
assert.strictEqual(p2tr.getOutputWeight(), 172);

const p2trLegacy = Dimensions.fromOutput({ scriptType: "p2trLegacy" });
assert.strictEqual(p2trLegacy.getOutputWeight(), 172);

// p2trMusig2: 34 bytes -> weight = 4 * (8 + 1 + 34) = 172
const p2trMusig2 = Dimensions.fromOutput({ scriptType: "p2trMusig2" });
assert.strictEqual(p2trMusig2.getOutputWeight(), 172);
});

it("scriptType should match equivalent length", function () {
// p2sh = 23 bytes
assert.strictEqual(
Dimensions.fromOutput({ scriptType: "p2sh" }).getOutputWeight(),
Dimensions.fromOutput({ length: 23 }).getOutputWeight(),
);

// p2wsh = 34 bytes
assert.strictEqual(
Dimensions.fromOutput({ scriptType: "p2wsh" }).getOutputWeight(),
Dimensions.fromOutput({ length: 34 }).getOutputWeight(),
);
});
});

describe("plus", function () {
Expand Down