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
10 changes: 10 additions & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ declare module "./wasm/wasm_utxo.js" {
namespace WrapMiniscript {
function fromString(miniscript: string, ctx: ScriptContext): WrapMiniscript;
function fromBitcoinScript(script: Uint8Array, ctx: ScriptContext): WrapMiniscript;
function fromStringExt(
miniscript: string,
ctx: ScriptContext,
extParams?: ExtParamsConfig,
): WrapMiniscript;
function fromBitcoinScriptExt(
script: Uint8Array,
ctx: ScriptContext,
extParams?: ExtParamsConfig,
): WrapMiniscript;
}

/** BIP32 derivation data from a PSBT */
Expand Down
81 changes: 81 additions & 0 deletions packages/wasm-utxo/src/wasm/miniscript.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::error::WasmUtxoError;
use crate::wasm::try_from_js_value::get_field;
use crate::wasm::try_into_js_value::TryIntoJsValue;
use miniscript::bitcoin::{PublicKey, XOnlyPublicKey};
use miniscript::miniscript::analyzable::ExtParams;
use miniscript::{bitcoin, Legacy, Miniscript, Segwitv0, Tap};
use std::fmt;
use std::str::FromStr;
Expand Down Expand Up @@ -86,6 +88,85 @@ impl WrapMiniscript {
_ => Err(WasmUtxoError::new("Invalid context type")),
}
}

#[wasm_bindgen(js_name = fromStringExt, skip_typescript)]
pub fn from_string_ext(
script: &str,
context_type: &str,
ext_params_config: JsValue,
) -> Result<WrapMiniscript, WasmUtxoError> {
let params = build_ext_params(&ext_params_config)?;
match context_type {
"tap" => Ok(WrapMiniscript::from(
Miniscript::<XOnlyPublicKey, Tap>::from_str_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
"segwitv0" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Segwitv0>::from_str_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
"legacy" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Legacy>::from_str_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
_ => Err(WasmUtxoError::new("Invalid context type")),
}
}

#[wasm_bindgen(js_name = fromBitcoinScriptExt, skip_typescript)]
pub fn from_bitcoin_script_ext(
script: &[u8],
context_type: &str,
ext_params_config: JsValue,
) -> Result<WrapMiniscript, WasmUtxoError> {
let params = build_ext_params(&ext_params_config)?;
let script = bitcoin::Script::from_bytes(script);
match context_type {
"tap" => Ok(WrapMiniscript::from(
Miniscript::<XOnlyPublicKey, Tap>::decode_with_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
"segwitv0" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Segwitv0>::decode_with_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
"legacy" => Ok(WrapMiniscript::from(
Miniscript::<PublicKey, Legacy>::decode_with_ext(script, &params)
.map_err(WasmUtxoError::from)?,
)),
_ => Err(WasmUtxoError::new("Invalid context type")),
}
}
}

fn build_ext_params(config: &JsValue) -> Result<ExtParams, WasmUtxoError> {
let flag = |key| -> Result<bool, WasmUtxoError> {
if config.is_undefined() || config.is_null() {
return Ok(false);
}
Ok(get_field::<Option<bool>>(config, key)?.unwrap_or(false))
};

let mut params = ExtParams::sane().drop();
if flag("topUnsafe")? {
params = params.top_unsafe();
}
if flag("resourceLimitations")? {
params = params.exceed_resource_limitations();
}
if flag("timelockMixing")? {
params = params.timelock_mixing();
}
if flag("malleability")? {
params = params.malleability();
}
if flag("repeatedPk")? {
params = params.repeated_pk();
}
if flag("rawPkh")? {
params = params.raw_pkh();
}
Ok(params)
}

impl From<Miniscript<XOnlyPublicKey, Tap>> for WrapMiniscript {
Expand Down
42 changes: 41 additions & 1 deletion packages/wasm-utxo/test/sbtc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from "assert";
import * as crypto from "crypto";
import { Descriptor } from "../js/index.js";
import { Descriptor, Miniscript } from "../js/index.js";
import { fromDescriptor, formatNode } from "../js/ast/index.js";
import { getDefaultXPubs, getUnspendableKey } from "../js/testutils/descriptor/descriptors.js";

Expand Down Expand Up @@ -247,6 +247,46 @@ describe("sBTC taproot descriptor", function () {
});
});

describe("Miniscript.fromStringExt / fromBitcoinScriptExt (reclaim leaf)", function () {
it("Miniscript.fromString rejects r:older (sanity baseline)", () => {
assert.throws(
() => Miniscript.fromString(RECLAIM_LEAF, "tap"),
/r:older|drop|wrapper|unexpected/i,
"expected fromString to reject the drop wrapper",
);
});

it("Miniscript.fromStringExt parses the reclaim leaf with drop enabled", () => {
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap");
assert.ok(ms);
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
});

it("Miniscript.fromStringExt accepts an explicit empty config", () => {
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap", {});
assert.ok(ms);
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
});

it("Miniscript.fromStringExt round-trips encode() back to RECLAIM_SCRIPT_HEX", () => {
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap");
assert.strictEqual(Buffer.from(ms.encode()).toString("hex"), RECLAIM_SCRIPT_HEX);
});

it("Miniscript.fromBitcoinScriptExt decodes RECLAIM_SCRIPT_HEX", () => {
const ms = Miniscript.fromBitcoinScriptExt(Buffer.from(RECLAIM_SCRIPT_HEX, "hex"), "tap", {});
assert.ok(ms);
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
});

it("Miniscript.fromStringExt rejects unknown context_type", () => {
assert.throws(
() => Miniscript.fromStringExt(RECLAIM_LEAF, "bogus" as never, {}),
/Invalid context type/,
);
});
});

describe("fromDescriptor (wasm → JS AST)", function () {
it("does not throw on a descriptor containing payload_drop", () => {
assert.doesNotThrow(() => fromDescriptor(descriptor));
Expand Down
Loading