-
Notifications
You must be signed in to change notification settings - Fork 302
Expand file tree
/
Copy pathparse.ts
More file actions
128 lines (119 loc) · 3.59 KB
/
parse.ts
File metadata and controls
128 lines (119 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { BIP32, bip32, Descriptor } from '@bitgo/wasm-utxo';
import { DescriptorBuilder, getDescriptorFromBuilder } from './builder.js';
type NodeUnary<Key extends string> = { [k in Key]: unknown };
function isUnaryNode<TKey extends string>(node: unknown, key: TKey): node is NodeUnary<TKey> {
if (typeof node !== 'object' || node === null) {
return false;
}
const keys = Object.keys(node);
return keys.length === 1 && keys[0] === key;
}
function unwrapNode(node: unknown, path: string[]): unknown {
let current = node;
for (const key of path) {
if (!isUnaryNode(current, key)) {
return undefined;
}
current = current[key];
}
return current;
}
function parseMulti(node: unknown): {
threshold: number;
keys: bip32.BIP32Interface[];
path: string;
} {
if (!Array.isArray(node)) {
throw new Error('Unexpected node');
}
const [threshold, ...keyNodes] = node;
if (typeof threshold !== 'number') {
throw new Error('Expected threshold number');
}
const keyWithPath = keyNodes.map((keyNode) => {
if (!isUnaryNode(keyNode, 'XPub')) {
throw new Error('Expected XPub node');
}
if (typeof keyNode.XPub !== 'string') {
throw new Error('Expected XPub string');
}
const parts = keyNode.XPub.split('/');
return { xpub: parts[0], path: parts.slice(1).join('/') };
});
const paths = keyWithPath.map((k) => k.path);
paths.forEach((path, i) => {
if (path !== paths[0]) {
throw new Error(`Expected all paths to be the same: ${path} !== ${paths[0]}`);
}
});
return {
threshold,
keys: keyWithPath.map((k) => BIP32.fromBase58(k.xpub)),
path: paths[0],
};
}
function parseWshMulti(node: unknown): DescriptorBuilder | undefined {
const wshMsMulti = unwrapNode(node, ['Wsh', 'Ms', 'Multi']);
if (wshMsMulti) {
const { threshold, keys, path } = parseMulti(wshMsMulti);
let name;
if (threshold === 2 && keys.length === 2) {
name = 'Wsh2Of2';
} else if (threshold === 2 && keys.length === 3) {
name = 'Wsh2Of3';
} else {
throw new Error('Unexpected multisig');
}
return {
name,
keys,
path,
};
}
}
function parseCltvDrop(
node: unknown,
name: 'Wsh2Of3CltvDrop' | 'ShWsh2Of3CltvDrop',
wrapping: string[]
): DescriptorBuilder | undefined {
const unwrapped = unwrapNode(node, wrapping);
if (!unwrapped) {
return;
}
if (Array.isArray(unwrapped) && unwrapped.length === 2) {
const [a, b] = unwrapped;
const dropAfterAbsLocktime = unwrapNode(a, ['Drop', 'After', 'absLockTime']);
if (typeof dropAfterAbsLocktime !== 'number') {
throw new Error('Expected absLockTime number');
}
if (!isUnaryNode(b, 'Multi')) {
throw new Error('Expected Multi node');
}
const multi = parseMulti(b.Multi);
if (multi.threshold === 2 && multi.keys.length === 3) {
return {
name,
locktime: dropAfterAbsLocktime,
keys: multi.keys,
path: multi.path,
};
}
}
}
export function parseDescriptorNode(node: unknown): DescriptorBuilder {
const parsed =
parseWshMulti(node) ??
parseCltvDrop(node, 'ShWsh2Of3CltvDrop', ['Sh', 'Wsh', 'Ms', 'AndV']) ??
parseCltvDrop(node, 'Wsh2Of3CltvDrop', ['Wsh', 'Ms', 'AndV']);
if (!parsed) {
throw new Error('Failed to parse descriptor node');
}
return parsed;
}
export function parseDescriptor(descriptor: Descriptor): DescriptorBuilder {
const builder = parseDescriptorNode(descriptor.node());
if (getDescriptorFromBuilder(builder).toString() !== descriptor.toString()) {
throw new Error('Failed to parse descriptor');
}
return builder;
}