@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/README.draft.md +84 -0
- package/README.md +74 -0
- package/dist/checksum.d.ts +2 -0
- package/dist/checksum.js +54 -0
- package/dist/descriptors.d.ts +15 -0
- package/dist/descriptors.js +627 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +53 -0
- package/dist/keyExpressions.d.ts +29 -0
- package/dist/keyExpressions.js +196 -0
- package/dist/ledger.d.ts +94 -0
- package/dist/ledger.js +283 -0
- package/dist/miniscript.d.ts +109 -0
- package/dist/miniscript.js +239 -0
- package/dist/psbt.d.ts +38 -0
- package/dist/psbt.js +197 -0
- package/dist/re.d.ts +21 -0
- package/dist/re.js +65 -0
- package/dist/scriptExpressions.d.ts +58 -0
- package/dist/scriptExpressions.js +52 -0
- package/dist/signers.d.ts +37 -0
- package/dist/signers.js +112 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +4 -0
- package/package.json +83 -0
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;
|
package/dist/ledger.d.ts
ADDED
|
@@ -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;
|