@metamask/eth-hd-keyring 4.0.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/.eslintrc.js ADDED
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ root: true,
3
+
4
+ extends: ['@metamask/eslint-config', '@metamask/eslint-config-nodejs'],
5
+
6
+ overrides: [
7
+ {
8
+ files: ['test/**/*.js'],
9
+ extends: ['@metamask/eslint-config-jest'],
10
+ },
11
+ ],
12
+
13
+ ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js'],
14
+ };
@@ -0,0 +1,4 @@
1
+ # Lines starting with '#' are comments.
2
+ # Each line is a file pattern followed by one or more owners.
3
+
4
+ * @MetaMask/devs
@@ -0,0 +1,48 @@
1
+ name: Lint and Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint-test:
10
+ name: Lint and Test
11
+ runs-on: ubuntu-20.04
12
+ strategy:
13
+ matrix:
14
+ node-version: [12.x, 14.x, 16.x]
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - name: Use Node.js ${{ matrix.node-version }}
18
+ uses: actions/setup-node@v2
19
+ with:
20
+ node-version: ${{ matrix.node-version }}
21
+ - name: Get Yarn cache directory
22
+ run: echo "::set-output name=YARN_CACHE_DIR::$(yarn cache dir)"
23
+ id: yarn-cache-dir
24
+ - name: Get Yarn version
25
+ run: echo "::set-output name=YARN_VERSION::$(yarn --version)"
26
+ id: yarn-version
27
+ - name: Cache yarn dependencies
28
+ uses: actions/cache@v2
29
+ with:
30
+ path: ${{ steps.yarn-cache-dir.outputs.YARN_CACHE_DIR }}
31
+ key: yarn-cache-${{ runner.os }}-${{ steps.yarn-version.outputs.YARN_VERSION }}-${{ hashFiles('yarn.lock') }}
32
+ - run: yarn --frozen-lockfile
33
+ - run: yarn allow-scripts
34
+ - run: yarn lint
35
+ - run: yarn test
36
+ - name: Validate RC changelog
37
+ if: ${{ startsWith(github.head_ref, 'release/') }}
38
+ run: yarn auto-changelog validate --rc
39
+ - name: Validate changelog
40
+ if: ${{ !startsWith(github.head_ref, 'release/') }}
41
+ run: yarn auto-changelog validate
42
+ all-jobs-pass:
43
+ name: All jobs pass
44
+ runs-on: ubuntu-20.04
45
+ needs:
46
+ - lint-test
47
+ steps:
48
+ - run: echo "Great success!"
@@ -0,0 +1,50 @@
1
+ name: Create Release Pull Request
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ base-branch:
7
+ description: 'The base branch for git operations and the pull request.'
8
+ default: 'main'
9
+ required: true
10
+ release-type:
11
+ description: 'A SemVer version diff, i.e. major, minor, patch, prerelease etc. Mutually exclusive with "release-version".'
12
+ required: false
13
+ release-version:
14
+ description: 'A specific version to bump to. Mutually exclusive with "release-type".'
15
+ required: false
16
+
17
+ jobs:
18
+ create-release-pr:
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ contents: write
22
+ pull-requests: write
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ with:
26
+ # This is to guarantee that the most recent tag is fetched.
27
+ # This can be configured to a more reasonable value by consumers.
28
+ fetch-depth: 0
29
+ # We check out the specified branch, which will be used as the base
30
+ # branch for all git operations and the release PR.
31
+ ref: ${{ github.event.inputs.base-branch }}
32
+ - name: Get Node.js version
33
+ id: nvm
34
+ run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc)
35
+ - uses: actions/setup-node@v2
36
+ with:
37
+ node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
38
+ - uses: MetaMask/action-create-release-pr@v1
39
+ env:
40
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41
+ with:
42
+ release-type: ${{ github.event.inputs.release-type }}
43
+ release-version: ${{ github.event.inputs.release-version }}
44
+ artifacts-path: gh-action__release-authors
45
+ # Upload the release author artifact for use in subsequent workflows
46
+ - uses: actions/upload-artifact@v2
47
+ with:
48
+ name: release-authors
49
+ path: gh-action__release-authors
50
+ if-no-files-found: error
@@ -0,0 +1,29 @@
1
+ name: Publish Release
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ jobs:
8
+ publish-release:
9
+ permissions:
10
+ contents: write
11
+ if: |
12
+ github.event.pull_request.merged == true &&
13
+ startsWith(github.event.pull_request.head.ref, 'release/')
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ with:
18
+ # We check out the release pull request's base branch, which will be
19
+ # used as the base branch for all git operations.
20
+ ref: ${{ github.event.pull_request.base.ref }}
21
+ - name: Get Node.js version
22
+ id: nvm
23
+ run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc)
24
+ - uses: actions/setup-node@v2
25
+ with:
26
+ node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
27
+ - uses: MetaMask/action-publish-release@v1
28
+ env:
29
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,29 @@
1
+ name: Require Additional Reviewer for Releases
2
+
3
+ on:
4
+ pull_request:
5
+ pull_request_review:
6
+
7
+ jobs:
8
+ require-additional-reviewer:
9
+ permissions:
10
+ actions: read
11
+ contents: read
12
+ pull-requests: read
13
+ statuses: write
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ with:
18
+ # If the base branch has been merged into the release branch, we
19
+ # need to find the earliest common ancestor commit of the base and
20
+ # release branches.
21
+ fetch-depth: 0
22
+ # We want the head / feature branch to be checked out, and we will
23
+ # compare it to the base branch in the action.
24
+ ref: ${{ github.event.pull_request.head.ref }}
25
+ - uses: MetaMask/action-require-additional-reviewer@v1
26
+ env:
27
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28
+ with:
29
+ read-org-token: ${{ secrets.ORG_READER }}
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v12
package/.prettierrc.js ADDED
@@ -0,0 +1,8 @@
1
+ // All of these are defaults except singleQuote, but we specify them
2
+ // for explicitness
3
+ module.exports = {
4
+ quoteProps: 'as-needed',
5
+ singleQuote: true,
6
+ tabWidth: 2,
7
+ trailingComma: 'all',
8
+ };
package/.yarnrc ADDED
@@ -0,0 +1 @@
1
+ ignore-scripts true
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [4.0.0]
10
+ ### Changed
11
+ - **BREAKING**: Do not allow re-initialization of keyring instance ([#55](https://github.com/MetaMask/eth-hd-keyring/pull/55))
12
+ - Consumers are now required to call generateRandomMnemonic() after initialization for creating new SRPs.
13
+ - **BREAKING**: Update minimum Node.js version from v10 to v12 ([#45](https://github.com/MetaMask/eth-hd-keyring/pull/45))
14
+ - Add `@lavamoat/allow-scripts` ([#47](https://github.com/MetaMask/eth-hd-keyring/pull/47))
15
+ - We now have an allowlist for all post-install scripts. The standard setup script has been added, along with new contributor documentation in the README to explain this script.
16
+ - Obfuscate serialized mnemonic ([#59](https://github.com/MetaMask/eth-hd-keyring/pull/59))
17
+ - Package name changed from `eth-hd-keyring` to `@metamask/eth-hd-keyring`
18
+
19
+ [Unreleased]: https://github.com/MetaMask/eth-hd-keyring/compare/v4.0.0...HEAD
20
+ [4.0.0]: https://github.com/MetaMask/eth-hd-keyring/releases/tag/v4.0.0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2020 MetaMask
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # HD Keyring
2
+
3
+ A simple JS class wrapped around [ethereumjs-wallet](https://github.com/ethereumjs/ethereumjs-wallet) designed to expose an interface common to many different signing strategies, to be used in a `KeyringController`, like is being used in [MetaMask](https://metamask.io/)
4
+
5
+ ## Installation
6
+
7
+ `yarn add @metamask/eth-hd-keyring`
8
+
9
+ or
10
+
11
+ `npm install @metamask/eth-hd-keyring`
12
+
13
+ ## The Keyring Class Protocol
14
+
15
+ One of the goals of this class is to allow developers to easily add new signing strategies to MetaMask. We call these signing strategies Keyrings, because they can manage multiple keys.
16
+
17
+ ### Keyring.type
18
+
19
+ A class property that returns a unique string describing the Keyring.
20
+ This is the only class property or method, the remaining methods are instance methods.
21
+
22
+ ### constructor( options )
23
+
24
+ As a Javascript class, your Keyring object will be used to instantiate new Keyring instances using the new keyword. For example:
25
+
26
+ ```
27
+ const keyring = new YourKeyringClass(options);
28
+ ```
29
+
30
+ The constructor currently receives an options object that will be defined by your keyring-building UI, once the user has gone through the steps required for you to fully instantiate a new keyring. For example, choosing a pattern for a vanity account, or entering a seed phrase.
31
+
32
+ We haven't defined the protocol for this account-generating UI yet, so for now please ensure your Keyring behaves nicely when not passed any options object.
33
+
34
+ ## Keyring Instance Methods
35
+
36
+ All below instance methods must return Promises to allow asynchronous resolution.
37
+
38
+ ### serialize()
39
+
40
+ In this method, you must return any JSON-serializable JavaScript object that you like. It will be encoded to a string, encrypted with the user's password, and stored to disk. This is the same object you will receive in the deserialize() method, so it should capture all the information you need to restore the Keyring's state.
41
+
42
+ ### deserialize( object )
43
+
44
+ As discussed above, the deserialize() method will be passed the JavaScript object that you returned when the serialize() method was called.
45
+
46
+ ### addAccounts( n = 1 )
47
+
48
+ The addAccounts(n) method is used to inform your keyring that the user wishes to create a new account. You should perform whatever internal steps are needed so that a call to serialize() will persist the new account, and then return an array of the new account addresses.
49
+
50
+ The method may be called with or without an argument, specifying the number of accounts to create. You should generally default to 1 per call.
51
+
52
+ ### getAccounts()
53
+
54
+ When this method is called, you must return an array of hex-string addresses for the accounts that your Keyring is able to sign for.
55
+
56
+ ### signTransaction(address, transaction)
57
+
58
+ This method will receive a hex-prefixed, all-lowercase address string for the account you should sign the incoming transaction with.
59
+
60
+ For your convenience, the transaction is an instance of ethereumjs-tx, (https://github.com/ethereumjs/ethereumjs-tx) so signing can be as simple as:
61
+
62
+ ```
63
+ transaction.sign(privateKey)
64
+ ```
65
+
66
+ You must return a valid signed ethereumjs-tx (https://github.com/ethereumjs/ethereumjs-tx) object when complete, it can be the same transaction you received.
67
+
68
+ ### signMessage(address, data)
69
+
70
+ The `eth_sign` method will receive the incoming data, alread hashed, and must sign that hash, and then return the raw signed hash.
71
+
72
+ ### exportAccount(address)
73
+
74
+ Exports the specified account as a private key hex string.
75
+
76
+ ## Contributing
77
+
78
+ ### Setup
79
+
80
+ - Install [Node.js](https://nodejs.org) version 12
81
+ - If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you.
82
+ - Install [Yarn v1](https://yarnpkg.com/en/docs/install)
83
+ - Run `yarn setup` to install dependencies and run any requried post-install scripts
84
+ - **Warning:** Do not use the `yarn` / `yarn install` command directly. Use `yarn setup` instead. The normal install command will skip required post-install scripts, leaving your development environment in an invalid state.
85
+
86
+ ### Testing and Linting
87
+
88
+ Run `yarn test` to run the tests once. To run tests on file changes, run `yarn test:watch`.
89
+
90
+ Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues.
91
+
92
+ ### Release & Publishing
93
+
94
+ The project follows the same release process as the other libraries in the MetaMask organization. The GitHub Actions [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) and [`action-publish-release`](https://github.com/MetaMask/action-publish-release) are used to automate the release process; see those repositories for more information about how they work.
95
+
96
+ 1. Choose a release version.
97
+
98
+ - The release version should be chosen according to SemVer. Analyze the changes to see whether they include any breaking changes, new features, or deprecations, then choose the appropriate SemVer version. See [the SemVer specification](https://semver.org/) for more information.
99
+
100
+ 2. If this release is backporting changes onto a previous release, then ensure there is a major version branch for that version (e.g. `1.x` for a `v1` backport release).
101
+
102
+ - The major version branch should be set to the most recent release with that major version. For example, when backporting a `v1.0.2` release, you'd want to ensure there was a `1.x` branch that was set to the `v1.0.1` tag.
103
+
104
+ 3. Trigger the [`workflow_dispatch`](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch) event [manually](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow) for the `Create Release Pull Request` action to create the release PR.
105
+
106
+ - For a backport release, the base branch should be the major version branch that you ensured existed in step 2. For a normal release, the base branch should be the main branch for that repository (which should be the default value).
107
+ - This should trigger the [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) workflow to create the release PR.
108
+
109
+ 4. Update the changelog to move each change entry into the appropriate change category ([See here](https://keepachangelog.com/en/1.0.0/#types) for the full list of change categories, and the correct ordering), and edit them to be more easily understood by users of the package.
110
+
111
+ - Generally any changes that don't affect consumers of the package (e.g. lockfile changes or development environment changes) are omitted. Exceptions may be made for changes that might be of interest despite not having an effect upon the published package (e.g. major test improvements, security improvements, improved documentation, etc.).
112
+ - Try to explain each change in terms that users of the package would understand (e.g. avoid referencing internal variables/concepts).
113
+ - Consolidate related changes into one change entry if it makes it easier to explain.
114
+ - Run `yarn auto-changelog validate --rc` to check that the changelog is correctly formatted.
115
+
116
+ 5. Review and QA the release.
117
+
118
+ - If changes are made to the base branch, the release branch will need to be updated with these changes and review/QA will need to restart again. As such, it's probably best to avoid merging other PRs into the base branch while review is underway.
119
+
120
+ 6. Squash & Merge the release.
121
+
122
+ - This should trigger the [`action-publish-release`](https://github.com/MetaMask/action-publish-release) workflow to tag the final release commit and publish the release on GitHub.
123
+
124
+ 7. Publish the release on npm.
125
+
126
+ - Be very careful to use a clean local environment to publish the release, and follow exactly the same steps used during CI.
127
+ - Use `npm publish --dry-run` to examine the release contents to ensure the correct files are included. Compare to previous releases if necessary (e.g. using `https://unpkg.com/browse/[package name]@[package version]/`).
128
+ - Once you are confident the release contents are correct, publish the release using `npm publish`.
package/index.js ADDED
@@ -0,0 +1,126 @@
1
+ const { hdkey } = require('ethereumjs-wallet');
2
+ const SimpleKeyring = require('eth-simple-keyring');
3
+ const bip39 = require('@metamask/bip39');
4
+ const { normalize } = require('@metamask/eth-sig-util');
5
+
6
+ // Options:
7
+ const hdPathString = `m/44'/60'/0'/0`;
8
+ const type = 'HD Key Tree';
9
+
10
+ class HdKeyring extends SimpleKeyring {
11
+ /* PUBLIC METHODS */
12
+ constructor(opts = {}) {
13
+ super();
14
+ this.type = type;
15
+ this.deserialize(opts);
16
+ }
17
+
18
+ generateRandomMnemonic() {
19
+ this._initFromMnemonic(bip39.generateMnemonic());
20
+ }
21
+
22
+ serialize() {
23
+ const mnemonicAsBuffer =
24
+ typeof this.mnemonic === 'string'
25
+ ? Buffer.from(this.mnemonic, 'utf8')
26
+ : this.mnemonic;
27
+
28
+ return Promise.resolve({
29
+ mnemonic: Array.from(mnemonicAsBuffer.values()),
30
+ numberOfAccounts: this.wallets.length,
31
+ hdPath: this.hdPath,
32
+ });
33
+ }
34
+
35
+ deserialize(opts = {}) {
36
+ if (this.root) {
37
+ throw new Error(
38
+ 'Eth-Hd-Keyring: Secret recovery phrase already provided',
39
+ );
40
+ }
41
+
42
+ this.opts = opts || {};
43
+ this.wallets = [];
44
+ this.mnemonic = null;
45
+ this.root = null;
46
+ this.hdPath = opts.hdPath || hdPathString;
47
+
48
+ if (opts.mnemonic) {
49
+ this._initFromMnemonic(opts.mnemonic);
50
+ }
51
+
52
+ if (opts.numberOfAccounts) {
53
+ return this.addAccounts(opts.numberOfAccounts);
54
+ }
55
+
56
+ return Promise.resolve([]);
57
+ }
58
+
59
+ addAccounts(numberOfAccounts = 1) {
60
+ if (!this.root) {
61
+ throw new Error('Eth-Hd-Keyring: No secret recovery phrase provided');
62
+ }
63
+
64
+ const oldLen = this.wallets.length;
65
+ const newWallets = [];
66
+ for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
67
+ const child = this.root.deriveChild(i);
68
+ const wallet = child.getWallet();
69
+ newWallets.push(wallet);
70
+ this.wallets.push(wallet);
71
+ }
72
+ const hexWallets = newWallets.map((w) => {
73
+ return normalize(w.getAddress().toString('hex'));
74
+ });
75
+ return Promise.resolve(hexWallets);
76
+ }
77
+
78
+ getAccounts() {
79
+ return Promise.resolve(
80
+ this.wallets.map((w) => {
81
+ return normalize(w.getAddress().toString('hex'));
82
+ }),
83
+ );
84
+ }
85
+
86
+ /* PRIVATE METHODS */
87
+
88
+ /**
89
+ * Sets appropriate properties for the keyring based on the given
90
+ * BIP39-compliant mnemonic.
91
+ *
92
+ * @param {string|Array<number>|Buffer} mnemonic - A seed phrase represented
93
+ * as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input
94
+ * passed as type buffer or array of UTF-8 bytes must be NFKD normalized.
95
+ */
96
+ _initFromMnemonic(mnemonic) {
97
+ if (this.root) {
98
+ throw new Error(
99
+ 'Eth-Hd-Keyring: Secret recovery phrase already provided',
100
+ );
101
+ }
102
+ // validate before initializing
103
+ const isValid = bip39.validateMnemonic(mnemonic);
104
+ if (!isValid) {
105
+ throw new Error(
106
+ 'Eth-Hd-Keyring: Invalid secret recovery phrase provided',
107
+ );
108
+ }
109
+
110
+ if (typeof mnemonic === 'string') {
111
+ this.mnemonic = Buffer.from(mnemonic, 'utf8');
112
+ } else if (Array.isArray(mnemonic)) {
113
+ this.mnemonic = Buffer.from(mnemonic);
114
+ } else {
115
+ this.mnemonic = mnemonic;
116
+ }
117
+
118
+ // eslint-disable-next-line node/no-sync
119
+ const seed = bip39.mnemonicToSeedSync(mnemonic);
120
+ this.hdWallet = hdkey.fromMasterSeed(seed);
121
+ this.root = this.hdWallet.derivePath(this.hdPath);
122
+ }
123
+ }
124
+
125
+ HdKeyring.type = type;
126
+ module.exports = HdKeyring;
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ collectCoverage: true,
3
+ coverageReporters: ['text', 'html'],
4
+ coverageThreshold: {
5
+ global: {
6
+ branches: 84,
7
+ functions: 100,
8
+ lines: 96,
9
+ statements: 96,
10
+ },
11
+ },
12
+ moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
13
+ // "resetMocks" resets all mocks, including mocked modules, to jest.fn(),
14
+ // between each test case.
15
+ resetMocks: true,
16
+ // "restoreMocks" restores all mocks created using jest.spyOn to their
17
+ // original implementations, between each test. It does not affect mocked
18
+ // modules.
19
+ restoreMocks: true,
20
+ testEnvironment: 'node',
21
+ testMatch: ['**/test/**/*.js'],
22
+ testTimeout: 2500,
23
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@metamask/eth-hd-keyring",
3
+ "version": "4.0.0",
4
+ "description": "A simple standard interface for a seed phrase generated set of Ethereum accounts.",
5
+ "keywords": [
6
+ "ethereum",
7
+ "keyring"
8
+ ],
9
+ "homepage": "https://github.com/MetaMask/eth-hd-keyring#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/MetaMask/eth-hd-keyring/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/MetaMask/eth-hd-keyring.git"
16
+ },
17
+ "license": "ISC",
18
+ "author": "Dan Finlay",
19
+ "main": "index.js",
20
+ "scripts": {
21
+ "setup": "yarn install && yarn allow-scripts",
22
+ "lint:eslint": "eslint . --cache --ext js,ts",
23
+ "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' --ignore-path .gitignore",
24
+ "lint": "yarn lint:eslint && yarn lint:misc --check",
25
+ "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write",
26
+ "test": "jest"
27
+ },
28
+ "dependencies": {
29
+ "@metamask/bip39": "^4.0.0",
30
+ "@metamask/eth-sig-util": "^4.0.0",
31
+ "eth-simple-keyring": "^4.2.0",
32
+ "ethereumjs-util": "^7.0.9",
33
+ "ethereumjs-wallet": "^1.0.1"
34
+ },
35
+ "devDependencies": {
36
+ "@lavamoat/allow-scripts": "^1.0.6",
37
+ "@metamask/auto-changelog": "^2.5.0",
38
+ "@metamask/eslint-config": "^8.0.0",
39
+ "@metamask/eslint-config-jest": "^9.0.0",
40
+ "@metamask/eslint-config-nodejs": "^8.0.0",
41
+ "@types/jest": "^27.4.1",
42
+ "eslint": "^7.32.0",
43
+ "eslint-config-prettier": "^8.3.0",
44
+ "eslint-plugin-import": "^2.24.2",
45
+ "eslint-plugin-jest": "^24.3.6",
46
+ "eslint-plugin-node": "^11.1.0",
47
+ "eslint-plugin-prettier": "^3.3.1",
48
+ "jest": "^27.5.1",
49
+ "prettier": "^2.4.1",
50
+ "prettier-plugin-packagejson": "^2.2.12"
51
+ },
52
+ "engines": {
53
+ "node": ">= 12.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public",
57
+ "registry": "https://registry.npmjs.org/"
58
+ },
59
+ "lavamoat": {
60
+ "allowScripts": {
61
+ "@lavamoat/preinstall-always-fail": false,
62
+ "keccak": false,
63
+ "secp256k1": false
64
+ }
65
+ }
66
+ }
package/test/index.js ADDED
@@ -0,0 +1,505 @@
1
+ const {
2
+ normalize,
3
+ personalSign,
4
+ recoverPersonalSignature,
5
+ recoverTypedSignature,
6
+ signTypedData,
7
+ SignTypedDataVersion,
8
+ } = require('@metamask/eth-sig-util');
9
+ const ethUtil = require('ethereumjs-util');
10
+ const HdKeyring = require('..');
11
+
12
+ // Sample account:
13
+ const privKeyHex =
14
+ 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952';
15
+
16
+ const sampleMnemonic =
17
+ 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango';
18
+ const firstAcct = '0x1c96099350f13d558464ec79b9be4445aa0ef579';
19
+ const secondAcct = '0x1b00aed43a693f3a957f9feb5cc08afa031e37a0';
20
+
21
+ describe('hd-keyring', () => {
22
+ let keyring;
23
+ beforeEach(() => {
24
+ keyring = new HdKeyring();
25
+ });
26
+
27
+ describe('constructor', () => {
28
+ it('constructs', async () => {
29
+ keyring = new HdKeyring({
30
+ mnemonic: sampleMnemonic,
31
+ numberOfAccounts: 2,
32
+ });
33
+
34
+ const accounts = await keyring.getAccounts();
35
+ expect(accounts[0]).toStrictEqual(firstAcct);
36
+ expect(accounts[1]).toStrictEqual(secondAcct);
37
+ });
38
+
39
+ it('throws on invalid mnemonic', () => {
40
+ expect(
41
+ () =>
42
+ new HdKeyring({
43
+ mnemonic: 'abc xyz',
44
+ numberOfAccounts: 2,
45
+ }),
46
+ ).toThrow('Eth-Hd-Keyring: Invalid secret recovery phrase provided');
47
+ });
48
+ });
49
+
50
+ describe('re-initialization protection', () => {
51
+ const alreadyProvidedError =
52
+ 'Eth-Hd-Keyring: Secret recovery phrase already provided';
53
+ it('double generateRandomMnemonic', () => {
54
+ keyring.generateRandomMnemonic();
55
+ expect(() => {
56
+ keyring.generateRandomMnemonic();
57
+ }).toThrow(alreadyProvidedError);
58
+ });
59
+
60
+ it('constructor + generateRandomMnemonic', () => {
61
+ keyring = new HdKeyring({
62
+ mnemonic: sampleMnemonic,
63
+ numberOfAccounts: 2,
64
+ });
65
+
66
+ expect(() => {
67
+ keyring.generateRandomMnemonic();
68
+ }).toThrow(alreadyProvidedError);
69
+ });
70
+
71
+ it('constructor + deserialize', () => {
72
+ keyring = new HdKeyring({
73
+ mnemonic: sampleMnemonic,
74
+ numberOfAccounts: 2,
75
+ });
76
+
77
+ expect(() => {
78
+ keyring.deserialize({
79
+ mnemonic: sampleMnemonic,
80
+ numberOfAccounts: 1,
81
+ });
82
+ }).toThrow(alreadyProvidedError);
83
+ });
84
+ });
85
+
86
+ describe('Keyring.type', () => {
87
+ it('is a class property that returns the type string.', () => {
88
+ const { type } = HdKeyring;
89
+ expect(typeof type).toBe('string');
90
+ });
91
+ });
92
+
93
+ describe('#type', () => {
94
+ it('returns the correct value', () => {
95
+ const { type } = keyring;
96
+ const correct = HdKeyring.type;
97
+ expect(type).toStrictEqual(correct);
98
+ });
99
+ });
100
+
101
+ describe('#serialize mnemonic.', () => {
102
+ it('serializes mnemonic class variable into a buffer array and does not add accounts', async () => {
103
+ keyring.generateRandomMnemonic();
104
+ const output = await keyring.serialize();
105
+ expect(output.numberOfAccounts).toBe(0);
106
+ expect(Array.isArray(output.mnemonic)).toBe(true);
107
+ });
108
+ });
109
+
110
+ describe('#deserialize a private key', () => {
111
+ it('serializes what it deserializes', async () => {
112
+ await keyring.deserialize({
113
+ mnemonic: sampleMnemonic,
114
+ numberOfAccounts: 1,
115
+ });
116
+ expect(keyring.wallets).toHaveLength(1);
117
+ await keyring.addAccounts(1);
118
+ const accounts = await keyring.getAccounts();
119
+ expect(accounts[0]).toStrictEqual(firstAcct);
120
+ expect(accounts[1]).toStrictEqual(secondAcct);
121
+ expect(accounts).toHaveLength(2);
122
+ const serialized = await keyring.serialize();
123
+ expect(Buffer.from(serialized.mnemonic).toString()).toStrictEqual(
124
+ sampleMnemonic,
125
+ );
126
+ });
127
+ });
128
+
129
+ describe('#addAccounts', () => {
130
+ describe('with no arguments', () => {
131
+ it('creates a single wallet', async () => {
132
+ keyring.generateRandomMnemonic();
133
+ await keyring.addAccounts();
134
+ expect(keyring.wallets).toHaveLength(1);
135
+ });
136
+ });
137
+
138
+ describe('with a numeric argument', () => {
139
+ it('creates that number of wallets', async () => {
140
+ keyring.generateRandomMnemonic();
141
+ await keyring.addAccounts(3);
142
+ expect(keyring.wallets).toHaveLength(3);
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('#getAccounts', () => {
148
+ it('calls getAddress on each wallet', async () => {
149
+ // Push a mock wallet
150
+ const desiredOutput = 'foo';
151
+ keyring.wallets.push({
152
+ getAddress() {
153
+ return {
154
+ toString() {
155
+ return desiredOutput;
156
+ },
157
+ };
158
+ },
159
+ });
160
+
161
+ const output = await keyring.getAccounts();
162
+ expect(output[0]).toBe(`0x${desiredOutput}`);
163
+ expect(output).toHaveLength(1);
164
+ });
165
+ });
166
+
167
+ describe('#signPersonalMessage', () => {
168
+ it('returns the expected value', async () => {
169
+ const address = firstAcct;
170
+ const message = '0x68656c6c6f20776f726c64';
171
+
172
+ await keyring.deserialize({
173
+ mnemonic: sampleMnemonic,
174
+ numberOfAccounts: 1,
175
+ });
176
+ const signature = await keyring.signPersonalMessage(address, message);
177
+ expect(signature).not.toBe(message);
178
+
179
+ const restored = recoverPersonalSignature({
180
+ data: message,
181
+ signature,
182
+ });
183
+
184
+ expect(restored).toStrictEqual(normalize(address));
185
+ });
186
+ });
187
+
188
+ describe('#signTypedData', () => {
189
+ it('can recover a basic signature', async () => {
190
+ Buffer.from(privKeyHex, 'hex');
191
+
192
+ const typedData = [
193
+ {
194
+ type: 'string',
195
+ name: 'message',
196
+ value: 'Hi, Alice!',
197
+ },
198
+ ];
199
+ keyring.generateRandomMnemonic();
200
+ await keyring.addAccounts(1);
201
+ const addresses = await keyring.getAccounts();
202
+ const address = addresses[0];
203
+ const signature = await keyring.signTypedData(address, typedData);
204
+ const restored = recoverTypedSignature({
205
+ data: typedData,
206
+ signature,
207
+ version: SignTypedDataVersion.V1,
208
+ });
209
+ expect(restored).toStrictEqual(address);
210
+ });
211
+ });
212
+
213
+ describe('#signTypedData_v1', () => {
214
+ const typedData = [
215
+ {
216
+ type: 'string',
217
+ name: 'message',
218
+ value: 'Hi, Alice!',
219
+ },
220
+ ];
221
+
222
+ it('signs in a compliant and recoverable way', async () => {
223
+ keyring.generateRandomMnemonic();
224
+ await keyring.addAccounts(1);
225
+ const addresses = await keyring.getAccounts();
226
+ const address = addresses[0];
227
+ const signature = await keyring.signTypedData_v1(address, typedData);
228
+ const restored = recoverTypedSignature({
229
+ data: typedData,
230
+ signature,
231
+ version: SignTypedDataVersion.V1,
232
+ });
233
+ expect(restored).toStrictEqual(address);
234
+ });
235
+ });
236
+
237
+ describe('#signTypedData_v3', () => {
238
+ it('signs in a compliant and recoverable way', async () => {
239
+ const typedData = {
240
+ types: {
241
+ EIP712Domain: [],
242
+ },
243
+ domain: {},
244
+ primaryType: 'EIP712Domain',
245
+ message: {},
246
+ };
247
+
248
+ await keyring.deserialize({
249
+ mnemonic: sampleMnemonic,
250
+ numberOfAccounts: 1,
251
+ });
252
+ const addresses = await keyring.getAccounts();
253
+ const address = addresses[0];
254
+ const signature = await keyring.signTypedData_v3(address, typedData);
255
+ const restored = recoverTypedSignature({
256
+ data: typedData,
257
+ signature,
258
+ version: SignTypedDataVersion.V3,
259
+ });
260
+ expect(restored).toStrictEqual(address);
261
+ });
262
+ });
263
+
264
+ describe('#signTypedData_v3 signature verification', () => {
265
+ it('signs in a recoverable way.', async () => {
266
+ const typedData = {
267
+ types: {
268
+ EIP712Domain: [
269
+ { name: 'name', type: 'string' },
270
+ { name: 'version', type: 'string' },
271
+ { name: 'chainId', type: 'uint256' },
272
+ { name: 'verifyingContract', type: 'address' },
273
+ ],
274
+ Person: [
275
+ { name: 'name', type: 'string' },
276
+ { name: 'wallet', type: 'address' },
277
+ ],
278
+ Mail: [
279
+ { name: 'from', type: 'Person' },
280
+ { name: 'to', type: 'Person' },
281
+ { name: 'contents', type: 'string' },
282
+ ],
283
+ },
284
+ primaryType: 'Mail',
285
+ domain: {
286
+ name: 'Ether Mail',
287
+ version: '1',
288
+ chainId: 1,
289
+ verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
290
+ },
291
+ message: {
292
+ from: {
293
+ name: 'Cow',
294
+ wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
295
+ },
296
+ to: {
297
+ name: 'Bob',
298
+ wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
299
+ },
300
+ contents: 'Hello, Bob!',
301
+ },
302
+ };
303
+
304
+ keyring.generateRandomMnemonic();
305
+ await keyring.addAccounts(1);
306
+ const addresses = await keyring.getAccounts();
307
+ const address = addresses[0];
308
+ const signature = await keyring.signTypedData_v3(address, typedData);
309
+ const restored = recoverTypedSignature({
310
+ data: typedData,
311
+ signature,
312
+ version: SignTypedDataVersion.V3,
313
+ });
314
+ expect(restored).toStrictEqual(address);
315
+ });
316
+ });
317
+
318
+ describe('custom hd paths', () => {
319
+ it('can deserialize with an hdPath param and generate the same accounts.', async () => {
320
+ const hdPathString = `m/44'/60'/0'/0`;
321
+ keyring.deserialize({
322
+ mnemonic: sampleMnemonic,
323
+ numberOfAccounts: 1,
324
+ hdPath: hdPathString,
325
+ });
326
+ const addresses = await keyring.getAccounts();
327
+ expect(addresses[0]).toStrictEqual(firstAcct);
328
+ const serialized = await keyring.serialize();
329
+ expect(serialized.hdPath).toStrictEqual(hdPathString);
330
+ });
331
+
332
+ it('can deserialize with an hdPath param and generate different accounts.', async () => {
333
+ const hdPathString = `m/44'/60'/0'/1`;
334
+
335
+ keyring.deserialize({
336
+ mnemonic: sampleMnemonic,
337
+ numberOfAccounts: 1,
338
+ hdPath: hdPathString,
339
+ });
340
+ const addresses = await keyring.getAccounts();
341
+ expect(addresses[0]).not.toBe(firstAcct);
342
+ const serialized = await keyring.serialize();
343
+ expect(serialized.hdPath).toStrictEqual(hdPathString);
344
+ });
345
+ });
346
+
347
+ // eslint-disable-next-line
348
+ /*
349
+ describe('create and restore 1k accounts', function () {
350
+ it('should restore same accounts with no problem', async function () {
351
+ this.timeout(20000)
352
+
353
+ for (let i = 0; i < 1e3; i++) {
354
+
355
+ keyring = new HdKeyring({
356
+ numberOfAccounts: 1,
357
+ })
358
+ const originalAccounts = await keyring.getAccounts()
359
+ const serialized = await keyring.serialize()
360
+ const mnemonic = serialized.mnemonic
361
+
362
+ keyring = new HdKeyring({
363
+ numberOfAccounts: 1,
364
+ mnemonic,
365
+ })
366
+ const restoredAccounts = await keyring.getAccounts()
367
+
368
+ const first = originalAccounts[0]
369
+ const restored = restoredAccounts[0]
370
+ const msg = `Should restore same account from mnemonic: "${mnemonic}"`
371
+ assert.equal(restoredAccounts[0], originalAccounts[0], msg)
372
+
373
+ }
374
+
375
+ return true
376
+ })
377
+ })
378
+ */
379
+
380
+ describe('getAppKeyAddress', () => {
381
+ it('should return a public address custom to the provided app key origin', async () => {
382
+ const address = firstAcct;
383
+
384
+ keyring = new HdKeyring({
385
+ mnemonic: sampleMnemonic,
386
+ numberOfAccounts: 1,
387
+ });
388
+ const appKeyAddress = await keyring.getAppKeyAddress(
389
+ address,
390
+ 'someapp.origin.io',
391
+ );
392
+
393
+ expect(address).not.toBe(appKeyAddress);
394
+ expect(ethUtil.isValidAddress(appKeyAddress)).toBe(true);
395
+
396
+ const accounts = await keyring.getAccounts();
397
+ expect(accounts[0]).toStrictEqual(firstAcct);
398
+ });
399
+
400
+ it('should return different addresses when provided different app key origins', async () => {
401
+ keyring = new HdKeyring({
402
+ mnemonic: sampleMnemonic,
403
+ numberOfAccounts: 1,
404
+ });
405
+
406
+ const address = firstAcct;
407
+
408
+ const appKeyAddress1 = await keyring.getAppKeyAddress(
409
+ address,
410
+ 'someapp.origin.io',
411
+ );
412
+
413
+ expect(ethUtil.isValidAddress(appKeyAddress1)).toBe(true);
414
+
415
+ const appKeyAddress2 = await keyring.getAppKeyAddress(
416
+ address,
417
+ 'anotherapp.origin.io',
418
+ );
419
+
420
+ expect(ethUtil.isValidAddress(appKeyAddress2)).toBe(true);
421
+
422
+ expect(appKeyAddress1).not.toBe(appKeyAddress2);
423
+ });
424
+
425
+ it('should return the same address when called multiple times with the same params', async () => {
426
+ keyring = new HdKeyring({
427
+ mnemonic: sampleMnemonic,
428
+ numberOfAccounts: 1,
429
+ });
430
+
431
+ const address = firstAcct;
432
+
433
+ const appKeyAddress1 = await keyring.getAppKeyAddress(
434
+ address,
435
+ 'someapp.origin.io',
436
+ );
437
+
438
+ expect(ethUtil.isValidAddress(appKeyAddress1)).toBe(true);
439
+
440
+ const appKeyAddress2 = await keyring.getAppKeyAddress(
441
+ address,
442
+ 'someapp.origin.io',
443
+ );
444
+
445
+ expect(ethUtil.isValidAddress(appKeyAddress2)).toBe(true);
446
+
447
+ expect(appKeyAddress1).toStrictEqual(appKeyAddress2);
448
+ });
449
+ });
450
+
451
+ describe('signing methods withAppKeyOrigin option', () => {
452
+ it('should signPersonalMessage with the expected key when passed a withAppKeyOrigin', async () => {
453
+ const address = firstAcct;
454
+ const message = '0x68656c6c6f20776f726c64';
455
+
456
+ const privateKey = Buffer.from(
457
+ '8e82d2d74c50e5c8460f771d38a560ebe1151a9134c65a7e92b28ad0cfae7151',
458
+ 'hex',
459
+ );
460
+ const expectedSig = personalSign({ privateKey, data: message });
461
+
462
+ await keyring.deserialize({
463
+ mnemonic: sampleMnemonic,
464
+ numberOfAccounts: 1,
465
+ });
466
+ const sig = await keyring.signPersonalMessage(address, message, {
467
+ withAppKeyOrigin: 'someapp.origin.io',
468
+ });
469
+
470
+ expect(sig).toStrictEqual(expectedSig);
471
+ });
472
+
473
+ it('should signTypedData with the expected key when passed a withAppKeyOrigin', async () => {
474
+ const address = firstAcct;
475
+ const typedData = {
476
+ types: {
477
+ EIP712Domain: [],
478
+ },
479
+ domain: {},
480
+ primaryType: 'EIP712Domain',
481
+ message: {},
482
+ };
483
+
484
+ const privateKey = Buffer.from(
485
+ '8e82d2d74c50e5c8460f771d38a560ebe1151a9134c65a7e92b28ad0cfae7151',
486
+ 'hex',
487
+ );
488
+ const expectedSig = signTypedData({
489
+ privateKey,
490
+ data: typedData,
491
+ version: SignTypedDataVersion.V3,
492
+ });
493
+
494
+ await keyring.deserialize({
495
+ mnemonic: sampleMnemonic,
496
+ numberOfAccounts: 1,
497
+ });
498
+
499
+ const sig = await keyring.signTypedData_v3(address, typedData, {
500
+ withAppKeyOrigin: 'someapp.origin.io',
501
+ });
502
+ expect(sig).toStrictEqual(expectedSig);
503
+ });
504
+ });
505
+ });