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
5 changes: 3 additions & 2 deletions packages/wasm-utxo/src/wasm/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ impl WrapDescriptor {
#[wasm_bindgen(js_name = fromStringDetectType, skip_typescript)]
pub fn from_string_detect_type(descriptor: &str) -> Result<WrapDescriptor, WasmUtxoError> {
let secp = Secp256k1::new();
let (descriptor, _key_map) = Descriptor::parse_descriptor(&secp, descriptor)
.map_err(|_| WasmUtxoError::new("Invalid descriptor"))?;
let (descriptor, _key_map) =
Descriptor::parse_descriptor_ext(&secp, descriptor, &ExtParams::sane().drop())
.map_err(|_| WasmUtxoError::new("Invalid descriptor"))?;
if descriptor.has_wildcard() {
WrapDescriptor::from_string_derivable(&secp, &descriptor.to_string())
} else {
Expand Down
93 changes: 92 additions & 1 deletion packages/wasm-utxo/test/sbtc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as assert from "assert";
import * as crypto from "crypto";
import { Descriptor } from "../js/index.js";
import { getUnspendableKey } from "../js/testutils/descriptor/descriptors.js";
import { getDefaultXPubs, getUnspendableKey } from "../js/testutils/descriptor/descriptors.js";

// sBTC protocol uses two taproot script leaves:
// 1. Deposit leaf: allows the signers to spend with a protocol payload
Expand Down Expand Up @@ -154,4 +154,95 @@ describe("sBTC taproot descriptor", function () {
assert.strictEqual(Buffer.from(scriptPubkeyBytes).toString("hex"), SCRIPT_PUBKEY_HEX);
});
});

describe("fromStringDetectType with wildcard xpubs", function () {
type GenericKey = { Single: string } | { XPub: string };
type DerivableSbtcNode = {
Tr: [
GenericKey,
{
Tree: [
{ Check: { AndV: [{ PayloadDrop: string }, { PkK: GenericKey }] } },
{
AndV: [
{ Drop: { Older: { relLockTime: number } } },
{ MultiA: [number, ...GenericKey[]] },
];
},
];
},
];
};

const xpubs = getDefaultXPubs();
const path = "0/*";
const depositLeafDerivable =
"c:and_v(payload_drop(" +
"0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5" +
")," +
`pk_k(${xpubs[0]}/${path})` +
")";
const reclaimLeafDerivable =
"and_v(r:older(1),multi_a(2," +
`${xpubs[0]}/${path},${xpubs[1]}/${path},${xpubs[2]}/${path}` +
"))";
const derivableDescriptor = Descriptor.fromStringDetectType(
getSbtcDescriptor(depositLeafDerivable, reclaimLeafDerivable),
);

it("parses as derivable when keys are xpubs with wildcards", () => {
assert.ok(derivableDescriptor);
assert.strictEqual(derivableDescriptor.hasWildcard(), true);
});

it("preserves payload_drop and Drop wrapper in derivable node structure", () => {
const node = derivableDescriptor.node() as DerivableSbtcNode;
const depositLeaf = node.Tr[1].Tree[0];
const reclaimLeaf = node.Tr[1].Tree[1];

assert.strictEqual(
depositLeaf.Check.AndV[0].PayloadDrop,
"0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5",
);
assert.strictEqual(reclaimLeaf.AndV[0].Drop.Older.relLockTime, 1);
// MultiA serializes as [threshold, ...keys]
assert.strictEqual(reclaimLeaf.AndV[1].MultiA[0], 2);
assert.strictEqual(reclaimLeaf.AndV[1].MultiA.length, 4);
});

it("derives at a concrete index and produces a P2TR scriptPubkey", () => {
const derived = derivableDescriptor.atDerivationIndex(0);
assert.strictEqual(derived.hasWildcard(), false);
const scriptPubkey = derived.scriptPubkey();
// P2TR: OP_1 (0x51) OP_PUSHBYTES_32 (0x20) <32-byte x-only key tweak>
assert.strictEqual(scriptPubkey.length, 34);
assert.strictEqual(scriptPubkey[0], 0x51);
assert.strictEqual(scriptPubkey[1], 0x20);
});
});

describe("fromStringDetectType", function () {
const detected = Descriptor.fromStringDetectType(getSbtcDescriptor(DEPOSIT_LEAF, RECLAIM_LEAF));

it("parses sBTC descriptor with payload_drop and r:older", () => {
assert.ok(detected, "Descriptor should parse successfully via fromStringDetectType");
assert.strictEqual(detected.hasWildcard(), false);
});

it("produces the same script pubkey as fromString", () => {
assert.strictEqual(Buffer.from(detected.scriptPubkey()).toString("hex"), SCRIPT_PUBKEY_HEX);
});

it("preserves payload_drop and Drop wrapper in node structure", () => {
const node = detected.node() as SbtcDescriptorNode;
const depositLeaf = node.Tr[1].Tree[0];
const reclaimLeaf = node.Tr[1].Tree[1];

assert.strictEqual(
depositLeaf.Check.AndV[0].PayloadDrop,
"0000000000013880051ad206838b7981a116c334e8cb1b950afb73eb54a5",
);
assert.strictEqual(reclaimLeaf.AndV[0].Drop.Older.relLockTime, 1);
});
});
});
Loading