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