Skip to content
This repository was archived by the owner on Oct 7, 2024. It is now read-only.

Commit 6d61e99

Browse files
authored
Revert "Typescript migration (#90)" (#102)
This reverts commit f2a6d07.
1 parent f4a8aa9 commit 6d61e99

File tree

12 files changed

+1588
-2004
lines changed

12 files changed

+1588
-2004
lines changed

.eslintrc.js

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,17 @@
11
module.exports = {
22
root: true,
33

4-
extends: ['@metamask/eslint-config'],
4+
extends: ['@metamask/eslint-config', '@metamask/eslint-config-nodejs'],
55

66
overrides: [
77
{
8-
files: ['*.ts'],
9-
extends: ['@metamask/eslint-config-typescript'],
10-
},
11-
12-
{
13-
files: ['*.js'],
14-
parserOptions: {
15-
sourceType: 'script',
8+
files: ['test/**/*.js'],
9+
extends: ['@metamask/eslint-config-jest'],
10+
rules: {
11+
'node/no-unpublished-require': 0,
1612
},
17-
extends: ['@metamask/eslint-config-nodejs'],
18-
},
19-
20-
{
21-
files: ['*.test.ts', '*.test.js'],
22-
extends: [
23-
'@metamask/eslint-config-jest',
24-
'@metamask/eslint-config-nodejs',
25-
],
2613
},
2714
],
2815

29-
ignorePatterns: [
30-
'!.eslintrc.js',
31-
'!.prettierrc.js',
32-
'dist/',
33-
'docs/',
34-
'.yarn/',
35-
],
16+
ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js'],
3617
};

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,4 @@ package-lock.json
1515
!.yarn/plugins
1616
!.yarn/releases
1717
!.yarn/sdks
18-
!.yarn/versions
19-
20-
dist/
18+
!.yarn/versions

index.js

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
const { HDKey } = require('ethereum-cryptography/hdkey');
2+
const { keccak256 } = require('ethereum-cryptography/keccak');
3+
const { bytesToHex } = require('ethereum-cryptography/utils');
4+
const {
5+
stripHexPrefix,
6+
privateToPublic,
7+
publicToAddress,
8+
ecsign,
9+
arrToBufArr,
10+
bufferToHex,
11+
} = require('@ethereumjs/util');
12+
const bip39 = require('@metamask/scure-bip39');
13+
const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english');
14+
const {
15+
concatSig,
16+
decrypt,
17+
getEncryptionPublicKey,
18+
normalize,
19+
personalSign,
20+
signTypedData,
21+
SignTypedDataVersion,
22+
} = require('@metamask/eth-sig-util');
23+
24+
// Options:
25+
const hdPathString = `m/44'/60'/0'/0`;
26+
const type = 'HD Key Tree';
27+
28+
class HdKeyring {
29+
/* PUBLIC METHODS */
30+
constructor(opts = {}) {
31+
this.type = type;
32+
this._wallets = [];
33+
this.deserialize(opts);
34+
}
35+
36+
generateRandomMnemonic() {
37+
this._initFromMnemonic(bip39.generateMnemonic(wordlist));
38+
}
39+
40+
_uint8ArrayToString(mnemonic) {
41+
const recoveredIndices = Array.from(
42+
new Uint16Array(new Uint8Array(mnemonic).buffer),
43+
);
44+
return recoveredIndices.map((i) => wordlist[i]).join(' ');
45+
}
46+
47+
_stringToUint8Array(mnemonic) {
48+
const indices = mnemonic.split(' ').map((word) => wordlist.indexOf(word));
49+
return new Uint8Array(new Uint16Array(indices).buffer);
50+
}
51+
52+
_mnemonicToUint8Array(mnemonic) {
53+
let mnemonicData = mnemonic;
54+
// when encrypted/decrypted, buffers get cast into js object with a property type set to buffer
55+
if (mnemonic && mnemonic.type && mnemonic.type === 'Buffer') {
56+
mnemonicData = mnemonic.data;
57+
}
58+
59+
if (
60+
// this block is for backwards compatibility with vaults that were previously stored as buffers, number arrays or plain text strings
61+
typeof mnemonicData === 'string' ||
62+
Buffer.isBuffer(mnemonicData) ||
63+
Array.isArray(mnemonicData)
64+
) {
65+
let mnemonicAsString = mnemonicData;
66+
if (Array.isArray(mnemonicData)) {
67+
mnemonicAsString = Buffer.from(mnemonicData).toString();
68+
} else if (Buffer.isBuffer(mnemonicData)) {
69+
mnemonicAsString = mnemonicData.toString();
70+
}
71+
return this._stringToUint8Array(mnemonicAsString);
72+
} else if (
73+
mnemonicData instanceof Object &&
74+
!(mnemonicData instanceof Uint8Array)
75+
) {
76+
// when encrypted/decrypted the Uint8Array becomes a js object we need to cast back to a Uint8Array
77+
return Uint8Array.from(Object.values(mnemonicData));
78+
}
79+
return mnemonicData;
80+
}
81+
82+
serialize() {
83+
const mnemonicAsString = this._uint8ArrayToString(this.mnemonic);
84+
const uint8ArrayMnemonic = new TextEncoder('utf-8').encode(
85+
mnemonicAsString,
86+
);
87+
88+
return Promise.resolve({
89+
mnemonic: Array.from(uint8ArrayMnemonic),
90+
numberOfAccounts: this._wallets.length,
91+
hdPath: this.hdPath,
92+
});
93+
}
94+
95+
deserialize(opts = {}) {
96+
if (opts.numberOfAccounts && !opts.mnemonic) {
97+
throw new Error(
98+
'Eth-Hd-Keyring: Deserialize method cannot be called with an opts value for numberOfAccounts and no menmonic',
99+
);
100+
}
101+
102+
if (this.root) {
103+
throw new Error(
104+
'Eth-Hd-Keyring: Secret recovery phrase already provided',
105+
);
106+
}
107+
this.opts = opts;
108+
this._wallets = [];
109+
this.mnemonic = null;
110+
this.root = null;
111+
this.hdPath = opts.hdPath || hdPathString;
112+
113+
if (opts.mnemonic) {
114+
this._initFromMnemonic(opts.mnemonic);
115+
}
116+
117+
if (opts.numberOfAccounts) {
118+
return this.addAccounts(opts.numberOfAccounts);
119+
}
120+
121+
return Promise.resolve([]);
122+
}
123+
124+
addAccounts(numberOfAccounts = 1) {
125+
if (!this.root) {
126+
throw new Error('Eth-Hd-Keyring: No secret recovery phrase provided');
127+
}
128+
129+
const oldLen = this._wallets.length;
130+
const newWallets = [];
131+
for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
132+
const wallet = this.root.deriveChild(i);
133+
newWallets.push(wallet);
134+
this._wallets.push(wallet);
135+
}
136+
const hexWallets = newWallets.map((w) => {
137+
return this._addressfromPublicKey(w.publicKey);
138+
});
139+
return Promise.resolve(hexWallets);
140+
}
141+
142+
getAccounts() {
143+
return this._wallets.map((w) => this._addressfromPublicKey(w.publicKey));
144+
}
145+
146+
/* BASE KEYRING METHODS */
147+
148+
// returns an address specific to an app
149+
async getAppKeyAddress(address, origin) {
150+
if (!origin || typeof origin !== 'string') {
151+
throw new Error(`'origin' must be a non-empty string`);
152+
}
153+
const wallet = this._getWalletForAccount(address, {
154+
withAppKeyOrigin: origin,
155+
});
156+
const appKeyAddress = normalize(
157+
publicToAddress(wallet.publicKey).toString('hex'),
158+
);
159+
160+
return appKeyAddress;
161+
}
162+
163+
// exportAccount should return a hex-encoded private key:
164+
async exportAccount(address, opts = {}) {
165+
const wallet = this._getWalletForAccount(address, opts);
166+
return bytesToHex(wallet.privateKey);
167+
}
168+
169+
// tx is an instance of the ethereumjs-transaction class.
170+
async signTransaction(address, tx, opts = {}) {
171+
const privKey = this._getPrivateKeyFor(address, opts);
172+
const signedTx = tx.sign(privKey);
173+
// Newer versions of Ethereumjs-tx are immutable and return a new tx object
174+
return signedTx === undefined ? tx : signedTx;
175+
}
176+
177+
// For eth_sign, we need to sign arbitrary data:
178+
async signMessage(address, data, opts = {}) {
179+
const message = stripHexPrefix(data);
180+
const privKey = this._getPrivateKeyFor(address, opts);
181+
const msgSig = ecsign(Buffer.from(message, 'hex'), privKey);
182+
const rawMsgSig = concatSig(msgSig.v, msgSig.r, msgSig.s);
183+
return rawMsgSig;
184+
}
185+
186+
// For personal_sign, we need to prefix the message:
187+
async signPersonalMessage(address, msgHex, opts = {}) {
188+
const privKey = this._getPrivateKeyFor(address, opts);
189+
const privateKey = Buffer.from(privKey, 'hex');
190+
const sig = personalSign({ privateKey, data: msgHex });
191+
return sig;
192+
}
193+
194+
// For eth_decryptMessage:
195+
async decryptMessage(withAccount, encryptedData) {
196+
const wallet = this._getWalletForAccount(withAccount);
197+
const { privateKey: privateKeyAsUint8Array } = wallet;
198+
const privateKeyAsHex = Buffer.from(privateKeyAsUint8Array).toString('hex');
199+
const sig = decrypt({ privateKey: privateKeyAsHex, encryptedData });
200+
return sig;
201+
}
202+
203+
// personal_signTypedData, signs data along with the schema
204+
async signTypedData(
205+
withAccount,
206+
typedData,
207+
opts = { version: SignTypedDataVersion.V1 },
208+
) {
209+
// Treat invalid versions as "V1"
210+
const version = Object.keys(SignTypedDataVersion).includes(opts.version)
211+
? opts.version
212+
: SignTypedDataVersion.V1;
213+
214+
const privateKey = this._getPrivateKeyFor(withAccount, opts);
215+
return signTypedData({ privateKey, data: typedData, version });
216+
}
217+
218+
removeAccount(account) {
219+
const address = normalize(account);
220+
if (
221+
!this._wallets
222+
.map(({ publicKey }) => this._addressfromPublicKey(publicKey))
223+
.includes(address)
224+
) {
225+
throw new Error(`Address ${address} not found in this keyring`);
226+
}
227+
228+
this._wallets = this._wallets.filter(
229+
({ publicKey }) => this._addressfromPublicKey(publicKey) !== address,
230+
);
231+
}
232+
233+
// get public key for nacl
234+
async getEncryptionPublicKey(withAccount, opts = {}) {
235+
const privKey = this._getPrivateKeyFor(withAccount, opts);
236+
const publicKey = getEncryptionPublicKey(privKey);
237+
return publicKey;
238+
}
239+
240+
_getPrivateKeyFor(address, opts = {}) {
241+
if (!address) {
242+
throw new Error('Must specify address.');
243+
}
244+
const wallet = this._getWalletForAccount(address, opts);
245+
return wallet.privateKey;
246+
}
247+
248+
_getWalletForAccount(account, opts = {}) {
249+
const address = normalize(account);
250+
let wallet = this._wallets.find(({ publicKey }) => {
251+
return this._addressfromPublicKey(publicKey) === address;
252+
});
253+
if (!wallet) {
254+
throw new Error('HD Keyring - Unable to find matching address.');
255+
}
256+
257+
if (opts.withAppKeyOrigin) {
258+
const { privateKey } = wallet;
259+
const appKeyOriginBuffer = Buffer.from(opts.withAppKeyOrigin, 'utf8');
260+
const appKeyBuffer = Buffer.concat([privateKey, appKeyOriginBuffer]);
261+
const appKeyPrivateKey = arrToBufArr(keccak256(appKeyBuffer, 256));
262+
const appKeyPublicKey = privateToPublic(appKeyPrivateKey);
263+
wallet = { privateKey: appKeyPrivateKey, publicKey: appKeyPublicKey };
264+
}
265+
266+
return wallet;
267+
}
268+
269+
/* PRIVATE / UTILITY METHODS */
270+
271+
/**
272+
* Sets appropriate properties for the keyring based on the given
273+
* BIP39-compliant mnemonic.
274+
*
275+
* @param {string|Array<number>|Buffer} mnemonic - A seed phrase represented
276+
* as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input
277+
* passed as type buffer or array of UTF-8 bytes must be NFKD normalized.
278+
*/
279+
_initFromMnemonic(mnemonic) {
280+
if (this.root) {
281+
throw new Error(
282+
'Eth-Hd-Keyring: Secret recovery phrase already provided',
283+
);
284+
}
285+
286+
this.mnemonic = this._mnemonicToUint8Array(mnemonic);
287+
288+
// validate before initializing
289+
const isValid = bip39.validateMnemonic(this.mnemonic, wordlist);
290+
if (!isValid) {
291+
throw new Error(
292+
'Eth-Hd-Keyring: Invalid secret recovery phrase provided',
293+
);
294+
}
295+
296+
// eslint-disable-next-line node/no-sync
297+
const seed = bip39.mnemonicToSeedSync(this.mnemonic, wordlist);
298+
this.hdWallet = HDKey.fromMasterSeed(seed);
299+
this.root = this.hdWallet.derive(this.hdPath);
300+
}
301+
302+
// small helper function to convert publicKey in Uint8Array form to a publicAddress as a hex
303+
_addressfromPublicKey(publicKey) {
304+
return bufferToHex(
305+
publicToAddress(Buffer.from(publicKey), true),
306+
).toLowerCase();
307+
}
308+
}
309+
310+
HdKeyring.type = type;
311+
module.exports = HdKeyring;

jest.config.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ module.exports = {
33
coverageReporters: ['text', 'html'],
44
coverageThreshold: {
55
global: {
6-
branches: 73.91,
6+
branches: 84,
77
functions: 100,
8-
lines: 91.81,
9-
statements: 91.95,
8+
lines: 95,
9+
statements: 95,
1010
},
1111
},
1212
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
@@ -18,9 +18,6 @@ module.exports = {
1818
// modules.
1919
restoreMocks: true,
2020
testEnvironment: 'node',
21-
testMatch: ['./**/*.test.ts'],
21+
testMatch: ['**/test/**/*.js'],
2222
testTimeout: 2500,
23-
transform: {
24-
'^.+\\.tsx?$': 'ts-jest',
25-
},
2623
};

0 commit comments

Comments
 (0)