├── .depcheckrc.json ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build-lint-test.yml │ ├── create-release-pr.yml │ ├── main.yml │ ├── publish-docs.yml │ ├── publish-main-docs.yml │ ├── publish-rc-docs.yml │ ├── publish-release.yml │ └── security-code-scanner.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-allow-scripts.cjs │ │ └── plugin-constraints.cjs └── releases │ └── yarn-3.2.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── constraints.pro ├── docs └── keyring.md ├── jest.config.js ├── package.json ├── scripts ├── get.sh └── prepack.sh ├── src ├── KeyringController.test.ts ├── KeyringController.ts ├── constants.ts ├── index.ts ├── readable-stream.d.ts ├── test │ ├── encryptor.mock.ts │ ├── index.ts │ ├── keyring.mock.ts │ └── transaction.mock.ts ├── types.ts ├── types │ ├── @metamask │ │ ├── eth-hd-keyring.d.ts │ │ └── eth-simple-keyring.d.ts │ └── obs-store.d.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json ├── typedoc.json └── yarn.lock /.depcheckrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignores": [ 3 | "@lavamoat/allow-scripts", 4 | "@lavamoat/preinstall-always-fail", 5 | "@metamask/auto-changelog", 6 | "@types/*", 7 | "prettier-plugin-packagejson", 8 | "ts-node", 9 | "typedoc" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | extends: ['@metamask/eslint-config'], 5 | 6 | overrides: [ 7 | { 8 | files: ['*.ts'], 9 | extends: ['@metamask/eslint-config-typescript'], 10 | }, 11 | 12 | { 13 | files: ['*.js'], 14 | parserOptions: { 15 | sourceType: 'script', 16 | }, 17 | extends: ['@metamask/eslint-config-nodejs'], 18 | }, 19 | 20 | { 21 | files: ['*.test.ts', '*.test.js'], 22 | extends: [ 23 | '@metamask/eslint-config-jest', 24 | '@metamask/eslint-config-nodejs', 25 | ], 26 | }, 27 | 28 | { 29 | files: ['*.d.ts'], 30 | rules: { 31 | 'import/unambiguous': 'off', 32 | }, 33 | }, 34 | ], 35 | 36 | ignorePatterns: [ 37 | '!.eslintrc.js', 38 | '!.prettierrc.js', 39 | 'dist/', 40 | 'docs/', 41 | '.yarn/', 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | yarn.lock linguist-generated=false 4 | 5 | # yarn v3 6 | # See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 7 | /.yarn/releases/** binary 8 | /.yarn/plugins/** binary 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @MetaMask/devs 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'npm' 7 | directory: '/' 8 | schedule: 9 | interval: 'daily' 10 | time: '06:00' 11 | allow: 12 | - dependency-name: '@metamask/*' 13 | target-branch: 'main' 14 | versioning-strategy: 'increase-if-necessary' 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 10 | 11 | ## Changes 12 | 13 | 27 | 28 | - ****: Your change here 29 | - ****: Your change here 30 | - ****: Your change here 31 | 32 | ## References 33 | 34 | 40 | 41 | ## Checklist 42 | 43 | - [ ] I've updated the test suite for new or updated code as appropriate 44 | - [ ] I've updated documentation for new or updated code as appropriate (note: this will usually be JSDoc) 45 | - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate 46 | -------------------------------------------------------------------------------- /.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 | build: 21 | name: Build 22 | runs-on: ubuntu-latest 23 | needs: 24 | - prepare 25 | strategy: 26 | matrix: 27 | node-version: [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 build 37 | - name: Require clean working directory 38 | shell: bash 39 | run: | 40 | if ! git diff --exit-code; then 41 | echo "Working tree dirty at end of job" 42 | exit 1 43 | fi 44 | 45 | lint: 46 | name: Lint 47 | runs-on: ubuntu-latest 48 | needs: 49 | - prepare 50 | strategy: 51 | matrix: 52 | node-version: [18.x, 20.x] 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: Use Node.js ${{ matrix.node-version }} 56 | uses: actions/setup-node@v3 57 | with: 58 | node-version: ${{ matrix.node-version }} 59 | cache: 'yarn' 60 | - run: yarn --immutable --immutable-cache 61 | - run: yarn lint 62 | - name: Validate RC changelog 63 | if: ${{ startsWith(github.head_ref, 'release/') }} 64 | run: yarn auto-changelog validate --rc 65 | - name: Validate changelog 66 | if: ${{ !startsWith(github.head_ref, 'release/') }} 67 | run: yarn auto-changelog validate 68 | - name: Require clean working directory 69 | shell: bash 70 | run: | 71 | if ! git diff --exit-code; then 72 | echo "Working tree dirty at end of job" 73 | exit 1 74 | fi 75 | 76 | test: 77 | name: Test 78 | runs-on: ubuntu-latest 79 | needs: 80 | - prepare 81 | strategy: 82 | matrix: 83 | node-version: [18.x, 20.x] 84 | steps: 85 | - uses: actions/checkout@v3 86 | - name: Use Node.js ${{ matrix.node-version }} 87 | uses: actions/setup-node@v3 88 | with: 89 | node-version: ${{ matrix.node-version }} 90 | cache: 'yarn' 91 | - run: yarn --immutable --immutable-cache 92 | - run: yarn test 93 | - name: Require clean working directory 94 | shell: bash 95 | run: | 96 | if ! git diff --exit-code; then 97 | echo "Working tree dirty at end of job" 98 | exit 1 99 | fi 100 | -------------------------------------------------------------------------------- /.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, or patch. 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.23 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 | 52 | is-release: 53 | # Filtering by `push` events ensures that we only release from the `main` branch, which is a 54 | # requirement for our npm publishing environment. 55 | # The commit author should always be 'github-actions' for releases created by the 56 | # 'create-release-pr' workflow, so we filter by that as well to prevent accidentally 57 | # triggering a release. 58 | if: github.event_name == 'push' && startsWith(github.event.head_commit.author.name, 'github-actions') 59 | needs: all-jobs-pass 60 | outputs: 61 | IS_RELEASE: ${{ steps.is-release.outputs.IS_RELEASE }} 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: MetaMask/action-is-release@v1 65 | id: is-release 66 | 67 | publish-release: 68 | needs: is-release 69 | if: needs.is-release.outputs.IS_RELEASE == 'true' 70 | name: Publish release 71 | permissions: 72 | contents: write 73 | uses: ./.github/workflows/publish-release.yml 74 | secrets: 75 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 76 | PUBLISH_DOCS_TOKEN: ${{ secrets.PUBLISH_DOCS_TOKEN }} 77 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 78 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs to GitHub Pages 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | destination_dir: 7 | required: true 8 | type: string 9 | secrets: 10 | PUBLISH_DOCS_TOKEN: 11 | required: true 12 | 13 | jobs: 14 | publish-docs-to-gh-pages: 15 | name: Publish docs to GitHub Pages 16 | runs-on: ubuntu-latest 17 | environment: github-pages 18 | permissions: 19 | contents: write 20 | steps: 21 | - name: Ensure `destination_dir` is not empty 22 | if: ${{ inputs.destination_dir == '' }} 23 | run: exit 1 24 | - name: Checkout the repository 25 | uses: actions/checkout@v3 26 | - name: Use Node.js 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version-file: '.nvmrc' 30 | cache: 'yarn' 31 | - name: Install npm dependencies 32 | run: yarn --immutable 33 | - name: Run build script 34 | run: yarn build:docs 35 | - name: Deploy to `${{ inputs.destination_dir }}` directory of `gh-pages` branch 36 | uses: peaceiris/actions-gh-pages@de7ea6f8efb354206b205ef54722213d99067935 37 | with: 38 | # This `PUBLISH_DOCS_TOKEN` needs to be manually set per-repository. 39 | # Look in the repository settings under "Environments", and set this token in the `github-pages` environment. 40 | personal_token: ${{ secrets.PUBLISH_DOCS_TOKEN }} 41 | publish_dir: ./docs 42 | destination_dir: ${{ inputs.destination_dir }} 43 | -------------------------------------------------------------------------------- /.github/workflows/publish-main-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish main branch docs to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: main 6 | 7 | jobs: 8 | publish-to-gh-pages: 9 | name: Publish docs to `staging` directory of `gh-pages` branch 10 | permissions: 11 | contents: write 12 | uses: ./.github/workflows/publish-docs.yml 13 | with: 14 | destination_dir: staging 15 | secrets: 16 | PUBLISH_DOCS_TOKEN: ${{ secrets.PUBLISH_DOCS_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-rc-docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish release candidate docs to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 'release/**' 6 | 7 | jobs: 8 | get-release-version: 9 | name: Get release version 10 | runs-on: ubuntu-latest 11 | outputs: 12 | release-version: ${{ steps.release-name.outputs.RELEASE_VERSION }} 13 | steps: 14 | - name: Extract release version from branch name 15 | id: release-name 16 | run: | 17 | BRANCH_NAME='${{ github.ref_name }}' 18 | echo "RELEASE_VERSION=v${BRANCH_NAME#release/}" >> "$GITHUB_OUTPUT" 19 | publish-to-gh-pages: 20 | name: Publish docs to `rc-${{ needs.get-release-version.outputs.release-version }}` directory of `gh-pages` branch 21 | permissions: 22 | contents: write 23 | uses: ./.github/workflows/publish-docs.yml 24 | needs: get-release-version 25 | with: 26 | destination_dir: rc-${{ needs.get-release-version.outputs.release-version }} 27 | secrets: 28 | PUBLISH_DOCS_TOKEN: ${{ secrets.PUBLISH_DOCS_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | NPM_TOKEN: 7 | required: true 8 | SLACK_WEBHOOK_URL: 9 | required: true 10 | PUBLISH_DOCS_TOKEN: 11 | required: true 12 | 13 | jobs: 14 | publish-release: 15 | permissions: 16 | contents: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | ref: ${{ github.sha }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version-file: '.nvmrc' 26 | - uses: MetaMask/action-publish-release@v2 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Install 30 | run: | 31 | yarn install 32 | yarn build 33 | - uses: actions/cache@v3 34 | id: restore-build 35 | with: 36 | path: | 37 | ./dist 38 | ./node_modules/.yarn-state.yml 39 | key: ${{ github.sha }} 40 | 41 | publish-npm-dry-run: 42 | runs-on: ubuntu-latest 43 | needs: publish-release 44 | steps: 45 | - uses: actions/checkout@v3 46 | with: 47 | ref: ${{ github.sha }} 48 | - uses: actions/cache@v3 49 | id: restore-build 50 | with: 51 | path: | 52 | ./dist 53 | ./node_modules/.yarn-state.yml 54 | key: ${{ github.sha }} 55 | - name: Dry Run Publish 56 | # omit npm-token token to perform dry run publish 57 | uses: MetaMask/action-npm-publish@v4 58 | with: 59 | slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} 60 | subteam: S042S7RE4AE # @metamask-npm-publishers 61 | env: 62 | SKIP_PREPACK: true 63 | 64 | publish-npm: 65 | environment: npm-publish 66 | runs-on: ubuntu-latest 67 | needs: publish-npm-dry-run 68 | steps: 69 | - uses: actions/checkout@v3 70 | with: 71 | ref: ${{ github.sha }} 72 | - uses: actions/cache@v3 73 | id: restore-build 74 | with: 75 | path: | 76 | ./dist 77 | ./node_modules/.yarn-state.yml 78 | key: ${{ github.sha }} 79 | - name: Publish 80 | uses: MetaMask/action-npm-publish@v2 81 | with: 82 | # This `NPM_TOKEN` needs to be manually set per-repository. 83 | # Look in the repository settings under "Environments", and set this token in the `npm-publish` environment. 84 | npm-token: ${{ secrets.NPM_TOKEN }} 85 | env: 86 | SKIP_PREPACK: true 87 | 88 | get-release-version: 89 | runs-on: ubuntu-latest 90 | needs: publish-npm 91 | outputs: 92 | RELEASE_VERSION: ${{ steps.get-release-version.outputs.RELEASE_VERSION }} 93 | steps: 94 | - uses: actions/checkout@v3 95 | with: 96 | ref: ${{ github.sha }} 97 | - id: get-release-version 98 | shell: bash 99 | run: ./scripts/get.sh ".version" "RELEASE_VERSION" 100 | 101 | publish-release-to-gh-pages: 102 | needs: get-release-version 103 | name: Publish docs to `${{ needs.get-release-version.outputs.RELEASE_VERSION }}` directory of `gh-pages` branch 104 | permissions: 105 | contents: write 106 | uses: ./.github/workflows/publish-docs.yml 107 | with: 108 | destination_dir: ${{ needs.get-release-version.outputs.RELEASE_VERSION }} 109 | secrets: 110 | PUBLISH_DOCS_TOKEN: ${{ secrets.PUBLISH_DOCS_TOKEN }} 111 | 112 | publish-release-to-latest-gh-pages: 113 | needs: publish-npm 114 | name: Publish docs to `latest` directory of `gh-pages` branch 115 | permissions: 116 | contents: write 117 | uses: ./.github/workflows/publish-docs.yml 118 | with: 119 | destination_dir: latest 120 | secrets: 121 | PUBLISH_DOCS_TOKEN: ${{ secrets.PUBLISH_DOCS_TOKEN }} 122 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | coverage/ 4 | docs/ 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | 39 | # TypeScript cache 40 | *.tsbuildinfo 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Microbundle cache 49 | .rpt2_cache/ 50 | .rts2_cache_cjs/ 51 | .rts2_cache_es/ 52 | .rts2_cache_umd/ 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | .env.test 66 | 67 | # Stores VSCode versions used for testing VSCode extensions 68 | .vscode-test 69 | 70 | # yarn v3 (w/o zero-install) 71 | # See: https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 72 | .pnp.* 73 | .yarn/* 74 | !.yarn/patches 75 | !.yarn/plugins 76 | !.yarn/releases 77 | !.yarn/sdks 78 | !.yarn/versions 79 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | }; 10 | -------------------------------------------------------------------------------- /.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 | - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs 15 | spec: "@yarnpkg/plugin-constraints" 16 | 17 | yarnPath: .yarn/releases/yarn-3.2.1.cjs 18 | -------------------------------------------------------------------------------- /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 | ## [17.0.1] 10 | ### Changed 11 | - Bump `@metamask/keyring-api` to ^3.0.0 ([#344](https://github.com/MetaMask/KeyringController/pull/344)) 12 | 13 | ## [17.0.0] 14 | ### Changed 15 | - **BREAKING**: Unify `createNewVaultAndKeychain` and `createNewVaultAndRestore` into new method `createNewVaultWithKeyring`. `createNewVaultWithKeyring` accepts a `password` and a `keyring` object provided by the client and returns the `KeyringControllerState` ([#329](https://github.com/MetaMask/KeyringController/pull/329)) 16 | - Bump @metamask/utils from 8.2.1 to 8.3.0 ([#335](https://github.com/MetaMask/KeyringController/pull/335)) 17 | 18 | ## [16.0.0] 19 | ### Changed 20 | - **BREAKING**: Bump `@metamask/obs-store` from `^8.1.0` to `^9.0.0` ([#306](https://github.com/MetaMask/KeyringController/pull/306)) 21 | - The `store` and `memStore` properties now expose streams confirming to `readable-stream@^3.6.2` 22 | - **BREAKING**: Replace `GenericEncryptor.updateVault` with `GenericEncryptor.isVaultUpdated` ([#310](https://github.com/MetaMask/KeyringController/pull/310))) 23 | - Bump `@metamask/browser-passworder` from `^4.2.0` to `^4.3.0` ([#310](https://github.com/MetaMask/KeyringController/pull/310) [#311](https://github.com/MetaMask/KeyringController/pull/311)) 24 | 25 | ### Fixed 26 | - Prefer cached `encryptionKey` over password when possible ([#307](https://github.com/MetaMask/KeyringController/pull/307)) 27 | 28 | ## [15.1.0] 29 | ### Added 30 | - Added optional `isVaultUpdated` property to `GenericEncryptor` type ([#312](https://github.com/MetaMask/KeyringController/pull/312)) 31 | 32 | ### Changed 33 | - Bump `@metamask/browser-passworder` to `^4.3.0` ([#312](https://github.com/MetaMask/KeyringController/pull/312)) 34 | 35 | ### Removed 36 | - Removed `updateVault` optional property from `GenericEncryptor` type ([#312](https://github.com/MetaMask/KeyringController/pull/312)) 37 | 38 | ### Fixed 39 | - Improved encryption performance when the controller is constructed with `cacheEncryptionKey: true` ([#312](https://github.com/MetaMask/KeyringController/pull/312)) 40 | 41 | ## [15.0.0] 42 | ### Changed 43 | - **BREAKING** Removed `encryptor` class variable ([#293](https://github.com/MetaMask/KeyringController/pull/293)) 44 | - **BREAKING** Removed `cacheEncryptionKey` class variable ([#293](https://github.com/MetaMask/KeyringController/pull/293)) 45 | - **BREAKING** Changed `encryptor` constructor option property type to `GenericEncryptor | ExportableKeyEncryptor | undefined` ([#293](https://github.com/MetaMask/KeyringController/pull/293)) 46 | - When the controller is instantiated with `cacheEncryptionKey = true`, the `encryptor` type is restricted to `ExportableKeyEncryptor | undefined` 47 | 48 | ## [14.0.1] 49 | ### Fixed 50 | - Fix `removeAccount` to await the account removal in order to account for Snaps keyrings ([#280](https://github.com/MetaMask/KeyringController/pull/280)) 51 | - Bump `@metamask/eth-simple-keyring` from `^6.0.0` to `^6.0.1` ([#287](https://github.com/MetaMask/KeyringController/pull/287)) 52 | 53 | ## [14.0.0] 54 | ### Changed 55 | - **BREAKING:** Bump `@metamask/eth-sig-util` from `^6.0.0` to `^7.0.0` ([#269](https://github.com/MetaMask/KeyringController/pull/269)) 56 | - **BREAKING:** Bump `@metamask/eth-hd-keyring` from `^6.0.0` to `^7.0.1` ([#275](https://github.com/MetaMask/KeyringController/pull/275)) 57 | - **BREAKING:** Bump `@metamask/eth-simple-keyring` from `^5.0.0` to `^6.0.0` ([#273](https://github.com/MetaMask/KeyringController/pull/273)) 58 | 59 | ## [13.0.1] 60 | ### Changed 61 | - Bump `@metamask/utils` from `^6.2.0` to `^8.1.0` ([#261](https://github.com/MetaMask/KeyringController/pull/261)) 62 | 63 | ## [13.0.0] 64 | ### Added 65 | - Added `KeyringControllerPersistentState` type which includes only persistent state, an optional string property with key `vault` ([#247](https://github.com/MetaMask/KeyringController/pull/247)) 66 | - Added `KeyringObject` type for how keyrings are represented in `memStore` ([#247](https://github.com/MetaMask/KeyringController/pull/247)) 67 | 68 | ### Changed 69 | - **BREAKING**: Add types for store and fix type discrepancies ([#247](https://github.com/MetaMask/KeyringController/pull/247)) 70 | - **BREAKING**: Constructor parameter `KeyringControllerArgs` fields changed ([#247](https://github.com/MetaMask/KeyringController/pull/247)): 71 | - **BREAKING**: type of `store` and `memStore` public properties changed ([#247](https://github.com/MetaMask/KeyringController/pull/247)): 72 | - `KeyringController.store` is now an `ObservableStore` 73 | - `KeyringController.memStore` is now an `ObservableStore` 74 | - **BREAKING**: `updateMemStoreKeyrings` method return type changed to `Promise` ([#247](https://github.com/MetaMask/KeyringController/pull/247)) 75 | - **BREAKING**: `KeyringControllerState` type changed to include only non-persistent state ([#247](https://github.com/MetaMask/KeyringController/pull/247)): 76 | - Now `undefined` is used instead of `null` when `encryptionKey` and `encryptionSalt` are unset 77 | - `keyrings` is now of type `KeyringObject[]` instead of `Keyring` 78 | - `password`, `store`, `memStore` have been removed - note that in practice this change only affects types 79 | - This changes cause the following methods also to change the return type: 80 | - `createNewVaultAndKeychain` 81 | - `createNewVaultAndRestore` 82 | - `setLocked` 83 | - `submitPassword` 84 | - `submitEncryptionKey` 85 | - `addNewAccount` 86 | - `removeAccount` 87 | - `fullUpdate` 88 | - **BREAKING**: When constructing a simple keyring with `addNewKeyring`, the second parameter (`opts`) is now expected to be an array of private keys rather than an object with a `privateKeys` property ([#253](https://github.com/MetaMask/KeyringController/pull/253)) 89 | - Restored support for keyrings with non-object serialized state ([#253](https://github.com/MetaMask/KeyringController/pull/253)) 90 | - Narrow return type of `signTypedMessage` and encryption methods ([#249](https://github.com/MetaMask/KeyringController/pull/249)) 91 | - The methods `signTypedMessage`, `getEncryptionPublicKey`, and `decryptMessage` now return `string` rather than `Bytes` 92 | 93 | ### Fixed 94 | - Fix `signTypedMessage` parameter types ([#250](https://github.com/MetaMask/KeyringController/pull/250)) 95 | - Restore compatibility with QR Keyring ([#252](https://github.com/MetaMask/KeyringController/pull/252)) 96 | - An empty object is no longer used as a default when deserialized state was not provided to the `addNewKeyring` method. This default empty object was breaking the QR keyring. 97 | 98 | 99 | ## [12.0.1] 100 | ### Fixed 101 | - Improved error handling when calling `getKeyringForAccount` with empty or invalid address ([#238](https://github.com/MetaMask/KeyringController/pull/238)) 102 | 103 | ## [12.0.0] 104 | ### Changed 105 | - **BREAKING**: Update `@metamask/eth-sig-util` to version `^6` ([#235](https://github.com/MetaMask/KeyringController/pull/235)) 106 | - `signPersonalMessage` now normalizes `msgParams.data` in a different way for `0` and empty strings inputs. `0` will be normalized to `0x00` and empty strings to `0x` 107 | - **BREAKING**: Update Node.js min version to 16.0.0 ([#236](https://github.com/MetaMask/KeyringController/pull/236)) 108 | - Update `@metamask/utils` package ([#234](https://github.com/MetaMask/KeyringController/pull/234)) 109 | - Destroy keyrings on reference drop ([#233](https://github.com/MetaMask/KeyringController/pull/233)) 110 | 111 | ## [11.0.0] 112 | ### Changed 113 | - **BREAKING**: The addNewKeyring method now expects an object containing the property `privateKeys` of type `string[]` in case the supplied keyring is a "Simple Keyring". ([#202](https://github.com/MetaMask/KeyringController/pull/202)), ([#228](https://github.com/MetaMask/KeyringController/pull/228)) 114 | - Migrate the project to TypeScript ([#202](https://github.com/MetaMask/KeyringController/pull/202)) 115 | - Methods that started with an underscore are now `#` private methods 116 | - Additional validation has been added to most methods 117 | - deps: Unpin and bump @metamask/eth-sig-utils@5.0.2->^5.1.0 ([#224](https://github.com/MetaMask/KeyringController/pull/224)) 118 | 119 | ## [10.0.1] 120 | ### Fixed 121 | - Save encryption salt when `persistAllKeyrings` yields a new encryption key ([#203](https://github.com/MetaMask/KeyringController/pull/203)) 122 | 123 | ## [10.0.0] 124 | ### Changed 125 | - **BREAKING:** Update module name to use `@metamask` scope ([#187](https://github.com/MetaMask/KeyringController/pull/187)) 126 | - Consumers will now need to import this package as `@metamask/eth-keyring-controller` 127 | - **BREAKING:** @metamask/eth-hd-keyring to v6.0.0 ([#193](https://github.com/MetaMask/KeyringController/pull/193)) 128 | - Reverts the serialization format of mnemonics on HDKeyrings from `Uint8Arrays` back to an untyped array of UTF8 encoded bytes, which was the format prior to v9.0.0 of this package. 129 | 130 | ## [9.0.0] [DEPRECATED] 131 | ### Added 132 | - Add support for keyring `init` method ([#163](https://github.com/MetaMask/KeyringController/pull/163)). 133 | - If a keyring has an `init` method, it will be called automatically upon construction. It is called with `await`, so it can be asynchronous. 134 | 135 | ### Changed 136 | - **BREAKING:** Replace constructor option and public property `keyringTypes` with `keyringBuilders` ([#163](https://github.com/MetaMask/KeyringController/pull/163)). 137 | - The constructor now takes keyring builder functions rather than classes. Each builder function should return a keyring instance when called, and it must have a `type` string property set to the keyring type name. See the newly exported `keyringBuilderFactory` function for an example. The builder functions must be synchronous; use an `init` method for asynchronous initialization steps. 138 | - **BREAKING:** `KeyringController` is now a named export instead of a default export ([#163](https://github.com/MetaMask/KeyringController/pull/163)). 139 | - **BREAKING:** Update `@metamask/eth-simple-keyring` from v4 to v5 ([#171](https://github.com/MetaMask/KeyringController/pull/171)). 140 | - This keyring type is included as a default. If you are using this keyring API directly, see [the `@metamask/eth-simple-keyring` release notes](https://github.com/MetaMask/eth-simple-keyring/releases/tag/v5.0.0) for details on required changes. 141 | - **BREAKING:** Replace `getKeyringClassForType` method with `getKeyringBuilderForType` ([#163](https://github.com/MetaMask/KeyringController/pull/163)). 142 | - **BREAKING:** Update `@metamask/eth-hd-keyring` to v5 ([#177](https://github.com/MetaMask/KeyringController/pull/177)) 143 | - This keyring type is included as a default. If you are using this keyring API directly, see [the `@metamask/eth-hd-keyring` release notes](https://github.com/MetaMask/eth-hd-keyring/releases/tag/v5.0.0) for details on required changes. 144 | - **BREAKING:** Require support for ES2020 ([#177](https://github.com/MetaMask/KeyringController/pull/177), [#180](https://github.com/MetaMask/KeyringController/pull/180)) 145 | - As a result of some dependency updates made in this release, this package now requires ES2020 support. If using this package in an environment that does not support ES2020 completely, consider investigating these two dependency changes and transpiling any packages using ES2020 syntax. 146 | - Update `@metamask/eth-sig-util` to v5 ([#180](https://github.com/MetaMask/KeyringController/pull/180)) 147 | - Update minimum supported version of `@metamask/browser-passworder` from v4.0.1 to v4.0.2 ([#182](https://github.com/MetaMask/KeyringController/pull/182)) 148 | - Remove `bip39` dependency ([#179](https://github.com/MetaMask/KeyringController/pull/179)) 149 | 150 | ### Fixed 151 | - Fix support for asynchronous `addAccounts` HD Keyring method ([#176](https://github.com/MetaMask/KeyringController/pull/176)) 152 | - This method was asynchronous, but was called synchronously. Currently the method does not do anything asynchronous so this should have no functional impact, but this ensures any future errors or asynchronous steps added to that method work correctly in the future. 153 | 154 | ## [8.1.0] 155 | ### Changed 156 | - Allow deserializing vaults with unrecognized keyrings ([#169](https://github.com/MetaMask/KeyringController/pull/169)) 157 | - When deserializing a vault with an unrecognized keyring, the controller will no longer crash. The unrecognized keyring vault data will be preserved in the vault for future use, but will otherwise be ignored. 158 | 159 | ## [8.0.1] 160 | ### Fixed 161 | - Restore full state return value ([#161](https://github.com/MetaMask/KeyringController/pull/161)) 162 | - Some methods were accidentally changed in v8.0.0 to return nothing, where previously they returned the full KeyringController state. 163 | - The affected methods were: 164 | - `createNewVaultAndKeychain` 165 | - `submitPassword` 166 | - `submitEncryptionKey` 167 | - `addNewAccount` 168 | - `removeAccount` 169 | - They now all return the full state, just as they did in earlier versions. 170 | 171 | ## [8.0.0] [DEPRECATED] 172 | ### Added 173 | - Allow login with encryption key rather than password ([#152](https://github.com/MetaMask/KeyringController/pull/152)) 174 | - This is required to support MetaMask extension builds using manifest v3. 175 | - This is enabled via the option `cacheEncryptionKey`. 176 | - The encryption key and salt have been added to the `memStore` as `encryptionKey` and `encryptionSalt`. The salt is used to verify that the key matches the vault being decrypted. 177 | - If the `cacheEncryptionKey` option is enabled, the encryption key and salt get cached in the `memStore` whenever the password is submitted. 178 | - The encryption key can be submitted with the new method `submitEncryptionKey`. 179 | - The `unlockKeyrings` method now accepts additional parameters for the encryption key and salt, though we don't recommend using this method directly. 180 | 181 | ### Changed 182 | - **BREAKING:** Update minimum Node.js version to v14 ([#146](https://github.com/MetaMask/KeyringController/pull/146)) 183 | - **BREAKING:**: Remove password parameter from `persistAllKeyrings` and `createFirstKeyTree` ([#154](https://github.com/MetaMask/KeyringController/pull/154)) 184 | - The password or encryption key must now be set already before these method are called. It is set by `createNewVaultAndKeychain`, `createNewVaultAndRestore`, and `submitPassword`/`submitEncryptionKey`. 185 | - This change was made to reduce redundant state changes. 186 | 187 | ### Fixed 188 | - Fix a typo in the duplicate account import error ([#153](https://github.com/MetaMask/KeyringController/pull/153)) 189 | 190 | ## [7.0.2] 191 | ### Fixed 192 | - `createNewVaultAndRestore` now accepts a seedphrase formatted as an array of numbers ([#138](https://github.com/MetaMask/KeyringController/pull/138)) 193 | 194 | ## [7.0.1] 195 | ### Fixed 196 | - Fix breaking change in `addNewKeyring` function that was accidentally introduced in v7.0.0 ([#136](https://github.com/MetaMask/KeyringController/pull/136)) 197 | - We updated the method such that keyrings were always constructed with constructor arguments, defaulting to an empty object if none were provided. But some keyrings ([such as the QR Keyring](https://github.com/KeystoneHQ/keystone-airgaped-base/blob/c5e2d06892118265ec2ee613b543095276d5b208/packages/base-eth-keyring/src/BaseKeyring.ts#L290)) relied upon the options being undefined in some cases. 198 | 199 | ## [7.0.0] 200 | ### Added 201 | - Add forget Keyring method for some hardware devices ([#124](https://github.com/MetaMask/KeyringController/pull/124)) 202 | - Add `@lavamoat/allow-scripts` ([#109](https://github.com/MetaMask/KeyringController/pull/109)) 203 | 204 | ### Changed 205 | - **BREAKING**: Bump eth-hd-keyring to latest version ([#132](https://github.com/MetaMask/KeyringController/pull/132)) 206 | - When calling the `addNewKeyring` method, an options object can no longer be passed containing a `numberOfAccounts` property without also including a `mnemonic`. Not adding any option argument will result in the generation of a new mnemonic and the addition of 1 account derived from that mnemonic to the keyring. 207 | - When calling `createNewVaultAndKeychain` all keyrings are cleared first thing ([#129](https://github.com/MetaMask/KeyringController/pull/129)) 208 | - Validate user imported seedphrase across all bip39 wordlists ([#77](https://github.com/MetaMask/KeyringController/pull/77)) 209 | 210 | 211 | [Unreleased]: https://github.com/MetaMask/KeyringController/compare/v17.0.1...HEAD 212 | [17.0.1]: https://github.com/MetaMask/KeyringController/compare/v17.0.0...v17.0.1 213 | [17.0.0]: https://github.com/MetaMask/KeyringController/compare/v16.0.0...v17.0.0 214 | [16.0.0]: https://github.com/MetaMask/KeyringController/compare/v15.1.0...v16.0.0 215 | [15.1.0]: https://github.com/MetaMask/KeyringController/compare/v15.0.0...v15.1.0 216 | [15.0.0]: https://github.com/MetaMask/KeyringController/compare/v14.0.1...v15.0.0 217 | [14.0.1]: https://github.com/MetaMask/KeyringController/compare/v14.0.0...v14.0.1 218 | [14.0.0]: https://github.com/MetaMask/KeyringController/compare/v13.0.1...v14.0.0 219 | [13.0.1]: https://github.com/MetaMask/KeyringController/compare/v13.0.0...v13.0.1 220 | [13.0.0]: https://github.com/MetaMask/KeyringController/compare/v12.0.1...v13.0.0 221 | [12.0.1]: https://github.com/MetaMask/KeyringController/compare/v12.0.0...v12.0.1 222 | [12.0.0]: https://github.com/MetaMask/KeyringController/compare/v11.0.0...v12.0.0 223 | [11.0.0]: https://github.com/MetaMask/KeyringController/compare/v10.0.1...v11.0.0 224 | [10.0.1]: https://github.com/MetaMask/KeyringController/compare/v10.0.0...v10.0.1 225 | [10.0.0]: https://github.com/MetaMask/KeyringController/compare/v9.0.0...v10.0.0 226 | [9.0.0]: https://github.com/MetaMask/KeyringController/compare/v8.1.0...v9.0.0 227 | [8.1.0]: https://github.com/MetaMask/KeyringController/compare/v8.0.1...v8.1.0 228 | [8.0.1]: https://github.com/MetaMask/KeyringController/compare/v8.0.0...v8.0.1 229 | [8.0.0]: https://github.com/MetaMask/KeyringController/compare/v7.0.2...v8.0.0 230 | [7.0.2]: https://github.com/MetaMask/KeyringController/compare/v7.0.1...v7.0.2 231 | [7.0.1]: https://github.com/MetaMask/KeyringController/compare/v7.0.0...v7.0.1 232 | [7.0.0]: https://github.com/MetaMask/KeyringController/releases/tag/v7.0.0 233 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eth Keyring Controller 2 | 3 | > [!WARNING] 4 | > This package has been merged into [`@metamask/keyring-controller`](https://github.com/MetaMask/core/tree/main/packages/keyring-controller). 5 | > This repository is no longer in use, and pull requests will no longer be accepted. 6 | 7 | A module for managing groups of Ethereum accounts called "Keyrings", defined originally for MetaMask's multiple-account-type feature. 8 | 9 | To add new account types to a `KeyringController`, just make sure it follows [The Keyring Class Protocol](./docs/keyring.md). 10 | 11 | The KeyringController has three main responsibilities: 12 | 13 | - Initializing & using (signing with) groups of Ethereum accounts ("keyrings"). 14 | - Keeping track of local nicknames for those individual accounts. 15 | - Providing password-encryption persisting & restoring of secret information. 16 | 17 | ## Installation 18 | 19 | `yarn add @metamask/eth-keyring-controller` 20 | 21 | This library uses the Node.js `events` API. If you are using this library outside of a Node.js context, ensure that you have a polyfill for the `events` API (this is built-in to `browserify`). 22 | 23 | ## Usage 24 | 25 | ```javascript 26 | const { KeyringController } = require('@metamask/eth-keyring-controller'); 27 | const SimpleKeyring = require('@metamask/eth-simple-keyring'); 28 | 29 | const keyringController = new KeyringController({ 30 | keyringTypes: [SimpleKeyring], // optional array of types to support. 31 | initState: initState.KeyringController, // Last emitted persisted state. 32 | encryptor: { 33 | // An optional object for defining encryption schemes: 34 | // Defaults to Browser-native SubtleCrypto. 35 | encrypt(password, object) { 36 | return new Promise('encrypted!'); 37 | }, 38 | decrypt(password, encryptedString) { 39 | return new Promise({ foo: 'bar' }); 40 | }, 41 | }, 42 | }); 43 | 44 | // The KeyringController is also an event emitter: 45 | this.keyringController.on('newAccount', (address) => { 46 | console.log(`New account created: ${address}`); 47 | }); 48 | this.keyringController.on('removedAccount', handleThat); 49 | ``` 50 | 51 | ## Methods 52 | 53 | Currently the methods are heavily commented in [the source code](./src/KeyringController.ts), so it's the best place to look until we aggregate it here as well. 54 | 55 | ## Contributing 56 | 57 | ### Setup 58 | 59 | - Install [Node.js](https://nodejs.org) version 18 60 | - If you are using [nvm](https://github.com/creationix/nvm#installation) (recommended) running `nvm use` will automatically choose the right node version for you. 61 | - Install [Yarn v3](https://yarnpkg.com/getting-started/install) 62 | - Run `yarn install` to install dependencies and run any required post-install scripts 63 | 64 | ### Testing and Linting 65 | 66 | Run `yarn test` to run the tests once. 67 | 68 | Run `yarn lint` to run the linter, or run `yarn lint:fix` to run the linter and fix any automatically fixable issues. 69 | 70 | ### Release & Publishing 71 | 72 | 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. 73 | 74 | 1. Choose a release version. 75 | 76 | - 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. 77 | 78 | 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). 79 | 80 | - 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. 81 | 82 | 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. 83 | 84 | - 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). 85 | - This should trigger the [`action-create-release-pr`](https://github.com/MetaMask/action-create-release-pr) workflow to create the release PR. 86 | 87 | 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. 88 | 89 | - 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.). 90 | - Try to explain each change in terms that users of the package would understand (e.g. avoid referencing internal variables/concepts). 91 | - Consolidate related changes into one change entry if it makes it easier to explain. 92 | - Run `yarn auto-changelog validate --rc` to check that the changelog is correctly formatted. 93 | 94 | 5. Review and QA the release. 95 | 96 | - 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. 97 | 98 | 6. Squash & Merge the release. 99 | 100 | - 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. 101 | 102 | 7. Publish the release on npm. 103 | 104 | - Wait for the `publish-release` GitHub Action workflow to finish. This should trigger a second job (`publish-npm`), which will wait for a run approval by the [`npm publishers`](https://github.com/orgs/MetaMask/teams/npm-publishers) team. 105 | - Approve the `publish-npm` job (or ask somebody on the npm publishers team to approve it for you). 106 | - Once the `publish-npm` job has finished, check npm to verify that it has been published. 107 | -------------------------------------------------------------------------------- /constraints.pro: -------------------------------------------------------------------------------- 1 | %=============================================================================== 2 | % Utility predicates 3 | %=============================================================================== 4 | 5 | % True if RepoName can be unified with the repository name part of RepoUrl, a 6 | % complete URL for a repository on GitHub. This URL must include the ".git" 7 | % extension. 8 | repo_name(RepoUrl, RepoName) :- 9 | Prefix = 'https://github.com/MetaMask/', 10 | atom_length(Prefix, PrefixLength), 11 | Suffix = '.git', 12 | atom_length(Suffix, SuffixLength), 13 | atom_length(RepoUrl, RepoUrlLength), 14 | sub_atom(RepoUrl, 0, PrefixLength, After, Prefix), 15 | sub_atom(RepoUrl, Before, SuffixLength, 0, Suffix), 16 | Start is RepoUrlLength - After + 1, 17 | End is Before + 1, 18 | RepoNameLength is End - Start, 19 | sub_atom(RepoUrl, PrefixLength, RepoNameLength, SuffixLength, RepoName). 20 | 21 | %=============================================================================== 22 | % Constraints 23 | %=============================================================================== 24 | 25 | % The package must have a name. 26 | \+ gen_enforced_field(WorkspaceCwd, 'name', null). 27 | 28 | % The package must have a description. 29 | \+ gen_enforced_field(WorkspaceCwd, 'description', null). 30 | % The description cannot end with a period. 31 | gen_enforced_field(WorkspaceCwd, 'description', DescriptionWithoutTrailingPeriod) :- 32 | workspace_field(WorkspaceCwd, 'description', Description), 33 | atom_length(Description, Length), 34 | LengthLessOne is Length - 1, 35 | sub_atom(Description, LengthLessOne, 1, 0, LastCharacter), 36 | sub_atom(Description, 0, LengthLessOne, 1, DescriptionWithoutPossibleTrailingPeriod), 37 | ( 38 | LastCharacter == '.' -> 39 | DescriptionWithoutTrailingPeriod = DescriptionWithoutPossibleTrailingPeriod ; 40 | DescriptionWithoutTrailingPeriod = Description 41 | ). 42 | 43 | % The homepage of the package must match its name (which is in turn based on its 44 | % workspace directory name). 45 | gen_enforced_field(WorkspaceCwd, 'homepage', CorrectHomepageUrl) :- 46 | workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), 47 | repo_name(RepoUrl, RepoName), 48 | atomic_list_concat(['https://github.com/MetaMask/', RepoName, '#readme'], CorrectHomepageUrl). 49 | 50 | % The bugs URL of the package must point to the Issues page for the repository. 51 | gen_enforced_field(WorkspaceCwd, 'bugs.url', CorrectBugsUrl) :- 52 | \+ workspace_field(WorkspaceCwd, 'private', true), 53 | workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), 54 | repo_name(RepoUrl, RepoName), 55 | atomic_list_concat(['https://github.com/MetaMask/', RepoName, '/issues'], CorrectBugsUrl). 56 | 57 | % The package must specify Git as the repository type. 58 | gen_enforced_field(WorkspaceCwd, 'repository.type', 'git'). 59 | 60 | % The package must match the URL of a repo within the MetaMask organization. 61 | gen_enforced_field(WorkspaceCwd, 'repository.url', 'https://github.com/MetaMask/.git') :- 62 | workspace_field(WorkspaceCwd, 'repository.url', RepoUrl), 63 | \+ repo_name(RepoUrl, _). 64 | 65 | % The license for the package must be specified. 66 | gen_enforced_field(WorkspaceCwd, 'license'). 67 | 68 | % The entrypoint for the package must be `./dist/index.js`. 69 | gen_enforced_field(WorkspaceCwd, 'main', './dist/index.js'). 70 | 71 | % The type definitions entrypoint the package must be `./dist/index.d.ts`. 72 | gen_enforced_field(WorkspaceCwd, 'types', './dist/index.d.ts'). 73 | 74 | % The list of files included in the package must only include files generated 75 | % during the build step. 76 | gen_enforced_field(WorkspaceCwd, 'files', ['dist/']). 77 | 78 | % If a dependency is listed under "dependencies", it should not be listed under 79 | % "devDependencies". 80 | gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, DependencyType) :- 81 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, 'dependencies'), 82 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType), 83 | DependencyType == 'devDependencies'. 84 | 85 | % The package must specify a minimum Node version of ^18.18 || >=20. 86 | gen_enforced_field(WorkspaceCwd, 'engines.node', '^18.18 || >=20'). 87 | 88 | % The package is public. 89 | gen_enforced_field(WorkspaceCwd, 'publishConfig.access', 'public'). 90 | % The package is available on the NPM registry. 91 | gen_enforced_field(WorkspaceCwd, 'publishConfig.registry', 'https://registry.npmjs.org/'). 92 | -------------------------------------------------------------------------------- /docs/keyring.md: -------------------------------------------------------------------------------- 1 | ## The Keyring Class Protocol 2 | 3 | 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. 4 | 5 | ### Keyring.type 6 | 7 | A class property that returns a unique string describing the Keyring. 8 | This is the only class property or method, the remaining methods are instance methods. 9 | 10 | ### constructor( options ) 11 | 12 | As a Javascript class, your Keyring object will be used to instantiate new Keyring instances using the new keyword. For example: 13 | 14 | ``` 15 | const keyring = new YourKeyringClass(options); 16 | ``` 17 | 18 | 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. 19 | 20 | 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. 21 | 22 | ## Keyring Instance Methods 23 | 24 | All below instance methods must return Promises to allow asynchronous resolution. 25 | 26 | ### serialize() 27 | 28 | 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. 29 | 30 | ### deserialize( object ) 31 | 32 | As discussed above, the deserialize() method will be passed the JavaScript object that you returned when the serialize() method was called. 33 | 34 | ### addAccounts( n = 1 ) 35 | 36 | 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. 37 | 38 | 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. 39 | 40 | ### getAccounts() 41 | 42 | 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. 43 | 44 | ### signTransaction(address, transaction) 45 | 46 | This method will receive a hex-prefixed, all-lowercase address string for the account you should sign the incoming transaction with. 47 | 48 | For your convenience, the transaction is an instance of ethereumjs-tx, (https://github.com/ethereumjs/ethereumjs-tx) so signing can be as simple as: 49 | 50 | ``` 51 | transaction.sign(privateKey) 52 | ``` 53 | 54 | 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. 55 | 56 | ### signMessage(address, data) 57 | 58 | The `eth_sign` method will receive the incoming data, already hashed, and must sign that hash, and then return the raw signed hash. 59 | 60 | ### exportAccount(address) 61 | 62 | Exports the specified account as a private key hex string. 63 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/fk/c3y07g0576j8_2s9m01pk4qw0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls, instances and results before every test. 17 | // This does not remove any mock implementation that may have been provided, 18 | // so we disable it. 19 | // clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | collectCoverage: true, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | collectCoverageFrom: ['./src/*.ts'], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | coverageProvider: 'babel', 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | coverageReporters: ['html', 'json-summary', 'text', 'lcov'], 40 | 41 | // An object that configures minimum threshold enforcement for coverage results 42 | coverageThreshold: { 43 | global: { 44 | branches: 90.09, 45 | functions: 96.77, 46 | lines: 97.44, 47 | statements: 97.5, 48 | }, 49 | }, 50 | preset: 'ts-jest', 51 | 52 | // Run tests from one or more projects 53 | // projects: undefined, 54 | 55 | // Use this configuration option to add custom reporters to Jest 56 | // reporters: undefined, 57 | 58 | // "resetMocks" resets all mocks, including mocked modules, to jest.fn(), 59 | // between each test case. 60 | resetMocks: true, 61 | 62 | // Reset the module registry before running each individual test 63 | // resetModules: false, 64 | 65 | // A path to a custom resolver 66 | // resolver: undefined, 67 | 68 | // "restoreMocks" restores all mocks created using jest.spyOn to their 69 | // original implementations, between each test. It does not affect mocked 70 | // modules. 71 | restoreMocks: true, 72 | 73 | // The root directory that Jest should scan for tests and modules within 74 | // rootDir: undefined, 75 | 76 | // A list of paths to directories that Jest should use to search for files in 77 | // roots: [ 78 | // "" 79 | // ], 80 | 81 | // Allows you to use a custom runner instead of Jest's default test runner 82 | // runner: "jest-runner", 83 | 84 | // The paths to modules that run some code to configure or set up the testing environment before each test 85 | // setupFiles: [], 86 | 87 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 88 | // setupFilesAfterEnv: [], 89 | 90 | // The number of seconds after which a test is considered as slow and reported as such in the results. 91 | // slowTestThreshold: 5, 92 | 93 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 94 | // snapshotSerializers: [], 95 | 96 | // The test environment that will be used for testing 97 | // testEnvironment: "jest-environment-node", 98 | 99 | // Options that will be passed to the testEnvironment 100 | // testEnvironmentOptions: {}, 101 | 102 | // Adds a location field to test results 103 | // testLocationInResults: false, 104 | 105 | // The glob patterns Jest uses to detect test files 106 | // testMatch: [ 107 | // "**/__tests__/**/*.[jt]s?(x)", 108 | // "**/?(*.)+(spec|test).[tj]s?(x)" 109 | // ], 110 | 111 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 112 | // testPathIgnorePatterns: [ 113 | // "/node_modules/" 114 | // ], 115 | 116 | // The regexp pattern or array of patterns that Jest uses to detect test files 117 | // testRegex: [], 118 | 119 | // This option allows the use of a custom results processor 120 | // testResultsProcessor: undefined, 121 | 122 | // This option allows use of a custom test runner 123 | // testRunner: "jest-circus/runner", 124 | 125 | // Reduce the default test timeout from 5s to 2.5s 126 | testTimeout: 2500, 127 | 128 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 129 | // testURL: "http://localhost", 130 | 131 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 132 | // timers: "real", 133 | 134 | // A map from regular expressions to paths to transformers 135 | // transform: undefined, 136 | 137 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 138 | // transformIgnorePatterns: [ 139 | // "/node_modules/", 140 | // "\\.pnp\\.[^\\/]+$" 141 | // ], 142 | 143 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 144 | // unmockedModulePathPatterns: undefined, 145 | 146 | // Indicates whether each individual test should be reported during the run 147 | // verbose: undefined, 148 | 149 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 150 | // watchPathIgnorePatterns: [], 151 | 152 | // Whether to use watchman for file crawling 153 | // watchman: true, 154 | }; 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metamask/eth-keyring-controller", 3 | "version": "17.0.1", 4 | "description": "A module for managing various keyrings of Ethereum accounts, encrypting them, and using them", 5 | "keywords": [ 6 | "ethereum", 7 | "metamask", 8 | "accounts", 9 | "keys" 10 | ], 11 | "homepage": "https://github.com/MetaMask/KeyringController#readme", 12 | "bugs": { 13 | "url": "https://github.com/MetaMask/KeyringController/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/MetaMask/KeyringController.git" 18 | }, 19 | "license": "ISC", 20 | "author": "Dan Finlay ", 21 | "main": "./dist/index.js", 22 | "types": "./dist/index.d.ts", 23 | "files": [ 24 | "dist/" 25 | ], 26 | "scripts": { 27 | "build": "tsc --project tsconfig.build.json", 28 | "build:clean": "rimraf dist && yarn build", 29 | "build:docs": "typedoc", 30 | "lint": "yarn lint:eslint && yarn lint:constraints && yarn lint:misc --check && yarn lint:dependencies --check && yarn lint:changelog", 31 | "lint:changelog": "auto-changelog validate", 32 | "lint:constraints": "yarn constraints", 33 | "lint:dependencies": "depcheck && yarn dedupe", 34 | "lint:eslint": "eslint . --cache --ext js,ts", 35 | "lint:fix": "yarn lint:eslint --fix && yarn lint:constraints --fix && yarn lint:misc --write && yarn lint:dependencies && yarn lint:changelog", 36 | "lint:misc": "prettier '**/*.json' '**/*.md' '!CHANGELOG.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern", 37 | "prepack": "./scripts/prepack.sh", 38 | "test": "jest && jest-it-up", 39 | "test:watch": "jest --watch" 40 | }, 41 | "resolutions": { 42 | "ink": "3.1.0", 43 | "react-reconciler": "0.24.0" 44 | }, 45 | "dependencies": { 46 | "@ethereumjs/tx": "^4.2.0", 47 | "@metamask/browser-passworder": "^5.0.0", 48 | "@metamask/eth-hd-keyring": "^7.0.1", 49 | "@metamask/eth-sig-util": "^7.0.0", 50 | "@metamask/eth-simple-keyring": "^6.0.1", 51 | "@metamask/keyring-api": "^3.0.0", 52 | "@metamask/obs-store": "^9.0.0", 53 | "@metamask/utils": "^8.2.0" 54 | }, 55 | "devDependencies": { 56 | "@ethereumjs/wallet": "^2.0.0", 57 | "@lavamoat/allow-scripts": "^2.3.1", 58 | "@lavamoat/preinstall-always-fail": "^1.0.0", 59 | "@metamask/auto-changelog": "^3.0.0", 60 | "@metamask/eslint-config": "^12.2.0", 61 | "@metamask/eslint-config-jest": "^12.1.0", 62 | "@metamask/eslint-config-nodejs": "^12.1.0", 63 | "@metamask/eslint-config-typescript": "^12.1.0", 64 | "@types/jest": "^29.4.0", 65 | "@types/node": "^16.18.46", 66 | "@types/sinon": "^10.0.13", 67 | "@typescript-eslint/eslint-plugin": "^5.55.0", 68 | "@typescript-eslint/parser": "^5.55.0", 69 | "@yarnpkg/cli": "^4.0.0-rc.50", 70 | "@yarnpkg/core": "^4.0.0-rc.50", 71 | "@yarnpkg/fslib": "^3.0.0-rc.50", 72 | "clipanion": "^4.0.0-rc.2", 73 | "depcheck": "^1.4.7", 74 | "eslint": "^8.48.0", 75 | "eslint-config-prettier": "^8.7.0", 76 | "eslint-plugin-import": "~2.26.0", 77 | "eslint-plugin-jest": "^27.2.1", 78 | "eslint-plugin-jsdoc": "^41", 79 | "eslint-plugin-n": "^15.7.0", 80 | "eslint-plugin-prettier": "^4.2.1", 81 | "eslint-plugin-promise": "^6.1.1", 82 | "jest": "^29.7.0", 83 | "jest-it-up": "^2.0.2", 84 | "prettier": "^2.8.1", 85 | "prettier-plugin-packagejson": "^2.3.0", 86 | "rimraf": "^3.0.2", 87 | "sinon": "^15.0.1", 88 | "ts-jest": "^29.1.0", 89 | "ts-node": "^10.9.1", 90 | "typedoc": "^0.23.28", 91 | "typescript": "~5.0.4" 92 | }, 93 | "packageManager": "yarn@3.2.1", 94 | "engines": { 95 | "node": "^18.18 || >=20" 96 | }, 97 | "publishConfig": { 98 | "access": "public", 99 | "registry": "https://registry.npmjs.org/" 100 | }, 101 | "lavamoat": { 102 | "allowScripts": { 103 | "@lavamoat/preinstall-always-fail": false, 104 | "@metamask/keyring-api>@metamask/snaps-utils>@metamask/permission-controller>@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>keccak": false, 105 | "@metamask/keyring-api>@metamask/snaps-utils>@metamask/permission-controller>@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>secp256k1": false 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /scripts/get.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | set -e 5 | set -u 6 | set -o pipefail 7 | 8 | KEY="${1}" 9 | OUTPUT="${2}" 10 | 11 | if [[ -z $KEY ]]; then 12 | echo "Error: KEY not specified." 13 | exit 1 14 | fi 15 | 16 | if [[ -z $OUTPUT ]]; then 17 | echo "Error: OUTPUT not specified." 18 | exit 1 19 | fi 20 | 21 | echo "$OUTPUT=$(jq --raw-output "$KEY" package.json)" >> "$GITHUB_OUTPUT" 22 | -------------------------------------------------------------------------------- /scripts/prepack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | set -e 5 | set -o pipefail 6 | 7 | if [[ -n $SKIP_PREPACK ]]; then 8 | echo "Notice: skipping prepack." 9 | exit 0 10 | fi 11 | 12 | yarn build:clean 13 | -------------------------------------------------------------------------------- /src/KeyringController.test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from '@ethereumjs/wallet'; 2 | import HdKeyring from '@metamask/eth-hd-keyring'; 3 | import { normalize as normalizeAddress } from '@metamask/eth-sig-util'; 4 | import type { Hex, Json, KeyringClass } from '@metamask/utils'; 5 | import { bytesToHex } from '@metamask/utils'; 6 | import { strict as assert } from 'assert'; 7 | import * as sinon from 'sinon'; 8 | 9 | import { KeyringController, keyringBuilderFactory } from '.'; 10 | import { KeyringType, KeyringControllerError } from './constants'; 11 | import { 12 | MockEncryptor, 13 | KeyringMockWithInit, 14 | PASSWORD, 15 | MOCK_HARDCODED_KEY, 16 | MOCK_ENCRYPTION_SALT, 17 | BaseKeyringMock, 18 | KeyringMockWithDestroy, 19 | buildMockTransaction, 20 | KeyringMockWithSignTransaction, 21 | KeyringMockWithUserOp, 22 | } from './test'; 23 | import type { KeyringControllerArgs } from './types'; 24 | 25 | const MOCK_ENCRYPTION_KEY = 26 | '{"alg":"A256GCM","ext":true,"k":"wYmxkxOOFBDP6F6VuuYFcRt_Po-tSLFHCWVolsHs4VI","key_ops":["encrypt","decrypt"],"kty":"oct"}'; 27 | const MOCK_ENCRYPTION_DATA = `{"data":"2fOOPRKClNrisB+tmqIcETyZvDuL2iIR1Hr1nO7XZHyMqVY1cDBetw2gY5C+cIo1qkpyv3bPp+4buUjp38VBsjbijM0F/FLOqWbcuKM9h9X0uwxsgsZ96uwcIf5I46NiMgoFlhppTTMZT0Nkocz+SnvHM0IgLsFan7JqBU++vSJvx2M1PDljZSunOsqyyL+DKmbYmM4umbouKV42dipUwrCvrQJmpiUZrSkpMJrPJk9ufDQO4CyIVo0qry3aNRdYFJ6rgSyq/k6rXMwGExCMHn8UlhNnAMuMKWPWR/ymK1bzNcNs4VU14iVjEXOZGPvD9cvqVe/VtcnIba6axNEEB4HWDOCdrDh5YNWwMlQVL7vSB2yOhPZByGhnEOloYsj2E5KEb9jFGskt7EKDEYNofr6t83G0c+B72VGYZeCvgtzXzgPwzIbhTtKkP+gdBmt2JNSYrTjLypT0q+v4C9BN1xWTxPmX6TTt0NzkI9pJxgN1VQAfSU9CyWTVpd4CBkgom2cSBsxZ2MNbdKF+qSWz3fQcmJ55hxM0EGJSt9+8eQOTuoJlBapRk4wdZKHR2jdKzPjSF2MAmyVD2kU51IKa/cVsckRFEes+m7dKyHRvlNwgT78W9tBDdZb5PSlfbZXnv8z5q1KtAj2lM2ogJ7brHBdevl4FISdTkObpwcUMcvACOOO0dj6CSYjSKr0ZJ2RLChVruZyPDxEhKGb/8Kv8trLOR3mck/et6d050/NugezycNk4nnzu5iP90gPbSzaqdZI=","iv":"qTGO1afGv3waHN9KoW34Eg==","salt":"${MOCK_ENCRYPTION_SALT}"}`; 28 | 29 | const walletOneSeedWords = 30 | 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'; 31 | 32 | const mockAddress = '0xef35ca8ebb9669a35c31b5f6f249a9941a812ac1'; 33 | const walletOneAddresses = ['0xef35ca8ebb9669a35c31b5f6f249a9941a812ac1']; 34 | const walletOnePrivateKey = [ 35 | 'ace918800411c0b96b915f76efbbd4d50e6c997180fee58e01f60d3a412d2f7e', 36 | ]; 37 | 38 | const walletTwoSeedWords = 39 | 'urge letter protect palace city barely security section midnight wealth south deer'; 40 | 41 | const walletTwoAddresses = [ 42 | '0xbbafcf3d00fb625b65bb1497c94bf42c1f4b3e78', 43 | '0x49dd2653f38f75d40fdbd51e83b9c9724c87f7eb', 44 | ]; 45 | 46 | /** 47 | * Create a keyring controller that has been initialized with the given options. 48 | * 49 | * @param options - Initialization options. 50 | * @param options.constructorOptions - Constructor options, merged with test defaults. 51 | * @param options.password - The vault password. If provided, creates a new vault (if necessary) 52 | * and unlocks the vault. 53 | * @param options.seedPhrase - A seed phrase. If provided, this is used to restore the vault. 54 | * @returns A keyring controller. 55 | */ 56 | async function initializeKeyringController({ 57 | constructorOptions, 58 | password, 59 | seedPhrase, 60 | }: { 61 | constructorOptions?: Partial; 62 | password?: string; 63 | seedPhrase?: string; 64 | } = {}) { 65 | const keyringController = new KeyringController({ 66 | encryptor: new MockEncryptor(), 67 | cacheEncryptionKey: false, 68 | keyringBuilders: [keyringBuilderFactory(BaseKeyringMock)], 69 | ...constructorOptions, 70 | }); 71 | 72 | if (seedPhrase && !password) { 73 | throw new Error('Password required to restore vault'); 74 | } else if (seedPhrase && password) { 75 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 76 | type: KeyringType.HD, 77 | opts: { 78 | mnemonic: walletOneSeedWords, 79 | numberOfAccounts: 1, 80 | }, 81 | }); 82 | } else if (password) { 83 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 84 | type: KeyringType.HD, 85 | }); 86 | } 87 | 88 | return keyringController; 89 | } 90 | 91 | /** 92 | * Delete the encryption key and salt from the `memStore` of the given keyring controller. 93 | * 94 | * @param keyringController - The keyring controller to delete the encryption key and salt from. 95 | */ 96 | function deleteEncryptionKeyAndSalt(keyringController: KeyringController) { 97 | const keyringControllerState = keyringController.memStore.getState(); 98 | delete keyringControllerState.encryptionKey; 99 | delete keyringControllerState.encryptionSalt; 100 | keyringController.memStore.updateState(keyringControllerState); 101 | } 102 | 103 | /** 104 | * Stub the `getAccounts` and `addAccounts` methods of the given keyring class to return the given 105 | * account. 106 | * 107 | * @param keyringClass - The keyring class to stub. 108 | * @param account - The account to return. 109 | */ 110 | function stubKeyringClassWithAccount( 111 | keyringClass: KeyringClass, 112 | account: string, 113 | ) { 114 | sinon 115 | .stub(keyringClass.prototype, 'getAccounts') 116 | .returns(Promise.resolve([account])); 117 | sinon 118 | .stub(keyringClass.prototype, 'addAccounts') 119 | .returns(Promise.resolve([account])); 120 | } 121 | 122 | describe('KeyringController', () => { 123 | afterEach(() => { 124 | sinon.restore(); 125 | }); 126 | 127 | describe('constructor', () => { 128 | describe('with cacheEncryptionKey = true', () => { 129 | it('should throw error if provided encryptor does not support key export', async () => { 130 | expect( 131 | () => 132 | // @ts-expect-error we want to bypass typechecks here. 133 | new KeyringController({ 134 | cacheEncryptionKey: true, 135 | encryptor: { 136 | decrypt: async (_pass: string, _text: string) => 137 | Promise.resolve('encrypted'), 138 | encrypt: async (_pass: string, _obj: any) => 'decrypted', 139 | }, 140 | }), 141 | ).toThrow(KeyringControllerError.UnsupportedEncryptionKeyExport); 142 | }); 143 | }); 144 | }); 145 | 146 | describe('setLocked', () => { 147 | it('setLocked correctly sets lock state', async () => { 148 | const keyringController = await initializeKeyringController({ 149 | password: PASSWORD, 150 | }); 151 | assert.notDeepEqual( 152 | keyringController.keyrings, 153 | [], 154 | 'keyrings should not be empty', 155 | ); 156 | 157 | await keyringController.setLocked(); 158 | 159 | expect(keyringController.password).toBeUndefined(); 160 | expect(keyringController.memStore.getState().isUnlocked).toBe(false); 161 | expect(keyringController.keyrings).toHaveLength(0); 162 | }); 163 | 164 | it('emits "lock" event', async () => { 165 | const keyringController = await initializeKeyringController({ 166 | password: PASSWORD, 167 | }); 168 | const lockSpy = sinon.spy(); 169 | keyringController.on('lock', lockSpy); 170 | 171 | await keyringController.setLocked(); 172 | 173 | expect(lockSpy.calledOnce).toBe(true); 174 | }); 175 | 176 | it('calls keyring optional destroy function', async () => { 177 | const keyringController = await initializeKeyringController({ 178 | password: PASSWORD, 179 | constructorOptions: { 180 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithDestroy)], 181 | }, 182 | }); 183 | const destroy = sinon.spy(KeyringMockWithDestroy.prototype, 'destroy'); 184 | await keyringController.addNewKeyring('Keyring Mock With Destroy'); 185 | 186 | await keyringController.setLocked(); 187 | 188 | expect(destroy.calledOnce).toBe(true); 189 | }); 190 | }); 191 | 192 | describe('submitPassword', () => { 193 | it('should not load keyrings when incorrect password', async () => { 194 | const keyringController = await initializeKeyringController({ 195 | password: PASSWORD, 196 | }); 197 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 198 | type: KeyringType.HD, 199 | }); 200 | await keyringController.persistAllKeyrings(); 201 | expect(keyringController.keyrings).toHaveLength(1); 202 | 203 | await keyringController.setLocked(); 204 | 205 | await expect( 206 | keyringController.submitPassword('Wrong password'), 207 | ).rejects.toThrow('Incorrect password.'); 208 | expect(keyringController.password).toBeUndefined(); 209 | expect(keyringController.keyrings).toHaveLength(0); 210 | }); 211 | 212 | it('emits "unlock" event', async () => { 213 | const keyringController = await initializeKeyringController({ 214 | password: PASSWORD, 215 | }); 216 | await keyringController.setLocked(); 217 | 218 | const unlockSpy = sinon.spy(); 219 | keyringController.on('unlock', unlockSpy); 220 | 221 | await keyringController.submitPassword(PASSWORD); 222 | expect(unlockSpy.calledOnce).toBe(true); 223 | }); 224 | }); 225 | 226 | describe('persistAllKeyrings', () => { 227 | it('should persist keyrings in _unsupportedKeyrings array', async () => { 228 | const mockEncryptor = new MockEncryptor(); 229 | const keyringController = await initializeKeyringController({ 230 | password: PASSWORD, 231 | constructorOptions: { 232 | encryptor: mockEncryptor, 233 | }, 234 | }); 235 | const encryptSpy = sinon.spy(mockEncryptor, 'encrypt'); 236 | const unsupportedKeyring = { type: 'DUMMY_KEYRING', data: {} }; 237 | keyringController.unsupportedKeyrings = [unsupportedKeyring]; 238 | 239 | await keyringController.persistAllKeyrings(); 240 | 241 | assert(keyringController.store.getState().vault, 'Vault is not set'); 242 | expect(encryptSpy.calledOnce).toBe(true); 243 | expect(encryptSpy.getCalls()[0]?.args[1]).toHaveLength(2); 244 | expect(encryptSpy.getCalls()[0]?.args[1]).toContain(unsupportedKeyring); 245 | }); 246 | 247 | describe('when `cacheEncryptionKey` is enabled', () => { 248 | describe('when `encryptionKey` is set', () => { 249 | it('should save an up to date encryption salt to the `memStore`', async () => { 250 | const keyringController = await initializeKeyringController({ 251 | password: PASSWORD, 252 | constructorOptions: { 253 | cacheEncryptionKey: true, 254 | }, 255 | }); 256 | const vaultEncryptionKey = '🔑'; 257 | const vaultEncryptionSalt = '🧂'; 258 | const vault = JSON.stringify({ salt: vaultEncryptionSalt }); 259 | keyringController.store.updateState({ vault }); 260 | 261 | await keyringController.unlockKeyrings( 262 | undefined, 263 | vaultEncryptionKey, 264 | vaultEncryptionSalt, 265 | ); 266 | 267 | expect(keyringController.memStore.getState().encryptionKey).toBe( 268 | vaultEncryptionKey, 269 | ); 270 | expect(keyringController.memStore.getState().encryptionSalt).toBe( 271 | vaultEncryptionSalt, 272 | ); 273 | 274 | const response = await keyringController.persistAllKeyrings(); 275 | 276 | expect(response).toBe(true); 277 | expect(keyringController.memStore.getState().encryptionKey).toBe( 278 | vaultEncryptionKey, 279 | ); 280 | expect(keyringController.memStore.getState().encryptionSalt).toBe( 281 | vaultEncryptionSalt, 282 | ); 283 | }); 284 | }); 285 | 286 | describe('when `encryptionKey` is not set and `password` is set', () => { 287 | it('should save an up to date encryption salt to the `memStore` when `password` is set through `createNewVaultAndKeychain`', async () => { 288 | const keyringController = await initializeKeyringController({ 289 | password: PASSWORD, 290 | constructorOptions: { 291 | cacheEncryptionKey: true, 292 | }, 293 | }); 294 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 295 | type: KeyringType.HD, 296 | }); 297 | deleteEncryptionKeyAndSalt(keyringController); 298 | 299 | const response = await keyringController.persistAllKeyrings(); 300 | 301 | expect(response).toBe(true); 302 | expect(keyringController.memStore.getState().encryptionKey).toBe( 303 | MOCK_HARDCODED_KEY, 304 | ); 305 | expect(keyringController.memStore.getState().encryptionSalt).toBe( 306 | MOCK_ENCRYPTION_SALT, 307 | ); 308 | }); 309 | 310 | it('should save an up to date encryption salt to the `memStore` when `password` is set through `submitPassword`', async () => { 311 | const keyringController = await initializeKeyringController({ 312 | password: PASSWORD, 313 | constructorOptions: { 314 | cacheEncryptionKey: true, 315 | }, 316 | }); 317 | await keyringController.submitPassword(PASSWORD); 318 | deleteEncryptionKeyAndSalt(keyringController); 319 | 320 | const response = await keyringController.persistAllKeyrings(); 321 | 322 | expect(response).toBe(true); 323 | expect(keyringController.memStore.getState().encryptionKey).toBe( 324 | MOCK_HARDCODED_KEY, 325 | ); 326 | expect(keyringController.memStore.getState().encryptionSalt).toBe( 327 | MOCK_ENCRYPTION_SALT, 328 | ); 329 | }); 330 | }); 331 | }); 332 | 333 | it('should add an `encryptionSalt` to the `memStore` when a vault is restored', async () => { 334 | const keyringController = await initializeKeyringController({ 335 | password: PASSWORD, 336 | constructorOptions: { 337 | cacheEncryptionKey: true, 338 | }, 339 | }); 340 | 341 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 342 | type: KeyringType.HD, 343 | opts: { 344 | mnemonic: walletOneSeedWords, 345 | numberOfAccounts: 1, 346 | }, 347 | }); 348 | 349 | const finalMemStore = keyringController.memStore.getState(); 350 | expect(finalMemStore.encryptionKey).toBe(MOCK_HARDCODED_KEY); 351 | expect(finalMemStore.encryptionSalt).toBe(MOCK_ENCRYPTION_SALT); 352 | }); 353 | }); 354 | 355 | describe('createNewVaultWithKeyring', () => { 356 | it('should create a new vault with a HD keyring', async () => { 357 | const keyringController = await initializeKeyringController({ 358 | password: PASSWORD, 359 | }); 360 | keyringController.store.putState({}); 361 | assert(!keyringController.store.getState().vault, 'no previous vault'); 362 | 363 | const newVault = await keyringController.createNewVaultWithKeyring( 364 | PASSWORD, 365 | { 366 | type: KeyringType.HD, 367 | }, 368 | ); 369 | const { vault } = keyringController.store.getState(); 370 | expect(vault).toStrictEqual(expect.stringMatching('.+')); 371 | expect(typeof newVault).toBe('object'); 372 | }); 373 | 374 | it('should create a new vault with a simple keyring', async () => { 375 | const keyringController = await initializeKeyringController({ 376 | password: PASSWORD, 377 | }); 378 | keyringController.store.putState({}); 379 | assert(!keyringController.store.getState().vault, 'no previous vault'); 380 | 381 | const newVault = await keyringController.createNewVaultWithKeyring( 382 | PASSWORD, 383 | { 384 | type: KeyringType.Simple, 385 | opts: walletOnePrivateKey, 386 | }, 387 | ); 388 | const { vault } = keyringController.store.getState(); 389 | expect(vault).toStrictEqual(expect.stringMatching('.+')); 390 | expect(typeof newVault).toBe('object'); 391 | 392 | const accounts = await keyringController.getAccounts(); 393 | expect(accounts).toHaveLength(1); 394 | expect(accounts[0]).toBe(walletOneAddresses[0]); 395 | }); 396 | 397 | it('should unlock the vault', async () => { 398 | const keyringController = await initializeKeyringController({ 399 | password: PASSWORD, 400 | }); 401 | keyringController.store.putState({}); 402 | assert(!keyringController.store.getState().vault, 'no previous vault'); 403 | 404 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 405 | type: KeyringType.HD, 406 | }); 407 | const { isUnlocked } = keyringController.memStore.getState(); 408 | expect(isUnlocked).toBe(true); 409 | }); 410 | 411 | it('should encrypt keyrings with the correct password each time they are persisted', async () => { 412 | const mockEncryptor = new MockEncryptor(); 413 | const encryptSpy = sinon.spy(mockEncryptor, 'encrypt'); 414 | const keyringController = await initializeKeyringController({ 415 | password: PASSWORD, 416 | constructorOptions: { 417 | encryptor: mockEncryptor, 418 | }, 419 | }); 420 | keyringController.store.putState({}); 421 | assert(!keyringController.store.getState().vault, 'no previous vault'); 422 | 423 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 424 | type: KeyringType.HD, 425 | }); 426 | const { vault } = keyringController.store.getState(); 427 | // eslint-disable-next-line jest/no-restricted-matchers 428 | expect(vault).toBeTruthy(); 429 | encryptSpy.args.forEach(([actualPassword]) => { 430 | expect(actualPassword).toBe(PASSWORD); 431 | }); 432 | }); 433 | 434 | it('should throw error if accounts are not generated correctly', async () => { 435 | const keyringController = await initializeKeyringController({ 436 | password: PASSWORD, 437 | }); 438 | jest 439 | .spyOn(HdKeyring.prototype, 'getAccounts') 440 | .mockImplementation(async () => Promise.resolve([])); 441 | 442 | await expect(async () => 443 | keyringController.createNewVaultWithKeyring(PASSWORD, { 444 | type: KeyringType.HD, 445 | }), 446 | ).rejects.toThrow(KeyringControllerError.NoFirstAccount); 447 | }); 448 | 449 | describe('when `cacheEncryptionKey` is enabled', () => { 450 | it('should add an `encryptionSalt` to the `memStore` when a new vault is created', async () => { 451 | const keyringController = await initializeKeyringController({ 452 | password: PASSWORD, 453 | constructorOptions: { 454 | cacheEncryptionKey: true, 455 | }, 456 | }); 457 | 458 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 459 | type: KeyringType.HD, 460 | }); 461 | 462 | const finalMemStore = keyringController.memStore.getState(); 463 | expect(finalMemStore.encryptionKey).toBe(MOCK_HARDCODED_KEY); 464 | expect(finalMemStore.encryptionSalt).toBe(MOCK_ENCRYPTION_SALT); 465 | }); 466 | }); 467 | 468 | it('clears old keyrings and creates a one', async () => { 469 | const keyringController = await initializeKeyringController({ 470 | password: PASSWORD, 471 | }); 472 | const initialAccounts = await keyringController.getAccounts(); 473 | expect(initialAccounts).toHaveLength(1); 474 | 475 | await keyringController.addNewKeyring(KeyringType.HD); 476 | const allAccounts = await keyringController.getAccounts(); 477 | expect(allAccounts).toHaveLength(2); 478 | 479 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 480 | type: KeyringType.HD, 481 | opts: { 482 | mnemonic: walletOneSeedWords, 483 | numberOfAccounts: 1, 484 | }, 485 | }); 486 | 487 | const allAccountsAfter = await keyringController.getAccounts(); 488 | expect(allAccountsAfter).toHaveLength(1); 489 | expect(allAccountsAfter[0]).toBe(walletOneAddresses[0]); 490 | }); 491 | 492 | it('throws error if argument password is not a string', async () => { 493 | const keyringController = await initializeKeyringController({ 494 | password: PASSWORD, 495 | }); 496 | await expect(async () => 497 | // @ts-expect-error Missing other required permission types. 498 | keyringController.createNewVaultWithKeyring(12, { 499 | type: KeyringType.HD, 500 | opts: { 501 | mnemonic: walletTwoSeedWords, 502 | numberOfAccounts: 1, 503 | }, 504 | }), 505 | ).rejects.toThrow('KeyringController - Password must be of type string.'); 506 | }); 507 | 508 | it('throws error if mnemonic passed is invalid', async () => { 509 | const keyringController = await initializeKeyringController({ 510 | password: PASSWORD, 511 | }); 512 | await expect(async () => 513 | keyringController.createNewVaultWithKeyring(PASSWORD, { 514 | type: KeyringType.HD, 515 | opts: { 516 | mnemonic: 517 | 'test test test palace city barely security section midnight wealth south deer', 518 | numberOfAccounts: 1, 519 | }, 520 | }), 521 | ).rejects.toThrow( 522 | 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', 523 | ); 524 | 525 | await expect(async () => 526 | keyringController.createNewVaultWithKeyring(PASSWORD, { 527 | type: KeyringType.HD, 528 | opts: { 529 | mnemonic: '1234', 530 | numberOfAccounts: 1, 531 | }, 532 | }), 533 | ).rejects.toThrow( 534 | 'Eth-Hd-Keyring: Invalid secret recovery phrase provided', 535 | ); 536 | }); 537 | 538 | it('accepts mnemonic passed as type array of numbers', async () => { 539 | const keyringController = await initializeKeyringController({ 540 | password: PASSWORD, 541 | }); 542 | const allAccountsBefore = await keyringController.getAccounts(); 543 | expect(allAccountsBefore[0]).not.toBe(walletTwoAddresses[0]); 544 | const mnemonicAsArrayOfNumbers = Array.from( 545 | Buffer.from(walletTwoSeedWords).values(), 546 | ); 547 | 548 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 549 | type: KeyringType.HD, 550 | opts: { 551 | mnemonic: mnemonicAsArrayOfNumbers, 552 | numberOfAccounts: 1, 553 | }, 554 | }); 555 | 556 | const allAccountsAfter = await keyringController.getAccounts(); 557 | expect(allAccountsAfter).toHaveLength(1); 558 | expect(allAccountsAfter[0]).toBe(walletTwoAddresses[0]); 559 | }); 560 | 561 | it('throws error if accounts are not created properly', async () => { 562 | const keyringController = await initializeKeyringController({ 563 | password: PASSWORD, 564 | }); 565 | jest 566 | .spyOn(HdKeyring.prototype, 'getAccounts') 567 | .mockImplementation(async () => Promise.resolve([])); 568 | 569 | await expect(async () => 570 | keyringController.createNewVaultWithKeyring(PASSWORD, { 571 | type: KeyringType.HD, 572 | opts: { 573 | mnemonic: walletTwoSeedWords, 574 | numberOfAccounts: 1, 575 | }, 576 | }), 577 | ).rejects.toThrow('KeyringController - First Account not found.'); 578 | }); 579 | }); 580 | 581 | describe('addNewKeyring', () => { 582 | it('should add simple key pair', async () => { 583 | const keyringController = await initializeKeyringController({ 584 | password: PASSWORD, 585 | }); 586 | const privateKey = 587 | 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'; 588 | const previousAccounts = await keyringController.getAccounts(); 589 | const keyring = await keyringController.addNewKeyring( 590 | KeyringType.Simple, 591 | [privateKey], 592 | ); 593 | 594 | const keyringAccounts = await keyring?.getAccounts(); 595 | const expectedKeyringAccounts = [ 596 | '0x627306090abab3a6e1400e9345bc60c78a8bef57', 597 | ]; 598 | expect(keyringAccounts).toStrictEqual(expectedKeyringAccounts); 599 | 600 | const allAccounts = await keyringController.getAccounts(); 601 | const expectedAllAccounts = previousAccounts.concat( 602 | expectedKeyringAccounts, 603 | ); 604 | expect(allAccounts).toStrictEqual(expectedAllAccounts); 605 | }); 606 | 607 | it('should add HD Key Tree without mnemonic passed as an argument', async () => { 608 | const keyringController = await initializeKeyringController({ 609 | password: PASSWORD, 610 | }); 611 | const previousAllAccounts = await keyringController.getAccounts(); 612 | expect(previousAllAccounts).toHaveLength(1); 613 | const keyring = await keyringController.addNewKeyring(KeyringType.HD); 614 | const keyringAccounts = await keyring?.getAccounts(); 615 | expect(keyringAccounts).toHaveLength(1); 616 | const allAccounts = await keyringController.getAccounts(); 617 | expect(allAccounts).toHaveLength(2); 618 | }); 619 | 620 | it('should add HD Key Tree with mnemonic passed as an argument', async () => { 621 | const keyringController = await initializeKeyringController({ 622 | password: PASSWORD, 623 | }); 624 | const previousAllAccounts = await keyringController.getAccounts(); 625 | expect(previousAllAccounts).toHaveLength(1); 626 | const keyring = await keyringController.addNewKeyring(KeyringType.HD, { 627 | numberOfAccounts: 2, 628 | mnemonic: walletTwoSeedWords, 629 | }); 630 | const keyringAccounts = await keyring?.getAccounts(); 631 | expect(keyringAccounts).toHaveLength(2); 632 | expect(keyringAccounts?.[0]).toStrictEqual(walletTwoAddresses[0]); 633 | expect(keyringAccounts?.[1]).toStrictEqual(walletTwoAddresses[1]); 634 | const allAccounts = await keyringController.getAccounts(); 635 | expect(allAccounts).toHaveLength(3); 636 | }); 637 | 638 | it('should add keyring that expects undefined serialized state', async () => { 639 | let deserializedSpy = sinon.spy(); 640 | const mockKeyringBuilder = () => { 641 | const keyring = new KeyringMockWithInit(); 642 | deserializedSpy = sinon.spy(keyring, 'deserialize'); 643 | return keyring; 644 | }; 645 | mockKeyringBuilder.type = 'Mock Keyring'; 646 | const keyringController = await initializeKeyringController({ 647 | constructorOptions: { 648 | keyringBuilders: [mockKeyringBuilder], 649 | }, 650 | password: PASSWORD, 651 | }); 652 | await keyringController.addNewKeyring('Mock Keyring'); 653 | 654 | expect(deserializedSpy.callCount).toBe(1); 655 | expect(deserializedSpy.calledWith(undefined)).toBe(true); 656 | }); 657 | 658 | it('should call init method if available', async () => { 659 | const keyringController = await initializeKeyringController({ 660 | password: PASSWORD, 661 | constructorOptions: { 662 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithInit)], 663 | }, 664 | }); 665 | const initSpy = sinon.spy(KeyringMockWithInit.prototype, 'init'); 666 | 667 | const keyring = await keyringController.addNewKeyring( 668 | 'Keyring Mock With Init', 669 | ); 670 | 671 | expect(keyring).toBeInstanceOf(KeyringMockWithInit); 672 | 673 | sinon.assert.calledOnce(initSpy); 674 | }); 675 | 676 | it('should add HD Key Tree when addAccounts is asynchronous', async () => { 677 | const keyringController = await initializeKeyringController({ 678 | password: PASSWORD, 679 | }); 680 | const originalAccAccounts = HdKeyring.prototype.addAccounts; 681 | sinon.stub(HdKeyring.prototype, 'addAccounts').callsFake(async () => { 682 | return new Promise((resolve) => { 683 | setImmediate(() => { 684 | resolve(originalAccAccounts.bind(this)()); 685 | }); 686 | }); 687 | }); 688 | 689 | sinon.stub(HdKeyring.prototype, 'deserialize').callsFake(async () => { 690 | return new Promise((resolve) => { 691 | setImmediate(() => { 692 | resolve(); 693 | }); 694 | }); 695 | }); 696 | 697 | sinon 698 | .stub(HdKeyring.prototype, 'getAccounts') 699 | .callsFake(() => ['mock account']); 700 | 701 | const keyring = await keyringController.addNewKeyring(KeyringType.HD, { 702 | mnemonic: 'mock mnemonic', 703 | }); 704 | 705 | const keyringAccounts = await keyring?.getAccounts(); 706 | expect(keyringAccounts).toHaveLength(1); 707 | }); 708 | }); 709 | 710 | describe('restoreKeyring', () => { 711 | it(`should pass a keyring's serialized data back to the correct type.`, async () => { 712 | const keyringController = await initializeKeyringController({ 713 | password: PASSWORD, 714 | }); 715 | const mockSerialized = { 716 | type: 'HD Key Tree', 717 | data: { 718 | mnemonic: walletOneSeedWords, 719 | numberOfAccounts: 1, 720 | }, 721 | }; 722 | 723 | const keyring = await keyringController.restoreKeyring(mockSerialized); 724 | // eslint-disable-next-line no-unsafe-optional-chaining 725 | // @ts-expect-error this value should never be undefined in this specific context. 726 | const { numberOfAccounts } = await keyring.serialize(); 727 | expect(numberOfAccounts).toBe(1); 728 | 729 | const accounts = await keyring?.getAccounts(); 730 | expect(accounts?.[0]).toBe(walletOneAddresses[0]); 731 | }); 732 | 733 | it('should return undefined if keyring type is not supported.', async () => { 734 | const keyringController = await initializeKeyringController({ 735 | password: PASSWORD, 736 | }); 737 | const unsupportedKeyring = { type: 'Ledger Keyring', data: 'DUMMY' }; 738 | const keyring = await keyringController.restoreKeyring( 739 | unsupportedKeyring, 740 | ); 741 | expect(keyring).toBeUndefined(); 742 | }); 743 | }); 744 | 745 | describe('getAccounts', () => { 746 | it('returns the result of getAccounts for each keyring', async () => { 747 | const keyringController = await initializeKeyringController({ 748 | password: PASSWORD, 749 | }); 750 | keyringController.keyrings = [ 751 | { 752 | // @ts-expect-error there's only a need to mock the getAccounts method for this test. 753 | async getAccounts() { 754 | return Promise.resolve([1, 2, 3]); 755 | }, 756 | }, 757 | { 758 | // @ts-expect-error there's only a need to mock the getAccounts method for this test. 759 | async getAccounts() { 760 | return Promise.resolve([4, 5, 6]); 761 | }, 762 | }, 763 | ]; 764 | 765 | const result = await keyringController.getAccounts(); 766 | expect(result).toStrictEqual([ 767 | '0x01', 768 | '0x02', 769 | '0x03', 770 | '0x04', 771 | '0x05', 772 | '0x06', 773 | ]); 774 | }); 775 | }); 776 | 777 | describe('removeAccount', () => { 778 | it('removes an account from the corresponding keyring', async () => { 779 | const keyringController = await initializeKeyringController({ 780 | password: PASSWORD, 781 | }); 782 | const account: { privateKey: string; publicKey: Hex } = { 783 | privateKey: 784 | 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', 785 | publicKey: '0x627306090abab3a6e1400e9345bc60c78a8bef57', 786 | }; 787 | 788 | const accountsBeforeAdding = await keyringController.getAccounts(); 789 | 790 | // Add a new keyring with one account 791 | await keyringController.addNewKeyring(KeyringType.Simple, [ 792 | account.privateKey, 793 | ]); 794 | expect(keyringController.keyrings).toHaveLength(2); 795 | 796 | // remove that account that we just added 797 | await keyringController.removeAccount(account.publicKey); 798 | 799 | expect(keyringController.keyrings).toHaveLength(1); 800 | // fetch accounts after removal 801 | const result = await keyringController.getAccounts(); 802 | expect(result).toStrictEqual(accountsBeforeAdding); 803 | }); 804 | 805 | it('removes the keyring if there are no accounts after removal', async () => { 806 | const keyringController = await initializeKeyringController({ 807 | password: PASSWORD, 808 | }); 809 | const account: { privateKey: string; publicKey: Hex } = { 810 | privateKey: 811 | 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', 812 | publicKey: '0x627306090abab3a6e1400e9345bc60c78a8bef57', 813 | }; 814 | 815 | // Add a new keyring with one account 816 | await keyringController.addNewKeyring(KeyringType.Simple, [ 817 | account.privateKey, 818 | ]); 819 | 820 | // We should have 2 keyrings 821 | expect(keyringController.keyrings).toHaveLength(2); 822 | 823 | // remove that account that we just added 824 | await keyringController.removeAccount(account.publicKey); 825 | 826 | // Check that the previous keyring with only one account 827 | // was also removed after removing the account 828 | expect(keyringController.keyrings).toHaveLength(1); 829 | }); 830 | 831 | it('calls keyring optional destroy function', async () => { 832 | const keyringController = await initializeKeyringController({ 833 | password: PASSWORD, 834 | constructorOptions: { 835 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithDestroy)], 836 | }, 837 | }); 838 | const destroy = sinon.spy(KeyringMockWithDestroy.prototype, 'destroy'); 839 | const keyring = await keyringController.addNewKeyring( 840 | 'Keyring Mock With Destroy', 841 | ); 842 | sinon.stub(keyringController, 'getKeyringForAccount').resolves(keyring); 843 | 844 | await keyringController.removeAccount('0x0'); 845 | 846 | expect(destroy.calledOnce).toBe(true); 847 | }); 848 | 849 | it('does not remove the keyring if there are accounts remaining after removing one from the keyring', async () => { 850 | const keyringController = await initializeKeyringController({ 851 | password: PASSWORD, 852 | }); 853 | // Add a new keyring with two accounts 854 | await keyringController.addNewKeyring(KeyringType.HD, { 855 | mnemonic: walletTwoSeedWords, 856 | numberOfAccounts: 2, 857 | }); 858 | 859 | // We should have 2 keyrings 860 | expect(keyringController.keyrings).toHaveLength(2); 861 | 862 | // remove one account from the keyring we just added 863 | // @ts-expect-error this value should never be undefied 864 | await keyringController.removeAccount(walletTwoAddresses[0]); 865 | 866 | // Check that the newly added keyring was not removed after 867 | // removing the account since it still has an account left 868 | expect(keyringController.keyrings).toHaveLength(2); 869 | }); 870 | 871 | it('throws an error if the keyring for the given account does not support account removal', async () => { 872 | const mockAccount = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 873 | stubKeyringClassWithAccount(BaseKeyringMock, mockAccount); 874 | const keyringController = await initializeKeyringController({ 875 | password: PASSWORD, 876 | constructorOptions: { 877 | keyringBuilders: [keyringBuilderFactory(BaseKeyringMock)], 878 | }, 879 | }); 880 | await keyringController.addNewKeyring(BaseKeyringMock.type); 881 | 882 | await expect( 883 | keyringController.removeAccount(mockAccount), 884 | ).rejects.toThrow(KeyringControllerError.UnsupportedRemoveAccount); 885 | }); 886 | }); 887 | 888 | describe('unlockKeyrings', () => { 889 | it('returns the list of keyrings', async () => { 890 | const keyringController = await initializeKeyringController({ 891 | password: PASSWORD, 892 | }); 893 | await keyringController.setLocked(); 894 | const keyrings = await keyringController.unlockKeyrings(PASSWORD); 895 | expect(keyrings).toHaveLength(1); 896 | await Promise.all( 897 | keyrings.map(async (keyring) => { 898 | // @ts-expect-error numberOfAccounts mising in Json specification. 899 | const { numberOfAccounts } = await keyring.serialize(); 900 | expect(numberOfAccounts).toBe(1); 901 | }), 902 | ); 903 | }); 904 | 905 | it('add serialized keyring to unsupportedKeyrings array if keyring type is not known', async () => { 906 | const mockEncryptor = new MockEncryptor(); 907 | const keyringController = await initializeKeyringController({ 908 | password: PASSWORD, 909 | constructorOptions: { 910 | encryptor: mockEncryptor, 911 | }, 912 | }); 913 | const unsupportedKeyrings = [{ type: 'Ledger Keyring', data: 'DUMMY' }]; 914 | await mockEncryptor.encrypt(PASSWORD, unsupportedKeyrings); 915 | await keyringController.setLocked(); 916 | 917 | const keyrings = await keyringController.unlockKeyrings(PASSWORD); 918 | 919 | expect(keyrings).toHaveLength(0); 920 | expect(keyringController.unsupportedKeyrings).toStrictEqual( 921 | unsupportedKeyrings, 922 | ); 923 | }); 924 | 925 | it('should throw error if there is no vault', async () => { 926 | const keyringController = new KeyringController({ 927 | cacheEncryptionKey: false, 928 | }); 929 | 930 | await expect(async () => 931 | keyringController.unlockKeyrings(PASSWORD), 932 | ).rejects.toThrow(KeyringControllerError.VaultError); 933 | }); 934 | 935 | it('should throw error if decrypted vault is not an array of serialized keyrings', async () => { 936 | const mockEncryptor = new MockEncryptor(); 937 | sinon 938 | .stub(mockEncryptor, 'decrypt') 939 | .resolves('[{"foo": "not a valid keyring}]'); 940 | const keyringController = await initializeKeyringController({ 941 | password: PASSWORD, 942 | constructorOptions: { 943 | encryptor: mockEncryptor, 944 | }, 945 | }); 946 | 947 | await expect(async () => 948 | keyringController.unlockKeyrings(PASSWORD), 949 | ).rejects.toThrow(KeyringControllerError.VaultDataError); 950 | }); 951 | 952 | it('should throw error if decrypted vault includes an invalid keyring', async () => { 953 | const mockEncryptor = new MockEncryptor(); 954 | sinon 955 | .stub(mockEncryptor, 'decrypt') 956 | .resolves([{ foo: 'not a valid keyring' }]); 957 | const keyringController = await initializeKeyringController({ 958 | password: PASSWORD, 959 | constructorOptions: { 960 | encryptor: mockEncryptor, 961 | }, 962 | }); 963 | 964 | await expect(async () => 965 | keyringController.unlockKeyrings(PASSWORD), 966 | ).rejects.toThrow(KeyringControllerError.VaultDataError); 967 | }); 968 | 969 | describe('with old vault format', () => { 970 | describe(`with cacheEncryptionKey = true and encryptionKey is unset`, () => { 971 | it('should update the vault', async () => { 972 | const mockEncryptor = new MockEncryptor(); 973 | const keyringController = await initializeKeyringController({ 974 | password: PASSWORD, 975 | constructorOptions: { 976 | cacheEncryptionKey: true, 977 | encryptor: mockEncryptor, 978 | }, 979 | }); 980 | deleteEncryptionKeyAndSalt(keyringController); 981 | const initialVault = keyringController.store.getState().vault; 982 | const mockEncryptionResult = { 983 | data: '0x1234', 984 | iv: 'an iv', 985 | }; 986 | sinon.stub(mockEncryptor, 'isVaultUpdated').returns(false); 987 | sinon 988 | .stub(mockEncryptor, 'encryptWithKey') 989 | .resolves(mockEncryptionResult); 990 | 991 | await keyringController.unlockKeyrings(PASSWORD); 992 | const updatedVault = keyringController.store.getState().vault; 993 | 994 | expect(initialVault).not.toBe(updatedVault); 995 | expect(updatedVault).toBe( 996 | JSON.stringify({ 997 | ...mockEncryptionResult, 998 | salt: MOCK_ENCRYPTION_SALT, 999 | }), 1000 | ); 1001 | }); 1002 | }); 1003 | 1004 | describe(`with cacheEncryptionKey = true and encryptionKey is set`, () => { 1005 | it('should not update the vault', async () => { 1006 | const mockEncryptor = new MockEncryptor(); 1007 | const keyringController = await initializeKeyringController({ 1008 | password: PASSWORD, 1009 | constructorOptions: { 1010 | cacheEncryptionKey: true, 1011 | encryptor: mockEncryptor, 1012 | }, 1013 | }); 1014 | const initialVault = keyringController.store.getState().vault; 1015 | sinon.stub(mockEncryptor, 'isVaultUpdated').returns(false); 1016 | 1017 | await keyringController.unlockKeyrings(PASSWORD); 1018 | const updatedVault = keyringController.store.getState().vault; 1019 | 1020 | expect(initialVault).toBe(updatedVault); 1021 | }); 1022 | }); 1023 | 1024 | describe(`with cacheEncryptionKey = false`, () => { 1025 | it('should update the vault', async () => { 1026 | const mockEncryptor = new MockEncryptor(); 1027 | const keyringController = await initializeKeyringController({ 1028 | password: PASSWORD, 1029 | constructorOptions: { 1030 | cacheEncryptionKey: false, 1031 | encryptor: mockEncryptor, 1032 | }, 1033 | }); 1034 | const initialVault = keyringController.store.getState().vault; 1035 | const updatedVaultMock = 1036 | '{"vault": "updated_vault_detail", "salt": "salt"}'; 1037 | sinon.stub(mockEncryptor, 'isVaultUpdated').returns(false); 1038 | sinon.stub(mockEncryptor, 'encrypt').resolves(updatedVaultMock); 1039 | 1040 | await keyringController.unlockKeyrings(PASSWORD); 1041 | const updatedVault = keyringController.store.getState().vault; 1042 | 1043 | expect(initialVault).not.toBe(updatedVault); 1044 | expect(updatedVault).toBe(updatedVaultMock); 1045 | }); 1046 | }); 1047 | }); 1048 | }); 1049 | 1050 | describe('verifyPassword', () => { 1051 | it('throws an error if no encrypted vault is in controller state', async () => { 1052 | const keyringController = await initializeKeyringController(); 1053 | 1054 | await expect(async () => 1055 | keyringController.verifyPassword('test'), 1056 | ).rejects.toThrow('Cannot unlock without a previous vault.'); 1057 | }); 1058 | 1059 | it('does not throw if a vault exists in state', async () => { 1060 | const keyringController = await initializeKeyringController(); 1061 | 1062 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 1063 | type: KeyringType.HD, 1064 | opts: { 1065 | mnemonic: walletOneSeedWords, 1066 | numberOfAccounts: 1, 1067 | }, 1068 | }); 1069 | 1070 | expect(async () => 1071 | keyringController.verifyPassword(PASSWORD), 1072 | ).not.toThrow(); 1073 | }); 1074 | }); 1075 | 1076 | describe('addNewAccount', () => { 1077 | it('adds a new account to the keyring it receives as an argument', async () => { 1078 | const keyringController = await initializeKeyringController({ 1079 | password: PASSWORD, 1080 | }); 1081 | const [HDKeyring] = keyringController.getKeyringsByType(KeyringType.HD); 1082 | const initialAccounts = await HDKeyring?.getAccounts(); 1083 | expect(initialAccounts).toHaveLength(1); 1084 | 1085 | // @ts-expect-error this value should never be undefined in this specific context. 1086 | await keyringController.addNewAccount(HDKeyring); 1087 | const accountsAfterAdd = await HDKeyring?.getAccounts(); 1088 | expect(accountsAfterAdd).toHaveLength(2); 1089 | }); 1090 | }); 1091 | 1092 | describe('getAppKeyAddress', () => { 1093 | it('returns the expected app key address', async () => { 1094 | const keyringController = await initializeKeyringController({ 1095 | password: PASSWORD, 1096 | }); 1097 | const address = '0x01560cd3bac62cc6d7e6380600d9317363400896'; 1098 | const privateKey = 1099 | '0xb8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'; 1100 | 1101 | const keyring = await keyringController.addNewKeyring( 1102 | KeyringType.Simple, 1103 | [privateKey], 1104 | ); 1105 | 1106 | const getAppKeyAddressSpy = sinon.spy( 1107 | keyringController, 1108 | 'getAppKeyAddress', 1109 | ); 1110 | 1111 | keyringController.getKeyringForAccount = sinon 1112 | .stub() 1113 | .returns(Promise.resolve(keyring)); 1114 | 1115 | await keyringController.getAppKeyAddress(address, 'someapp.origin.io'); 1116 | 1117 | expect(getAppKeyAddressSpy.calledOnce).toBe(true); 1118 | expect(getAppKeyAddressSpy.getCall(0).args[0]).toBe( 1119 | normalizeAddress(address), 1120 | ); 1121 | expect(getAppKeyAddressSpy.calledOnce).toBe(true); 1122 | expect(getAppKeyAddressSpy.getCall(0).args).toStrictEqual([ 1123 | normalizeAddress(address), 1124 | 'someapp.origin.io', 1125 | ]); 1126 | }); 1127 | 1128 | it('throws error if keyring for account does not support getAppKeyAddress', async () => { 1129 | const address = '0x01560cd3bac62cc6d7e6380600d9317363400896'; 1130 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1131 | const keyringController = await initializeKeyringController({ 1132 | password: PASSWORD, 1133 | }); 1134 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1135 | 1136 | await expect( 1137 | keyringController.getAppKeyAddress(address, 'someapp.origin.io'), 1138 | ).rejects.toThrow(KeyringControllerError.UnsupportedGetAppKeyAddress); 1139 | }); 1140 | }); 1141 | 1142 | describe('exportAppKeyForAddress', () => { 1143 | it('returns a unique key', async () => { 1144 | const keyringController = await initializeKeyringController({ 1145 | password: PASSWORD, 1146 | }); 1147 | const address = '0x01560cd3bac62cc6d7e6380600d9317363400896'; 1148 | const privateKey = 1149 | '0xb8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'; 1150 | await keyringController.addNewKeyring(KeyringType.Simple, [privateKey]); 1151 | const appKeyAddress = await keyringController.getAppKeyAddress( 1152 | address, 1153 | 'someapp.origin.io', 1154 | ); 1155 | 1156 | const privateAppKey = await keyringController.exportAppKeyForAddress( 1157 | address, 1158 | 'someapp.origin.io', 1159 | ); 1160 | 1161 | const wallet = Wallet.fromPrivateKey(Buffer.from(privateAppKey, 'hex')); 1162 | const recoveredAddress = bytesToHex(wallet.getAddress()); 1163 | 1164 | expect(recoveredAddress).toBe(appKeyAddress); 1165 | expect(privateAppKey).not.toBe(privateKey); 1166 | }); 1167 | 1168 | it('throws error if keyring for account does not support exportAppKeyForAddress', async () => { 1169 | const address = '0x01560cd3bac62cc6d7e6380600d9317363400896'; 1170 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1171 | const keyringController = await initializeKeyringController({ 1172 | password: PASSWORD, 1173 | }); 1174 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1175 | 1176 | await expect( 1177 | keyringController.exportAppKeyForAddress(address, 'someapp.origin.io'), 1178 | ).rejects.toThrow( 1179 | KeyringControllerError.UnsupportedExportAppKeyForAddress, 1180 | ); 1181 | }); 1182 | }); 1183 | 1184 | describe('getKeyringForAccount', () => { 1185 | it('throws error when address is not provided', async () => { 1186 | const keyringController = await initializeKeyringController({ 1187 | password: PASSWORD, 1188 | }); 1189 | await expect( 1190 | // @ts-expect-error Missing other required permission types. 1191 | keyringController.getKeyringForAccount(undefined), 1192 | ).rejects.toThrow( 1193 | new Error( 1194 | `${KeyringControllerError.NoKeyring}. Error info: The address passed in is invalid/empty`, 1195 | ), 1196 | ); 1197 | }); 1198 | 1199 | it('throws error when address is invalid', async () => { 1200 | const keyringController = await initializeKeyringController({ 1201 | password: PASSWORD, 1202 | }); 1203 | await expect( 1204 | keyringController.getKeyringForAccount('0x04'), 1205 | ).rejects.toThrow( 1206 | new Error( 1207 | `${KeyringControllerError.NoKeyring}. Error info: The address passed in is invalid/empty`, 1208 | ), 1209 | ); 1210 | }); 1211 | 1212 | it('throws error when there are no keyrings', async () => { 1213 | const keyringController = await initializeKeyringController({ 1214 | password: PASSWORD, 1215 | }); 1216 | keyringController.keyrings = []; 1217 | await expect( 1218 | keyringController.getKeyringForAccount( 1219 | '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1220 | ), 1221 | ).rejects.toThrow( 1222 | new Error( 1223 | `${KeyringControllerError.NoKeyring}. Error info: There are no keyrings`, 1224 | ), 1225 | ); 1226 | }); 1227 | 1228 | it('throws error when there are no matching keyrings', async () => { 1229 | const keyringController = await initializeKeyringController({ 1230 | password: PASSWORD, 1231 | }); 1232 | keyringController.keyrings = [ 1233 | { 1234 | // @ts-expect-error there's only a need to mock the getAccounts method for this test. 1235 | async getAccounts() { 1236 | return Promise.resolve([1, 2, 3]); 1237 | }, 1238 | }, 1239 | ]; 1240 | 1241 | await expect( 1242 | keyringController.getKeyringForAccount( 1243 | '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 1244 | ), 1245 | ).rejects.toThrow( 1246 | new Error( 1247 | `${KeyringControllerError.NoKeyring}. Error info: There are keyrings, but none match the address`, 1248 | ), 1249 | ); 1250 | }); 1251 | }); 1252 | 1253 | describe('cacheEncryptionKey', () => { 1254 | it('sets encryption key data upon submitPassword', async () => { 1255 | const keyringController = await initializeKeyringController({ 1256 | password: PASSWORD, 1257 | constructorOptions: { 1258 | cacheEncryptionKey: true, 1259 | }, 1260 | }); 1261 | await keyringController.submitPassword(PASSWORD); 1262 | 1263 | expect(keyringController.password).toBe(PASSWORD); 1264 | expect(keyringController.memStore.getState().encryptionSalt).toBe( 1265 | MOCK_ENCRYPTION_SALT, 1266 | ); 1267 | expect(keyringController.memStore.getState().encryptionKey).toStrictEqual( 1268 | expect.stringMatching('.+'), 1269 | ); 1270 | }); 1271 | 1272 | it('unlocks the keyrings with valid information', async () => { 1273 | const mockEncryptor = new MockEncryptor(); 1274 | const keyringController = await initializeKeyringController({ 1275 | password: PASSWORD, 1276 | constructorOptions: { 1277 | cacheEncryptionKey: true, 1278 | encryptor: mockEncryptor, 1279 | }, 1280 | }); 1281 | const returnValue = await mockEncryptor.decryptWithKey('', ''); 1282 | const decryptWithKeyStub = sinon 1283 | .stub(mockEncryptor, 'decryptWithKey') 1284 | .resolves(returnValue); 1285 | 1286 | keyringController.store.updateState({ vault: MOCK_ENCRYPTION_DATA }); 1287 | 1288 | await keyringController.setLocked(); 1289 | 1290 | await keyringController.submitEncryptionKey( 1291 | MOCK_ENCRYPTION_KEY, 1292 | MOCK_ENCRYPTION_SALT, 1293 | ); 1294 | 1295 | expect(decryptWithKeyStub.calledOnce).toBe(true); 1296 | expect(keyringController.keyrings).toHaveLength(1); 1297 | }); 1298 | 1299 | it('does not load keyrings when invalid encryptionKey format', async () => { 1300 | const keyringController = await initializeKeyringController({ 1301 | password: PASSWORD, 1302 | constructorOptions: { 1303 | cacheEncryptionKey: true, 1304 | }, 1305 | }); 1306 | await keyringController.setLocked(); 1307 | keyringController.store.updateState({ vault: MOCK_ENCRYPTION_DATA }); 1308 | 1309 | await expect( 1310 | keyringController.submitEncryptionKey(`{}`, MOCK_ENCRYPTION_SALT), 1311 | ).rejects.toThrow( 1312 | `Failed to execute 'importKey' on 'SubtleCrypto': The provided value is not of type '(ArrayBuffer or ArrayBufferView or JsonWebKey)'.`, 1313 | ); 1314 | expect(keyringController.password).toBeUndefined(); 1315 | expect(keyringController.keyrings).toHaveLength(0); 1316 | }); 1317 | 1318 | it('does not load keyrings when encryptionKey is expired', async () => { 1319 | const keyringController = await initializeKeyringController({ 1320 | password: PASSWORD, 1321 | constructorOptions: { 1322 | cacheEncryptionKey: true, 1323 | }, 1324 | }); 1325 | await keyringController.setLocked(); 1326 | keyringController.store.updateState({ vault: MOCK_ENCRYPTION_DATA }); 1327 | 1328 | await expect( 1329 | keyringController.submitEncryptionKey( 1330 | MOCK_ENCRYPTION_KEY, 1331 | 'OUTDATED_SALT', 1332 | ), 1333 | ).rejects.toThrow('Encryption key and salt provided are expired'); 1334 | expect(keyringController.password).toBeUndefined(); 1335 | expect(keyringController.keyrings).toHaveLength(0); 1336 | }); 1337 | 1338 | it('persists keyrings when actions are performed', async () => { 1339 | const keyringController = await initializeKeyringController({ 1340 | password: PASSWORD, 1341 | constructorOptions: { 1342 | cacheEncryptionKey: true, 1343 | }, 1344 | }); 1345 | await keyringController.setLocked(); 1346 | keyringController.store.updateState({ vault: MOCK_ENCRYPTION_DATA }); 1347 | await keyringController.submitEncryptionKey( 1348 | MOCK_ENCRYPTION_KEY, 1349 | MOCK_ENCRYPTION_SALT, 1350 | ); 1351 | 1352 | const [firstKeyring] = keyringController.keyrings; 1353 | 1354 | // @ts-expect-error this value should never be undefined in this specific context. 1355 | await keyringController.addNewAccount(firstKeyring); 1356 | expect(await keyringController.getAccounts()).toHaveLength(2); 1357 | 1358 | // @ts-expect-error this value should never be undefined in this specific context. 1359 | await keyringController.addNewAccount(firstKeyring); 1360 | expect(await keyringController.getAccounts()).toHaveLength(3); 1361 | 1362 | const account: { privateKey: string; publicKey: Hex } = { 1363 | privateKey: 1364 | 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3', 1365 | publicKey: '0x627306090abab3a6e1400e9345bc60c78a8bef57', 1366 | }; 1367 | 1368 | // Add a new keyring with one account 1369 | await keyringController.addNewKeyring(KeyringType.Simple, [ 1370 | account.privateKey, 1371 | ]); 1372 | expect(await keyringController.getAccounts()).toHaveLength(4); 1373 | 1374 | // remove that account that we just added 1375 | await keyringController.removeAccount(account.publicKey); 1376 | expect(await keyringController.getAccounts()).toHaveLength(3); 1377 | }); 1378 | 1379 | it('triggers an error when trying to persist without password or encryption key', async () => { 1380 | const keyringController = await initializeKeyringController({ 1381 | password: PASSWORD, 1382 | }); 1383 | delete keyringController.password; 1384 | await expect(keyringController.persistAllKeyrings()).rejects.toThrow( 1385 | 'Cannot persist vault without password and encryption key', 1386 | ); 1387 | }); 1388 | 1389 | it('cleans up login artifacts upon lock', async () => { 1390 | const keyringController = await initializeKeyringController({ 1391 | password: PASSWORD, 1392 | constructorOptions: { 1393 | cacheEncryptionKey: true, 1394 | }, 1395 | }); 1396 | await keyringController.submitPassword(PASSWORD); 1397 | expect(keyringController.password).toBe(PASSWORD); 1398 | expect( 1399 | keyringController.memStore.getState().encryptionSalt, 1400 | ).toStrictEqual(expect.stringMatching('.+')); 1401 | 1402 | expect(keyringController.memStore.getState().encryptionKey).toStrictEqual( 1403 | expect.stringMatching('.+'), 1404 | ); 1405 | 1406 | await keyringController.setLocked(); 1407 | 1408 | expect( 1409 | keyringController.memStore.getState().encryptionSalt, 1410 | ).toBeUndefined(); 1411 | expect(keyringController.password).toBeUndefined(); 1412 | expect( 1413 | keyringController.memStore.getState().encryptionKey, 1414 | ).toBeUndefined(); 1415 | }); 1416 | }); 1417 | 1418 | describe('exportAccount', () => { 1419 | it('returns the private key for the public key it is passed', async () => { 1420 | const keyringController = await initializeKeyringController({ 1421 | password: PASSWORD, 1422 | }); 1423 | await keyringController.createNewVaultWithKeyring(PASSWORD, { 1424 | type: KeyringType.HD, 1425 | opts: { 1426 | mnemonic: walletOneSeedWords, 1427 | numberOfAccounts: 1, 1428 | }, 1429 | }); 1430 | const privateKey = await keyringController.exportAccount( 1431 | // @ts-expect-error this value should never be undefined in this specific context. 1432 | walletOneAddresses[0], 1433 | ); 1434 | expect(privateKey).toStrictEqual(walletOnePrivateKey[0]); 1435 | }); 1436 | 1437 | it('throws an error if the keyring type does not support export', async () => { 1438 | const mockAccount = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1439 | stubKeyringClassWithAccount(KeyringMockWithInit, mockAccount); 1440 | const keyringController = await initializeKeyringController({ 1441 | password: PASSWORD, 1442 | constructorOptions: { 1443 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithInit)], 1444 | }, 1445 | }); 1446 | await keyringController.addNewKeyring(KeyringMockWithInit.type); 1447 | 1448 | await expect( 1449 | keyringController.exportAccount( 1450 | '0x5aC6d462f054690A373Fabf8cc28E161003aEB19', 1451 | ), 1452 | ).rejects.toThrow(KeyringControllerError.UnsupportedExportAccount); 1453 | }); 1454 | }); 1455 | 1456 | describe('signTransaction', () => { 1457 | it('throws error if the address is invalid', async () => { 1458 | const mockTransaction = buildMockTransaction(); 1459 | const keyringController = await initializeKeyringController({ 1460 | password: PASSWORD, 1461 | }); 1462 | 1463 | await expect( 1464 | keyringController.signTransaction(mockTransaction, '0x0'), 1465 | ).rejects.toThrow( 1466 | new Error( 1467 | `${KeyringControllerError.NoKeyring}. Error info: The address passed in is invalid/empty`, 1468 | ), 1469 | ); 1470 | }); 1471 | 1472 | it('throws error if keyring for the given address does not support signTransaction', async () => { 1473 | const mockTransaction = buildMockTransaction(); 1474 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1475 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1476 | const keyringController = await initializeKeyringController({ 1477 | password: PASSWORD, 1478 | constructorOptions: { 1479 | keyringBuilders: [keyringBuilderFactory(BaseKeyringMock)], 1480 | }, 1481 | }); 1482 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1483 | 1484 | await expect( 1485 | keyringController.signTransaction(mockTransaction, address), 1486 | ).rejects.toThrow(KeyringControllerError.UnsupportedSignTransaction); 1487 | }); 1488 | 1489 | it('forwards the transaction to the keyring for signing', async () => { 1490 | const mockTransaction = buildMockTransaction(); 1491 | const address = '0x5ac6d462f054690a373fabf8cc28e161003aeb19'; 1492 | stubKeyringClassWithAccount(KeyringMockWithSignTransaction, address); 1493 | const keyringController = await initializeKeyringController({ 1494 | password: PASSWORD, 1495 | constructorOptions: { 1496 | keyringBuilders: [ 1497 | keyringBuilderFactory(KeyringMockWithSignTransaction), 1498 | ], 1499 | }, 1500 | }); 1501 | const signTransactionSpy = sinon.spy( 1502 | KeyringMockWithSignTransaction.prototype, 1503 | 'signTransaction', 1504 | ); 1505 | await keyringController.addNewKeyring( 1506 | KeyringMockWithSignTransaction.type, 1507 | ); 1508 | 1509 | await keyringController.signTransaction(mockTransaction, address); 1510 | 1511 | expect(signTransactionSpy.calledOnce).toBe(true); 1512 | expect(signTransactionSpy.getCall(0).args[0]).toStrictEqual(address); 1513 | expect(signTransactionSpy.getCall(0).args[1]).toStrictEqual( 1514 | mockTransaction, 1515 | ); 1516 | }); 1517 | }); 1518 | 1519 | describe('signMessage', () => { 1520 | it('should sign message', async () => { 1521 | const keyringController = await initializeKeyringController({ 1522 | password: PASSWORD, 1523 | seedPhrase: walletOneSeedWords, 1524 | }); 1525 | const inputParams = { 1526 | from: walletOneAddresses[0], 1527 | data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', 1528 | origin: 'https://metamask.github.io', 1529 | }; 1530 | // @ts-expect-error this value should never be undefined in this specific context. 1531 | const result = await keyringController.signMessage(inputParams); 1532 | expect(result).toMatchInlineSnapshot( 1533 | `"0x93e0035090e8144debae03f45c5339a78d24c41e38e810a82dd3387e48353db645bd77716f3b7c4fb1f07f3b97bdbd33b0d7c55f7e7eedf3a678a2081948b67f1c"`, 1534 | ); 1535 | }); 1536 | 1537 | it('should throw if the keyring for the given address does not support signMessage', async () => { 1538 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1539 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1540 | const keyringController = await initializeKeyringController({ 1541 | password: PASSWORD, 1542 | }); 1543 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1544 | const inputParams = { 1545 | from: address, 1546 | data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', 1547 | origin: 'https://metamask.github.io', 1548 | }; 1549 | 1550 | await expect(keyringController.signMessage(inputParams)).rejects.toThrow( 1551 | KeyringControllerError.UnsupportedSignMessage, 1552 | ); 1553 | }); 1554 | }); 1555 | 1556 | describe('prepareUserOperation', () => { 1557 | it('converts a base transaction to a base UserOperation', async () => { 1558 | const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95'; 1559 | stubKeyringClassWithAccount(KeyringMockWithUserOp, sender); 1560 | const baseTxs = [ 1561 | { 1562 | to: '0x8cBC0EA145491fe83104abA9ef916f8632367227', 1563 | value: '0x0', 1564 | data: '0x', 1565 | }, 1566 | ]; 1567 | const baseUserOp = { 1568 | callData: '0x7064', 1569 | initCode: '0x22ff', 1570 | nonce: '0x1', 1571 | gasLimits: { 1572 | callGasLimit: '0x58a83', 1573 | verificationGasLimit: '0xe8c4', 1574 | preVerificationGas: '0xc57c', 1575 | }, 1576 | dummySignature: '0x0000', 1577 | dummyPaymasterAndData: '0x', 1578 | bundlerUrl: 'https://bundler.example.com/rpc', 1579 | }; 1580 | const keyringController = await initializeKeyringController({ 1581 | password: PASSWORD, 1582 | constructorOptions: { 1583 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithUserOp)], 1584 | }, 1585 | }); 1586 | await keyringController.addNewKeyring(KeyringMockWithUserOp.type); 1587 | jest 1588 | .spyOn(KeyringMockWithUserOp.prototype, 'prepareUserOperation') 1589 | .mockResolvedValueOnce(baseUserOp); 1590 | 1591 | const result = await keyringController.prepareUserOperation( 1592 | sender, 1593 | baseTxs, 1594 | ); 1595 | 1596 | expect(result).toStrictEqual(baseUserOp); 1597 | }); 1598 | 1599 | it("throws when the keyring doesn't implement prepareUserOperation", async () => { 1600 | const keyringController = await initializeKeyringController({ 1601 | password: PASSWORD, 1602 | seedPhrase: walletOneSeedWords, 1603 | }); 1604 | const txs = [ 1605 | { 1606 | to: '0x8cBC0EA145491fe83104abA9ef916f8632367227', 1607 | value: '0x0', 1608 | data: '0x', 1609 | }, 1610 | ]; 1611 | 1612 | const result = keyringController.prepareUserOperation( 1613 | walletOneAddresses[0] as string, 1614 | txs, 1615 | ); 1616 | 1617 | await expect(result).rejects.toThrow( 1618 | 'KeyringController - The keyring for the current address does not support the method prepareUserOperation.', 1619 | ); 1620 | }); 1621 | }); 1622 | 1623 | describe('patchUserOperation', () => { 1624 | it('uses the patchUserOperation method on the keyring to patch properties of a UserOperation', async () => { 1625 | const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95'; 1626 | stubKeyringClassWithAccount(KeyringMockWithUserOp, sender); 1627 | const keyringController = await initializeKeyringController({ 1628 | password: PASSWORD, 1629 | constructorOptions: { 1630 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithUserOp)], 1631 | }, 1632 | }); 1633 | await keyringController.addNewKeyring(KeyringMockWithUserOp.type); 1634 | const userOp = { 1635 | sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4', 1636 | nonce: '0x1', 1637 | initCode: '0x', 1638 | callData: '0x7064', 1639 | callGasLimit: '0x58a83', 1640 | verificationGasLimit: '0xe8c4', 1641 | preVerificationGas: '0xc57c', 1642 | maxFeePerGas: '0x87f0878c0', 1643 | maxPriorityFeePerGas: '0x1dcd6500', 1644 | paymasterAndData: '0x', 1645 | signature: '0x', 1646 | }; 1647 | const patch = { 1648 | paymasterAndData: '0x1234', 1649 | }; 1650 | jest 1651 | .spyOn(KeyringMockWithUserOp.prototype, 'patchUserOperation') 1652 | .mockResolvedValueOnce(patch); 1653 | 1654 | const result = await keyringController.patchUserOperation(sender, userOp); 1655 | 1656 | expect(result).toStrictEqual(patch); 1657 | }); 1658 | 1659 | it("throws when the keyring doesn't implement patchUserOperation", async () => { 1660 | const keyringController = await initializeKeyringController({ 1661 | password: PASSWORD, 1662 | seedPhrase: walletOneSeedWords, 1663 | }); 1664 | 1665 | const userOp = { 1666 | sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4', 1667 | nonce: '0x1', 1668 | initCode: '0x', 1669 | callData: '0x7064', 1670 | callGasLimit: '0x58a83', 1671 | verificationGasLimit: '0xe8c4', 1672 | preVerificationGas: '0xc57c', 1673 | maxFeePerGas: '0x87f0878c0', 1674 | maxPriorityFeePerGas: '0x1dcd6500', 1675 | paymasterAndData: '0x', 1676 | signature: '0x', 1677 | }; 1678 | 1679 | const result = keyringController.patchUserOperation( 1680 | walletOneAddresses[0] as string, 1681 | userOp, 1682 | ); 1683 | 1684 | await expect(result).rejects.toThrow( 1685 | 'KeyringController - The keyring for the current address does not support the method patchUserOperation.', 1686 | ); 1687 | }); 1688 | }); 1689 | 1690 | describe('signUserOperation', () => { 1691 | it('calls the signUserOperation method on the keyring, returning its result', async () => { 1692 | const sender = '0x998B3FBB8159aF51a827DBf43A8054A5A3A28c95'; 1693 | stubKeyringClassWithAccount(KeyringMockWithUserOp, sender); 1694 | const keyringController = await initializeKeyringController({ 1695 | password: PASSWORD, 1696 | constructorOptions: { 1697 | keyringBuilders: [keyringBuilderFactory(KeyringMockWithUserOp)], 1698 | }, 1699 | }); 1700 | await keyringController.addNewKeyring(KeyringMockWithUserOp.type); 1701 | const userOp = { 1702 | sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4', 1703 | nonce: '0x1', 1704 | initCode: '0x', 1705 | callData: '0x7064', 1706 | callGasLimit: '0x58a83', 1707 | verificationGasLimit: '0xe8c4', 1708 | preVerificationGas: '0xc57c', 1709 | maxFeePerGas: '0x87f0878c0', 1710 | maxPriorityFeePerGas: '0x1dcd6500', 1711 | paymasterAndData: '0x', 1712 | signature: '0x', 1713 | }; 1714 | const signature = '0x1234'; 1715 | jest 1716 | .spyOn(KeyringMockWithUserOp.prototype, 'signUserOperation') 1717 | .mockResolvedValueOnce(signature); 1718 | 1719 | const result = await keyringController.signUserOperation(sender, userOp); 1720 | 1721 | expect(result).toStrictEqual(signature); 1722 | }); 1723 | 1724 | it("throws when the keyring doesn't implement signUserOperation", async () => { 1725 | const keyringController = await initializeKeyringController({ 1726 | password: PASSWORD, 1727 | seedPhrase: walletOneSeedWords, 1728 | }); 1729 | const userOp = { 1730 | sender: '0x4584d2B4905087A100420AFfCe1b2d73fC69B8E4', 1731 | nonce: '0x1', 1732 | initCode: '0x', 1733 | callData: '0x7064', 1734 | callGasLimit: '0x58a83', 1735 | verificationGasLimit: '0xe8c4', 1736 | preVerificationGas: '0xc57c', 1737 | maxFeePerGas: '0x87f0878c0', 1738 | maxPriorityFeePerGas: '0x1dcd6500', 1739 | paymasterAndData: '0x', 1740 | signature: '0x', 1741 | }; 1742 | 1743 | const result = keyringController.signUserOperation( 1744 | walletOneAddresses[0] as string, 1745 | userOp, 1746 | ); 1747 | 1748 | await expect(result).rejects.toThrow( 1749 | 'KeyringController - The keyring for the current address does not support the method signUserOperation.', 1750 | ); 1751 | }); 1752 | }); 1753 | 1754 | describe('signPersonalMessage', () => { 1755 | it('signPersonalMessage', async () => { 1756 | const keyringController = await initializeKeyringController({ 1757 | password: PASSWORD, 1758 | seedPhrase: walletOneSeedWords, 1759 | }); 1760 | const inputParams = { 1761 | from: walletOneAddresses[0], 1762 | data: '0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765', 1763 | origin: 'https://metamask.github.io', 1764 | }; 1765 | // @ts-expect-error this value should never be undefined in this specific context. 1766 | const result = await keyringController.signPersonalMessage(inputParams); 1767 | expect(result).toBe( 1768 | '0xfa2e5989b483e1f40a41b306f275b0009bcc07bfe5322c87682145e7d4889a3247182b4bd8138a965a7e37dea9d9b492b6f9f6d01185412f2d80466237b2805e1b', 1769 | ); 1770 | }); 1771 | 1772 | it('should throw if the keyring for the given address does not support signPersonalMessage', async () => { 1773 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1774 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1775 | const keyringController = await initializeKeyringController({ 1776 | password: PASSWORD, 1777 | }); 1778 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1779 | const inputParams = { 1780 | from: address, 1781 | data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', 1782 | origin: 'https://metamask.github.io', 1783 | }; 1784 | 1785 | await expect( 1786 | keyringController.signPersonalMessage(inputParams), 1787 | ).rejects.toThrow(KeyringControllerError.UnsupportedSignPersonalMessage); 1788 | }); 1789 | }); 1790 | 1791 | describe('getEncryptionPublicKey', () => { 1792 | it('should get the correct encryption public key', async () => { 1793 | const keyringController = await initializeKeyringController({ 1794 | password: PASSWORD, 1795 | seedPhrase: walletOneSeedWords, 1796 | }); 1797 | const result = await keyringController.getEncryptionPublicKey( 1798 | // @ts-expect-error this value should never be undefined in this specific context. 1799 | walletOneAddresses[0], 1800 | ); 1801 | expect(result).toBe('SR6bQ1m3OTHvI1FLwcGzm+Uk6hffoFPxsQ0DTOeKMEc='); 1802 | }); 1803 | 1804 | it('should throw if the keyring for the given address does not support getEncryptionPublicKey', async () => { 1805 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1806 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1807 | const keyringController = await initializeKeyringController({ 1808 | password: PASSWORD, 1809 | }); 1810 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1811 | 1812 | await expect( 1813 | keyringController.getEncryptionPublicKey(address), 1814 | ).rejects.toThrow( 1815 | KeyringControllerError.UnsupportedGetEncryptionPublicKey, 1816 | ); 1817 | }); 1818 | }); 1819 | 1820 | describe('decryptMessage', () => { 1821 | it('should decrypt a message encrypted with the encryption public key', async () => { 1822 | const keyringController = await initializeKeyringController({ 1823 | password: PASSWORD, 1824 | seedPhrase: walletOneSeedWords, 1825 | }); 1826 | const inputParams = { 1827 | from: walletOneAddresses[0] as string, 1828 | data: { 1829 | ciphertext: 'piu57JEMwAp/Q6+SA7eTels+YoeUAdcu+68FMgKEir80pxT34Luq', 1830 | ephemPublicKey: 'BRI699kE0tSMq5IRjCYmFAVDDEbL3bKz7/NMHFoev1w=', 1831 | nonce: 'oJqpL0ObAh+NVggV67bO+BTmCGmV//rz', 1832 | version: 'x25519-xsalsa20-poly1305', 1833 | }, 1834 | }; 1835 | 1836 | const result = await keyringController.decryptMessage(inputParams); 1837 | 1838 | expect(result).toBe('Hello, encrypted world!'); 1839 | }); 1840 | 1841 | it('should throw if the keyring for the given address does not support decryptMessage', async () => { 1842 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 1843 | stubKeyringClassWithAccount(BaseKeyringMock, address); 1844 | const keyringController = await initializeKeyringController({ 1845 | password: PASSWORD, 1846 | }); 1847 | await keyringController.addNewKeyring(BaseKeyringMock.type); 1848 | const inputParams = { 1849 | from: address, 1850 | data: { 1851 | ciphertext: 'piu57JEMwAp/Q6+SA7eTels+YoeUAdcu+68FMgKEir80pxT34Luq', 1852 | ephemPublicKey: 'BRI699kE0tSMq5IRjCYmFAVDDEbL3bKz7/NMHFoev1w=', 1853 | nonce: 'oJqpL0ObAh+NVggV67bO+BTmCGmV//rz', 1854 | version: 'x25519-xsalsa20-poly1305', 1855 | }, 1856 | }; 1857 | 1858 | await expect( 1859 | keyringController.decryptMessage(inputParams), 1860 | ).rejects.toThrow(KeyringControllerError.UnsupportedDecryptMessage); 1861 | }); 1862 | }); 1863 | 1864 | describe('signTypedMessage', () => { 1865 | it('should sign a v1 typed message if no version is provided', async () => { 1866 | const keyringController = await initializeKeyringController({ 1867 | password: PASSWORD, 1868 | seedPhrase: walletOneSeedWords, 1869 | }); 1870 | const inputParams = { 1871 | from: mockAddress, 1872 | data: [ 1873 | { 1874 | type: 'string', 1875 | name: 'Message', 1876 | value: 'Hi, Alice!', 1877 | }, 1878 | { 1879 | type: 'uint32', 1880 | name: 'A number', 1881 | value: '1337', 1882 | }, 1883 | ], 1884 | origin: 'https://metamask.github.io', 1885 | }; 1886 | const result = await keyringController.signTypedMessage(inputParams); 1887 | expect(result).toMatchInlineSnapshot( 1888 | `"0x089bb031f5bf2b2cbdf49eb2bb37d6071ab71f950b9dc49e398ca2ba984aca3c189b3b8de6c14c56461460dd9f59443340f1b144aeeff73275ace41ac184e54f1c"`, 1889 | ); 1890 | }); 1891 | 1892 | it('should sign a v1 typed message', async () => { 1893 | const keyringController = await initializeKeyringController({ 1894 | password: PASSWORD, 1895 | seedPhrase: walletOneSeedWords, 1896 | }); 1897 | const inputParams = { 1898 | from: mockAddress, 1899 | data: [ 1900 | { 1901 | type: 'string', 1902 | name: 'Message', 1903 | value: 'Hi, Alice!', 1904 | }, 1905 | { 1906 | type: 'uint32', 1907 | name: 'A number', 1908 | value: '1337', 1909 | }, 1910 | ], 1911 | origin: 'https://metamask.github.io', 1912 | }; 1913 | const result = await keyringController.signTypedMessage(inputParams, { 1914 | version: 'V1', 1915 | }); 1916 | expect(result).toMatchInlineSnapshot( 1917 | `"0x089bb031f5bf2b2cbdf49eb2bb37d6071ab71f950b9dc49e398ca2ba984aca3c189b3b8de6c14c56461460dd9f59443340f1b144aeeff73275ace41ac184e54f1c"`, 1918 | ); 1919 | }); 1920 | 1921 | it('should sign a v3 typed message', async () => { 1922 | const keyringController = await initializeKeyringController({ 1923 | password: PASSWORD, 1924 | seedPhrase: walletOneSeedWords, 1925 | }); 1926 | const typedData = { 1927 | types: { 1928 | EIP712Domain: [ 1929 | { name: 'name', type: 'string' }, 1930 | { name: 'version', type: 'string' }, 1931 | { name: 'chainId', type: 'uint256' }, 1932 | { name: 'verifyingContract', type: 'address' }, 1933 | ], 1934 | Person: [ 1935 | { name: 'name', type: 'string' }, 1936 | { name: 'wallet', type: 'address' }, 1937 | ], 1938 | Mail: [ 1939 | { name: 'from', type: 'Person' }, 1940 | { name: 'to', type: 'Person' }, 1941 | { name: 'contents', type: 'string' }, 1942 | ], 1943 | }, 1944 | primaryType: 'Mail' as const, 1945 | domain: { 1946 | name: 'Ether Mail', 1947 | version: '1', 1948 | chainId: 1, 1949 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 1950 | }, 1951 | message: { 1952 | from: { 1953 | name: 'Cow', 1954 | wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 1955 | }, 1956 | to: { 1957 | name: 'Bob', 1958 | wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', 1959 | }, 1960 | contents: 'Hello, Bob!', 1961 | }, 1962 | }; 1963 | const inputParams = { 1964 | from: mockAddress, 1965 | data: typedData, 1966 | origin: 'https://metamask.github.io', 1967 | }; 1968 | const result = await keyringController.signTypedMessage(inputParams, { 1969 | version: 'V3', 1970 | }); 1971 | expect(result).toMatchInlineSnapshot( 1972 | `"0x1c496cc9f42fc8f8a30bef731b20a1b8722569473643c0cd92e3e494be9c62725043275475ca81d9691c6c31e188dfbd5884b4352ba21bd99f38e6d357c738b81b"`, 1973 | ); 1974 | }); 1975 | 1976 | it('should sign a v4 typed message', async () => { 1977 | const keyringController = await initializeKeyringController({ 1978 | password: PASSWORD, 1979 | seedPhrase: walletOneSeedWords, 1980 | }); 1981 | const typedData = { 1982 | types: { 1983 | EIP712Domain: [ 1984 | { name: 'name', type: 'string' }, 1985 | { name: 'version', type: 'string' }, 1986 | { name: 'chainId', type: 'uint256' }, 1987 | { name: 'verifyingContract', type: 'address' }, 1988 | ], 1989 | Person: [ 1990 | { name: 'name', type: 'string' }, 1991 | { name: 'wallet', type: 'address[]' }, 1992 | ], 1993 | Mail: [ 1994 | { name: 'from', type: 'Person' }, 1995 | { name: 'to', type: 'Person[]' }, 1996 | { name: 'contents', type: 'string' }, 1997 | ], 1998 | }, 1999 | primaryType: 'Mail' as const, 2000 | domain: { 2001 | name: 'Ether Mail', 2002 | version: '1', 2003 | chainId: 1, 2004 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 2005 | }, 2006 | message: { 2007 | from: { 2008 | name: 'Cow', 2009 | wallet: [ 2010 | '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 2011 | '0xDD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', 2012 | ], 2013 | }, 2014 | to: [ 2015 | { 2016 | name: 'Bob', 2017 | wallet: ['0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'], 2018 | }, 2019 | ], 2020 | contents: 'Hello, Bob!', 2021 | }, 2022 | }; 2023 | const inputParams = { 2024 | from: mockAddress, 2025 | data: typedData, 2026 | origin: 'https://metamask.github.io', 2027 | }; 2028 | const result = await keyringController.signTypedMessage(inputParams, { 2029 | version: 'V4', 2030 | }); 2031 | expect(result).toMatchInlineSnapshot( 2032 | `"0xe8d6baed58a611bbe247aecf2a8cbe0e3877bf1828c6bd9402749ce9e16f557a5669102bd05f0c3e33c200ff965abf07dab9299cb4bcdc504c9a695205240b321c"`, 2033 | ); 2034 | }); 2035 | 2036 | it('should throw if the keyring for the given address does not support signTypedMessage', async () => { 2037 | const address = '0x5aC6d462f054690A373Fabf8cc28E161003aEB19'; 2038 | stubKeyringClassWithAccount(BaseKeyringMock, address); 2039 | const keyringController = await initializeKeyringController({ 2040 | password: PASSWORD, 2041 | }); 2042 | await keyringController.addNewKeyring(BaseKeyringMock.type); 2043 | const inputParams = { 2044 | from: address, 2045 | data: [ 2046 | { 2047 | type: 'string', 2048 | name: 'Message', 2049 | value: 'Hi, Alice!', 2050 | }, 2051 | { 2052 | type: 'uint32', 2053 | name: 'A number', 2054 | value: '1337', 2055 | }, 2056 | ], 2057 | origin: 'https://metamask.github.io', 2058 | }; 2059 | 2060 | await expect( 2061 | keyringController.signTypedMessage(inputParams), 2062 | ).rejects.toThrow(KeyringControllerError.UnsupportedSignTypedMessage); 2063 | }); 2064 | }); 2065 | }); 2066 | -------------------------------------------------------------------------------- /src/KeyringController.ts: -------------------------------------------------------------------------------- 1 | import type { TypedTransaction, TxData } from '@ethereumjs/tx'; 2 | import * as encryptorUtils from '@metamask/browser-passworder'; 3 | import HDKeyring from '@metamask/eth-hd-keyring'; 4 | import { normalize as normalizeToHex } from '@metamask/eth-sig-util'; 5 | import SimpleKeyring from '@metamask/eth-simple-keyring'; 6 | import type { 7 | EthBaseTransaction, 8 | EthBaseUserOperation, 9 | EthKeyring, 10 | EthUserOperation, 11 | EthUserOperationPatch, 12 | } from '@metamask/keyring-api'; 13 | import { ObservableStore } from '@metamask/obs-store'; 14 | import { 15 | remove0x, 16 | isValidHexAddress, 17 | isObject, 18 | isValidJson, 19 | } from '@metamask/utils'; 20 | import type { 21 | Hex, 22 | Json, 23 | KeyringClass, 24 | Eip1024EncryptedData, 25 | } from '@metamask/utils'; 26 | // TODO: Stop using `events`, and remove the notice about this from the README 27 | // eslint-disable-next-line import/no-nodejs-modules 28 | import { EventEmitter } from 'events'; 29 | 30 | import { KeyringType, KeyringControllerError } from './constants'; 31 | import type { 32 | SerializedKeyring, 33 | KeyringControllerArgs, 34 | KeyringControllerState, 35 | KeyringControllerPersistentState, 36 | GenericEncryptor, 37 | ExportableKeyEncryptor, 38 | } from './types'; 39 | import { throwError } from './utils'; 40 | 41 | const defaultKeyringBuilders = [ 42 | keyringBuilderFactory(SimpleKeyring), 43 | keyringBuilderFactory(HDKeyring), 44 | ]; 45 | 46 | class KeyringController extends EventEmitter { 47 | keyringBuilders: { (): EthKeyring; type: string }[]; 48 | 49 | public store: ObservableStore; 50 | 51 | public memStore: ObservableStore; 52 | 53 | public keyrings: EthKeyring[]; 54 | 55 | public unsupportedKeyrings: SerializedKeyring[]; 56 | 57 | public password?: string; 58 | 59 | #encryptor: GenericEncryptor | ExportableKeyEncryptor; 60 | 61 | #cacheEncryptionKey: boolean; 62 | 63 | constructor({ 64 | keyringBuilders, 65 | cacheEncryptionKey, 66 | initState = {}, 67 | encryptor = encryptorUtils, 68 | }: KeyringControllerArgs) { 69 | super(); 70 | this.keyringBuilders = keyringBuilders 71 | ? defaultKeyringBuilders.concat(keyringBuilders) 72 | : defaultKeyringBuilders; 73 | this.store = new ObservableStore(initState); 74 | this.memStore = new ObservableStore({ 75 | isUnlocked: false, 76 | keyringTypes: this.keyringBuilders.map( 77 | (keyringBuilder) => keyringBuilder.type, 78 | ), 79 | keyrings: [], 80 | }); 81 | 82 | this.#encryptor = encryptor; 83 | this.keyrings = []; 84 | this.unsupportedKeyrings = []; 85 | 86 | // This option allows the controller to cache an exported key 87 | // for use in decrypting and encrypting data without password 88 | this.#cacheEncryptionKey = Boolean(cacheEncryptionKey); 89 | if (this.#cacheEncryptionKey) { 90 | assertIsExportableKeyEncryptor(encryptor); 91 | } 92 | } 93 | 94 | /** 95 | * Full Update 96 | * 97 | * Emits the `update` event and @returns a Promise that resolves to 98 | * the current state. 99 | * 100 | * Frequently used to end asynchronous chains in this class, 101 | * indicating consumers can often either listen for updates, 102 | * or accept a state-resolving promise to consume their results. 103 | * 104 | * @returns The controller state. 105 | */ 106 | fullUpdate() { 107 | this.emit('update', this.memStore.getState()); 108 | return this.memStore.getState(); 109 | } 110 | 111 | /** 112 | * ======================================= 113 | * === Public Vault Management Methods === 114 | * ======================================= 115 | */ 116 | 117 | /** 118 | * Create new vault And with a specific keyring 119 | * 120 | * Destroys any old encrypted storage, 121 | * creates a new encrypted store with the given password, 122 | * creates a new wallet with 1 account. 123 | * 124 | * @fires KeyringController#unlock 125 | * @param password - The password to encrypt the vault with. 126 | * @param keyring - A object containing the params to instantiate a new keyring. 127 | * @param keyring.type - The keyring type. 128 | * @param keyring.opts - Optional parameters required to instantiate the keyring. 129 | * @returns A promise that resolves to the state. 130 | */ 131 | async createNewVaultWithKeyring( 132 | password: string, 133 | keyring: { 134 | type: string; 135 | opts?: unknown; 136 | }, 137 | ): Promise { 138 | if (typeof password !== 'string') { 139 | throw new TypeError(KeyringControllerError.WrongPasswordType); 140 | } 141 | this.password = password; 142 | 143 | await this.#clearKeyrings(); 144 | await this.#createKeyring(keyring.type, keyring.opts); 145 | this.#setUnlocked(); 146 | return this.fullUpdate(); 147 | } 148 | 149 | /** 150 | * Set Locked. 151 | * This method deallocates all secrets, and effectively locks MetaMask. 152 | * 153 | * @fires KeyringController#lock 154 | * @returns A promise that resolves to the state. 155 | */ 156 | async setLocked(): Promise { 157 | delete this.password; 158 | 159 | // set locked 160 | this.memStore.putState({ 161 | isUnlocked: false, 162 | keyrings: [], 163 | }); 164 | 165 | // remove keyrings 166 | await this.#clearKeyrings(); 167 | this.emit('lock'); 168 | return this.fullUpdate(); 169 | } 170 | 171 | /** 172 | * Submit password. 173 | * 174 | * Attempts to decrypt the current vault and load its keyrings 175 | * into memory. 176 | * 177 | * Temporarily also migrates any old-style vaults first, as well 178 | * (Pre MetaMask 3.0.0). 179 | * 180 | * @fires KeyringController#unlock 181 | * @param password - The keyring controller password. 182 | * @returns A promise that resolves to the state. 183 | */ 184 | async submitPassword(password: string): Promise { 185 | this.keyrings = await this.unlockKeyrings(password); 186 | 187 | this.#setUnlocked(); 188 | return this.fullUpdate(); 189 | } 190 | 191 | /** 192 | * Submit Encryption Key. 193 | * 194 | * Attempts to decrypt the current vault and load its keyrings 195 | * into memory based on the vault and CryptoKey information. 196 | * 197 | * @fires KeyringController#unlock 198 | * @param encryptionKey - The encrypted key information used to decrypt the vault. 199 | * @param encryptionSalt - The salt used to generate the last key. 200 | * @returns A promise that resolves to the state. 201 | */ 202 | async submitEncryptionKey( 203 | encryptionKey: string, 204 | encryptionSalt: string, 205 | ): Promise { 206 | this.keyrings = await this.unlockKeyrings( 207 | undefined, 208 | encryptionKey, 209 | encryptionSalt, 210 | ); 211 | this.#setUnlocked(); 212 | return this.fullUpdate(); 213 | } 214 | 215 | /** 216 | * Verify Password 217 | * 218 | * Attempts to decrypt the current vault with a given password 219 | * to verify its validity. 220 | * 221 | * @param password - The vault password. 222 | */ 223 | async verifyPassword(password: string): Promise { 224 | const encryptedVault = this.store.getState().vault; 225 | if (!encryptedVault) { 226 | throw new Error(KeyringControllerError.VaultError); 227 | } 228 | await this.#encryptor.decrypt(password, encryptedVault); 229 | } 230 | 231 | /** 232 | * ========================================= 233 | * === Public Account Management Methods === 234 | * ========================================= 235 | */ 236 | 237 | /** 238 | * Add New Account. 239 | * 240 | * Calls the `addAccounts` method on the given keyring, 241 | * and then saves those changes. 242 | * 243 | * @param selectedKeyring - The currently selected keyring. 244 | * @returns A Promise that resolves to the state. 245 | */ 246 | async addNewAccount( 247 | selectedKeyring: EthKeyring, 248 | ): Promise { 249 | const accounts = await selectedKeyring.addAccounts(1); 250 | accounts.forEach((hexAccount) => { 251 | this.emit('newAccount', hexAccount); 252 | }); 253 | 254 | await this.persistAllKeyrings(); 255 | return this.fullUpdate(); 256 | } 257 | 258 | /** 259 | * Export Account 260 | * 261 | * Requests the private key from the keyring controlling 262 | * the specified address. 263 | * 264 | * Returns a Promise that may resolve with the private key string. 265 | * 266 | * @param address - The address of the account to export. 267 | * @returns The private key of the account. 268 | */ 269 | async exportAccount(address: string): Promise { 270 | const keyring = await this.getKeyringForAccount(address); 271 | if (!keyring.exportAccount) { 272 | throw new Error(KeyringControllerError.UnsupportedExportAccount); 273 | } 274 | 275 | return await keyring.exportAccount(normalizeToHex(address) as Hex); 276 | } 277 | 278 | /** 279 | * Remove Account. 280 | * 281 | * Removes a specific account from a keyring 282 | * If the account is the last/only one then it also removes the keyring. 283 | * 284 | * @param address - The address of the account to remove. 285 | * @returns A promise that resolves if the operation was successful. 286 | */ 287 | async removeAccount(address: Hex): Promise { 288 | const keyring = await this.getKeyringForAccount(address); 289 | 290 | // Not all the keyrings support this, so we have to check 291 | if (!keyring.removeAccount) { 292 | throw new Error(KeyringControllerError.UnsupportedRemoveAccount); 293 | } 294 | 295 | // The `removeAccount` method of snaps keyring is async. We have to update 296 | // the interface of the other keyrings to be async as well. 297 | // eslint-disable-next-line @typescript-eslint/await-thenable 298 | await keyring.removeAccount(address); 299 | this.emit('removedAccount', address); 300 | 301 | const accounts = await keyring.getAccounts(); 302 | // Check if this was the last/only account 303 | if (accounts.length === 0) { 304 | await this.removeEmptyKeyrings(); 305 | } 306 | 307 | await this.persistAllKeyrings(); 308 | return this.fullUpdate(); 309 | } 310 | 311 | /** 312 | * Get Accounts 313 | * 314 | * Returns the public addresses of all current accounts 315 | * managed by all currently unlocked keyrings. 316 | * 317 | * @returns The array of accounts. 318 | */ 319 | async getAccounts(): Promise { 320 | const keyrings = this.keyrings || []; 321 | 322 | const keyringArrays = await Promise.all( 323 | keyrings.map(async (keyring) => keyring.getAccounts()), 324 | ); 325 | const addresses = keyringArrays.reduce((res, arr) => { 326 | return res.concat(arr); 327 | }, []); 328 | 329 | // Cast to `Hex[]` here is safe here because `addresses` has no nullish 330 | // values, and `normalizeToHex` returns `Hex` unless given a nullish value 331 | return addresses.map(normalizeToHex) as Hex[]; 332 | } 333 | 334 | /** 335 | * Get Keyring Class For Type 336 | * 337 | * Searches the current `keyringBuilders` array 338 | * for a Keyring builder whose unique `type` property 339 | * matches the provided `type`, 340 | * returning it if it exists. 341 | * 342 | * @param type - The type whose class to get. 343 | * @returns The class, if it exists. 344 | */ 345 | getKeyringBuilderForType( 346 | type: string, 347 | ): { (): EthKeyring; type: string } | undefined { 348 | return this.keyringBuilders.find( 349 | (keyringBuilder) => keyringBuilder.type === type, 350 | ); 351 | } 352 | 353 | /** 354 | * Update memStore Keyrings 355 | * 356 | * Updates the in-memory keyrings, without persisting. 357 | */ 358 | async updateMemStoreKeyrings(): Promise { 359 | const keyrings = await Promise.all(this.keyrings.map(displayForKeyring)); 360 | this.memStore.updateState({ keyrings }); 361 | } 362 | 363 | /** 364 | * =========================================== 365 | * === Public RPC Requests Routing Methods === 366 | * =========================================== 367 | */ 368 | 369 | /** 370 | * Sign Ethereum Transaction 371 | * 372 | * Signs an Ethereum transaction object. 373 | * 374 | * @param ethTx - The transaction to sign. 375 | * @param rawAddress - The transaction 'from' address. 376 | * @param opts - Signing options. 377 | * @returns The signed transaction object. 378 | */ 379 | async signTransaction( 380 | ethTx: TypedTransaction, 381 | rawAddress: string, 382 | opts: Record = {}, 383 | ): Promise { 384 | const address = normalizeToHex(rawAddress) as Hex; 385 | const keyring = await this.getKeyringForAccount(address); 386 | if (!keyring.signTransaction) { 387 | throw new Error(KeyringControllerError.UnsupportedSignTransaction); 388 | } 389 | 390 | return await keyring.signTransaction(address, ethTx, opts); 391 | } 392 | 393 | /** 394 | * Sign Message 395 | * 396 | * Attempts to sign the provided message parameters. 397 | * 398 | * @param msgParams - The message parameters to sign. 399 | * @param msgParams.from - From address. 400 | * @param msgParams.data - The message to sign. 401 | * @param opts - Additional signing options. 402 | * @returns The raw signature. 403 | */ 404 | async signMessage( 405 | msgParams: { 406 | from: string; 407 | data: string; 408 | }, 409 | opts: Record = {}, 410 | ): Promise { 411 | const address = normalizeToHex(msgParams.from) as Hex; 412 | const keyring = await this.getKeyringForAccount(address); 413 | if (!keyring.signMessage) { 414 | throw new Error(KeyringControllerError.UnsupportedSignMessage); 415 | } 416 | 417 | return await keyring.signMessage(address, msgParams.data, opts); 418 | } 419 | 420 | /** 421 | * Sign Personal Message 422 | * 423 | * Attempts to sign the provided message parameters. 424 | * Prefixes the hash before signing per the personal sign expectation. 425 | * 426 | * @param msgParams - The message parameters to sign. 427 | * @param msgParams.from - From address. 428 | * @param msgParams.data - The message to sign. 429 | * @param opts - Additional signing options. 430 | * @returns The raw signature. 431 | */ 432 | async signPersonalMessage( 433 | msgParams: { 434 | from: string; 435 | data: string; 436 | }, 437 | opts: Record = {}, 438 | ): Promise { 439 | const address = normalizeToHex(msgParams.from) as Hex; 440 | const keyring = await this.getKeyringForAccount(address); 441 | if (!keyring.signPersonalMessage) { 442 | throw new Error(KeyringControllerError.UnsupportedSignPersonalMessage); 443 | } 444 | 445 | const normalizedData = normalizeToHex(msgParams.data) as Hex; 446 | 447 | return await keyring.signPersonalMessage(address, normalizedData, opts); 448 | } 449 | 450 | /** 451 | * Get encryption public key 452 | * 453 | * Get encryption public key for using in encrypt/decrypt process. 454 | * 455 | * @param address - The address to get the encryption public key for. 456 | * @param opts - Additional encryption options. 457 | * @returns The public key. 458 | */ 459 | async getEncryptionPublicKey( 460 | address: string, 461 | opts: Record = {}, 462 | ): Promise { 463 | const normalizedAddress = normalizeToHex(address) as Hex; 464 | const keyring = await this.getKeyringForAccount(address); 465 | if (!keyring.getEncryptionPublicKey) { 466 | throw new Error(KeyringControllerError.UnsupportedGetEncryptionPublicKey); 467 | } 468 | 469 | return await keyring.getEncryptionPublicKey(normalizedAddress, opts); 470 | } 471 | 472 | /** 473 | * Decrypt Message 474 | * 475 | * Attempts to decrypt the provided message parameters. 476 | * 477 | * @param msgParams - The decryption message parameters. 478 | * @param msgParams.from - The address of the account you want to use to decrypt the message. 479 | * @param msgParams.data - The encrypted data that you want to decrypt. 480 | * @returns The raw decryption result. 481 | */ 482 | async decryptMessage(msgParams: { 483 | from: string; 484 | data: Eip1024EncryptedData; 485 | }): Promise { 486 | const address = normalizeToHex(msgParams.from) as Hex; 487 | const keyring = await this.getKeyringForAccount(address); 488 | if (!keyring.decryptMessage) { 489 | throw new Error(KeyringControllerError.UnsupportedDecryptMessage); 490 | } 491 | 492 | return keyring.decryptMessage(address, msgParams.data); 493 | } 494 | 495 | /** 496 | * Sign Typed Data. 497 | * 498 | * @see {@link https://github.com/ethereum/EIPs/pull/712#issuecomment-329988454|EIP712}. 499 | * @param msgParams - The message parameters to sign. 500 | * @param msgParams.from - From address. 501 | * @param msgParams.data - The data to sign. 502 | * @param opts - Additional signing options. 503 | * @returns The raw signature. 504 | */ 505 | async signTypedMessage( 506 | msgParams: { 507 | from: string; 508 | data: Record | Record[]; 509 | }, 510 | opts: Record = { version: 'V1' }, 511 | ): Promise { 512 | // Cast to `Hex` here is safe here because `msgParams.from` is not nullish. 513 | // `normalizeToHex` returns `Hex` unless given a nullish value. 514 | const address = normalizeToHex(msgParams.from) as Hex; 515 | const keyring = await this.getKeyringForAccount(address); 516 | if (!keyring.signTypedData) { 517 | throw new Error(KeyringControllerError.UnsupportedSignTypedMessage); 518 | } 519 | 520 | // Looks like this is not well defined in the Keyring interface since 521 | // our tests show that we should be able to pass an array. 522 | // @ts-expect-error Missing other required permission types. 523 | return keyring.signTypedData(address, msgParams.data, opts); 524 | } 525 | 526 | /** 527 | * Gets the app key address for the given Ethereum address and origin. 528 | * 529 | * @param rawAddress - The Ethereum address for the app key. 530 | * @param origin - The origin for the app key. 531 | * @returns The app key address. 532 | */ 533 | async getAppKeyAddress(rawAddress: string, origin: string): Promise { 534 | const address = normalizeToHex(rawAddress) as Hex; 535 | const keyring = await this.getKeyringForAccount(address); 536 | if (!keyring.getAppKeyAddress) { 537 | throw new Error(KeyringControllerError.UnsupportedGetAppKeyAddress); 538 | } 539 | 540 | return keyring.getAppKeyAddress(address, origin); 541 | } 542 | 543 | /** 544 | * Exports an app key private key for the given Ethereum address and origin. 545 | * 546 | * @param rawAddress - The Ethereum address for the app key. 547 | * @param origin - The origin for the app key. 548 | * @returns The app key private key. 549 | */ 550 | async exportAppKeyForAddress( 551 | rawAddress: string, 552 | origin: string, 553 | ): Promise { 554 | const address = normalizeToHex(rawAddress) as Hex; 555 | const keyring = await this.getKeyringForAccount(address); 556 | if (!keyring.exportAccount) { 557 | throw new Error(KeyringControllerError.UnsupportedExportAppKeyForAddress); 558 | } 559 | return keyring.exportAccount(address, { withAppKeyOrigin: origin }); 560 | } 561 | 562 | /** 563 | * Convert a base transaction to a base UserOperation. 564 | * 565 | * @param rawAddress - Address of the sender. 566 | * @param transactions - Base transactions to include in the UserOperation. 567 | * @returns A pseudo-UserOperation that can be used to construct a real. 568 | */ 569 | async prepareUserOperation( 570 | rawAddress: string, 571 | transactions: EthBaseTransaction[], 572 | ): Promise { 573 | const address = normalizeToHex(rawAddress) as Hex; 574 | const keyring = await this.getKeyringForAccount(address); 575 | 576 | return keyring.prepareUserOperation 577 | ? await keyring.prepareUserOperation(address, transactions) 578 | : throwError(KeyringControllerError.UnsupportedPrepareUserOperation); 579 | } 580 | 581 | /** 582 | * Patches properties of a UserOperation. Currently, only the 583 | * `paymasterAndData` can be patched. 584 | * 585 | * @param rawAddress - Address of the sender. 586 | * @param userOp - UserOperation to patch. 587 | * @returns A patch to apply to the UserOperation. 588 | */ 589 | async patchUserOperation( 590 | rawAddress: string, 591 | userOp: EthUserOperation, 592 | ): Promise { 593 | const address = normalizeToHex(rawAddress) as Hex; 594 | const keyring = await this.getKeyringForAccount(address); 595 | 596 | return keyring.patchUserOperation 597 | ? await keyring.patchUserOperation(address, userOp) 598 | : throwError(KeyringControllerError.UnsupportedPatchUserOperation); 599 | } 600 | 601 | /** 602 | * Signs an UserOperation. 603 | * 604 | * @param rawAddress - Address of the sender. 605 | * @param userOp - UserOperation to sign. 606 | * @returns The signature of the UserOperation. 607 | */ 608 | async signUserOperation( 609 | rawAddress: string, 610 | userOp: EthUserOperation, 611 | ): Promise { 612 | const address = normalizeToHex(rawAddress) as Hex; 613 | const keyring = await this.getKeyringForAccount(address); 614 | 615 | return keyring.signUserOperation 616 | ? await keyring.signUserOperation(address, userOp) 617 | : throwError(KeyringControllerError.UnsupportedSignUserOperation); 618 | } 619 | 620 | /** 621 | * ========================================= 622 | * === Public Keyring Management Methods === 623 | * ========================================= 624 | */ 625 | 626 | /** 627 | * Add New Keyring 628 | * 629 | * Adds a new Keyring of the given `type` to the vault 630 | * and the current decrypted Keyrings array. 631 | * 632 | * All Keyring classes implement a unique `type` string, 633 | * and this is used to retrieve them from the keyringBuilders array. 634 | * 635 | * @param type - The type of keyring to add. 636 | * @param opts - The constructor options for the keyring. 637 | * @returns The new keyring. 638 | */ 639 | async addNewKeyring(type: string, opts?: unknown): Promise> { 640 | const keyring = await this.#newKeyring(type, opts); 641 | 642 | if (!keyring) { 643 | throw new Error(KeyringControllerError.NoKeyring); 644 | } 645 | 646 | if (type === KeyringType.HD && (!isObject(opts) || !opts.mnemonic)) { 647 | if (!keyring.generateRandomMnemonic) { 648 | throw new Error( 649 | KeyringControllerError.UnsupportedGenerateRandomMnemonic, 650 | ); 651 | } 652 | 653 | keyring.generateRandomMnemonic(); 654 | await keyring.addAccounts(1); 655 | } 656 | 657 | const accounts = await keyring.getAccounts(); 658 | await this.checkForDuplicate(type, accounts); 659 | 660 | this.keyrings.push(keyring); 661 | await this.persistAllKeyrings(); 662 | 663 | this.fullUpdate(); 664 | 665 | return keyring; 666 | } 667 | 668 | /** 669 | * Remove empty keyrings. 670 | * 671 | * Loops through the keyrings and removes the ones with empty accounts 672 | * (usually after removing the last / only account) from a keyring. 673 | */ 674 | async removeEmptyKeyrings(): Promise { 675 | const validKeyrings: EthKeyring[] = []; 676 | 677 | // Since getAccounts returns a Promise 678 | // We need to wait to hear back form each keyring 679 | // in order to decide which ones are now valid (accounts.length > 0) 680 | 681 | await Promise.all( 682 | this.keyrings.map(async (keyring: EthKeyring) => { 683 | const accounts = await keyring.getAccounts(); 684 | if (accounts.length > 0) { 685 | validKeyrings.push(keyring); 686 | } else { 687 | await this.#destroyKeyring(keyring); 688 | } 689 | }), 690 | ); 691 | this.keyrings = validKeyrings; 692 | } 693 | 694 | /** 695 | * Checks for duplicate keypairs, using the the first account in the given 696 | * array. Rejects if a duplicate is found. 697 | * 698 | * Only supports 'Simple Key Pair'. 699 | * 700 | * @param type - The key pair type to check for. 701 | * @param newAccountArray - Array of new accounts. 702 | * @returns The account, if no duplicate is found. 703 | */ 704 | async checkForDuplicate( 705 | type: string, 706 | newAccountArray: string[], 707 | ): Promise { 708 | const accounts = await this.getAccounts(); 709 | 710 | switch (type) { 711 | case KeyringType.Simple: { 712 | const isIncluded = Boolean( 713 | accounts.find( 714 | (key) => 715 | newAccountArray[0] && 716 | (key === newAccountArray[0] || 717 | key === remove0x(newAccountArray[0])), 718 | ), 719 | ); 720 | 721 | if (isIncluded) { 722 | throw new Error(KeyringControllerError.DuplicatedAccount); 723 | } 724 | return newAccountArray; 725 | } 726 | 727 | default: { 728 | return newAccountArray; 729 | } 730 | } 731 | } 732 | 733 | /** 734 | * Get Keyring For Account 735 | * 736 | * Returns the currently initialized keyring that manages 737 | * the specified `address` if one exists. 738 | * 739 | * @param address - An account address. 740 | * @returns The keyring of the account, if it exists. 741 | */ 742 | async getKeyringForAccount(address: string): Promise> { 743 | // Cast to `Hex` here is safe here because `address` is not nullish. 744 | // `normalizeToHex` returns `Hex` unless given a nullish value. 745 | const hexed = normalizeToHex(address) as Hex; 746 | 747 | const candidates = await Promise.all( 748 | this.keyrings.map(async (keyring) => { 749 | return Promise.all([keyring, keyring.getAccounts()]); 750 | }), 751 | ); 752 | 753 | const winners = candidates.filter((candidate) => { 754 | const accounts = candidate[1].map(normalizeToHex); 755 | return accounts.includes(hexed); 756 | }); 757 | 758 | if (winners.length && winners[0]?.length) { 759 | return winners[0][0]; 760 | } 761 | 762 | // Adding more info to the error 763 | let errorInfo = ''; 764 | if (!isValidHexAddress(hexed)) { 765 | errorInfo = 'The address passed in is invalid/empty'; 766 | } else if (!candidates.length) { 767 | errorInfo = 'There are no keyrings'; 768 | } else if (!winners.length) { 769 | errorInfo = 'There are keyrings, but none match the address'; 770 | } 771 | throw new Error( 772 | `${KeyringControllerError.NoKeyring}. Error info: ${errorInfo}`, 773 | ); 774 | } 775 | 776 | /** 777 | * Restore Keyring 778 | * 779 | * Attempts to initialize a new keyring from the provided serialized payload. 780 | * On success, updates the memStore keyrings and returns the resulting 781 | * keyring instance. 782 | * 783 | * @param serialized - The serialized keyring. 784 | * @returns The deserialized keyring. 785 | */ 786 | async restoreKeyring( 787 | serialized: SerializedKeyring, 788 | ): Promise | undefined> { 789 | const keyring = await this.#restoreKeyring(serialized); 790 | if (keyring) { 791 | await this.updateMemStoreKeyrings(); 792 | } 793 | return keyring; 794 | } 795 | 796 | /** 797 | * Get Keyrings by Type 798 | * 799 | * Gets all keyrings of the given type. 800 | * 801 | * @param type - The keyring types to retrieve. 802 | * @returns Keyrings matching the specified type. 803 | */ 804 | getKeyringsByType(type: string): EthKeyring[] { 805 | return this.keyrings.filter((keyring) => keyring.type === type); 806 | } 807 | 808 | /** 809 | * Persist All Keyrings 810 | * 811 | * Iterates the current `keyrings` array, 812 | * serializes each one into a serialized array, 813 | * encrypts that array with the provided `password`, 814 | * and persists that encrypted string to storage. 815 | * 816 | * @returns Resolves to true once keyrings are persisted. 817 | */ 818 | async persistAllKeyrings(): Promise { 819 | const { encryptionKey, encryptionSalt } = this.memStore.getState(); 820 | 821 | if (!this.password && !encryptionKey) { 822 | throw new Error(KeyringControllerError.MissingCredentials); 823 | } 824 | 825 | const serializedKeyrings = await Promise.all( 826 | this.keyrings.map(async (keyring) => { 827 | const [type, data] = await Promise.all([ 828 | keyring.type, 829 | keyring.serialize(), 830 | ]); 831 | return { type, data }; 832 | }), 833 | ); 834 | 835 | serializedKeyrings.push(...this.unsupportedKeyrings); 836 | 837 | let vault; 838 | let newEncryptionKey; 839 | 840 | if (this.#cacheEncryptionKey) { 841 | assertIsExportableKeyEncryptor(this.#encryptor); 842 | 843 | if (encryptionKey) { 844 | const key = await this.#encryptor.importKey(encryptionKey); 845 | const vaultJSON = await this.#encryptor.encryptWithKey( 846 | key, 847 | serializedKeyrings, 848 | ); 849 | vaultJSON.salt = encryptionSalt; 850 | vault = JSON.stringify(vaultJSON); 851 | } else if (this.password) { 852 | const { vault: newVault, exportedKeyString } = 853 | await this.#encryptor.encryptWithDetail( 854 | this.password, 855 | serializedKeyrings, 856 | ); 857 | 858 | vault = newVault; 859 | newEncryptionKey = exportedKeyString; 860 | } 861 | } else { 862 | if (typeof this.password !== 'string') { 863 | throw new TypeError(KeyringControllerError.WrongPasswordType); 864 | } 865 | vault = await this.#encryptor.encrypt(this.password, serializedKeyrings); 866 | } 867 | 868 | if (!vault) { 869 | throw new Error(KeyringControllerError.MissingVaultData); 870 | } 871 | 872 | this.store.updateState({ vault }); 873 | 874 | // The keyring updates need to be announced before updating the encryptionKey 875 | // so that the updated keyring gets propagated to the extension first. 876 | // Not calling {@link updateMemStoreKeyrings} results in the wrong account being selected 877 | // in the extension. 878 | await this.updateMemStoreKeyrings(); 879 | if (newEncryptionKey) { 880 | this.memStore.updateState({ 881 | encryptionKey: newEncryptionKey, 882 | encryptionSalt: JSON.parse(vault).salt, 883 | }); 884 | } 885 | 886 | return true; 887 | } 888 | 889 | /** 890 | * Unlock Keyrings. 891 | * 892 | * Attempts to unlock the persisted encrypted storage, 893 | * initializing the persisted keyrings to RAM. 894 | * 895 | * @param password - The keyring controller password. 896 | * @param encryptionKey - An exported key string to unlock keyrings with. 897 | * @param encryptionSalt - The salt used to encrypt the vault. 898 | * @returns The keyrings array. 899 | */ 900 | async unlockKeyrings( 901 | password: string | undefined, 902 | encryptionKey?: string, 903 | encryptionSalt?: string, 904 | ): Promise[]> { 905 | const encryptedVault = this.store.getState().vault; 906 | if (!encryptedVault) { 907 | throw new Error(KeyringControllerError.VaultError); 908 | } 909 | 910 | await this.#clearKeyrings(); 911 | 912 | let vault; 913 | 914 | if (this.#cacheEncryptionKey) { 915 | assertIsExportableKeyEncryptor(this.#encryptor); 916 | 917 | if (password) { 918 | const result = await this.#encryptor.decryptWithDetail( 919 | password, 920 | encryptedVault, 921 | ); 922 | vault = result.vault; 923 | this.password = password; 924 | 925 | this.memStore.updateState({ 926 | encryptionKey: result.exportedKeyString, 927 | encryptionSalt: result.salt, 928 | }); 929 | } else { 930 | const parsedEncryptedVault = JSON.parse(encryptedVault); 931 | 932 | if (encryptionSalt !== parsedEncryptedVault.salt) { 933 | throw new Error(KeyringControllerError.ExpiredCredentials); 934 | } 935 | 936 | if (typeof encryptionKey !== 'string') { 937 | throw new TypeError(KeyringControllerError.WrongPasswordType); 938 | } 939 | 940 | const key = await this.#encryptor.importKey(encryptionKey); 941 | vault = await this.#encryptor.decryptWithKey(key, parsedEncryptedVault); 942 | 943 | // This call is required on the first call because encryptionKey 944 | // is not yet inside the memStore 945 | this.memStore.updateState({ 946 | encryptionKey, 947 | // we can safely assume that encryptionSalt is defined here 948 | // because we compare it with the salt from the vault 949 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 950 | encryptionSalt: encryptionSalt!, 951 | }); 952 | } 953 | } else { 954 | if (typeof password !== 'string') { 955 | throw new TypeError(KeyringControllerError.WrongPasswordType); 956 | } 957 | 958 | vault = await this.#encryptor.decrypt(password, encryptedVault); 959 | this.password = password; 960 | } 961 | 962 | if (!isSerializedKeyringsArray(vault)) { 963 | throw new Error(KeyringControllerError.VaultDataError); 964 | } 965 | 966 | await Promise.all(vault.map(this.#restoreKeyring.bind(this))); 967 | await this.updateMemStoreKeyrings(); 968 | 969 | if ( 970 | this.password && 971 | (!this.#cacheEncryptionKey || !encryptionKey) && 972 | this.#encryptor.isVaultUpdated && 973 | !this.#encryptor.isVaultUpdated(encryptedVault) 974 | ) { 975 | // Re-encrypt the vault with safer method if one is available 976 | await this.persistAllKeyrings(); 977 | } 978 | 979 | return this.keyrings; 980 | } 981 | 982 | // ======================= 983 | // === Private Methods === 984 | // ======================= 985 | 986 | /** 987 | * Create keyring. 988 | * 989 | * - Creates a new vault. 990 | * - Creates a new keyring with at least one account. 991 | * - Makes the first account the selected account. 992 | * @param type - Keyring type to instantiate. 993 | * @param opts - Optional parameters required to instantiate the keyring. 994 | * @returns A promise that resolves if the operation was successful. 995 | */ 996 | async #createKeyring(type: string, opts?: unknown): Promise { 997 | const keyring = await this.addNewKeyring(type, opts); 998 | if (!keyring) { 999 | throw new Error(KeyringControllerError.NoKeyring); 1000 | } 1001 | 1002 | const [firstAccount] = await keyring.getAccounts(); 1003 | if (!firstAccount) { 1004 | throw new Error(KeyringControllerError.NoFirstAccount); 1005 | } 1006 | 1007 | const hexAccount = normalizeToHex(firstAccount); 1008 | this.emit('newVault', hexAccount); 1009 | return null; 1010 | } 1011 | 1012 | /** 1013 | * Restore Keyring Helper 1014 | * 1015 | * Attempts to initialize a new keyring from the provided serialized payload. 1016 | * On success, returns the resulting keyring instance. 1017 | * 1018 | * @param serialized - The serialized keyring. 1019 | * @param serialized.type - Keyring type. 1020 | * @param serialized.data - Keyring data. 1021 | * @returns The deserialized keyring or undefined if the keyring type is unsupported. 1022 | */ 1023 | async #restoreKeyring( 1024 | serialized: SerializedKeyring, 1025 | ): Promise | undefined> { 1026 | const { type, data } = serialized; 1027 | 1028 | let keyring: EthKeyring | undefined; 1029 | try { 1030 | keyring = await this.#newKeyring(type, data); 1031 | } catch (error) { 1032 | // Ignore error. 1033 | console.error(error); 1034 | } 1035 | 1036 | if (!keyring) { 1037 | this.unsupportedKeyrings.push(serialized); 1038 | return undefined; 1039 | } 1040 | 1041 | // getAccounts also validates the accounts for some keyrings 1042 | await keyring.getAccounts(); 1043 | this.keyrings.push(keyring); 1044 | return keyring; 1045 | } 1046 | 1047 | /** 1048 | * Clear Keyrings 1049 | * 1050 | * Deallocates all currently managed keyrings and accounts. 1051 | * Used before initializing a new vault and after locking 1052 | * MetaMask. 1053 | */ 1054 | async #clearKeyrings() { 1055 | // clear keyrings from memory 1056 | for (const keyring of this.keyrings) { 1057 | await this.#destroyKeyring(keyring); 1058 | } 1059 | this.keyrings = []; 1060 | this.memStore.updateState({ 1061 | keyrings: [], 1062 | }); 1063 | } 1064 | 1065 | /** 1066 | * Destroy Keyring 1067 | * 1068 | * Some keyrings support a method called `destroy`, that destroys the 1069 | * keyring along with removing all its event listeners and, in some cases, 1070 | * clears the keyring bridge iframe from the DOM. 1071 | * 1072 | * @param keyring - The keyring to destroy. 1073 | */ 1074 | async #destroyKeyring(keyring: EthKeyring) { 1075 | await keyring.destroy?.(); 1076 | } 1077 | 1078 | /** 1079 | * Unlock Keyrings 1080 | * 1081 | * Unlocks the keyrings. 1082 | * 1083 | * @fires KeyringController#unlock 1084 | */ 1085 | #setUnlocked(): void { 1086 | this.memStore.updateState({ isUnlocked: true }); 1087 | this.emit('unlock'); 1088 | } 1089 | 1090 | /** 1091 | * Instantiate, initialize and return a new keyring 1092 | * 1093 | * The keyring instantiated is of the given `type`. 1094 | * 1095 | * @param type - The type of keyring to add. 1096 | * @param data - The data to restore a previously serialized keyring. 1097 | * @returns The new keyring. 1098 | */ 1099 | async #newKeyring(type: string, data: unknown): Promise> { 1100 | const keyringBuilder = this.getKeyringBuilderForType(type); 1101 | 1102 | if (!keyringBuilder) { 1103 | throw new Error( 1104 | `${KeyringControllerError.NoKeyringBuilder}. Keyring type: ${type}`, 1105 | ); 1106 | } 1107 | 1108 | const keyring = keyringBuilder(); 1109 | 1110 | // @ts-expect-error Enforce data type after updating clients 1111 | await keyring.deserialize(data); 1112 | 1113 | if (keyring.init) { 1114 | await keyring.init(); 1115 | } 1116 | 1117 | return keyring; 1118 | } 1119 | } 1120 | 1121 | /** 1122 | * Get builder function for `Keyring` 1123 | * 1124 | * Returns a builder function for `Keyring` with a `type` property. 1125 | * 1126 | * @param KeyringConstructor - The Keyring class for the builder. 1127 | * @returns A builder function for the given Keyring. 1128 | */ 1129 | function keyringBuilderFactory(KeyringConstructor: KeyringClass) { 1130 | const builder = () => new KeyringConstructor(); 1131 | 1132 | builder.type = KeyringConstructor.type; 1133 | 1134 | return builder; 1135 | } 1136 | 1137 | /** 1138 | * Display For Keyring 1139 | * 1140 | * Is used for adding the current keyrings to the state object. 1141 | * 1142 | * @param keyring - The keyring to display. 1143 | * @returns A keyring display object, with type and accounts properties. 1144 | */ 1145 | async function displayForKeyring( 1146 | keyring: EthKeyring, 1147 | ): Promise<{ type: string; accounts: string[] }> { 1148 | const accounts = await keyring.getAccounts(); 1149 | 1150 | return { 1151 | type: keyring.type, 1152 | // Cast to `Hex[]` here is safe here because `addresses` has no nullish 1153 | // values, and `normalizeToHex` returns `Hex` unless given a nullish value 1154 | accounts: accounts.map(normalizeToHex) as Hex[], 1155 | }; 1156 | } 1157 | 1158 | /** 1159 | * Assert that the provided encryptor supports 1160 | * encryption and encryption key export. 1161 | * 1162 | * @param encryptor - The encryptor to check. 1163 | * @throws If the encryptor does not support key encryption. 1164 | */ 1165 | function assertIsExportableKeyEncryptor( 1166 | encryptor: GenericEncryptor | ExportableKeyEncryptor, 1167 | ): asserts encryptor is ExportableKeyEncryptor { 1168 | if ( 1169 | !( 1170 | 'importKey' in encryptor && 1171 | typeof encryptor.importKey === 'function' && 1172 | 'decryptWithKey' in encryptor && 1173 | typeof encryptor.decryptWithKey === 'function' && 1174 | 'encryptWithKey' in encryptor && 1175 | typeof encryptor.encryptWithKey === 'function' 1176 | ) 1177 | ) { 1178 | throw new Error(KeyringControllerError.UnsupportedEncryptionKeyExport); 1179 | } 1180 | } 1181 | 1182 | /** 1183 | * Checks if the provided value is a serialized keyrings array. 1184 | * 1185 | * @param array - The value to check. 1186 | * @returns True if the value is a serialized keyrings array. 1187 | */ 1188 | function isSerializedKeyringsArray( 1189 | array: unknown, 1190 | ): array is SerializedKeyring[] { 1191 | return ( 1192 | typeof array === 'object' && 1193 | Array.isArray(array) && 1194 | array.every((value) => value.type && isValidJson(value.data)) 1195 | ); 1196 | } 1197 | 1198 | export { KeyringController, keyringBuilderFactory }; 1199 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum KeyringType { 2 | HD = 'HD Key Tree', 3 | Simple = 'Simple Key Pair', 4 | } 5 | 6 | export enum KeyringControllerError { 7 | NoKeyring = 'KeyringController - No keyring found', 8 | WrongPasswordType = 'KeyringController - Password must be of type string.', 9 | NoFirstAccount = 'KeyringController - First Account not found.', 10 | DuplicatedAccount = 'KeyringController - The account you are trying to import is a duplicate', 11 | VaultError = 'KeyringController - Cannot unlock without a previous vault.', 12 | VaultDataError = 'KeyringController - The decrypted vault has an unexpected shape.', 13 | UnsupportedEncryptionKeyExport = 'KeyringController - The encryptor does not support encryption key export.', 14 | UnsupportedGenerateRandomMnemonic = 'KeyringController - The current keyring does not support the method generateRandomMnemonic.', 15 | UnsupportedExportAccount = '`KeyringController - The keyring for the current address does not support the method exportAccount', 16 | UnsupportedRemoveAccount = '`KeyringController - The keyring for the current address does not support the method removeAccount', 17 | UnsupportedSignTransaction = 'KeyringController - The keyring for the current address does not support the method signTransaction.', 18 | UnsupportedSignMessage = 'KeyringController - The keyring for the current address does not support the method signMessage.', 19 | UnsupportedSignPersonalMessage = 'KeyringController - The keyring for the current address does not support the method signPersonalMessage.', 20 | UnsupportedGetEncryptionPublicKey = 'KeyringController - The keyring for the current address does not support the method getEncryptionPublicKey.', 21 | UnsupportedDecryptMessage = 'KeyringController - The keyring for the current address does not support the method decryptMessage.', 22 | UnsupportedSignTypedMessage = 'KeyringController - The keyring for the current address does not support the method signTypedMessage.', 23 | UnsupportedGetAppKeyAddress = 'KeyringController - The keyring for the current address does not support the method getAppKeyAddress.', 24 | UnsupportedExportAppKeyForAddress = 'KeyringController - The keyring for the current address does not support the method exportAppKeyForAddress.', 25 | UnsupportedPrepareUserOperation = 'KeyringController - The keyring for the current address does not support the method prepareUserOperation.', 26 | UnsupportedPatchUserOperation = 'KeyringController - The keyring for the current address does not support the method patchUserOperation.', 27 | UnsupportedSignUserOperation = 'KeyringController - The keyring for the current address does not support the method signUserOperation.', 28 | NoAccountOnKeychain = "KeyringController - The keychain doesn't have accounts.", 29 | MissingCredentials = 'KeyringController - Cannot persist vault without password and encryption key', 30 | MissingVaultData = 'KeyringController - Cannot persist vault without vault information', 31 | ExpiredCredentials = 'KeyringController - Encryption key and salt provided are expired', 32 | NoKeyringBuilder = 'KeyringController - No keyringBuilder found for keyring', 33 | DataType = 'KeyringController - Incorrect data type provided', 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { KeyringController, keyringBuilderFactory } from './KeyringController'; 2 | 3 | export { KeyringType, KeyringControllerError } from './constants'; 4 | -------------------------------------------------------------------------------- /src/readable-stream.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-nodejs-modules */ 2 | declare module 'readable-stream' { 3 | export { Duplex, Transform } from 'stream'; 4 | } 5 | -------------------------------------------------------------------------------- /src/test/encryptor.mock.ts: -------------------------------------------------------------------------------- 1 | import type { Json } from '@metamask/utils'; 2 | 3 | import type { ExportableKeyEncryptor } from '../types'; 4 | 5 | export const PASSWORD = 'password123'; 6 | export const MOCK_ENCRYPTION_KEY = JSON.stringify({ 7 | alg: 'A256GCM', 8 | ext: true, 9 | k: 'wYmxkxOOFBDP6F6VuuYFcRt_Po-tSLFHCWVolsHs4VI', 10 | // eslint-disable-next-line @typescript-eslint/naming-convention 11 | key_ops: ['encrypt', 'decrypt'], 12 | kty: 'oct', 13 | }); 14 | export const MOCK_ENCRYPTION_SALT = 15 | 'HQ5sfhsb8XAQRJtD+UqcImT7Ve4n3YMagrh05YTOsjk='; 16 | export const MOCK_HARDCODED_KEY = 'key'; 17 | export const MOCK_HEX = '0xabcdef0123456789'; 18 | // eslint-disable-next-line no-restricted-globals 19 | const MOCK_KEY = Buffer.alloc(32); 20 | const INVALID_PASSWORD_ERROR = 'Incorrect password.'; 21 | 22 | let cacheVal: Json; 23 | 24 | export class MockEncryptor implements ExportableKeyEncryptor { 25 | async encrypt(password: string, dataObj: any) { 26 | return JSON.stringify({ 27 | ...(await this.encryptWithKey(password, dataObj)), 28 | salt: this.generateSalt(), 29 | }); 30 | } 31 | 32 | async decrypt(_password: string, _text: string) { 33 | if (_password && _password !== PASSWORD) { 34 | throw new Error(INVALID_PASSWORD_ERROR); 35 | } 36 | 37 | return cacheVal ?? {}; 38 | } 39 | 40 | async encryptWithKey(_key: unknown, dataObj: any) { 41 | cacheVal = dataObj; 42 | return { 43 | data: MOCK_HEX, 44 | iv: 'anIv', 45 | }; 46 | } 47 | 48 | async encryptWithDetail(key: string, dataObj: any) { 49 | return { 50 | vault: await this.encrypt(key, dataObj), 51 | exportedKeyString: MOCK_HARDCODED_KEY, 52 | }; 53 | } 54 | 55 | async decryptWithDetail(key: string, text: string) { 56 | return { 57 | vault: await this.decrypt(key, text), 58 | salt: MOCK_ENCRYPTION_SALT, 59 | exportedKeyString: MOCK_ENCRYPTION_KEY, 60 | }; 61 | } 62 | 63 | async decryptWithKey(key: unknown, text: string) { 64 | return this.decrypt(key as string, text); 65 | } 66 | 67 | async keyFromPassword(_password: string) { 68 | return MOCK_KEY; 69 | } 70 | 71 | async importKey(key: string) { 72 | if (key === '{}') { 73 | throw new TypeError( 74 | `Failed to execute 'importKey' on 'SubtleCrypto': The provided value is not of type '(ArrayBuffer or ArrayBufferView or JsonWebKey)'.`, 75 | ); 76 | } 77 | return null; 78 | } 79 | 80 | async updateVault(_vault: string, _password: string) { 81 | return _vault; 82 | } 83 | 84 | isVaultUpdated(_vault: string) { 85 | return true; 86 | } 87 | 88 | generateSalt() { 89 | return MOCK_ENCRYPTION_SALT; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | MockEncryptor, 3 | PASSWORD, 4 | MOCK_HARDCODED_KEY, 5 | MOCK_HEX, 6 | MOCK_ENCRYPTION_KEY, 7 | MOCK_ENCRYPTION_SALT, 8 | } from './encryptor.mock'; 9 | 10 | export * from './keyring.mock'; 11 | 12 | export * from './transaction.mock'; 13 | -------------------------------------------------------------------------------- /src/test/keyring.mock.ts: -------------------------------------------------------------------------------- 1 | import type { TxData } from '@ethereumjs/tx'; 2 | import type { 3 | EthBaseTransaction, 4 | EthBaseUserOperation, 5 | EthKeyring, 6 | EthUserOperation, 7 | EthUserOperationPatch, 8 | } from '@metamask/keyring-api'; 9 | import type { Json, Hex } from '@metamask/utils'; 10 | 11 | export class BaseKeyringMock implements EthKeyring { 12 | static type = 'Keyring Mock'; 13 | 14 | public type = 'Keyring Mock'; 15 | 16 | #accounts: Hex[] = []; 17 | 18 | constructor(options: Record | undefined = {}) { 19 | // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/promise-function-async 20 | this.deserialize(options); 21 | } 22 | 23 | async init() { 24 | return Promise.resolve(); 25 | } 26 | 27 | async addAccounts(_: number): Promise { 28 | return Promise.resolve(this.#accounts); 29 | } 30 | 31 | async getAccounts() { 32 | return Promise.resolve(this.#accounts); 33 | } 34 | 35 | async serialize() { 36 | return Promise.resolve({}); 37 | } 38 | 39 | async deserialize(_: any) { 40 | return Promise.resolve(); 41 | } 42 | } 43 | 44 | export class KeyringMockWithInit extends BaseKeyringMock { 45 | static type = 'Keyring Mock With Init'; 46 | 47 | public type = 'Keyring Mock With Init'; 48 | 49 | constructor(options: Record | undefined = {}) { 50 | super(options); 51 | } 52 | 53 | async init() { 54 | return Promise.resolve(); 55 | } 56 | } 57 | 58 | export class KeyringMockWithRemoveAccount extends BaseKeyringMock { 59 | static type = 'Keyring Mock With Remove Account'; 60 | 61 | public type = 'Keyring Mock With Remove Account'; 62 | 63 | constructor(options: Record | undefined = {}) { 64 | super(options); 65 | } 66 | 67 | async removeAccount(_: any) { 68 | return Promise.resolve(); 69 | } 70 | } 71 | 72 | export class KeyringMockWithDestroy extends KeyringMockWithRemoveAccount { 73 | static type = 'Keyring Mock With Destroy'; 74 | 75 | public type = 'Keyring Mock With Destroy'; 76 | 77 | constructor(options: Record | undefined = {}) { 78 | super(options); 79 | } 80 | 81 | async destroy() { 82 | return Promise.resolve(); 83 | } 84 | 85 | async prepareUserOperation( 86 | _from: string, 87 | _txs: EthBaseTransaction[], 88 | ): Promise { 89 | return Promise.resolve() as any; 90 | } 91 | 92 | async patchUserOperation( 93 | _from: string, 94 | _userOp: EthUserOperation, 95 | ): Promise { 96 | return Promise.resolve() as any; 97 | } 98 | 99 | async signUserOperation( 100 | _from: string, 101 | _userOp: EthUserOperation, 102 | ): Promise { 103 | return Promise.resolve() as any; 104 | } 105 | } 106 | 107 | export class KeyringMockWithSignTransaction extends BaseKeyringMock { 108 | static type = 'Keyring Mock With Sign Transaction'; 109 | 110 | public type = 'Keyring Mock With Sign Transaction'; 111 | 112 | constructor(options: Record | undefined = {}) { 113 | super(options); 114 | } 115 | 116 | async signTransaction(_from: any, txData: any, _opts: any): Promise { 117 | return Promise.resolve(txData); 118 | } 119 | } 120 | 121 | export class KeyringMockWithUserOp extends BaseKeyringMock { 122 | static type = 'Keyring Mock With User Op'; 123 | 124 | public type = 'Keyring Mock With User Op'; 125 | 126 | constructor(options: Record | undefined = {}) { 127 | super(options); 128 | } 129 | 130 | async prepareUserOperation( 131 | _from: string, 132 | _txs: EthBaseTransaction[], 133 | ): Promise { 134 | return Promise.resolve() as any; 135 | } 136 | 137 | async patchUserOperation( 138 | _from: string, 139 | _userOp: EthUserOperation, 140 | ): Promise { 141 | return Promise.resolve() as any; 142 | } 143 | 144 | async signUserOperation( 145 | _from: string, 146 | _userOp: EthUserOperation, 147 | ): Promise { 148 | return Promise.resolve() as any; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/transaction.mock.ts: -------------------------------------------------------------------------------- 1 | import { TransactionFactory, type TxData } from '@ethereumjs/tx'; 2 | 3 | /** 4 | * Build a mock transaction, optionally overriding 5 | * any of the default values. 6 | * 7 | * @param options - The transaction options to override. 8 | * @returns The mock transaction. 9 | */ 10 | export const buildMockTransaction = (options: TxData = {}) => 11 | TransactionFactory.fromTxData({ 12 | to: '0xB1A13aBECeB71b2E758c7e0Da404DF0C72Ca3a12', 13 | value: '0x0', 14 | data: '0x', 15 | gasPrice: '0x0', 16 | nonce: '0x0', 17 | ...options, 18 | }); 19 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DetailedDecryptResult, 3 | DetailedEncryptionResult, 4 | EncryptionResult, 5 | KeyDerivationOptions, 6 | } from '@metamask/browser-passworder'; 7 | import type { EthKeyring } from '@metamask/keyring-api'; 8 | import type { Json } from '@metamask/utils'; 9 | 10 | export type KeyringControllerArgs = { 11 | keyringBuilders?: { (): EthKeyring; type: string }[]; 12 | initState?: KeyringControllerPersistentState; 13 | } & ( 14 | | { encryptor?: ExportableKeyEncryptor; cacheEncryptionKey: true } 15 | | { 16 | encryptor?: GenericEncryptor | ExportableKeyEncryptor; 17 | cacheEncryptionKey: false; 18 | } 19 | ); 20 | 21 | export type KeyringObject = { 22 | type: string; 23 | accounts: string[]; 24 | }; 25 | 26 | export type KeyringControllerPersistentState = { 27 | vault?: string; 28 | }; 29 | 30 | export type KeyringControllerState = { 31 | keyrings: KeyringObject[]; 32 | isUnlocked: boolean; 33 | } & ( 34 | | { encryptionKey: string; encryptionSalt: string } 35 | | { 36 | encryptionKey?: never; 37 | encryptionSalt?: never; 38 | } 39 | ); 40 | 41 | export type SerializedKeyring = { 42 | type: string; 43 | data: Json; 44 | }; 45 | 46 | /** 47 | * A generic encryptor interface that supports encrypting and decrypting 48 | * serializable data with a password. 49 | */ 50 | export type GenericEncryptor = { 51 | /** 52 | * Encrypts the given object with the given password. 53 | * 54 | * @param password - The password to encrypt with. 55 | * @param object - The object to encrypt. 56 | * @returns The encrypted string. 57 | */ 58 | encrypt: (password: string, object: Json) => Promise; 59 | /** 60 | * Decrypts the given encrypted string with the given password. 61 | * 62 | * @param password - The password to decrypt with. 63 | * @param encryptedString - The encrypted string to decrypt. 64 | * @returns The decrypted object. 65 | */ 66 | decrypt: (password: string, encryptedString: string) => Promise; 67 | /** 68 | * Optional vault migration helper. Checks if the provided vault is up to date 69 | * with the desired encryption algorithm. 70 | * 71 | * @param vault - The encrypted string to check. 72 | * @param targetDerivationParams - The desired target derivation params. 73 | * @returns The updated encrypted string. 74 | */ 75 | isVaultUpdated?: ( 76 | vault: string, 77 | targetDerivationParams?: KeyDerivationOptions, 78 | ) => boolean; 79 | }; 80 | 81 | /** 82 | * An encryptor interface that supports encrypting and decrypting 83 | * serializable data with a password, and exporting and importing keys. 84 | */ 85 | export type ExportableKeyEncryptor = GenericEncryptor & { 86 | /** 87 | * Encrypts the given object with the given encryption key. 88 | * 89 | * @param key - The encryption key to encrypt with. 90 | * @param object - The object to encrypt. 91 | * @returns The encryption result. 92 | */ 93 | encryptWithKey: (key: unknown, object: Json) => Promise; 94 | /** 95 | * Encrypts the given object with the given password, and returns the 96 | * encryption result and the exported key string. 97 | * 98 | * @param password - The password to encrypt with. 99 | * @param object - The object to encrypt. 100 | * @param salt - The optional salt to use for encryption. 101 | * @returns The encrypted string and the exported key string. 102 | */ 103 | encryptWithDetail: ( 104 | password: string, 105 | object: Json, 106 | salt?: string, 107 | ) => Promise; 108 | /** 109 | * Decrypts the given encrypted string with the given encryption key. 110 | * 111 | * @param key - The encryption key to decrypt with. 112 | * @param encryptedString - The encrypted string to decrypt. 113 | * @returns The decrypted object. 114 | */ 115 | decryptWithKey: (key: unknown, encryptedString: string) => Promise; 116 | /** 117 | * Decrypts the given encrypted string with the given password, and returns 118 | * the decrypted object and the salt and exported key string used for 119 | * encryption. 120 | * 121 | * @param password - The password to decrypt with. 122 | * @param encryptedString - The encrypted string to decrypt. 123 | * @returns The decrypted object and the salt and exported key string used for 124 | * encryption. 125 | */ 126 | decryptWithDetail: ( 127 | password: string, 128 | encryptedString: string, 129 | ) => Promise; 130 | /** 131 | * Generates an encryption key from exported key string. 132 | * 133 | * @param key - The exported key string. 134 | * @returns The encryption key. 135 | */ 136 | importKey: (key: string) => Promise; 137 | }; 138 | -------------------------------------------------------------------------------- /src/types/@metamask/eth-hd-keyring.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/unambiguous 2 | declare module '@metamask/eth-hd-keyring'; 3 | -------------------------------------------------------------------------------- /src/types/@metamask/eth-simple-keyring.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/unambiguous 2 | declare module '@metamask/eth-simple-keyring'; 3 | -------------------------------------------------------------------------------- /src/types/obs-store.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/unambiguous 2 | declare module 'obs-store'; 3 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Throws an error. 3 | * 4 | * @param error - Error message or Error object to throw. 5 | */ 6 | export function throwError(error: string | Error): never { 7 | throw typeof error === 'string' ? new Error(error) : error; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "inlineSources": true, 6 | "noEmit": false, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "sourceMap": true 10 | }, 11 | "include": ["./src/**/*.ts"], 12 | "exclude": ["./src/**/*.test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "exactOptionalPropertyTypes": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "lib": ["ES2020", "dom"], 7 | "module": "CommonJS", 8 | "moduleResolution": "node", 9 | "noEmit": true, 10 | "noErrorTruncation": true, 11 | "noUncheckedIndexedAccess": true, 12 | "strict": true, 13 | "target": "ES2017", 14 | "skipLibCheck": true 15 | }, 16 | "exclude": ["./dist/**/*", "node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./src/index.ts"], 3 | "excludePrivate": true, 4 | "hideGenerator": true, 5 | "out": "docs" 6 | } 7 | --------------------------------------------------------------------------------