@bitcoinerlab/descriptors 0.3.0 → 0.3.1

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 CHANGED
@@ -79,8 +79,8 @@ constructor({
79
79
  // to denote an arbitrary index.
80
80
  index, // The descriptor's index in the case of a range descriptor
81
81
  // (must be an integer >= 0).
82
- checksumRequired = false, // Flag indicating if the descriptor is required
83
- // to include a checksum.
82
+ checksumRequired = false // Optional flag indicating if the descriptor is
83
+ // required to include a checksum. Defaults to false.
84
84
  allowMiniscriptInP2SH = false, // Flag indicating if this instance can parse
85
85
  // and generate script satisfactions for
86
86
  // sh(miniscript) top-level expressions of
@@ -142,7 +142,15 @@ const result = expand({
142
142
  network: networks.testnet, // One of bitcoinjs-lib `networks`
143
143
  // (https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/src/networks.js)
144
144
  // or another one with the same interface.
145
- allowMiniscriptInP2SH: true // Optional flag to allow miniscript in P2SH
145
+ // Optional (defaults to bitcoin mainnet).
146
+ allowMiniscriptInP2SH: true, // Optional flag to allow miniscript in P2SH.
147
+ // Defaults to false.
148
+ index, // Optional. The descriptor's index in the case of a range descriptor
149
+ // (must be an integer >= 0). If not set for ranged descriptors, then
150
+ // the function will return an expansionMap with ranged keyPaths and
151
+ // won't compute Payment or scripts.
152
+ checksumRequired = false // Optional flag indicating if the descriptor is
153
+ // required to include a checksum. Defaults to false.
146
154
  });
147
155
  ```
148
156
 
@@ -156,6 +164,8 @@ The `expand()` function returns an object with the following properties:
156
164
  - `expandedMiniscript: string | undefined`: The expanded miniscript, if any.
157
165
  - `redeemScript: Buffer | undefined`: The redeem script for the descriptor, if applicable.
158
166
  - `witnessScript: Buffer | undefined`: The witness script for the descriptor, if applicable.
167
+ - `isRanged: boolean` : Whether the expression represents a ranged descriptor.
168
+ - `canonicalExpression` : This is the preferred or authoritative representation of the descriptor expression. It standardizes the descriptor by replacing indexes on wildcards and eliminating checksums.
159
169
 
160
170
  For the example expression provided, the `expandedExpression` and a portion of the `expansionMap` would be as follows:
161
171
 
@@ -55,7 +55,7 @@ function countNonPushOnlyOPs(script) {
55
55
  const decompile = bitcoinjs_lib_1.script.decompile(script);
56
56
  if (!decompile)
57
57
  throw new Error(`Error: cound not decompile ${script}`);
58
- return decompile.filter(op => op > bitcoinjs_lib_1.script.OPS['OP_16']).length;
58
+ return decompile.filter(op => typeof op === 'number' && op > bitcoinjs_lib_1.script.OPS['OP_16']).length;
59
59
  }
60
60
  /*
61
61
  * Returns a bare descriptor without checksum and particularized for a certain
@@ -75,20 +75,20 @@ function evaluate({ expression, checksumRequired, index }) {
75
75
  throw new Error(`Error: invalid descriptor checksum for ${expression}`);
76
76
  }
77
77
  }
78
- const mWildcard = evaluatedExpression.match(/\*/g);
79
- if (mWildcard && mWildcard.length > 0) {
80
- if (index === undefined)
81
- throw new Error(`Error: index was not provided for ranged descriptor`);
82
- if (!Number.isInteger(index) || index < 0)
83
- throw new Error(`Error: invalid index ${index}`);
84
- //From https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
85
- //To prevent a combinatorial explosion of the search space, if more than
86
- //one of the multi() key arguments is a BIP32 wildcard path ending in /* or
87
- //*', the multi() expression only matches multisig scripts with the ith
88
- //child key from each wildcard path in lockstep, rather than scripts with
89
- //any combination of child keys from each wildcard path.
90
- //We extend this reasoning for musig for all cases
91
- evaluatedExpression = evaluatedExpression.replaceAll('*', index.toString());
78
+ if (index !== undefined) {
79
+ const mWildcard = evaluatedExpression.match(/\*/g);
80
+ if (mWildcard && mWildcard.length > 0) {
81
+ //From https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
82
+ //To prevent a combinatorial explosion of the search space, if more than
83
+ //one of the multi() key arguments is a BIP32 wildcard path ending in /* or
84
+ //*', the multi() expression only matches multisig scripts with the ith
85
+ //child key from each wildcard path in lockstep, rather than scripts with
86
+ //any combination of child keys from each wildcard path.
87
+ //We extend this reasoning for musig for all cases
88
+ evaluatedExpression = evaluatedExpression.replaceAll('*', index.toString());
89
+ }
90
+ else
91
+ throw new Error(`Error: index passed for non-ranged descriptor: ${expression}`);
92
92
  }
93
93
  return evaluatedExpression;
94
94
  }
@@ -132,13 +132,15 @@ function DescriptorsFactory(ecc) {
132
132
  * - expandedMiniscript: The expanded miniscript, if any.
133
133
  * - redeemScript: The redeem script for the descriptor, if applicable.
134
134
  * - witnessScript: The witness script for the descriptor, if applicable.
135
+ * - isRanged: Whether this expression representas a ranged-descriptor
136
+ * - canonicalExpression: This is the preferred or authoritative
137
+ * representation of the descriptor expression. It standardizes the
138
+ * descriptor by replacing indexes on wildcards and eliminating checksums.
139
+ * This helps ensure consistency and facilitates efficient interpretation and handling by systems or software.
135
140
  *
136
141
  * @throws {Error} Throws an error if the descriptor cannot be parsed or does not conform to the expected format.
137
142
  */
138
- const expand = ({ expression, loggedExpression, //this is the expression that will be used for logging error messages
139
- network = bitcoinjs_lib_1.networks.bitcoin, allowMiniscriptInP2SH = false }) => {
140
- //remove the checksum before proceeding:
141
- expression = expression.replace(new RegExp(RE.reChecksum + '$'), '');
143
+ const expand = ({ expression, index, checksumRequired = false, network = bitcoinjs_lib_1.networks.bitcoin, allowMiniscriptInP2SH = false }) => {
142
144
  let expandedExpression;
143
145
  let miniscript;
144
146
  let expansionMap;
@@ -148,15 +150,24 @@ function DescriptorsFactory(ecc) {
148
150
  let witnessScript;
149
151
  let redeemScript;
150
152
  const isRanged = expression.indexOf('*') !== -1;
151
- if (!loggedExpression)
152
- loggedExpression = expression;
153
+ if (index !== undefined)
154
+ if (!Number.isInteger(index) || index < 0)
155
+ throw new Error(`Error: invalid index ${index}`);
156
+ //Verify and remove checksum (if exists) and
157
+ //particularize range descriptor for index (if desc is range descriptor)
158
+ const canonicalExpression = evaluate({
159
+ expression,
160
+ ...(index !== undefined ? { index } : {}),
161
+ checksumRequired
162
+ });
163
+ const isCanonicalRanged = canonicalExpression.indexOf('*') !== -1;
153
164
  //addr(ADDR)
154
- if (expression.match(RE.reAddrAnchored)) {
165
+ if (canonicalExpression.match(RE.reAddrAnchored)) {
155
166
  if (isRanged)
156
167
  throw new Error(`Error: addr() cannot be ranged`);
157
- const matchedAddress = expression.match(RE.reAddrAnchored)?.[1]; //[1]-> whatever is found addr(->HERE<-)
168
+ const matchedAddress = canonicalExpression.match(RE.reAddrAnchored)?.[1]; //[1]-> whatever is found addr(->HERE<-)
158
169
  if (!matchedAddress)
159
- throw new Error(`Error: could not get an address in ${loggedExpression}`);
170
+ throw new Error(`Error: could not get an address in ${expression}`);
160
171
  let output;
161
172
  try {
162
173
  output = bitcoinjs_lib_1.address.toOutputScript(matchedAddress, network);
@@ -189,98 +200,94 @@ function DescriptorsFactory(ecc) {
189
200
  }
190
201
  }
191
202
  //pk(KEY)
192
- else if (expression.match(RE.rePkAnchored)) {
203
+ else if (canonicalExpression.match(RE.rePkAnchored)) {
193
204
  isSegwit = false;
194
- const keyExpression = expression.match(RE.reKeyExp)?.[0];
205
+ const keyExpression = canonicalExpression.match(RE.reKeyExp)?.[0];
195
206
  if (!keyExpression)
196
207
  throw new Error(`Error: keyExpression could not me extracted`);
197
- if (expression !== `pk(${keyExpression})`)
198
- throw new Error(`Error: invalid expression ${loggedExpression}`);
208
+ if (canonicalExpression !== `pk(${keyExpression})`)
209
+ throw new Error(`Error: invalid expression ${expression}`);
199
210
  expandedExpression = 'pk(@0)';
200
- expansionMap = {
201
- '@0': parseKeyExpression({ keyExpression, network, isSegwit })
202
- };
203
- if (!isRanged) {
204
- const pubkey = expansionMap['@0'].pubkey;
211
+ const pKE = parseKeyExpression({ keyExpression, network, isSegwit });
212
+ expansionMap = { '@0': pKE };
213
+ if (!isCanonicalRanged) {
214
+ const pubkey = pKE.pubkey;
205
215
  //Note there exists no address for p2pk, but we can still use the script
206
216
  if (!pubkey)
207
- throw new Error(`Error: could not extract a pubkey from ${loggedExpression}`);
217
+ throw new Error(`Error: could not extract a pubkey from ${expression}`);
208
218
  payment = p2pk({ pubkey, network });
209
219
  }
210
220
  }
211
221
  //pkh(KEY) - legacy
212
- else if (expression.match(RE.rePkhAnchored)) {
222
+ else if (canonicalExpression.match(RE.rePkhAnchored)) {
213
223
  isSegwit = false;
214
- const keyExpression = expression.match(RE.reKeyExp)?.[0];
224
+ const keyExpression = canonicalExpression.match(RE.reKeyExp)?.[0];
215
225
  if (!keyExpression)
216
226
  throw new Error(`Error: keyExpression could not me extracted`);
217
- if (expression !== `pkh(${keyExpression})`)
218
- throw new Error(`Error: invalid expression ${loggedExpression}`);
227
+ if (canonicalExpression !== `pkh(${keyExpression})`)
228
+ throw new Error(`Error: invalid expression ${expression}`);
219
229
  expandedExpression = 'pkh(@0)';
220
- expansionMap = {
221
- '@0': parseKeyExpression({ keyExpression, network, isSegwit })
222
- };
223
- if (!isRanged) {
224
- const pubkey = expansionMap['@0'].pubkey;
230
+ const pKE = parseKeyExpression({ keyExpression, network, isSegwit });
231
+ expansionMap = { '@0': pKE };
232
+ if (!isCanonicalRanged) {
233
+ const pubkey = pKE.pubkey;
225
234
  if (!pubkey)
226
- throw new Error(`Error: could not extract a pubkey from ${loggedExpression}`);
235
+ throw new Error(`Error: could not extract a pubkey from ${expression}`);
227
236
  payment = p2pkh({ pubkey, network });
228
237
  }
229
238
  }
230
239
  //sh(wpkh(KEY)) - nested segwit
231
- else if (expression.match(RE.reShWpkhAnchored)) {
240
+ else if (canonicalExpression.match(RE.reShWpkhAnchored)) {
232
241
  isSegwit = true;
233
- const keyExpression = expression.match(RE.reKeyExp)?.[0];
242
+ const keyExpression = canonicalExpression.match(RE.reKeyExp)?.[0];
234
243
  if (!keyExpression)
235
244
  throw new Error(`Error: keyExpression could not me extracted`);
236
- if (expression !== `sh(wpkh(${keyExpression}))`)
237
- throw new Error(`Error: invalid expression ${loggedExpression}`);
245
+ if (canonicalExpression !== `sh(wpkh(${keyExpression}))`)
246
+ throw new Error(`Error: invalid expression ${expression}`);
238
247
  expandedExpression = 'sh(wpkh(@0))';
239
- expansionMap = {
240
- '@0': parseKeyExpression({ keyExpression, network, isSegwit })
241
- };
242
- if (!isRanged) {
243
- const pubkey = expansionMap['@0'].pubkey;
248
+ const pKE = parseKeyExpression({ keyExpression, network, isSegwit });
249
+ expansionMap = { '@0': pKE };
250
+ if (!isCanonicalRanged) {
251
+ const pubkey = pKE.pubkey;
244
252
  if (!pubkey)
245
- throw new Error(`Error: could not extract a pubkey from ${loggedExpression}`);
253
+ throw new Error(`Error: could not extract a pubkey from ${expression}`);
246
254
  payment = p2sh({ redeem: p2wpkh({ pubkey, network }), network });
247
255
  redeemScript = payment.redeem?.output;
248
256
  if (!redeemScript)
249
- throw new Error(`Error: could not calculate redeemScript for ${loggedExpression}`);
257
+ throw new Error(`Error: could not calculate redeemScript for ${expression}`);
250
258
  }
251
259
  }
252
260
  //wpkh(KEY) - native segwit
253
- else if (expression.match(RE.reWpkhAnchored)) {
261
+ else if (canonicalExpression.match(RE.reWpkhAnchored)) {
254
262
  isSegwit = true;
255
- const keyExpression = expression.match(RE.reKeyExp)?.[0];
263
+ const keyExpression = canonicalExpression.match(RE.reKeyExp)?.[0];
256
264
  if (!keyExpression)
257
265
  throw new Error(`Error: keyExpression could not me extracted`);
258
- if (expression !== `wpkh(${keyExpression})`)
259
- throw new Error(`Error: invalid expression ${loggedExpression}`);
266
+ if (canonicalExpression !== `wpkh(${keyExpression})`)
267
+ throw new Error(`Error: invalid expression ${expression}`);
260
268
  expandedExpression = 'wpkh(@0)';
261
- expansionMap = {
262
- '@0': parseKeyExpression({ keyExpression, network, isSegwit })
263
- };
264
- if (!isRanged) {
265
- const pubkey = expansionMap['@0'].pubkey;
269
+ const pKE = parseKeyExpression({ keyExpression, network, isSegwit });
270
+ expansionMap = { '@0': pKE };
271
+ if (!isCanonicalRanged) {
272
+ const pubkey = pKE.pubkey;
266
273
  if (!pubkey)
267
- throw new Error(`Error: could not extract a pubkey from ${loggedExpression}`);
274
+ throw new Error(`Error: could not extract a pubkey from ${expression}`);
268
275
  payment = p2wpkh({ pubkey, network });
269
276
  }
270
277
  }
271
278
  //sh(wsh(miniscript))
272
- else if (expression.match(RE.reShWshMiniscriptAnchored)) {
279
+ else if (canonicalExpression.match(RE.reShWshMiniscriptAnchored)) {
273
280
  isSegwit = true;
274
- miniscript = expression.match(RE.reShWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(wsh(->HERE<-))
281
+ miniscript = canonicalExpression.match(RE.reShWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(wsh(->HERE<-))
275
282
  if (!miniscript)
276
- throw new Error(`Error: could not get miniscript in ${loggedExpression}`);
283
+ throw new Error(`Error: could not get miniscript in ${expression}`);
277
284
  ({ expandedMiniscript, expansionMap } = expandMiniscript({
278
285
  miniscript,
279
286
  isSegwit,
280
287
  network
281
288
  }));
282
289
  expandedExpression = `sh(wsh(${expandedMiniscript}))`;
283
- if (!isRanged) {
290
+ if (!isCanonicalRanged) {
284
291
  const script = (0, miniscript_1.miniscript2Script)({ expandedMiniscript, expansionMap });
285
292
  witnessScript = script;
286
293
  if (script.byteLength > MAX_STANDARD_P2WSH_SCRIPT_SIZE) {
@@ -296,17 +303,17 @@ function DescriptorsFactory(ecc) {
296
303
  });
297
304
  redeemScript = payment.redeem?.output;
298
305
  if (!redeemScript)
299
- throw new Error(`Error: could not calculate redeemScript for ${loggedExpression}`);
306
+ throw new Error(`Error: could not calculate redeemScript for ${expression}`);
300
307
  }
301
308
  }
302
309
  //sh(miniscript)
303
- else if (expression.match(RE.reShMiniscriptAnchored)) {
310
+ else if (canonicalExpression.match(RE.reShMiniscriptAnchored)) {
304
311
  //isSegwit false because we know it's a P2SH of a miniscript and not a
305
312
  //P2SH that embeds a witness payment.
306
313
  isSegwit = false;
307
- miniscript = expression.match(RE.reShMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(->HERE<-)
314
+ miniscript = canonicalExpression.match(RE.reShMiniscriptAnchored)?.[1]; //[1]-> whatever is found sh(->HERE<-)
308
315
  if (!miniscript)
309
- throw new Error(`Error: could not get miniscript in ${loggedExpression}`);
316
+ throw new Error(`Error: could not get miniscript in ${expression}`);
310
317
  if (allowMiniscriptInP2SH === false &&
311
318
  //These top-level expressions within sh are allowed within sh.
312
319
  //They can be parsed with miniscript2Script, but first we must make sure
@@ -320,7 +327,7 @@ function DescriptorsFactory(ecc) {
320
327
  network
321
328
  }));
322
329
  expandedExpression = `sh(${expandedMiniscript})`;
323
- if (!isRanged) {
330
+ if (!isCanonicalRanged) {
324
331
  const script = (0, miniscript_1.miniscript2Script)({ expandedMiniscript, expansionMap });
325
332
  redeemScript = script;
326
333
  if (script.byteLength > MAX_SCRIPT_ELEMENT_SIZE) {
@@ -334,18 +341,18 @@ function DescriptorsFactory(ecc) {
334
341
  }
335
342
  }
336
343
  //wsh(miniscript)
337
- else if (expression.match(RE.reWshMiniscriptAnchored)) {
344
+ else if (canonicalExpression.match(RE.reWshMiniscriptAnchored)) {
338
345
  isSegwit = true;
339
- miniscript = expression.match(RE.reWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found wsh(->HERE<-)
346
+ miniscript = canonicalExpression.match(RE.reWshMiniscriptAnchored)?.[1]; //[1]-> whatever is found wsh(->HERE<-)
340
347
  if (!miniscript)
341
- throw new Error(`Error: could not get miniscript in ${loggedExpression}`);
348
+ throw new Error(`Error: could not get miniscript in ${expression}`);
342
349
  ({ expandedMiniscript, expansionMap } = expandMiniscript({
343
350
  miniscript,
344
351
  isSegwit,
345
352
  network
346
353
  }));
347
354
  expandedExpression = `wsh(${expandedMiniscript})`;
348
- if (!isRanged) {
355
+ if (!isCanonicalRanged) {
349
356
  const script = (0, miniscript_1.miniscript2Script)({ expandedMiniscript, expansionMap });
350
357
  witnessScript = script;
351
358
  if (script.byteLength > MAX_STANDARD_P2WSH_SCRIPT_SIZE) {
@@ -359,7 +366,7 @@ function DescriptorsFactory(ecc) {
359
366
  }
360
367
  }
361
368
  else {
362
- throw new Error(`Error: Could not parse descriptor ${loggedExpression}`);
369
+ throw new Error(`Error: Could not parse descriptor ${expression}`);
363
370
  }
364
371
  return {
365
372
  ...(payment !== undefined ? { payment } : {}),
@@ -369,7 +376,9 @@ function DescriptorsFactory(ecc) {
369
376
  ...(isSegwit !== undefined ? { isSegwit } : {}),
370
377
  ...(expandedMiniscript !== undefined ? { expandedMiniscript } : {}),
371
378
  ...(redeemScript !== undefined ? { redeemScript } : {}),
372
- ...(witnessScript !== undefined ? { witnessScript } : {})
379
+ ...(witnessScript !== undefined ? { witnessScript } : {}),
380
+ isRanged,
381
+ canonicalExpression
373
382
  };
374
383
  };
375
384
  /**
@@ -420,19 +429,15 @@ function DescriptorsFactory(ecc) {
420
429
  __classPrivateFieldSet(this, _Descriptor_preimages, preimages, "f");
421
430
  if (typeof expression !== 'string')
422
431
  throw new Error(`Error: invalid descriptor type`);
423
- //Verify and remove checksum (if exists) and
424
- //particularize range descriptor for index (if desc is range descriptor)
425
- const evaluatedExpression = evaluate({
432
+ const expandedResult = expand({
426
433
  expression,
427
434
  ...(index !== undefined ? { index } : {}),
428
- checksumRequired
429
- });
430
- const expandedResult = expand({
431
- expression: evaluatedExpression,
432
- loggedExpression: expression,
435
+ checksumRequired,
433
436
  network,
434
437
  allowMiniscriptInP2SH
435
438
  });
439
+ if (expandedResult.isRanged && index === undefined)
440
+ throw new Error(`Error: index was not provided for ranged descriptor`);
436
441
  if (!expandedResult.payment)
437
442
  throw new Error(`Error: could not extract a payment from ${expression}`);
438
443
  __classPrivateFieldSet(this, _Descriptor_payment, expandedResult.payment, "f");
@@ -464,7 +469,7 @@ function DescriptorsFactory(ecc) {
464
469
  }
465
470
  else {
466
471
  //We should only miss expansionMap in addr() expressions:
467
- if (!evaluatedExpression.match(RE.reAddrAnchored)) {
472
+ if (!expandedResult.canonicalExpression.match(RE.reAddrAnchored)) {
468
473
  throw new Error(`Error: expansionMap not available for expression ${expression} that is not an address`);
469
474
  }
470
475
  __classPrivateFieldSet(this, _Descriptor_signersPubKeys, [this.getScriptPubKey()], "f");
package/dist/types.d.ts CHANGED
@@ -47,7 +47,8 @@ export interface ParseKeyExpression {
47
47
  export interface Expand {
48
48
  (params: {
49
49
  expression: string;
50
- loggedExpression?: string;
50
+ index?: number;
51
+ checksumRequired?: boolean;
51
52
  network?: Network;
52
53
  allowMiniscriptInP2SH?: boolean;
53
54
  }): {
@@ -59,6 +60,8 @@ export interface Expand {
59
60
  expandedMiniscript?: string;
60
61
  redeemScript?: Buffer;
61
62
  witnessScript?: Buffer;
63
+ isRanged: boolean;
64
+ canonicalExpression: string;
62
65
  };
63
66
  }
64
67
  interface XOnlyPointAddTweakResult {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bitcoinerlab/descriptors",
3
3
  "homepage": "https://github.com/bitcoinerlab/descriptors",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -64,8 +64,8 @@
64
64
  "devDependencies": {
65
65
  "@babel/plugin-transform-modules-commonjs": "^7.20.11",
66
66
  "@ledgerhq/hw-transport-node-hid": "^6.27.12",
67
- "@typescript-eslint/eslint-plugin": "^5.53.0",
68
- "@typescript-eslint/parser": "^5.53.0",
67
+ "@typescript-eslint/eslint-plugin": "^5.60.1",
68
+ "@typescript-eslint/parser": "^5.60.1",
69
69
  "babel-plugin-transform-import-meta": "^2.2.0",
70
70
  "better-docs": "^2.7.2",
71
71
  "bip39": "^3.0.4",
@@ -80,6 +80,7 @@
80
80
  "path": "^0.12.7",
81
81
  "prettier": "^2.8.8",
82
82
  "regtest-client": "^0.2.0",
83
- "ts-node-dev": "^2.0.0"
83
+ "ts-node-dev": "^2.0.0",
84
+ "typescript": "5.0"
84
85
  }
85
86
  }