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

Commit

Permalink
Revert Typescript migration (#90) (#101)
Browse files Browse the repository at this point in the history
This reverts commit f2a6d07
  • Loading branch information
legobeat authored Sep 26, 2023
1 parent fc0b087 commit 05b900a
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 1,313 deletions.
31 changes: 6 additions & 25 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,17 @@
module.exports = {
root: true,

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

overrides: [
{
files: ['*.ts'],
extends: ['@metamask/eslint-config-typescript'],
},

{
files: ['*.js'],
parserOptions: {
sourceType: 'script',
files: ['test/**/*.js'],
extends: ['@metamask/eslint-config-jest'],
rules: {
'node/no-unpublished-require': 0,
},
extends: ['@metamask/eslint-config-nodejs'],
},

{
files: ['*.test.ts', '*.test.js'],
extends: [
'@metamask/eslint-config-jest',
'@metamask/eslint-config-nodejs',
],
},
],

ignorePatterns: [
'!.eslintrc.js',
'!.prettierrc.js',
'dist/',
'docs/',
'.yarn/',
],
ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js'],
};
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ package-lock.json
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

dist/
!.yarn/versions
312 changes: 312 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
const { HDKey } = require('ethereum-cryptography/hdkey');
const { keccak256 } = require('ethereum-cryptography/keccak');
const { bytesToHex } = require('ethereum-cryptography/utils');
const {
privateToPublic,
publicToAddress,
ecsign,
arrToBufArr,
bufferToHex,
} = require('@ethereumjs/util');
const bip39 = require('@metamask/scure-bip39');
const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english');
const {
concatSig,
decrypt,
getEncryptionPublicKey,
normalize,
personalSign,
signTypedData,
SignTypedDataVersion,
} = require('@metamask/eth-sig-util');
const { assertIsHexString, remove0x } = require('@metamask/utils');

// Options:
const hdPathString = `m/44'/60'/0'/0`;
const type = 'HD Key Tree';

class HdKeyring {
/* PUBLIC METHODS */
constructor(opts = {}) {
this.type = type;
this._wallets = [];
this.deserialize(opts);
}

generateRandomMnemonic() {
this._initFromMnemonic(bip39.generateMnemonic(wordlist));
}

_uint8ArrayToString(mnemonic) {
const recoveredIndices = Array.from(
new Uint16Array(new Uint8Array(mnemonic).buffer),
);
return recoveredIndices.map((i) => wordlist[i]).join(' ');
}

_stringToUint8Array(mnemonic) {
const indices = mnemonic.split(' ').map((word) => wordlist.indexOf(word));
return new Uint8Array(new Uint16Array(indices).buffer);
}

_mnemonicToUint8Array(mnemonic) {
let mnemonicData = mnemonic;
// when encrypted/decrypted, buffers get cast into js object with a property type set to buffer
if (mnemonic && mnemonic.type && mnemonic.type === 'Buffer') {
mnemonicData = mnemonic.data;
}

if (
// this block is for backwards compatibility with vaults that were previously stored as buffers, number arrays or plain text strings
typeof mnemonicData === 'string' ||
Buffer.isBuffer(mnemonicData) ||
Array.isArray(mnemonicData)
) {
let mnemonicAsString = mnemonicData;
if (Array.isArray(mnemonicData)) {
mnemonicAsString = Buffer.from(mnemonicData).toString();
} else if (Buffer.isBuffer(mnemonicData)) {
mnemonicAsString = mnemonicData.toString();
}
return this._stringToUint8Array(mnemonicAsString);
} else if (
mnemonicData instanceof Object &&
!(mnemonicData instanceof Uint8Array)
) {
// when encrypted/decrypted the Uint8Array becomes a js object we need to cast back to a Uint8Array
return Uint8Array.from(Object.values(mnemonicData));
}
return mnemonicData;
}

serialize() {
const mnemonicAsString = this._uint8ArrayToString(this.mnemonic);
const uint8ArrayMnemonic = new TextEncoder('utf-8').encode(
mnemonicAsString,
);

return Promise.resolve({
mnemonic: Array.from(uint8ArrayMnemonic),
numberOfAccounts: this._wallets.length,
hdPath: this.hdPath,
});
}

deserialize(opts = {}) {
if (opts.numberOfAccounts && !opts.mnemonic) {
throw new Error(
'Eth-Hd-Keyring: Deserialize method cannot be called with an opts value for numberOfAccounts and no menmonic',
);
}

if (this.root) {
throw new Error(
'Eth-Hd-Keyring: Secret recovery phrase already provided',
);
}
this.opts = opts;
this._wallets = [];
this.mnemonic = null;
this.root = null;
this.hdPath = opts.hdPath || hdPathString;

if (opts.mnemonic) {
this._initFromMnemonic(opts.mnemonic);
}

if (opts.numberOfAccounts) {
return this.addAccounts(opts.numberOfAccounts);
}

return Promise.resolve([]);
}

addAccounts(numberOfAccounts = 1) {
if (!this.root) {
throw new Error('Eth-Hd-Keyring: No secret recovery phrase provided');
}

const oldLen = this._wallets.length;
const newWallets = [];
for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
const wallet = this.root.deriveChild(i);
newWallets.push(wallet);
this._wallets.push(wallet);
}
const hexWallets = newWallets.map((w) => {
return this._addressfromPublicKey(w.publicKey);
});
return Promise.resolve(hexWallets);
}

getAccounts() {
return this._wallets.map((w) => this._addressfromPublicKey(w.publicKey));
}

/* BASE KEYRING METHODS */

// returns an address specific to an app
async getAppKeyAddress(address, origin) {
if (!origin || typeof origin !== 'string') {
throw new Error(`'origin' must be a non-empty string`);
}
const wallet = this._getWalletForAccount(address, {
withAppKeyOrigin: origin,
});
const appKeyAddress = normalize(
publicToAddress(wallet.publicKey).toString('hex'),
);

return appKeyAddress;
}

// exportAccount should return a hex-encoded private key:
async exportAccount(address, opts = {}) {
const wallet = this._getWalletForAccount(address, opts);
return bytesToHex(wallet.privateKey);
}

// tx is an instance of the ethereumjs-transaction class.
async signTransaction(address, tx, opts = {}) {
const privKey = this._getPrivateKeyFor(address, opts);
const signedTx = tx.sign(privKey);
// Newer versions of Ethereumjs-tx are immutable and return a new tx object
return signedTx === undefined ? tx : signedTx;
}

// For eth_sign, we need to sign arbitrary data:
async signMessage(address, data, opts = {}) {
assertIsHexString(data);
const message = remove0x(data);
const privKey = this._getPrivateKeyFor(address, opts);
const msgSig = ecsign(Buffer.from(message, 'hex'), privKey);
const rawMsgSig = concatSig(msgSig.v, msgSig.r, msgSig.s);
return rawMsgSig;
}

// For personal_sign, we need to prefix the message:
async signPersonalMessage(address, msgHex, opts = {}) {
const privKey = this._getPrivateKeyFor(address, opts);
const privateKey = Buffer.from(privKey, 'hex');
const sig = personalSign({ privateKey, data: msgHex });
return sig;
}

// For eth_decryptMessage:
async decryptMessage(withAccount, encryptedData) {
const wallet = this._getWalletForAccount(withAccount);
const { privateKey: privateKeyAsUint8Array } = wallet;
const privateKeyAsHex = Buffer.from(privateKeyAsUint8Array).toString('hex');
const sig = decrypt({ privateKey: privateKeyAsHex, encryptedData });
return sig;
}

// personal_signTypedData, signs data along with the schema
async signTypedData(
withAccount,
typedData,
opts = { version: SignTypedDataVersion.V1 },
) {
// Treat invalid versions as "V1"
const version = Object.keys(SignTypedDataVersion).includes(opts.version)
? opts.version
: SignTypedDataVersion.V1;

const privateKey = this._getPrivateKeyFor(withAccount, opts);
return signTypedData({ privateKey, data: typedData, version });
}

removeAccount(account) {
const address = normalize(account);
if (
!this._wallets
.map(({ publicKey }) => this._addressfromPublicKey(publicKey))
.includes(address)
) {
throw new Error(`Address ${address} not found in this keyring`);
}

this._wallets = this._wallets.filter(
({ publicKey }) => this._addressfromPublicKey(publicKey) !== address,
);
}

// get public key for nacl
async getEncryptionPublicKey(withAccount, opts = {}) {
const privKey = this._getPrivateKeyFor(withAccount, opts);
const publicKey = getEncryptionPublicKey(privKey);
return publicKey;
}

_getPrivateKeyFor(address, opts = {}) {
if (!address) {
throw new Error('Must specify address.');
}
const wallet = this._getWalletForAccount(address, opts);
return wallet.privateKey;
}

_getWalletForAccount(account, opts = {}) {
const address = normalize(account);
let wallet = this._wallets.find(({ publicKey }) => {
return this._addressfromPublicKey(publicKey) === address;
});
if (!wallet) {
throw new Error('HD Keyring - Unable to find matching address.');
}

if (opts.withAppKeyOrigin) {
const { privateKey } = wallet;
const appKeyOriginBuffer = Buffer.from(opts.withAppKeyOrigin, 'utf8');
const appKeyBuffer = Buffer.concat([privateKey, appKeyOriginBuffer]);
const appKeyPrivateKey = arrToBufArr(keccak256(appKeyBuffer, 256));
const appKeyPublicKey = privateToPublic(appKeyPrivateKey);
wallet = { privateKey: appKeyPrivateKey, publicKey: appKeyPublicKey };
}

return wallet;
}

/* PRIVATE / UTILITY METHODS */

/**
* Sets appropriate properties for the keyring based on the given
* BIP39-compliant mnemonic.
*
* @param {string|Array<number>|Buffer} mnemonic - A seed phrase represented
* as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input
* passed as type buffer or array of UTF-8 bytes must be NFKD normalized.
*/
_initFromMnemonic(mnemonic) {
if (this.root) {
throw new Error(
'Eth-Hd-Keyring: Secret recovery phrase already provided',
);
}

this.mnemonic = this._mnemonicToUint8Array(mnemonic);

// validate before initializing
const isValid = bip39.validateMnemonic(this.mnemonic, wordlist);
if (!isValid) {
throw new Error(
'Eth-Hd-Keyring: Invalid secret recovery phrase provided',
);
}

// eslint-disable-next-line node/no-sync
const seed = bip39.mnemonicToSeedSync(this.mnemonic, wordlist);
this.hdWallet = HDKey.fromMasterSeed(seed);
this.root = this.hdWallet.derive(this.hdPath);
}

// small helper function to convert publicKey in Uint8Array form to a publicAddress as a hex
_addressfromPublicKey(publicKey) {
return bufferToHex(
publicToAddress(Buffer.from(publicKey), true),
).toLowerCase();
}
}

HdKeyring.type = type;
module.exports = HdKeyring;
11 changes: 4 additions & 7 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module.exports = {
coverageReporters: ['text', 'html'],
coverageThreshold: {
global: {
branches: 73.91,
branches: 84,
functions: 100,
lines: 91.81,
statements: 91.95,
lines: 95,
statements: 95,
},
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
Expand All @@ -18,9 +18,6 @@ module.exports = {
// modules.
restoreMocks: true,
testEnvironment: 'node',
testMatch: ['./**/*.test.ts'],
testMatch: ['**/test/**/*.js'],
testTimeout: 2500,
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};
Loading

0 comments on commit 05b900a

Please sign in to comment.