From 5b02d4e68d8137022d7153a306202b1a59c3e518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:18:25 +0200 Subject: [PATCH 01/10] Bump @metamask/base-controller from 3.2.0 to 3.2.1 (#1698) Bumps [@metamask/base-controller](https://github.com/MetaMask/core) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/MetaMask/core/releases) - [Commits](https://github.com/MetaMask/core/compare/@metamask/base-controller@3.2.0...@metamask/base-controller@3.2.1) --- updated-dependencies: - dependency-name: "@metamask/base-controller" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Frederik Bolding --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ec3939f199..e40c249df1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,12 +3680,12 @@ __metadata: linkType: hard "@metamask/base-controller@npm:^3.2.0": - version: 3.2.0 - resolution: "@metamask/base-controller@npm:3.2.0" + version: 3.2.1 + resolution: "@metamask/base-controller@npm:3.2.1" dependencies: "@metamask/utils": ^6.2.0 immer: ^9.0.6 - checksum: 3be6f2594309c013e07f83c4bb8271e1e99f02b6ff829c18b5e7218fbab4e6a9e03bcb49056704ce47f84ae2f38b1bc1c10284ec538aad56ed7b554ef2d3e189 + checksum: 73723a275ad2c8c6f9fcfcd69fd8b469bf8f4455fc572fc19ac9a8d76e5b65152cb111d95bfb9a4b9443298147e03a5f9778747dec2ca2203495ace54c97688c languageName: node linkType: hard From 9ca4a9d4d9e7e621e3f5d6437422400619fd43af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:51:42 +0200 Subject: [PATCH 02/10] Bump @metamask/approval-controller from 3.5.0 to 3.5.1 (#1696) Bumps [@metamask/approval-controller](https://github.com/MetaMask/core) from 3.5.0 to 3.5.1. - [Release notes](https://github.com/MetaMask/core/releases) - [Commits](https://github.com/MetaMask/core/compare/@metamask/approval-controller@3.5.0...@metamask/approval-controller@3.5.1) --- updated-dependencies: - dependency-name: "@metamask/approval-controller" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index e40c249df1..b35a73b582 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3653,15 +3653,15 @@ __metadata: linkType: hard "@metamask/approval-controller@npm:^3.5.0": - version: 3.5.0 - resolution: "@metamask/approval-controller@npm:3.5.0" + version: 3.5.1 + resolution: "@metamask/approval-controller@npm:3.5.1" dependencies: - "@metamask/base-controller": ^3.2.0 + "@metamask/base-controller": ^3.2.1 "@metamask/utils": ^6.2.0 eth-rpc-errors: ^4.0.2 immer: ^9.0.6 nanoid: ^3.1.31 - checksum: d555e2b1365fcbc6337ec0ba0a35f9b00485c92cc734e7d6457a77e4c4833eca46bf2950dbcfe4f4831d980af5457851bac522b23bd68c4dc44a58fd026d7674 + checksum: 5d4d1425d44b3848b4486744472fb8eeb31ca9a70d58c6ca6e0603bcbba32f1c5dcd04a8fb2ab6b4bdbf2b4e3309f75387d7d345cd2aedee5e3d6b5d4af4dbad languageName: node linkType: hard @@ -3679,7 +3679,7 @@ __metadata: languageName: node linkType: hard -"@metamask/base-controller@npm:^3.2.0": +"@metamask/base-controller@npm:^3.2.0, @metamask/base-controller@npm:^3.2.1": version: 3.2.1 resolution: "@metamask/base-controller@npm:3.2.1" dependencies: From a596bee787044df36b4f46e2f56ac7e6af615931 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 29 Aug 2023 14:31:37 +0200 Subject: [PATCH 03/10] Update pull request using MetaMask bot (#1639) * Automatically update Dependabot pull requests * Update condition and commit user and email * Update pull request when triggered by comment * Add missing workflow name to needs * Use versioning-strategy: increase for Dependabot * Remove unnecessary checks --- .github/dependabot.yml | 4 +- .github/workflows/update-pull-request.yml | 212 ++++++++++++++++++++++ 2 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/update-pull-request.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 098d74c290..42753c7d3f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,5 @@ # Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: @@ -11,5 +11,5 @@ updates: allow: - dependency-name: '@metamask/*' target-branch: 'main' - versioning-strategy: 'increase-if-necessary' + versioning-strategy: 'increase' open-pull-requests-limit: 10 diff --git a/.github/workflows/update-pull-request.yml b/.github/workflows/update-pull-request.yml new file mode 100644 index 0000000000..da6bfbc5b8 --- /dev/null +++ b/.github/workflows/update-pull-request.yml @@ -0,0 +1,212 @@ +name: Update pull request + +on: + issue_comment: + types: + - created + +jobs: + is-fork-pull-request: + name: Determine whether this issue comment was on a pull request from a fork + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '@metamaskbot update-pr') }} + runs-on: ubuntu-latest + outputs: + IS_FORK: ${{ steps.is-fork.outputs.IS_FORK }} + steps: + - uses: actions/checkout@v3 + - name: Determine whether this PR is from a fork + id: is-fork + run: echo "IS_FORK=$(gh pr view --json isCrossRepository --jq '.isCrossRepository' "${PR_NUMBER}" )" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + + react-to-comment: + name: React to the comment + runs-on: ubuntu-latest + needs: is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks. + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: React to the comment + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \ + -f content='+1' + env: + COMMENT_ID: ${{ github.event.comment.id }} + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + REPO: ${{ github.repository }} + + prepare: + name: Prepare dependencies + runs-on: ubuntu-latest + needs: is-fork-pull-request + # Early exit if this is a fork, since later steps are skipped for forks. + if: ${{ needs.is-fork-pull-request.outputs.IS_FORK == 'false' }} + outputs: + COMMIT_SHA: ${{ steps.commit-sha.outputs.COMMIT_SHA }} + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install Yarn dependencies + run: yarn --immutable + - name: Get commit SHA + id: commit-sha + run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + dedupe-yarn-lock: + name: Deduplicate yarn.lock + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Deduplicate yarn.lock + run: yarn dedupe + - name: Cache yarn.lock + uses: actions/cache/save@v3 + with: + path: yarn.lock + key: cache-yarn-lock-${{ needs.prepare.outputs.COMMIT_SHA }} + + regenerate-lavamoat-policies: + name: Regenerate LavaMoat policies + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Regenerate LavaMoat policies + run: yarn build:lavamoat:policy + - name: Cache LavaMoat policies + uses: actions/cache/save@v3 + with: + path: packages/snaps-execution-environments/lavamoat + key: cache-lavamoat-${{ needs.prepare.outputs.COMMIT_SHA }} + + update-examples: + name: Update examples + runs-on: ubuntu-latest + needs: prepare + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + - name: Install dependencies from cache + run: yarn --immutable --immutable-cache + - name: Update examples + run: yarn build + - name: Cache examples + uses: actions/cache/save@v3 + with: + path: packages/examples/packages + key: cache-examples-${{ needs.prepare.outputs.COMMIT_SHA }} + + commit-result: + name: Commit result + runs-on: ubuntu-latest + needs: + - prepare + - dedupe-yarn-lock + - regenerate-lavamoat-policies + - update-examples + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + # Use PAT to ensure that the commit later can trigger status check + # workflows. + token: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + - name: Checkout pull request + run: gh pr checkout "${PR_NUMBER}" + env: + GITHUB_TOKEN: ${{ secrets.PULL_REQUEST_UPDATE_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + - name: Configure Git + run: | + git config --global user.name 'MetaMask Bot' + git config --global user.email 'metamaskbot@users.noreply.github.com' + - name: Get commit SHA + id: commit-sha + run: echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + - name: Restore yarn.lock + uses: actions/cache/restore@v3 + with: + path: yarn.lock + key: cache-yarn-lock-${{ needs.prepare.outputs.COMMIT_SHA }} + fail-on-cache-miss: true + - name: Commit yarn.lock + run: | + git add yarn.lock + git commit -m "Deduplicate yarn.lock" || true + - name: Restore LavaMoat policies + uses: actions/cache/restore@v3 + with: + path: packages/snaps-execution-environments/lavamoat + key: cache-lavamoat-${{ needs.prepare.outputs.COMMIT_SHA }} + fail-on-cache-miss: true + - name: Commit LavaMoat policies + run: | + git add packages/snaps-execution-environments/lavamoat + git commit -m "Update LavaMoat policies" || true + - name: Restore examples + uses: actions/cache/restore@v3 + with: + path: packages/examples/packages + key: cache-examples-${{ needs.prepare.outputs.COMMIT_SHA }} + fail-on-cache-miss: true + - name: Commit examples + run: | + git add packages/examples/packages + git commit -m "Update example snaps" || true + - name: Push changes + run: git push From a399091612bcae66496e77687b7957352d679b9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:32:40 +0200 Subject: [PATCH 04/10] Bump @metamask/post-message-stream from 6.1.2 to 6.2.0 (#1707) Bumps [@metamask/post-message-stream](https://github.com/MetaMask/post-message-stream) from 6.1.2 to 6.2.0. - [Release notes](https://github.com/MetaMask/post-message-stream/releases) - [Changelog](https://github.com/MetaMask/post-message-stream/blob/main/CHANGELOG.md) - [Commits](https://github.com/MetaMask/post-message-stream/compare/v6.1.2...v6.2.0) --- updated-dependencies: - dependency-name: "@metamask/post-message-stream" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/snaps-controllers/package.json | 2 +- packages/snaps-execution-environments/package.json | 2 +- packages/snaps-utils/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/snaps-controllers/package.json b/packages/snaps-controllers/package.json index 3439fb75c6..d0265a2fff 100644 --- a/packages/snaps-controllers/package.json +++ b/packages/snaps-controllers/package.json @@ -47,7 +47,7 @@ "@metamask/base-controller": "^3.2.0", "@metamask/object-multiplex": "^1.2.0", "@metamask/permission-controller": "^4.1.0", - "@metamask/post-message-stream": "^6.1.2", + "@metamask/post-message-stream": "^6.2.0", "@metamask/rpc-methods": "workspace:^", "@metamask/snaps-execution-environments": "workspace:^", "@metamask/snaps-registry": "^1.2.2", diff --git a/packages/snaps-execution-environments/package.json b/packages/snaps-execution-environments/package.json index 4b04c9c972..9d5d57478c 100644 --- a/packages/snaps-execution-environments/package.json +++ b/packages/snaps-execution-environments/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@metamask/object-multiplex": "^1.2.0", - "@metamask/post-message-stream": "^6.1.2", + "@metamask/post-message-stream": "^6.2.0", "@metamask/providers": "^11.1.1", "@metamask/rpc-methods": "workspace:^", "@metamask/snaps-utils": "workspace:^", diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 1bb492ea3e..81f9ad4385 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -97,7 +97,7 @@ "@metamask/eslint-config-jest": "^12.1.0", "@metamask/eslint-config-nodejs": "^12.1.0", "@metamask/eslint-config-typescript": "^12.1.0", - "@metamask/post-message-stream": "^6.1.2", + "@metamask/post-message-stream": "^6.2.0", "@swc/cli": "^0.1.62", "@swc/core": "1.3.78", "@swc/jest": "^0.2.26", diff --git a/yarn.lock b/yarn.lock index b35a73b582..d3aac5c22a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4728,13 +4728,13 @@ __metadata: languageName: node linkType: hard -"@metamask/post-message-stream@npm:^6.1.2": - version: 6.1.2 - resolution: "@metamask/post-message-stream@npm:6.1.2" +"@metamask/post-message-stream@npm:^6.2.0": + version: 6.2.0 + resolution: "@metamask/post-message-stream@npm:6.2.0" dependencies: "@metamask/utils": ^5.0.0 readable-stream: 2.3.3 - checksum: 3591ec9b7fd602806b07cbc0fed5075fb7a347c279c43ef1f25fbdd8634dfcad9ce192ae59457fb76554ef0bc15cbf25cfaa5875aee2d72668a273b7a6852c32 + checksum: 657cdb2dd61a46a4da7f036a97ef0aa9ad8e918d8f8c0fd620eaede4a32c2ff909738a7dfb2b1e6099e7771fd03c3466b60fedab56e39a5cc5507927758e3cb7 languageName: node linkType: hard @@ -5034,7 +5034,7 @@ __metadata: "@metamask/eslint-config-typescript": ^12.1.0 "@metamask/object-multiplex": ^1.2.0 "@metamask/permission-controller": ^4.1.0 - "@metamask/post-message-stream": ^6.1.2 + "@metamask/post-message-stream": ^6.2.0 "@metamask/rpc-methods": "workspace:^" "@metamask/snaps-execution-environments": "workspace:^" "@metamask/snaps-registry": ^1.2.2 @@ -5121,7 +5121,7 @@ __metadata: "@metamask/eslint-config-nodejs": ^12.1.0 "@metamask/eslint-config-typescript": ^12.1.0 "@metamask/object-multiplex": ^1.2.0 - "@metamask/post-message-stream": ^6.1.2 + "@metamask/post-message-stream": ^6.2.0 "@metamask/providers": ^11.1.1 "@metamask/rpc-methods": "workspace:^" "@metamask/snaps-utils": "workspace:^" @@ -5484,7 +5484,7 @@ __metadata: "@metamask/eslint-config-typescript": ^12.1.0 "@metamask/key-tree": ^9.0.0 "@metamask/permission-controller": ^4.1.0 - "@metamask/post-message-stream": ^6.1.2 + "@metamask/post-message-stream": ^6.2.0 "@metamask/snaps-registry": ^1.2.2 "@metamask/snaps-ui": "workspace:^" "@metamask/utils": ^7.1.0 From 5ed28e41699784e25c0ecf37b0205a44df3406cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 09:46:09 +0200 Subject: [PATCH 05/10] Bump @metamask/permission-controller from 4.1.0 to 4.1.1 (#1697) Bumps [@metamask/permission-controller](https://github.com/MetaMask/core) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/MetaMask/core/releases) - [Commits](https://github.com/MetaMask/core/compare/@metamask/permission-controller@4.1.0...@metamask/permission-controller@4.1.1) --- updated-dependencies: - dependency-name: "@metamask/permission-controller" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com> Co-authored-by: Frederik Bolding --- yarn.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index d3aac5c22a..d61241519d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3652,7 +3652,7 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^3.5.0": +"@metamask/approval-controller@npm:^3.5.0, @metamask/approval-controller@npm:^3.5.1": version: 3.5.1 resolution: "@metamask/approval-controller@npm:3.5.1" dependencies: @@ -3897,9 +3897,9 @@ __metadata: languageName: unknown linkType: soft -"@metamask/controller-utils@npm:^4.3.0": - version: 4.3.1 - resolution: "@metamask/controller-utils@npm:4.3.1" +"@metamask/controller-utils@npm:^4.3.2": + version: 4.3.2 + resolution: "@metamask/controller-utils@npm:4.3.2" dependencies: "@metamask/eth-query": ^3.0.1 "@metamask/utils": ^6.2.0 @@ -3909,7 +3909,7 @@ __metadata: ethereumjs-util: ^7.0.10 ethjs-unit: ^0.1.6 fast-deep-equal: ^3.1.3 - checksum: 5bb471df560a12fba1b7fa147fe0332e06b527637c04facff1774b1279dd388b4cf1d74340469adb13551c08cc156f204d90e36599ad69b54716b11e5842b348 + checksum: 0af7de11bee8cea81946c424d51e2f0a27f3deeebb491aed00262b2f76b4b1e16ff8fa129309944b3e16a6531b0d153dde6794011bf355234fdebb965261372e languageName: node linkType: hard @@ -4709,12 +4709,12 @@ __metadata: linkType: hard "@metamask/permission-controller@npm:^4.1.0": - version: 4.1.0 - resolution: "@metamask/permission-controller@npm:4.1.0" + version: 4.1.1 + resolution: "@metamask/permission-controller@npm:4.1.1" dependencies: - "@metamask/approval-controller": ^3.5.0 - "@metamask/base-controller": ^3.2.0 - "@metamask/controller-utils": ^4.3.0 + "@metamask/approval-controller": ^3.5.1 + "@metamask/base-controller": ^3.2.1 + "@metamask/controller-utils": ^4.3.2 "@metamask/utils": ^6.2.0 "@types/deep-freeze-strict": ^1.1.0 deep-freeze-strict: ^1.1.1 @@ -4723,8 +4723,8 @@ __metadata: json-rpc-engine: ^6.1.0 nanoid: ^3.1.31 peerDependencies: - "@metamask/approval-controller": ^3.5.0 - checksum: dc0a78321d1331070eb3775928c4c0b0138515c6449c09a73c2243ca8d55801f5a97c4ce2229cdbf630d1a893ec373474d8c17cb35e06c26b0d5ea490c402c48 + "@metamask/approval-controller": ^3.5.1 + checksum: 45f94c805302256d54f4e25d7f15fd22ea8c9c8548cca4514babeccfe31cfef47ca79dacd5fc45013120a0ad22eaa0ded4b56d37bf59f4dc2712d58edcc0aaca languageName: node linkType: hard From 094bcca3352487835c2dfc9a392eeb288f9c395b Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Aug 2023 10:45:31 +0200 Subject: [PATCH 06/10] Fix error when using single quotes in UI builder (#1709) --- packages/snaps-simulator/src/features/builder/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-simulator/src/features/builder/utils.ts b/packages/snaps-simulator/src/features/builder/utils.ts index a719039c48..e1057aff0c 100644 --- a/packages/snaps-simulator/src/features/builder/utils.ts +++ b/packages/snaps-simulator/src/features/builder/utils.ts @@ -89,7 +89,7 @@ function getComponentArgs(component: Component): string { case NodeType.Text: case NodeType.Heading: case NodeType.Copyable: - return `'${component.value}'`; + return `'${component.value.replace(/'/gu, "\\'")}'`; case NodeType.Spinner: case NodeType.Divider: default: From 690d62155038c1f60461da077f130b53f73e8825 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 11:14:32 +0200 Subject: [PATCH 07/10] Fix unpacking zero byte files from NPM (#1708) --- packages/snaps-controllers/src/snaps/location/npm.test.ts | 4 ++-- packages/snaps-controllers/src/snaps/location/npm.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/location/npm.test.ts b/packages/snaps-controllers/src/snaps/location/npm.test.ts index 837bd17b27..d15edfc7c9 100644 --- a/packages/snaps-controllers/src/snaps/location/npm.test.ts +++ b/packages/snaps-controllers/src/snaps/location/npm.test.ts @@ -66,11 +66,11 @@ describe('NpmLocation', () => { const manifest = await location.manifest(); const sourceCode = ( await location.fetch(manifest.result.source.location.npm.filePath) - ).value.toString(); + ).toString(); assert(manifest.result.source.location.npm.iconPath); const svgIcon = ( await location.fetch(manifest.result.source.location.npm.iconPath) - ).value.toString(); + ).toString(); expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock).toHaveBeenNthCalledWith( diff --git a/packages/snaps-controllers/src/snaps/location/npm.ts b/packages/snaps-controllers/src/snaps/location/npm.ts index ffa807e549..4f82fa3b98 100644 --- a/packages/snaps-controllers/src/snaps/location/npm.ts +++ b/packages/snaps-controllers/src/snaps/location/npm.ts @@ -376,7 +376,7 @@ function createTarballStream( // The name is a path if the header type is "file". const path = headerName.replace(NPM_TARBALL_PATH_PREFIX, ''); return entryStream.pipe( - concat((data) => { + concat({ encoding: 'uint8array' }, (data) => { try { totalSize += data.byteLength; // To prevent zip bombs, we set a safety limit for the total size of tarballs. From 9677c20826461524e5398a53dcda4c4798607deb Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Aug 2023 12:36:48 +0200 Subject: [PATCH 08/10] Document Snaps platform internals (#1635) * WIP: Document Snaps platform internals * Add documentation for snaps registry * Add documentation for permissions * Update docs as part of readthrough * Add RPC methods snippet * Minor fixes * Remove empty section * Apply suggestions from code review Co-authored-by: Frederik Bolding --------- Co-authored-by: Frederik Bolding --- docs/internals/architecture.md | 76 +++++ docs/internals/execution.md | 52 +++ docs/internals/json-rpc.md | 80 +++++ docs/internals/permissions.md | 305 ++++++++++++++++++ .../platform/execution-environment.md | 33 ++ docs/internals/platform/execution-service.md | 67 ++++ docs/internals/platform/readme.md | 12 + docs/internals/platform/rpc-methods.md | 24 ++ docs/internals/platform/snap-controller.md | 33 ++ docs/internals/platform/snaps-registry.md | 47 +++ docs/internals/readme.md | 20 ++ docs/readme.md | 10 + 12 files changed, 759 insertions(+) create mode 100644 docs/internals/architecture.md create mode 100644 docs/internals/execution.md create mode 100644 docs/internals/json-rpc.md create mode 100644 docs/internals/permissions.md create mode 100644 docs/internals/platform/execution-environment.md create mode 100644 docs/internals/platform/execution-service.md create mode 100644 docs/internals/platform/readme.md create mode 100644 docs/internals/platform/rpc-methods.md create mode 100644 docs/internals/platform/snap-controller.md create mode 100644 docs/internals/platform/snaps-registry.md create mode 100644 docs/internals/readme.md create mode 100644 docs/readme.md diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md new file mode 100644 index 0000000000..753b5a306f --- /dev/null +++ b/docs/internals/architecture.md @@ -0,0 +1,76 @@ +# Architecture + +Below is a sequence diagram of the architecture of the Metamask Snaps system. It +shows the flow of a request from a dapp to a snap, and back. + +```mermaid +sequenceDiagram + participant d as Dapp + participant mm as MetaMask Background + participant sc as Snap Controller + participant ex as Execution Service + participant i as Iframe + participant exe as Execution Environment + participant ses as SES Compartment + participant s as A-Snap + Note over d,mm: Invoke A-Snap with RPC request + Note over d,mm: Permission for the dapp to invoke + d->>mm: invokeSnap(snapId, request) + Note over mm: Provider engine:
Permission to call handler check
Call matching handler + mm->>sc: handleRequest + Note over sc: > Snap is not running + sc->>sc: startSnap(snapId, code) + Note over sc: Permissions translated to actual endowments + sc->>ex: executeSnap
(snapId, code, endowments) + ex->>i: Create iframe + i->exe: Load environment + i-->>ex: _ + exe->>exe: Initialization + ex->>ex: Set up streams and job + Note over ex,exe: postMessage is set up
⚠️ e.source===_targetWindow + Note over ex, exe: can use command(method, RPC) now + ex->>exe: await command("ping", …) + exe-->>ex: OK + ex->>mm: setupSnapProvider(snapId, stream) + Note over mm,ex: Communication is set up on the stream,
⚠️ checked for subjectType and snapId as origin + mm-->>ex: _ + ex->>exe: await command("executeSnap", A-Snap code) + exe->>exe: Create endowments, module etc. + exe->>ses: Create Compartment + exe->>exe: this.executeInSnapContext + exe->>ses: evaluate(A-Snap code) + ses->>s: Execute + s-->>ses: Export RPC handler + ses-->>exe: _ + exe->>exe: ⚠️ Validate and register exports + exe-->>ex: OK + ex->>ex: createSnapHooks + Note over ex: Wires up snapRpc to the exported handler + ex-->>sc: OK + Note over sc: Remember: We received a request.
It can now be sent to snapRpc + sc->>ex: handleRpcRequest + Note over sc,ex: Request from dapp is wrapped in
another RPC for snap command + sc->>sc: Set up timer + ex->>exe: Handle RPC + exe->s: Handle RPC + s->>s: Do stuff + Note over exe,s: Snap sends an RPC request through
the endowed API + s->>exe: request + exe->>exe: Check if method is wallet_* or snap_* + Note over exe: Asserts defensively, doesn't use
method.startsWith + exe->>mm: RPC request + Note over mm: Provider engine:
Permission to call handler check
Call matching handler + mm-->>exe: RPC response + exe-->>s: RPC response + s->>s: Do stuff + + s-->>exe: Snap response + exe->>exe: Check if returned value is valid JSON + + exe-->>ex: Snap response + ex->>ex: Throw if response is an error + + ex-->>sc: Snap response + sc->>sc: Cancel timer + sc-->>d: Snap response +``` diff --git a/docs/internals/execution.md b/docs/internals/execution.md new file mode 100644 index 0000000000..b30c4055f8 --- /dev/null +++ b/docs/internals/execution.md @@ -0,0 +1,52 @@ +# Secure snap execution in SES + +To avoid snaps from getting access to the client, dangerous APIs, and such, +we run snaps in a different "realm," which is a separate JavaScript execution +environment. Inside this realm we use [SES]'s lockdown feature to harden the +realm, and prevent the snap from breaking out of the sandbox. For certain APIs, +such as the events API, we must do an extra lockdown, to make sure that the +snap can't break out of the sandbox. + +Inside this realm, we create an [SES] compartment, which is a sandboxed +JavaScript environment that limits access to global APIs, letting us control +what the snap can do. + +## Endowments + +The endowments are the global APIs that are available to the snap, such as the +`console` API, the `fetch` function, and so on. To avoid the snap +breaking out of the sandbox, we only give it access to a limited set of APIs, +and we make sure that the APIs we give it are safe to use. For example, snaps +don't have access to the `window` or `document` object, so they can't access the +DOM. + +Each endowment we provide to the snap is hardened in a couple of ways: + +- We freeze and seal the object, so that the snap can't modify it or add new + properties to it. +- We only provide a limited subset of APIs. +- Certain APIs are wrapped to ensure that they can be torn down properly + when the snap is being stopped as well as to prevent snaps interfering with + each other. + +Some endowments are provided to the snap by default. A list of these can be +found [here](../../packages/snaps-utils/src/default-endowments.ts). Other +endowments must be requested by the snap, using the permissions system. + +Endowments granted via the permission system map to one or more global APIs, +e.g., [endowment:network-access] grants access to `fetch`, `Request`, `Headers`, +and `Response`. These endowments may also be further hardened before being +passed to the snap, see for example [network hardening], which hardens the +`fetch` global before granting it to the snap. + + + +[endowment:network-access]: ../../packages/snaps-controllers/src/snaps/endowments/network-access.ts +[network hardening]: ../../packages/snaps-execution-environments/src/common/endowments/network.ts +[ses]: https://github.com/endojs/endo/tree/master/packages/ses diff --git a/docs/internals/json-rpc.md b/docs/internals/json-rpc.md new file mode 100644 index 0000000000..3820cd010d --- /dev/null +++ b/docs/internals/json-rpc.md @@ -0,0 +1,80 @@ +# JSON-RPC + +The JSON-RPC stack is the core of the MetaMask clients. It is responsible for +handling all JSON-RPC requests from dapps, and for sending JSON-RPC requests to +the right places, depending on the request. The stack consists of the following +components: + +- The JSON-RPC engine, which is the base of the stack. It receives JSON-RPC + requests from any source, and sends them to the right handler. +- One or more middlewares, which can modify the request before it is sent to the + handler, or handle the request themselves. + +## The JSON-RPC engine + +The JSON-RPC engine is the base of the JSON-RPC stack. It receives JSON-RPC +requests from any source, and sends them to the right handler. It is +implemented in the [json-rpc-engine] package. + +Each connection to MetaMask creates a new instance of the engine with a +potentially unique configuration. This allows us to have different middlewares +for different connections. For example, requests from snaps are handled +differently than requests from dapps. + +## Middlewares + +Middlewares are functions that can modify the request before it is sent to the +handler, or handle the request themselves. They are executed in the order they +are added to the engine, **meaning the order they are added to the engine in +matters**. + +A middleware function looks like this: + +```ts +type Middleware = ( + request: JsonRpcRequest, + response: JsonRpcResponse, + next: NextFunction, + end: EndFunction, +) => void; +``` + +The `request` and `response` parameters are the JSON-RPC request and response +objects, respectively. The response object is initially empty, and is passed +from middleware to middleware. Each middleware can modify both the request and +response objects before passing them to the next middleware. Since the +middlewares are executed in order, one middleware may modify the objects that +the following middlewares see. + +The `next` and `end` functions are used to control the flow of the middleware +stack. If the `next` function is called, the next middleware in the stack will +be executed. If the `end` function is called, no other middleware will be +executed. The response object will be sent to the client, when the `end` +function is called. + +```mermaid +sequenceDiagram + Client->>Engine: Send request + loop + Engine->>Middleware: Call middleware + Middleware->>Middleware: Modify response and/or request + Middleware->>Engine: Call next middleware + end + Middleware->>Engine: Call end + Engine->>Client: Send response +``` + +Once all middlewares have been executed or the `end` callback has been invoked +the JSON-RPC engine will return the resulting response object. If defined, this +object should be a JSON-RPC specification result object that contains either a +`result` property or an error `property`. The engine may also return an error in +case of issues processing a request. + +## JSON-RPC in MetaMask Snaps + +Snaps are executed in a separate environment, and communicate with the extension +through JSON-RPC. The JSON-RPC stack for snaps is different from the JSON-RPC +stack for dapps: Snaps don't have the same capabilities as dapps, and they +shouldn't be able to access the same APIs. + +[json-rpc-engine]: https://github.com/MetaMask/json-rpc-engine diff --git a/docs/internals/permissions.md b/docs/internals/permissions.md new file mode 100644 index 0000000000..3d5474f856 --- /dev/null +++ b/docs/internals/permissions.md @@ -0,0 +1,305 @@ +# Permissions + +> Based on the [PermissionController architecture documentation](https://github.com/MetaMask/core/blob/main/packages/permission-controller/ARCHITECTURE.md), +> originally written by Erik Marks. + +MetaMask Snaps uses the [PermissionController] for managing permissions for +snaps, just like the extension uses it to manage permissions for websites. The +[PermissionController] is the heart of an object capability-inspired permission +system. It is a stateful system that mediates access to resources, and it +provides a mechanism for attenuating the authority granted by a permission. + +The [PermissionController] exposes a [JSON-RPC] middleware, which is added to +the JSON-RPC engine of the MetaMask instance. This middleware is responsible +for enforcing the rules of the permission system. + +## Overview + +The permission system itself belongs to a **host**, like the MetaMask +extension, and it mediates the access to resources – called **targets** – of +distinct **subjects**. A target can belong to the host itself, or another +subject. + +When a subject attempts to access a target, we say that they **invoke** it. The +system ensures that subjects can only invoke a target if they have the +**permission** to do so. Permissions are associated with a subject and target, +and they are part of the state of the permission system. + +Permissions can have **caveats**, which are host-defined attenuations of the +authority a permission grants over a particular target. + +## Implementation + +At any given moment, the [PermissionController] state tree describes the +complete state of the permissions of all subjects known to the host (i.e., the +MetaMask instance). The [PermissionController] also provides methods for adding, +updating, and removing permissions, and enforcing the rules described by its +state tree. Permission system concepts correspond to components of the MetaMask +stack as follows: + +| Concept | Implementation | +| :---------------- | :-------------------------------------------------------------- | +| Host | The MetaMask application | +| Subjects | Websites, snaps, or other extensions | +| Targets | JSON-RPC methods, endowments | +| Invocations | JSON-RPC requests, endowment retrieval | +| Permissions | Permission objects | +| Caveats | Caveat objects | +| Permission system | The [PermissionController] and its `json-rpc-engine` middleware | + +### Permission and target types + +In practice, targets can be different things, necessitating distinct +implementations in order to enforce the logic of the permission system. This +being the case, the [PermissionController] defines different **permission and +target types**, intended for different kinds of permission targets. + +#### JSON-RPC Methods + +Restricting access to JSON-RPC methods was the motivating and only supported use +case for the original permission system, and remains the predominant kind of +permission to this day. The [PermissionController] provides patterns for +creating restricted JSON-RPC method implementations and caveats, and a +`json-rpc-engine` middleware function factory. To permission a JSON-RPC server, +every JSON-RPC method must be enumerated and designated as either "restricted" +or "unrestricted", and a permission middleware function must be added to the +`json-rpc-engine` middleware stack. Unrestricted methods can always be called +by anyone. Restricted methods require the requisite permission in order to be +called. + +Once the permission middleware is injected into the middleware stack, every +JSON-RPC request will be handled in one of the following ways: + +- If the requested method is neither restricted nor unrestricted, the request + will be rejected with a `methodNotFound` error. +- If the requested method is unrestricted, it will pass through the middleware + unmodified. +- If the requested method is restricted, the middleware will attempt to get the + permission corresponding to the subject and target, and: + - If the request is authorized, call the corresponding method with the request + parameters. + - If the request is not authorized, reject the request with an `unauthorized` + error. + +#### Endowments + +The name "endowment" comes from the endowments that you may provide to +[Secure EcmaScript (SES) `Compartment`](https://github.com/endojs/endo/tree/26d991afb01cf824827db0c958c50970e038112f/packages/ses#compartment) +when it is constructed. SES endowments are simply names that appear in the +compartment's global scope. In the context of the [PermissionController], +endowments are simply "things" that subjects should not be able to access by +default. They _could_ be the names of endowments that are to be made available +to a particular SES `Compartment`, but they could also be any JavaScript value, +and it is the host's responsibility to make sense of them. + +### Caveats + +Caveats are arbitrary restrictions on restricted method requests. Every +permission has a `caveats` field, which is either an array of caveats or `null`. +Every caveat has a string `type`, and every type has an associated function that +is used to apply the caveat to a restricted method request. When the +[PermissionController] is constructed, the consumer specifies the available +caveat types and their implementations. + +#### Caveat mappings + +For MetaMask Snaps specifically, each permission that uses caveats has a +"caveat mapper," which is a function that maps a certain value to a valid +caveat. For example, the `snap_getBip32Entropy` permission has a caveat mapper +that maps the value in the snap's manifest to a `PermittedDerivationPaths` +object: + +```ts +export function permittedDerivationPathsCaveatMapper( + value: Json, +): Pick { + return { + caveats: [ + { + type: SnapCaveatType.PermittedDerivationPaths, + value, + }, + ], + }; +} +``` + +This makes it easier for snap developers to specify caveats, since they +don't have to know the exact format of the caveats. + +## Examples + +### Construction + +```typescript +// To construct a permission controller, we first need to define the caveat +// types and restricted methods. + +const caveatSpecifications = { + filterArrayResponse: { + type: 'filterArrayResponse', + // If a permission has any caveats, its corresponding restricted method + // implementation is decorated / wrapped with the implementations of its + // caveats, using the caveat's decorator function. + decorator: + ( + // Restricted methods and other caveats can be async, so we have to + // assume that the method is async. + method: AsyncRestrictedMethod, + caveat: FilterArrayCaveat, + ) => + async (args: RestrictedMethodOptions) => { + const result = await method(args); + if (!Array.isArray(result)) { + throw Error('not an array'); + } + + return result.filter((resultValue) => + caveat.value.includes(resultValue), + ); + }, + }, +}; + +// The property names of this object must be target names. +const permissionSpecifications = { + // This is a plain restricted method. + wallet_getSecretArray: { + // Every permission must have this field. + permissionType: PermissionType.RestrictedMethod, + // i.e., the restricted method name + targetName: 'wallet_getSecretArray', + allowedCaveats: ['filterArrayResponse'], + // Every restricted method must specify its implementation in its + // specification. + methodImplementation: ( + _args: RestrictedMethodOptions, + ) => { + return ['secret1', 'secret2', 'secret3']; + }, + }, + + // This is an endowment. + secretEndowment: { + permissionType: PermissionType.Endowment, + // Naming conventions for endowments are yet to be established. + targetName: 'endowment:globals', + // This function will be called to retrieve the subject's endowment(s). + // Here we imagine that these are the names of globals that will be made + // available to a SES Compartment. + endowmentGetter: (_options: EndowmentGetterParams) => [ + 'fetch', + 'Math', + 'setTimeout', + ], + }, +}; + +const permissionController = new PermissionController({ + caveatSpecifications, + messenger: controllerMessenger, // assume this was given + permissionSpecifications, + unrestrictedMethods: ['wallet_unrestrictedMethod'], +}); +``` + +### Adding the permission middleware + +```typescript +// This should take place where a middleware stack is created for a particular +// subject. + +// The subject could be a port, stream, socket, etc. +const origin = getOrigin(subject); + +const engine = new JsonRpcEngine(); +engine.push(/* your various middleware*/); +engine.push(permissionController.createPermissionMiddleware({ origin })); +// Your middleware stack is now permissioned. +engine.push(/* your other various middleware*/); +``` + +### Calling a Restricted Method Internally + +```typescript +// Sometimes, we need to call a restricted method internally, as a particular +// subject. +permissionController.executeRestrictedMethod(origin, 'wallet_getSecretArray'); + +// If the restricted method has any parameters, they are given as the third +// argument to executeRestrictedMethod(). +permissionController.executeRestrictedMethod(origin, 'wallet_getSecret', { + secretType: 'array', +}); +``` + +### Getting Endowments + +```typescript +// Getting endowments internally is the only option, since the host has to apply +// them in some way external to the permission system. +const endowments = await permissionController.getEndowments( + origin, + 'endowment:globals', +); + +// Now the endowments can be applied, whatever that means. +applyEndowments(origin, endowments); +``` + +### Requesting and getting permissions + +```typescript +// From the perspective of subjects, requesting and getting permissions +// works the same as it does with `rpc-cap`. +const approvedPermissions = await ethereum.request({ + method: 'wallet_requestPermissions', + params: [{ + wallet_getSecretArray: {}, + }] +}) + +const existingPermissions = await ethereum.request({ + method: 'wallet_getPermissions', +) +``` + +### Restricted method caveat decorators + +Here follows some more example caveat decorator implementations. + +```typescript +// Validation / passthrough +export function onlyArrayParams( + method: AsyncRestrictedMethod, + _caveat: Caveat<'PassthroughCaveat', never>, +) { + return async (args: RestrictedMethodOptions) => { + if (!Array.isArray(args.params)) { + throw new EthereumJsonRpcError(); + } + + return method(args); + }; +} + +// "Return handler" example +export function eth_accounts( + method: AsyncRestrictedMethod, + caveat: Caveat<'RestrictAccountCaveat', string[]>, +) { + return async (args: RestrictedMethodOptions) => { + const accounts: string[] | Json = await method(args); + if (!Array.isArray(args.params)) { + throw new EthereumJsonRpcError(); + } + + return ( + accounts.filter((account: string) => caveat.value.includes(account)) ?? [] + ); + }; +} +``` + +[permissioncontroller]: https://github.com/MetaMask/core/tree/main/packages/permission-controller +[json-rpc]: ./json-rpc.md diff --git a/docs/internals/platform/execution-environment.md b/docs/internals/platform/execution-environment.md new file mode 100644 index 0000000000..a71138b629 --- /dev/null +++ b/docs/internals/platform/execution-environment.md @@ -0,0 +1,33 @@ +# Execution Environment + +The MetaMask Snaps platform has several execution **environments**, not to be +confused with execution **services**. The execution environment is responsible +for executing the snap code, and for communicating with the [Execution Service]. + +Each execution environment consists of an **executor**, which is a JavaScript +bundle that is built with LavaMoat and run LavaMoat at runtime for runtime +protections. They are designed to run in a specific environment and to be paired +with a specific [Execution Service]. + +Currently, the following execution environments are supported: + +- Iframe (Manifest V2), which creates an iframe in the extension background DOM. +- Node.js process, which spawns a new process for each snap execution. +- Node.js worker threads, which creates a new worker thread for each snap + execution. +- Offscreen (Manifest V3), which uses the [Offscreen Document API] to create a + proxy to the iframe execution environment. +- WebWorker (Manifest V2), which creates a web worker for each snap execution. + +## Proxy execution environments + +The web worker and offscreen execution environments use proxy execution +environments. This means that the execution environment is actually a proxy to +another execution environment. This is done because the corresponding +[Execution Service]s of these execution environments are not able to communicate +directly with the execution environment. Instead, they communicate with the +proxy execution environment, which then forwards the messages to the actual +execution environment. + +[execution service]: ./execution-service.md +[offscreen document api]: https://developer.chrome.com/docs/extensions/reference/offscreen/ diff --git a/docs/internals/platform/execution-service.md b/docs/internals/platform/execution-service.md new file mode 100644 index 0000000000..82095c3132 --- /dev/null +++ b/docs/internals/platform/execution-service.md @@ -0,0 +1,67 @@ +# Execution Service + +The MetaMask Snaps platform has several execution **services**, not to be +confused with execution **environments**, which handle communication between the +[Snap Controller] and the [Execution Environment]. The execution service is +responsible for managing these execution environments. + +Currently, the execution service supports the following execution environments: + +- Iframe (Manifest V2), which creates an iframe in the extension background DOM. +- Node.js process, which spawns a new process for each snap execution. +- Node.js worker threads, which creates a new worker thread for each snap + execution. +- Offscreen (Manifest V3), which uses the [Offscreen Document API] to create a + proxy to the iframe execution environment. +- WebWorker (Manifest V2), which creates a web worker for each snap execution. + +Generally, these execution services work as follows: + +1. The [Snap Controller] calls the execution service to execute a snap. +2. The execution service creates an [Execution Environment] for the snap, e.g., + it creates an iframe in the extension. +3. The execution service sets up a stream using one of the [post message + stream]s, depending on the environment. +4. The execution service calls the [Execution Environment] to execute the snap + code in the [Execution Environment]. +5. The snap is now running and ready to service requests. + +```mermaid +sequenceDiagram + Controller->>+Service: Execute snap + activate Service + Service->>Service: Set up stream + Service-->>Environment: Create environment + Environment->>Environment: Set up stream + Environment->>Service: "SYN" + Service->>Environment: "ACK" + Service->>Environment: "ping" + Environment->>Service: "OK" + Service->>Environment: "executeSnap" + Environment->>Service: "OK" + Service->>Controller: "OK" + + deactivate Service +``` + +After this initial boot-up, the snap is now ready to service requests, also via +JSON-RPC. + +```mermaid +sequenceDiagram + Controller->>Service: handleRpcRequest + Service->>Environment: "snapRpc" + Environment->>Environment: Snap export is executed with given parameters + Environment->>Service: Response + Service->>Controller: Response +``` + +All of the communication between the [Snap Controller] and the execution service +is done through the controller messaging system in the MetaMask extension. The +execution service is hooked up to the controller messaging system, so it can +handle requests from other parts of the extension. + +[snap controller]: ./snap-controller.md +[execution environment]: ./execution-environment.md +[post message stream]: https://github.com/MetaMask/post-message-stream +[offscreen document api]: https://developer.chrome.com/docs/extensions/reference/offscreen/ diff --git a/docs/internals/platform/readme.md b/docs/internals/platform/readme.md new file mode 100644 index 0000000000..9af849487e --- /dev/null +++ b/docs/internals/platform/readme.md @@ -0,0 +1,12 @@ +# Platform + +This folder contains documentation about the MetaMask Snaps platform, such as +the snap controller, execution service, and execution environment. + +## Table of contents + +- [Execution Environment](./execution-environment.md) +- [Execution Service](./execution-service.md) +- [Snap Controller](./snap-controller.md) +- [Snaps Registry](./snaps-registry.md) +- [RPC Methods](./rpc-methods.md) diff --git a/docs/internals/platform/rpc-methods.md b/docs/internals/platform/rpc-methods.md new file mode 100644 index 0000000000..a28345279a --- /dev/null +++ b/docs/internals/platform/rpc-methods.md @@ -0,0 +1,24 @@ +# RPC Methods + +Apart from the execution of snaps itself, the Snaps platform is also a +collection of JSON-RPC method implementations that are available to websites and +snaps, though some are restricted to be snap-exclusive, i.e., only available to +snaps. + +The JSON-RPC methods are split into two groups: **restricted** and +**permitted**. All permitted RPC method handlers are included in an exported +middleware called [createSnapsMethodMiddleware]. This middleware should be +included in the JSON-RPC engine stack of the client implementing the Snaps +platform. All of the restricted JSON-RPC methods handlers are exported as +[restrictedMethodPermissionBuilders] and use the permission specification +builder pattern. These permission specifications should be built and included in +the [PermissionController] configuration. + +These RPC methods implement both globally available APIs that are required to +use the Snaps platform, e.g., `wallet_requestSnaps` as well as more dangerous +APIs that should only be available to specific snaps with specific permissions, +e.g., `snap_getBip44Entropy`. + +[createsnapsmethodmiddleware]: ../../../packages/rpc-methods/src/permitted/middleware.ts +[restrictedmethodpermissionbuilders]: ../../../packages/rpc-methods/src/restricted/index.ts +[permissioncontroller]: ../permissions.md diff --git a/docs/internals/platform/snap-controller.md b/docs/internals/platform/snap-controller.md new file mode 100644 index 0000000000..332b1bd610 --- /dev/null +++ b/docs/internals/platform/snap-controller.md @@ -0,0 +1,33 @@ +# Snap Controller + +The [Snap Controller] is the brain of the MetaMask Snaps platform. It is +responsible for: + +- Starting and stopping snaps. +- Managing permissions. +- Managing snap state. +- Handling incoming requests from dapps, and routing them to the correct + snap (through the [Execution Service]). +- Installing and uninstalling snaps. +- Checking if snaps are allowed to be installed (through the [Snaps Registry]). + +As such, it is the main entry point for the MetaMask Snaps platform. It is +hooked up to the controller messaging system in the MetaMask extension, so it +can handle requests from other parts of the extension. + +The snap controller uses a state machine to manage the state of the snaps. The +state machine is implemented using the [XState] library. This allows us to +easily reason about the state of the snaps. + +## Starting and stopping snaps + +The Snap Controller is responsible for starting and stopping snaps. It does +this by calling the [Execution Service] to execute the snap code in a +[Execution Environment]. When a snap is started, stopped, crashed, etc., the +Snap Controller updates the state of the snap in the state machine. + +[snap controller]: ../../../packages/snaps-controllers/src/snaps/SnapController.ts +[execution service]: ./execution-service.md +[execution environment]: ../../../packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts +[xstate]: https://xstate.js.org/ +[snaps registry]: ./snaps-registry.md diff --git a/docs/internals/platform/snaps-registry.md b/docs/internals/platform/snaps-registry.md new file mode 100644 index 0000000000..77b7e2ebe6 --- /dev/null +++ b/docs/internals/platform/snaps-registry.md @@ -0,0 +1,47 @@ +# Snaps Registry + +The Snaps Registry contains a list of snaps that are available to be installed +(allow-listed) and snaps that are blocked from being installed (block-listed). +At the moment, the registry is a JSON file that is hosted on the MetaMask +infrastructure. In the future, we may move to a smart contract based registry. + +The registry is used by the [Snap Controller] to determine which snaps are +allowed to be installed. The [Snap Controller] will check the registry when a +snap is installed, and if the snap is not in the registry, it will not be +installed. + +## JSON-based registry + +For the current implementation, the registry is a JSON file that is hosted on +the MetaMask infrastructure. The registry file can be found in a separate +repository: [Snaps Registry]. The hosted version consists of two files: + +- `registry.json`: The main registry file. This file contains a list of snaps + that are allowed to be installed, or blocked from being installed. +- `signature.json`: The signature file. This file contains the signature of + the registry file, signed when the registry is updated. This allows us to + verify that the registry file has not been tampered with. + +When the registry is updated, the signature file is re-generated by the +CI/CD pipeline. The signature is generated using the `sign-registry` script from +the [Snaps Registry] repository. + +These files are fetched by the [JsonSnapsRegistry] class, which is an +implementation of the registry interface. It checks the signature of the +registry file, by checking it against a known public key. If the signature is +not valid, the registry will not be loaded. Depending on the `requireAllowlist` +feature flag, the [Snap Controller] will not allow any snaps to be installed if +the registry is not loaded. This feature flag is enabled by default in the +stable version of the MetaMask extension. + +## Smart contract based registry + +In the future, we may move to a smart contract based registry. This would allow +us to have a decentralized registry, and allow snap developers to register their +snaps themselves. This is currently a work in progress. Progress can be tracked +in the [Permissionless Snaps Registry] repository. + +[snap controller]: ./snap-controller.md +[jsonsnapsregistry]: ../../../packages/snaps-controllers/src/snaps/registry/json.ts +[snaps registry]: https://github.com/MetaMask/snaps-registry +[permissionless snaps registry]: https://github.com/MetaMask/permissionless-snaps-registry diff --git a/docs/internals/readme.md b/docs/internals/readme.md new file mode 100644 index 0000000000..2367675eff --- /dev/null +++ b/docs/internals/readme.md @@ -0,0 +1,20 @@ +# Internals reference + +In this documentation we will describe the internals of MetaMask Snaps, for +reference while developing or debugging. This document is not intended for +developers using snaps, but for developers working on the MetaMask Snaps +platform itself. + +It is recommended to read the [architecture](./architecture.md) +document first, as it provides a high-level overview of the system. + +## Table of contents + +- [Architecture](./architecture.md) +- [Execution](./execution.md) +- [JSON-RPC](./json-rpc.md) +- [Permissions](./permissions.md) +- [Platform](./platform/readme.md) + - [Execution Environment](./platform/execution-environment.md) + - [Execution Service](./platform/execution-service.md) + - [Snap Controller](./platform/snap-controller.md) diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000000..b4a592d648 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,10 @@ +# Documentation + +This folder contains the documentation for the project. It is intended for +developers working on the MetaMask Snaps platform itself, and not for developers +building snaps. + +## Table of contents + +- [Contributing](./contributing.md) +- [Internals](./internals/readme.md) From be83c4504038c18d5cb2756f0257d9848d2ee62a Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 12:55:38 +0200 Subject: [PATCH 09/10] Add basic support for account RPC methods in snaps simulator (#1710) * Support account RPC methods in snaps simulator * Update packages/snaps-simulator/src/features/simulation/middleware.ts Co-authored-by: Maarten Zuidhoorn * Refactor getAccountsHandler * Add E2E * Add middleware test --------- Co-authored-by: Maarten Zuidhoorn --- .../ethereum-provider/src/index.test.ts | 17 ++++++ .../features/simulation/middleware.test.ts | 42 +++++++++++++- .../src/features/simulation/middleware.ts | 55 +++++++++++++++++-- .../src/features/simulation/sagas.ts | 8 ++- 4 files changed, 113 insertions(+), 9 deletions(-) diff --git a/packages/examples/packages/ethereum-provider/src/index.test.ts b/packages/examples/packages/ethereum-provider/src/index.test.ts index e729884692..a73ddb1adf 100644 --- a/packages/examples/packages/ethereum-provider/src/index.test.ts +++ b/packages/examples/packages/ethereum-provider/src/index.test.ts @@ -68,4 +68,21 @@ describe('onRpcRequest', () => { await close(); }); }); + + describe('getAccounts', () => { + it('returns the addresses granted access to by the user', async () => { + const { request, close } = await installSnap(); + + const response = await request({ + method: 'getAccounts', + }); + + // Currently, snaps-jest will always return this account. + expect(response).toRespondWith([ + '0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf', + ]); + + await close(); + }); + }); }); diff --git a/packages/snaps-simulator/src/features/simulation/middleware.test.ts b/packages/snaps-simulator/src/features/simulation/middleware.test.ts index 893caa08a0..3e3e1078ab 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.test.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.test.ts @@ -1,11 +1,15 @@ +import { mnemonicPhraseToBytes } from '@metamask/key-tree'; import { JsonRpcEngine } from 'json-rpc-engine'; +import { DEFAULT_SRP } from '../configuration'; import { createMiscMethodMiddleware } from './middleware'; +const hooks = { getMnemonic: async () => mnemonicPhraseToBytes(DEFAULT_SRP) }; + describe('createMiscMethodMiddleware', () => { it('supports metamask_getProviderState', async () => { const engine = new JsonRpcEngine(); - engine.push(createMiscMethodMiddleware()); + engine.push(createMiscMethodMiddleware(hooks)); const response = await engine.handle({ jsonrpc: '2.0', @@ -25,4 +29,40 @@ describe('createMiscMethodMiddleware', () => { }, }); }); + + it('supports eth_accounts', async () => { + const engine = new JsonRpcEngine(); + engine.push(createMiscMethodMiddleware(hooks)); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'eth_accounts', + params: [], + }); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: ['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf'], + }); + }); + + it('supports eth_requestAccounts', async () => { + const engine = new JsonRpcEngine(); + engine.push(createMiscMethodMiddleware(hooks)); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'eth_requestAccounts', + params: [], + }); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: ['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf'], + }); + }); }); diff --git a/packages/snaps-simulator/src/features/simulation/middleware.ts b/packages/snaps-simulator/src/features/simulation/middleware.ts index 219dad762c..0b6ff69efa 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.ts @@ -1,3 +1,4 @@ +import { BIP44Node } from '@metamask/key-tree'; import { logError } from '@metamask/snaps-utils'; import type { JsonRpcEngineEndCallback, @@ -7,10 +8,50 @@ import type { PendingJsonRpcResponse, } from 'json-rpc-engine'; +/* eslint-disable @typescript-eslint/naming-convention */ export const methodHandlers = { - // eslint-disable-next-line @typescript-eslint/naming-convention metamask_getProviderState: getProviderStateHandler, + eth_requestAccounts: getAccountsHandler, + eth_accounts: getAccountsHandler, }; +/* eslint-enable @typescript-eslint/naming-convention */ + +export type MiscMiddlewareHooks = { + getMnemonic: () => Promise; +}; + +/** + * A mock handler for account related methods that always returns the first address for the selected SRP. + * + * @param _request - Incoming JSON-RPC request, ignored for this specific handler. + * @param response - The outgoing JSON-RPC response, modified to return the result. + * @param _next - The json-rpc-engine middleware next handler. + * @param end - The json-rpc-engine middleware end handler. + * @param hooks - Any hooks required by this handler. + */ +async function getAccountsHandler( + _request: JsonRpcRequest, + response: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + hooks: MiscMiddlewareHooks, +) { + const { getMnemonic } = hooks; + + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + await getMnemonic(), + `bip32:44'`, + `bip32:60'`, + `bip32:0'`, + `bip32:0`, + `bip32:0`, + ], + }); + + response.result = [node.address]; + return end(); +} /** * A mock handler for metamask_getProviderState that always returns a specific hardcoded result. @@ -39,12 +80,14 @@ async function getProviderStateHandler( /** * Creates a middleware for handling misc RPC methods normally handled internally by the MM client. * + * NOTE: This middleware provides all `hooks` to all handlers and should therefore NOT be used outside of the simulator. + * + * @param hooks - Any hooks used by the middleware handlers. * @returns Nothing. */ -export function createMiscMethodMiddleware(): JsonRpcMiddleware< - unknown, - unknown -> { +export function createMiscMethodMiddleware( + hooks: MiscMiddlewareHooks, +): JsonRpcMiddleware { // This should probably use createAsyncMiddleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async function methodMiddleware(request, response, next, end) { @@ -53,7 +96,7 @@ export function createMiscMethodMiddleware(): JsonRpcMiddleware< if (handler) { try { // Implementations may or may not be async, so we must await them. - return await handler(request, response, next, end); + return await handler(request, response, next, end, hooks); } catch (error: any) { logError(error); return end(error); diff --git a/packages/snaps-simulator/src/features/simulation/sagas.ts b/packages/snaps-simulator/src/features/simulation/sagas.ts index 0acf1ca7dd..e68b9c4b24 100644 --- a/packages/snaps-simulator/src/features/simulation/sagas.ts +++ b/packages/snaps-simulator/src/features/simulation/sagas.ts @@ -80,16 +80,20 @@ export function* initSaga({ payload }: PayloadAction) { const srp: string = yield select(getSrp); + const sharedHooks = { + getMnemonic: async () => mnemonicPhraseToBytes(srp), + }; + const permissionSpecifications = { ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), ...buildSnapRestrictedMethodSpecifications([], { + ...sharedHooks, // TODO: Add all the hooks required encrypt, decrypt, // TODO: Allow changing this? getLocale: async () => Promise.resolve('en'), getUnlockPromise: async () => Promise.resolve(true), - getMnemonic: async () => mnemonicPhraseToBytes(srp), showDialog: async (...args: Parameters) => await runSaga(showDialog, ...args).toPromise(), showNativeNotification: async ( @@ -137,7 +141,7 @@ export function* initSaga({ payload }: PayloadAction) { const engine = new JsonRpcEngine(); - engine.push(createMiscMethodMiddleware()); + engine.push(createMiscMethodMiddleware(sharedHooks)); engine.push( permissionController.createPermissionMiddleware({ From 1d14bcba3a24f7b68939fee1806a15a98f082396 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Wed, 30 Aug 2023 13:46:47 +0200 Subject: [PATCH 10/10] Speed up update PR workflow by only building dependencies (#1713) * Speed up update PR workflow by only building dependencies * Revert snap manifest --- .github/workflows/update-pull-request.yml | 6 +++++- package.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-pull-request.yml b/.github/workflows/update-pull-request.yml index da6bfbc5b8..566888387f 100644 --- a/.github/workflows/update-pull-request.yml +++ b/.github/workflows/update-pull-request.yml @@ -143,8 +143,12 @@ jobs: cache: 'yarn' - name: Install dependencies from cache run: yarn --immutable --immutable-cache + - name: Build dependencies + run: | + yarn build:source + yarn build:types - name: Update examples - run: yarn build + run: yarn build:examples - name: Cache examples uses: actions/cache/save@v3 with: diff --git a/package.json b/package.json index 544a84683a..74d97f91d9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build:clean": "yarn clean && yarn build", "build:source": "yarn workspaces foreach --parallel --verbose run build:source", "build:types": "tsc --build tsconfig.build.json", + "build:examples": "yarn workspace @metamask/example-snaps build", "build:post-tsc": "yarn workspaces foreach --parallel --topological --topological-dev --verbose run build:post-tsc", "build:post-tsc:ci": "yarn workspaces foreach --parallel --topological --topological-dev --verbose --exclude root --exclude \"@metamask/snaps-simulator\" --exclude \"@metamask/snaps-execution-environments\" --exclude \"@metamask/snaps-jest\" --exclude \"@metamask/example-snaps\" --exclude \"@metamask/test-snaps\" run build:post-tsc", "clean": "yarn workspaces foreach --parallel --verbose run clean",