@kukks/bitcoin-descriptors 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -0
- package/dist/checksum.d.ts +6 -0
- package/dist/checksum.js +54 -0
- package/dist/descriptors.d.ts +113 -0
- package/dist/descriptors.js +1033 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/keyExpressions.d.ts +59 -0
- package/dist/keyExpressions.js +197 -0
- package/dist/miniscript.d.ts +55 -0
- package/dist/miniscript.js +163 -0
- package/dist/networks.d.ts +23 -0
- package/dist/networks.js +37 -0
- package/dist/psbt.d.ts +38 -0
- package/dist/psbt.js +200 -0
- package/dist/re.d.ts +31 -0
- package/dist/re.js +75 -0
- package/dist/scriptExpressions.d.ts +62 -0
- package/dist/scriptExpressions.js +47 -0
- package/dist/scriptUtils.d.ts +19 -0
- package/dist/scriptUtils.js +124 -0
- package/dist/signers.d.ts +62 -0
- package/dist/signers.js +129 -0
- package/dist/types.d.ts +263 -0
- package/dist/types.js +3 -0
- package/package.json +68 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { KeyInfo, Expansion } from './types.js';
|
|
2
|
+
export type { OutputInstance } from './descriptors.js';
|
|
3
|
+
export { DescriptorsFactory, OutputConstructor } from './descriptors.js';
|
|
4
|
+
export { DescriptorChecksum as checksum } from './checksum.js';
|
|
5
|
+
import * as signers from './signers.js';
|
|
6
|
+
export { signers };
|
|
7
|
+
export { keyExpressionBIP32 } from './keyExpressions.js';
|
|
8
|
+
import * as scriptExpressions from './scriptExpressions.js';
|
|
9
|
+
export { scriptExpressions };
|
|
10
|
+
export type { PsbtLike } from './psbt.js';
|
|
11
|
+
export { networks } from './networks.js';
|
|
12
|
+
export type { Network } from './networks.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
export { DescriptorsFactory } from './descriptors.js';
|
|
4
|
+
export { DescriptorChecksum as checksum } from './checksum.js';
|
|
5
|
+
import * as signers from './signers.js';
|
|
6
|
+
export { signers };
|
|
7
|
+
export { keyExpressionBIP32 } from './keyExpressions.js';
|
|
8
|
+
import * as scriptExpressions from './scriptExpressions.js';
|
|
9
|
+
export { scriptExpressions };
|
|
10
|
+
export { networks } from './networks.js';
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Network } from './networks.js';
|
|
2
|
+
import type { ECPairAPI, BIP32API, BIP32Interface } from './types.js';
|
|
3
|
+
import type { KeyInfo } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Parses a key expression (xpub, xprv, pubkey or wif) into {@link KeyInfo | `KeyInfo`}.
|
|
6
|
+
*
|
|
7
|
+
* For example, given this `keyExpression`: `"[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*"`, this is its parsed result:
|
|
8
|
+
*
|
|
9
|
+
* ```javascript
|
|
10
|
+
* {
|
|
11
|
+
* keyExpression:
|
|
12
|
+
* "[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*",
|
|
13
|
+
* keyPath: '/1/2/3/4/*',
|
|
14
|
+
* originPath: "/49'/0'/0'",
|
|
15
|
+
* path: "m/49'/0'/0'/1/2/3/4/*",
|
|
16
|
+
* // Other relevant properties of the type `KeyInfo`: `pubkey`, `ecpair` & `bip32` interfaces, `masterFingerprint`, etc.
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseKeyExpression({ keyExpression, isSegwit, isTaproot, ECPair, BIP32, network }: {
|
|
21
|
+
keyExpression: string;
|
|
22
|
+
/** @default networks.bitcoin */
|
|
23
|
+
network?: Network;
|
|
24
|
+
/**
|
|
25
|
+
* Indicates if this key expression belongs to a a SegWit output. When set,
|
|
26
|
+
* further checks are done to ensure the public key (if present in the
|
|
27
|
+
* expression) is compressed (33 bytes).
|
|
28
|
+
*/
|
|
29
|
+
isSegwit?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Indicates if this key expression belongs to a Taproot output. For Taproot,
|
|
32
|
+
* the key must be represented as an x-only public key (32 bytes).
|
|
33
|
+
* If a 33-byte compressed pubkey is derived, it is converted to its x-only
|
|
34
|
+
* representation.
|
|
35
|
+
*/
|
|
36
|
+
isTaproot?: boolean;
|
|
37
|
+
ECPair: ECPairAPI;
|
|
38
|
+
BIP32: BIP32API;
|
|
39
|
+
}): KeyInfo;
|
|
40
|
+
/**
|
|
41
|
+
* Constructs a key expression string from its constituent components.
|
|
42
|
+
*
|
|
43
|
+
* This function essentially performs the reverse operation of
|
|
44
|
+
* {@link _Internal_.ParseKeyExpression | ParseKeyExpression}. For detailed
|
|
45
|
+
* explanations and examples of the terms used here, refer to
|
|
46
|
+
* {@link _Internal_.ParseKeyExpression | ParseKeyExpression}.
|
|
47
|
+
*/
|
|
48
|
+
export declare function keyExpressionBIP32({ masterNode, originPath, keyPath, change, index, isPublic }: {
|
|
49
|
+
masterNode: BIP32Interface;
|
|
50
|
+
originPath: string;
|
|
51
|
+
change?: number | undefined;
|
|
52
|
+
index?: number | undefined | '*';
|
|
53
|
+
keyPath?: string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Compute an xpub or xprv
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
isPublic?: boolean;
|
|
59
|
+
}): string;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
import { networks } from './networks.js';
|
|
4
|
+
import { hex } from '@scure/base';
|
|
5
|
+
import { concatBytes } from '@scure/btc-signer/utils.js';
|
|
6
|
+
import * as RE from './re.js';
|
|
7
|
+
const derivePath = (node, path) => {
|
|
8
|
+
if (typeof path !== 'string') {
|
|
9
|
+
throw new Error(`Error: invalid derivation path ${path}`);
|
|
10
|
+
}
|
|
11
|
+
const parsedPath = path.replaceAll('H', "'").replaceAll('h', "'").slice(1);
|
|
12
|
+
const splitPath = parsedPath.split('/');
|
|
13
|
+
for (const element of splitPath) {
|
|
14
|
+
const unhardened = element.endsWith("'") ? element.slice(0, -1) : element;
|
|
15
|
+
if (!Number.isInteger(Number(unhardened)) ||
|
|
16
|
+
Number(unhardened) >= 0x80000000)
|
|
17
|
+
throw new Error(`Error: BIP 32 path element overflow`);
|
|
18
|
+
}
|
|
19
|
+
return node.derivePath(parsedPath);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Parses a key expression (xpub, xprv, pubkey or wif) into {@link KeyInfo | `KeyInfo`}.
|
|
23
|
+
*
|
|
24
|
+
* For example, given this `keyExpression`: `"[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*"`, this is its parsed result:
|
|
25
|
+
*
|
|
26
|
+
* ```javascript
|
|
27
|
+
* {
|
|
28
|
+
* keyExpression:
|
|
29
|
+
* "[d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*",
|
|
30
|
+
* keyPath: '/1/2/3/4/*',
|
|
31
|
+
* originPath: "/49'/0'/0'",
|
|
32
|
+
* path: "m/49'/0'/0'/1/2/3/4/*",
|
|
33
|
+
* // Other relevant properties of the type `KeyInfo`: `pubkey`, `ecpair` & `bip32` interfaces, `masterFingerprint`, etc.
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function parseKeyExpression({ keyExpression, isSegwit, isTaproot, ECPair, BIP32, network = networks.bitcoin }) {
|
|
38
|
+
let pubkey; //won't be computed for ranged keyExpressions
|
|
39
|
+
let ecpair;
|
|
40
|
+
let bip32;
|
|
41
|
+
let masterFingerprint;
|
|
42
|
+
let originPath;
|
|
43
|
+
let keyPath;
|
|
44
|
+
let path;
|
|
45
|
+
const isRanged = keyExpression.indexOf('*') !== -1;
|
|
46
|
+
const reKeyExp = isTaproot
|
|
47
|
+
? RE.reTaprootKeyExp
|
|
48
|
+
: isSegwit
|
|
49
|
+
? RE.reSegwitKeyExp
|
|
50
|
+
: RE.reNonSegwitKeyExp;
|
|
51
|
+
const rePubKey = isTaproot
|
|
52
|
+
? RE.reTaprootPubKey
|
|
53
|
+
: isSegwit
|
|
54
|
+
? RE.reSegwitPubKey
|
|
55
|
+
: RE.reNonSegwitPubKey;
|
|
56
|
+
//Validate the keyExpression:
|
|
57
|
+
const keyExpressions = keyExpression.match(reKeyExp);
|
|
58
|
+
if (keyExpressions === null || keyExpressions[0] !== keyExpression) {
|
|
59
|
+
throw new Error(`Error: expected a keyExpression but got ${keyExpression}`);
|
|
60
|
+
}
|
|
61
|
+
const reOriginAnchoredStart = RegExp(String.raw `^(${RE.reOrigin})?`); //starts with ^origin
|
|
62
|
+
const mOrigin = keyExpression.match(reOriginAnchoredStart);
|
|
63
|
+
if (mOrigin) {
|
|
64
|
+
const bareOrigin = mOrigin[0].replace(/[[\]]/g, ''); //strip the "[" and "]" in [origin]
|
|
65
|
+
const reMasterFingerprintAnchoredStart = String.raw `^(${RE.reMasterFingerprint})`;
|
|
66
|
+
const mMasterFingerprint = bareOrigin.match(reMasterFingerprintAnchoredStart);
|
|
67
|
+
const masterFingerprintHex = mMasterFingerprint
|
|
68
|
+
? mMasterFingerprint[0]
|
|
69
|
+
: '';
|
|
70
|
+
originPath = bareOrigin.replace(masterFingerprintHex, '');
|
|
71
|
+
if (masterFingerprintHex.length > 0) {
|
|
72
|
+
if (masterFingerprintHex.length !== 8)
|
|
73
|
+
throw new Error(`Error: masterFingerprint ${masterFingerprintHex} invalid for keyExpression: ${keyExpression}`);
|
|
74
|
+
masterFingerprint = hex.decode(masterFingerprintHex);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//Remove the origin (if it exists) and store result in actualKey
|
|
78
|
+
const actualKey = keyExpression.replace(reOriginAnchoredStart, '');
|
|
79
|
+
let mPubKey, mWIF, mXpubKey, mXprvKey;
|
|
80
|
+
//match pubkey:
|
|
81
|
+
if ((mPubKey = actualKey.match(RE.anchorStartAndEnd(rePubKey))) !== null) {
|
|
82
|
+
pubkey = hex.decode(mPubKey[0]);
|
|
83
|
+
if (isTaproot && pubkey.length === 32)
|
|
84
|
+
//convert the xonly point to a compressed point assuming even parity
|
|
85
|
+
pubkey = concatBytes(Uint8Array.from([0x02]), pubkey);
|
|
86
|
+
ecpair = ECPair.fromPublicKey(pubkey, { network });
|
|
87
|
+
//Validate the pubkey (compressed or uncompressed)
|
|
88
|
+
if (!ECPair.isPoint(pubkey) ||
|
|
89
|
+
!(pubkey.length === 33 || pubkey.length === 65)) {
|
|
90
|
+
throw new Error(`Error: invalid pubkey`);
|
|
91
|
+
}
|
|
92
|
+
//Do an extra check in case we know this pubkey refers to a segwit input
|
|
93
|
+
if (typeof isSegwit === 'boolean' &&
|
|
94
|
+
isSegwit &&
|
|
95
|
+
pubkey.length !== 33 //Inside wpkh and wsh, only compressed public keys are permitted.
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(`Error: invalid pubkey`);
|
|
98
|
+
}
|
|
99
|
+
//match WIF:
|
|
100
|
+
}
|
|
101
|
+
else if ((mWIF = actualKey.match(RE.anchorStartAndEnd(RE.reWIF))) !== null) {
|
|
102
|
+
ecpair = ECPair.fromWIF(mWIF[0], network);
|
|
103
|
+
//fromWIF will throw if the wif is not valid
|
|
104
|
+
pubkey = ecpair.publicKey;
|
|
105
|
+
//Check segwit requires compressed keys
|
|
106
|
+
if (typeof isSegwit === 'boolean' && isSegwit && pubkey.length !== 33) {
|
|
107
|
+
throw new Error(`Error: invalid pubkey`);
|
|
108
|
+
}
|
|
109
|
+
//match xpub:
|
|
110
|
+
}
|
|
111
|
+
else if ((mXpubKey = actualKey.match(RE.anchorStartAndEnd(RE.reXpubKey))) !== null) {
|
|
112
|
+
const xPubKey = mXpubKey[0];
|
|
113
|
+
const xPub = xPubKey.match(RE.reXpub)?.[0];
|
|
114
|
+
if (!xPub)
|
|
115
|
+
throw new Error(`Error: xpub could not be matched`);
|
|
116
|
+
bip32 = BIP32.fromBase58(xPub, network);
|
|
117
|
+
const mPath = xPubKey.match(RE.rePath);
|
|
118
|
+
if (mPath !== null) {
|
|
119
|
+
keyPath = xPubKey.match(RE.rePath)?.[0];
|
|
120
|
+
if (!keyPath)
|
|
121
|
+
throw new Error(`Error: could not extract a path`);
|
|
122
|
+
//fromBase58 and derivePath will throw if xPub or path are not valid
|
|
123
|
+
if (!isRanged)
|
|
124
|
+
pubkey = derivePath(bip32, keyPath).publicKey;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
pubkey = bip32.publicKey;
|
|
128
|
+
}
|
|
129
|
+
//match xprv:
|
|
130
|
+
}
|
|
131
|
+
else if ((mXprvKey = actualKey.match(RE.anchorStartAndEnd(RE.reXprvKey))) !== null) {
|
|
132
|
+
const xPrvKey = mXprvKey[0];
|
|
133
|
+
const xPrv = xPrvKey.match(RE.reXprv)?.[0];
|
|
134
|
+
if (!xPrv)
|
|
135
|
+
throw new Error(`Error: xprv could not be matched`);
|
|
136
|
+
bip32 = BIP32.fromBase58(xPrv, network);
|
|
137
|
+
const mPath = xPrvKey.match(RE.rePath);
|
|
138
|
+
if (mPath !== null) {
|
|
139
|
+
keyPath = xPrvKey.match(RE.rePath)?.[0];
|
|
140
|
+
if (!keyPath)
|
|
141
|
+
throw new Error(`Error: could not extract a path`);
|
|
142
|
+
//fromBase58 and derivePath will throw if xPrv or path are not valid
|
|
143
|
+
if (!isRanged)
|
|
144
|
+
pubkey = derivePath(bip32, keyPath).publicKey;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
pubkey = bip32.publicKey;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
throw new Error(`Error: could not get pubkey for keyExpression ${keyExpression}`);
|
|
152
|
+
}
|
|
153
|
+
if (originPath || keyPath) {
|
|
154
|
+
path = `m${originPath ?? ''}${keyPath ?? ''}`;
|
|
155
|
+
}
|
|
156
|
+
if (pubkey !== undefined && isTaproot && pubkey.length === 33)
|
|
157
|
+
// If we get a 33-byte compressed key, drop the first byte.
|
|
158
|
+
pubkey = pubkey.slice(1, 33);
|
|
159
|
+
return {
|
|
160
|
+
keyExpression,
|
|
161
|
+
...(pubkey !== undefined ? { pubkey } : {}),
|
|
162
|
+
...(ecpair !== undefined ? { ecpair } : {}),
|
|
163
|
+
...(bip32 !== undefined ? { bip32 } : {}),
|
|
164
|
+
...(masterFingerprint !== undefined ? { masterFingerprint } : {}),
|
|
165
|
+
...(originPath !== undefined && originPath !== '' ? { originPath } : {}),
|
|
166
|
+
...(keyPath !== undefined && keyPath !== '' ? { keyPath } : {}),
|
|
167
|
+
...(path !== undefined ? { path } : {})
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function assertChangeIndexKeyPath({ change, index, keyPath }) {
|
|
171
|
+
if (!((change === undefined && index === undefined) ||
|
|
172
|
+
(change !== undefined && index !== undefined)))
|
|
173
|
+
throw new Error(`Error: Pass change and index or neither`);
|
|
174
|
+
if ((change !== undefined) === (keyPath !== undefined))
|
|
175
|
+
throw new Error(`Error: Pass either change and index or a keyPath`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Constructs a key expression string from its constituent components.
|
|
179
|
+
*
|
|
180
|
+
* This function essentially performs the reverse operation of
|
|
181
|
+
* {@link _Internal_.ParseKeyExpression | ParseKeyExpression}. For detailed
|
|
182
|
+
* explanations and examples of the terms used here, refer to
|
|
183
|
+
* {@link _Internal_.ParseKeyExpression | ParseKeyExpression}.
|
|
184
|
+
*/
|
|
185
|
+
export function keyExpressionBIP32({ masterNode, originPath, keyPath, change, index, isPublic = true }) {
|
|
186
|
+
assertChangeIndexKeyPath({ change, index, keyPath });
|
|
187
|
+
const masterFingerprint = masterNode.fingerprint;
|
|
188
|
+
const origin = `[${hex.encode(masterFingerprint)}${originPath}]`;
|
|
189
|
+
const xpub = isPublic
|
|
190
|
+
? masterNode.derivePath(`m${originPath}`).neutered().toBase58().toString()
|
|
191
|
+
: masterNode.derivePath(`m${originPath}`).toBase58().toString();
|
|
192
|
+
const keyRoot = `${origin}${xpub}`;
|
|
193
|
+
if (keyPath !== undefined)
|
|
194
|
+
return `${keyRoot}${keyPath}`;
|
|
195
|
+
else
|
|
196
|
+
return `${keyRoot}/${change}/${index}`;
|
|
197
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Network } from './networks.js';
|
|
2
|
+
import type { ECPairAPI } from './types.js';
|
|
3
|
+
import type { BIP32API } from './types.js';
|
|
4
|
+
import type { PartialSig } from './types.js';
|
|
5
|
+
import type { Preimage, TimeConstraints, ExpansionMap } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Expand a miniscript to a generalized form using variables instead of key
|
|
8
|
+
* expressions. Variables will be of this form: @0, @1, ...
|
|
9
|
+
* This is done so that it can be compiled with compileMiniscript and
|
|
10
|
+
* satisfied with satisfier.
|
|
11
|
+
* Also compute pubkeys from descriptors to use them later.
|
|
12
|
+
*/
|
|
13
|
+
export declare function expandMiniscript({ miniscript, isSegwit, isTaproot, network, ECPair, BIP32 }: {
|
|
14
|
+
miniscript: string;
|
|
15
|
+
isSegwit: boolean;
|
|
16
|
+
isTaproot: boolean;
|
|
17
|
+
network?: Network;
|
|
18
|
+
ECPair: ECPairAPI;
|
|
19
|
+
BIP32: BIP32API;
|
|
20
|
+
}): {
|
|
21
|
+
expandedMiniscript: string;
|
|
22
|
+
expansionMap: ExpansionMap;
|
|
23
|
+
};
|
|
24
|
+
export declare function miniscript2Script({ expandedMiniscript, expansionMap }: {
|
|
25
|
+
expandedMiniscript: string;
|
|
26
|
+
expansionMap: ExpansionMap;
|
|
27
|
+
}): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Assumptions:
|
|
30
|
+
* The attacker does not have access to any of the private keys of public keys
|
|
31
|
+
* that participate in the Script.
|
|
32
|
+
*
|
|
33
|
+
* The attacker only has access to hash preimages that honest users have access
|
|
34
|
+
* to as well.
|
|
35
|
+
*
|
|
36
|
+
* Pass timeConstraints to search for the first solution with this nLockTime and
|
|
37
|
+
* nSequence. Throw if no solution is possible using these constraints.
|
|
38
|
+
*
|
|
39
|
+
* Don't pass timeConstraints (this is the default) if you want to get the
|
|
40
|
+
* smallest size solution altogether.
|
|
41
|
+
*
|
|
42
|
+
* If a solution is not found this function throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures, preimages, timeConstraints }: {
|
|
45
|
+
expandedMiniscript: string;
|
|
46
|
+
expansionMap: ExpansionMap;
|
|
47
|
+
signatures?: PartialSig[];
|
|
48
|
+
preimages?: Preimage[];
|
|
49
|
+
timeConstraints?: TimeConstraints;
|
|
50
|
+
}): {
|
|
51
|
+
scriptSatisfaction: Uint8Array;
|
|
52
|
+
nLockTime: number | undefined;
|
|
53
|
+
nSequence: number | undefined;
|
|
54
|
+
};
|
|
55
|
+
export { numberEncodeAsm } from './scriptUtils.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
import { networks } from './networks.js';
|
|
4
|
+
import { fromASM, numberEncodeAsm } from './scriptUtils.js';
|
|
5
|
+
import { hash160 } from '@scure/btc-signer/utils.js';
|
|
6
|
+
import { hex } from '@scure/base';
|
|
7
|
+
import { parseKeyExpression } from './keyExpressions.js';
|
|
8
|
+
import * as RE from './re.js';
|
|
9
|
+
import { compileMiniscript, satisfier } from '@bitcoinerlab/miniscript';
|
|
10
|
+
/**
|
|
11
|
+
* Expand a miniscript to a generalized form using variables instead of key
|
|
12
|
+
* expressions. Variables will be of this form: @0, @1, ...
|
|
13
|
+
* This is done so that it can be compiled with compileMiniscript and
|
|
14
|
+
* satisfied with satisfier.
|
|
15
|
+
* Also compute pubkeys from descriptors to use them later.
|
|
16
|
+
*/
|
|
17
|
+
export function expandMiniscript({ miniscript, isSegwit, isTaproot, network = networks.bitcoin, ECPair, BIP32 }) {
|
|
18
|
+
if (isTaproot)
|
|
19
|
+
throw new Error('Taproot miniscript not yet supported.');
|
|
20
|
+
const reKeyExp = isTaproot
|
|
21
|
+
? RE.reTaprootKeyExp
|
|
22
|
+
: isSegwit
|
|
23
|
+
? RE.reSegwitKeyExp
|
|
24
|
+
: RE.reNonSegwitKeyExp;
|
|
25
|
+
const expansionMap = {};
|
|
26
|
+
const expandedMiniscript = miniscript.replace(RegExp(reKeyExp, 'g'), (keyExpression) => {
|
|
27
|
+
const key = '@' + Object.keys(expansionMap).length;
|
|
28
|
+
expansionMap[key] = parseKeyExpression({
|
|
29
|
+
keyExpression,
|
|
30
|
+
isSegwit,
|
|
31
|
+
network,
|
|
32
|
+
ECPair,
|
|
33
|
+
BIP32
|
|
34
|
+
});
|
|
35
|
+
return key;
|
|
36
|
+
});
|
|
37
|
+
//Do some assertions. Miniscript must not have duplicate keys, also all
|
|
38
|
+
//keyExpressions must produce a valid pubkey (unless it's ranged and we want
|
|
39
|
+
//to expand a generalized form, then we don't check)
|
|
40
|
+
const pubkeysHex = Object.values(expansionMap)
|
|
41
|
+
.filter(keyInfo => keyInfo.keyExpression.indexOf('*') === -1)
|
|
42
|
+
.map(keyInfo => {
|
|
43
|
+
if (!keyInfo.pubkey)
|
|
44
|
+
throw new Error(`Error: keyExpression ${keyInfo.keyExpression} does not have a pubkey`);
|
|
45
|
+
return hex.encode(keyInfo.pubkey);
|
|
46
|
+
});
|
|
47
|
+
if (new Set(pubkeysHex).size !== pubkeysHex.length) {
|
|
48
|
+
throw new Error(`Error: miniscript ${miniscript} is not sane: contains duplicate public keys.`);
|
|
49
|
+
}
|
|
50
|
+
return { expandedMiniscript, expansionMap };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Particularize an expanded ASM expression using the variables in
|
|
54
|
+
* expansionMap.
|
|
55
|
+
* This is the kind of the opposite to what expandMiniscript does.
|
|
56
|
+
* Signatures and preimages are already subsituted by the satisfier calling
|
|
57
|
+
* this function.
|
|
58
|
+
*/
|
|
59
|
+
function substituteAsm({ expandedAsm, expansionMap }) {
|
|
60
|
+
//Replace back variables into the pubkeys previously computed.
|
|
61
|
+
let asm = Object.keys(expansionMap).reduce((accAsm, key) => {
|
|
62
|
+
const pubkey = expansionMap[key]?.pubkey;
|
|
63
|
+
if (!pubkey) {
|
|
64
|
+
throw new Error(`Error: invalid expansionMap for ${key}`);
|
|
65
|
+
}
|
|
66
|
+
return accAsm
|
|
67
|
+
.replaceAll(`<${key}>`, `<${hex.encode(pubkey)}>`)
|
|
68
|
+
.replaceAll(`<HASH160(${key})>`, `<${hex.encode(hash160(pubkey))}>`);
|
|
69
|
+
}, expandedAsm);
|
|
70
|
+
//Now clean it and prepare it so that fromASM can be called:
|
|
71
|
+
asm = asm
|
|
72
|
+
.trim()
|
|
73
|
+
//Replace one or more consecutive whitespace characters (spaces, tabs,
|
|
74
|
+
//or line breaks) with a single space.
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
//Now encode numbers to little endian hex. Note that numbers are not
|
|
77
|
+
//enclosed in <>, since <> represents hex code already encoded.
|
|
78
|
+
//The regex below will match one or more digits within a string,
|
|
79
|
+
//except if the sequence is surrounded by "<" and ">"
|
|
80
|
+
.replace(/(<\d+>)|\b\d+\b/g, match => match.startsWith('<') ? match : numberEncodeAsm(Number(match)))
|
|
81
|
+
//we don't have numbers anymore, now it's safe to remove < and > since we
|
|
82
|
+
//know that every remaining is either an op_code or a hex encoded number
|
|
83
|
+
.replace(/[<>]/g, '');
|
|
84
|
+
return asm;
|
|
85
|
+
}
|
|
86
|
+
export function miniscript2Script({ expandedMiniscript, expansionMap }) {
|
|
87
|
+
const compiled = compileMiniscript(expandedMiniscript);
|
|
88
|
+
if (compiled.issane !== true) {
|
|
89
|
+
throw new Error(`Error: Miniscript ${expandedMiniscript} is not sane`);
|
|
90
|
+
}
|
|
91
|
+
return fromASM(substituteAsm({ expandedAsm: compiled.asm, expansionMap }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Assumptions:
|
|
95
|
+
* The attacker does not have access to any of the private keys of public keys
|
|
96
|
+
* that participate in the Script.
|
|
97
|
+
*
|
|
98
|
+
* The attacker only has access to hash preimages that honest users have access
|
|
99
|
+
* to as well.
|
|
100
|
+
*
|
|
101
|
+
* Pass timeConstraints to search for the first solution with this nLockTime and
|
|
102
|
+
* nSequence. Throw if no solution is possible using these constraints.
|
|
103
|
+
*
|
|
104
|
+
* Don't pass timeConstraints (this is the default) if you want to get the
|
|
105
|
+
* smallest size solution altogether.
|
|
106
|
+
*
|
|
107
|
+
* If a solution is not found this function throws.
|
|
108
|
+
*/
|
|
109
|
+
export function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures = [], preimages = [], timeConstraints }) {
|
|
110
|
+
//convert 'sha256(6c...33)' to: { ['<sha256_preimage(6c...33)>']: '10...5f'}
|
|
111
|
+
const preimageMap = {};
|
|
112
|
+
preimages.forEach(preimage => {
|
|
113
|
+
preimageMap['<' + preimage.digest.replace('(', '_preimage(') + '>'] =
|
|
114
|
+
'<' + preimage.preimage + '>';
|
|
115
|
+
});
|
|
116
|
+
//convert the pubkeys in signatures into [{['<sig(@0)>']: '30450221'}, ...]
|
|
117
|
+
//get the keyExpressions: @0, @1 from the keys in expansionMap
|
|
118
|
+
const expandedSignatureMap = {};
|
|
119
|
+
signatures.forEach(signature => {
|
|
120
|
+
const pubkeyHex = hex.encode(signature.pubkey);
|
|
121
|
+
const keyExpression = Object.keys(expansionMap).find(k => {
|
|
122
|
+
const pk = expansionMap[k]?.pubkey;
|
|
123
|
+
return pk ? hex.encode(pk) === pubkeyHex : false;
|
|
124
|
+
});
|
|
125
|
+
expandedSignatureMap['<sig(' + keyExpression + ')>'] =
|
|
126
|
+
'<' + hex.encode(signature.signature) + '>';
|
|
127
|
+
});
|
|
128
|
+
const expandedKnownsMap = { ...preimageMap, ...expandedSignatureMap };
|
|
129
|
+
const knowns = Object.keys(expandedKnownsMap);
|
|
130
|
+
//satisfier verifies again internally whether expandedKnownsMap with given knowns is sane
|
|
131
|
+
const { nonMalleableSats } = satisfier(expandedMiniscript, { knowns });
|
|
132
|
+
if (!Array.isArray(nonMalleableSats) || !nonMalleableSats[0])
|
|
133
|
+
throw new Error(`Error: unresolvable miniscript ${expandedMiniscript}`);
|
|
134
|
+
let sat;
|
|
135
|
+
if (!timeConstraints) {
|
|
136
|
+
sat = nonMalleableSats[0];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
sat = nonMalleableSats.find(nonMalleableSat => nonMalleableSat.nSequence === timeConstraints.nSequence &&
|
|
140
|
+
nonMalleableSat.nLockTime === timeConstraints.nLockTime);
|
|
141
|
+
if (sat === undefined) {
|
|
142
|
+
throw new Error(`Error: unresolvable miniscript ${expandedMiniscript}. Could not find solutions for sequence ${timeConstraints.nSequence} & locktime=${timeConstraints.nLockTime}. Signatures are applied to a hash that depends on sequence and locktime. Did you provide all the signatures wrt the signers keys declared and include all preimages?`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//substitute signatures and preimages:
|
|
146
|
+
let expandedAsm = sat.asm;
|
|
147
|
+
//replace in expandedAsm all the <sig(@0)> and <sha256_preimage(6c...33)>
|
|
148
|
+
//to <304...01> and <107...5f> ...
|
|
149
|
+
for (const search in expandedKnownsMap) {
|
|
150
|
+
const replace = expandedKnownsMap[search];
|
|
151
|
+
if (!replace || replace === '<>')
|
|
152
|
+
throw new Error(`Error: invalid expandedKnownsMap`);
|
|
153
|
+
expandedAsm = expandedAsm.replaceAll(search, replace);
|
|
154
|
+
}
|
|
155
|
+
const scriptSatisfaction = fromASM(substituteAsm({ expandedAsm, expansionMap }));
|
|
156
|
+
return {
|
|
157
|
+
scriptSatisfaction,
|
|
158
|
+
nLockTime: sat.nLockTime,
|
|
159
|
+
nSequence: sat.nSequence
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Re-export numberEncodeAsm from scriptUtils
|
|
163
|
+
export { numberEncodeAsm } from './scriptUtils.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Network {
|
|
2
|
+
messagePrefix: string;
|
|
3
|
+
bech32: string;
|
|
4
|
+
bip32: {
|
|
5
|
+
public: number;
|
|
6
|
+
private: number;
|
|
7
|
+
};
|
|
8
|
+
pubKeyHash: number;
|
|
9
|
+
scriptHash: number;
|
|
10
|
+
wif: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const networks: {
|
|
13
|
+
bitcoin: Network;
|
|
14
|
+
testnet: Network;
|
|
15
|
+
regtest: Network;
|
|
16
|
+
};
|
|
17
|
+
/** Convert our Network to the format expected by @scure/btc-signer */
|
|
18
|
+
export declare function toBtcSignerNetwork(network: Network): {
|
|
19
|
+
bech32: string;
|
|
20
|
+
pubKeyHash: number;
|
|
21
|
+
scriptHash: number;
|
|
22
|
+
wif: number;
|
|
23
|
+
};
|
package/dist/networks.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) 2025 Jose-Luis Landabaso - https://bitcoinerlab.com
|
|
2
|
+
// Distributed under the MIT software license
|
|
3
|
+
export const networks = {
|
|
4
|
+
bitcoin: {
|
|
5
|
+
messagePrefix: '\x18Bitcoin Signed Message:\n',
|
|
6
|
+
bech32: 'bc',
|
|
7
|
+
bip32: { public: 0x0488b21e, private: 0x0488ade4 },
|
|
8
|
+
pubKeyHash: 0x00,
|
|
9
|
+
scriptHash: 0x05,
|
|
10
|
+
wif: 0x80
|
|
11
|
+
},
|
|
12
|
+
testnet: {
|
|
13
|
+
messagePrefix: '\x18Bitcoin Signed Message:\n',
|
|
14
|
+
bech32: 'tb',
|
|
15
|
+
bip32: { public: 0x043587cf, private: 0x04358394 },
|
|
16
|
+
pubKeyHash: 0x6f,
|
|
17
|
+
scriptHash: 0xc4,
|
|
18
|
+
wif: 0xef
|
|
19
|
+
},
|
|
20
|
+
regtest: {
|
|
21
|
+
messagePrefix: '\x18Bitcoin Signed Message:\n',
|
|
22
|
+
bech32: 'bcrt',
|
|
23
|
+
bip32: { public: 0x043587cf, private: 0x04358394 },
|
|
24
|
+
pubKeyHash: 0x6f,
|
|
25
|
+
scriptHash: 0xc4,
|
|
26
|
+
wif: 0xef
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** Convert our Network to the format expected by @scure/btc-signer */
|
|
30
|
+
export function toBtcSignerNetwork(network) {
|
|
31
|
+
return {
|
|
32
|
+
bech32: network.bech32,
|
|
33
|
+
pubKeyHash: network.pubKeyHash,
|
|
34
|
+
scriptHash: network.scriptHash,
|
|
35
|
+
wif: network.wif
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/psbt.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as btc from '@scure/btc-signer';
|
|
2
|
+
import type { KeyInfo } from './types.js';
|
|
3
|
+
type PsbtLike = InstanceType<typeof btc.Transaction>;
|
|
4
|
+
/**
|
|
5
|
+
* Computes final scripts for miniscript-based inputs.
|
|
6
|
+
*
|
|
7
|
+
* @param scriptSatisfaction - The script satisfaction (input script data)
|
|
8
|
+
* @param lockingScript - The "meaningful" locking script (witnessScript or redeemScript)
|
|
9
|
+
* @param isSegwit - Whether this is a segwit input
|
|
10
|
+
* @param isP2SH - Whether this is a P2SH input
|
|
11
|
+
* @param network - The network
|
|
12
|
+
* @returns Object with finalScriptSig and finalScriptWitness (as Uint8Array[])
|
|
13
|
+
*/
|
|
14
|
+
export declare function computeFinalScripts(scriptSatisfaction: Uint8Array, lockingScript: Uint8Array, isSegwit: boolean, isP2SH: boolean, redeemScript?: Uint8Array): {
|
|
15
|
+
finalScriptSig: Uint8Array | undefined;
|
|
16
|
+
finalScriptWitness: Uint8Array[] | undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Important: Read comments on descriptor.updatePsbt regarding not passing txHex
|
|
20
|
+
*/
|
|
21
|
+
export declare function updatePsbt({ psbt, vout, txHex, txId, value, sequence, locktime, keysInfo, scriptPubKey, isSegwit, tapInternalKey, witnessScript, redeemScript, rbf }: {
|
|
22
|
+
psbt: PsbtLike;
|
|
23
|
+
vout: number;
|
|
24
|
+
txHex?: string;
|
|
25
|
+
txId?: string;
|
|
26
|
+
value?: number | bigint;
|
|
27
|
+
sequence: number | undefined;
|
|
28
|
+
locktime: number | undefined;
|
|
29
|
+
keysInfo: KeyInfo[];
|
|
30
|
+
scriptPubKey: Uint8Array;
|
|
31
|
+
isSegwit: boolean;
|
|
32
|
+
/** for taproot **/
|
|
33
|
+
tapInternalKey?: Uint8Array | undefined;
|
|
34
|
+
witnessScript: Uint8Array | undefined;
|
|
35
|
+
redeemScript: Uint8Array | undefined;
|
|
36
|
+
rbf: boolean;
|
|
37
|
+
}): number;
|
|
38
|
+
export type { PsbtLike };
|