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
94 changes: 94 additions & 0 deletions packages/wasm-miniscript/js/ast/formatNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*

This file contains type definitions for building an Abstract Syntax Tree for
Bitcoin Descriptors and Miniscript expressions.

Currently, the types do not encode any validity or soundness checks, so it is
possible to construct invalid descriptors.

*/
type Key = string;

// https://bitcoin.sipa.be/miniscript/
// r is for custom bitgo extension OP_DROP
type Identities = "a" | "s" | "c" | "t" | "d" | "v" | "j" | "n" | "l" | "u" | "r";

// Union of all possible prefixes: { f: T } => { 'a:f': T } | { 's:f': T } | ...
type PrefixWith<T, P extends string> = {
[K in keyof T & string as `${P}:${K}`]: T[K];
};
type PrefixIdUnion<T> = { [P in Identities]: PrefixWith<T, P> }[Identities];

// Wrap a type with a union of all possible prefixes
type Wrap<T> = T | PrefixIdUnion<T>;

type Miniscript =
| Wrap<{ pk: Key }>
| Wrap<{ pkh: Key }>
| Wrap<{ wpkh: Key }>
| Wrap<{ multi: [number, ...Key[]] }>
| Wrap<{ sortedmulti: [number, ...Key[]] }>
| Wrap<{ multi_a: [number, ...Key[]] }>
| Wrap<{ sortedmulti_a: [number, ...Key[]] }>
| Wrap<{ tr: Key | [Key, Miniscript] }>
| Wrap<{ sh: Miniscript }>
| Wrap<{ wsh: Miniscript }>
| Wrap<{ and_v: [Miniscript, Miniscript] }>
| Wrap<{ and_b: [Miniscript, Miniscript] }>
| Wrap<{ andor: [Miniscript, Miniscript, Miniscript] }>
| Wrap<{ or_b: [Miniscript, Miniscript] }>
| Wrap<{ or_c: [Miniscript, Miniscript] }>
| Wrap<{ or_d: [Miniscript, Miniscript] }>
| Wrap<{ or_i: [Miniscript, Miniscript] }>
| Wrap<{ thresh: [number, ...Miniscript[]] }>
| Wrap<{ sha256: string }>
| Wrap<{ ripemd160: string }>
| Wrap<{ hash256: string }>
| Wrap<{ hash160: string }>
| Wrap<{ older: number }>
| Wrap<{ after: number }>;

// Top level descriptor expressions
// https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md#reference
type Descriptor =
| { sh: Miniscript | { wsh: Miniscript } }
| { wsh: Miniscript }
| { pk: Key }
| { pkh: Key }
| { wpkh: Key }
| { combo: Key }
| { tr: [Key, Miniscript] }
| { addr: string }
| { raw: string }
| { rawtr: string };

type Node = Miniscript | Descriptor | number | string;

function formatN(n: Node | Node[]): string {
if (typeof n === "string") {
return n;
}
if (typeof n === "number") {
return String(n);
}
if (Array.isArray(n)) {
return n.map(formatN).join(",");
}
if (n && typeof n === "object") {
const entries = Object.entries(n);
if (entries.length !== 1) {
throw new Error(`Invalid node: ${n}`);
}
const [name, value] = entries[0];
return `${name}(${formatN(value)})`;
}
throw new Error(`Invalid node: ${n}`);
}

export type MiniscriptNode = Miniscript;
export type DescriptorNode = Descriptor;

/** Format a Miniscript or Descriptor node as a descriptor string (without checksum) */
export function formatNode(n: MiniscriptNode | DescriptorNode): string {
return formatN(n);
}
130 changes: 130 additions & 0 deletions packages/wasm-miniscript/js/ast/fromWasmNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { DescriptorNode, MiniscriptNode } from "./formatNode";
import { Descriptor, Miniscript } from "../index";

function getSingleEntry(v: unknown): [string, unknown] {
if (typeof v === "object" && v) {
const entries = Object.entries(v);
if (entries.length === 1) {
return entries[0];
}
}

throw new Error("Expected single entry object");
}

