@bitcoinerlab/descriptors-core 3.1.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.
Files changed (76) hide show
  1. package/README.md +710 -0
  2. package/dist/adapters/applyPR2137.d.ts +2 -0
  3. package/dist/adapters/applyPR2137.js +150 -0
  4. package/dist/adapters/bitcoinjs.d.ts +8 -0
  5. package/dist/adapters/bitcoinjs.js +36 -0
  6. package/dist/adapters/scure/address.d.ts +2 -0
  7. package/dist/adapters/scure/address.js +50 -0
  8. package/dist/adapters/scure/bip32.d.ts +2 -0
  9. package/dist/adapters/scure/bip32.js +16 -0
  10. package/dist/adapters/scure/common.d.ts +14 -0
  11. package/dist/adapters/scure/common.js +36 -0
  12. package/dist/adapters/scure/ecpair.d.ts +2 -0
  13. package/dist/adapters/scure/ecpair.js +58 -0
  14. package/dist/adapters/scure/payments.d.ts +2 -0
  15. package/dist/adapters/scure/payments.js +216 -0
  16. package/dist/adapters/scure/psbt.d.ts +43 -0
  17. package/dist/adapters/scure/psbt.js +382 -0
  18. package/dist/adapters/scure/script.d.ts +20 -0
  19. package/dist/adapters/scure/script.js +163 -0
  20. package/dist/adapters/scure/transaction.d.ts +2 -0
  21. package/dist/adapters/scure/transaction.js +32 -0
  22. package/dist/adapters/scure.d.ts +6 -0
  23. package/dist/adapters/scure.js +37 -0
  24. package/dist/adapters/scureKeys.d.ts +4 -0
  25. package/dist/adapters/scureKeys.js +135 -0
  26. package/dist/bip174.d.ts +87 -0
  27. package/dist/bip174.js +12 -0
  28. package/dist/bitcoinLib.d.ts +385 -0
  29. package/dist/bitcoinLib.js +19 -0
  30. package/dist/bitcoinjs-lib-internals.d.ts +6 -0
  31. package/dist/bitcoinjs-lib-internals.js +60 -0
  32. package/dist/bitcoinjs.d.ts +12 -0
  33. package/dist/bitcoinjs.js +18 -0
  34. package/dist/checksum.d.ts +6 -0
  35. package/dist/checksum.js +58 -0
  36. package/dist/crypto.d.ts +3 -0
  37. package/dist/crypto.js +79 -0
  38. package/dist/descriptors.d.ts +481 -0
  39. package/dist/descriptors.js +1888 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.js +87 -0
  42. package/dist/keyExpressions.d.ts +124 -0
  43. package/dist/keyExpressions.js +310 -0
  44. package/dist/keyInterfaces.d.ts +5 -0
  45. package/dist/keyInterfaces.js +50 -0
  46. package/dist/ledger.d.ts +183 -0
  47. package/dist/ledger.js +618 -0
  48. package/dist/miniscript.d.ts +125 -0
  49. package/dist/miniscript.js +310 -0
  50. package/dist/multipath.d.ts +13 -0
  51. package/dist/multipath.js +76 -0
  52. package/dist/networkUtils.d.ts +3 -0
  53. package/dist/networkUtils.js +16 -0
  54. package/dist/networks.d.ts +16 -0
  55. package/dist/networks.js +31 -0
  56. package/dist/parseUtils.d.ts +7 -0
  57. package/dist/parseUtils.js +46 -0
  58. package/dist/psbt.d.ts +40 -0
  59. package/dist/psbt.js +228 -0
  60. package/dist/re.d.ts +31 -0
  61. package/dist/re.js +79 -0
  62. package/dist/resourceLimits.d.ts +28 -0
  63. package/dist/resourceLimits.js +84 -0
  64. package/dist/scriptExpressions.d.ts +95 -0
  65. package/dist/scriptExpressions.js +98 -0
  66. package/dist/scure.d.ts +4 -0
  67. package/dist/scure.js +10 -0
  68. package/dist/signers.d.ts +161 -0
  69. package/dist/signers.js +324 -0
  70. package/dist/tapMiniscript.d.ts +231 -0
  71. package/dist/tapMiniscript.js +524 -0
  72. package/dist/tapTree.d.ts +91 -0
  73. package/dist/tapTree.js +166 -0
  74. package/dist/types.d.ts +296 -0
  75. package/dist/types.js +4 -0
  76. package/package.json +148 -0
