Skip to content

Commit bddcc1c

Browse files
committed
feat(wasm-utxo): add Miniscript fromStringExt for drop wrapper support
Ticket: CSHLD-770
1 parent 502e390 commit bddcc1c

3 files changed

Lines changed: 132 additions & 1 deletion

File tree

packages/wasm-utxo/js/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ declare module "./wasm/wasm_utxo.js" {
7474
namespace WrapMiniscript {
7575
function fromString(miniscript: string, ctx: ScriptContext): WrapMiniscript;
7676
function fromBitcoinScript(script: Uint8Array, ctx: ScriptContext): WrapMiniscript;
77+
function fromStringExt(
78+
miniscript: string,
79+
ctx: ScriptContext,
80+
extParams?: ExtParamsConfig,
81+
): WrapMiniscript;
82+
function fromBitcoinScriptExt(
83+
script: Uint8Array,
84+
ctx: ScriptContext,
85+
extParams?: ExtParamsConfig,
86+
): WrapMiniscript;
7787
}
7888

7989
/** BIP32 derivation data from a PSBT */

packages/wasm-utxo/src/wasm/miniscript.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::error::WasmUtxoError;
2+
use crate::wasm::try_from_js_value::get_field;
23
use crate::wasm::try_into_js_value::TryIntoJsValue;
34
use miniscript::bitcoin::{PublicKey, XOnlyPublicKey};
5+
use miniscript::miniscript::analyzable::ExtParams;
46
use miniscript::{bitcoin, Legacy, Miniscript, Segwitv0, Tap};
57
use std::fmt;
68
use std::str::FromStr;
@@ -86,6 +88,85 @@ impl WrapMiniscript {
8688
_ => Err(WasmUtxoError::new("Invalid context type")),
8789
}
8890
}
91+
92+
#[wasm_bindgen(js_name = fromStringExt, skip_typescript)]
93+
pub fn from_string_ext(
94+
script: &str,
95+
context_type: &str,
96+
ext_params_config: JsValue,
97+
) -> Result<WrapMiniscript, WasmUtxoError> {
98+
let params = build_ext_params(&ext_params_config)?;
99+
match context_type {
100+
"tap" => Ok(WrapMiniscript::from(
101+
Miniscript::<XOnlyPublicKey, Tap>::from_str_ext(script, &params)
102+
.map_err(WasmUtxoError::from)?,
103+
)),
104+
"segwitv0" => Ok(WrapMiniscript::from(
105+
Miniscript::<PublicKey, Segwitv0>::from_str_ext(script, &params)
106+
.map_err(WasmUtxoError::from)?,
107+
)),
108+
"legacy" => Ok(WrapMiniscript::from(
109+
Miniscript::<PublicKey, Legacy>::from_str_ext(script, &params)
110+
.map_err(WasmUtxoError::from)?,
111+
)),
112+
_ => Err(WasmUtxoError::new("Invalid context type")),
113+
}
114+
}
115+
116+
#[wasm_bindgen(js_name = fromBitcoinScriptExt, skip_typescript)]
117+
pub fn from_bitcoin_script_ext(
118+
script: &[u8],
119+
context_type: &str,
120+
ext_params_config: JsValue,
121+
) -> Result<WrapMiniscript, WasmUtxoError> {
122+
let params = build_ext_params(&ext_params_config)?;
123+
let script = bitcoin::Script::from_bytes(script);
124+
match context_type {
125+
"tap" => Ok(WrapMiniscript::from(
126+
Miniscript::<XOnlyPublicKey, Tap>::decode_with_ext(script, &params)
127+
.map_err(WasmUtxoError::from)?,
128+
)),
129+
"segwitv0" => Ok(WrapMiniscript::from(
130+
Miniscript::<PublicKey, Segwitv0>::decode_with_ext(script, &params)
131+
.map_err(WasmUtxoError::from)?,
132+
)),
133+
"legacy" => Ok(WrapMiniscript::from(
134+
Miniscript::<PublicKey, Legacy>::decode_with_ext(script, &params)
135+
.map_err(WasmUtxoError::from)?,
136+
)),
137+
_ => Err(WasmUtxoError::new("Invalid context type")),
138+
}
139+
}
140+
}
141+
142+
fn build_ext_params(config: &JsValue) -> Result<ExtParams, WasmUtxoError> {
143+
let flag = |key| -> Result<bool, WasmUtxoError> {
144+
if config.is_undefined() || config.is_null() {
145+
return Ok(false);
146+
}
147+
Ok(get_field::<Option<bool>>(config, key)?.unwrap_or(false))
148+
};
149+
150+
let mut params = ExtParams::sane().drop();
151+
if flag("topUnsafe")? {
152+
params = params.top_unsafe();
153+
}
154+
if flag("resourceLimitations")? {
155+
params = params.exceed_resource_limitations();
156+
}
157+
if flag("timelockMixing")? {
158+
params = params.timelock_mixing();
159+
}
160+
if flag("malleability")? {
161+
params = params.malleability();
162+
}
163+
if flag("repeatedPk")? {
164+
params = params.repeated_pk();
165+
}
166+
if flag("rawPkh")? {
167+
params = params.raw_pkh();
168+
}
169+
Ok(params)
89170
}
90171