function node(type: string, value: unknown): MiniscriptNode | DescriptorNode {
return { [type]: fromUnknown(value) } as MiniscriptNode | DescriptorNode;
}

function wrap(type: string, value: unknown): MiniscriptNode {
const n = fromWasmNode(value);
const [name, inner] = getSingleEntry(n);
return { [`${type}:${name}`]: inner } as MiniscriptNode;
}

type Node = DescriptorNode | MiniscriptNode | string | number;

function fromUnknown(v: unknown): Node | Node[] {
if (typeof v === "number" || typeof v === "string") {
return v;
}
if (Array.isArray(v)) {
return v.map(fromUnknown) as Node[];
}
if (typeof v === "object" && v) {
const [type, value] = getSingleEntry(v);
switch (type) {
case "Bare":
case "Single":
case "Ms":
case "XPub":
case "relLockTime":
case "absLockTime":
return fromUnknown(value);
case "Sh":
case "Wsh":
case "Tr":
case "Pk":
case "Pkh":
case "PkH":
case "Wpkh":
case "Combo":
case "SortedMulti":
case "Addr":
case "Raw":
case "RawTr":
case "After":
case "Older":
case "Sha256":
case "Hash256":
case "Ripemd160":
case "Hash160":
return node(type.toLocaleLowerCase(), value);
case "PkK":
return node("pk", value);
case "RawPkH":
return node("raw_pkh", value);

// Wrappers
case "Alt":
return wrap("a", value);
case "Swap":
return wrap("s", value);
case "Check":
return fromUnknown(value);
case "DupIf":
return wrap("d", value);
case "Verify":
return wrap("v", value);
case "ZeroNotEqual":
return wrap("n", value);

// Conjunctions
case "AndV":
return node("and_v", value);
case "AndB":
return node("and_b", value);
case "AndOr":
if (!Array.isArray(value)) {
throw new Error(`Invalid AndOr node: ${JSON.stringify(value)}`);
}
const [cond, success, failure] = value;
if (failure === false) {
return node("and_n", [cond, success]);
}
return node("andor", [cond, success, failure]);

// Disjunctions
case "OrB":
return node("or_b", value);
case "OrD":
return node("or_d", value);
case "OrC":
return node("or_c", value);
case "OrI":
return node("or_i", value);

// Thresholds
case "Thresh":
return node("thresh", value);
case "Multi":
return node("multi", value);
case "MultiA":
return node("multi_a", value);
}
}

throw new Error(`Unknown node ${JSON.stringify(v)}`);
}

function fromWasmNode(v: unknown): DescriptorNode | MiniscriptNode {
return fromUnknown(v) as DescriptorNode | MiniscriptNode;
}

export function fromDescriptor(d: Descriptor): DescriptorNode {
return fromWasmNode(d.node()) as DescriptorNode;
}

export function fromMiniscript(m: Miniscript): MiniscriptNode {
return fromWasmNode(m.node()) as MiniscriptNode;
}
2 changes: 2 additions & 0 deletions packages/wasm-miniscript/js/ast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./formatNode";
export * from "./fromWasmNode";
4 changes: 4 additions & 0 deletions packages/wasm-miniscript/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ScriptContext = "tap" | "segwitv0" | "legacy";