package/dist/ledger.js ADDED
@@ -0,0 +1,618 @@
1
+ "use strict";
2
+ // Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
3
+ // Distributed under the MIT software license
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.importAndValidateLedgerBitcoin = importAndValidateLedgerBitcoin;
6
+ exports.assertLedgerApp = assertLedgerApp;
7
+ exports.getLedgerMasterFingerPrint = getLedgerMasterFingerPrint;
8
+ exports.getLedgerXpub = getLedgerXpub;
9
+ exports.ledgerPolicyFromPsbtInput = ledgerPolicyFromPsbtInput;
10
+ exports.ledgerPolicyFromOutput = ledgerPolicyFromOutput;
11
+ exports.registerLedgerWallet = registerLedgerWallet;
12
+ exports.ledgerPolicyFromStandard = ledgerPolicyFromStandard;
13
+ exports.comparePolicies = comparePolicies;
14
+ exports.ledgerPolicyFromState = ledgerPolicyFromState;
15
+ /*
16
+ * Notes on Ledger implementation:
17
+ *
18
+ * Ledger assumes as external all keyRoots that do not have origin information.
19
+ *
20
+ * Some known Ledger Limitations (based on my tests as of Febr 2023):
21
+ *
22
+ * 1) All keyExpressions must be expanded into @i. In other words,
23
+ * this template is not valid:
24
+ * wsh(and_v(v:pk(03ed0b41d808b012b3a77dd7f6a30c4180dfbcab604133d90ce7593ec7f3e4037b),and_v(v:sha256(6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333),and_v(and_v(v:pk(@0/**),v:pk(@1/**)),older(5)))))
25
+ * (note the fixed 03ed0b41d808b012b3a77dd7f6a30c4180dfbcab604133d90ce7593ec7f3e4037b pubkey)
26
+ *
27
+ * 2) All elements in the keyRoot vector must be xpub-type (no xprv-type, no pubkey-type, ...)
28
+ *
29
+ * 3) All originPaths of the expressions in the keyRoot vector must be the same.
30
+ * On the other hand, an empty originPath is permitted for external keys.
31
+ *
32
+ * 4) Since all originPaths must be the same and originPaths for the Ledger are
33
+ * necessary, a Ledger device can only sign at most 1 key per policy and input.
34
+ *
35
+ * All the conditions above are checked in function ledgerPolicyFromOutput.
36
+ */
37
+ const descriptors_1 = require("./descriptors");
38
+ const psbt_1 = require("./psbt");
39
+ const uint8array_tools_1 = require("uint8array-tools");
40
+ const networks_1 = require("./networks");
41
+ const networkUtils_1 = require("./networkUtils");
42
+ const re_1 = require("./re");
43
+ const keyInterfaces_1 = require("./keyInterfaces");
44
+ /**
45
+ * Dynamically imports the '@ledgerhq/ledger-bitcoin' module and, if provided, checks if `ledgerClient` is an instance of `AppClient`.
46
+ *
47
+ * @async
48
+ * @param {unknown} ledgerClient - An optional parameter that, if provided, is checked to see if it's an instance of `AppClient`.
49
+ * @throws {Error} Throws an error if `ledgerClient` is provided but is not an instance of `AppClient`.
50
+ * @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.
51
+ * @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
52
+ *
53
+ * @example
54
+ *
55
+ * importAndValidateLedgerBitcoin(ledgerClient)
56
+ * .then((module) => {
57
+ * const { AppClient, PsbtV2, DefaultWalletPolicy, WalletPolicy, DefaultDescriptorTemplate, PartialSignature } = module;
58
+ * // Use the imported objects...
59
+ * })
60
+ * .catch((error) => console.error(error));
61
+ */
62
+ async function importAndValidateLedgerBitcoin(ledgerClient) {
63
+ let ledgerBitcoinModule;
64
+ try {
65
+ // Originally, the code used dynamic imports:
66
+ // ledgerBitcoinModule = await import('@ledgerhq/ledger-bitcoin');
67
+ // However, in React Native with the Metro bundler, there's an issue with
68
+ // recognizing dynamic imports inside try-catch blocks. For details, refer to:
69
+ // https://github.com/react-native-community/discussions-and-proposals/issues/120
70
+ // The dynamic import gets transpiled to:
71
+ // ledgerBitcoinModule = Promise.resolve().then(() => __importStar(require('@ledgerhq/ledger-bitcoin')));
72
+ // Metro bundler fails to recognize the above as conditional. Hence, it tries
73
+ // to require '@ledgerhq/ledger-bitcoin' unconditionally, leading to potential errors if
74
+ // '@ledgerhq/ledger-bitcoin' is not installed (given it's an optional peerDependency).
75
+ // To bypass this, we directly use require:
76
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
77
+ ledgerBitcoinModule = require('@ledgerhq/ledger-bitcoin');
78
+ }
79
+ catch (error) {
80
+ void error;
81
+ 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.');
82
+ }
83
+ const { AppClient } = ledgerBitcoinModule;
84
+ if (ledgerClient !== undefined && !(ledgerClient instanceof AppClient)) {
85
+ throw new Error('Error: invalid AppClient instance');
86
+ }
87
+ return ledgerBitcoinModule;
88
+ }
89
+ /**
90
+ *
91
+ * bitcoinjs-lib is a peerDependency. However, it is a dependency for
92
+ * @ledgerhq/ledger-bitcoin anyway, so it is safe to assume that it will be
93
+ * installed if @ledgerhq/ledger-bitcoin is also installed.
94
+ *
95
+ */
96
+ function requireBitcoinjsTransaction() {
97
+ let bitcoinjsModule;
98
+ try {
99
+ // Read comments above (importAndValidateLedgerBitcoin)
100
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
101
+ bitcoinjsModule = require('bitcoinjs-lib');
102
+ }
103
+ catch (error) {
104
+ void error;
105
+ throw new Error('Could not import "bitcoinjs-lib". Ledger signing requires this peer dependency to parse nonWitnessUtxo transactions. Please run "npm install bitcoinjs-lib" to use Ledger Hardware Wallet functionality.');
106
+ }
107
+ const { Transaction } = bitcoinjsModule;
108
+ if (!Transaction || typeof Transaction.fromBuffer !== 'function') {
109
+ throw new Error('Error: invalid bitcoinjs-lib Transaction export');
110
+ }
111
+ return Transaction;
112
+ }
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ async function ledgerAppInfo(transport) {
115
+ const r = await transport.send(0xb0, 0x01, 0x00, 0x00);
116
+ let i = 0;
117
+ const format = r[i++];
118
+ const nameLength = r[i++];
119
+ const name = r.slice(i, (i += nameLength)).toString('ascii');
120
+ const versionLength = r[i++];
121
+ const version = r.slice(i, (i += versionLength)).toString('ascii');
122
+ const flagLength = r[i++];
123
+ const flags = r.slice(i, (i += flagLength));
124
+ return { name, version, flags, format };
125
+ }
126
+ /**
127
+ * Verifies if the Ledger device is connected, if the required Bitcoin App is opened,
128
+ * and if the version of the app meets the minimum requirements.
129
+ *
130
+ * @throws Will throw an error if the Ledger device is not connected, the required
131
+ * Bitcoin App is not opened, or if the version is below the required number.
132
+ *
133
+ * @returns Promise<void> - A promise that resolves if all assertions pass, or throws otherwise.
134
+ */
135
+ async function assertLedgerApp({ transport, name, minVersion }) {
136
+ const { name: openName, version } = await ledgerAppInfo(transport);
137
+ if (openName !== name) {
138
+ throw new Error(`Open the ${name} app and try again`);
139
+ }
140
+ else {
141
+ const [mVmajor, mVminor, mVpatch] = minVersion.split('.').map(Number);
142
+ const [major, minor, patch] = version.split('.').map(Number);
143
+ if (mVmajor === undefined ||
144
+ mVminor === undefined ||
145
+ mVpatch === undefined) {
146
+ throw new Error(`Pass a minVersion using semver notation: major.minor.patch`);
147
+ }
148
+ if (major < mVmajor ||
149
+ (major === mVmajor && minor < mVminor) ||
150
+ (major === mVmajor && minor === mVminor && patch < mVpatch))
151
+ throw new Error(`Error: please upgrade ${name} to version ${minVersion}`);
152
+ }
153
+ }
154
+ function isLedgerStandard({ ledgerTemplate, keyRoots, network = networks_1.networks.bitcoin }) {
155
+ if (keyRoots.length !== 1)
156
+ return false;
157
+ const originPath = keyRoots[0]?.match(re_1.reOriginPath)?.[1];
158
+ if (!originPath)
159
+ return false;
160
+ const originCoinType = originPath.match(/^\/\d+'\/([01])'/)?.[1];
161
+ if (!originCoinType)
162
+ return false;
163
+ if (originCoinType !== `${(0, networkUtils_1.coinTypeFromNetwork)(network)}`)
164
+ return false;
165
+ if ((ledgerTemplate === 'pkh(@0/**)' &&
166
+ originPath.match(/^\/44'\/[01]'\/(\d+)'$/)) ||
167
+ (ledgerTemplate === 'wpkh(@0/**)' &&
168
+ originPath.match(/^\/84'\/[01]'\/(\d+)'$/)) ||
169
+ (ledgerTemplate === 'sh(wpkh(@0/**))' &&
170
+ originPath.match(/^\/49'\/[01]'\/(\d+)'$/)) ||
171
+ (ledgerTemplate === 'tr(@0/**)' &&
172
+ originPath.match(/^\/86'\/[01]'\/(\d+)'$/)))
173
+ return true;
174
+ return false;
175
+ }
176
+ function getLedgerOutputConstructor(ledgerManager) {
177
+ if (ledgerManager.Output)
178
+ return ledgerManager.Output;
179
+ if (ledgerManager.ecc)
180
+ return (0, descriptors_1.DescriptorsFactory)(ledgerManager.ecc).Output;
181
+ throw new Error('Error: pass ledgerManager.Output. Legacy ledgerManager.ecc is deprecated.');
182
+ }
183
+ async function getLedgerMasterFingerPrint({ ledgerManager }) {
184
+ const { ledgerClient, ledgerState } = ledgerManager;
185
+ const { AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
186
+ if (!(ledgerClient instanceof AppClient))
187
+ throw new Error(`Error: pass a valid ledgerClient`);
188
+ let masterFingerprint = ledgerState.masterFingerprint;
189
+ if (!masterFingerprint) {
190
+ masterFingerprint = (0, uint8array_tools_1.fromHex)(await ledgerClient.getMasterFingerprint());
191
+ ledgerState.masterFingerprint = masterFingerprint;
192
+ }
193
+ return masterFingerprint;
194
+ }
195
+ async function getLedgerXpub({ originPath, ledgerManager }) {
196
+ const { ledgerClient, ledgerState } = ledgerManager;
197
+ const { AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
198
+ if (!(ledgerClient instanceof AppClient))
199
+ throw new Error(`Error: pass a valid ledgerClient`);
200
+ if (!ledgerState.xpubs)
201
+ ledgerState.xpubs = {};
202
+ let xpub = ledgerState.xpubs[originPath];
203
+ if (!xpub) {
204
+ try {
205
+ //Try getting the xpub without user confirmation
206
+ xpub = await ledgerClient.getExtendedPubkey(`m${originPath}`, false);
207
+ }
208
+ catch (err) {
209
+ void err;
210
+ xpub = await ledgerClient.getExtendedPubkey(`m${originPath}`, true);
211
+ }
212
+ if (typeof xpub !== 'string')
213
+ throw new Error(`Error: ledgerClient did not return a valid xpub`);
214
+ ledgerState.xpubs[originPath] = xpub;
215
+ }
216
+ return xpub;
217
+ }
218
+ /**
219
+ * Checks whether there is a policy in ledgerState that the ledger
220
+ * could use to sign this psbt input.
221
+ *
222
+ * It found return the policy, otherwise, return undefined
223
+ *
224
+ * All considerations in the header of this file are applied
225
+ */
226
+ async function ledgerPolicyFromPsbtInput({ ledgerManager, psbt, index }) {
227
+ psbt = (0, psbt_1.toPsbt)(psbt);
228
+ const { ledgerState, network } = ledgerManager;
229
+ const Transaction = requireBitcoinjsTransaction();
230
+ const Output = getLedgerOutputConstructor(ledgerManager);
231
+ const input = psbt.data.inputs[index];
232
+ if (!input)
233
+ throw new Error(`Error: input ${index} not available`);
234
+ let scriptPubKey;
235
+ if (input.nonWitnessUtxo) {
236
+ const txInput = psbt.txInputs[index];
237
+ if (!txInput)
238
+ throw new Error(`Error: tx input ${index} not available`);
239
+ const vout = txInput.index;
240
+ const nonWitnessScript = Transaction.fromBuffer(input.nonWitnessUtxo).outs[vout]?.script;
241
+ scriptPubKey = nonWitnessScript;
242
+ }
243
+ else if (input.witnessUtxo) {
244
+ scriptPubKey = input.witnessUtxo.script;
245
+ }
246
+ if (!scriptPubKey)
247
+ throw new Error(`Could not retrieve scriptPubKey for input ${index}.`);
248
+ const keyDerivations = [
249
+ ...(input.bip32Derivation || []),
250
+ ...(input.tapBip32Derivation || [])
251
+ ];
252
+ if (keyDerivations.length === 0)
253
+ throw new Error(`Input ${index} does not contain bip32 or tapBip32 derivations.`);
254
+ const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
255
+ ledgerManager
256
+ });
257
+ for (const keyDerivation of keyDerivations) {
258
+ //get the keyRoot and keyPath. If it matches one of our policies then
259
+ //we are still not sure this is the policy that must be used yet
260
+ //So we must use the template and the keyRoot of each policy and compute the
261
+ //scriptPubKey:
262
+ if ((0, uint8array_tools_1.compare)(keyDerivation.masterFingerprint, ledgerMasterFingerprint) === 0) {
263
+ // Match /m followed by n consecutive hardened levels and then 2 consecutive unhardened levels:
264
+ const match = keyDerivation.path.match(/m((\/\d+['hH])*)(\/\d+\/\d+)?/);
265
+ const originPath = match ? match[1] : undefined; //n consecutive hardened levels
266
+ const keyPath = match ? match[3] : undefined; //2 unhardened levels or undefined
267
+ if (originPath && keyPath) {
268
+ const [, strChange, strIndex] = keyPath.split('/');
269
+ if (!strChange || !strIndex)
270
+ throw new Error(`keyPath ${keyPath} incorrectly extracted`);
271
+ const change = parseInt(strChange, 10);
272
+ const index = parseInt(strIndex, 10);
273
+ const coinType = (0, networkUtils_1.coinTypeFromNetwork)(network);
274
+ //standard policy candidate. This policy will be added to the pool
275
+ //of policies below and check if it produces the correct scriptPubKey
276
+ let standardPolicy;
277
+ if (change === 0 || change === 1) {
278
+ const standardTemplate = originPath.match(new RegExp(`^/44'/${coinType}'/(\\d+)'$`))
279
+ ? 'pkh(@0/**)'
280
+ : originPath.match(new RegExp(`^/84'/${coinType}'/(\\d+)'$`))
281
+ ? 'wpkh(@0/**)'
282
+ : originPath.match(new RegExp(`^/49'/${coinType}'/(\\d+)'$`))
283
+ ? 'sh(wpkh(@0/**))'
284
+ : originPath.match(new RegExp(`^/86'/${coinType}'/(\\d+)'$`))
285
+ ? 'tr(@0/**)'
286
+ : undefined;
287
+ if (standardTemplate) {
288
+ const xpub = await getLedgerXpub({ originPath, ledgerManager });
289
+ standardPolicy = {
290
+ ledgerTemplate: standardTemplate,
291
+ keyRoots: [
292
+ `[${(0, uint8array_tools_1.toHex)(ledgerMasterFingerprint)}${originPath}]${xpub}`
293
+ ]
294
+ };
295
+ }
296
+ }
297
+ const policies = [...(ledgerState.policies || [])];
298
+ if (standardPolicy)
299
+ policies.push(standardPolicy);
300
+ for (const policy of policies) {
301
+ //Build the descriptor from the ledgerTemplate + keyRoots
302
+ //then get the scriptPubKey
303
+ let descriptor = policy.ledgerTemplate;
304
+ // Replace change (making sure the value in the change level for the
305
+ // template of the policy meets the change in bip32Derivation):
306
+ descriptor = descriptor.replace(/\/\*\*/g, `/<0;1>/*`);
307
+ let tupleMismatch = false;
308
+ descriptor = descriptor.replace(/\/<(\d+);(\d+)>/g, (token, strM, strN) => {
309
+ const [M, N] = [parseInt(strM, 10), parseInt(strN, 10)];
310
+ if (M === change || N === change)
311
+ return `/${change}`;
312
+ tupleMismatch = true;
313
+ return token;
314
+ });
315
+ if (tupleMismatch)
316
+ descriptor = undefined;
317
+ if (descriptor) {
318
+ // Replace index:
319
+ descriptor = descriptor.replace(/\/\*/g, `/${index}`);
320
+ // Replace origin in reverse order to prevent
321
+ // misreplacements, e.g., @10 being mistaken for @1 and leaving a 0.
322
+ for (let i = policy.keyRoots.length - 1; i >= 0; i--) {
323
+ const keyRoot = policy.keyRoots[i];
324
+ if (!keyRoot)
325
+ throw new Error(`keyRoot ${keyRoot} invalidly extracted.`);
326
+ const match = keyRoot.match(/\[([^]+)\]/);
327
+ const keyRootOrigin = match && match[1];
328
+ if (keyRootOrigin) {
329
+ const [, ...arrKeyRootOriginPath] = keyRootOrigin.split('/');
330
+ const keyRootOriginPath = '/' + arrKeyRootOriginPath.join('/');
331
+ //We check all origins to be the same even if they do not
332
+ //belong to the ledger (read the header in this file)
333
+ if (descriptor && keyRootOriginPath === originPath)
334
+ descriptor = descriptor.replace(new RegExp(`@${i}`, 'g'), keyRoot);
335
+ else
336
+ descriptor = undefined;
337
+ }
338
+ else {
339
+ // Keys without origin info are treated as external by Ledger
340
+ // and are allowed in policy matching.
341
+ if (descriptor)
342
+ descriptor = descriptor.replace(new RegExp(`@${i}`, 'g'), keyRoot);
343
+ }
344
+ }
345
+ //verify the scriptPubKey from the input vs. the one obtained from
346
+ //the policy after having filled in the keyPath in the template
347
+ if (descriptor) {
348
+ const policyScriptPubKey = new Output({
349
+ descriptor,
350
+ network
351
+ }).getScriptPubKey();
352
+ if ((0, uint8array_tools_1.compare)(policyScriptPubKey, scriptPubKey) === 0) {
353
+ return policy;
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ return;
362
+ }
363
+ /**
364
+ * Given an output, it extracts its descriptor and converts it to a Ledger
365
+ * Wallet Policy, that is, its keyRoots and template.
366
+ *
367
+ * keyRoots and template follow Ledger's specifications:
368
+ * https://github.com/LedgerHQ/app-bitcoin-new/blob/develop/doc/wallet.md
369
+ *
370
+ * keyRoots and template are a generalization of a descriptor and serve to
371
+ * describe internal and external addresses and any index.
372
+ *
373
+ * So, this function starts from a descriptor and obtains generalized Ledger
374
+ * wallet policy.
375
+ *
376
+ * keyRoots is an array of strings, encoding xpub-type key expressions up to the origin.
377
+ * F.ex.: [76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF
378
+ *
379
+ * Template encodes the descriptor script expression, where its key
380
+ * expressions are represented using variables for each keyRoot and finished with "/**"
381
+ * (for change 1 or 0 and any index). F.ex.:
382
+ * wsh(sortedmulti(2,@0/**,@1/**)), where @0 corresponds the first element in the keyRoots array.
383
+ *
384
+ * If this descriptor does not contain any key that can be signed with the ledger
385
+ * (non-matching masterFingerprint), then this function returns null.
386
+ *
387
+ * This function takes into account all the considerations regarding Ledger
388
+ * policy implementation details expressed in the header of this file.
389
+ */
390
+ async function ledgerPolicyFromOutput({ output, ledgerManager }) {
391
+ const expanded = output.expand();
392
+ let expandedExpression = expanded.expandedExpression;
393
+ const expansionMap = expanded.expansionMap
394
+ ? { ...expanded.expansionMap }
395
+ : undefined;
396
+ // Taproot script-path keys are expanded in tapTreeInfo leaf expansion maps,
397
+ // not in the top-level expansionMap. For ledger policy derivation we remap
398
+ // leaf-local placeholders to global placeholders and merge all leaf keys into
399
+ // the top-level expansionMap.
400
+ if (expandedExpression?.startsWith('tr(@0,') &&
401
+ expansionMap &&
402
+ expanded.tapTreeInfo) {
403
+ const keyExpressionToGlobalPlaceholder = new Map(Object.entries(expansionMap).map(([placeholder, keyInfo]) => [
404
+ keyInfo.keyExpression,
405
+ placeholder
406
+ ]));
407
+ let nextPlaceholderIndex = Object.keys(expansionMap).length;
408
+ const globalPlaceholderFor = (keyInfo) => {
409
+ const existing = keyExpressionToGlobalPlaceholder.get(keyInfo.keyExpression);
410
+ if (existing)
411
+ return existing;
412
+ const placeholder = `@${nextPlaceholderIndex}`;
413
+ nextPlaceholderIndex += 1;
414
+ keyExpressionToGlobalPlaceholder.set(keyInfo.keyExpression, placeholder);
415
+ expansionMap[placeholder] = keyInfo;
416
+ return placeholder;
417
+ };
418
+ const remapTapTree = (node) => {
419
+ if ('expression' in node) {
420
+ // Prefer descriptor-level expanded expression for policy templates so
421
+ // script expressions (e.g. sortedmulti_a) are preserved. Fall back to
422
+ // expandedMiniscript for older metadata.
423
+ let remappedMiniscript = node.expandedExpression ?? node.expandedMiniscript;
424
+ if (!remappedMiniscript)
425
+ throw new Error(`Error: taproot leaf expansion not available`);
426
+ const localEntries = Object.entries(node.expansionMap);
427
+ const localToGlobalPlaceholder = new Map();
428
+ for (const [localPlaceholder, keyInfo] of localEntries) {
429
+ const globalPlaceholder = globalPlaceholderFor(keyInfo);
430
+ localToGlobalPlaceholder.set(localPlaceholder, globalPlaceholder);
431
+ }
432
+ remappedMiniscript = remappedMiniscript.replace(/@\d+/g, placeholder => localToGlobalPlaceholder.get(placeholder) ?? placeholder);
433
+ return remappedMiniscript;
434
+ }
435
+ return `{${remapTapTree(node.left)},${remapTapTree(node.right)}}`;
436
+ };
437
+ expandedExpression = `tr(@0,${remapTapTree(expanded.tapTreeInfo)})`;
438
+ }
439
+ if (!expandedExpression || !expansionMap)
440
+ throw new Error(`Error: invalid output`);
441
+ const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
442
+ ledgerManager
443
+ });
444
+ // Keep placeholders in numeric order (@0, @1, @2, ...). This avoids
445
+ // lexicographic pitfalls like @10 being ordered before @2.
446
+ const allKeys = Object.keys(expansionMap).sort((a, b) => {
447
+ const aIndex = Number(a.slice(1));
448
+ const bIndex = Number(b.slice(1));
449
+ if (Number.isNaN(aIndex) || Number.isNaN(bIndex))
450
+ return a.localeCompare(b);
451
+ return aIndex - bIndex;
452
+ });
453
+ const ledgerKeys = allKeys.filter(key => {
454
+ const masterFingerprint = expansionMap[key]?.masterFingerprint;
455
+ return (masterFingerprint &&
456
+ (0, uint8array_tools_1.compare)(masterFingerprint, ledgerMasterFingerprint) === 0);
457
+ });
458
+ if (ledgerKeys.length === 0)
459
+ return null;
460
+ if (ledgerKeys.length > 1)
461
+ throw new Error(`Error: descriptor ${expandedExpression} does not contain exactly 1 ledger key`);
462
+ const ledgerKey = ledgerKeys[0];
463
+ const masterFingerprint = expansionMap[ledgerKey].masterFingerprint;
464
+ const originPath = expansionMap[ledgerKey].originPath;
465
+ const keyPath = expansionMap[ledgerKey].keyPath;
466
+ const bip32Like = expansionMap[ledgerKey].bip32;
467
+ const bip32 = bip32Like ? (0, keyInterfaces_1.toBIP32Interface)(bip32Like) : undefined;
468
+ if (!masterFingerprint || !originPath || !keyPath || !bip32) {
469
+ throw new Error(`Error: Ledger key expression must have a valid masterFingerprint: ${masterFingerprint}, originPath: ${originPath}, keyPath: ${keyPath} and a valid bip32 node`);
470
+ }
471
+ if (!/^\/[01]\/\d+$/.test(keyPath))
472
+ throw new Error(`Error: key paths must be /<1;0>/index, where change is 1 or 0 and index >= 0`);
473
+ const keyRoots = [];
474
+ const placeholderToLedgerPlaceholder = new Map();
475
+ allKeys.forEach((key, index) => {
476
+ if (key !== ledgerKey) {
477
+ //This block here only does data integrity assertions:
478
+ const otherKeyInfo = expansionMap[key];
479
+ if (!otherKeyInfo.bip32) {
480
+ throw new Error(`Error: ledger only allows xpub-type key expressions`);
481
+ }
482
+ if (otherKeyInfo.originPath) {
483
+ if (otherKeyInfo.originPath !== originPath) {
484
+ throw new Error(`Error: all originPaths must be the same for Ledger being able to sign. On the other hand, you can leave the origin info empty for external keys: ${otherKeyInfo.originPath} !== ${originPath}`);
485
+ }
486
+ }
487
+ if (otherKeyInfo.keyPath !== keyPath) {
488
+ throw new Error(`Error: all keyPaths must be the same for Ledger being able to sign: ${otherKeyInfo.keyPath} !== ${keyPath}`);
489
+ }
490
+ }
491
+ placeholderToLedgerPlaceholder.set(key, `@${index}/**`);
492
+ const keyInfo = expansionMap[key];
493
+ const keyBip32 = keyInfo.bip32 ? (0, keyInterfaces_1.toBIP32Interface)(keyInfo.bip32) : null;
494
+ if (keyInfo.masterFingerprint && keyInfo.originPath)
495
+ keyRoots.push(`[${(0, uint8array_tools_1.toHex)(keyInfo.masterFingerprint)}${keyInfo.originPath}]${keyBip32?.neutered().toBase58()}`);
496
+ else
497
+ keyRoots.push(`${keyBip32?.neutered().toBase58()}`);
498
+ });
499
+ const ledgerTemplate = expandedExpression.replace(/@\d+/g, placeholder => placeholderToLedgerPlaceholder.get(placeholder) ?? placeholder);
500
+ return { ledgerTemplate, keyRoots };
501
+ }
502
+ /**
503
+ * Registers a policy based on a provided descriptor.
504
+ *
505
+ * This function will:
506
+ * 1. Store the policy in `ledgerState` inside the `ledgerManager`.
507
+ * 2. Avoid re-registering if the policy was previously registered.
508
+ * 3. Skip registration if the policy is considered "standard".
509
+ *
510
+ * It's important to understand the nature of the Ledger Policy being registered:
511
+ * - While a descriptor might point to a specific output index of a particular change address,
512
+ * the corresponding Ledger Policy abstracts this and represents potential outputs for
513
+ * all addresses (both external and internal).
514
+ * - This means that the registered Ledger Policy is a generalized version of the descriptor,
515
+ * not assuming specific values for the keyPath.
516
+ */
517
+ async function registerLedgerWallet({ descriptor, ledgerManager, policyName }) {
518
+ const { ledgerClient, ledgerState, network } = ledgerManager;
519
+ const { WalletPolicy, AppClient } = (await importAndValidateLedgerBitcoin(ledgerClient));
520
+ if (!(ledgerClient instanceof AppClient))
521
+ throw new Error(`Error: pass a valid ledgerClient`);
522
+ const Output = getLedgerOutputConstructor(ledgerManager);
523
+ const output = new Output({
524
+ descriptor,
525
+ ...(descriptor.includes('*') ? { index: 0 } : {}), //if ranged set any index
526
+ network
527
+ });
528
+ if (await ledgerPolicyFromStandard({ output, ledgerManager }))
529
+ return;
530
+ const result = await ledgerPolicyFromOutput({ output, ledgerManager });
531
+ if (await ledgerPolicyFromStandard({ output, ledgerManager }))
532
+ return;
533
+ if (!result)
534
+ throw new Error(`Error: output does not have a ledger input`);
535
+ const { ledgerTemplate, keyRoots } = result;
536
+ if (!ledgerState.policies)
537
+ ledgerState.policies = [];
538
+ let walletPolicy, policyHmac;
539
+ //Search in ledgerState first
540
+ const policy = await ledgerPolicyFromState({ output, ledgerManager });
541
+ if (policy) {
542
+ if (policy.policyName !== policyName)
543
+ throw new Error(`Error: policy was already registered with a different name: ${policy.policyName}`);
544
+ //It already existed. No need to register it again.
545
+ }
546
+ else {
547
+ walletPolicy = new WalletPolicy(policyName, ledgerTemplate, keyRoots);
548
+ let policyId;
549
+ [policyId, policyHmac] = await ledgerClient.registerWallet(walletPolicy);
550
+ const policy = {
551
+ policyName,
552
+ ledgerTemplate,
553
+ keyRoots,
554
+ policyId,
555
+ policyHmac
556
+ };
557
+ ledgerState.policies.push(policy);
558
+ }
559
+ }
560
+ /**
561
+ * Retrieve a standard ledger policy or null if it does correspond.
562
+ **/
563
+ async function ledgerPolicyFromStandard({ output, ledgerManager }) {
564
+ const result = await ledgerPolicyFromOutput({
565
+ output,
566
+ ledgerManager
567
+ });
568
+ if (!result)
569
+ throw new Error(`Error: descriptor does not have a ledger input`);
570
+ const { ledgerTemplate, keyRoots } = result;
571
+ if (isLedgerStandard({
572
+ ledgerTemplate,
573
+ keyRoots,
574
+ network: output.getNetwork()
575
+ }))
576
+ return { ledgerTemplate, keyRoots };
577
+ return null;
578
+ }
579
+ function compareKeyRoots(arr1, arr2) {
580
+ if (arr1.length !== arr2.length) {
581
+ return false;
582
+ }
583
+ for (let i = 0; i < arr1.length; i++) {
584
+ if (arr1[i] !== arr2[i]) {
585
+ return false;
586
+ }
587
+ }
588
+ return true;
589
+ }
590
+ function comparePolicies(policyA, policyB) {
591
+ return (compareKeyRoots(policyA.keyRoots, policyB.keyRoots) &&
592
+ policyA.ledgerTemplate === policyB.ledgerTemplate);
593
+ }
594
+ /**
595
+ * Retrieve a ledger policy from ledgerState or null if it does not exist yet.
596
+ **/
597
+ async function ledgerPolicyFromState({ output, ledgerManager }) {
598
+ const { ledgerState } = ledgerManager;
599
+ const result = await ledgerPolicyFromOutput({
600
+ output,
601
+ ledgerManager
602
+ });
603
+ if (!result)
604
+ throw new Error(`Error: output does not have a ledger input`);
605
+ const { ledgerTemplate, keyRoots } = result;
606
+ if (!ledgerState.policies)
607
+ ledgerState.policies = [];
608
+ //Search in ledgerState:
609
+ const policies = ledgerState.policies.filter(policy => comparePolicies(policy, { ledgerTemplate, keyRoots }));
610
+ if (policies.length > 1)
611
+ throw new Error(`Error: duplicated policy`);
612
+ if (policies.length === 1) {
613
+ return policies[0];
614
+ }
615
+ else {
616
+ return null;
617
+ }
618
+ }