91172
impl From<Miniscript<XOnlyPublicKey, Tap>> for WrapMiniscript {

packages/wasm-utxo/test/sbtc.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as assert from "assert";
22
import * as crypto from "crypto";
3-
import { Descriptor } from "../js/index.js";
3+
import { Descriptor, Miniscript } from "../js/index.js";
44
import { fromDescriptor, formatNode } from "../js/ast/index.js";
55
import { getDefaultXPubs, getUnspendableKey } from "../js/testutils/descriptor/descriptors.js";
66

@@ -247,6 +247,46 @@ describe("sBTC taproot descriptor", function () {
247247
});
248248
});
249249

250+
describe("Miniscript.fromStringExt / fromBitcoinScriptExt (reclaim leaf)", function () {
251+
it("Miniscript.fromString rejects r:older (sanity baseline)", () => {
252+
assert.throws(
253+
() => Miniscript.fromString(RECLAIM_LEAF, "tap"),
254+
/r:older|drop|wrapper|unexpected/i,
255+
"expected fromString to reject the drop wrapper",
256+
);
257+
});
258+
259+
it("Miniscript.fromStringExt parses the reclaim leaf with drop enabled", () => {
260+
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap");
261+
assert.ok(ms);
262+
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
263+
});
264+
265+
it("Miniscript.fromStringExt accepts an explicit empty config", () => {
266+
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap", {});
267+
assert.ok(ms);
268+
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
269+
});
270+
271+
it("Miniscript.fromStringExt round-trips encode() back to RECLAIM_SCRIPT_HEX", () => {
272+
const ms = Miniscript.fromStringExt(RECLAIM_LEAF, "tap");
273+
assert.strictEqual(Buffer.from(ms.encode()).toString("hex"), RECLAIM_SCRIPT_HEX);
274+
});
275+
276+
it("Miniscript.fromBitcoinScriptExt decodes RECLAIM_SCRIPT_HEX", () => {
277+
const ms = Miniscript.fromBitcoinScriptExt(Buffer.from(RECLAIM_SCRIPT_HEX, "hex"), "tap", {});
278+
assert.ok(ms);
279+
assert.strictEqual(ms.toString(), RECLAIM_LEAF);
280+
});
281+
282+
it("Miniscript.fromStringExt rejects unknown context_type", () => {
283+
assert.throws(
284+
() => Miniscript.fromStringExt(RECLAIM_LEAF, "bogus" as never, {}),
285+
/Invalid context type/,
286+
);
287+
});
288+
});
289+
250290
describe("fromDescriptor (wasm → JS AST)", function () {
251291
it("does not throw on a descriptor containing payload_drop", () => {
252292
assert.doesNotThrow(() => fromDescriptor(descriptor));

0 commit comments

Comments
 (0)