@bitcoinerlab/descriptors 2.3.6 → 3.0.0
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 +173 -77
- package/dist/applyPR2137.js +36 -17
- package/dist/bitcoinjs-lib-internals.d.ts +10 -0
- package/dist/bitcoinjs-lib-internals.js +18 -0
- package/dist/descriptors.d.ts +161 -392
- package/dist/descriptors.js +512 -281
- package/dist/index.d.ts +2 -29
- package/dist/index.js +0 -14
- package/dist/keyExpressions.d.ts +4 -13
- package/dist/keyExpressions.js +15 -18
- package/dist/ledger.d.ts +14 -37
- package/dist/ledger.js +118 -100
- package/dist/miniscript.d.ts +20 -6
- package/dist/miniscript.js +59 -17
- package/dist/multipath.d.ts +13 -0
- package/dist/multipath.js +76 -0
- package/dist/networkUtils.d.ts +3 -0
- package/dist/networkUtils.js +16 -0
- package/dist/parseUtils.d.ts +7 -0
- package/dist/parseUtils.js +46 -0
- package/dist/psbt.d.ts +17 -13
- package/dist/psbt.js +34 -50
- package/dist/resourceLimits.d.ts +25 -0
- package/dist/resourceLimits.js +89 -0
- package/dist/scriptExpressions.d.ts +29 -77
- package/dist/scriptExpressions.js +19 -16
- package/dist/signers.d.ts +1 -21
- package/dist/signers.js +85 -129
- package/dist/stackResourceLimits.d.ts +17 -0
- package/dist/stackResourceLimits.js +35 -0
- package/dist/tapMiniscript.d.ts +193 -0
- package/dist/tapMiniscript.js +428 -0
- package/dist/tapTree.d.ts +76 -0
- package/dist/tapTree.js +163 -0
- package/dist/types.d.ts +46 -6
- package/package.json +13 -13
package/dist/ledger.js
CHANGED
|
@@ -36,15 +36,17 @@ exports.ledgerPolicyFromState = ledgerPolicyFromState;
|
|
|
36
36
|
*/
|
|
37
37
|
const descriptors_1 = require("./descriptors");
|
|
38
38
|
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
|
|
39
|
+
const uint8array_tools_1 = require("uint8array-tools");
|
|
40
|
+
const networkUtils_1 = require("./networkUtils");
|
|
39
41
|
const re_1 = require("./re");
|
|
40
42
|
/**
|
|
41
|
-
* Dynamically imports the 'ledger-bitcoin' module and, if provided, checks if `ledgerClient` is an instance of `AppClient`.
|
|
43
|
+
* Dynamically imports the '@ledgerhq/ledger-bitcoin' module and, if provided, checks if `ledgerClient` is an instance of `AppClient`.
|
|
42
44
|
*
|
|
43
45
|
* @async
|
|
44
46
|
* @param {unknown} ledgerClient - An optional parameter that, if provided, is checked to see if it's an instance of `AppClient`.
|
|
45
47
|
* @throws {Error} Throws an error if `ledgerClient` is provided but is not an instance of `AppClient`.
|
|
46
|
-
* @throws {Error} Throws an error if the 'ledger-bitcoin' module cannot be imported. This typically indicates that the 'ledger-bitcoin' peer dependency is not installed.
|
|
47
|
-
* @returns {Promise<unknown>} Returns a promise that resolves with the entire 'ledger-bitcoin' module if it can be successfully imported. We force it to return an unknown type so that the declaration of this function won't break projects that don't use ledger-bitcoin as dependency
|
|
48
|
+
* @throws {Error} Throws an error if the '@ledgerhq/ledger-bitcoin' module cannot be imported. This typically indicates that the '@ledgerhq/ledger-bitcoin' peer dependency is not installed.
|
|
49
|
+
* @returns {Promise<unknown>} Returns a promise that resolves with the entire '@ledgerhq/ledger-bitcoin' module if it can be successfully imported. We force it to return an unknown type so that the declaration of this function won't break projects that don't use @ledgerhq/ledger-bitcoin as dependency
|
|
48
50
|
*
|
|
49
51
|
* @example
|
|
50
52
|
*
|
|
@@ -59,22 +61,22 @@ async function importAndValidateLedgerBitcoin(ledgerClient) {
|
|
|
59
61
|
let ledgerBitcoinModule;
|
|
60
62
|
try {
|
|
61
63
|
// Originally, the code used dynamic imports:
|
|
62
|
-
// ledgerBitcoinModule = await import('ledger-bitcoin');
|
|
64
|
+
// ledgerBitcoinModule = await import('@ledgerhq/ledger-bitcoin');
|
|
63
65
|
// However, in React Native with the Metro bundler, there's an issue with
|
|
64
66
|
// recognizing dynamic imports inside try-catch blocks. For details, refer to:
|
|
65
67
|
// https://github.com/react-native-community/discussions-and-proposals/issues/120
|
|
66
68
|
// The dynamic import gets transpiled to:
|
|
67
|
-
// ledgerBitcoinModule = Promise.resolve().then(() => __importStar(require('ledger-bitcoin')));
|
|
69
|
+
// ledgerBitcoinModule = Promise.resolve().then(() => __importStar(require('@ledgerhq/ledger-bitcoin')));
|
|
68
70
|
// Metro bundler fails to recognize the above as conditional. Hence, it tries
|
|
69
|
-
// to require 'ledger-bitcoin' unconditionally, leading to potential errors if
|
|
70
|
-
// 'ledger-bitcoin' is not installed (given it's an optional peerDependency).
|
|
71
|
+
// to require '@ledgerhq/ledger-bitcoin' unconditionally, leading to potential errors if
|
|
72
|
+
// '@ledgerhq/ledger-bitcoin' is not installed (given it's an optional peerDependency).
|
|
71
73
|
// To bypass this, we directly use require:
|
|
72
74
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
73
|
-
ledgerBitcoinModule = require('ledger-bitcoin');
|
|
75
|
+
ledgerBitcoinModule = require('@ledgerhq/ledger-bitcoin');
|
|
74
76
|
}
|
|
75
77
|
catch (error) {
|
|
76
78
|
void error;
|
|
77
|
-
throw new Error('Could not import "ledger-bitcoin". This is a peer dependency and needs to be installed explicitly. Please run "npm install ledger-bitcoin" to use Ledger Hardware Wallet functionality.');
|
|
79
|
+
throw new Error('Could not import "@ledgerhq/ledger-bitcoin". This is a peer dependency and needs to be installed explicitly. Please run "npm install @ledgerhq/ledger-bitcoin" to use Ledger Hardware Wallet functionality.');
|
|
78
80
|
}
|
|
79
81
|
const { AppClient } = ledgerBitcoinModule;
|
|
80
82
|
if (ledgerClient !== undefined && !(ledgerClient instanceof AppClient)) {
|
|
@@ -129,8 +131,10 @@ function isLedgerStandard({ ledgerTemplate, keyRoots, network = bitcoinjs_lib_1.
|
|
|
129
131
|
const originPath = keyRoots[0]?.match(re_1.reOriginPath)?.[1];
|
|
130
132
|
if (!originPath)
|
|
131
133
|
return false;
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
+
const originCoinType = originPath.match(/^\/\d+'\/([01])'/)?.[1];
|
|
135
|
+
if (!originCoinType)
|
|
136
|
+
return false;
|
|
137
|
+
if (originCoinType !== `${(0, networkUtils_1.coinTypeFromNetwork)(network)}`)
|
|
134
138
|
return false;
|
|
135
139
|
if ((ledgerTemplate === 'pkh(@0/**)' &&
|
|
136
140
|
originPath.match(/^\/44'\/[01]'\/(\d+)'$/)) ||
|
|
@@ -143,32 +147,20 @@ function isLedgerStandard({ ledgerTemplate, keyRoots, network = bitcoinjs_lib_1.
|
|
|
143
147
|
return true;
|
|
144
148
|
return false;
|
|
145
149
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (ledgerManager && (ledgerClient || ledgerState))
|
|
149
|
-
throw new Error(`ledgerClient and ledgerState have been deprecated`);
|
|
150
|
-
if (ledgerManager)
|
|
151
|
-
({ ledgerClient, ledgerState } = ledgerManager);
|
|
152
|
-
if (!ledgerClient || !ledgerState)
|
|
153
|
-
throw new Error(`Could not retrieve ledgerClient or ledgerState`);
|
|
150
|
+
async function getLedgerMasterFingerPrint({ ledgerManager }) {
|
|
151
|
+
const { ledgerClient, ledgerState } = ledgerManager;
|
|
154
152
|
const { AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
|
|
155
153
|
if (!(ledgerClient instanceof AppClient))
|
|
156
154
|
throw new Error(`Error: pass a valid ledgerClient`);
|
|
157
155
|
let masterFingerprint = ledgerState.masterFingerprint;
|
|
158
156
|
if (!masterFingerprint) {
|
|
159
|
-
masterFingerprint =
|
|
157
|
+
masterFingerprint = (0, uint8array_tools_1.fromHex)(await ledgerClient.getMasterFingerprint());
|
|
160
158
|
ledgerState.masterFingerprint = masterFingerprint;
|
|
161
159
|
}
|
|
162
160
|
return masterFingerprint;
|
|
163
161
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (ledgerManager && (ledgerClient || ledgerState))
|
|
167
|
-
throw new Error(`ledgerClient and ledgerState have been deprecated`);
|
|
168
|
-
if (ledgerManager)
|
|
169
|
-
({ ledgerClient, ledgerState } = ledgerManager);
|
|
170
|
-
if (!ledgerClient || !ledgerState)
|
|
171
|
-
throw new Error(`Could not retrieve ledgerClient or ledgerState`);
|
|
162
|
+
async function getLedgerXpub({ originPath, ledgerManager }) {
|
|
163
|
+
const { ledgerClient, ledgerState } = ledgerManager;
|
|
172
164
|
const { AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
|
|
173
165
|
if (!(ledgerClient instanceof AppClient))
|
|
174
166
|
throw new Error(`Error: pass a valid ledgerClient`);
|
|
@@ -199,7 +191,7 @@ async function getLedgerXpub({ originPath, ledgerClient, ledgerState, ledgerMana
|
|
|
199
191
|
* All considerations in the header of this file are applied
|
|
200
192
|
*/
|
|
201
193
|
async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
202
|
-
const { ledgerState,
|
|
194
|
+
const { ledgerState, ecc, network } = ledgerManager;
|
|
203
195
|
const { Output } = (0, descriptors_1.DescriptorsFactory)(ecc);
|
|
204
196
|
const input = psbt.data.inputs[index];
|
|
205
197
|
if (!input)
|
|
@@ -209,28 +201,31 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
209
201
|
const vout = psbt.txInputs[index]?.index;
|
|
210
202
|
if (vout === undefined)
|
|
211
203
|
throw new Error(`Could not extract vout from nonWitnessUtxo for input ${index}.`);
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
const nonWitnessScript = bitcoinjs_lib_1.Transaction.fromBuffer(input.nonWitnessUtxo).outs[vout]?.script;
|
|
205
|
+
scriptPubKey = nonWitnessScript;
|
|
214
206
|
}
|
|
215
207
|
else if (input.witnessUtxo) {
|
|
216
208
|
scriptPubKey = input.witnessUtxo.script;
|
|
217
209
|
}
|
|
218
210
|
if (!scriptPubKey)
|
|
219
211
|
throw new Error(`Could not retrieve scriptPubKey for input ${index}.`);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
212
|
+
const keyDerivations = [
|
|
213
|
+
...(input.bip32Derivation || []),
|
|
214
|
+
...(input.tapBip32Derivation || [])
|
|
215
|
+
];
|
|
216
|
+
if (keyDerivations.length === 0)
|
|
217
|
+
throw new Error(`Input ${index} does not contain bip32 or tapBip32 derivations.`);
|
|
223
218
|
const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
|
|
224
219
|
ledgerManager
|
|
225
220
|
});
|
|
226
|
-
for (const
|
|
221
|
+
for (const keyDerivation of keyDerivations) {
|
|
227
222
|
//get the keyRoot and keyPath. If it matches one of our policies then
|
|
228
223
|
//we are still not sure this is the policy that must be used yet
|
|
229
224
|
//So we must use the template and the keyRoot of each policy and compute the
|
|
230
225
|
//scriptPubKey:
|
|
231
|
-
if (
|
|
226
|
+
if ((0, uint8array_tools_1.compare)(keyDerivation.masterFingerprint, ledgerMasterFingerprint) === 0) {
|
|
232
227
|
// Match /m followed by n consecutive hardened levels and then 2 consecutive unhardened levels:
|
|
233
|
-
const match =
|
|
228
|
+
const match = keyDerivation.path.match(/m((\/\d+['hH])*)(\/\d+\/\d+)?/);
|
|
234
229
|
const originPath = match ? match[1] : undefined; //n consecutive hardened levels
|
|
235
230
|
const keyPath = match ? match[3] : undefined; //2 unhardened levels or undefined
|
|
236
231
|
if (originPath && keyPath) {
|
|
@@ -239,7 +234,7 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
239
234
|
throw new Error(`keyPath ${keyPath} incorrectly extracted`);
|
|
240
235
|
const change = parseInt(strChange, 10);
|
|
241
236
|
const index = parseInt(strIndex, 10);
|
|
242
|
-
const coinType =
|
|
237
|
+
const coinType = (0, networkUtils_1.coinTypeFromNetwork)(network);
|
|
243
238
|
//standard policy candidate. This policy will be added to the pool
|
|
244
239
|
//of policies below and check if it produces the correct scriptPubKey
|
|
245
240
|
let standardPolicy;
|
|
@@ -254,15 +249,11 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
254
249
|
? 'tr(@0/**)'
|
|
255
250
|
: undefined;
|
|
256
251
|
if (standardTemplate) {
|
|
257
|
-
const xpub = await getLedgerXpub({
|
|
258
|
-
originPath,
|
|
259
|
-
ledgerClient,
|
|
260
|
-
ledgerState
|
|
261
|
-
});
|
|
252
|
+
const xpub = await getLedgerXpub({ originPath, ledgerManager });
|
|
262
253
|
standardPolicy = {
|
|
263
254
|
ledgerTemplate: standardTemplate,
|
|
264
255
|
keyRoots: [
|
|
265
|
-
`[${
|
|
256
|
+
`[${(0, uint8array_tools_1.toHex)(ledgerMasterFingerprint)}${originPath}]${xpub}`
|
|
266
257
|
]
|
|
267
258
|
};
|
|
268
259
|
}
|
|
@@ -310,8 +301,12 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
310
301
|
else
|
|
311
302
|
descriptor = undefined;
|
|
312
303
|
}
|
|
313
|
-
else
|
|
314
|
-
|
|
304
|
+
else {
|
|
305
|
+
// Keys without origin info are treated as external by Ledger
|
|
306
|
+
// and are allowed in policy matching.
|
|
307
|
+
if (descriptor)
|
|
308
|
+
descriptor = descriptor.replace(new RegExp(`@${i}`, 'g'), keyRoot);
|
|
309
|
+
}
|
|
315
310
|
}
|
|
316
311
|
//verify the scriptPubKey from the input vs. the one obtained from
|
|
317
312
|
//the policy after having filled in the keyPath in the template
|
|
@@ -320,7 +315,7 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
320
315
|
descriptor,
|
|
321
316
|
network
|
|
322
317
|
}).getScriptPubKey();
|
|
323
|
-
if (
|
|
318
|
+
if ((0, uint8array_tools_1.compare)(policyScriptPubKey, scriptPubKey) === 0) {
|
|
324
319
|
return policy;
|
|
325
320
|
}
|
|
326
321
|
}
|
|
@@ -358,14 +353,52 @@ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
|
|
|
358
353
|
* This function takes into account all the considerations regarding Ledger
|
|
359
354
|
* policy implementation details expressed in the header of this file.
|
|
360
355
|
*/
|
|
361
|
-
async function ledgerPolicyFromOutput({ output,
|
|
362
|
-
const
|
|
363
|
-
|
|
356
|
+
async function ledgerPolicyFromOutput({ output, ledgerManager }) {
|
|
357
|
+
const expanded = output.expand();
|
|
358
|
+
let expandedExpression = expanded.expandedExpression;
|
|
359
|
+
const expansionMap = expanded.expansionMap
|
|
360
|
+
? { ...expanded.expansionMap }
|
|
361
|
+
: undefined;
|
|
362
|
+
// Taproot script-path keys are expanded in tapTreeInfo leaf expansion maps,
|
|
363
|
+
// not in the top-level expansionMap. For ledger policy derivation we remap
|
|
364
|
+
// leaf-local placeholders to global placeholders and merge all leaf keys into
|
|
365
|
+
// the top-level expansionMap.
|
|
366
|
+
if (expandedExpression?.startsWith('tr(@0,') &&
|
|
367
|
+
expansionMap &&
|
|
368
|
+
expanded.tapTreeInfo) {
|
|
369
|
+
const keyExpressionToGlobalPlaceholder = new Map(Object.entries(expansionMap).map(([placeholder, keyInfo]) => [
|
|
370
|
+
keyInfo.keyExpression,
|
|
371
|
+
placeholder
|
|
372
|
+
]));
|
|
373
|
+
let nextPlaceholderIndex = Object.keys(expansionMap).length;
|
|
374
|
+
const globalPlaceholderFor = (keyInfo) => {
|
|
375
|
+
const existing = keyExpressionToGlobalPlaceholder.get(keyInfo.keyExpression);
|
|
376
|
+
if (existing)
|
|
377
|
+
return existing;
|
|
378
|
+
const placeholder = `@${nextPlaceholderIndex}`;
|
|
379
|
+
nextPlaceholderIndex += 1;
|
|
380
|
+
keyExpressionToGlobalPlaceholder.set(keyInfo.keyExpression, placeholder);
|
|
381
|
+
expansionMap[placeholder] = keyInfo;
|
|
382
|
+
return placeholder;
|
|
383
|
+
};
|
|
384
|
+
const remapTapTree = (node) => {
|
|
385
|
+
if ('miniscript' in node) {
|
|
386
|
+
let remappedMiniscript = node.expandedMiniscript;
|
|
387
|
+
const localEntries = Object.entries(node.expansionMap).sort(([placeholderA], [placeholderB]) => placeholderB.length - placeholderA.length);
|
|
388
|
+
for (const [localPlaceholder, keyInfo] of localEntries) {
|
|
389
|
+
const globalPlaceholder = globalPlaceholderFor(keyInfo);
|
|
390
|
+
remappedMiniscript = remappedMiniscript.replaceAll(localPlaceholder, globalPlaceholder);
|
|
391
|
+
}
|
|
392
|
+
return remappedMiniscript;
|
|
393
|
+
}
|
|
394
|
+
return `{${remapTapTree(node.left)},${remapTapTree(node.right)}}`;
|
|
395
|
+
};
|
|
396
|
+
expandedExpression = `tr(@0,${remapTapTree(expanded.tapTreeInfo)})`;
|
|
397
|
+
}
|
|
364
398
|
if (!expandedExpression || !expansionMap)
|
|
365
399
|
throw new Error(`Error: invalid output`);
|
|
366
400
|
const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
|
|
367
|
-
|
|
368
|
-
ledgerState
|
|
401
|
+
ledgerManager
|
|
369
402
|
});
|
|
370
403
|
//It's important to have keys sorted in ascii order. keys
|
|
371
404
|
//are of this type: @0, @1, @2, .... and they also appear in the expandedExpression
|
|
@@ -375,7 +408,7 @@ async function ledgerPolicyFromOutput({ output, ledgerClient, ledgerState }) {
|
|
|
375
408
|
const ledgerKeys = allKeys.filter(key => {
|
|
376
409
|
const masterFingerprint = expansionMap[key]?.masterFingerprint;
|
|
377
410
|
return (masterFingerprint &&
|
|
378
|
-
|
|
411
|
+
(0, uint8array_tools_1.compare)(masterFingerprint, ledgerMasterFingerprint) === 0);
|
|
379
412
|
});
|
|
380
413
|
if (ledgerKeys.length === 0)
|
|
381
414
|
return null;
|
|
@@ -412,52 +445,42 @@ async function ledgerPolicyFromOutput({ output, ledgerClient, ledgerState }) {
|
|
|
412
445
|
ledgerTemplate = ledgerTemplate.replaceAll(key, `@${keyRoots.length}/**`);
|
|
413
446
|
const keyInfo = expansionMap[key];
|
|
414
447
|
if (keyInfo.masterFingerprint && keyInfo.originPath)
|
|
415
|
-
keyRoots.push(`[${keyInfo.masterFingerprint
|
|
448
|
+
keyRoots.push(`[${(0, uint8array_tools_1.toHex)(keyInfo.masterFingerprint)}${keyInfo.originPath}]${keyInfo?.bip32?.neutered().toBase58()}`);
|
|
416
449
|
else
|
|
417
450
|
keyRoots.push(`${keyInfo?.bip32?.neutered().toBase58()}`);
|
|
418
451
|
});
|
|
419
452
|
return { ledgerTemplate, keyRoots };
|
|
420
453
|
}
|
|
421
454
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
455
|
+
* Registers a policy based on a provided descriptor.
|
|
456
|
+
*
|
|
457
|
+
* This function will:
|
|
458
|
+
* 1. Store the policy in `ledgerState` inside the `ledgerManager`.
|
|
459
|
+
* 2. Avoid re-registering if the policy was previously registered.
|
|
460
|
+
* 3. Skip registration if the policy is considered "standard".
|
|
461
|
+
*
|
|
462
|
+
* It's important to understand the nature of the Ledger Policy being registered:
|
|
463
|
+
* - While a descriptor might point to a specific output index of a particular change address,
|
|
464
|
+
* the corresponding Ledger Policy abstracts this and represents potential outputs for
|
|
465
|
+
* all addresses (both external and internal).
|
|
466
|
+
* - This means that the registered Ledger Policy is a generalized version of the descriptor,
|
|
467
|
+
* not assuming specific values for the keyPath.
|
|
468
|
+
*/
|
|
469
|
+
async function registerLedgerWallet({ descriptor, ledgerManager, policyName }) {
|
|
470
|
+
const { ledgerClient, ledgerState, ecc, network } = ledgerManager;
|
|
437
471
|
const { WalletPolicy, AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
|
|
438
472
|
if (!(ledgerClient instanceof AppClient))
|
|
439
473
|
throw new Error(`Error: pass a valid ledgerClient`);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
output = new Output({
|
|
446
|
-
descriptor,
|
|
447
|
-
...(descriptor.includes('*') ? { index: 0 } : {}), //if ranged set any index
|
|
448
|
-
network: ledgerManager.network
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
else
|
|
452
|
-
output = descriptor;
|
|
453
|
-
if (await ledgerPolicyFromStandard({ output, ledgerClient, ledgerState }))
|
|
454
|
-
return;
|
|
455
|
-
const result = await ledgerPolicyFromOutput({
|
|
456
|
-
output,
|
|
457
|
-
ledgerClient,
|
|
458
|
-
ledgerState
|
|
474
|
+
const { Output } = (0, descriptors_1.DescriptorsFactory)(ecc);
|
|
475
|
+
const output = new Output({
|
|
476
|
+
descriptor,
|
|
477
|
+
...(descriptor.includes('*') ? { index: 0 } : {}), //if ranged set any index
|
|
478
|
+
network
|
|
459
479
|
});
|
|
460
|
-
if (await ledgerPolicyFromStandard({ output,
|
|
480
|
+
if (await ledgerPolicyFromStandard({ output, ledgerManager }))
|
|
481
|
+
return;
|
|
482
|
+
const result = await ledgerPolicyFromOutput({ output, ledgerManager });
|
|
483
|
+
if (await ledgerPolicyFromStandard({ output, ledgerManager }))
|
|
461
484
|
return;
|
|
462
485
|
if (!result)
|
|
463
486
|
throw new Error(`Error: output does not have a ledger input`);
|
|
@@ -466,11 +489,7 @@ async function registerLedgerWallet({ descriptor, ledgerClient, ledgerState, led
|
|
|
466
489
|
ledgerState.policies = [];
|
|
467
490
|
let walletPolicy, policyHmac;
|
|
468
491
|
//Search in ledgerState first
|
|
469
|
-
const policy = await ledgerPolicyFromState({
|
|
470
|
-
output,
|
|
471
|
-
ledgerClient,
|
|
472
|
-
ledgerState
|
|
473
|
-
});
|
|
492
|
+
const policy = await ledgerPolicyFromState({ output, ledgerManager });
|
|
474
493
|
if (policy) {
|
|
475
494
|
if (policy.policyName !== policyName)
|
|
476
495
|
throw new Error(`Error: policy was already registered with a different name: ${policy.policyName}`);
|
|
@@ -493,11 +512,10 @@ async function registerLedgerWallet({ descriptor, ledgerClient, ledgerState, led
|
|
|
493
512
|
/**
|
|
494
513
|
* Retrieve a standard ledger policy or null if it does correspond.
|
|
495
514
|
**/
|
|
496
|
-
async function ledgerPolicyFromStandard({ output,
|
|
515
|
+
async function ledgerPolicyFromStandard({ output, ledgerManager }) {
|
|
497
516
|
const result = await ledgerPolicyFromOutput({
|
|
498
517
|
output,
|
|
499
|
-
|
|
500
|
-
ledgerState
|
|
518
|
+
ledgerManager
|
|
501
519
|
});
|
|
502
520
|
if (!result)
|
|
503
521
|
throw new Error(`Error: descriptor does not have a ledger input`);
|
|
@@ -528,11 +546,11 @@ function comparePolicies(policyA, policyB) {
|
|
|
528
546
|
/**
|
|
529
547
|
* Retrieve a ledger policy from ledgerState or null if it does not exist yet.
|
|
530
548
|
**/
|
|
531
|
-
async function ledgerPolicyFromState({ output,
|
|
549
|
+
async function ledgerPolicyFromState({ output, ledgerManager }) {
|
|
550
|
+
const { ledgerState } = ledgerManager;
|
|
532
551
|
const result = await ledgerPolicyFromOutput({
|
|
533
552
|
output,
|
|
534
|
-
|
|
535
|
-
ledgerState
|
|
553
|
+
ledgerManager
|
|
536
554
|
});
|
|
537
555
|
if (!result)
|
|
538
556
|
throw new Error(`Error: output does not have a ledger input`);
|
package/dist/miniscript.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Network } from 'bitcoinjs-lib';
|
|
2
2
|
import type { ECPairAPI } from 'ecpair';
|
|
3
3
|
import type { BIP32API } from 'bip32';
|
|
4
|
-
import type { PartialSig } from 'bip174
|
|
4
|
+
import type { PartialSig } from 'bip174';
|
|
5
5
|
import type { Preimage, TimeConstraints, ExpansionMap } from './types';
|
|
6
6
|
/**
|
|
7
7
|
* Expand a miniscript to a generalized form using variables instead of key
|
|
@@ -21,10 +21,11 @@ export declare function expandMiniscript({ miniscript, isSegwit, isTaproot, netw
|
|
|
21
21
|
expandedMiniscript: string;
|
|
22
22
|
expansionMap: ExpansionMap;
|
|
23
23
|
};
|
|
24
|
-
export declare function miniscript2Script({ expandedMiniscript, expansionMap }: {
|
|
24
|
+
export declare function miniscript2Script({ expandedMiniscript, expansionMap, tapscript }: {
|
|
25
25
|
expandedMiniscript: string;
|
|
26
26
|
expansionMap: ExpansionMap;
|
|
27
|
-
|
|
27
|
+
tapscript?: boolean;
|
|
28
|
+
}): Uint8Array;
|
|
28
29
|
/**
|
|
29
30
|
* Assumptions:
|
|
30
31
|
* The attacker does not have access to any of the private keys of public keys
|
|
@@ -36,19 +37,32 @@ export declare function miniscript2Script({ expandedMiniscript, expansionMap }:
|
|
|
36
37
|
* Pass timeConstraints to search for the first solution with this nLockTime and
|
|
37
38
|
* nSequence. Throw if no solution is possible using these constraints.
|
|
38
39
|
*
|
|
40
|
+
* Time constraints are used to keep the chosen satisfaction stable between the
|
|
41
|
+
* planning pass (fake signatures) and the signing pass (real signatures).
|
|
42
|
+
* We run the satisfier once with fake signatures to discover the implied
|
|
43
|
+
* nLockTime/nSequence without requiring user signatures. If real signatures
|
|
44
|
+
* had the same length, the satisfier would typically pick the same
|
|
45
|
+
* minimal-weight solution again. But ECDSA signature sizes can vary (71–73
|
|
46
|
+
* bytes), which may change which solution is considered "smallest".
|
|
47
|
+
*
|
|
48
|
+
* Passing the previously derived timeConstraints in the second pass forces the
|
|
49
|
+
* same solution to be selected, ensuring locktime/sequence do not change
|
|
50
|
+
* between planning and finalization.
|
|
51
|
+
*
|
|
39
52
|
* Don't pass timeConstraints (this is the default) if you want to get the
|
|
40
53
|
* smallest size solution altogether.
|
|
41
54
|
*
|
|
42
55
|
* If a solution is not found this function throws.
|
|
43
56
|
*/
|
|
44
|
-
export declare function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures, preimages, timeConstraints }: {
|
|
57
|
+
export declare function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures, preimages, timeConstraints, tapscript }: {
|
|
45
58
|
expandedMiniscript: string;
|
|
46
59
|
expansionMap: ExpansionMap;
|
|
47
60
|
signatures?: PartialSig[];
|
|
48
61
|
preimages?: Preimage[];
|
|
49
62
|
timeConstraints?: TimeConstraints;
|
|
63
|
+
tapscript?: boolean;
|
|
50
64
|
}): {
|
|
51
|
-
scriptSatisfaction:
|
|
65
|
+
scriptSatisfaction: Uint8Array;
|
|
52
66
|
nLockTime: number | undefined;
|
|
53
67
|
nSequence: number | undefined;
|
|
54
68
|
};
|
|
@@ -78,7 +92,7 @@ export declare function satisfyMiniscript({ expandedMiniscript, expansionMap, si
|
|
|
78
92
|
* However, the `0` number is an edge case that we specially handle with this
|
|
79
93
|
* function.
|
|
80
94
|
*
|
|
81
|
-
* bitcoinjs-lib's `bscript.number.encode(0)` produces an empty
|
|
95
|
+
* bitcoinjs-lib's `bscript.number.encode(0)` produces an empty array.
|
|
82
96
|
* This is what the Bitcoin interpreter does and it is what `script.number.encode` was
|
|
83
97
|
* implemented to do.
|
|
84
98
|
*
|
package/dist/miniscript.js
CHANGED
|
@@ -43,6 +43,7 @@ const bitcoinjs_lib_1 = require("bitcoinjs-lib");
|
|
|
43
43
|
const keyExpressions_1 = require("./keyExpressions");
|
|
44
44
|
const RE = __importStar(require("./re"));
|
|
45
45
|
const miniscript_1 = require("@bitcoinerlab/miniscript");
|
|
46
|
+
const uint8array_tools_1 = require("uint8array-tools");
|
|
46
47
|
/**
|
|
47
48
|
* Expand a miniscript to a generalized form using variables instead of key
|
|
48
49
|
* expressions. Variables will be of this form: @0, @1, ...
|
|
@@ -51,24 +52,50 @@ const miniscript_1 = require("@bitcoinerlab/miniscript");
|
|
|
51
52
|
* Also compute pubkeys from descriptors to use them later.
|
|
52
53
|
*/
|
|
53
54
|
function expandMiniscript({ miniscript, isSegwit, isTaproot, network = bitcoinjs_lib_1.networks.bitcoin, ECPair, BIP32 }) {
|
|
54
|
-
if (isTaproot)
|
|
55
|
-
throw new Error('Taproot miniscript not yet supported.');
|
|
56
55
|
const reKeyExp = isTaproot
|
|
57
56
|
? RE.reTaprootKeyExp
|
|
58
57
|
: isSegwit
|
|
59
58
|
? RE.reSegwitKeyExp
|
|
60
59
|
: RE.reNonSegwitKeyExp;
|
|
61
60
|
const expansionMap = {};
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
let keyIndex = 0;
|
|
62
|
+
const keyExpressionRegex = new RegExp(String.raw `^${reKeyExp}$`);
|
|
63
|
+
const replaceKeyExpression = (keyExpression) => {
|
|
64
|
+
const trimmed = keyExpression.trim();
|
|
65
|
+
if (!trimmed)
|
|
66
|
+
throw new Error(`Error: expected a keyExpression but got ${keyExpression}`);
|
|
67
|
+
if (!keyExpressionRegex.test(trimmed))
|
|
68
|
+
throw new Error(`Error: expected a keyExpression but got ${trimmed}`);
|
|
69
|
+
const key = `@${keyIndex}`;
|
|
70
|
+
keyIndex += 1;
|
|
64
71
|
expansionMap[key] = (0, keyExpressions_1.parseKeyExpression)({
|
|
65
|
-
keyExpression,
|
|
72
|
+
keyExpression: trimmed,
|
|
66
73
|
isSegwit,
|
|
74
|
+
isTaproot,
|
|
67
75
|
network,
|
|
68
76
|
ECPair,
|
|
69
77
|
BIP32
|
|
70
78
|
});
|
|
71
79
|
return key;
|
|
80
|
+
};
|
|
81
|
+
//These are the fragments where keys are allowed. Note that we only look
|
|
82
|
+
//inside these fragments to avoid problems in pregimages like sha256 which can
|
|
83
|
+
//contain hex values which could be confussed with a key
|
|
84
|
+
const keyFragmentRegex = /\b(pk|pkh|sortedmulti_a|sortedmulti|multi_a|multi)\(([^()]*)\)/g;
|
|
85
|
+
const expandedMiniscript = miniscript.replace(keyFragmentRegex, (_, name, inner) => {
|
|
86
|
+
if (name === 'pk' || name === 'pkh')
|
|
87
|
+
return `${name}(${replaceKeyExpression(inner)})`;
|
|
88
|
+
//now do *multi* which has arguments:
|
|
89
|
+
const parts = inner.split(',').map(part => part.trim());
|
|
90
|
+
if (parts.length < 2)
|
|
91
|
+
throw new Error(`Error: invalid miniscript ${miniscript} (missing keys)`);
|
|
92
|
+
const k = parts[0] ?? '';
|
|
93
|
+
if (!k)
|
|
94
|
+
throw new Error(`Error: invalid miniscript ${miniscript} (missing threshold)`);
|
|
95
|
+
const replacedKeys = parts
|
|
96
|
+
.slice(1)
|
|
97
|
+
.map(keyExpression => replaceKeyExpression(keyExpression));
|
|
98
|
+
return `${name}(${[k, ...replacedKeys].join(',')})`;
|
|
72
99
|
});
|
|
73
100
|
//Do some assertions. Miniscript must not have duplicate keys, also all
|
|
74
101
|
//keyExpressions must produce a valid pubkey (unless it's ranged and we want
|
|
@@ -78,7 +105,7 @@ function expandMiniscript({ miniscript, isSegwit, isTaproot, network = bitcoinjs
|
|
|
78
105
|
.map(keyInfo => {
|
|
79
106
|
if (!keyInfo.pubkey)
|
|
80
107
|
throw new Error(`Error: keyExpression ${keyInfo.keyExpression} does not have a pubkey`);
|
|
81
|
-
return keyInfo.pubkey
|
|
108
|
+
return (0, uint8array_tools_1.toHex)(keyInfo.pubkey);
|
|
82
109
|
});
|
|
83
110
|
if (new Set(pubkeysHex).size !== pubkeysHex.length) {
|
|
84
111
|
throw new Error(`Error: miniscript ${miniscript} is not sane: contains duplicate public keys.`);
|
|
@@ -100,8 +127,8 @@ function substituteAsm({ expandedAsm, expansionMap }) {
|
|
|
100
127
|
throw new Error(`Error: invalid expansionMap for ${key}`);
|
|
101
128
|
}
|
|
102
129
|
return accAsm
|
|
103
|
-
.replaceAll(`<${key}>`, `<${
|
|
104
|
-
.replaceAll(`<HASH160(${key})>`, `<${bitcoinjs_lib_1.crypto.hash160(pubkey)
|
|
130
|
+
.replaceAll(`<${key}>`, `<${(0, uint8array_tools_1.toHex)(pubkey)}>`)
|
|
131
|
+
.replaceAll(`<HASH160(${key})>`, `<${(0, uint8array_tools_1.toHex)(bitcoinjs_lib_1.crypto.hash160(pubkey))}>`);
|
|
105
132
|
}, expandedAsm);
|
|
106
133
|
//Now clean it and prepare it so that fromASM can be called:
|
|
107
134
|
asm = asm
|
|
@@ -119,8 +146,8 @@ function substituteAsm({ expandedAsm, expansionMap }) {
|
|
|
119
146
|
.replace(/[<>]/g, '');
|
|
120
147
|
return asm;
|
|
121
148
|
}
|
|
122
|
-
function miniscript2Script({ expandedMiniscript, expansionMap }) {
|
|
123
|
-
const compiled = (0, miniscript_1.compileMiniscript)(expandedMiniscript);
|
|
149
|
+
function miniscript2Script({ expandedMiniscript, expansionMap, tapscript = false }) {
|
|
150
|
+
const compiled = (0, miniscript_1.compileMiniscript)(expandedMiniscript, { tapscript });
|
|
124
151
|
if (compiled.issane !== true) {
|
|
125
152
|
throw new Error(`Error: Miniscript ${expandedMiniscript} is not sane`);
|
|
126
153
|
}
|
|
@@ -137,12 +164,24 @@ function miniscript2Script({ expandedMiniscript, expansionMap }) {
|
|
|
137
164
|
* Pass timeConstraints to search for the first solution with this nLockTime and
|
|
138
165
|
* nSequence. Throw if no solution is possible using these constraints.
|
|
139
166
|
*
|
|
167
|
+
* Time constraints are used to keep the chosen satisfaction stable between the
|
|
168
|
+
* planning pass (fake signatures) and the signing pass (real signatures).
|
|
169
|
+
* We run the satisfier once with fake signatures to discover the implied
|
|
170
|
+
* nLockTime/nSequence without requiring user signatures. If real signatures
|
|
171
|
+
* had the same length, the satisfier would typically pick the same
|
|
172
|
+
* minimal-weight solution again. But ECDSA signature sizes can vary (71–73
|
|
173
|
+
* bytes), which may change which solution is considered "smallest".
|
|
174
|
+
*
|
|
175
|
+
* Passing the previously derived timeConstraints in the second pass forces the
|
|
176
|
+
* same solution to be selected, ensuring locktime/sequence do not change
|
|
177
|
+
* between planning and finalization.
|
|
178
|
+
*
|
|
140
179
|
* Don't pass timeConstraints (this is the default) if you want to get the
|
|
141
180
|
* smallest size solution altogether.
|
|
142
181
|
*
|
|
143
182
|
* If a solution is not found this function throws.
|
|
144
183
|
*/
|
|
145
|
-
function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures = [], preimages = [], timeConstraints }) {
|
|
184
|
+
function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures = [], preimages = [], timeConstraints, tapscript = false }) {
|
|
146
185
|
//convert 'sha256(6c...33)' to: { ['<sha256_preimage(6c...33)>']: '10...5f'}
|
|
147
186
|
const preimageMap = {};
|
|
148
187
|
preimages.forEach(preimage => {
|
|
@@ -153,15 +192,18 @@ function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures = [],
|
|
|
153
192
|
//get the keyExpressions: @0, @1 from the keys in expansionMap
|
|
154
193
|
const expandedSignatureMap = {};
|
|
155
194
|
signatures.forEach(signature => {
|
|
156
|
-
const pubkeyHex = signature.pubkey
|
|
157
|
-
const keyExpression = Object.keys(expansionMap).find(k => expansionMap[k]?.pubkey
|
|
195
|
+
const pubkeyHex = (0, uint8array_tools_1.toHex)(signature.pubkey);
|
|
196
|
+
const keyExpression = Object.keys(expansionMap).find(k => expansionMap[k]?.pubkey && (0, uint8array_tools_1.toHex)(expansionMap[k].pubkey) === pubkeyHex);
|
|
158
197
|
expandedSignatureMap['<sig(' + keyExpression + ')>'] =
|
|
159
|
-
'<' + signature.signature
|
|
198
|
+
'<' + (0, uint8array_tools_1.toHex)(signature.signature) + '>';
|
|
160
199
|
});
|
|
161
200
|
const expandedKnownsMap = { ...preimageMap, ...expandedSignatureMap };
|
|
162
201
|
const knowns = Object.keys(expandedKnownsMap);
|
|
163
202
|
//satisfier verifies again internally whether expandedKnownsMap with given knowns is sane
|
|
164
|
-
const { nonMalleableSats } = (0, miniscript_1.satisfier)(expandedMiniscript, {
|
|
203
|
+
const { nonMalleableSats } = (0, miniscript_1.satisfier)(expandedMiniscript, {
|
|
204
|
+
knowns,
|
|
205
|
+
tapscript
|
|
206
|
+
});
|
|
165
207
|
if (!Array.isArray(nonMalleableSats) || !nonMalleableSats[0])
|
|
166
208
|
throw new Error(`Error: unresolvable miniscript ${expandedMiniscript}`);
|
|
167
209
|
let sat;
|
|
@@ -218,7 +260,7 @@ function satisfyMiniscript({ expandedMiniscript, expansionMap, signatures = [],
|
|
|
218
260
|
* However, the `0` number is an edge case that we specially handle with this
|
|
219
261
|
* function.
|
|
220
262
|
*
|
|
221
|
-
* bitcoinjs-lib's `bscript.number.encode(0)` produces an empty
|
|
263
|
+
* bitcoinjs-lib's `bscript.number.encode(0)` produces an empty array.
|
|
222
264
|
* This is what the Bitcoin interpreter does and it is what `script.number.encode` was
|
|
223
265
|
* implemented to do.
|
|
224
266
|
*
|
|
@@ -254,5 +296,5 @@ function numberEncodeAsm(number) {
|
|
|
254
296
|
return 'OP_0';
|
|
255
297
|
}
|
|
256
298
|
else
|
|
257
|
-
return bitcoinjs_lib_1.script.number.encode(number)
|
|
299
|
+
return (0, uint8array_tools_1.toHex)(bitcoinjs_lib_1.script.number.encode(number));
|
|
258
300
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves all multipath tuple segments (for example `/<0;1>/*`) in lockstep
|
|
3
|
+
* using the provided `change` value.
|
|
4
|
+
*
|
|
5
|
+
* - `/**` is first canonicalized to `/<0;1>/*`.
|
|
6
|
+
* - All tuples in the descriptor must have the same cardinality.
|
|
7
|
+
* - Tuple values must be strictly increasing decimal numbers.
|
|
8
|
+
* - `change` must match one of the values in each tuple.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveMultipathDescriptor({ descriptor, change }: {
|
|
11
|
+
descriptor: string;
|
|
12
|
+
change?: number;
|
|
13
|
+
}): string;
|