@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/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
- //Network is the 6th character: /44'/0'
133
- if (originPath[5] !== (network === bitcoinjs_lib_1.networks.bitcoin ? '0' : '1'))
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
- /** @overload */
147
- async function getLedgerMasterFingerPrint({ ledgerClient, ledgerState, ledgerManager }) {
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 = Buffer.from(await ledgerClient.getMasterFingerprint(), 'hex');
157
+ masterFingerprint = (0, uint8array_tools_1.fromHex)(await ledgerClient.getMasterFingerprint());
160
158
  ledgerState.masterFingerprint = masterFingerprint;
161
159
  }
162
160
  return masterFingerprint;
163
161
  }
164
- /** @hidden */
165
- async function getLedgerXpub({ originPath, ledgerClient, ledgerState, ledgerManager }) {
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, ledgerClient, ecc, network } = ledgerManager;
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
- scriptPubKey = bitcoinjs_lib_1.Transaction.fromBuffer(input.nonWitnessUtxo).outs[vout]
213
- ?.script;
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 bip32Derivations = input.bip32Derivation;
221
- if (!bip32Derivations || !bip32Derivations.length)
222
- throw new Error(`Input ${index} does not contain bip32 derivations.`);
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 bip32Derivation of bip32Derivations) {
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 (Buffer.compare(bip32Derivation.masterFingerprint, ledgerMasterFingerprint) === 0) {
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 = bip32Derivation.path.match(/m((\/\d+['hH])*)(\/\d+\/\d+)?/);
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 = network === bitcoinjs_lib_1.networks.bitcoin ? 0 : 1;
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
- `[${ledgerMasterFingerprint.toString('hex')}${originPath}]${xpub}`
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
- descriptor = undefined;
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 (Buffer.compare(policyScriptPubKey, scriptPubKey) === 0) {
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, ledgerClient, ledgerState }) {
362
- const expandedExpression = output.expand().expandedExpression;
363
- const expansionMap = output.expand().expansionMap;
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
- ledgerClient,
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
- Buffer.compare(masterFingerprint, ledgerMasterFingerprint) === 0);
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?.toString('hex')}${keyInfo.originPath}]${keyInfo?.bip32?.neutered().toBase58()}`);
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
- * To be removed in v3.0 and replaced by a version that does not accept
423
- * descriptors
424
- * @overload
425
- **/
426
- async function registerLedgerWallet({ descriptor, ledgerClient, ledgerState, ledgerManager, policyName }) {
427
- if (typeof descriptor !== 'string' && ledgerManager)
428
- throw new Error(`Invalid usage: descriptor must be a string`);
429
- if (ledgerManager && (ledgerClient || ledgerState))
430
- throw new Error(`Invalid usage: either ledgerManager or ledgerClient + ledgerState`);
431
- if (ledgerManager)
432
- ({ ledgerClient, ledgerState } = ledgerManager);
433
- if (!ledgerClient)
434
- throw new Error(`ledgerManager not provided`);
435
- if (!ledgerState)
436
- throw new Error(`ledgerManager not provided`);
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
- let output;
441
- if (typeof descriptor === 'string') {
442
- if (!ledgerManager)
443
- throw new Error(`ledgerManager not provided`);
444
- const { Output } = (0, descriptors_1.DescriptorsFactory)(ledgerManager.ecc);
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, ledgerClient, ledgerState }))
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, ledgerClient, ledgerState }) {
515
+ async function ledgerPolicyFromStandard({ output, ledgerManager }) {
497
516
  const result = await ledgerPolicyFromOutput({
498
517
  output,
499
- ledgerClient,
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, ledgerClient, ledgerState }) {
549
+ async function ledgerPolicyFromState({ output, ledgerManager }) {
550
+ const { ledgerState } = ledgerManager;
532
551
  const result = await ledgerPolicyFromOutput({
533
552
  output,
534
- ledgerClient,
535
- ledgerState
553
+ ledgerManager
536
554
  });
537
555
  if (!result)
538
556
  throw new Error(`Error: output does not have a ledger input`);
@@ -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/src/lib/interfaces';
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
- }): Buffer;
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: Buffer;
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 Buffer.
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
  *
@@ -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
- const expandedMiniscript = miniscript.replace(RegExp(reKeyExp, 'g'), (keyExpression) => {
63
- const key = '@' + Object.keys(expansionMap).length;
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.toString('hex');
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}>`, `<${pubkey.toString('hex')}>`)
104
- .replaceAll(`<HASH160(${key})>`, `<${bitcoinjs_lib_1.crypto.hash160(pubkey).toString('hex')}>`);
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.toString('hex');
157
- const keyExpression = Object.keys(expansionMap).find(k => expansionMap[k]?.pubkey?.toString('hex') === pubkeyHex);
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.toString('hex') + '>';
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, { knowns });
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 Buffer.
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).toString('hex');
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;