@metamask/eth-hd-keyring 6.0.1 → 6.0.2

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/src/HDKeyring.ts DELETED
@@ -1,459 +0,0 @@
1
- import { HDKey } from 'ethereum-cryptography/hdkey';
2
- import { keccak256 } from 'ethereum-cryptography/keccak';
3
- import { bytesToHex } from 'ethereum-cryptography/utils';
4
- import {
5
- stripHexPrefix,
6
- privateToPublic,
7
- publicToAddress,
8
- ecsign,
9
- arrToBufArr,
10
- bufferToHex,
11
- ECDSASignature,
12
- } from '@ethereumjs/util';
13
-
14
- import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english';
15
- import {
16
- concatSig,
17
- decrypt,
18
- getEncryptionPublicKey,
19
- normalize,
20
- personalSign,
21
- signTypedData,
22
- SignTypedDataVersion,
23
- TypedDataV1,
24
- TypedMessage,
25
- } from '@metamask/eth-sig-util';
26
- import { Hex, Keyring, Eip1024EncryptedData } from '@metamask/utils';
27
- import { TxData, TypedTransaction } from '@ethereumjs/tx';
28
- import { HDKeyringErrors } from './errors';
29
-
30
- // TODO: Find out why when imported usin ES6, mnemonic changes
31
- // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
32
- const bip39 = require('@metamask/scure-bip39');
33
-
34
- type JsCastedBuffer = {
35
- type: string;
36
- data: any;
37
- };
38
- type KeyringOpt = {
39
- mnemonic?: Buffer | JsCastedBuffer | string | Uint8Array | number[];
40
- numberOfAccounts?: number;
41
- hdPath?: string;
42
- withAppKeyOrigin?: string;
43
- version?: SignTypedDataVersion;
44
- };
45
-
46
- type SerializedHdKeyringState = {
47
- mnemonic: number[];
48
- numberOfAccounts: number;
49
- hdPath: string;
50
- };
51
-
52
- // Options:
53
- const hdPathString = `m/44'/60'/0'/0`;
54
- const type = 'HD Key Tree';
55
-
56
- export class HDKeyring implements Keyring<SerializedHdKeyringState> {
57
- static type: string = type;
58
-
59
- type: string;
60
-
61
- #wallets: HDKey[] = [];
62
-
63
- root: HDKey | undefined | null;
64
-
65
- mnemonic: Uint8Array | undefined | null;
66
-
67
- hdWallet: HDKey | undefined | null;
68
-
69
- hdPath: string = hdPathString;
70
-
71
- opts: KeyringOpt | undefined | null;
72
-
73
- /* PUBLIC METHODS */
74
- constructor(opts: KeyringOpt = {}) {
75
- this.type = type;
76
- this.#wallets = [];
77
- // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/promise-function-async
78
- this.deserialize(opts);
79
- }
80
-
81
- generateRandomMnemonic() {
82
- this.#initFromMnemonic(bip39.generateMnemonic(wordlist));
83
- }
84
-
85
- #uint8ArrayToString(mnemonic: Uint8Array): string {
86
- const recoveredIndices = Array.from(
87
- new Uint16Array(new Uint8Array(mnemonic).buffer),
88
- );
89
- return recoveredIndices.map((i) => wordlist[i]).join(' ');
90
- }
91
-
92
- #stringToUint8Array(mnemonic: string): Uint8Array {
93
- const indices = mnemonic.split(' ').map((word) => wordlist.indexOf(word));
94
- return new Uint8Array(new Uint16Array(indices).buffer);
95
- }
96
-
97
- #mnemonicToUint8Array(
98
- mnemonic: Buffer | JsCastedBuffer | string | Uint8Array | number[],
99
- ): Uint8Array {
100
- let mnemonicData = mnemonic;
101
- // when encrypted/decrypted, buffers get cast into js object with a property type set to buffer
102
- if (
103
- mnemonic &&
104
- typeof mnemonic !== 'string' &&
105
- !ArrayBuffer.isView(mnemonic) &&
106
- !Array.isArray(mnemonic) &&
107
- !Buffer.isBuffer(mnemonic) &&
108
- mnemonic.type === 'Buffer'
109
- ) {
110
- mnemonicData = mnemonic.data;
111
- }
112
-
113
- if (
114
- // this block is for backwards compatibility with vaults that were previously stored as buffers, number arrays or plain text strings
115
- typeof mnemonicData === 'string' ||
116
- Buffer.isBuffer(mnemonicData) ||
117
- Array.isArray(mnemonicData)
118
- ) {
119
- let mnemonicAsString;
120
- if (Array.isArray(mnemonicData)) {
121
- mnemonicAsString = Buffer.from(mnemonicData).toString();
122
- } else if (Buffer.isBuffer(mnemonicData)) {
123
- mnemonicAsString = mnemonicData.toString();
124
- } else {
125
- mnemonicAsString = mnemonicData;
126
- }
127
- return this.#stringToUint8Array(mnemonicAsString);
128
- } else if (
129
- mnemonicData instanceof Object &&
130
- !(mnemonicData instanceof Uint8Array)
131
- ) {
132
- // when encrypted/decrypted the Uint8Array becomes a js object we need to cast back to a Uint8Array
133
- return Uint8Array.from(Object.values(mnemonicData));
134
- }
135
- return mnemonicData;
136
- }
137
-
138
- async serialize(): Promise<SerializedHdKeyringState> {
139
- if (!this.mnemonic) {
140
- throw new Error(HDKeyringErrors.MissingMnemonic);
141
- }
142
-
143
- const mnemonicAsString = this.#uint8ArrayToString(this.mnemonic);
144
- const uint8ArrayMnemonic = new TextEncoder().encode(mnemonicAsString);
145
-
146
- return Promise.resolve({
147
- mnemonic: Array.from(uint8ArrayMnemonic),
148
- numberOfAccounts: this.#wallets.length,
149
- hdPath: this.hdPath,
150
- });
151
- }
152
-
153
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
154
- // @ts-ignore return type is void
155
- // eslint-disable-next-line @typescript-eslint/promise-function-async
156
- deserialize(state: KeyringOpt = {}) {
157
- if (state.numberOfAccounts && !state.mnemonic) {
158
- throw new Error(
159
- HDKeyringErrors.DeserializeErrorNumberOfAccountWithMissingMnemonic,
160
- );
161
- }
162
-
163
- if (this.root) {
164
- throw new Error(HDKeyringErrors.SRPAlreadyProvided);
165
- }
166
- this.opts = state;
167
- this.#wallets = [];
168
- this.mnemonic = null;
169
- this.root = null;
170
- this.hdPath = state.hdPath ?? hdPathString;
171
-
172
- if (state.mnemonic) {
173
- this.#initFromMnemonic(state.mnemonic);
174
- }
175
-
176
- if (state.numberOfAccounts) {
177
- return this.addAccounts(state.numberOfAccounts);
178
- }
179
-
180
- return Promise.resolve([]);
181
- }
182
-
183
- async addAccounts(numberOfAccounts = 1): Promise<Hex[]> {
184
- if (!this.root) {
185
- throw new Error(HDKeyringErrors.NoSRPProvided);
186
- }
187
-
188
- const oldLen = this.#wallets.length;
189
- const newWallets: HDKey[] = [];
190
- for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
191
- const wallet = this.root.deriveChild(i);
192
- newWallets.push(wallet);
193
- this.#wallets.push(wallet);
194
- }
195
-
196
- const hexWallets: Hex[] = newWallets.map((w) => {
197
- if (!w.publicKey) {
198
- throw new Error(HDKeyringErrors.MissingPublicKey);
199
- }
200
- // HDKey's method publicKey can return null
201
- return this.#addressfromPublicKey(w.publicKey);
202
- });
203
- return Promise.resolve(hexWallets);
204
- }
205
-
206
- async getAccounts(): Promise<Hex[]> {
207
- return Promise.resolve(
208
- this.#wallets.map((w) => {
209
- if (!w.publicKey) {
210
- throw new Error(HDKeyringErrors.MissingPublicKey);
211
- }
212
- return this.#addressfromPublicKey(w.publicKey);
213
- }),
214
- );
215
- }
216
-
217
- /* BASE KEYRING METHODS */
218
-
219
- // returns an address specific to an app
220
- async getAppKeyAddress(address: Hex, origin: string): Promise<Hex> {
221
- if (!origin || typeof origin !== 'string') {
222
- throw new Error(HDKeyringErrors.OriginNotEmpty);
223
- }
224
- const wallet = this.#getWalletForAccount(address, {
225
- withAppKeyOrigin: origin,
226
- });
227
-
228
- if (!wallet.publicKey) {
229
- throw new Error(HDKeyringErrors.MissingPublicKey);
230
- }
231
- // normalize will prefix the address with 0x
232
- const appKeyAddress = normalize(
233
- publicToAddress(Buffer.from(wallet.publicKey)).toString('hex'),
234
- ) as Hex;
235
-
236
- return appKeyAddress;
237
- }
238
-
239
- // exportAccount should return a hex-encoded private key:
240
- async exportAccount(address: Hex, opts: KeyringOpt = {}): Promise<string> {
241
- const wallet = this.#getWalletForAccount(address, opts);
242
- if (!wallet.privateKey) {
243
- throw new Error(HDKeyringErrors.MissingPrivateKey);
244
- }
245
- return bytesToHex(wallet.privateKey);
246
- }
247
-
248
- // tx is an instance of the ethereumjs-transaction class.
249
- async signTransaction(
250
- address: Hex,
251
- transaction: TypedTransaction,
252
- options: KeyringOpt = {},
253
- ): Promise<TxData> {
254
- const privKey = this.#getPrivateKeyFor(address, options);
255
- const signedTx = transaction.sign(Buffer.from(privKey));
256
-
257
- // Newer versions of Ethereumjs-tx are immutable and return a new tx object
258
- return signedTx === undefined ? transaction : signedTx;
259
- }
260
-
261
- // For eth_sign, we need to sign arbitrary data:
262
- async signMessage(
263
- address: Hex,
264
- data: string,
265
- opts: KeyringOpt = {},
266
- ): Promise<string> {
267
- const message: string = stripHexPrefix(data);
268
- const privKey: Uint8Array = this.#getPrivateKeyFor(address, opts);
269
- const msgSig: ECDSASignature = ecsign(
270
- Buffer.from(message, 'hex'),
271
- Buffer.from(privKey),
272
- );
273
- const rawMsgSig: string = concatSig(
274
- msgSig.v as unknown as Buffer,
275
- msgSig.r,
276
- msgSig.s,
277
- );
278
- return rawMsgSig;
279
- }
280
-
281
- // For personal_sign, we need to prefix the message:
282
- async signPersonalMessage(
283
- address: Hex,
284
- message: Hex,
285
- options: Record<string, unknown> = {},
286
- ): Promise<string> {
287
- const privKey: Uint8Array = this.#getPrivateKeyFor(address, options);
288
- const privateKey = Buffer.from(privKey);
289
- const sig = personalSign({ privateKey, data: message as string });
290
- return sig;
291
- }
292
-
293
- // For eth_decryptMessage:
294
- async decryptMessage(
295
- withAccount: Hex,
296
- encryptedData: Eip1024EncryptedData,
297
- ): Promise<string> {
298
- const wallet = this.#getWalletForAccount(withAccount);
299
- const { privateKey: privateKeyAsUint8Array } = wallet;
300
- if (!privateKeyAsUint8Array) {
301
- throw new Error(HDKeyringErrors.MissingPrivateKey);
302
- }
303
- const privateKeyAsHex = Buffer.from(privateKeyAsUint8Array).toString('hex');
304
- const sig = decrypt({ privateKey: privateKeyAsHex, encryptedData });
305
- return sig;
306
- }
307
-
308
- // personal_signTypedData, signs data along with the schema
309
- async signTypedData(
310
- withAccount: Hex,
311
- typedData: Record<string, unknown> | TypedDataV1 | TypedMessage<any>,
312
- opts: KeyringOpt = { version: SignTypedDataVersion.V1 },
313
- ): Promise<string> {
314
- let version: SignTypedDataVersion;
315
- if (
316
- opts.version &&
317
- Object.keys(SignTypedDataVersion).includes(opts.version as string)
318
- ) {
319
- version = opts.version;
320
- } else {
321
- // Treat invalid versions as "V1"
322
- version = SignTypedDataVersion.V1;
323
- }
324
-
325
- const privateKey: Uint8Array = this.#getPrivateKeyFor(withAccount, opts);
326
- return signTypedData({
327
- privateKey: Buffer.from(privateKey),
328
- data: typedData as unknown as TypedDataV1 | TypedMessage<any>,
329
- version,
330
- });
331
- }
332
-
333
- removeAccount(account: Hex): void {
334
- const address = account;
335
- if (
336
- !this.#wallets
337
- .map(({ publicKey }) => {
338
- if (!publicKey) {
339
- throw new Error(HDKeyringErrors.MissingPublicKey);
340
- }
341
- return this.#addressfromPublicKey(publicKey);
342
- })
343
- .includes(address)
344
- ) {
345
- throw new Error(
346
- HDKeyringErrors.AddressNotFound.replace('$address', address),
347
- );
348
- }
349
-
350
- this.#wallets = this.#wallets.filter(({ publicKey }) => {
351
- if (!publicKey) {
352
- // should never be here
353
- throw new Error(HDKeyringErrors.MissingPublicKey);
354
- }
355
- return this.#addressfromPublicKey(publicKey) !== address;
356
- });
357
- }
358
-
359
- // get public key for nacl
360
- async getEncryptionPublicKey(
361
- withAccount: Hex,
362
- opts: KeyringOpt = {},
363
- ): Promise<string> {
364
- const privKey = this.#getPrivateKeyFor(withAccount, opts);
365
- const publicKey = getEncryptionPublicKey(
366
- Buffer.from(privKey).toString('hex'),
367
- );
368
- return publicKey;
369
- }
370
-
371
- #getPrivateKeyFor(address: Hex, opts: KeyringOpt = {}): Uint8Array {
372
- if (!address) {
373
- throw new Error(HDKeyringErrors.AddressNotProvided);
374
- }
375
- const wallet = this.#getWalletForAccount(address, opts);
376
- if (!wallet.privateKey) {
377
- throw new Error(HDKeyringErrors.MissingPrivateKey);
378
- }
379
- return wallet.privateKey;
380
- }
381
-
382
- #getWalletForAccount(address: string, opts: KeyringOpt = {}): HDKey {
383
- const normalizedAddress = normalize(address);
384
- let wallet = this.#wallets.find(({ publicKey }) => {
385
- if (!publicKey) {
386
- throw new Error(HDKeyringErrors.MissingPublicKey);
387
- }
388
- // If a wallet is found, public key will not be null
389
- return this.#addressfromPublicKey(publicKey) === normalizedAddress;
390
- });
391
-
392
- if (opts.withAppKeyOrigin) {
393
- if (!wallet) {
394
- throw new Error(HDKeyringErrors.NoMatchingAddress);
395
- }
396
- const { privateKey } = wallet;
397
- if (!privateKey) {
398
- throw new Error(HDKeyringErrors.MissingPrivateKey);
399
- }
400
- const appKeyOriginBuffer = Buffer.from(opts.withAppKeyOrigin, 'utf8');
401
- const appKeyBuffer = Buffer.concat([privateKey, appKeyOriginBuffer]);
402
- const appKeyPrivateKey = arrToBufArr(keccak256(appKeyBuffer));
403
- const appKeyPublicKey = privateToPublic(appKeyPrivateKey);
404
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
405
- // @ts-ignore special case for appKey
406
- wallet = { privateKey: appKeyPrivateKey, publicKey: appKeyPublicKey };
407
- }
408
-
409
- if (!wallet) {
410
- throw new Error(HDKeyringErrors.NoMatchingAddress);
411
- }
412
-
413
- return wallet;
414
- }
415
-
416
- /* PRIVATE / UTILITY METHODS */
417
-
418
- /**
419
- * Sets appropriate properties for the keyring based on the given
420
- * BIP39-compliant mnemonic.
421
- *
422
- * @param mnemonic - A seed phrase represented
423
- * as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input
424
- * passed as type buffer or array of UTF-8 bytes must be NFKD normalized.
425
- */
426
- #initFromMnemonic(
427
- mnemonic: string | number[] | Buffer | Uint8Array | JsCastedBuffer,
428
- ): void {
429
- if (this.root) {
430
- throw new Error(HDKeyringErrors.SRPAlreadyProvided);
431
- }
432
-
433
- this.mnemonic = this.#mnemonicToUint8Array(mnemonic);
434
-
435
- // validate before initializing
436
- const isValid = bip39.validateMnemonic(this.mnemonic, wordlist);
437
- if (!isValid) {
438
- throw new Error(HDKeyringErrors.InvalidSRP);
439
- }
440
-
441
- // eslint-disable-next-line node/no-sync
442
- const seed = bip39.mnemonicToSeedSync(this.mnemonic, wordlist);
443
- this.hdWallet = HDKey.fromMasterSeed(seed);
444
- if (!this.hdPath) {
445
- throw new Error(HDKeyringErrors.MissingHdPath);
446
- }
447
- this.root = this.hdWallet.derive(this.hdPath);
448
- }
449
-
450
- // small helper function to convert publicKey in Uint8Array form to a publicAddress as a hex
451
- #addressfromPublicKey(publicKey: Uint8Array): Hex {
452
- // bufferToHex adds a 0x prefix
453
- const address = bufferToHex(
454
- publicToAddress(Buffer.from(publicKey), true),
455
- ).toLowerCase() as Hex;
456
-
457
- return address;
458
- }
459
- }
package/src/errors.ts DELETED
@@ -1,16 +0,0 @@
1
- /* eslint-disable no-shadow */
2
- /* eslint-disable no-unused-vars */
3
- export enum HDKeyringErrors {
4
- MissingMnemonic = 'Eth-Hd-Keyring: Missing mnemonic when serializing',
5
- DeserializeErrorNumberOfAccountWithMissingMnemonic = 'Eth-Hd-Keyring: Deserialize method cannot be called with an opts value for numberOfAccounts and no menmonic',
6
- NoSRPProvided = 'Eth-Hd-Keyring: No secret recovery phrase provided',
7
- OriginNotEmpty = `Eth-Hd-Keyring: 'origin' must be a non-empty string`,
8
- AddressNotFound = 'Eth-Hd-Keyring: Address $address not found in this keyring',
9
- AddressNotProvided = 'Eth-Hd-Keyring: Must specify address.',
10
- NoMatchingAddress = 'Eth-Hd-Keyring: Unable to find matching address.',
11
- InvalidSRP = 'Eth-Hd-Keyring: Invalid secret recovery phrase provided',
12
- SRPAlreadyProvided = 'Eth-Hd-Keyring: Secret recovery phrase already provided',
13
- MissingHdPath = 'Eth-Hd-Keyring: Missing hd path',
14
- MissingPrivateKey = 'Eth-Hd-Keyring: Missing private key in wallet',
15
- MissingPublicKey = 'Eth-Hd-Keyring: Missing public key in wallet',
16
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export { HDKeyring } from './HDKeyring';
@@ -1,13 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "declaration": true,
5
- "inlineSources": true,
6
- "noEmit": false,
7
- "outDir": "dist",
8
- "rootDir": "src",
9
- "sourceMap": true
10
- },
11
- "include": ["./src/**/*.ts"],
12
- "exclude": ["./src/**/*.test.ts"]
13
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "esModuleInterop": true,
4
- "exactOptionalPropertyTypes": true,
5
- "forceConsistentCasingInFileNames": true,
6
- "lib": ["ES2020"],
7
- "module": "CommonJS",
8
- "moduleResolution": "node",
9
- "noEmit": true,
10
- "noErrorTruncation": true,
11
- "noUncheckedIndexedAccess": true,
12
- "strict": true,
13
- "target": "es2020"
14
- },
15
- "exclude": ["./dist/**/*"]
16
- }