diff --git a/index.js b/index.js index 86dabf4f..4c0ac7a0 100644 --- a/index.js +++ b/index.js @@ -5,10 +5,14 @@ const encryptor = require('browser-passworder'); const { normalize: normalizeAddress } = require('eth-sig-util'); const SimpleKeyring = require('eth-simple-keyring'); -const HdKeyring = require('eth-hd-keyring'); +const HdKeyring = require('@metamask/eth-hd-keyring'); const keyringTypes = [SimpleKeyring, HdKeyring]; +const KEYRINGS_TYPE_MAP = { + HD_KEYRING: 'HD Key Tree', + SIMPLE_KEYRING: 'Simple Key Pair', +}; /** * Strip the hex prefix from an address, if present * @param {string} address - The address that might be hex prefixed. @@ -107,7 +111,7 @@ class KeyringController extends EventEmitter { return this.persistAllKeyrings(password) .then(() => { - return this.addNewKeyring('HD Key Tree', { + return this.addNewKeyring(KEYRINGS_TYPE_MAP.HD_KEYRING, { mnemonic: seed, numberOfAccounts: 1, }); @@ -194,9 +198,14 @@ class KeyringController extends EventEmitter { * @param {Object} opts - The constructor options for the keyring. * @returns {Promise} The new keyring. */ - addNewKeyring(type, opts) { + addNewKeyring(type, opts = {}) { const Keyring = this.getKeyringClassForType(type); const keyring = new Keyring(opts); + if (!opts.mnemonic && type === KEYRINGS_TYPE_MAP.HD_KEYRING) { + keyring.generateRandomMnemonic(); + keyring.addAccounts(); + } + return keyring .getAccounts() .then((accounts) => { @@ -250,7 +259,7 @@ class KeyringController extends EventEmitter { checkForDuplicate(type, newAccountArray) { return this.getAccounts().then((accounts) => { switch (type) { - case 'Simple Key Pair': { + case KEYRINGS_TYPE_MAP.SIMPLE_KEYRING: { const isIncluded = Boolean( accounts.find( (key) => @@ -503,7 +512,7 @@ class KeyringController extends EventEmitter { createFirstKeyTree(password) { this.password = password; this.clearKeyrings(); - return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) + return this.addNewKeyring(KEYRINGS_TYPE_MAP.HD_KEYRING) .then((keyring) => { return keyring.getAccounts(); }) diff --git a/package.json b/package.json index c95a415d..96a77aa4 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write" }, "dependencies": { + "@metamask/eth-hd-keyring": "^4.0.2", "bip39": "^3.0.4", "browser-passworder": "^2.0.3", - "eth-hd-keyring": "^3.6.0", "eth-sig-util": "^3.0.1", "eth-simple-keyring": "^4.2.0", "obs-store": "^4.0.3" diff --git a/test/index.js b/test/index.js index c2c25fe4..3c9bdbc5 100644 --- a/test/index.js +++ b/test/index.js @@ -8,14 +8,20 @@ const Wallet = require('ethereumjs-wallet').default; const KeyringController = require('..'); const mockEncryptor = require('./lib/mock-encryptor'); -const mockAddress = '0xeF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase(); - +const password = 'password123'; +const walletOneSeedWords = + 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'; +const walletOneAddresses = ['0xef35ca8ebb9669a35c31b5f6f249a9941a812ac1']; + +const walletTwoSeedWords = + 'urge letter protect palace city barely security section midnight wealth south deer'; + +const walletTwoAddresses = [ + '0xbbafcf3d00fb625b65bb1497c94bf42c1f4b3e78', + '0x49dd2653f38f75d40fdbd51e83b9c9724c87f7eb', +]; describe('KeyringController', function () { let keyringController; - const password = 'password123'; - const seedWords = - 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'; - const addresses = [mockAddress]; beforeEach(async function () { keyringController = new KeyringController({ @@ -103,6 +109,41 @@ describe('KeyringController', function () { }); }); + describe('createNewVaultAndRestore', function () { + it('clears old keyrings and creates a one', async function () { + const initialAccounts = await keyringController.getAccounts(); + expect(initialAccounts).toHaveLength(1); + await keyringController.addNewKeyring('HD Key Tree'); + + const allAccounts = await keyringController.getAccounts(); + expect(allAccounts).toHaveLength(2); + + await keyringController.createNewVaultAndRestore( + password, + walletOneSeedWords, + ); + + const allAccountsAfter = await keyringController.getAccounts(); + expect(allAccountsAfter).toHaveLength(1); + expect(allAccountsAfter[0]).toBe(walletOneAddresses[0]); + }); + + it('throws error if argument password is not a string', async function () { + await expect(() => + keyringController.createNewVaultAndRestore(12, walletTwoSeedWords), + ).rejects.toThrow('Password must be text.'); + }); + + it('throws error if mnemonic passed is invalid', async function () { + await expect(() => + keyringController.createNewVaultAndRestore( + password, + 'test test test palace city barely security section midnight wealth south deer', + ), + ).rejects.toThrow('Seed phrase is invalid.'); + }); + }); + describe('addNewKeyring', function () { it('should add simple key pair', async function () { const privateKey = @@ -123,6 +164,31 @@ describe('KeyringController', function () { ); expect(allAccounts).toStrictEqual(expectedAllAccounts); }); + + it('should add HD Key Tree without mnemonic passed as an argument', async function () { + const previousAllAccounts = await keyringController.getAccounts(); + expect(previousAllAccounts).toHaveLength(1); + const keyring = await keyringController.addNewKeyring('HD Key Tree'); + const keyringAccounts = await keyring.getAccounts(); + expect(keyringAccounts).toHaveLength(1); + const allAccounts = await keyringController.getAccounts(); + expect(allAccounts).toHaveLength(2); + }); + + it('should add HD Key Tree with mnemonic passed as an argument', async function () { + const previousAllAccounts = await keyringController.getAccounts(); + expect(previousAllAccounts).toHaveLength(1); + const keyring = await keyringController.addNewKeyring('HD Key Tree', { + numberOfAccounts: 2, + mnemonic: walletTwoSeedWords, + }); + const keyringAccounts = await keyring.getAccounts(); + expect(keyringAccounts).toHaveLength(2); + expect(keyringAccounts[0]).toStrictEqual(walletTwoAddresses[0]); + expect(keyringAccounts[1]).toStrictEqual(walletTwoAddresses[1]); + const allAccounts = await keyringController.getAccounts(); + expect(allAccounts).toHaveLength(3); + }); }); describe('restoreKeyring', function () { @@ -130,7 +196,7 @@ describe('KeyringController', function () { const mockSerialized = { type: 'HD Key Tree', data: { - mnemonic: seedWords, + mnemonic: walletOneSeedWords, numberOfAccounts: 1, }, }; @@ -139,7 +205,7 @@ describe('KeyringController', function () { expect(keyring.wallets).toHaveLength(1); const accounts = await keyring.getAccounts(); - expect(accounts[0]).toBe(addresses[0]); + expect(accounts[0]).toBe(walletOneAddresses[0]); }); }); @@ -228,6 +294,31 @@ describe('KeyringController', function () { }); }); + describe('verifyPassword', function () { + it('throws an error if no encrypted vault is in controller state', async function () { + keyringController = new KeyringController({ + encryptor: mockEncryptor, + }); + await expect(() => + keyringController.verifyPassword('test'), + ).rejects.toThrow('Cannot unlock without a previous vault.'); + }); + }); + + describe('addNewAccount', function () { + it('adds a new account to the keyring it receives as an argument', async function () { + const [HDKeyring] = await keyringController.getKeyringsByType( + 'HD Key Tree', + ); + const initialAccounts = await HDKeyring.getAccounts(); + expect(initialAccounts).toHaveLength(1); + + await keyringController.addNewAccount(HDKeyring); + const accountsAfterAdd = await HDKeyring.getAccounts(); + expect(accountsAfterAdd).toHaveLength(2); + }); + }); + describe('getAppKeyAddress', function () { it('returns the expected app key address', async function () { const address = '0x01560cd3bac62cc6d7e6380600d9317363400896'; diff --git a/yarn.lock b/yarn.lock index d3d8bba6..a3f6e26f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -537,6 +537,16 @@ semver "^7.3.5" yargs "^17.0.1" +"@metamask/bip39@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@metamask/bip39/-/bip39-4.0.0.tgz#1cb867a8454e3d45d065107b4e070d58bdb64aac" + integrity sha512-xH2g8mFe9p2WePnKeQJH4U8MB6pWPyvwpsz4stb0YdnMOR7cKA6Jm/KOSFiPKr1i9+AzNDImt/XxhwF5ej4jXQ== + dependencies: + "@types/node" "11.11.6" + create-hash "^1.1.0" + pbkdf2 "^3.0.9" + randombytes "^2.0.1" + "@metamask/eslint-config-jest@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@metamask/eslint-config-jest/-/eslint-config-jest-7.0.0.tgz#81612aaf5307c3d65bb43366000233cd0b6e9db4" @@ -552,6 +562,28 @@ resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-7.0.1.tgz#eeb87baa902965ca7931c26911d7027373706f34" integrity sha512-dMZ+iyZrHdZK0D1uStTx8UN6Q6IK9YGbqPUwxgTj63M0mZOsuqs8qpGf+9Dn7uqS+8Oe9jNqejKxozjTiJvsEw== +"@metamask/eth-hd-keyring@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-4.0.2.tgz#0a81556a556b361755c8d6fb5aced1ce5be0331c" + integrity sha512-v47VOTCCmZUZ6uxM5tQNoasQjLdrZADmgph2fhk4m7zKVUxDvYFU7FJT3Rm55fk8mg+dKSbEObDriqbdWeBbcA== + dependencies: + "@metamask/bip39" "^4.0.0" + "@metamask/eth-sig-util" "^4.0.0" + eth-simple-keyring "^4.2.0" + ethereumjs-util "^7.0.9" + ethereumjs-wallet "^1.0.1" + +"@metamask/eth-sig-util@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.0.tgz#11553ba06de0d1352332c1bde28c8edd00e0dcf6" + integrity sha512-LczOjjxY4A7XYloxzyxJIHONELmUxVZncpOLoClpEcTiebiVdM46KRPYXGuULro9oNNR2xdVx3yoKiQjdfWmoA== + dependencies: + ethereumjs-abi "^0.6.8" + ethereumjs-util "^6.2.1" + ethjs-util "^0.1.6" + tweetnacl "^1.0.3" + tweetnacl-util "^0.15.1" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1140,17 +1172,6 @@ bindings@^1.2.1, bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bip39@^2.2.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/bip39/-/bip39-2.6.0.tgz#9e3a720b42ec8b3fbe4038f1e445317b6a99321c" - integrity sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg== - dependencies: - create-hash "^1.1.0" - pbkdf2 "^3.0.9" - randombytes "^2.0.1" - safe-buffer "^5.0.1" - unorm "^1.3.3" - bip39@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0" @@ -1942,17 +1963,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eth-hd-keyring@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/eth-hd-keyring/-/eth-hd-keyring-3.6.0.tgz#6835d30aa411b8d3ef098e82f6427b5325082abb" - integrity sha512-n2CwE9VNXsxLrXQa6suv0Umt4NT6+HtoahKgWx3YviXx4rQFwVT5nDwZfjhwrT31ESuoXYNIeJgz5hKLD96QeQ== - dependencies: - bip39 "^2.2.0" - eth-sig-util "^3.0.1" - eth-simple-keyring "^4.2.0" - ethereumjs-util "^7.0.9" - ethereumjs-wallet "^1.0.1" - eth-sig-util@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/eth-sig-util/-/eth-sig-util-3.0.1.tgz#8753297c83a3f58346bd13547b59c4b2cd110c96" @@ -2015,18 +2025,18 @@ ethereumjs-util@^5.1.1: safe-buffer "^5.1.1" secp256k1 "^3.0.1" -ethereumjs-util@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.0.tgz#23ec79b2488a7d041242f01e25f24e5ad0357960" - integrity sha512-vb0XN9J2QGdZGIEKG2vXM+kUdEivUfU6Wmi5y0cg+LRhDYKnXIZ/Lz7XjFbHRR9VIKq2lVGLzGBkA++y2nOdOQ== +ethereumjs-util@^6.0.0, ethereumjs-util@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz#fcb4e4dd5ceacb9d2305426ab1a5cd93e3163b69" + integrity sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw== dependencies: "@types/bn.js" "^4.11.3" bn.js "^4.11.0" create-hash "^1.1.2" + elliptic "^6.5.2" + ethereum-cryptography "^0.1.3" ethjs-util "0.1.6" - keccak "^2.0.0" rlp "^2.2.3" - secp256k1 "^3.0.1" ethereumjs-util@^7.0.2: version "7.0.10" @@ -2066,7 +2076,7 @@ ethereumjs-wallet@^1.0.1: utf8 "^3.0.0" uuid "^3.3.2" -ethjs-util@0.1.6, ethjs-util@^0.1.3: +ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -3359,16 +3369,6 @@ keccak@^1.0.2: nan "^2.2.1" safe-buffer "^5.1.0" -keccak@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/keccak/-/keccak-2.1.0.tgz#734ea53f2edcfd0f42cdb8d5f4c358fef052752b" - integrity sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q== - dependencies: - bindings "^1.5.0" - inherits "^2.0.4" - nan "^2.14.0" - safe-buffer "^5.2.0" - keccak@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff" @@ -4191,7 +4191,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -4699,7 +4699,7 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl-util@^0.15.0: +tweetnacl-util@^0.15.0, tweetnacl-util@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== @@ -4765,11 +4765,6 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -unorm@^1.3.3: - version "1.6.0" - resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.6.0.tgz#029b289661fba714f1a9af439eb51d9b16c205af" - integrity sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA== - uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"