@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/README.md +9 -487
- package/index.d.ts +13 -0
- package/index.js +16 -0
- package/package.json +22 -53
- package/dist/applyPR2137.d.ts +0 -2
- package/dist/applyPR2137.js +0 -153
- package/dist/bitcoinjs-lib-internals.d.ts +0 -10
- package/dist/bitcoinjs-lib-internals.js +0 -50
- package/dist/checksum.d.ts +0 -6
- package/dist/checksum.js +0 -58
- package/dist/descriptors.d.ts +0 -433
- package/dist/descriptors.js +0 -1743
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -85
- package/dist/keyExpressions.d.ts +0 -83
- package/dist/keyExpressions.js +0 -248
- package/dist/ledger.d.ts +0 -167
- package/dist/ledger.js +0 -580
- package/dist/miniscript.d.ts +0 -123
- package/dist/miniscript.js +0 -305
- package/dist/multipath.d.ts +0 -13
- package/dist/multipath.js +0 -76
- package/dist/networkUtils.d.ts +0 -3
- package/dist/networkUtils.js +0 -16
- package/dist/parseUtils.d.ts +0 -7
- package/dist/parseUtils.js +0 -46
- package/dist/psbt.d.ts +0 -44
- package/dist/psbt.js +0 -193
- package/dist/re.d.ts +0 -31
- package/dist/re.js +0 -79
- package/dist/resourceLimits.d.ts +0 -25
- package/dist/resourceLimits.js +0 -89
- package/dist/scriptExpressions.d.ts +0 -95
- package/dist/scriptExpressions.js +0 -89
- package/dist/signers.d.ts +0 -84
- package/dist/signers.js +0 -215
- package/dist/tapMiniscript.d.ts +0 -215
- package/dist/tapMiniscript.js +0 -515
- package/dist/tapTree.d.ts +0 -86
- package/dist/tapTree.js +0 -167
- package/dist/types.d.ts +0 -238
- package/dist/types.js +0 -4
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
|
-
}
|