diff --git a/packages/wasm-miniscript/js/ast/formatNode.ts b/packages/wasm-miniscript/js/ast/formatNode.ts index 3bc21b2..bb6d08b 100644 --- a/packages/wasm-miniscript/js/ast/formatNode.ts +++ b/packages/wasm-miniscript/js/ast/formatNode.ts @@ -48,6 +48,8 @@ type Miniscript = | Wrap<{ older: number }> | Wrap<{ after: number }>; +type TapTree = [TapTree, TapTree] | Miniscript; + // Top level descriptor expressions // https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md#reference type Descriptor = @@ -57,13 +59,22 @@ type Descriptor = | { pkh: Key } | { wpkh: Key } | { combo: Key } - | { tr: [Key, Miniscript] } + | { tr: [Key, TapTree] } | { addr: string } | { raw: string } | { rawtr: string }; type Node = Miniscript | Descriptor | number | string; +/** + * Format a TapTree as a string + * If the argument is an array, descend recursively with formatTr and wrap result in curly braces + * Otherwise, format the node with formatN + */ +function formatTr(tree: TapTree): string { + return Array.isArray(tree) ? `{` + tree.map(formatTr).join(",") + `}` : formatN(tree); +} + function formatN(n: Node | Node[]): string { if (typeof n === "string") { return n; @@ -80,11 +91,16 @@ function formatN(n: Node | Node[]): string { throw new Error(`Invalid node: ${n}`); } const [name, value] = entries[0]; + if (name === "tr" && Array.isArray(value)) { + const [key, tree] = value; + return formatN({ tr: formatN([key, formatTr(tree)]) }); + } return `${name}(${formatN(value)})`; } throw new Error(`Invalid node: ${n}`); } +export type TapTreeNode = TapTree; export type MiniscriptNode = Miniscript; export type DescriptorNode = Descriptor; diff --git a/packages/wasm-miniscript/js/ast/fromWasmNode.ts b/packages/wasm-miniscript/js/ast/fromWasmNode.ts index 7d7d7a9..9828df9 100644 --- a/packages/wasm-miniscript/js/ast/fromWasmNode.ts +++ b/packages/wasm-miniscript/js/ast/fromWasmNode.ts @@ -1,4 +1,4 @@ -import { DescriptorNode, MiniscriptNode } from "./formatNode"; +import { DescriptorNode, MiniscriptNode, TapTreeNode } from "./formatNode"; import { Descriptor, Miniscript } from "../index"; function getSingleEntry(v: unknown): [string, unknown] { @@ -22,7 +22,7 @@ function wrap(type: string, value: unknown): MiniscriptNode { return { [`${type}:${name}`]: inner } as MiniscriptNode; } -type Node = DescriptorNode | MiniscriptNode | string | number; +type Node = DescriptorNode | MiniscriptNode | TapTreeNode | string | number; function fromUnknown(v: unknown): Node | Node[] { if (typeof v === "number" || typeof v === "string") { @@ -113,6 +113,12 @@ function fromUnknown(v: unknown): Node | Node[] { return node("multi", value); case "MultiA": return node("multi_a", value); + + case "Tree": + if (!Array.isArray(value) || value.length !== 2) { + throw new Error(`Invalid Tree node: ${JSON.stringify(value)}`); + } + return [fromUnknown(value[0]), fromUnknown(value[1])] as TapTreeNode; } } diff --git a/packages/wasm-miniscript/test/ast/formatNode.ts b/packages/wasm-miniscript/test/ast/formatNode.ts index 19130b9..b3b2c9e 100644 --- a/packages/wasm-miniscript/test/ast/formatNode.ts +++ b/packages/wasm-miniscript/test/ast/formatNode.ts @@ -10,5 +10,14 @@ describe("formatNode", function () { formatNode({ and_v: [{ after: 1 }, { after: 1 }] }), "and_v(after(1),after(1))", ); + // taproot single key + assert.strictEqual(formatNode({ tr: "k" }), "tr(k)"); + // key with single-node taproot tree + assert.strictEqual(formatNode({ tr: ["k", { pk: "k1" }] }), "tr(k,pk(k1))"); + // key with multi-node taproot tree + assert.strictEqual( + formatNode({ tr: ["k", [{ pk: "k1" }, { pk: "k2" }]] }), + "tr(k,{pk(k1),pk(k2)})", + ); }); }); diff --git a/packages/wasm-miniscript/test/descriptorFixtures.ts b/packages/wasm-miniscript/test/descriptorFixtures.ts index 411dcb7..2d5c5ec 100644 --- a/packages/wasm-miniscript/test/descriptorFixtures.ts +++ b/packages/wasm-miniscript/test/descriptorFixtures.ts @@ -357,6 +357,21 @@ export const fixtures = { script: "0020823bcb22035958d32afe8ec04357535a3e73da3ed9cd90a4251970f9995077a5", checksumRequired: false, }, + { + descriptor: + "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200)))#t9nj25pu", + script: "5120a887274080b8b01b124bc0f3b7e180e2910e30ef6c319b17bf878cb2576397e4", + }, + { + descriptor: + "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),{and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(100))}})#cfz66mzw", + script: "5120230320466c8eb821585e10e1ca35f98ea064aea8d972db994a50ce14a5d49689", + }, + { + descriptor: + "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200))})#ruusuuvn", + script: "51207f0160320dcb290c130b93cf18d445c3f3c8208fccd6cf6e6b2790a360378d15", + }, ], invalid: [ { diff --git a/packages/wasm-miniscript/test/fixtures/60.json b/packages/wasm-miniscript/test/fixtures/60.json new file mode 100644 index 0000000..2892b1c --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures/60.json @@ -0,0 +1,43 @@ +{ + "descriptor": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200)))#t9nj25pu", + "wasmNode": { + "Tr": [ + { + "Single": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "Older": { + "relLockTime": 200 + } + } + ] + } + ] + }, + "ast": { + "tr": [ + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "older": 200 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/packages/wasm-miniscript/test/fixtures/61.json b/packages/wasm-miniscript/test/fixtures/61.json new file mode 100644 index 0000000..72f3986 --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures/61.json @@ -0,0 +1,159 @@ +{ + "descriptor": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),{and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(100))}})#cfz66mzw", + "wasmNode": { + "Tr": [ + { + "Single": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + { + "Tree": [ + { + "AndV": [ + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "Verify": { + "MultiA": [ + 1, + { + "Single": "850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900" + }, + { + "Single": "a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3" + } + ] + } + } + ] + }, + { + "MultiA": [ + 2, + { + "Single": "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b" + }, + { + "Single": "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + } + ] + } + ] + }, + { + "Tree": [ + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "MultiA": [ + 2, + { + "Single": "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b" + }, + { + "Single": "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + } + ] + } + ] + }, + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "Older": { + "relLockTime": 100 + } + } + ] + } + ] + } + ] + } + ] + }, + "ast": { + "tr": [ + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + [ + { + "and_v": [ + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "v:multi_a": [ + 1, + "850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900", + "a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3" + ] + } + ] + }, + { + "multi_a": [ + 2, + "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b", + "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + ] + } + ] + }, + [ + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "multi_a": [ + 2, + "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b", + "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + ] + } + ] + }, + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "older": 100 + } + ] + } + ] + ] + ] + } +} \ No newline at end of file diff --git a/packages/wasm-miniscript/test/fixtures/62.json b/packages/wasm-miniscript/test/fixtures/62.json new file mode 100644 index 0000000..5b74476 --- /dev/null +++ b/packages/wasm-miniscript/test/fixtures/62.json @@ -0,0 +1,115 @@ +{ + "descriptor": "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{and_v(and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),v:multi_a(1,850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900,a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3)),multi_a(2,2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b,6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f)),and_v(v:pk(7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9),older(200))})#ruusuuvn", + "wasmNode": { + "Tr": [ + { + "Single": "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + }, + { + "Tree": [ + { + "AndV": [ + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "Verify": { + "MultiA": [ + 1, + { + "Single": "850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900" + }, + { + "Single": "a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3" + } + ] + } + } + ] + }, + { + "MultiA": [ + 2, + { + "Single": "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b" + }, + { + "Single": "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + } + ] + } + ] + }, + { + "AndV": [ + { + "Verify": { + "Check": { + "PkK": { + "Single": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + } + } + } + }, + { + "Older": { + "relLockTime": 200 + } + } + ] + } + ] + } + ] + }, + "ast": { + "tr": [ + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + [ + { + "and_v": [ + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "v:multi_a": [ + 1, + "850854d26df93570748d94e3da361f134c522f7970bd7f8701a164547308a900", + "a037b663bf29e98b59d7d81567d2ab9d9824dd9a16dc7489b81d5b86f936d9c3" + ] + } + ] + }, + { + "multi_a": [ + 2, + "2f3432bf9054e482041b890e084bc1a34cfbc0b63aded113bfa36b6f2402832b", + "6672282496e17eccaae560aac4d9fe85d8c166a4aa43f51fa8963c80a165831f" + ] + } + ] + }, + { + "and_v": [ + { + "v:pk": "7b6a08504c46336f985a5787a5423eb18c10b149fef29cd3316ad86f565cd2b9" + }, + { + "older": 200 + } + ] + } + ] + ] + } +} \ No newline at end of file