├── .nvmrc ├── .github ├── CODEOWNERS └── workflows │ ├── security-code-scanner.yml │ ├── publish-release.yml │ ├── create-release-pr.yml │ ├── main.yml │ └── build-lint-test.yml ├── .prettierrc.js ├── .gitignore ├── .eslintrc.js ├── .yarnrc.yml ├── jest.config.js ├── LICENSE ├── .yarn └── plugins │ └── @yarnpkg │ └── plugin-allow-scripts.cjs ├── package.json ├── CHANGELOG.md ├── README.md ├── index.js └── test └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @MetaMask/accounts-engineers 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 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 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | 4 | # Optional eslint cache 5 | .eslintcache 6 | 7 | # Jest 8 | /coverage 9 | 10 | # yarn v3 (w/o zero-install) 11 | # See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 12 | .pnp.* 13 | .yarn/* 14 | !.yarn/patches 15 | !.yarn/plugins 16 | !.yarn/releases 17 | !.yarn/sdks 18 | !.yarn/versions -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 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 | rules: { 11 | 'node/no-unpublished-require': 0, 12 | }, 13 | }, 14 | ], 15 | 16 | ignorePatterns: ['!.eslintrc.js', '!.prettierrc.js'], 17 | }; 18 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableScripts: false 2 | 3 | enableTelemetry: 0 4 | 5 | logFilters: 6 | - code: YN0004 7 | level: discard 8 | 9 | nodeLinker: node-modules 10 | 11 | plugins: 12 | - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs 13 | spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js" 14 | 15 | yarnPath: .yarn/releases/yarn-3.3.0.cjs -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageReporters: ['text', 'html'], 4 | coverageThreshold: { 5 | global: { 6 | branches: 84, 7 | functions: 100, 8 | lines: 95, 9 | statements: 95, 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 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 16 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-allow-scripts", 5 | factory: function (require) { 6 | var plugin=(()=>{var a=Object.create,l=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var s=Object.getOwnPropertyNames;var p=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty;var u=e=>l(e,"__esModule",{value:!0});var f=e=>{if(typeof require!="undefined")return require(e);throw new Error('Dynamic require of "'+e+'" is not supported')};var g=(e,o)=>{for(var r in o)l(e,r,{get:o[r],enumerable:!0})},m=(e,o,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let t of s(o))!c.call(e,t)&&t!=="default"&&l(e,t,{get:()=>o[t],enumerable:!(r=i(o,t))||r.enumerable});return e},x=e=>m(u(l(e!=null?a(p(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var k={};g(k,{default:()=>d});var n=x(f("@yarnpkg/shell")),y={hooks:{afterAllInstalled:async()=>{let e=await(0,n.execute)("yarn run allow-scripts");e!==0&&process.exit(e)}}},d=y;return k;})(); 7 | return plugin; 8 | } 9 | }; -------------------------------------------------------------------------------- /.github/workflows/security-code-scanner.yml: -------------------------------------------------------------------------------- 1 | name: 'MetaMask Security Code Scanner' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | run-security-scan: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | steps: 17 | - name: MetaMask Security Code Scanner 18 | uses: MetaMask/Security-Code-Scanner@main 19 | with: 20 | repo: ${{ github.repository }} 21 | paths_ignored: | 22 | .storybook/ 23 | '**/__snapshots__/' 24 | '**/*.snap' 25 | '**/*.stories.js' 26 | '**/*.stories.tsx' 27 | '**/*.test.browser.ts*' 28 | '**/*.test.js*' 29 | '**/*.test.ts*' 30 | '**/fixtures/' 31 | '**/jest.config.js' 32 | '**/jest.environment.js' 33 | '**/mocks/' 34 | '**/test*/' 35 | docs/ 36 | e2e/ 37 | merged-packages/ 38 | node_modules 39 | storybook/ 40 | test*/ 41 | rules_excluded: example 42 | project_metrics_token: ${{ secrets.SECURITY_SCAN_METRICS_TOKEN }} 43 | slack_webhook: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | NPM_TOKEN: 7 | required: true 8 | 9 | jobs: 10 | publish-release: 11 | permissions: 12 | contents: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | ref: ${{ github.sha }} 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version-file: '.nvmrc' 22 | - uses: MetaMask/action-publish-release@v2 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | publish-npm-dry-run: 27 | runs-on: ubuntu-latest 28 | needs: publish-release 29 | steps: 30 | - uses: actions/checkout@v3 31 | with: 32 | ref: ${{ github.sha }} 33 | - name: Dry Run Publish 34 | # omit npm-token token to perform dry run publish 35 | uses: MetaMask/action-npm-publish@v2 36 | 37 | publish-npm: 38 | environment: npm-publish 39 | runs-on: ubuntu-latest 40 | needs: publish-npm-dry-run 41 | steps: 42 | - uses: actions/checkout@v3 43 | with: 44 | ref: ${{ github.sha }} 45 | - name: Publish 46 | uses: MetaMask/action-npm-publish@v2 47 | with: 48 | # This `NPM_TOKEN` needs to be manually set per-repository. 49 | # Look in the repository settings under "Environments", and set this token in the `npm-publish` environment. 50 | npm-token: ${{ secrets.NPM_TOKEN }} 51 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 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@v3 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: Setup Node.js 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version-file: '.nvmrc' 36 | - uses: MetaMask/action-create-release-pr@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | release-type: ${{ github.event.inputs.release-type }} 41 | release-version: ${{ github.event.inputs.release-version }} 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | check-workflows: 10 | name: Check workflows 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Download actionlint 15 | id: download-actionlint 16 | run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/7fdc9630cc360ea1a469eed64ac6d78caeda1234/scripts/download-actionlint.bash) 1.6.22 17 | shell: bash 18 | - name: Check workflow files 19 | run: ${{ steps.download-actionlint.outputs.executable }} -color 20 | shell: bash 21 | 22 | build-lint-test: 23 | name: Build, lint, and test 24 | uses: ./.github/workflows/build-lint-test.yml 25 | 26 | all-jobs-completed: 27 | name: All jobs completed 28 | runs-on: ubuntu-latest 29 | needs: 30 | - check-workflows 31 | - build-lint-test 32 | outputs: 33 | PASSED: ${{ steps.set-output.outputs.PASSED }} 34 | steps: 35 | - name: Set PASSED output 36 | id: set-output 37 | run: echo "PASSED=true" >> "$GITHUB_OUTPUT" 38 | 39 | all-jobs-pass: 40 | name: All jobs pass 41 | if: ${{ always() }} 42 | runs-on: ubuntu-latest 43 | needs: all-jobs-completed 44 | steps: 45 | - name: Check that all jobs have passed 46 | run: | 47 | passed="${{ needs.all-jobs-completed.outputs.PASSED }}" 48 | if [[ $passed != "true" ]]; then 49 | exit 1 50 | fi 51 | is-release: 52 | # release merge commits come from github-actions 53 | if: startsWith(github.event.commits[0].author.name, 'github-actions') 54 | needs: all-jobs-pass 55 | outputs: 56 | IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: MetaMask/action-is-release@v1 60 | id: is-release 61 | 62 | publish-release: 63 | needs: is-release 64 | if: needs.is-release.outputs.IS_RELEASE == 'true' 65 | name: Publish release 66 | permissions: 67 | contents: write 68 | uses: ./.github/workflows/publish-release.yml 69 | secrets: 70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/build-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Build, Lint, and Test 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | prepare: 8 | name: Prepare 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Use Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version-file: '.nvmrc' 16 | cache: 'yarn' 17 | - name: Install Yarn dependencies 18 | run: yarn --immutable 19 | 20 | lint: 21 | name: Lint 22 | runs-on: ubuntu-latest 23 | needs: 24 | - prepare 25 | strategy: 26 | matrix: 27 | node-version: [16.x, 18.x, 20.x] 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | cache: 'yarn' 35 | - run: yarn --immutable --immutable-cache 36 | - run: yarn lint 37 | - name: Validate RC changelog 38 | if: ${{ startsWith(github.head_ref, 'release/') }} 39 | run: yarn auto-changelog validate --rc 40 | - name: Validate changelog 41 | if: ${{ !startsWith(github.head_ref, 'release/') }} 42 | run: yarn auto-changelog validate 43 | - name: Require clean working directory 44 | shell: bash 45 | run: | 46 | if ! git diff --exit-code; then 47 | echo "Working tree dirty at end of job" 48 | exit 1 49 | fi 50 | test: 51 | name: Test 52 | runs-on: ubuntu-latest 53 | needs: 54 | - prepare 55 | strategy: 56 | matrix: 57 | node-version: [16.x, 18.x, 20.x] 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Use Node.js ${{ matrix.node-version }} 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | cache: 'yarn' 65 | - run: yarn --immutable --immutable-cache 66 | - run: yarn test 67 | - name: Require clean working directory 68 | shell: bash 69 | run: | 70 | if ! git diff --exit-code; then 71 | echo "Working tree dirty at end of job" 72 | exit 1 73 | fi 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metamask/eth-hd-keyring", 3 | "version": "7.0.1", 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 | "lint": "yarn lint:eslint && yarn lint:misc --check", 22 | "lint:eslint": "eslint . --cache --ext js,ts", 23 | "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", 24 | "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern", 25 | "test": "jest" 26 | }, 27 | "dependencies": { 28 | "@ethereumjs/util": "^8.1.0", 29 | "@metamask/eth-sig-util": "^7.0.0", 30 | "@metamask/scure-bip39": "^2.1.0", 31 | "@metamask/utils": "^8.1.0", 32 | "ethereum-cryptography": "^2.1.2" 33 | }, 34 | "devDependencies": { 35 | "@ethereumjs/tx": "^4.0.1", 36 | "@lavamoat/allow-scripts": "^3.0.4", 37 | "@lavamoat/preinstall-always-fail": "^1.0.0", 38 | "@metamask/auto-changelog": "^2.5.0", 39 | "@metamask/bip39": "^4.0.0", 40 | "@metamask/eslint-config": "^8.0.0", 41 | "@metamask/eslint-config-jest": "^9.0.0", 42 | "@metamask/eslint-config-nodejs": "^8.0.0", 43 | "@metamask/eth-hd-keyring": "4.0.1", 44 | "@types/jest": "^29.4.0", 45 | "eslint": "^7.32.0", 46 | "eslint-config-prettier": "^8.3.0", 47 | "eslint-plugin-import": "^2.24.2", 48 | "eslint-plugin-jest": "^24.3.6", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-prettier": "^3.3.1", 51 | "jest": "^29.4.3", 52 | "prettier": "^2.4.1", 53 | "prettier-plugin-packagejson": "^2.2.12" 54 | }, 55 | "packageManager": "yarn@3.3.0", 56 | "engines": { 57 | "node": "^16.20 || ^18.16 || >=20" 58 | }, 59 | "publishConfig": { 60 | "access": "public", 61 | "registry": "https://registry.npmjs.org/" 62 | }, 63 | "lavamoat": { 64 | "allowScripts": { 65 | "@lavamoat/preinstall-always-fail": false, 66 | "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>keccak": false, 67 | "@metamask/eth-hd-keyring>ethereumjs-util>ethereum-cryptography>secp256k1": false 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 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 | ## [7.0.1] 10 | ### Changed 11 | - **BREAKING:** Update minimum Node.js version from v14 to v16 ([#98](https://github.com/MetaMask/eth-hd-keyring/pull/98)) 12 | - Bump dependencies ([#99](https://github.com/MetaMask/eth-hd-keyring/pull/99)) 13 | * **BREAKING:** `@metamask/eth-sig-util` from `^6.0.0` to `^7.0.0` 14 | * **BREAKING:** `@metamask/utils` from `^5.0.2` to `^8.1.0` 15 | * `@ethereumjs/tx` from `^4.1.1` to `^4.2.0` 16 | * `@ethereumjs/util` from `8.0.5` to `^8.1.0` 17 | * `ethereum-cryptography` from `^1.2.0` to `^2.1.2` 18 | 19 | ## [7.0.0] [RETRACTED] 20 | ### Changed 21 | - This version was retracted due to a bug causing code to be missing from published package. 22 | 23 | ## [6.0.2] 24 | ### Fixed 25 | - Bump dependencies ([#94](https://github.com/MetaMask/eth-hd-keyring/pull/94)) 26 | * `@ethereumjs/util` from `^8.0.2` to `^8.1.0` 27 | * `@metamask/eth-sig-util` from `^5.0.2` to `^6.0.0` 28 | * `@metamask/scure-bip39` from `^2.0.3` to `^2.1.0` 29 | * `@metamask/utils` from `^5.0.0` to `^5.0.2` 30 | * `ethereum-cryptography` from `^1.1.2` to `^1.2.0` 31 | 32 | ## [6.0.1] [RETRACTED] 33 | ### Changed 34 | - This version was retracted due to a bug causing code to be missing from published package. 35 | 36 | ## [6.0.0] 37 | ### Changed 38 | - Revert mnemonic serialization format from `Record` (i.e. a stringified `Uint8Array`) which was introduced in v5.0.0 back to an untyped array of utf8 encoded bytes, which was the format prior to v5.0.0 ([#81](https://github.com/MetaMask/eth-hd-keyring/pull/81)) 39 | 40 | ## [5.0.1] [DEPRECATED] 41 | ### Removed 42 | - Remove prepack script and references in order to fix publish release flow ([#77](https://github.com/MetaMask/eth-hd-keyring/pull/77)) 43 | 44 | ## [5.0.0] [DEPRECATED] 45 | ### Changed 46 | - **BREAKING**: Update minimum Node.js version from v12 to v14 ([#67](https://github.com/MetaMask/eth-hd-keyring/pull/67)) 47 | - **BREAKING:** Makes version-specific `signTypedData` methods private ([#71](https://github.com/MetaMask/eth-hd-keyring/pull/71)) 48 | - Consumers should use the generic `signTypedData` method and pass the version they'd like as a property in the options argument. 49 | - **BREAKING:** Makes the `wallets` property private ([#71](https://github.com/MetaMask/eth-hd-keyring/pull/71)) 50 | - Consumers should not use this property as it is intended for internal use only. 51 | - **BREAKING:** Makes `getPrivateKeyFor` a private method ([#71](https://github.com/MetaMask/eth-hd-keyring/pull/71)) 52 | - Consumers who wish to get the private key for a given account should use the `exportAccount` method. 53 | - **BREAKING:** Bumps browser requirements to those with ES2020 support or greater ([#70](https://github.com/MetaMask/eth-hd-keyring/pull/70)) 54 | - This change is introduced in update of `@metamask/eth-sig-util` to v5 and new direct dependency on `ethereumjs/util` v8.0.2 55 | - Replaces use of `ethereumjs-wallet` implementation of hdkey with one from `ethereum-cryptography` and adapts accordingly. ([#69](https://github.com/MetaMask/eth-hd-keyring/pull/69)) 56 | - Replaces `@metamask/bip39` with `@metamask/scure-bip39` ([#67](https://github.com/MetaMask/eth-hd-keyring/pull/67)) 57 | 58 | ### Removed 59 | - **BREAKING:** Remove redundant `newGethSignMessage` method ([#71](https://github.com/MetaMask/eth-hd-keyring/pull/71)) 60 | - Consumers can use `signPersonalMessage` method as a replacement for newGethSignMessage. 61 | - **BREAKING:** `HDKeyring` no longer extends `EventEmitter`, so no `EventEmitter` methods are available on this class ([#70](https://github.com/MetaMask/eth-hd-keyring/pull/70)) 62 | - Removes `ethereumjs-util` dependency. ([#67](https://github.com/MetaMask/eth-hd-keyring/pull/67)) 63 | 64 | ## [4.0.2] 65 | ### Added 66 | - Add parameter validation for constructor / `deserialize` method ([#65](https://github.com/MetaMask/eth-hd-keyring/pull/65)) 67 | - As of v4.0.0, the `deserialize` method (which is also called by the constructor) can no longer generate accounts with the `numberOfAccounts` option without a `mnemonic`. Prior to v4.0.0, a mnemonic was generated automatically if it was missing, but we now want to ensure a mnemonic is never implicitly generated without the caller knowing. 68 | 69 | ## [4.0.1] 70 | ### Added 71 | - Add tests to get coverage to 100% ([#62](https://github.com/MetaMask/eth-hd-keyring/pull/62)) 72 | 73 | ### Fixed 74 | - Fix bug where an unexpected error would occur if the mnemonic passed to `_initFromMnemonic` was a buffer array ([#62](https://github.com/MetaMask/eth-hd-keyring/pull/62)) 75 | 76 | ## [4.0.0] 77 | ### Changed 78 | - **BREAKING**: Do not allow re-initialization of keyring instance ([#55](https://github.com/MetaMask/eth-hd-keyring/pull/55)) 79 | - Consumers are now required to call generateRandomMnemonic() after initialization for creating new SRPs. 80 | - **BREAKING**: Update minimum Node.js version from v10 to v12 ([#45](https://github.com/MetaMask/eth-hd-keyring/pull/45)) 81 | - Add `@lavamoat/allow-scripts` ([#47](https://github.com/MetaMask/eth-hd-keyring/pull/47)) 82 | - 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. 83 | - Obfuscate serialized mnemonic ([#59](https://github.com/MetaMask/eth-hd-keyring/pull/59)) 84 | - Class variable `mnemonic` on `HdKeyring` can now be either type `Buffer` or type `string`. 85 | - Deserialize method (and `HdKeyring` constructor by extension) can no longer be passed an options object containing a value for `numberOfAccounts` if it is not also containing a value for `mnemonic`. 86 | - Package name changed from `eth-hd-keyring` to `@metamask/eth-hd-keyring`. 87 | 88 | [Unreleased]: https://github.com/MetaMask/eth-hd-keyring/compare/v7.0.1...HEAD 89 | [7.0.1]: https://github.com/MetaMask/eth-hd-keyring/compare/v7.0.0...v7.0.1 90 | [7.0.0]: https://github.com/MetaMask/eth-hd-keyring/compare/v6.0.2...v7.0.0 91 | [6.0.2]: https://github.com/MetaMask/eth-hd-keyring/compare/v6.0.1...v6.0.2 92 | [6.0.1]: https://github.com/MetaMask/eth-hd-keyring/compare/v6.0.0...v6.0.1 93 | [6.0.0]: https://github.com/MetaMask/eth-hd-keyring/compare/v5.0.1...v6.0.0 94 | [5.0.1]: https://github.com/MetaMask/eth-hd-keyring/compare/v5.0.0...v5.0.1 95 | [5.0.0]: https://github.com/MetaMask/eth-hd-keyring/compare/v4.0.2...v5.0.0 96 | [4.0.2]: https://github.com/MetaMask/eth-hd-keyring/compare/v4.0.1...v4.0.2 97 | [4.0.1]: https://github.com/MetaMask/eth-hd-keyring/compare/v4.0.0...v4.0.1 98 | [4.0.0]: https://github.com/MetaMask/eth-hd-keyring/releases/tag/v4.0.0 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HD Keyring 2 | 3 | > [!WARNING] 4 | > This package has been moved into a 5 | > [new monorepo](https://github.com/MetaMask/accounts/tree/main/packages/keyring-eth-hd). 6 | > This repository is no longer in use, and pull requests will no longer be accepted. 7 | 8 | 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/) 9 | 10 | ## Installation 11 | 12 | `yarn add @metamask/eth-hd-keyring` 13 | 14 | or 15 | 16 | `npm install @metamask/eth-hd-keyring` 17 | 18 | ## The Keyring Class Protocol 19 | 20 | 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. 21 | 22 | ### Keyring.type 23 | 24 | A class property that returns a unique string describing the Keyring. 25 | This is the only class property or method, the remaining methods are instance methods. 26 | 27 | ### constructor( options ) 28 | 29 | As a Javascript class, your Keyring object will be used to instantiate new Keyring instances using the new keyword. For example: 30 | 31 | ``` 32 | const keyring = new YourKeyringClass(options); 33 | ``` 34 | 35 | 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. 36 | 37 | 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. 38 | 39 | ## Keyring Instance Methods 40 | 41 | All below instance methods must return Promises to allow asynchronous resolution. 42 | 43 | ### serialize() 44 | 45 | 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. 46 | 47 | ### deserialize( object ) 48 | 49 | As discussed above, the deserialize() method will be passed the JavaScript object that you returned when the serialize() method was called. 50 | 51 | ### addAccounts( n = 1 ) 52 | 53 | 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. 54 | 55 | 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. 56 | 57 | ### getAccounts() 58 | 59 | 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. 60 | 61 | ### signTransaction(address, transaction) 62 | 63 | This method will receive a hex-prefixed, all-lowercase address string for the account you should sign the incoming transaction with. 64 | 65 | For your convenience, the transaction is an instance of ethereumjs-tx, (https://github.com/ethereumjs/ethereumjs-tx) so signing can be as simple as: 66 | 67 | ``` 68 | transaction.sign(privateKey) 69 | ``` 70 | 71 | 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. 72 | 73 | ### signMessage(address, data) 74 | 75 | The `eth_sign` method will receive the incoming data, alread hashed, and must sign that hash, and then return the raw signed hash. 76 | 77 | ### exportAccount(address) 78 | 79 | Exports the specified account as a private key hex string. 80 | 81 | ## Contributing 82 | 83 | ### Setup 84 | 85 | - Install [Node.js](https://nodejs.org) version 18 86 | - If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you. 87 | - Install [Yarn v3](https://yarnpkg.com/getting-started/install) 88 | - Run `yarn install` to install dependencies and run any required post-install scripts 89 | 90 | ### Testing and Linting 91 | 92 | Run `yarn test` to run the tests once. To run tests on file changes, run `yarn test:watch`. 93 | 94 | Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues. 95 | 96 | ### Release & Publishing 97 | 98 | 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. 99 | 100 | 1. Choose a release version. 101 | 102 | - 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. 103 | 104 | 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). 105 | 106 | - 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. 107 | 108 | 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. 109 | 110 | - 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). 111 | - This should trigger the [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) workflow to create the release PR. 112 | 113 | 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. 114 | 115 | - 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.). 116 | - Try to explain each change in terms that users of the package would understand (e.g. avoid referencing internal variables/concepts). 117 | - Consolidate related changes into one change entry if it makes it easier to explain. 118 | - Run `yarn auto-changelog validate --rc` to check that the changelog is correctly formatted. 119 | 120 | 5. Review and QA the release. 121 | 122 | - 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. 123 | 124 | 6. Squash & Merge the release. 125 | 126 | - 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. 127 | 128 | 7. Publish the release on npm. 129 | 130 | - Be very careful to use a clean local environment to publish the release, and follow exactly the same steps used during CI. 131 | - 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]/`). 132 | - Once you are confident the release contents are correct, publish the release using `npm publish`. 133 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { HDKey } = require('ethereum-cryptography/hdkey'); 2 | const { keccak256 } = require('ethereum-cryptography/keccak'); 3 | const { bytesToHex } = require('ethereum-cryptography/utils'); 4 | const { 5 | privateToPublic, 6 | publicToAddress, 7 | ecsign, 8 | arrToBufArr, 9 | bufferToHex, 10 | } = require('@ethereumjs/util'); 11 | const bip39 = require('@metamask/scure-bip39'); 12 | const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english'); 13 | const { 14 | concatSig, 15 | decrypt, 16 | getEncryptionPublicKey, 17 | normalize, 18 | personalSign, 19 | signTypedData, 20 | SignTypedDataVersion, 21 | } = require('@metamask/eth-sig-util'); 22 | const { assertIsHexString, remove0x } = require('@metamask/utils'); 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 | assertIsHexString(data); 180 | const message = remove0x(data); 181 | const privKey = this._getPrivateKeyFor(address, opts); 182 | const msgSig = ecsign(Buffer.from(message, 'hex'), privKey); 183 | const rawMsgSig = concatSig(msgSig.v, msgSig.r, msgSig.s); 184 | return rawMsgSig; 185 | } 186 | 187 | // For personal_sign, we need to prefix the message: 188 | async signPersonalMessage(address, msgHex, opts = {}) { 189 | const privKey = this._getPrivateKeyFor(address, opts); 190 | const privateKey = Buffer.from(privKey, 'hex'); 191 | const sig = personalSign({ privateKey, data: msgHex }); 192 | return sig; 193 | } 194 | 195 | // For eth_decryptMessage: 196 | async decryptMessage(withAccount, encryptedData) { 197 | const wallet = this._getWalletForAccount(withAccount); 198 | const { privateKey: privateKeyAsUint8Array } = wallet; 199 | const privateKeyAsHex = Buffer.from(privateKeyAsUint8Array).toString('hex'); 200 | const sig = decrypt({ privateKey: privateKeyAsHex, encryptedData }); 201 | return sig; 202 | } 203 | 204 | // personal_signTypedData, signs data along with the schema 205 | async signTypedData( 206 | withAccount, 207 | typedData, 208 | opts = { version: SignTypedDataVersion.V1 }, 209 | ) { 210 | // Treat invalid versions as "V1" 211 | const version = Object.keys(SignTypedDataVersion).includes(opts.version) 212 | ? opts.version 213 | : SignTypedDataVersion.V1; 214 | 215 | const privateKey = this._getPrivateKeyFor(withAccount, opts); 216 | return signTypedData({ privateKey, data: typedData, version }); 217 | } 218 | 219 | removeAccount(account) { 220 | const address = normalize(account); 221 | if ( 222 | !this._wallets 223 | .map(({ publicKey }) => this._addressfromPublicKey(publicKey)) 224 | .includes(address) 225 | ) { 226 | throw new Error(`Address ${address} not found in this keyring`); 227 | } 228 | 229 | this._wallets = this._wallets.filter( 230 | ({ publicKey }) => this._addressfromPublicKey(publicKey) !== address, 231 | ); 232 | } 233 | 234 | // get public key for nacl 235 | async getEncryptionPublicKey(withAccount, opts = {}) { 236 | const privKey = this._getPrivateKeyFor(withAccount, opts); 237 | const publicKey = getEncryptionPublicKey(privKey); 238 | return publicKey; 239 | } 240 | 241 | _getPrivateKeyFor(address, opts = {}) { 242 | if (!address) { 243 | throw new Error('Must specify address.'); 244 | } 245 | const wallet = this._getWalletForAccount(address, opts); 246 | return wallet.privateKey; 247 | } 248 | 249 | _getWalletForAccount(account, opts = {}) { 250 | const address = normalize(account); 251 | let wallet = this._wallets.find(({ publicKey }) => { 252 | return this._addressfromPublicKey(publicKey) === address; 253 | }); 254 | if (!wallet) { 255 | throw new Error('HD Keyring - Unable to find matching address.'); 256 | } 257 | 258 | if (opts.withAppKeyOrigin) { 259 | const { privateKey } = wallet; 260 | const appKeyOriginBuffer = Buffer.from(opts.withAppKeyOrigin, 'utf8'); 261 | const appKeyBuffer = Buffer.concat([privateKey, appKeyOriginBuffer]); 262 | const appKeyPrivateKey = arrToBufArr(keccak256(appKeyBuffer, 256)); 263 | const appKeyPublicKey = privateToPublic(appKeyPrivateKey); 264 | wallet = { privateKey: appKeyPrivateKey, publicKey: appKeyPublicKey }; 265 | } 266 | 267 | return wallet; 268 | } 269 | 270 | /* PRIVATE / UTILITY METHODS */ 271 | 272 | /** 273 | * Sets appropriate properties for the keyring based on the given 274 | * BIP39-compliant mnemonic. 275 | * 276 | * @param {string|Array|Buffer} mnemonic - A seed phrase represented 277 | * as a string, an array of UTF-8 bytes, or a Buffer. Mnemonic input 278 | * passed as type buffer or array of UTF-8 bytes must be NFKD normalized. 279 | */ 280 | _initFromMnemonic(mnemonic) { 281 | if (this.root) { 282 | throw new Error( 283 | 'Eth-Hd-Keyring: Secret recovery phrase already provided', 284 | ); 285 | } 286 | 287 | this.mnemonic = this._mnemonicToUint8Array(mnemonic); 288 | 289 | // validate before initializing 290 | const isValid = bip39.validateMnemonic(this.mnemonic, wordlist); 291 | if (!isValid) { 292 | throw new Error( 293 | 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', 294 | ); 295 | } 296 | 297 | // eslint-disable-next-line node/no-sync 298 | const seed = bip39.mnemonicToSeedSync(this.mnemonic, wordlist); 299 | this.hdWallet = HDKey.fromMasterSeed(seed); 300 | this.root = this.hdWallet.derive(this.hdPath); 301 | } 302 | 303 | // small helper function to convert publicKey in Uint8Array form to a publicAddress as a hex 304 | _addressfromPublicKey(publicKey) { 305 | return bufferToHex( 306 | publicToAddress(Buffer.from(publicKey), true), 307 | ).toLowerCase(); 308 | } 309 | } 310 | 311 | HdKeyring.type = type; 312 | module.exports = HdKeyring; 313 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | normalize, 3 | personalSign, 4 | recoverPersonalSignature, 5 | recoverTypedSignature, 6 | signTypedData, 7 | SignTypedDataVersion, 8 | encrypt, 9 | } = require('@metamask/eth-sig-util'); 10 | const { wordlist } = require('@metamask/scure-bip39/dist/wordlists/english'); 11 | const oldMMForkBIP39 = require('@metamask/bip39'); 12 | const { 13 | isValidAddress, 14 | bufferToHex, 15 | toBuffer, 16 | ecrecover, 17 | pubToAddress, 18 | } = require('@ethereumjs/util'); 19 | const { 20 | TransactionFactory, 21 | Transaction: EthereumTx, 22 | } = require('@ethereumjs/tx'); 23 | 24 | const OldHdKeyring = require('@metamask/eth-hd-keyring'); 25 | const { keccak256 } = require('ethereum-cryptography/keccak'); 26 | const HdKeyring = require('..'); 27 | 28 | // Sample account: 29 | const privKeyHex = 30 | 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'; 31 | 32 | const sampleMnemonic = 33 | 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango'; 34 | const firstAcct = '0x1c96099350f13d558464ec79b9be4445aa0ef579'; 35 | const secondAcct = '0x1b00aed43a693f3a957f9feb5cc08afa031e37a0'; 36 | 37 | const notKeyringAddress = '0xbD20F6F5F1616947a39E11926E78ec94817B3931'; 38 | 39 | describe('hd-keyring', () => { 40 | describe('compare old bip39 implementation with new', () => { 41 | it('should derive the same accounts from the same mnemonics', async () => { 42 | const mnemonics = []; 43 | for (let i = 0; i < 99; i++) { 44 | mnemonics.push(oldMMForkBIP39.generateMnemonic()); 45 | } 46 | 47 | await Promise.all( 48 | mnemonics.map(async (mnemonic) => { 49 | const newHDKeyring = new HdKeyring({ mnemonic, numberOfAccounts: 3 }); 50 | const oldHDKeyring = new OldHdKeyring({ 51 | mnemonic, 52 | numberOfAccounts: 3, 53 | }); 54 | const newAccounts = await newHDKeyring.getAccounts(); 55 | const oldAccounts = await oldHDKeyring.getAccounts(); 56 | await expect(newAccounts[0]).toStrictEqual(oldAccounts[0]); 57 | 58 | await expect(newAccounts[1]).toStrictEqual(oldAccounts[1]); 59 | 60 | await expect(newAccounts[2]).toStrictEqual(oldAccounts[2]); 61 | }), 62 | ); 63 | }); 64 | }); 65 | 66 | describe('constructor', () => { 67 | it('constructs with a typeof string mnemonic', async () => { 68 | const keyring = new HdKeyring({ 69 | mnemonic: sampleMnemonic, 70 | numberOfAccounts: 2, 71 | }); 72 | 73 | const accounts = await keyring.getAccounts(); 74 | expect(accounts[0]).toStrictEqual(firstAcct); 75 | expect(accounts[1]).toStrictEqual(secondAcct); 76 | }); 77 | 78 | it('constructs with a typeof buffer mnemonic', async () => { 79 | const keyring = new HdKeyring({ 80 | mnemonic: Buffer.from(sampleMnemonic, 'utf8'), 81 | numberOfAccounts: 2, 82 | }); 83 | 84 | const accounts = await keyring.getAccounts(); 85 | expect(accounts[0]).toStrictEqual(firstAcct); 86 | expect(accounts[1]).toStrictEqual(secondAcct); 87 | }); 88 | 89 | it('constructs with a typeof Uint8Array mnemonic', async () => { 90 | const indices = sampleMnemonic 91 | .split(' ') 92 | .map((word) => wordlist.indexOf(word)); 93 | const uInt8ArrayOfMnemonic = new Uint8Array( 94 | new Uint16Array(indices).buffer, 95 | ); 96 | const keyring = new HdKeyring({ 97 | mnemonic: uInt8ArrayOfMnemonic, 98 | numberOfAccounts: 2, 99 | }); 100 | 101 | const accounts = await keyring.getAccounts(); 102 | expect(accounts[0]).toStrictEqual(firstAcct); 103 | expect(accounts[1]).toStrictEqual(secondAcct); 104 | }); 105 | 106 | it('throws on invalid mnemonic', () => { 107 | expect( 108 | () => 109 | new HdKeyring({ 110 | mnemonic: 'abc xyz', 111 | numberOfAccounts: 2, 112 | }), 113 | ).toThrow('Eth-Hd-Keyring: Invalid secret recovery phrase provided'); 114 | }); 115 | 116 | it('throws when numberOfAccounts is passed with no mnemonic', () => { 117 | expect( 118 | () => 119 | new HdKeyring({ 120 | numberOfAccounts: 2, 121 | }), 122 | ).toThrow( 123 | 'Eth-Hd-Keyring: Deserialize method cannot be called with an opts value for numberOfAccounts and no menmonic', 124 | ); 125 | }); 126 | }); 127 | 128 | describe('re-initialization protection', () => { 129 | const alreadyProvidedError = 130 | 'Eth-Hd-Keyring: Secret recovery phrase already provided'; 131 | it('double generateRandomMnemonic', () => { 132 | const keyring = new HdKeyring(); 133 | keyring.generateRandomMnemonic(); 134 | expect(() => { 135 | keyring.generateRandomMnemonic(); 136 | }).toThrow(alreadyProvidedError); 137 | }); 138 | 139 | it('constructor + generateRandomMnemonic', () => { 140 | const keyring = new HdKeyring({ 141 | mnemonic: sampleMnemonic, 142 | numberOfAccounts: 2, 143 | }); 144 | 145 | expect(() => { 146 | keyring.generateRandomMnemonic(); 147 | }).toThrow(alreadyProvidedError); 148 | }); 149 | 150 | it('constructor + deserialize', () => { 151 | const keyring = new HdKeyring({ 152 | mnemonic: sampleMnemonic, 153 | numberOfAccounts: 2, 154 | }); 155 | 156 | expect(() => { 157 | keyring.deserialize({ 158 | mnemonic: sampleMnemonic, 159 | numberOfAccounts: 1, 160 | }); 161 | }).toThrow(alreadyProvidedError); 162 | }); 163 | }); 164 | 165 | describe('Keyring.type', () => { 166 | it('is a class property that returns the type string.', () => { 167 | const { type } = HdKeyring; 168 | expect(typeof type).toBe('string'); 169 | }); 170 | }); 171 | 172 | describe('#type', () => { 173 | it('returns the correct value', () => { 174 | const keyring = new HdKeyring(); 175 | 176 | const { type } = keyring; 177 | const correct = HdKeyring.type; 178 | expect(type).toStrictEqual(correct); 179 | }); 180 | }); 181 | 182 | describe('#serialize mnemonic.', () => { 183 | it('serializes the mnemonic in the same format as previous version (an array of utf8 encoded bytes)', async () => { 184 | const keyring = new HdKeyring({ 185 | mnemonic: sampleMnemonic, 186 | }); 187 | // uses previous version of eth-hd-keyring to ensure backwards compatibility 188 | const oldHDKeyring = new OldHdKeyring({ mnemonic: sampleMnemonic }); 189 | const { mnemonic: oldKeyringSerializedMnemonic } = 190 | await oldHDKeyring.serialize(); 191 | 192 | const output = await keyring.serialize(); 193 | expect(output.mnemonic).toStrictEqual(oldKeyringSerializedMnemonic); 194 | }); 195 | 196 | it('serializes mnemonic passed in as a string to an array of utf8 encoded bytes', async () => { 197 | const keyring = new HdKeyring({ 198 | mnemonic: sampleMnemonic, 199 | }); 200 | const output = await keyring.serialize(); 201 | // this Buffer.from(...).toString() is the method of converting from an array of utf8 encoded bytes back to a string 202 | const mnemonicAsString = Buffer.from(output.mnemonic).toString(); 203 | expect(mnemonicAsString).toStrictEqual(sampleMnemonic); 204 | }); 205 | 206 | it('serializes mnemonic passed in as a an array of utf8 encoded bytes in the same format', async () => { 207 | const uint8Array = new TextEncoder('utf-8').encode(sampleMnemonic); 208 | const mnemonicAsArrayOfUtf8EncodedBytes = Array.from(uint8Array); 209 | const keyring = new HdKeyring({ 210 | mnemonic: mnemonicAsArrayOfUtf8EncodedBytes, 211 | }); 212 | 213 | const output = await keyring.serialize(); 214 | // this Buffer.from(...).toString() is the method of converting from an array of utf8 encoded bytes back to a string 215 | const mnemonicAsString = Buffer.from(output.mnemonic).toString(); 216 | expect(mnemonicAsString).toStrictEqual(sampleMnemonic); 217 | }); 218 | }); 219 | 220 | describe('#deserialize a private key', () => { 221 | it('serializes what it deserializes', async () => { 222 | const keyring = new HdKeyring(); 223 | await keyring.deserialize({ 224 | mnemonic: sampleMnemonic, 225 | numberOfAccounts: 1, 226 | }); 227 | const accountsFirstCheck = await keyring.getAccounts(); 228 | 229 | expect(accountsFirstCheck).toHaveLength(1); 230 | await keyring.addAccounts(1); 231 | const accountsSecondCheck = await keyring.getAccounts(); 232 | expect(accountsSecondCheck[0]).toStrictEqual(firstAcct); 233 | expect(accountsSecondCheck[1]).toStrictEqual(secondAcct); 234 | expect(accountsSecondCheck).toHaveLength(2); 235 | const serialized = await keyring.serialize(); 236 | expect(Buffer.from(serialized.mnemonic).toString()).toStrictEqual( 237 | sampleMnemonic, 238 | ); 239 | }); 240 | }); 241 | 242 | describe('#addAccounts', () => { 243 | describe('with no arguments', () => { 244 | it('creates a single wallet', async () => { 245 | const keyring = new HdKeyring(); 246 | keyring.generateRandomMnemonic(); 247 | await keyring.addAccounts(); 248 | const accounts = await keyring.getAccounts(); 249 | expect(accounts).toHaveLength(1); 250 | }); 251 | 252 | it('throws an error when no SRP has been generated yet', async () => { 253 | const keyring = new HdKeyring(); 254 | expect(() => keyring.addAccounts()).toThrow( 255 | 'Eth-Hd-Keyring: No secret recovery phrase provided', 256 | ); 257 | }); 258 | }); 259 | 260 | describe('with a numeric argument', () => { 261 | it('creates that number of wallets', async () => { 262 | const keyring = new HdKeyring(); 263 | keyring.generateRandomMnemonic(); 264 | await keyring.addAccounts(3); 265 | const accounts = await keyring.getAccounts(); 266 | expect(accounts).toHaveLength(3); 267 | }); 268 | }); 269 | }); 270 | 271 | describe('#signPersonalMessage', () => { 272 | it('returns the expected value', async () => { 273 | const keyring = new HdKeyring(); 274 | 275 | const address = firstAcct; 276 | const message = '0x68656c6c6f20776f726c64'; 277 | 278 | await keyring.deserialize({ 279 | mnemonic: sampleMnemonic, 280 | numberOfAccounts: 1, 281 | }); 282 | const signature = await keyring.signPersonalMessage(address, message); 283 | expect(signature).not.toBe(message); 284 | 285 | const restored = recoverPersonalSignature({ 286 | data: message, 287 | signature, 288 | }); 289 | 290 | expect(restored).toStrictEqual(normalize(address)); 291 | }); 292 | }); 293 | 294 | describe('#signTypedData', () => { 295 | it('can recover a basic signature', async () => { 296 | const keyring = new HdKeyring(); 297 | Buffer.from(privKeyHex, 'hex'); 298 | const typedData = [ 299 | { 300 | type: 'string', 301 | name: 'message', 302 | value: 'Hi, Alice!', 303 | }, 304 | ]; 305 | keyring.generateRandomMnemonic(); 306 | await keyring.addAccounts(1); 307 | const addresses = await keyring.getAccounts(); 308 | const address = addresses[0]; 309 | const signature = await keyring.signTypedData(address, typedData); 310 | const restored = recoverTypedSignature({ 311 | data: typedData, 312 | signature, 313 | version: SignTypedDataVersion.V1, 314 | }); 315 | expect(restored).toStrictEqual(address); 316 | }); 317 | }); 318 | 319 | describe('#signTypedData_v1', () => { 320 | const typedData = [ 321 | { 322 | type: 'string', 323 | name: 'message', 324 | value: 'Hi, Alice!', 325 | }, 326 | ]; 327 | 328 | it('signs in a compliant and recoverable way', async () => { 329 | const keyring = new HdKeyring(); 330 | keyring.generateRandomMnemonic(); 331 | await keyring.addAccounts(1); 332 | const addresses = await keyring.getAccounts(); 333 | const address = addresses[0]; 334 | const signature = await keyring.signTypedData(address, typedData, { 335 | version: SignTypedDataVersion.V1, 336 | }); 337 | const restored = recoverTypedSignature({ 338 | data: typedData, 339 | signature, 340 | version: SignTypedDataVersion.V1, 341 | }); 342 | expect(restored).toStrictEqual(address); 343 | }); 344 | }); 345 | 346 | describe('#signTypedData_v3', () => { 347 | it('signs in a compliant and recoverable way', async () => { 348 | const keyring = new HdKeyring(); 349 | const typedData = { 350 | types: { 351 | EIP712Domain: [], 352 | }, 353 | domain: {}, 354 | primaryType: 'EIP712Domain', 355 | message: {}, 356 | }; 357 | 358 | await keyring.deserialize({ 359 | mnemonic: sampleMnemonic, 360 | numberOfAccounts: 1, 361 | }); 362 | const addresses = await keyring.getAccounts(); 363 | const address = addresses[0]; 364 | const signature = await keyring.signTypedData(address, typedData, { 365 | version: SignTypedDataVersion.V3, 366 | }); 367 | const restored = recoverTypedSignature({ 368 | data: typedData, 369 | signature, 370 | version: SignTypedDataVersion.V3, 371 | }); 372 | expect(restored).toStrictEqual(address); 373 | }); 374 | }); 375 | 376 | describe('#signTypedData_v3 signature verification', () => { 377 | it('signs in a recoverable way.', async () => { 378 | const keyring = new HdKeyring(); 379 | const typedData = { 380 | types: { 381 | EIP712Domain: [ 382 | { name: 'name', type: 'string' }, 383 | { name: 'version', type: 'string' }, 384 | { name: 'chainId', type: 'uint256' }, 385 | { name: 'verifyingContract', type: 'address' }, 386 | ], 387 | Person: [ 388 | { name: 'name', type: 'string' }, 389 | { name: 'wallet', type: 'address' }, 390 | ], 391 | Mail: [ 392 | { name: 'from', type: 'Person' }, 393 | { name: 'to', type: 'Person' }, 394 | { name: 'contents', type: 'string' }, 395 | ], 396 | }, 397 | primaryType: 'Mail', 398 | domain: { 399 | name: 'Ether Mail', 400 | version: '1', 401 | chainId: 1, 402 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 403 | }, 404 | message: { 405 | from: { 406 | name: 'Cow', 407 | wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 408 | }, 409 | to: { 410 | name: 'Bob', 411 | wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 412 | }, 413 | contents: 'Hello, Bob!', 414 | }, 415 | }; 416 | 417 | keyring.generateRandomMnemonic(); 418 | await keyring.addAccounts(1); 419 | const addresses = await keyring.getAccounts(); 420 | const address = addresses[0]; 421 | const signature = await keyring.signTypedData(address, typedData, { 422 | version: SignTypedDataVersion.V3, 423 | }); 424 | const restored = recoverTypedSignature({ 425 | data: typedData, 426 | signature, 427 | version: SignTypedDataVersion.V3, 428 | }); 429 | expect(restored).toStrictEqual(address); 430 | }); 431 | }); 432 | 433 | describe('custom hd paths', () => { 434 | it('can deserialize with an hdPath param and generate the same accounts.', async () => { 435 | const keyring = new HdKeyring(); 436 | const hdPathString = `m/44'/60'/0'/0`; 437 | keyring.deserialize({ 438 | mnemonic: sampleMnemonic, 439 | numberOfAccounts: 1, 440 | hdPath: hdPathString, 441 | }); 442 | const addresses = await keyring.getAccounts(); 443 | expect(addresses[0]).toStrictEqual(firstAcct); 444 | const serialized = await keyring.serialize(); 445 | expect(serialized.hdPath).toStrictEqual(hdPathString); 446 | }); 447 | 448 | it('can deserialize with an hdPath param and generate different accounts.', async () => { 449 | const keyring = new HdKeyring(); 450 | const hdPathString = `m/44'/60'/0'/1`; 451 | keyring.deserialize({ 452 | mnemonic: sampleMnemonic, 453 | numberOfAccounts: 1, 454 | hdPath: hdPathString, 455 | }); 456 | const addresses = await keyring.getAccounts(); 457 | expect(addresses[0]).not.toBe(firstAcct); 458 | const serialized = await keyring.serialize(); 459 | expect(serialized.hdPath).toStrictEqual(hdPathString); 460 | }); 461 | }); 462 | 463 | // eslint-disable-next-line 464 | /* 465 | describe('create and restore 1k accounts', function () { 466 | it('should restore same accounts with no problem', async function () { 467 | this.timeout(20000) 468 | 469 | for (let i = 0; i < 1e3; i++) { 470 | 471 | const keyring = new HdKeyring({ 472 | numberOfAccounts: 1, 473 | }) 474 | const originalAccounts = await keyring.getAccounts() 475 | const serialized = await keyring.serialize() 476 | const mnemonic = serialized.mnemonic 477 | 478 | const keyring = new HdKeyring({ 479 | numberOfAccounts: 1, 480 | mnemonic, 481 | }) 482 | const restoredAccounts = await keyring.getAccounts() 483 | 484 | const first = originalAccounts[0] 485 | const restored = restoredAccounts[0] 486 | const msg = `Should restore same account from mnemonic: "${mnemonic}"` 487 | assert.equal(restoredAccounts[0], originalAccounts[0], msg) 488 | 489 | } 490 | 491 | return true 492 | }) 493 | }) 494 | */ 495 | 496 | describe('signing methods withAppKeyOrigin option', () => { 497 | it('should signPersonalMessage with the expected key when passed a withAppKeyOrigin', async () => { 498 | const keyring = new HdKeyring(); 499 | const address = firstAcct; 500 | const message = '0x68656c6c6f20776f726c64'; 501 | 502 | const privateKey = Buffer.from( 503 | '8e82d2d74c50e5c8460f771d38a560ebe1151a9134c65a7e92b28ad0cfae7151', 504 | 'hex', 505 | ); 506 | const expectedSig = personalSign({ privateKey, data: message }); 507 | 508 | await keyring.deserialize({ 509 | mnemonic: sampleMnemonic, 510 | numberOfAccounts: 1, 511 | }); 512 | const sig = await keyring.signPersonalMessage(address, message, { 513 | withAppKeyOrigin: 'someapp.origin.io', 514 | }); 515 | 516 | expect(sig).toStrictEqual(expectedSig); 517 | }); 518 | 519 | it('should signTypedData with the expected key when passed a withAppKeyOrigin', async () => { 520 | const keyring = new HdKeyring(); 521 | const address = firstAcct; 522 | const typedData = { 523 | types: { 524 | EIP712Domain: [], 525 | }, 526 | domain: {}, 527 | primaryType: 'EIP712Domain', 528 | message: {}, 529 | }; 530 | 531 | const privateKey = Buffer.from( 532 | '8e82d2d74c50e5c8460f771d38a560ebe1151a9134c65a7e92b28ad0cfae7151', 533 | 'hex', 534 | ); 535 | const expectedSig = signTypedData({ 536 | privateKey, 537 | data: typedData, 538 | version: SignTypedDataVersion.V3, 539 | }); 540 | 541 | await keyring.deserialize({ 542 | mnemonic: sampleMnemonic, 543 | numberOfAccounts: 1, 544 | }); 545 | 546 | const sig = await keyring.signTypedData(address, typedData, { 547 | withAppKeyOrigin: 'someapp.origin.io', 548 | version: SignTypedDataVersion.V3, 549 | }); 550 | expect(sig).toStrictEqual(expectedSig); 551 | }); 552 | }); 553 | 554 | // / 555 | /* TESTS FOR BASE-KEYRING METHODS */ 556 | // / 557 | 558 | describe('#signMessage', function () { 559 | const message = 560 | '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0'; 561 | const expectedResult = 562 | '0xb21867b2221db0172e970b7370825b71c57823ff8714168ce9748f32f450e2c43d0fe396eb5b5f59284b7fd108c8cf61a6180a6756bdd3d4b7b9ccc4ac6d51611b'; 563 | 564 | it('passes the dennis test', async function () { 565 | const keyring = new HdKeyring(); 566 | await keyring.deserialize({ 567 | mnemonic: sampleMnemonic, 568 | numberOfAccounts: 1, 569 | }); 570 | const result = await keyring.signMessage(firstAcct, message); 571 | expect(result).toBe(expectedResult); 572 | }); 573 | 574 | it('reliably can decode messages it signs', async function () { 575 | const keyring = new HdKeyring(); 576 | await keyring.deserialize({ 577 | mnemonic: sampleMnemonic, 578 | numberOfAccounts: 1, 579 | }); 580 | const localMessage = 'hello there!'; 581 | const msgHashHex = bufferToHex( 582 | Buffer.from(keccak256(Buffer.from(localMessage))), 583 | ); 584 | await keyring.addAccounts(9); 585 | const addresses = await keyring.getAccounts(); 586 | const signatures = await Promise.all( 587 | addresses.map(async (accountAddress) => { 588 | return await keyring.signMessage(accountAddress, msgHashHex); 589 | }), 590 | ); 591 | signatures.forEach((sgn, index) => { 592 | const accountAddress = addresses[index]; 593 | 594 | const r = toBuffer(sgn.slice(0, 66)); 595 | const s = toBuffer(`0x${sgn.slice(66, 130)}`); 596 | const v = BigInt(`0x${sgn.slice(130, 132)}`); 597 | const m = toBuffer(msgHashHex); 598 | const pub = ecrecover(m, v, r, s); 599 | const adr = `0x${pubToAddress(pub).toString('hex')}`; 600 | 601 | expect(adr).toBe(accountAddress); 602 | }); 603 | }); 604 | 605 | it('throw error for invalid message', async function () { 606 | const keyring = new HdKeyring(); 607 | await keyring.deserialize({ 608 | mnemonic: sampleMnemonic, 609 | numberOfAccounts: 1, 610 | }); 611 | 612 | await expect(keyring.signMessage(firstAcct, '')).rejects.toThrow( 613 | 'Value must be a hexadecimal string', 614 | ); 615 | }); 616 | 617 | it('throw error if empty address is passed', async function () { 618 | const keyring = new HdKeyring(); 619 | await keyring.deserialize({ 620 | mnemonic: sampleMnemonic, 621 | numberOfAccounts: 1, 622 | }); 623 | 624 | await expect(keyring.signMessage('', message)).rejects.toThrow( 625 | 'Must specify address.', 626 | ); 627 | }); 628 | 629 | it('throw error if address not associated with the current keyring is passed', async function () { 630 | const keyring = new HdKeyring(); 631 | await keyring.deserialize({ 632 | mnemonic: sampleMnemonic, 633 | numberOfAccounts: 1, 634 | }); 635 | 636 | await expect( 637 | keyring.signMessage(notKeyringAddress, message), 638 | ).rejects.toThrow('HD Keyring - Unable to find matching address.'); 639 | }); 640 | }); 641 | 642 | describe('#removeAccount', function () { 643 | let keyring; 644 | beforeEach(() => { 645 | keyring = new HdKeyring({ 646 | mnemonic: sampleMnemonic, 647 | numberOfAccounts: 1, 648 | }); 649 | }); 650 | 651 | describe('if the account exists', function () { 652 | it('should remove that account', async function () { 653 | const addresses = await keyring.getAccounts(); 654 | expect(addresses).toHaveLength(1); 655 | keyring.removeAccount(addresses[0]); 656 | const addressesAfterRemoval = await keyring.getAccounts(); 657 | expect(addressesAfterRemoval).toHaveLength(0); 658 | }); 659 | }); 660 | 661 | describe('if the account does not exist', function () { 662 | it('should throw an error', function () { 663 | const unexistingAccount = '0x0000000000000000000000000000000000000000'; 664 | expect(() => keyring.removeAccount(unexistingAccount)).toThrow( 665 | `Address ${unexistingAccount} not found in this keyring`, 666 | ); 667 | }); 668 | }); 669 | }); 670 | 671 | describe('getAppKeyAddress', function () { 672 | let keyring; 673 | beforeEach(() => { 674 | keyring = new HdKeyring({ 675 | mnemonic: sampleMnemonic, 676 | numberOfAccounts: 1, 677 | }); 678 | }); 679 | 680 | it('should return a public address custom to the provided app key origin', async function () { 681 | const appKeyAddress = await keyring.getAppKeyAddress( 682 | firstAcct, 683 | 'someapp.origin.io', 684 | ); 685 | 686 | expect(firstAcct).not.toBe(appKeyAddress); 687 | expect(isValidAddress(appKeyAddress)).toBe(true); 688 | }); 689 | 690 | it('should return different addresses when provided different app key origins', async function () { 691 | const appKeyAddress1 = await keyring.getAppKeyAddress( 692 | firstAcct, 693 | 'someapp.origin.io', 694 | ); 695 | 696 | expect(isValidAddress(appKeyAddress1)).toBe(true); 697 | 698 | const appKeyAddress2 = await keyring.getAppKeyAddress( 699 | firstAcct, 700 | 'anotherapp.origin.io', 701 | ); 702 | 703 | expect(isValidAddress(appKeyAddress2)).toBe(true); 704 | expect(appKeyAddress1).not.toBe(appKeyAddress2); 705 | }); 706 | 707 | it('should return the same address when called multiple times with the same params', async function () { 708 | const appKeyAddress1 = await keyring.getAppKeyAddress( 709 | firstAcct, 710 | 'someapp.origin.io', 711 | ); 712 | 713 | expect(isValidAddress(appKeyAddress1)).toBe(true); 714 | 715 | const appKeyAddress2 = await keyring.getAppKeyAddress( 716 | firstAcct, 717 | 'someapp.origin.io', 718 | ); 719 | 720 | expect(isValidAddress(appKeyAddress2)).toBe(true); 721 | expect(appKeyAddress1).toBe(appKeyAddress2); 722 | }); 723 | 724 | it('should throw error if the provided origin is not a string', async function () { 725 | await expect(keyring.getAppKeyAddress(firstAcct, [])).rejects.toThrow( 726 | `'origin' must be a non-empty string`, 727 | ); 728 | }); 729 | 730 | it('should throw error if the provided origin is an empty string', async function () { 731 | await expect(keyring.getAppKeyAddress(firstAcct, '')).rejects.toThrow( 732 | `'origin' must be a non-empty string`, 733 | ); 734 | }); 735 | }); 736 | 737 | describe('exportAccount', function () { 738 | let keyring; 739 | beforeEach(() => { 740 | keyring = new HdKeyring({ 741 | mnemonic: sampleMnemonic, 742 | numberOfAccounts: 1, 743 | }); 744 | }); 745 | 746 | it('should return a hex-encoded private key', async function () { 747 | const expectedPrivateKeyResult = 748 | '0xd3cc16948a02a91b9fcf83735653bf3dfd82c86543fdd1e9a828bd25e8a7b68d'; 749 | const privKeyHexValue = await keyring.exportAccount(firstAcct); 750 | 751 | expect(expectedPrivateKeyResult).toBe(`0x${privKeyHexValue}`); 752 | }); 753 | 754 | it('throw error if account is not present', async function () { 755 | await expect(keyring.exportAccount(notKeyringAddress)).rejects.toThrow( 756 | 'HD Keyring - Unable to find matching address.', 757 | ); 758 | }); 759 | }); 760 | 761 | describe('#encryptionPublicKey', function () { 762 | const publicKey = 'LV7lWhd0mUDcvxkMU2o6uKXftu25zq4bMYdmMqppXic='; 763 | let keyring; 764 | beforeEach(() => { 765 | keyring = new HdKeyring({ 766 | mnemonic: sampleMnemonic, 767 | numberOfAccounts: 1, 768 | }); 769 | }); 770 | 771 | it('returns the expected value', async function () { 772 | const encryptionPublicKey = await keyring.getEncryptionPublicKey( 773 | firstAcct, 774 | ); 775 | expect(publicKey).toBe(encryptionPublicKey); 776 | }); 777 | 778 | it('throw error if address is blank', async function () { 779 | await expect(keyring.getEncryptionPublicKey('')).rejects.toThrow( 780 | 'Must specify address.', 781 | ); 782 | }); 783 | 784 | it('throw error if address is not present in the keyring', async function () { 785 | await expect( 786 | keyring.getEncryptionPublicKey(notKeyringAddress), 787 | ).rejects.toThrow('HD Keyring - Unable to find matching address.'); 788 | }); 789 | }); 790 | 791 | describe('#signTypedData V4 signature verification', function () { 792 | let keyring; 793 | beforeEach(() => { 794 | keyring = new HdKeyring({ 795 | mnemonic: sampleMnemonic, 796 | numberOfAccounts: 1, 797 | }); 798 | }); 799 | 800 | const expectedSignature = 801 | '0x220917664ef676d592bd709a5bffedaf69c5f6c72f13c6c4547a41d211f0923c3180893b1dec023433f11b664fabda22b74b57d21094f7798fc85b7650f8edbb1b'; 802 | 803 | it('returns the expected value', async function () { 804 | const typedData = { 805 | types: { 806 | EIP712Domain: [ 807 | { name: 'name', type: 'string' }, 808 | { name: 'version', type: 'string' }, 809 | { name: 'chainId', type: 'uint256' }, 810 | { name: 'verifyingContract', type: 'address' }, 811 | ], 812 | Person: [ 813 | { name: 'name', type: 'string' }, 814 | { name: 'wallets', type: 'address[]' }, 815 | ], 816 | Mail: [ 817 | { name: 'from', type: 'Person' }, 818 | { name: 'to', type: 'Person[]' }, 819 | { name: 'contents', type: 'string' }, 820 | ], 821 | Group: [ 822 | { name: 'name', type: 'string' }, 823 | { name: 'members', type: 'Person[]' }, 824 | ], 825 | }, 826 | domain: { 827 | name: 'Ether Mail', 828 | version: '1', 829 | chainId: 1, 830 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 831 | }, 832 | primaryType: 'Mail', 833 | message: { 834 | from: { 835 | name: 'Cow', 836 | wallets: [ 837 | '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 838 | '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF', 839 | ], 840 | }, 841 | to: [ 842 | { 843 | name: 'Bob', 844 | wallets: [ 845 | '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 846 | '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57', 847 | '0xB0B0b0b0b0b0B000000000000000000000000000', 848 | ], 849 | }, 850 | ], 851 | contents: 'Hello, Bob!', 852 | }, 853 | }; 854 | 855 | const addresses = await keyring.getAccounts(); 856 | const [address] = addresses; 857 | 858 | const signature = await keyring.signTypedData(address, typedData, { 859 | version: 'V4', 860 | }); 861 | expect(signature).toBe(expectedSignature); 862 | const restored = recoverTypedSignature({ 863 | data: typedData, 864 | signature, 865 | version: SignTypedDataVersion.V4, 866 | }); 867 | expect(restored).toBe(address); 868 | }); 869 | }); 870 | 871 | describe('#decryptMessage', function () { 872 | const message = 'Hello world!'; 873 | let encryptedMessage, keyring; 874 | 875 | beforeEach(async () => { 876 | keyring = new HdKeyring({ 877 | mnemonic: sampleMnemonic, 878 | numberOfAccounts: 1, 879 | }); 880 | 881 | const encryptionPublicKey = await keyring.getEncryptionPublicKey( 882 | firstAcct, 883 | ); 884 | encryptedMessage = encrypt({ 885 | publicKey: encryptionPublicKey, 886 | data: message, 887 | version: 'x25519-xsalsa20-poly1305', 888 | }); 889 | }); 890 | 891 | it('returns the expected value', async function () { 892 | const decryptedMessage = await keyring.decryptMessage( 893 | firstAcct, 894 | encryptedMessage, 895 | ); 896 | expect(message).toBe(decryptedMessage); 897 | }); 898 | 899 | it('throw error if address passed is not present in the keyring', async function () { 900 | await expect( 901 | keyring.decryptMessage(notKeyringAddress, encryptedMessage), 902 | ).rejects.toThrow('HD Keyring - Unable to find matching address.'); 903 | }); 904 | 905 | it('throw error if wrong encrypted data object is passed', async function () { 906 | await expect(keyring.decryptMessage(firstAcct, {})).rejects.toThrow( 907 | 'Encryption type/version not supported.', 908 | ); 909 | }); 910 | }); 911 | 912 | describe('#signTransaction', function () { 913 | let keyring; 914 | beforeEach(() => { 915 | keyring = new HdKeyring({ 916 | mnemonic: sampleMnemonic, 917 | numberOfAccounts: 1, 918 | }); 919 | }); 920 | 921 | const txParams = { 922 | from: firstAcct, 923 | nonce: '0x00', 924 | gasPrice: '0x09184e72a000', 925 | gasLimit: '0x2710', 926 | to: firstAcct, 927 | value: '0x1000', 928 | }; 929 | 930 | it('returns a signed legacy tx object', async function () { 931 | const tx = new EthereumTx(txParams); 932 | expect(tx.isSigned()).toBe(false); 933 | 934 | const signed = await keyring.signTransaction(firstAcct, tx); 935 | expect(signed.isSigned()).toBe(true); 936 | }); 937 | 938 | it('returns a signed tx object', async function () { 939 | const tx = TransactionFactory.fromTxData(txParams); 940 | expect(tx.isSigned()).toBe(false); 941 | 942 | const signed = await keyring.signTransaction(firstAcct, tx); 943 | expect(signed.isSigned()).toBe(true); 944 | }); 945 | 946 | it('returns rejected promise if empty address is passed', async function () { 947 | const tx = TransactionFactory.fromTxData(txParams); 948 | await expect(keyring.signTransaction('', tx)).rejects.toThrow( 949 | 'Must specify address.', 950 | ); 951 | }); 952 | 953 | it('throw error if wrong address is passed', async function () { 954 | const tx = TransactionFactory.fromTxData(txParams); 955 | await expect( 956 | keyring.signTransaction(notKeyringAddress, tx), 957 | ).rejects.toThrow('HD Keyring - Unable to find matching address.'); 958 | }); 959 | }); 960 | }); 961 | --------------------------------------------------------------------------------