From bddcc1c4134ef2b143a48c8bda547158d6c51ddc Mon Sep 17 00:00:00 2001 From: Veetrag Jain Date: Thu, 21 May 2026 11:27:31 +0530 Subject: [PATCH] feat(wasm-utxo): add Miniscript fromStringExt for drop wrapper support Ticket: CSHLD-770 --- packages/wasm-utxo/js/index.ts | 10 +++ packages/wasm-utxo/src/wasm/miniscript.rs | 81 +++++++++++++++++++++++ packages/wasm-utxo/test/sbtc.ts | 42 +++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index 2db503805b1..193363d95be 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -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 */ diff --git a/packages/wasm-utxo/src/wasm/miniscript.rs b/packages/wasm-utxo/src/wasm/miniscript.rs index 66fe2b6e78f..eb3315f2d4e 100644 --- a/packages/wasm-utxo/src/wasm/miniscript.rs +++ b/packages/wasm-utxo/src/wasm/miniscript.rs @@ -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; @@ -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 { + let params = build_ext_params(&ext_params_config)?; + match context_type { + "tap" => Ok(WrapMiniscript::from( + Miniscript::::from_str_ext(script, ¶ms) + .map_err(WasmUtxoError::from)?, + )), + "segwitv0" => Ok(WrapMiniscript::from( + Miniscript::::from_str_ext(script, ¶ms) + .map_err(WasmUtxoError::from)?, + )), + "legacy" => Ok(WrapMiniscript::from( + Miniscript::::from_str_ext(script, ¶ms) + .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 { + let params = build_ext_params(&ext_params_config)?; + let script = bitcoin::Script::from_bytes(script); + match context_type { + "tap" => Ok(WrapMiniscript::from( + Miniscript::::decode_with_ext(script, ¶ms) + .map_err(WasmUtxoError::from)?, + )), + "segwitv0" => Ok(WrapMiniscript::from( + Miniscript::::decode_with_ext(script, ¶ms) + .map_err(WasmUtxoError::from)?, + )), + "legacy" => Ok(WrapMiniscript::from( + Miniscript::::decode_with_ext(script, ¶ms) + .map_err(WasmUtxoError::from)?, + )), + _ => Err(WasmUtxoError::new("Invalid context type")), + } + } +} + +fn build_ext_params(config: &JsValue) -> Result { + let flag = |key| -> Result { + if config.is_undefined() || config.is_null() { + return Ok(false); + } + Ok(get_field::>(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> for WrapMiniscript { diff --git a/packages/wasm-utxo/test/sbtc.ts b/packages/wasm-utxo/test/sbtc.ts index 54dc84f5062..9ab6eb60329 100644 --- a/packages/wasm-utxo/test/sbtc.ts +++ b/packages/wasm-utxo/test/sbtc.ts @@ -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"; @@ -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));