declare module "./wasm/wasm_miniscript" {
interface WrapDescriptor {
/** These are not the same types of nodes as in the ast module */
node(): unknown;
}

Expand All @@ -18,6 +19,7 @@ declare module "./wasm/wasm_miniscript" {
}

interface WrapMiniscript {
/** These are not the same types of nodes as in the ast module */
node(): unknown;
}

Expand All @@ -30,3 +32,5 @@ declare module "./wasm/wasm_miniscript" {
export { WrapDescriptor as Descriptor } from "./wasm/wasm_miniscript";
export { WrapMiniscript as Miniscript } from "./wasm/wasm_miniscript";
export { WrapPsbt as Psbt } from "./wasm/wasm_miniscript";

export * as ast from "./ast";
4 changes: 2 additions & 2 deletions packages/wasm-miniscript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"dist/*/js/wasm/wasm_miniscript_bg.js",
"dist/*/js/wasm/wasm_miniscript_bg.wasm",
"dist/*/js/wasm/wasm_miniscript_bg.wasm.d.ts",
"dist/*/js/index.d.ts",
"dist/*/js/index.js"
"dist/*/js/ast/*",
"dist/*/js/index.*"
],
"main": "dist/node/js/index.js",
"types": "dist/node/js/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/wasm-miniscript/src/try_into_js_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl TryIntoJsValue for AbsLockTime {

impl TryIntoJsValue for RelLockTime {
fn try_to_js_value(&self) -> Result<JsValue, JsError> {
Ok(JsValue::from_str(&self.to_string()))
Ok(JsValue::from_f64(self.to_consensus_u32() as f64))
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/wasm-miniscript/test/ast/formatNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as assert from "assert";

import { formatNode } from "../../js/ast";

describe("formatNode", function () {
it("formats simple nodes", function () {
assert.strictEqual(formatNode({ pk: "lol" }), "pk(lol)");
assert.strictEqual(formatNode({ after: 1 }), "after(1)");
assert.strictEqual(
formatNode({ and_v: [{ after: 1 }, { after: 1 }] }),
"and_v(after(1),after(1))",
);
});
});
12 changes: 10 additions & 2 deletions packages/wasm-miniscript/test/descriptorUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from "fs/promises";
import * as utxolib from "@bitgo/utxo-lib";
import { Descriptor } from "../js";
import * as assert from "node:assert";
import { DescriptorNode, MiniscriptNode } from "../js/ast";

async function assertEqualJSON(path: string, value: unknown): Promise<void> {
try {
Expand All @@ -16,8 +17,15 @@ async function assertEqualJSON(path: string, value: unknown): Promise<void> {
}
}

export async function assertEqualAst(path: string, descriptor: Descriptor): Promise<void> {
await assertEqualJSON(path, { descriptor: descriptor.toString(), ast: descriptor.node() });
export async function assertEqualFixture(
path: string,
content: {
descriptor: string;
wasmNode: unknown;
ast: DescriptorNode | MiniscriptNode;
},
): Promise<void> {
await assertEqualJSON(path, content);
}

/** Expand a template with the given root wallet keys and chain code */
Expand Down
5 changes: 4 additions & 1 deletion packages/wasm-miniscript/test/fixtures/0.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"descriptor": "pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)#9pcxlpvx",
"ast": {
"wasmNode": {
"Bare": {
"Check": {
"PkK": {
"Single": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}
}
},
"ast": {
"pk": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}
5 changes: 4 additions & 1 deletion packages/wasm-miniscript/test/fixtures/1.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"descriptor": "pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd)#9pcxlpvx",
"ast": {
"wasmNode": {
"Bare": {
"Check": {
"PkK": {
"Single": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}
}
},
"ast": {
"pk": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}
5 changes: 4 additions & 1 deletion packages/wasm-miniscript/test/fixtures/10.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"descriptor": "pkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)#9907vvwz",
"ast": {
"wasmNode": {
"Pkh": {
"Single": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235"
}
},
"ast": {
"pkh": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235"
}
}
5 changes: 4 additions & 1 deletion packages/wasm-miniscript/test/fixtures/11.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"descriptor": "pkh(04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)#9907vvwz",
"ast": {
"wasmNode": {
"Pkh": {
"Single": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235"
}
},
"ast": {
"pkh": "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235"
}
}
7 changes: 6 additions & 1 deletion packages/wasm-miniscript/test/fixtures/12.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"descriptor": "sh(pk(03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd))#s53ls94y",
"ast": {
"wasmNode": {
"Sh": {
"Ms": {
"Check": {
Expand All @@ -10,5 +10,10 @@
}
}
}
},
"ast": {
"sh": {
"pk": "03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd"
}
}
}
Loading