@bitcoinerlab/descriptors 0.2.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/index.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ // Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
3
+ // Distributed under the MIT software license
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || function (mod) {
21
+ if (mod && mod.__esModule) return mod;
22
+ var result = {};
23
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24
+ __setModuleDefault(result, mod);
25
+ return result;
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.ledger = exports.scriptExpressions = exports.keyExpressionLedger = exports.keyExpressionBIP32 = exports.finalizePsbt = exports.signers = exports.checksum = exports.DescriptorsFactory = void 0;
29
+ var types_1 = require("./types");
30
+ var descriptors_1 = require("./descriptors");
31
+ Object.defineProperty(exports, "DescriptorsFactory", { enumerable: true, get: function () { return descriptors_1.DescriptorsFactory; } });
32
+ var checksum_1 = require("./checksum");
33
+ Object.defineProperty(exports, "checksum", { enumerable: true, get: function () { return checksum_1.DescriptorChecksum; } });
34
+ const signers = __importStar(require("./signers"));
35
+ exports.signers = signers;
36
+ function finalizePsbt({ psbt, descriptors, validate = true }) {
37
+ descriptors.forEach((descriptor, inputIndex) => descriptor.finalizePsbtInput({ index: inputIndex, psbt, validate }));
38
+ }
39
+ exports.finalizePsbt = finalizePsbt;
40
+ var keyExpressions_1 = require("./keyExpressions");
41
+ Object.defineProperty(exports, "keyExpressionBIP32", { enumerable: true, get: function () { return keyExpressions_1.keyExpressionBIP32; } });
42
+ Object.defineProperty(exports, "keyExpressionLedger", { enumerable: true, get: function () { return keyExpressions_1.keyExpressionLedger; } });
43
+ const scriptExpressions = __importStar(require("./scriptExpressions"));
44
+ exports.scriptExpressions = scriptExpressions;
45
+ const ledger_1 = require("@bitcoinerlab/ledger");
46
+ const ledger_2 = require("./ledger");
47
+ exports.ledger = {
48
+ getLedgerMasterFingerPrint: ledger_2.getLedgerMasterFingerPrint,
49
+ getLedgerXpub: ledger_2.getLedgerXpub,
50
+ registerLedgerWallet: ledger_2.registerLedgerWallet,
51
+ assertLedgerApp: ledger_2.assertLedgerApp,
52
+ AppClient: ledger_1.AppClient
53
+ };
@@ -0,0 +1,29 @@
1
+ import { Network } from 'bitcoinjs-lib';
2
+ import type { ECPairAPI } from 'ecpair';
3
+ import type { BIP32API, BIP32Interface } from 'bip32';
4
+ import type { KeyInfo } from './types';
5
+ import { LedgerState } from './ledger';
6
+ import type { AppClient } from '@bitcoinerlab/ledger';
7
+ export declare function parseKeyExpression({ keyExpression, isSegwit, ECPair, BIP32, network }: {
8
+ keyExpression: string;
9
+ network?: Network;
10
+ isSegwit?: boolean;
11
+ ECPair: ECPairAPI;
12
+ BIP32: BIP32API;
13
+ }): KeyInfo;
14
+ export declare function keyExpressionLedger({ ledgerClient, ledgerState, originPath, keyPath, change, index }: {
15
+ ledgerClient: AppClient;
16
+ ledgerState: LedgerState;
17
+ originPath: string;
18
+ change?: number | undefined;
19
+ index?: number | undefined | '*';
20
+ keyPath?: string | undefined;
21
+ }): Promise<string>;
22
+ export declare function keyExpressionBIP32({ masterNode, originPath, keyPath, change, index, isPublic }: {
23
+ masterNode: BIP32Interface;
24
+ originPath: string;
25
+ change?: number | undefined;
26
+ index?: number | undefined | '*';
27
+ keyPath?: string | undefined;
28
+ isPublic?: boolean;
29
+ }): string;
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ // Copyright (c) 2023 Jose-Luis Landabaso - https://bitcoinerlab.com
3
+ // Distributed under the MIT software license
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || function (mod) {
21
+ if (mod && mod.__esModule) return mod;
22
+ var result = {};
23
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24
+ __setModuleDefault(result, mod);
25
+ return result;
26
+ };
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.keyExpressionBIP32 = exports.keyExpressionLedger = exports.parseKeyExpression = void 0;
29
+ const bitcoinjs_lib_1 = require("bitcoinjs-lib");
30
+ const ledger_1 = require("./ledger");
31
+ const RE = __importStar(require("./re"));
32
+ const derivePath = (node, path) => {
33
+ if (typeof path !== 'string') {
34
+ throw new Error(`Error: invalid derivation path ${path}`);
35
+ }
36
+ const parsedPath = path.replaceAll('H', "'").replaceAll('h', "'").slice(1);
37
+ const splitPath = parsedPath.split('/');
38
+ for (const element of splitPath) {
39
+ const unhardened = element.endsWith("'") ? element.slice(0, -1) : element;
40
+ if (!Number.isInteger(Number(unhardened)) ||
41
+ Number(unhardened) >= 0x80000000)
42
+ throw new Error(`Error: BIP 32 path element overflow`);
43
+ }
44
+ return node.derivePath(parsedPath);
45
+ };
46
+ /*
47
+ * Takes a key expression (xpub, xprv, pubkey or wif) and returns a pubkey in
48
+ * binary format
49
+ */
50
+ function parseKeyExpression({ keyExpression, isSegwit, ECPair, BIP32, network = bitcoinjs_lib_1.networks.bitcoin }) {
51
+ let pubkey;
52
+ let ecpair;
53
+ let bip32;
54
+ let masterFingerprint;
55
+ let originPath;
56
+ let keyPath;
57
+ let path;
58
+ //Validate the keyExpression:
59
+ const keyExpressions = keyExpression.match(RE.reKeyExp);
60
+ if (keyExpressions === null || keyExpressions[0] !== keyExpression) {
61
+ throw new Error(`Error: expected a keyExpression but got ${keyExpression}`);
62
+ }
63
+ const reOriginAnchoredStart = RegExp(String.raw `^(${RE.reOrigin})?`); //starts with ^origin
64
+ const mOrigin = keyExpression.match(reOriginAnchoredStart);
65
+ if (mOrigin) {
66
+ const bareOrigin = mOrigin[0].replace(/[[\]]/g, ''); //strip the "[" and "]" in [origin]
67
+ const reMasterFingerprintAnchoredStart = String.raw `^(${RE.reMasterFingerprint})`;
68
+ const mMasterFingerprint = bareOrigin.match(reMasterFingerprintAnchoredStart);
69
+ const masterFingerprintHex = mMasterFingerprint
70
+ ? mMasterFingerprint[0]
71
+ : '';
72
+ originPath = bareOrigin.replace(masterFingerprintHex, '');
73
+ if (masterFingerprintHex.length > 0) {
74
+ if (masterFingerprintHex.length !== 8)
75
+ throw new Error(`Error: masterFingerprint ${masterFingerprintHex} invalid for keyExpression: ${keyExpression}`);
76
+ masterFingerprint = Buffer.from(masterFingerprintHex, 'hex');
77
+ }
78
+ }
79
+ //Remove the origin (if it exists) and store result in actualKey
80
+ const actualKey = keyExpression.replace(reOriginAnchoredStart, '');
81
+ let mPubKey, mWIF, mXpubKey, mXprvKey;
82
+ //match pubkey:
83
+ if ((mPubKey = actualKey.match(RE.anchorStartAndEnd(RE.rePubKey))) !== null) {
84
+ pubkey = Buffer.from(mPubKey[0], 'hex');
85
+ ecpair = ECPair.fromPublicKey(pubkey, { network });
86
+ //Validate the pubkey (compressed or uncompressed)
87
+ if (!ECPair.isPoint(pubkey) ||
88
+ !(pubkey.length === 33 || pubkey.length === 65)) {
89
+ throw new Error(`Error: invalid pubkey`);
90
+ }
91
+ //Do an extra check in case we know this pubkey refers to a segwit input
92
+ if (typeof isSegwit === 'boolean' &&
93
+ isSegwit &&
94
+ pubkey.length !== 33 //Inside wpkh and wsh, only compressed public keys are permitted.
95
+ ) {
96
+ throw new Error(`Error: invalid pubkey`);
97
+ }
98
+ //match WIF:
99
+ }
100
+ else if ((mWIF = actualKey.match(RE.anchorStartAndEnd(RE.reWIF))) !== null) {
101
+ ecpair = ECPair.fromWIF(mWIF[0], network);
102
+ //fromWIF will throw if the wif is not valid
103
+ pubkey = ecpair.publicKey;
104
+ //match xpub:
105
+ }
106
+ else if ((mXpubKey = actualKey.match(RE.anchorStartAndEnd(RE.reXpubKey))) !== null) {
107
+ const xPubKey = mXpubKey[0];
108
+ const xPub = xPubKey.match(RE.reXpub)?.[0];
109
+ if (!xPub)
110
+ throw new Error(`Error: xpub could not be matched`);
111
+ bip32 = BIP32.fromBase58(xPub, network);
112
+ const mPath = xPubKey.match(RE.rePath);
113
+ if (mPath !== null) {
114
+ keyPath = xPubKey.match(RE.rePath)?.[0];
115
+ if (!keyPath)
116
+ throw new Error(`Error: could not extract a path`);
117
+ //fromBase58 and derivePath will throw if xPub or path are not valid
118
+ pubkey = derivePath(bip32, keyPath).publicKey;
119
+ }
120
+ else {
121
+ pubkey = bip32.publicKey;
122
+ }
123
+ //match xprv:
124
+ }
125
+ else if ((mXprvKey = actualKey.match(RE.anchorStartAndEnd(RE.reXprvKey))) !== null) {
126
+ const xPrvKey = mXprvKey[0];
127
+ const xPrv = xPrvKey.match(RE.reXprv)?.[0];
128
+ if (!xPrv)
129
+ throw new Error(`Error: xprv could not be matched`);
130
+ bip32 = BIP32.fromBase58(xPrv, network);
131
+ const mPath = xPrvKey.match(RE.rePath);
132
+ if (mPath !== null) {
133
+ keyPath = xPrvKey.match(RE.rePath)?.[0];
134
+ if (!keyPath)
135
+ throw new Error(`Error: could not extract a path`);
136
+ //fromBase58 and derivePath will throw if xPrv or path are not valid
137
+ pubkey = derivePath(bip32, keyPath).publicKey;
138
+ }
139
+ else {
140
+ pubkey = bip32.publicKey;
141
+ }
142
+ }
143
+ else {
144
+ throw new Error(`Error: could not get pubkey for keyExpression ${keyExpression}`);
145
+ }
146
+ if (masterFingerprint && (originPath || keyPath)) {
147
+ path = 'm' + originPath + keyPath;
148
+ }
149
+ return {
150
+ pubkey,
151
+ keyExpression,
152
+ ...(ecpair !== undefined ? { ecpair } : {}),
153
+ ...(bip32 !== undefined ? { bip32 } : {}),
154
+ ...(masterFingerprint !== undefined ? { masterFingerprint } : {}),
155
+ ...(originPath !== undefined && originPath !== '' ? { originPath } : {}),
156
+ ...(keyPath !== undefined && keyPath !== '' ? { keyPath } : {}),
157
+ ...(path !== undefined ? { path } : {})
158
+ };
159
+ }
160
+ exports.parseKeyExpression = parseKeyExpression;
161
+ function assertChangeIndexKeyPath({ change, index, keyPath }) {
162
+ if (!((change === undefined && index === undefined) ||
163
+ (change !== undefined && index !== undefined)))
164
+ throw new Error(`Error: Pass change, index and network or neither`);
165
+ if ((change !== undefined) === (keyPath !== undefined))
166
+ throw new Error(`Error: Pass either change and index or a keyPath`);
167
+ }
168
+ async function keyExpressionLedger({ ledgerClient, ledgerState, originPath, keyPath, change, index }) {
169
+ assertChangeIndexKeyPath({ change, index, keyPath });
170
+ const masterFingerprint = await (0, ledger_1.getLedgerMasterFingerPrint)({
171
+ ledgerClient,
172
+ ledgerState
173
+ });
174
+ const origin = `[${masterFingerprint.toString('hex')}${originPath}]`;
175
+ const xpub = await (0, ledger_1.getLedgerXpub)({ originPath, ledgerClient, ledgerState });
176
+ const keyRoot = `${origin}${xpub}`;
177
+ if (keyPath !== undefined)
178
+ return `${keyRoot}${keyPath}`;
179
+ else
180
+ return `${keyRoot}/${change}/${index}`;
181
+ }
182
+ exports.keyExpressionLedger = keyExpressionLedger;
183
+ function keyExpressionBIP32({ masterNode, originPath, keyPath, change, index, isPublic = true }) {
184
+ assertChangeIndexKeyPath({ change, index, keyPath });
185
+ const masterFingerprint = masterNode.fingerprint;
186
+ const origin = `[${masterFingerprint.toString('hex')}${originPath}]`;
187
+ const xpub = isPublic
188
+ ? masterNode.derivePath(`m${originPath}`).neutered().toBase58().toString()
189
+ : masterNode.derivePath(`m${originPath}`).toBase58().toString();
190
+ const keyRoot = `${origin}${xpub}`;
191
+ if (keyPath !== undefined)
192
+ return `${keyRoot}${keyPath}`;
193
+ else
194
+ return `${keyRoot}/${change}/${index}`;
195
+ }
196
+ exports.keyExpressionBIP32 = keyExpressionBIP32;
@@ -0,0 +1,94 @@
1
+ /// <reference types="node" />
2
+ import type { DescriptorInterface } from './types';
3
+ import { AppClient } from '@bitcoinerlab/ledger';
4
+ export declare function assertLedgerApp({ transport, name, minVersion }: {
5
+ transport: any;
6
+ name: string;
7
+ minVersion: string;
8
+ }): Promise<void>;
9
+ export type LedgerPolicy = {
10
+ policyName?: string;
11
+ ledgerTemplate: string;
12
+ keyRoots: string[];
13
+ policyId?: Buffer;
14
+ policyHmac?: Buffer;
15
+ };
16
+ export type LedgerState = {
17
+ masterFingerprint?: Buffer;
18
+ policies?: LedgerPolicy[];
19
+ xpubs?: {
20
+ [key: string]: string;
21
+ };
22
+ };
23
+ export declare function getLedgerMasterFingerPrint({ ledgerClient, ledgerState }: {
24
+ ledgerClient: AppClient;
25
+ ledgerState: LedgerState;
26
+ }): Promise<Buffer>;
27
+ export declare function getLedgerXpub({ originPath, ledgerClient, ledgerState }: {
28
+ originPath: string;
29
+ ledgerClient: AppClient;
30
+ ledgerState: LedgerState;
31
+ }): Promise<string>;
32
+ /**
33
+ * Takes a descriptor and gets its Ledger Wallet Policy, that is, its keyRoots and template.
34
+ * keyRoots and template follow Ledger's specifications:
35
+ * https://github.com/LedgerHQ/app-bitcoin-new/blob/develop/doc/wallet.md
36
+ *
37
+ * keyRoots and template are a generalization of a descriptor and serve to
38
+ * describe internal and external addresses and any index.
39
+ *
40
+ * So, this function starts from a descriptor and obtains generalized Ledger
41
+ * wallet policy.
42
+ *
43
+ * keyRoots is an array of strings, encoding xpub-type key expressions up to the origin.
44
+ * F.ex.: [76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF
45
+ *
46
+ * Template encodes the descriptor script expression, where its key
47
+ * expressions are represented using variables for each keyRoot and finished with "/**"
48
+ * (for change 1 or 0 and any index). F.ex.:
49
+ * wsh(sortedmulti(2,@0/**,@1/**)), where @0 corresponds the first element in the keyRoots array.
50
+ *
51
+ * If this descriptor does not contain any key that can be signed with the ledger
52
+ * (non-matching masterFingerprint), then this function returns null.
53
+ *
54
+ * This function takes into account all the considerations regarding Ledger
55
+ * policy implementation details expressed in the header of this file.
56
+ */
57
+ export declare function descriptorToLedgerFormat({ descriptor, ledgerClient, ledgerState }: {
58
+ descriptor: DescriptorInterface;
59
+ ledgerClient: AppClient;
60
+ ledgerState: LedgerState;
61
+ }): Promise<{
62
+ ledgerTemplate: string;
63
+ keyRoots: string[];
64
+ } | null>;
65
+ /**
66
+ * It registers a policy based on a descriptor. It stores it in ledgerState.
67
+ *
68
+ * If the policy was already registered, it does not register it.
69
+ * If the policy is standard, it does not register it.
70
+ *
71
+ **/
72
+ export declare function registerLedgerWallet({ descriptor, ledgerClient, ledgerState, policyName }: {
73
+ descriptor: DescriptorInterface;
74
+ ledgerClient: AppClient;
75
+ ledgerState: LedgerState;
76
+ policyName: string;
77
+ }): Promise<void>;
78
+ /**
79
+ * Retrieve a standard ledger policy or null if it does correspond.
80
+ **/
81
+ export declare function ledgerPolicyFromStandard({ descriptor, ledgerClient, ledgerState }: {
82
+ descriptor: DescriptorInterface;
83
+ ledgerClient: AppClient;
84
+ ledgerState: LedgerState;
85
+ }): Promise<LedgerPolicy | null>;
86
+ export declare function comparePolicies(policyA: LedgerPolicy, policyB: LedgerPolicy): boolean;
87
+ /**
88
+ * Retrieve a ledger policy from ledgerState or null if it does not exist yet.
89
+ **/
90
+ export declare function ledgerPolicyFromState({ descriptor, ledgerClient, ledgerState }: {
91
+ descriptor: DescriptorInterface;
92
+ ledgerClient: AppClient;
93
+ ledgerState: LedgerState;
94
+ }): Promise<LedgerPolicy | null>;
package/dist/ledger.js ADDED
@@ -0,0 +1,283 @@
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.ledgerPolicyFromState = exports.comparePolicies = exports.ledgerPolicyFromStandard = exports.registerLedgerWallet = exports.descriptorToLedgerFormat = exports.getLedgerXpub = exports.getLedgerMasterFingerPrint = exports.assertLedgerApp = void 0;
6
+ const ledger_1 = require("@bitcoinerlab/ledger");
7
+ const bitcoinjs_lib_1 = require("bitcoinjs-lib");
8
+ const re_1 = require("./re");
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ async function ledgerAppInfo(transport) {
11
+ const r = await transport.send(0xb0, 0x01, 0x00, 0x00);
12
+ let i = 0;
13
+ const format = r[i++];
14
+ const nameLength = r[i++];
15
+ const name = r.slice(i, (i += nameLength)).toString('ascii');
16
+ const versionLength = r[i++];
17
+ const version = r.slice(i, (i += versionLength)).toString('ascii');
18
+ const flagLength = r[i++];
19
+ const flags = r.slice(i, (i += flagLength));
20
+ return { name, version, flags, format };
21
+ }
22
+ async function assertLedgerApp({ transport, name, minVersion }) {
23
+ const { name: openName, version } = await ledgerAppInfo(transport);
24
+ if (openName !== name) {
25
+ throw new Error(`Open the ${name} app and try again`);
26
+ }
27
+ else {
28
+ const [mVmajor, mVminor, mVpatch] = minVersion.split('.').map(Number);
29
+ const [major, minor, patch] = version.split('.').map(Number);
30
+ if (mVmajor === undefined ||
31
+ mVminor === undefined ||
32
+ mVpatch === undefined) {
33
+ throw new Error(`Pass a minVersion using semver notation: major.minor.patch`);
34
+ }
35
+ if (major < mVmajor ||
36
+ (major === mVmajor && minor < mVminor) ||
37
+ (major === mVmajor && minor === mVminor && patch < mVpatch))
38
+ throw new Error(`Error: please upgrade ${name} to version ${minVersion}`);
39
+ }
40
+ }
41
+ exports.assertLedgerApp = assertLedgerApp;
42
+ function isLedgerStandard({ ledgerTemplate, keyRoots, network = bitcoinjs_lib_1.networks.bitcoin }) {
43
+ if (keyRoots.length !== 1)
44
+ return false;
45
+ const originPath = keyRoots[0]?.match(re_1.reOriginPath)?.[1];
46
+ if (!originPath)
47
+ return false;
48
+ //Network is the 6th character: /44'/0'
49
+ if (originPath[5] !== (network === bitcoinjs_lib_1.networks.bitcoin ? '0' : '1'))
50
+ return false;
51
+ if ((ledgerTemplate === 'pkh(@0/**)' &&
52
+ originPath.match(/^\/44'\/[01]'\/(\d+)'$/)) ||
53
+ (ledgerTemplate === 'wpkh(@0/**)' &&
54
+ originPath.match(/^\/84'\/[01]'\/(\d+)'$/)) ||
55
+ (ledgerTemplate === 'sh(wpkh(@0/**))' &&
56
+ originPath.match(/^\/49'\/[01]'\/(\d+)'$/)) ||
57
+ (ledgerTemplate === 'tr(@0/**)' &&
58
+ originPath.match(/^\/86'\/[01]'\/(\d+)'$/)))
59
+ return true;
60
+ return false;
61
+ }
62
+ async function getLedgerMasterFingerPrint({ ledgerClient, ledgerState }) {
63
+ let masterFingerprint = ledgerState.masterFingerprint;
64
+ if (!masterFingerprint) {
65
+ masterFingerprint = Buffer.from(await ledgerClient.getMasterFingerprint(), 'hex');
66
+ ledgerState.masterFingerprint = masterFingerprint;
67
+ }
68
+ return masterFingerprint;
69
+ }
70
+ exports.getLedgerMasterFingerPrint = getLedgerMasterFingerPrint;
71
+ async function getLedgerXpub({ originPath, ledgerClient, ledgerState }) {
72
+ if (!ledgerState.xpubs)
73
+ ledgerState.xpubs = {};
74
+ let xpub = ledgerState.xpubs[originPath];
75
+ if (!xpub) {
76
+ try {
77
+ //Try getting the xpub without user confirmation
78
+ xpub = await ledgerClient.getExtendedPubkey(`m${originPath}`, false);
79
+ }
80
+ catch (err) {
81
+ xpub = await ledgerClient.getExtendedPubkey(`m${originPath}`, true);
82
+ }
83
+ ledgerState.xpubs[originPath] = xpub;
84
+ }
85
+ return xpub;
86
+ }
87
+ exports.getLedgerXpub = getLedgerXpub;
88
+ /**
89
+ * Takes a descriptor and gets its Ledger Wallet Policy, that is, its keyRoots and template.
90
+ * keyRoots and template follow Ledger's specifications:
91
+ * https://github.com/LedgerHQ/app-bitcoin-new/blob/develop/doc/wallet.md
92
+ *
93
+ * keyRoots and template are a generalization of a descriptor and serve to
94
+ * describe internal and external addresses and any index.
95
+ *
96
+ * So, this function starts from a descriptor and obtains generalized Ledger
97
+ * wallet policy.
98
+ *
99
+ * keyRoots is an array of strings, encoding xpub-type key expressions up to the origin.
100
+ * F.ex.: [76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF
101
+ *
102
+ * Template encodes the descriptor script expression, where its key
103
+ * expressions are represented using variables for each keyRoot and finished with "/**"
104
+ * (for change 1 or 0 and any index). F.ex.:
105
+ * wsh(sortedmulti(2,@0/**,@1/**)), where @0 corresponds the first element in the keyRoots array.
106
+ *
107
+ * If this descriptor does not contain any key that can be signed with the ledger
108
+ * (non-matching masterFingerprint), then this function returns null.
109
+ *
110
+ * This function takes into account all the considerations regarding Ledger
111
+ * policy implementation details expressed in the header of this file.
112
+ */
113
+ async function descriptorToLedgerFormat({ descriptor, ledgerClient, ledgerState }) {
114
+ const expandedExpression = descriptor.expand().expandedExpression;
115
+ const expansionMap = descriptor.expand().expansionMap;
116
+ if (!expandedExpression || !expansionMap)
117
+ throw new Error(`Error: invalid descriptor`);
118
+ const ledgerMasterFingerprint = await getLedgerMasterFingerPrint({
119
+ ledgerClient,
120
+ ledgerState
121
+ });
122
+ //It's important to have keys sorted in ascii order. keys
123
+ //are of this type: @0, @1, @2, .... and they also appear in the expandedExpression
124
+ //in ascending ascii order. Note that Object.keys(expansionMap ) does not ensure
125
+ //that the order is respected and so we force it.
126
+ const allKeys = Object.keys(expansionMap).sort();
127
+ const ledgerKeys = allKeys.filter(key => {
128
+ const masterFingerprint = expansionMap[key]?.masterFingerprint;
129
+ return (masterFingerprint &&
130
+ Buffer.compare(masterFingerprint, ledgerMasterFingerprint) === 0);
131
+ });
132
+ if (ledgerKeys.length === 0)
133
+ return null;
134
+ if (ledgerKeys.length > 1)
135
+ throw new Error(`Error: descriptor ${expandedExpression} does not contain exactly 1 ledger key`);
136
+ const ledgerKey = ledgerKeys[0];
137
+ const masterFingerprint = expansionMap[ledgerKey].masterFingerprint;
138
+ const originPath = expansionMap[ledgerKey].originPath;
139
+ const keyPath = expansionMap[ledgerKey].keyPath;
140
+ const bip32 = expansionMap[ledgerKey].bip32;
141
+ if (!masterFingerprint || !originPath || !keyPath || !bip32) {
142
+ throw new Error(`Error: Ledger key expression must have a valid masterFingerprint: ${masterFingerprint}, originPath: ${originPath}, keyPath: ${keyPath} and a valid bip32 node`);
143
+ }
144
+ if (!/^\/[01]\/\d+$/.test(keyPath))
145
+ throw new Error(`Error: key paths must be /<1;0>/index, where change is 1 or 0 and index >= 0`);
146
+ const keyRoots = [];
147
+ let ledgerTemplate = expandedExpression;
148
+ allKeys.forEach(key => {
149
+ if (key !== ledgerKey) {
150
+ //This block here only does data integrity assertions:
151
+ const otherKeyInfo = expansionMap[key];
152
+ if (!otherKeyInfo.bip32) {
153
+ throw new Error(`Error: ledger only allows xpub-type key expressions`);
154
+ }
155
+ if (otherKeyInfo.originPath) {
156
+ if (otherKeyInfo.originPath !== originPath) {
157
+ 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}`);
158
+ }
159
+ }
160
+ if (otherKeyInfo.keyPath !== keyPath) {
161
+ throw new Error(`Error: all keyPaths must be the same for Ledger being able to sign: ${otherKeyInfo.keyPath} !== ${keyPath}`);
162
+ }
163
+ }
164
+ ledgerTemplate = ledgerTemplate.replaceAll(key, `@${keyRoots.length}/**`);
165
+ const keyInfo = expansionMap[key];
166
+ if (keyInfo.masterFingerprint && keyInfo.originPath)
167
+ keyRoots.push(`[${keyInfo.masterFingerprint?.toString('hex')}${keyInfo.originPath}]${keyInfo?.bip32?.neutered().toBase58()}`);
168
+ else
169
+ keyRoots.push(`${keyInfo?.bip32?.neutered().toBase58()}`);
170
+ });
171
+ return { ledgerTemplate, keyRoots };
172
+ }
173
+ exports.descriptorToLedgerFormat = descriptorToLedgerFormat;
174
+ /**
175
+ * It registers a policy based on a descriptor. It stores it in ledgerState.
176
+ *
177
+ * If the policy was already registered, it does not register it.
178
+ * If the policy is standard, it does not register it.
179
+ *
180
+ **/
181
+ async function registerLedgerWallet({ descriptor, ledgerClient, ledgerState, policyName }) {
182
+ const result = await descriptorToLedgerFormat({
183
+ descriptor,
184
+ ledgerClient,
185
+ ledgerState
186
+ });
187
+ if (await ledgerPolicyFromStandard({ descriptor, ledgerClient, ledgerState }))
188
+ return;
189
+ if (!result)
190
+ throw new Error(`Error: descriptor does not have a ledger input`);
191
+ const { ledgerTemplate, keyRoots } = result;
192
+ if (!ledgerState.policies)
193
+ ledgerState.policies = [];
194
+ let walletPolicy, policyHmac;
195
+ //Search in ledgerState first
196
+ const policy = await ledgerPolicyFromState({
197
+ descriptor,
198
+ ledgerClient,
199
+ ledgerState
200
+ });
201
+ if (policy) {
202
+ if (policy.policyName !== policyName)
203
+ throw new Error(`Error: policy was already registered with a different name: ${policy.policyName}`);
204
+ //It already existed. No need to register it again.
205
+ }
206
+ else {
207
+ walletPolicy = new ledger_1.WalletPolicy(policyName, ledgerTemplate, keyRoots);
208
+ let policyId;
209
+ [policyId, policyHmac] = await ledgerClient.registerWallet(walletPolicy);
210
+ const policy = {
211
+ policyName,
212
+ ledgerTemplate,
213
+ keyRoots,
214
+ policyId,
215
+ policyHmac
216
+ };
217
+ ledgerState.policies.push(policy);
218
+ }
219
+ }
220
+ exports.registerLedgerWallet = registerLedgerWallet;
221
+ /**
222
+ * Retrieve a standard ledger policy or null if it does correspond.
223
+ **/
224
+ async function ledgerPolicyFromStandard({ descriptor, ledgerClient, ledgerState }) {
225
+ const result = await descriptorToLedgerFormat({
226
+ descriptor,
227
+ ledgerClient,
228
+ ledgerState
229
+ });
230
+ if (!result)
231
+ throw new Error(`Error: descriptor does not have a ledger input`);
232
+ const { ledgerTemplate, keyRoots } = result;
233
+ if (isLedgerStandard({
234
+ ledgerTemplate,
235
+ keyRoots,
236
+ network: descriptor.getNetwork()
237
+ }))
238
+ return { ledgerTemplate, keyRoots };
239
+ return null;
240
+ }
241
+ exports.ledgerPolicyFromStandard = ledgerPolicyFromStandard;
242
+ function compareKeyRoots(arr1, arr2) {
243
+ if (arr1.length !== arr2.length) {
244
+ return false;
245
+ }
246
+ for (let i = 0; i < arr1.length; i++) {
247
+ if (arr1[i] !== arr2[i]) {
248
+ return false;
249
+ }
250
+ }
251
+ return true;
252
+ }
253
+ function comparePolicies(policyA, policyB) {
254
+ return (compareKeyRoots(policyA.keyRoots, policyB.keyRoots) &&
255
+ policyA.ledgerTemplate === policyB.ledgerTemplate);
256
+ }
257
+ exports.comparePolicies = comparePolicies;
258
+ /**
259
+ * Retrieve a ledger policy from ledgerState or null if it does not exist yet.
260
+ **/
261
+ async function ledgerPolicyFromState({ descriptor, ledgerClient, ledgerState }) {
262
+ const result = await descriptorToLedgerFormat({
263
+ descriptor,
264
+ ledgerClient,
265
+ ledgerState
266
+ });
267
+ if (!result)
268
+ throw new Error(`Error: descriptor does not have a ledger input`);
269
+ const { ledgerTemplate, keyRoots } = result;
270
+ if (!ledgerState.policies)
271
+ ledgerState.policies = [];
272
+ //Search in ledgerState:
273
+ const policies = ledgerState.policies.filter(policy => comparePolicies(policy, { ledgerTemplate, keyRoots }));
274
+ if (policies.length > 1)
275
+ throw new Error(`Error: duplicated policy`);
276
+ if (policies.length === 1) {
277
+ return policies[0];
278
+ }
279
+ else {
280
+ return null;
281
+ }
282
+ }
283
+ exports.ledgerPolicyFromState = ledgerPolicyFromState;