diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7694f4e6f06..258a7144164 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -54,7 +54,6 @@ ## Wallet API Platform Team /packages/chain-agnostic-permission @MetaMask/wallet-api-platform-engineers /packages/eip1193-permission-middleware @MetaMask/wallet-api-platform-engineers -/packages/multichain @MetaMask/wallet-api-platform-engineers /packages/multichain-api-middleware @MetaMask/wallet-api-platform-engineers /packages/queued-request-controller @MetaMask/wallet-api-platform-engineers /packages/selected-network-controller @MetaMask/wallet-api-platform-engineers @@ -129,8 +128,6 @@ /packages/phishing-controller/CHANGELOG.md @MetaMask/product-safety @MetaMask/wallet-framework-engineers /packages/profile-sync-controller/package.json @MetaMask/identity @MetaMask/wallet-framework-engineers /packages/profile-sync-controller/CHANGELOG.md @MetaMask/identity @MetaMask/wallet-framework-engineers -/packages/multichain/package.json @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers -/packages/multichain/CHANGELOG.md @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers /packages/queued-request-controller/package.json @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers /packages/queued-request-controller/CHANGELOG.md @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers /packages/selected-network-controller/package.json @MetaMask/wallet-api-platform-engineers @MetaMask/wallet-framework-engineers diff --git a/README.md b/README.md index e6289249a33..d66c5ddcef7 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ Each package in this repository has its own README where you can find installati - [`@metamask/keyring-controller`](packages/keyring-controller) - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) -- [`@metamask/multichain`](packages/multichain) - [`@metamask/multichain-api-middleware`](packages/multichain-api-middleware) - [`@metamask/multichain-network-controller`](packages/multichain-network-controller) - [`@metamask/multichain-transactions-controller`](packages/multichain-transactions-controller) @@ -79,7 +78,7 @@ Each package in this repository has its own README where you can find installati %%{ init: { 'flowchart': { 'curve': 'bumpX' } } }%% graph LR; linkStyle default opacity:0.5 - account_wallet_controller(["@metamask/account-tree-controller"]); + account_tree_controller(["@metamask/account-tree-controller"]); accounts_controller(["@metamask/accounts-controller"]); address_book_controller(["@metamask/address-book-controller"]); announcement_controller(["@metamask/announcement-controller"]); @@ -106,7 +105,6 @@ linkStyle default opacity:0.5 keyring_controller(["@metamask/keyring-controller"]); logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); - multichain(["@metamask/multichain"]); multichain_api_middleware(["@metamask/multichain-api-middleware"]); multichain_network_controller(["@metamask/multichain-network-controller"]); multichain_transactions_controller(["@metamask/multichain-transactions-controller"]); @@ -129,9 +127,9 @@ linkStyle default opacity:0.5 token_search_discovery_controller(["@metamask/token-search-discovery-controller"]); transaction_controller(["@metamask/transaction-controller"]); user_operation_controller(["@metamask/user-operation-controller"]); - account_wallet_controller --> base_controller; - account_wallet_controller --> accounts_controller; - account_wallet_controller --> keyring_controller; + account_tree_controller --> base_controller; + account_tree_controller --> accounts_controller; + account_tree_controller --> keyring_controller; accounts_controller --> base_controller; accounts_controller --> keyring_controller; accounts_controller --> network_controller; @@ -148,6 +146,7 @@ linkStyle default opacity:0.5 assets_controllers --> keyring_controller; assets_controllers --> network_controller; assets_controllers --> permission_controller; + assets_controllers --> phishing_controller; assets_controllers --> preferences_controller; assets_controllers --> transaction_controller; base_controller --> json_rpc_engine; @@ -192,6 +191,7 @@ linkStyle default opacity:0.5 ens_controller --> base_controller; ens_controller --> controller_utils; ens_controller --> network_controller; + error_reporting_service --> base_controller; eth_json_rpc_provider --> json_rpc_engine; gas_fee_controller --> base_controller; gas_fee_controller --> controller_utils; @@ -203,10 +203,6 @@ linkStyle default opacity:0.5 logging_controller --> controller_utils; message_manager --> base_controller; message_manager --> controller_utils; - multichain --> controller_utils; - multichain --> json_rpc_engine; - multichain --> network_controller; - multichain --> permission_controller; multichain_api_middleware --> chain_agnostic_permission; multichain_api_middleware --> controller_utils; multichain_api_middleware --> json_rpc_engine; @@ -226,6 +222,7 @@ linkStyle default opacity:0.5 name_controller --> controller_utils; network_controller --> base_controller; network_controller --> controller_utils; + network_controller --> error_reporting_service; network_controller --> eth_json_rpc_provider; network_controller --> json_rpc_engine; notification_services_controller --> base_controller; @@ -261,6 +258,8 @@ linkStyle default opacity:0.5 sample_controllers --> base_controller; sample_controllers --> controller_utils; sample_controllers --> network_controller; + seedless_onboarding_controller --> base_controller; + seedless_onboarding_controller --> keyring_controller; selected_network_controller --> base_controller; selected_network_controller --> json_rpc_engine; selected_network_controller --> network_controller; diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 847cca2288b..cca3ed4748e 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -256,80 +256,6 @@ "packages/message-manager/src/utils.ts": { "@typescript-eslint/no-unused-vars": 1 }, - "packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts": { - "import-x/order": 1 - }, - "packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 1, - "jsdoc/tag-lines": 5 - }, - "packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts": { - "import-x/order": 1 - }, - "packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts": { - "jsdoc/tag-lines": 5 - }, - "packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts": { - "@typescript-eslint/no-unused-vars": 2 - }, - "packages/multichain/src/caip25Permission.test.ts": { - "@typescript-eslint/no-unused-vars": 5 - }, - "packages/multichain/src/caip25Permission.ts": { - "@typescript-eslint/no-unused-vars": 1 - }, - "packages/multichain/src/handlers/wallet-getSession.ts": { - "@typescript-eslint/no-unused-vars": 1, - "jsdoc/require-returns": 1 - }, - "packages/multichain/src/handlers/wallet-invokeMethod.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 1, - "@typescript-eslint/no-unused-vars": 1, - "jsdoc/require-returns": 1 - }, - "packages/multichain/src/handlers/wallet-revokeSession.test.ts": { - "import-x/order": 1, - "prettier/prettier": 2 - }, - "packages/multichain/src/handlers/wallet-revokeSession.ts": { - "@typescript-eslint/no-unused-vars": 1, - "jsdoc/require-returns": 1 - }, - "packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts": { - "prettier/prettier": 1 - }, - "packages/multichain/src/middlewares/MultichainSubscriptionManager.ts": { - "@typescript-eslint/prefer-readonly": 2, - "import-x/order": 1 - }, - "packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts": { - "@typescript-eslint/prefer-promise-reject-errors": 20 - }, - "packages/multichain/src/scope/assert.test.ts": { - "@typescript-eslint/no-unused-vars": 3 - }, - "packages/multichain/src/scope/assert.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 1 - }, - "packages/multichain/src/scope/authorization.test.ts": { - "@typescript-eslint/no-unused-vars": 2 - }, - "packages/multichain/src/scope/errors.ts": { - "jsdoc/tag-lines": 5 - }, - "packages/multichain/src/scope/filter.test.ts": { - "jest/no-conditional-in-test": 9 - }, - "packages/multichain/src/scope/filter.ts": { - "@typescript-eslint/no-unused-vars": 1, - "jsdoc/require-returns": 1 - }, - "packages/multichain/src/scope/supported.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 6 - }, - "packages/multichain/src/scope/validation.ts": { - "jsdoc/tag-lines": 2 - }, "packages/name-controller/src/NameController.ts": { "@typescript-eslint/no-unsafe-enum-comparison": 1, "@typescript-eslint/prefer-readonly": 2 diff --git a/packages/multichain/CHANGELOG.md b/packages/multichain/CHANGELOG.md deleted file mode 100644 index 112e6bf5445..00000000000 --- a/packages/multichain/CHANGELOG.md +++ /dev/null @@ -1,206 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Changed - -- Bump `@metamask/controller-utils` to `^11.10.0` ([#5935](https://github.com/MetaMask/core/pull/5935)) - -## [4.1.0] - -### Changed - -- Bump `@metamask/api-specs` to `^0.14.0` ([#5817](https://github.com/MetaMask/core/pull/5817)) -- Bump `@metamask/controller-utils` to `^11.9.0` ([#5583](https://github.com/MetaMask/core/pull/5583), [#5765](https://github.com/MetaMask/core/pull/5765), [#5812](https://github.com/MetaMask/core/pull/5812)) - -## [4.0.0] - -### Added - -- **BREAKING**: `getSessionScopes()` now expects an additional hooks object as its last param. The hooks object should have a `getNonEvmSupportedMethods` property whose value should be a function that accepts a `CaipChainId` and returns an array of supported methods. ([#5191](https://github.com/MetaMask/core/pull/5191)) -- **BREAKING**: `caip25CaveatBuilder()` now expects two additional properties it's singular param object. The param object should now also have a `isNonEvmScopeSupported` property whose value should be a function that accepts a `CaipChainId` and returns a boolean, and a `getNonEvmAccountAddresses` property whose value should be a function that accepts a `CaipChainId` and returns an array of CAIP-10 account addresses. ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The CAIP-25 caveat specification now also validates if non-evm scopes and accounts are supported -- **BREAKING**: The `wallet_getSession` handler now expects `getNonEvmSupportedMethods` to be provided in it's hooks. ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The handler now resolves methods for non-evm scopes in the returned `sessionScopes` result -- **BREAKING**: The `wallet_invokeMethod` handler now expects `getNonEvmSupportedMethods` and `handleNonEvmRequestForOrigin` to be provided in it's hooks. ([#5191](https://github.com/MetaMask/core/pull/5191)) - - - `handleNonEvmRequestForOrigin` should be a function with the following signature: - ``` - handleNonEvmRequestForOrigin: (params: { - connectedAddresses: CaipAccountId[]; - scope: CaipChainId; - request: JsonRpcRequest; - }) => Promise; - ``` - -- **BREAKING**: `assertScopeSupported()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - isChainIdSupported: (chainId: Hex) => boolean; - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - } - ``` -- **BREAKING**: `assertScopesSupported()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - isChainIdSupported: (chainId: Hex) => boolean; - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - } - ``` -- **BREAKING**: `bucketScopes()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isEvmChainIdSupportable: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - } - ``` -- **BREAKING**: `bucketScopesBySupport()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - } - ``` -- **BREAKING**: `getSessionScopes()` now expects a hooks object as its last param. The hooks object should have a `getNonEvmSupportedMethods` property whose value should be a function that accepts a `CaipChainId` and returns an array of supported methods. ([#5191](https://github.com/MetaMask/core/pull/5191)) -- **BREAKING**: `isSupportedScopeString()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - } - ``` -- **BREAKING**: `isSupportedAccount()` now expects a new hooks object as its last param ([#5191](https://github.com/MetaMask/core/pull/5191)) - - The new hooks object is: - ``` - { - getEvmInternalAccounts: () => { type: string; address: Hex }[]; - getNonEvmAccountAddresses: (scope: CaipChainId) => string[]; - } - ``` -- **BREAKING**: `isSupportedMethod()` now expects a new hooks object as its last param: - - The new hooks object is: - ``` - { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - } - ``` -- Added `wallet_invokeMethod` handler now supports non-EVM requests ([#5191](https://github.com/MetaMask/core/pull/5191)) -- Added `wallet_getPermissions` handler (originally migrated from extension repo) ([#5420](https://github.com/MetaMask/core/pull/5420)) -- Added `wallet_requestPermissions` handler (originally migrated from extension repo) ([#5420](https://github.com/MetaMask/core/pull/5420)) -- Added `wallet_revokePermissions` handler (originally migrated from extension repo) ([#5420](https://github.com/MetaMask/core/pull/5420)) - -## [3.0.0] - -### Added - -- **BREAKING** Renamed `mergeScopes` to `mergeNormalizedScopes` ([#5283](https://github.com/MetaMask/core/pull/5283)) -- Added merger to CaveatSpecification returned by `caip25CaveatBuilder()` ([#5283](https://github.com/MetaMask/core/pull/5283)) -- Added `mergeInternalScopes` which merges two `InternalScopesObject`s ([#5283](https://github.com/MetaMask/core/pull/5283)) - -## [2.2.0] - -### Changed - -- Bump `@metamask/utils` from ^11.1.0 to ^11.2.0 ([#5301](https://github.com/MetaMask/core/pull/5301)) - -### Fixed - -- Fixes scope creation to not insert accounts into `wallet` scope ([#5374](https://github.com/MetaMask/core/pull/5374)) -- Fixes invalid type import path in `@metamask/multichain` ([#5313](https://github.com/MetaMask/core/pull/5313)) - -## [2.1.1] - -### Changed - -- Bump `@metamask/controller-utils` from `^11.4.5` to `^11.5.0` ([#5272](https://github.com/MetaMask/core/pull/5272)) -- Bump `@metamask/utils` from `^11.0.1` to `^11.1.0` ([#5223](https://github.com/MetaMask/core/pull/5223)) - -## [2.1.0] - -### Added - -- Add key Multichain API methods ([#4813](https://github.com/MetaMask/core/pull/4813)) - - Adds `getInternalScopesObject` and `getSessionScopes` helpers for transforming between `NormalizedScopesObject` and `InternalScopesObject`. - - Adds handlers for `wallet_getSession`, `wallet_invokeMethod`, and `wallet_revokeSession` methods. - - Adds `multichainMethodCallValidatorMiddleware` for validating Multichain API method params as defined in `@metamask/api-specs`. - - Adds `MultichainMiddlewareManager` to multiplex a request to other middleware based on requested scope. - - Adds `MultichainSubscriptionManager` to handle concurrent subscriptions across multiple scopes. - - Adds `bucketScopes` which groups the scopes in a `NormalizedScopesObject` based on if the scopes are already supported, could be supported, or are not supportable. - - Adds `getSupportedScopeObjects` helper for getting only the supported methods and notifications from each `NormalizedScopeObject` in a `NormalizedScopesObject`. - -### Changed - -- Bump `@metamask/controller-utils` from `^11.4.4` to `^11.4.5` ([#5012](https://github.com/MetaMask/core/pull/5135)) -- Bump `@metamask/permission-controller` from `^11.0.4` to `^11.0.5` ([#5012](https://github.com/MetaMask/core/pull/5135)) -- Bump `@metamask/utils` to `^11.0.1` and `@metamask/rpc-errors` to `^7.0.2` ([#5080](https://github.com/MetaMask/core/pull/5080)) - -### Fixed - -- Fixes `removeScope` mutator incorrectly returning malformed CAIP-25 caveat values ([#5183](https://github.com/MetaMask/core/pull/5183)). - -## [2.0.0] - -### Added - -- Adds `caip25CaveatBuilder` helper that builds a specification for the CAIP-25 caveat that can be passed to the relevant `PermissionController` constructor param([#5064](https://github.com/MetaMask/core/pull/5064)). - -### Changed - -- **BREAKING:** The validator returned by `caip25EndowmentBuilder` now only verifies that there is single CAIP-25 caveat and nothing else([#5064](https://github.com/MetaMask/core/pull/5064)). - -## [1.1.2] - -### Changed - -- Bump `@metamask/eth-json-rpc-filters` from `^7.0.0` to `^9.0.0` ([#5040](https://github.com/MetaMask/core/pull/5040)) - -## [1.1.1] - -### Changed - -- Bump `@metamask/controller-utils` from `^11.4.3` to `^11.4.4` ([#5012](https://github.com/MetaMask/core/pull/5012)) -- Correct ESM-compatible build so that imports of the following packages that re-export other modules via `export *` are no longer corrupted: ([#5011](https://github.com/MetaMask/core/pull/5011)) - - `@metamask/api-specs` - - `lodash` - -## [1.1.0] - -### Changed - -- Revoke the CAIP-25 endowment if the only eip155 account or scope is removed ([#4978](https://github.com/MetaMask/core/pull/4978)) - -## [1.0.0] - -### Added - -- Initial release ([#4962](https://github.com/MetaMask/core/pull/4962)) - -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/multichain@4.1.0...HEAD -[4.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@4.0.0...@metamask/multichain@4.1.0 -[4.0.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@3.0.0...@metamask/multichain@4.0.0 -[3.0.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@2.2.0...@metamask/multichain@3.0.0 -[2.2.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@2.1.1...@metamask/multichain@2.2.0 -[2.1.1]: https://github.com/MetaMask/core/compare/@metamask/multichain@2.1.0...@metamask/multichain@2.1.1 -[2.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@2.0.0...@metamask/multichain@2.1.0 -[2.0.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.2...@metamask/multichain@2.0.0 -[1.1.2]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.1...@metamask/multichain@1.1.2 -[1.1.1]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.1.0...@metamask/multichain@1.1.1 -[1.1.0]: https://github.com/MetaMask/core/compare/@metamask/multichain@1.0.0...@metamask/multichain@1.1.0 -[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/multichain@1.0.0 diff --git a/packages/multichain/LICENSE b/packages/multichain/LICENSE deleted file mode 100644 index 6f8bff03fc4..00000000000 --- a/packages/multichain/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -MIT License - -Copyright (c) 2024 MetaMask - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/packages/multichain/README.md b/packages/multichain/README.md deleted file mode 100644 index dc89e0fade9..00000000000 --- a/packages/multichain/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# `@metamask/multichain` - -Provides types, helpers, adapters, and wrappers for facilitating CAIP Multichain sessions - -## Installation - -`yarn add @metamask/multichain` - -or - -`npm install @metamask/multichain` - -## Contributing - -This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js deleted file mode 100644 index ca084133399..00000000000 --- a/packages/multichain/jest.config.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/configuration - */ - -const merge = require('deepmerge'); -const path = require('path'); - -const baseConfig = require('../../jest.config.packages'); - -const displayName = path.basename(__dirname); - -module.exports = merge(baseConfig, { - // The display name when running multiple projects - displayName, - - // An object that configures minimum threshold enforcement for coverage results - coverageThreshold: { - global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, - }, - }, -}); diff --git a/packages/multichain/package.json b/packages/multichain/package.json deleted file mode 100644 index 2e7ad5bc403..00000000000 --- a/packages/multichain/package.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "name": "@metamask/multichain", - "version": "4.1.0", - "description": "Provides types, helpers, adapters, and wrappers for facilitating CAIP Multichain sessions", - "keywords": [ - "MetaMask", - "Ethereum" - ], - "homepage": "https://github.com/MetaMask/core/tree/main/packages/multichain#readme", - "bugs": { - "url": "https://github.com/MetaMask/core/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/MetaMask/core.git" - }, - "license": "MIT", - "sideEffects": false, - "exports": { - ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - }, - "./package.json": "./package.json" - }, - "main": "./dist/index.cjs", - "types": "./dist/index.d.cts", - "files": [ - "dist/" - ], - "scripts": { - "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", - "build:docs": "typedoc", - "changelog:update": "../../scripts/update-changelog.sh @metamask/multichain", - "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain", - "publish:preview": "yarn npm publish --tag preview", - "since-latest-release": "../../scripts/since-latest-release.sh", - "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", - "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", - "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", - "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" - }, - "dependencies": { - "@metamask/api-specs": "^0.14.0", - "@metamask/controller-utils": "^11.10.0", - "@metamask/eth-json-rpc-filters": "^9.0.0", - "@metamask/rpc-errors": "^7.0.2", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^11.2.0", - "@open-rpc/schema-utils-js": "^2.0.5", - "jsonschema": "^1.4.1", - "lodash": "^4.17.21" - }, - "devDependencies": { - "@metamask/auto-changelog": "^3.4.4", - "@metamask/json-rpc-engine": "^10.0.3", - "@metamask/network-controller": "^23.6.0", - "@metamask/permission-controller": "^11.0.6", - "@open-rpc/meta-schema": "^1.14.6", - "@types/jest": "^27.4.1", - "deepmerge": "^4.2.2", - "jest": "^27.5.1", - "ts-jest": "^27.1.4", - "typedoc": "^0.24.8", - "typedoc-plugin-missing-exports": "^2.0.0", - "typescript": "~5.2.2" - }, - "peerDependencies": { - "@metamask/network-controller": "^23.0.0", - "@metamask/permission-controller": "^11.0.0" - }, - "engines": { - "node": "^18.18 || >=20" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - } -} diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts deleted file mode 100644 index 4b213963a9d..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - getEthAccounts, - setEthAccounts, -} from './caip-permission-adapter-eth-accounts'; - -describe('CAIP-25 eth_accounts adapters', () => { - describe('getEthAccounts', () => { - it('returns an empty array if the required scopes are empty', () => { - const ethAccounts = getEthAccounts({ - requiredScopes: {}, - optionalScopes: {}, - }); - expect(ethAccounts).toStrictEqual([]); - }); - it('returns an empty array if the scope objects have no accounts', () => { - const ethAccounts = getEthAccounts({ - requiredScopes: { - 'eip155:1': { accounts: [] }, - 'eip155:2': { accounts: [] }, - }, - optionalScopes: {}, - }); - expect(ethAccounts).toStrictEqual([]); - }); - it('returns an empty array if the scope objects have no eth accounts', () => { - const ethAccounts = getEthAccounts({ - requiredScopes: { - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [ - 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - ], - }, - }, - optionalScopes: {}, - }); - expect(ethAccounts).toStrictEqual([]); - }); - - it('returns the unique set of EIP155 accounts from the CAIP-25 caveat value', () => { - const ethAccounts = getEthAccounts({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'eip155:5': { - accounts: ['eip155:5:0x2', 'eip155:1:0x3'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [ - 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - ], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x4'], - }, - 'eip155:10': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - 'wallet:eip155': { - accounts: ['wallet:eip155:0x5'], - }, - }, - }); - - expect(ethAccounts).toStrictEqual([ - '0x1', - '0x2', - '0x3', - '0x4', - '0x100', - '0x5', - ]); - }); - }); - - describe('setEthAccounts', () => { - it('returns a CAIP-25 caveat value with all EIP-155 scopeObject.accounts set to CAIP-10 account addresses formed from the accounts param', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'eip155:5': { - accounts: ['eip155:5:0x2', 'eip155:1:0x3'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [ - 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - ], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x4'], - }, - 'eip155:10': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - 'wallet:eip155': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - isMultichainOrigin: false, - }; - - const result = setEthAccounts(input, ['0x1', '0x2', '0x3']); - expect(result).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2', 'eip155:1:0x3'], - }, - 'eip155:5': { - accounts: ['eip155:5:0x1', 'eip155:5:0x2', 'eip155:5:0x3'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [ - 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - ], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2', 'eip155:1:0x3'], - }, - 'eip155:10': { - accounts: ['eip155:10:0x1', 'eip155:10:0x2', 'eip155:10:0x3'], - }, - 'eip155:100': { - accounts: ['eip155:100:0x1', 'eip155:100:0x2', 'eip155:100:0x3'], - }, - 'wallet:eip155': { - accounts: [ - 'wallet:eip155:0x1', - 'wallet:eip155:0x2', - 'wallet:eip155:0x3', - ], - }, - wallet: { - accounts: [], - }, - }, - isMultichainOrigin: false, - }); - }); - - it('does not modify the input CAIP-25 caveat value object in place', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const result = setEthAccounts(input, ['0x1', '0x2', '0x3']); - expect(input).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }); - expect(input).not.toStrictEqual(result); - }); - }); -}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts deleted file mode 100644 index fdd84bf2eb7..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - assertIsStrictHexString, - type CaipAccountId, - type Hex, - KnownCaipNamespace, - parseCaipAccountId, -} from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { KnownWalletScopeString } from '../scope/constants'; -import { getUniqueArrayItems } from '../scope/transform'; -import type { InternalScopeString, InternalScopesObject } from '../scope/types'; -import { parseScopeString } from '../scope/types'; - -/** - * Checks if a scope string is either an EIP155 or wallet namespaced scope string. - * @param scopeString - The scope string to check. - * @returns True if the scope string is an EIP155 or wallet namespaced scope string, false otherwise. - */ -const isEip155ScopeString = (scopeString: InternalScopeString) => { - const { namespace } = parseScopeString(scopeString); - - return ( - namespace === KnownCaipNamespace.Eip155 || - scopeString === KnownWalletScopeString.Eip155 - ); -}; - -/** - * Gets the Ethereum (EIP155 namespaced) accounts from internal scopes. - * @param scopes - The internal scopes from which to get the Ethereum accounts. - * @returns An array of Ethereum accounts. - */ -const getEthAccountsFromScopes = (scopes: InternalScopesObject) => { - const ethAccounts: Hex[] = []; - - Object.entries(scopes).forEach(([_, { accounts }]) => { - accounts?.forEach((account) => { - const { address, chainId } = parseCaipAccountId(account); - - if (isEip155ScopeString(chainId)) { - // This address should always be a valid Hex string because - // it's an EIP155/Ethereum account - assertIsStrictHexString(address); - ethAccounts.push(address); - } - }); - }); - - return ethAccounts; -}; - -/** - * Gets the Ethereum (EIP155 namespaced) accounts from the required and optional scopes. - * @param caip25CaveatValue - The CAIP-25 caveat value to get the Ethereum accounts from. - * @returns An array of Ethereum accounts. - */ -export const getEthAccounts = ( - caip25CaveatValue: Pick< - Caip25CaveatValue, - 'requiredScopes' | 'optionalScopes' - >, -): Hex[] => { - const { requiredScopes, optionalScopes } = caip25CaveatValue; - - const ethAccounts: Hex[] = [ - ...getEthAccountsFromScopes(requiredScopes), - ...getEthAccountsFromScopes(optionalScopes), - ]; - - return getUniqueArrayItems(ethAccounts); -}; - -/** - * Sets the Ethereum (EIP155 namespaced) accounts for the given scopes object. - * @param scopesObject - The scopes object to set the Ethereum accounts for. - * @param accounts - The Ethereum accounts to set. - * @returns The updated scopes object with the Ethereum accounts set. - */ -const setEthAccountsForScopesObject = ( - scopesObject: InternalScopesObject, - accounts: Hex[], -) => { - const updatedScopesObject: InternalScopesObject = {}; - Object.entries(scopesObject).forEach(([key, scopeObject]) => { - // Cast needed because index type is returned as `string` by `Object.entries` - const scopeString = key as keyof typeof scopesObject; - const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet; - const { namespace, reference } = parseScopeString(scopeString); - if (!isEip155ScopeString(scopeString) && !isWalletNamespace) { - updatedScopesObject[scopeString] = scopeObject; - return; - } - - let caipAccounts: CaipAccountId[] = []; - if (namespace && reference) { - caipAccounts = accounts.map( - (account) => `${namespace}:${reference}:${account}`, - ); - } - - updatedScopesObject[scopeString] = { - ...scopeObject, - accounts: caipAccounts, - }; - }); - - return updatedScopesObject; -}; - -/** - * Sets the Ethereum (EIP155 namespaced) accounts for the given CAIP-25 caveat value. - * We set the same accounts for all the scopes that are EIP155 or Wallet namespaced because - * we do not provide UI/UX flows for selecting different accounts across different chains. - * @param caip25CaveatValue - The CAIP-25 caveat value to set the Ethereum accounts for. - * @param accounts - The Ethereum accounts to set. - * @returns The updated CAIP-25 caveat value with the Ethereum accounts set. - */ -export const setEthAccounts = ( - caip25CaveatValue: Caip25CaveatValue, - accounts: Hex[], -): Caip25CaveatValue => { - return { - ...caip25CaveatValue, - requiredScopes: setEthAccountsForScopesObject( - caip25CaveatValue.requiredScopes, - accounts, - ), - optionalScopes: setEthAccountsForScopesObject( - caip25CaveatValue.optionalScopes, - accounts, - ), - }; -}; diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts deleted file mode 100644 index bc9b0ccd7ca..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - addPermittedEthChainId, - getPermittedEthChainIds, - setPermittedEthChainIds, -} from './caip-permission-adapter-permittedChains'; - -describe('CAIP-25 permittedChains adapters', () => { - describe('getPermittedEthChainIds', () => { - it('returns the unique set of EIP155 chainIds in hexadecimal format from the CAIP-25 caveat value', () => { - const ethChainIds = getPermittedEthChainIds({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'eip155:5': { - accounts: ['eip155:5:0x2', 'eip155:1:0x3'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [ - 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', - ], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x4'], - }, - 'eip155:10': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - }, - }); - - expect(ethChainIds).toStrictEqual(['0x1', '0x5', '0xa', '0x64']); - }); - }); - - describe('addPermittedEthChainId', () => { - it('returns a version of the caveat value with a new optional scope for the chainId if it does not already exist in required or optional scopes', () => { - const result = addPermittedEthChainId( - { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - 'wallet:eip155': { - accounts: [], - }, - }, - isMultichainOrigin: false, - }, - '0x65', - ); - - expect(result).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - 'eip155:101': { - accounts: [], - }, - 'wallet:eip155': { - accounts: [], - }, - }, - isMultichainOrigin: false, - }); - }); - - it('does not modify the input CAIP-25 caveat value object', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const result = addPermittedEthChainId(input, '0x65'); - - expect(input).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }); - expect(input).not.toStrictEqual(result); - }); - - it('does not add an optional scope for the chainId if already exists in the required scopes', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - }, - isMultichainOrigin: false, - }; - const result = addPermittedEthChainId(input, '0x1'); - - expect(result).toStrictEqual(input); - }); - - it('does not add an optional scope for the chainId if already exists in the optional scopes', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - }, - isMultichainOrigin: false, - }; - const result = addPermittedEthChainId(input, '0x64'); // 0x64 === 100 - - expect(result).toStrictEqual(input); - }); - }); - - describe('setPermittedEthChainIds', () => { - it('returns a CAIP-25 caveat value with EIP-155 scopes missing from the chainIds array removed', () => { - const result = setPermittedEthChainIds( - { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [], - }, - }, - optionalScopes: { - wallet: { - accounts: [], - }, - 'eip155:1': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - }, - isMultichainOrigin: false, - }, - ['0x1'], - ); - - expect(result).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [], - }, - }, - optionalScopes: { - wallet: { - accounts: [], - }, - 'eip155:1': { - accounts: [], - }, - }, - isMultichainOrigin: false, - }); - }); - - it('returns a CAIP-25 caveat value with optional scopes added for missing chainIds', () => { - const result = setPermittedEthChainIds( - { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - }, - isMultichainOrigin: false, - }, - ['0x1', '0x64', '0x65'], - ); - - expect(result).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:100': { - accounts: ['eip155:100:0x100'], - }, - 'eip155:101': { - accounts: [], - }, - }, - isMultichainOrigin: false, - }); - }); - - it('does not modify the input CAIP-25 caveat value object', () => { - const input: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const result = setPermittedEthChainIds(input, ['0x1', '0x2', '0x3']); - - expect(input).toStrictEqual({ - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }); - expect(input).not.toStrictEqual(result); - }); - }); -}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts deleted file mode 100644 index a2dfffa7369..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { toHex } from '@metamask/controller-utils'; -import type { Hex } from '@metamask/utils'; -import { hexToBigInt, KnownCaipNamespace } from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { getUniqueArrayItems } from '../scope/transform'; -import type { InternalScopesObject } from '../scope/types'; -import { parseScopeString } from '../scope/types'; - -/** - * Gets the Ethereum (EIP155 namespaced) chainIDs from internal scopes. - * @param scopes - The internal scopes from which to get the Ethereum chainIDs. - * @returns An array of Ethereum chainIDs. - */ -const getPermittedEthChainIdsFromScopes = (scopes: InternalScopesObject) => { - const ethChainIds: Hex[] = []; - - Object.keys(scopes).forEach((scopeString) => { - const { namespace, reference } = parseScopeString(scopeString); - if (namespace === KnownCaipNamespace.Eip155 && reference) { - ethChainIds.push(toHex(reference)); - } - }); - - return ethChainIds; -}; - -/** - * Gets the Ethereum (EIP155 namespaced) chainIDs from the required and optional scopes. - * @param caip25CaveatValue - The CAIP-25 caveat value from which to get the Ethereum chainIDs. - * @returns An array of Ethereum chainIDs. - */ -export const getPermittedEthChainIds = ( - caip25CaveatValue: Pick< - Caip25CaveatValue, - 'requiredScopes' | 'optionalScopes' - >, -) => { - const { requiredScopes, optionalScopes } = caip25CaveatValue; - - const ethChainIds: Hex[] = [ - ...getPermittedEthChainIdsFromScopes(requiredScopes), - ...getPermittedEthChainIdsFromScopes(optionalScopes), - ]; - - return getUniqueArrayItems(ethChainIds); -}; - -/** - * Adds an Ethereum (EIP155 namespaced) chainID to the optional scopes if it is not already present - * in either the pre-existing required or optional scopes. - * @param caip25CaveatValue - The CAIP-25 caveat value to add the Ethereum chainID to. - * @param chainId - The Ethereum chainID to add. - * @returns The updated CAIP-25 caveat value with the added Ethereum chainID. - */ -export const addPermittedEthChainId = ( - caip25CaveatValue: Caip25CaveatValue, - chainId: Hex, -): Caip25CaveatValue => { - const scopeString = `eip155:${hexToBigInt(chainId).toString(10)}`; - if ( - Object.keys(caip25CaveatValue.requiredScopes).includes(scopeString) || - Object.keys(caip25CaveatValue.optionalScopes).includes(scopeString) - ) { - return caip25CaveatValue; - } - - return { - ...caip25CaveatValue, - optionalScopes: { - ...caip25CaveatValue.optionalScopes, - [scopeString]: { - accounts: [], - }, - }, - }; -}; - -/** - * Filters the scopes object to only include: - * - Scopes without references (e.g. "wallet:") - * - EIP155 scopes for the given chainIDs - * - Non EIP155 scopes (e.g. "bip122:" or any other non ethereum namespaces) - * @param scopesObject - The scopes object to filter. - * @param chainIds - The chainIDs to filter EIP155 scopes by. - * @returns The filtered scopes object. - */ -const filterEthScopesObjectByChainId = ( - scopesObject: InternalScopesObject, - chainIds: Hex[], -): InternalScopesObject => { - const updatedScopesObject: InternalScopesObject = {}; - - Object.entries(scopesObject).forEach(([key, scopeObject]) => { - // Cast needed because index type is returned as `string` by `Object.entries` - const scopeString = key as keyof typeof scopesObject; - const { namespace, reference } = parseScopeString(scopeString); - if (!reference) { - updatedScopesObject[scopeString] = scopeObject; - return; - } - if (namespace === KnownCaipNamespace.Eip155) { - const chainId = toHex(reference); - if (chainIds.includes(chainId)) { - updatedScopesObject[scopeString] = scopeObject; - } - } else { - updatedScopesObject[scopeString] = scopeObject; - } - }); - - return updatedScopesObject; -}; - -/** - * Sets the permitted Ethereum (EIP155 namespaced) chainIDs for the required and optional scopes. - * @param caip25CaveatValue - The CAIP-25 caveat value to set the permitted Ethereum chainIDs for. - * @param chainIds - The Ethereum chainIDs to set as permitted. - * @returns The updated CAIP-25 caveat value with the permitted Ethereum chainIDs. - */ -export const setPermittedEthChainIds = ( - caip25CaveatValue: Caip25CaveatValue, - chainIds: Hex[], -): Caip25CaveatValue => { - let updatedCaveatValue: Caip25CaveatValue = { - ...caip25CaveatValue, - requiredScopes: filterEthScopesObjectByChainId( - caip25CaveatValue.requiredScopes, - chainIds, - ), - optionalScopes: filterEthScopesObjectByChainId( - caip25CaveatValue.optionalScopes, - chainIds, - ), - }; - - chainIds.forEach((chainId) => { - updatedCaveatValue = addPermittedEthChainId(updatedCaveatValue, chainId); - }); - - return updatedCaveatValue; -}; diff --git a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts deleted file mode 100644 index 79fa1bf740a..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { - getInternalScopesObject, - getSessionScopes, -} from './caip-permission-adapter-session-scopes'; -import { - KnownNotifications, - KnownRpcMethods, - KnownWalletNamespaceRpcMethods, - KnownWalletRpcMethods, -} from '../scope/constants'; - -describe('CAIP-25 session scopes adapters', () => { - describe('getInternalScopesObject', () => { - it('returns an InternalScopesObject with only the accounts from each NormalizedScopeObject', () => { - const result = getInternalScopesObject({ - 'wallet:eip155': { - methods: ['foo', 'bar'], - notifications: ['baz'], - accounts: ['wallet:eip155:0xdead'], - }, - 'eip155:1': { - methods: ['eth_call'], - notifications: ['eth_subscription'], - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - }); - - expect(result).toStrictEqual({ - 'wallet:eip155': { - accounts: ['wallet:eip155:0xdead'], - }, - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - }); - }); - }); - - describe('getSessionScopes', () => { - const getNonEvmSupportedMethods = jest.fn(); - - it('returns a NormalizedScopesObject for the wallet scope', () => { - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - wallet: { - accounts: [], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - wallet: { - methods: KnownWalletRpcMethods, - notifications: [], - accounts: [], - }, - }); - }); - - it('returns a NormalizedScopesObject for the wallet:eip155 scope', () => { - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'wallet:eip155': { - accounts: ['wallet:eip155:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'wallet:eip155': { - methods: KnownWalletNamespaceRpcMethods.eip155, - notifications: [], - accounts: ['wallet:eip155:0xdeadbeef'], - }, - }); - }); - - it('gets methods from getNonEvmSupportedMethods for scope with wallet namespace and non-evm reference', () => { - getNonEvmSupportedMethods.mockReturnValue(['nonEvmMethod']); - - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'wallet:foobar': { - accounts: ['wallet:foobar:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(getNonEvmSupportedMethods).toHaveBeenCalledWith('wallet:foobar'); - }); - - it('returns a NormalizedScopesObject with methods from getNonEvmSupportedMethods and empty notifications for scope with wallet namespace and non-evm reference', () => { - getNonEvmSupportedMethods.mockReturnValue(['nonEvmMethod']); - - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'wallet:foobar': { - accounts: ['wallet:foobar:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'wallet:foobar': { - methods: ['nonEvmMethod'], - notifications: [], - accounts: ['wallet:foobar:0xdeadbeef'], - }, - }); - }); - - it('gets methods from getNonEvmSupportedMethods for non-evm (not `eip155`, `wallet` or `wallet:eip155`) scopes', () => { - getNonEvmSupportedMethods.mockReturnValue(['nonEvmMethod']); - - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'foo:1': { - accounts: ['foo:1:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(getNonEvmSupportedMethods).toHaveBeenCalledWith('foo:1'); - }); - - it('returns a NormalizedScopesObject with methods from getNonEvmSupportedMethods and empty notifications for scope non-evm namespace', () => { - getNonEvmSupportedMethods.mockReturnValue(['nonEvmMethod']); - - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'foo:1': { - accounts: ['foo:1:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'foo:1': { - methods: ['nonEvmMethod'], - notifications: [], - accounts: ['foo:1:0xdeadbeef'], - }, - }); - }); - - it('returns a NormalizedScopesObject for a eip155 namespaced scope', () => { - const result = getSessionScopes( - { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdeadbeef'], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'eip155:1': { - methods: KnownRpcMethods.eip155, - notifications: KnownNotifications.eip155, - accounts: ['eip155:1:0xdeadbeef'], - }, - }); - }); - }); -}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts deleted file mode 100644 index ac3819c6907..00000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - type CaipChainId, - isCaipChainId, - KnownCaipNamespace, -} from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - KnownNotifications, - KnownRpcMethods, - KnownWalletNamespaceRpcMethods, - KnownWalletRpcMethods, -} from '../scope/constants'; -import { mergeNormalizedScopes } from '../scope/transform'; -import type { - InternalScopesObject, - NormalizedScopesObject, -} from '../scope/types'; -import { parseScopeString } from '../scope/types'; - -/** - * Converts an NormalizedScopesObject to a InternalScopesObject. - * - * @param normalizedScopesObject - The NormalizedScopesObject to convert. - * @returns An InternalScopesObject. - */ -export const getInternalScopesObject = ( - normalizedScopesObject: NormalizedScopesObject, -) => { - const internalScopes: InternalScopesObject = {}; - - Object.entries(normalizedScopesObject).forEach( - ([_scopeString, { accounts }]) => { - const scopeString = _scopeString as keyof typeof normalizedScopesObject; - - internalScopes[scopeString] = { - accounts, - }; - }, - ); - - return internalScopes; -}; - -/** - * Converts an InternalScopesObject to a NormalizedScopesObject. - * - * @param internalScopesObject - The InternalScopesObject to convert. - * @param hooks - An object containing the following properties: - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns A NormalizedScopesObject. - */ -const getNormalizedScopesObject = ( - internalScopesObject: InternalScopesObject, - { - getNonEvmSupportedMethods, - }: { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - const normalizedScopes: NormalizedScopesObject = {}; - - Object.entries(internalScopesObject).forEach( - ([_scopeString, { accounts }]) => { - const scopeString = _scopeString as keyof typeof internalScopesObject; - const { namespace, reference } = parseScopeString(scopeString); - let methods: string[] = []; - let notifications: string[] = []; - - if ( - scopeString === KnownCaipNamespace.Wallet || - namespace === KnownCaipNamespace.Wallet - ) { - if (reference === KnownCaipNamespace.Eip155) { - methods = KnownWalletNamespaceRpcMethods[reference]; - } else if (isCaipChainId(scopeString)) { - methods = getNonEvmSupportedMethods(scopeString); - } else { - methods = KnownWalletRpcMethods; - } - } else if (namespace === KnownCaipNamespace.Eip155) { - methods = KnownRpcMethods[namespace]; - notifications = KnownNotifications[namespace]; - } else { - methods = getNonEvmSupportedMethods(scopeString); - notifications = []; - } - - normalizedScopes[scopeString] = { - methods, - notifications, - accounts, - }; - }, - ); - - return normalizedScopes; -}; - -/** - * Takes the scopes from an endowment:caip25 permission caveat value, - * hydrates them with supported methods and notifications, and returns a NormalizedScopesObject. - * - * @param caip25CaveatValue - The CAIP-25 CaveatValue to convert. - * @param hooks - An object containing the following properties: - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns A NormalizedScopesObject. - */ -export const getSessionScopes = ( - caip25CaveatValue: Pick< - Caip25CaveatValue, - 'requiredScopes' | 'optionalScopes' - >, - { - getNonEvmSupportedMethods, - }: { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - return mergeNormalizedScopes( - getNormalizedScopesObject(caip25CaveatValue.requiredScopes, { - getNonEvmSupportedMethods, - }), - getNormalizedScopesObject(caip25CaveatValue.optionalScopes, { - getNonEvmSupportedMethods, - }), - ); -}; diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts deleted file mode 100644 index 6a2ffbc3149..00000000000 --- a/packages/multichain/src/caip25Permission.test.ts +++ /dev/null @@ -1,1290 +0,0 @@ -import { - CaveatMutatorOperation, - PermissionType, -} from '@metamask/permission-controller'; - -import type { Caip25CaveatValue } from './caip25Permission'; -import { - Caip25CaveatType, - caip25EndowmentBuilder, - Caip25EndowmentPermissionName, - Caip25CaveatMutators, - createCaip25Caveat, - caip25CaveatBuilder, - diffScopesForCaip25CaveatValue, -} from './caip25Permission'; -import * as ScopeSupported from './scope/supported'; - -jest.mock('./scope/supported', () => ({ - ...jest.requireActual('./scope/supported'), - isSupportedScopeString: jest.fn(), - isSupportedAccount: jest.fn(), -})); -const MockScopeSupported = jest.mocked(ScopeSupported); - -const { removeAccount, removeScope } = Caip25CaveatMutators[Caip25CaveatType]; - -describe('caip25EndowmentBuilder', () => { - describe('specificationBuilder', () => { - it('builds the expected permission specification', () => { - const specification = caip25EndowmentBuilder.specificationBuilder({ - methodHooks: { - findNetworkClientIdByChainId: jest.fn(), - listAccounts: jest.fn(), - }, - }); - expect(specification).toStrictEqual({ - permissionType: PermissionType.Endowment, - targetName: Caip25EndowmentPermissionName, - endowmentGetter: expect.any(Function), - allowedCaveats: [Caip25CaveatType], - validator: expect.any(Function), - }); - - expect(specification.endowmentGetter()).toBeNull(); - }); - }); - - describe('createCaip25Caveat', () => { - it('builds the caveat', () => { - expect( - createCaip25Caveat({ - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }), - ).toStrictEqual({ - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }, - }); - }); - }); - - describe('Caip25CaveatMutators.authorizedScopes', () => { - describe('removeScope', () => { - it('updates the caveat with the given scope removed from requiredScopes if it is present', () => { - const caveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('updates the caveat with the given scope removed from optionalScopes if it is present', () => { - const caveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:5'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: {}, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('updates the caveat with the given scope removed from requiredScopes and optionalScopes if it is present', () => { - const caveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:5'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: {}, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('revokes the permission if the only non wallet scope is removed', () => { - const caveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - 'wallet:eip155': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:5'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.RevokePermission, - }); - }); - - it('does nothing if the target scope does not exist but the permission only has wallet scopes', () => { - const caveatValue = { - requiredScopes: {}, - optionalScopes: { - 'wallet:eip155': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:5'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, - }); - }); - - it('does nothing if the given scope is not found in either requiredScopes or optionalScopes', () => { - const caveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(caveatValue, 'eip155:2'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, - }); - }); - }); - - describe('removeAccount', () => { - it('updates the caveat with the given account removed from requiredScopes if it is present', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: {}, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x2'], - }, - }, - optionalScopes: {}, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('updates the caveat with the given account removed from optionalScopes if it is present', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x2'], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('updates the caveat with the given account removed from requiredScopes and optionalScopes if it is present', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'eip155:2': { - accounts: ['eip155:2:0x1', 'eip155:2:0x2'], - }, - }, - optionalScopes: { - 'eip155:3': { - accounts: ['eip155:3:0x1', 'eip155:3:0x2'], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x2'], - }, - 'eip155:2': { - accounts: ['eip155:2:0x2'], - }, - }, - optionalScopes: { - 'eip155:3': { - accounts: ['eip155:3:0x2'], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }, - }); - }); - - it('revokes the permission if the only account is removed', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1'], - }, - }, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.RevokePermission, - }); - }); - - it('updates the permission with the target account removed if the target account does exist and `wallet:eip155` is the only scope with remaining accounts after', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1'], - }, - 'wallet:eip155': { - accounts: ['wallet:eip155:0x1', 'wallet:eip155:0x2'], - }, - }, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - 'wallet:eip155': { - accounts: ['wallet:eip155:0x2'], - }, - }, - isMultichainOrigin: true, - }, - }); - }); - - it('does nothing if the target account does not exist but the permission already has no accounts', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, - }); - }); - - it('does nothing if the given account is not found in either requiredScopes or optionalScopes', () => { - const caveatValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }; - const result = removeAccount(caveatValue, '0x3'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, - }); - }); - }); - }); - - describe('permission validator', () => { - const { validator } = caip25EndowmentBuilder.specificationBuilder({}); - - it('throws an error if there is not exactly one caveat', () => { - expect(() => { - validator({ - caveats: [ - { - type: 'caveatType', - value: {}, - }, - { - type: 'caveatType', - value: {}, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ), - ); - - expect(() => { - validator({ - // @ts-expect-error Intentionally invalid input - caveats: [], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ), - ); - }); - - it('throws an error if there is no CAIP-25 caveat', () => { - expect(() => { - validator({ - caveats: [ - { - type: 'NotCaip25Caveat', - value: {}, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ), - ); - }); - }); -}); - -describe('caip25CaveatBuilder', () => { - const findNetworkClientIdByChainId = jest.fn(); - const listAccounts = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - const getNonEvmAccountAddresses = jest.fn(); - const { validator, merger } = caip25CaveatBuilder({ - findNetworkClientIdByChainId, - listAccounts, - isNonEvmScopeSupported, - getNonEvmAccountAddresses, - }); - - it('throws an error if the CAIP-25 caveat is malformed', () => { - expect(() => { - validator({ - type: Caip25CaveatType, - value: { - missingRequiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); - - expect(() => { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: {}, - missingOptionalScopes: {}, - isMultichainOrigin: true, - }, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); - - expect(() => { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: 'NotABoolean', - }, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ), - ); - }); - - it('asserts the internal required scopeStrings are supported', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - - try { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - } catch (err) { - // noop - } - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:1', - { - isEvmChainIdSupported: expect.any(Function), - isNonEvmScopeSupported: expect.any(Function), - }, - ); - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'bip122:000000000019d6689c085ae165831e93', - { - isEvmChainIdSupported: expect.any(Function), - isNonEvmScopeSupported: expect.any(Function), - }, - ); - - MockScopeSupported.isSupportedScopeString.mock.calls[0][1].isEvmChainIdSupported( - '0x1', - ); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); - }); - - it('asserts the internal optional scopeStrings are supported', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - - try { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - } catch (err) { - // noop - } - - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'eip155:5', - { - isEvmChainIdSupported: expect.any(Function), - isNonEvmScopeSupported: expect.any(Function), - }, - ); - expect(MockScopeSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'bip122:12a765e31ffd4059bada1e25190f6e98', - { - isEvmChainIdSupported: expect.any(Function), - isNonEvmScopeSupported: expect.any(Function), - }, - ); - - MockScopeSupported.isSupportedScopeString.mock.calls[1][1].isEvmChainIdSupported( - '0x5', - ); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x5'); - }); - - it('does not throw if unable to find a network client for the evm chainId', () => { - findNetworkClientIdByChainId.mockImplementation(() => { - throw new Error('unable to find network client'); - }); - try { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - } catch (err) { - // noop - } - - expect( - MockScopeSupported.isSupportedScopeString.mock.calls[0][1].isEvmChainIdSupported( - '0x1', - ), - ).toBe(false); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); - }); - - it('throws if not all scopeStrings are supported', () => { - expect(() => { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: [], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ), - ); - }); - - it('asserts the required accounts are supported', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - MockScopeSupported.isSupportedAccount.mockReturnValue(true); - - try { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: ['bip122:000000000019d6689c085ae165831e93:123'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: ['bip122:12a765e31ffd4059bada1e25190f6e98:456'], - }, - }, - isMultichainOrigin: true, - }, - }); - } catch (err) { - // noop - } - expect(MockScopeSupported.isSupportedAccount).toHaveBeenCalledWith( - 'eip155:1:0xdead', - { - getEvmInternalAccounts: expect.any(Function), - getNonEvmAccountAddresses: expect.any(Function), - }, - ); - expect(MockScopeSupported.isSupportedAccount).toHaveBeenCalledWith( - 'bip122:000000000019d6689c085ae165831e93:123', - { - getEvmInternalAccounts: expect.any(Function), - getNonEvmAccountAddresses: expect.any(Function), - }, - ); - }); - - it('asserts the optional accounts are supported', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - MockScopeSupported.isSupportedAccount.mockReturnValue(true); - - try { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: ['bip122:000000000019d6689c085ae165831e93:123'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: ['bip122:12a765e31ffd4059bada1e25190f6e98:456'], - }, - }, - isMultichainOrigin: true, - }, - }); - } catch (err) { - // noop - } - expect(MockScopeSupported.isSupportedAccount).toHaveBeenCalledWith( - 'eip155:5:0xbeef', - { - getEvmInternalAccounts: expect.any(Function), - getNonEvmAccountAddresses: expect.any(Function), - }, - ); - expect(MockScopeSupported.isSupportedAccount).toHaveBeenCalledWith( - 'bip122:000000000019d6689c085ae165831e93:123', - { - getEvmInternalAccounts: expect.any(Function), - getNonEvmAccountAddresses: expect.any(Function), - }, - ); - }); - - it('throws if the accounts specified in the internal scopeObjects are not supported', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - - expect(() => { - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - }, - isMultichainOrigin: true, - }, - }); - }).toThrow( - new Error( - `${Caip25EndowmentPermissionName} error: Received account value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ), - ); - }); - - it('does not throw if the CAIP-25 caveat value is valid', () => { - MockScopeSupported.isSupportedScopeString.mockReturnValue(true); - MockScopeSupported.isSupportedAccount.mockReturnValue(true); - - expect( - validator({ - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'bip122:000000000019d6689c085ae165831e93': { - accounts: ['bip122:000000000019d6689c085ae165831e93:123'], - }, - }, - optionalScopes: { - 'eip155:5': { - accounts: ['eip155:5:0xbeef'], - }, - 'bip122:12a765e31ffd4059bada1e25190f6e98': { - accounts: ['bip122:12a765e31ffd4059bada1e25190f6e98:456'], - }, - }, - isMultichainOrigin: true, - }, - }), - ).toBeUndefined(); - }); - - describe('permission merger', () => { - describe('incremental request an existing scope (requiredScopes), and 2 whole new scopes (optionalScopes) with accounts', () => { - it('should return merged scope with previously existing chain and accounts, plus new requested chains with new accounts', () => { - const initLeftValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const rightValue: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbadd'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef', 'eip155:10:0xbadd'], - }, - 'eip155:426161': { - accounts: [ - 'eip155:426161:0xdead', - 'eip155:426161:0xbeef', - 'eip155:426161:0xbadd', - ], - }, - }, - isMultichainOrigin: false, - }; - - const expectedMergedValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { accounts: ['eip155:1:0xdead'] }, - }, - optionalScopes: { - 'eip155:1': { accounts: ['eip155:1:0xdead', 'eip155:1:0xbadd'] }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef', 'eip155:10:0xbadd'], - }, - 'eip155:426161': { - accounts: [ - 'eip155:426161:0xdead', - 'eip155:426161:0xbeef', - 'eip155:426161:0xbadd', - ], - }, - }, - isMultichainOrigin: false, - }; - const expectedDiff: Caip25CaveatValue = { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { accounts: ['eip155:1:0xdead', 'eip155:1:0xbadd'] }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef', 'eip155:10:0xbadd'], - }, - 'eip155:426161': { - accounts: [ - 'eip155:426161:0xdead', - 'eip155:426161:0xbeef', - 'eip155:426161:0xbadd', - ], - }, - }, - isMultichainOrigin: false, - }; - const [newValue, diff] = merger(initLeftValue, rightValue); - - expect(newValue).toStrictEqual( - expect.objectContaining(expectedMergedValue), - ); - expect(diff).toStrictEqual(expect.objectContaining(expectedDiff)); - }); - }); - }); -}); - -describe('diffScopesForCaip25CaveatValue', () => { - describe('incremental request existing optional scope with a new account', () => { - it('should return scope with existing chain and new requested account', () => { - const leftValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xbeef'], - }, - }, - isMultichainOrigin: false, - requiredScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'optionalScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request a whole new optional scope without accounts', () => { - it('should return scope with new requested chain and no accounts', () => { - const leftValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'eip155:10': { - accounts: [], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - optionalScopes: { - 'eip155:10': { - accounts: [], - }, - }, - isMultichainOrigin: false, - requiredScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'optionalScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request a whole new optional scope with accounts', () => { - it('should return scope with new requested chain and new account', () => { - const leftValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - optionalScopes: { - 'eip155:10': { - accounts: ['eip155:10:0xbeef'], - }, - }, - isMultichainOrigin: false, - requiredScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'optionalScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request an existing optional scope with new accounts, and whole new optional scope with accounts', () => { - it('should return scope with previously existing chain and accounts, plus new requested chain with new accounts', () => { - const leftValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }, - requiredScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xbeef'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }, - isMultichainOrigin: false, - requiredScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'optionalScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request existing required scope with a new account', () => { - it('should return scope with existing chain and new requested account', () => { - const leftValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xbeef'], - }, - }, - isMultichainOrigin: false, - optionalScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'requiredScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request a whole new required scope without accounts', () => { - it('should return scope with new requested chain and no accounts', () => { - const leftValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'eip155:10': { - accounts: [], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - requiredScopes: { - 'eip155:10': { - accounts: [], - }, - }, - isMultichainOrigin: false, - optionalScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'requiredScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request a whole new required scope with accounts', () => { - it('should return scope with new requested chain and new account', () => { - const leftValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - requiredScopes: { - 'eip155:10': { - accounts: ['eip155:10:0xbeef'], - }, - }, - isMultichainOrigin: false, - optionalScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'requiredScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); - - describe('incremental request an existing required scope with new accounts, and whole new required scope with accounts', () => { - it('should return scope with previously existing chain and accounts, plus new requested chain with new accounts', () => { - const leftValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const mergedValue: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }, - optionalScopes: {}, - isMultichainOrigin: false, - }; - - const expectedDiff: Caip25CaveatValue = { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xbeef'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }, - isMultichainOrigin: false, - optionalScopes: {}, - }; - - const diff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'requiredScopes', - ); - - expect(diff).toStrictEqual(expectedDiff); - }); - }); -}); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts deleted file mode 100644 index 46f0cf14b02..00000000000 --- a/packages/multichain/src/caip25Permission.ts +++ /dev/null @@ -1,461 +0,0 @@ -import type { NetworkClientId } from '@metamask/network-controller'; -import type { - PermissionSpecificationBuilder, - EndowmentGetterParams, - ValidPermissionSpecification, - PermissionValidatorConstraint, - PermissionConstraint, - EndowmentCaveatSpecificationConstraint, -} from '@metamask/permission-controller'; -import { - CaveatMutatorOperation, - PermissionType, -} from '@metamask/permission-controller'; -import type { CaipAccountId, CaipChainId, Json } from '@metamask/utils'; -import { - hasProperty, - KnownCaipNamespace, - parseCaipAccountId, - type Hex, - type NonEmptyArray, -} from '@metamask/utils'; -import { cloneDeep, isEqual } from 'lodash'; - -import { assertIsInternalScopesObject } from './scope/assert'; -import { isSupportedAccount, isSupportedScopeString } from './scope/supported'; -import { mergeInternalScopes } from './scope/transform'; -import { - parseScopeString, - type ExternalScopeString, - type InternalScopeObject, - type InternalScopesObject, -} from './scope/types'; - -/** - * The CAIP-25 permission caveat value. - * This permission contains the required and optional scopes and session properties from the [CAIP-25](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) request that initiated the permission session. - * It also contains a boolean (isMultichainOrigin) indicating if the permission session is multichain, which may be needed to determine implicit permissioning. - */ -export type Caip25CaveatValue = { - requiredScopes: InternalScopesObject; - optionalScopes: InternalScopesObject; - sessionProperties?: Record; - isMultichainOrigin: boolean; -}; - -/** - * The name of the CAIP-25 permission caveat. - */ -export const Caip25CaveatType = 'authorizedScopes'; - -/** - * The target name of the CAIP-25 endowment permission. - */ -export const Caip25EndowmentPermissionName = 'endowment:caip25'; - -/** - * Creates a CAIP-25 permission caveat. - * - * @param value - The CAIP-25 permission caveat value. - * @returns The CAIP-25 permission caveat (now including the type). - */ -export const createCaip25Caveat = (value: Caip25CaveatValue) => { - return { - type: Caip25CaveatType, - value, - }; -}; - -type Caip25EndowmentCaveatSpecificationBuilderOptions = { - findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; - listAccounts: () => { type: string; address: Hex }[]; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmAccountAddresses: (scope: CaipChainId) => string[]; -}; - -/** - * Calculates the difference between two provided CAIP-25 permission caveat values, but only considering a single scope property at a time. - * - * @param originalValue - The existing CAIP-25 permission caveat value. - * @param mergedValue - The result from merging existing and incoming CAIP-25 permission caveat values. - * @param scopeToDiff - The required or optional scopes from the [CAIP-25](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) request. - * @returns The difference between original and merged CAIP-25 permission caveat values. - */ -export function diffScopesForCaip25CaveatValue( - originalValue: Caip25CaveatValue, - mergedValue: Caip25CaveatValue, - scopeToDiff: 'optionalScopes' | 'requiredScopes', -): Caip25CaveatValue { - const diff = cloneDeep(originalValue); - - const mergedScopeToDiff = mergedValue[scopeToDiff]; - for (const [scopeString, mergedScopeObject] of Object.entries( - mergedScopeToDiff, - )) { - const internalScopeString = scopeString as keyof typeof mergedScopeToDiff; - const originalScopeObject = diff[scopeToDiff][internalScopeString]; - - if (originalScopeObject) { - const newAccounts = mergedScopeObject.accounts.filter( - (account) => !originalScopeObject?.accounts.includes(account), - ); - if (newAccounts.length > 0) { - diff[scopeToDiff][internalScopeString] = { - accounts: newAccounts, - }; - continue; - } - delete diff[scopeToDiff][internalScopeString]; - } else { - diff[scopeToDiff][internalScopeString] = mergedScopeObject; - } - } - - return diff; -} - -/** - * Checks if every account in the given scopes object is supported. - * - * @param scopesObject - The scopes object to iterate over. - * @param listAccounts - The hook for getting internalAccount objects for all evm accounts. - * @param getNonEvmAccountAddresses - The hook that returns the supported CAIP-10 account addresses for a non EVM scope. - * addresses. - * @returns True if every account in the scopes object is supported, false otherwise. - */ -function isEveryAccountInScopesObjectSupported( - scopesObject: InternalScopesObject, - listAccounts: () => { type: string; address: Hex }[], - getNonEvmAccountAddresses: (scope: CaipChainId) => string[], -) { - return Object.values(scopesObject).every((scopeObject) => - scopeObject.accounts.every((account) => - isSupportedAccount(account, { - getEvmInternalAccounts: listAccounts, - getNonEvmAccountAddresses, - }), - ), - ); -} - -/** - * Helper that returns a `authorizedScopes` CAIP-25 caveat specification - * that can be passed into the PermissionController constructor. - * - * @param options - The specification builder options. - * @param options.findNetworkClientIdByChainId - The hook for getting the networkClientId that serves a chainId. - * @param options.listAccounts - The hook for getting internalAccount objects for all evm accounts. - * @param options.isNonEvmScopeSupported - The hook that determines if an non EVM scopeString is supported. - * @param options.getNonEvmAccountAddresses - The hook that returns the supported CAIP-10 account addresses for a non EVM scope. - * @returns The specification for the `caip25` caveat. - */ -export const caip25CaveatBuilder = ({ - findNetworkClientIdByChainId, - listAccounts, - isNonEvmScopeSupported, - getNonEvmAccountAddresses, -}: Caip25EndowmentCaveatSpecificationBuilderOptions): EndowmentCaveatSpecificationConstraint & - Required< - Pick - > => { - return { - type: Caip25CaveatType, - validator: ( - caveat: { type: typeof Caip25CaveatType; value: unknown }, - _origin?: string, - _target?: string, - ) => { - if ( - !caveat.value || - !hasProperty(caveat.value, 'requiredScopes') || - !hasProperty(caveat.value, 'optionalScopes') || - !hasProperty(caveat.value, 'isMultichainOrigin') || - typeof caveat.value.isMultichainOrigin !== 'boolean' - ) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, - ); - } - - const { requiredScopes, optionalScopes } = caveat.value; - - assertIsInternalScopesObject(requiredScopes); - assertIsInternalScopesObject(optionalScopes); - - const isEvmChainIdSupported = (chainId: Hex) => { - try { - findNetworkClientIdByChainId(chainId); - return true; - } catch (err) { - return false; - } - }; - - const allRequiredScopesSupported = Object.keys(requiredScopes).every( - (scopeString) => - isSupportedScopeString(scopeString, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ); - const allOptionalScopesSupported = Object.keys(optionalScopes).every( - (scopeString) => - isSupportedScopeString(scopeString, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ); - if (!allRequiredScopesSupported || !allOptionalScopesSupported) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Received scopeString value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ); - } - - const allRequiredAccountsSupported = - isEveryAccountInScopesObjectSupported( - requiredScopes, - listAccounts, - getNonEvmAccountAddresses, - ); - const allOptionalAccountsSupported = - isEveryAccountInScopesObjectSupported( - optionalScopes, - listAccounts, - getNonEvmAccountAddresses, - ); - if (!allRequiredAccountsSupported || !allOptionalAccountsSupported) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Received account value(s) for caveat of type "${Caip25CaveatType}" that are not supported by the wallet.`, - ); - } - }, - merger: ( - leftValue: Caip25CaveatValue, - rightValue: Caip25CaveatValue, - ): [Caip25CaveatValue, Caip25CaveatValue] => { - const mergedRequiredScopes = mergeInternalScopes( - leftValue.requiredScopes, - rightValue.requiredScopes, - ); - const mergedOptionalScopes = mergeInternalScopes( - leftValue.optionalScopes, - rightValue.optionalScopes, - ); - - const mergedValue: Caip25CaveatValue = { - requiredScopes: mergedRequiredScopes, - optionalScopes: mergedOptionalScopes, - isMultichainOrigin: leftValue.isMultichainOrigin, - }; - - const partialDiff = diffScopesForCaip25CaveatValue( - leftValue, - mergedValue, - 'requiredScopes', - ); - - const diff = diffScopesForCaip25CaveatValue( - partialDiff, - mergedValue, - 'optionalScopes', - ); - - return [mergedValue, diff]; - }, - }; -}; - -type Caip25EndowmentSpecification = ValidPermissionSpecification<{ - permissionType: PermissionType.Endowment; - targetName: typeof Caip25EndowmentPermissionName; - endowmentGetter: (_options?: EndowmentGetterParams) => null; - validator: PermissionValidatorConstraint; - allowedCaveats: Readonly> | null; -}>; - -/** - * Helper that returns a `endowment:caip25` specification that - * can be passed into the PermissionController constructor. - * - * @returns The specification for the `caip25` endowment. - */ -const specificationBuilder: PermissionSpecificationBuilder< - PermissionType.Endowment, - Record, - Caip25EndowmentSpecification -> = () => { - return { - permissionType: PermissionType.Endowment, - targetName: Caip25EndowmentPermissionName, - allowedCaveats: [Caip25CaveatType], - endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, - validator: (permission: PermissionConstraint) => { - if ( - permission.caveats?.length !== 1 || - permission.caveats?.[0]?.type !== Caip25CaveatType - ) { - throw new Error( - `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, - ); - } - }, - }; -}; - -/** - * The `caip25` endowment specification builder. Passed to the - * `PermissionController` for constructing and validating the - * `endowment:caip25` permission. - */ -export const caip25EndowmentBuilder = Object.freeze({ - targetName: Caip25EndowmentPermissionName, - specificationBuilder, -} as const); - -/** - * Factories that construct caveat mutator functions that are passed to - * PermissionController.updatePermissionsByCaveat. - */ -export const Caip25CaveatMutators = { - [Caip25CaveatType]: { - removeScope, - removeAccount, - }, -}; - -/** - * Removes the account from the scope object. - * - * @param targetAddress - The address to remove from the scope object. - * @returns A function that removes the account from the scope object. - */ -function removeAccountFilterFn(targetAddress: string) { - return (account: CaipAccountId) => { - const parsed = parseCaipAccountId(account); - return parsed.address !== targetAddress; - }; -} - -/** - * Removes the account from the scope object. - * - * @param scopeObject - The scope object to remove the account from. - * @param targetAddress - The address to remove from the scope object. - */ -function removeAccountFromScopeObject( - scopeObject: InternalScopeObject, - targetAddress: string, -) { - if (scopeObject.accounts) { - scopeObject.accounts = scopeObject.accounts.filter( - removeAccountFilterFn(targetAddress), - ); - } -} - -/** - * Removes the target account from the scope object. - * - * @param caip25CaveatValue - The CAIP-25 permission caveat value from which to remove the account (across all chain scopes). - * @param targetAddress - The address to remove from the scope object. Not a CAIP-10 formatted address because it will be removed across each chain scope. - * @returns The updated scope object. - */ -function removeAccount( - caip25CaveatValue: Caip25CaveatValue, - targetAddress: Hex, -) { - const updatedCaveatValue = cloneDeep(caip25CaveatValue); - - [ - updatedCaveatValue.requiredScopes, - updatedCaveatValue.optionalScopes, - ].forEach((scopes) => { - Object.entries(scopes).forEach(([, scopeObject]) => { - removeAccountFromScopeObject(scopeObject, targetAddress); - }); - }); - - const noChange = isEqual(updatedCaveatValue, caip25CaveatValue); - - if (noChange) { - return { - operation: CaveatMutatorOperation.Noop, - }; - } - - const hasAccounts = [ - ...Object.values(updatedCaveatValue.requiredScopes), - ...Object.values(updatedCaveatValue.optionalScopes), - ].some(({ accounts }) => accounts.length > 0); - - if (hasAccounts) { - return { - operation: CaveatMutatorOperation.UpdateValue, - value: updatedCaveatValue, - }; - } - - return { - operation: CaveatMutatorOperation.RevokePermission, - }; -} - -/** - * Removes the target scope from the value arrays of the given - * `endowment:caip25` caveat. No-ops if the target scopeString is not in - * the existing scopes. - * - * @param caip25CaveatValue - The CAIP-25 permission caveat value to remove the scope from. - * @param targetScopeString - The scope that is being removed. - * @returns The updated CAIP-25 permission caveat value. - */ -function removeScope( - caip25CaveatValue: Caip25CaveatValue, - targetScopeString: ExternalScopeString, -) { - const newRequiredScopes = Object.entries( - caip25CaveatValue.requiredScopes, - ).filter(([scope]) => scope !== targetScopeString); - const newOptionalScopes = Object.entries( - caip25CaveatValue.optionalScopes, - ).filter(([scope]) => { - return scope !== targetScopeString; - }); - - const requiredScopesRemoved = - newRequiredScopes.length !== - Object.keys(caip25CaveatValue.requiredScopes).length; - const optionalScopesRemoved = - newOptionalScopes.length !== - Object.keys(caip25CaveatValue.optionalScopes).length; - - if (!requiredScopesRemoved && !optionalScopesRemoved) { - return { - operation: CaveatMutatorOperation.Noop, - }; - } - - const updatedCaveatValue = { - ...caip25CaveatValue, - requiredScopes: Object.fromEntries(newRequiredScopes), - optionalScopes: Object.fromEntries(newOptionalScopes), - }; - - const hasNonWalletScopes = [...newRequiredScopes, ...newOptionalScopes].some( - ([scopeString]) => { - const { namespace } = parseScopeString(scopeString); - return namespace !== KnownCaipNamespace.Wallet; - }, - ); - - if (hasNonWalletScopes) { - return { - operation: CaveatMutatorOperation.UpdateValue, - value: updatedCaveatValue, - }; - } - - return { - operation: CaveatMutatorOperation.RevokePermission, - }; -} diff --git a/packages/multichain/src/constants/permissions.ts b/packages/multichain/src/constants/permissions.ts deleted file mode 100644 index 1317fbefb19..00000000000 --- a/packages/multichain/src/constants/permissions.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum CaveatTypes { - RestrictReturnedAccounts = 'restrictReturnedAccounts', - RestrictNetworkSwitching = 'restrictNetworkSwitching', -} - -export enum EndowmentTypes { - PermittedChains = 'endowment:permitted-chains', -} - -export enum RestrictedMethods { - EthAccounts = 'eth_accounts', -} diff --git a/packages/multichain/src/handlers/wallet-getPermissions.test.ts b/packages/multichain/src/handlers/wallet-getPermissions.test.ts deleted file mode 100644 index 750787ea3d6..00000000000 --- a/packages/multichain/src/handlers/wallet-getPermissions.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -import type { - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; - -import { getPermissionsHandler } from './wallet-getPermissions'; -import * as caipPermissionAdapterPermittedChains from '../adapters/caip-permission-adapter-permittedChains'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { - CaveatTypes, - EndowmentTypes, - RestrictedMethods, -} from '../constants/permissions'; - -jest.mock('../adapters/caip-permission-adapter-permittedChains', () => ({ - __esModule: true, - ...jest.requireActual('../adapters/caip-permission-adapter-permittedChains'), -})); - -const baseRequest = { - jsonrpc: '2.0' as const, - id: 0, - method: 'wallet_getPermissions', -}; - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const getPermissionsForOrigin = jest.fn().mockReturnValue( - Object.freeze({ - [Caip25EndowmentPermissionName]: { - id: '1', - parentCapability: Caip25EndowmentPermissionName, - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - }, - 'eip155:5': { - accounts: ['eip155:5:0x1', 'eip155:5:0x3'], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: ['eip155:1:0xdeadbeef'], - }, - }, - }, - }, - ], - }, - otherPermission: { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - }), - ); - const getAccounts = jest.fn().mockReturnValue([]); - const response: PendingJsonRpcResponse = { - jsonrpc: '2.0' as const, - id: 0, - }; - const handler = (request: JsonRpcRequest) => - getPermissionsHandler.implementation(request, response, next, end, { - getPermissionsForOrigin, - getAccounts, - }); - - return { - response, - next, - end, - getPermissionsForOrigin, - getAccounts, - handler, - }; -}; - -describe('getPermissionsHandler', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - beforeEach(() => { - jest - .spyOn(caipPermissionAdapterPermittedChains, 'getPermittedEthChainIds') - .mockReturnValue([]); - }); - - it('gets the permissions for the origin', async () => { - const { handler, getPermissionsForOrigin } = createMockedHandler(); - - await handler(baseRequest); - expect(getPermissionsForOrigin).toHaveBeenCalled(); - }); - - it('returns permissions unmodified if no CAIP-25 endowment permission has been granted', async () => { - const { handler, getPermissionsForOrigin, response } = - createMockedHandler(); - - getPermissionsForOrigin.mockReturnValue( - Object.freeze({ - otherPermission: { - id: '1', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - }), - ); - - await handler(baseRequest); - expect(response.result).toStrictEqual([ - { - id: '1', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - ]); - }); - - describe('CAIP-25 endowment permissions has been granted', () => { - it('returns the permissions with the CAIP-25 permission removed', async () => { - const { handler, getAccounts, getPermissionsForOrigin, response } = - createMockedHandler(); - getPermissionsForOrigin.mockReturnValue( - Object.freeze({ - [Caip25EndowmentPermissionName]: { - id: '1', - parentCapability: Caip25EndowmentPermissionName, - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - }, - }, - ], - }, - otherPermission: { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - }), - ); - getAccounts.mockReturnValue([]); - jest - .spyOn(caipPermissionAdapterPermittedChains, 'getPermittedEthChainIds') - .mockReturnValue([]); - - await handler(baseRequest); - expect(response.result).toStrictEqual([ - { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - ]); - }); - - it('gets the lastSelected sorted permitted eth accounts for the origin', async () => { - const { handler, getAccounts } = createMockedHandler(); - await handler(baseRequest); - expect(getAccounts).toHaveBeenCalledWith({ ignoreLock: true }); - }); - - it('returns the permissions with an eth_accounts permission if some eth accounts are permitted', async () => { - const { handler, getAccounts, response } = createMockedHandler(); - getAccounts.mockReturnValue(['0x1', '0x2', '0x3', '0xdeadbeef']); - - await handler(baseRequest); - expect(response.result).toStrictEqual([ - { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - { - id: '1', - parentCapability: RestrictedMethods.EthAccounts, - caveats: [ - { - type: CaveatTypes.RestrictReturnedAccounts, - value: ['0x1', '0x2', '0x3', '0xdeadbeef'], - }, - ], - }, - ]); - }); - - it('gets the permitted eip155 chainIds from the CAIP-25 caveat value', async () => { - const { handler, getPermissionsForOrigin } = createMockedHandler(); - getPermissionsForOrigin.mockReturnValue( - Object.freeze({ - [Caip25EndowmentPermissionName]: { - id: '1', - parentCapability: Caip25EndowmentPermissionName, - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - }, - }, - }, - ], - }, - otherPermission: { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - }), - ); - await handler(baseRequest); - expect( - caipPermissionAdapterPermittedChains.getPermittedEthChainIds, - ).toHaveBeenCalledWith({ - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - }, - }); - }); - - it('returns the permissions with a permittedChains permission if some eip155 chainIds are permitted', async () => { - const { handler, response } = createMockedHandler(); - jest - .spyOn(caipPermissionAdapterPermittedChains, 'getPermittedEthChainIds') - .mockReturnValue(['0x1', '0x64']); - - await handler(baseRequest); - expect(response.result).toStrictEqual([ - { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - { - id: '1', - parentCapability: EndowmentTypes.PermittedChains, - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x1', '0x64'], - }, - ], - }, - ]); - }); - - it('returns the permissions with a eth_accounts and permittedChains permission if some eip155 accounts and chainIds are permitted', async () => { - const { handler, getAccounts, response } = createMockedHandler(); - getAccounts.mockReturnValue(['0x1', '0x2', '0xdeadbeef']); - jest - .spyOn(caipPermissionAdapterPermittedChains, 'getPermittedEthChainIds') - .mockReturnValue(['0x1', '0x64']); - - await handler(baseRequest); - expect(response.result).toStrictEqual([ - { - id: '2', - parentCapability: 'otherPermission', - caveats: [ - { - value: { - foo: 'bar', - }, - }, - ], - }, - { - id: '1', - parentCapability: RestrictedMethods.EthAccounts, - caveats: [ - { - type: CaveatTypes.RestrictReturnedAccounts, - value: ['0x1', '0x2', '0xdeadbeef'], - }, - ], - }, - { - id: '1', - parentCapability: EndowmentTypes.PermittedChains, - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x1', '0x64'], - }, - ], - }, - ]); - }); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-getPermissions.ts b/packages/multichain/src/handlers/wallet-getPermissions.ts deleted file mode 100644 index 31f5462cad6..00000000000 --- a/packages/multichain/src/handlers/wallet-getPermissions.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { - AsyncJsonRpcEngineNextCallback, - JsonRpcEngineEndCallback, -} from '@metamask/json-rpc-engine'; -import { - type CaveatSpecificationConstraint, - MethodNames, - type PermissionController, - type PermissionSpecificationConstraint, -} from '@metamask/permission-controller'; -import type { - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; - -import { getPermittedEthChainIds } from '../adapters/caip-permission-adapter-permittedChains'; -import { - Caip25CaveatType, - type Caip25CaveatValue, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { - EndowmentTypes, - RestrictedMethods, - CaveatTypes, -} from '../constants/permissions'; - -export const getPermissionsHandler = { - methodNames: [MethodNames.GetPermissions], - implementation: getPermissionsImplementation, - hookNames: { - getPermissionsForOrigin: true, - getAccounts: true, - }, -}; - -/** - * Get Permissions implementation to be used in JsonRpcEngine middleware, specifically for `wallet_getPermissions` RPC method. - * It makes use of a CAIP-25 endowment permission returned by `getPermissionsForOrigin` hook, if it exists. - * - * @param _req - The JsonRpcEngine request - unused - * @param res - The JsonRpcEngine result object - * @param _next - JsonRpcEngine next() callback - unused - * @param end - JsonRpcEngine end() callback - * @param options - Method hooks passed to the method implementation - * @param options.getPermissionsForOrigin - The specific method hook needed for this method implementation - * @param options.getAccounts - A hook that returns the permitted eth accounts for the origin sorted by lastSelected. - * @returns A promise that resolves to nothing - */ -async function getPermissionsImplementation( - _req: JsonRpcRequest, - res: PendingJsonRpcResponse, - _next: AsyncJsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - { - getPermissionsForOrigin, - getAccounts, - }: { - getPermissionsForOrigin: () => ReturnType< - PermissionController< - PermissionSpecificationConstraint, - CaveatSpecificationConstraint - >['getPermissions'] - >; - getAccounts: (options?: { ignoreLock?: boolean }) => string[]; - }, -) { - const permissions = { ...getPermissionsForOrigin() }; - const caip25Endowment = permissions[Caip25EndowmentPermissionName]; - const caip25CaveatValue = caip25Endowment?.caveats?.find( - ({ type }) => type === Caip25CaveatType, - )?.value as Caip25CaveatValue | undefined; - delete permissions[Caip25EndowmentPermissionName]; - - if (caip25CaveatValue) { - // We cannot derive ethAccounts directly from the CAIP-25 permission - // because the accounts will not be in order of lastSelected - const ethAccounts = getAccounts({ ignoreLock: true }); - - if (ethAccounts.length > 0) { - permissions[RestrictedMethods.EthAccounts] = { - ...caip25Endowment, - parentCapability: RestrictedMethods.EthAccounts, - caveats: [ - { - type: CaveatTypes.RestrictReturnedAccounts, - value: ethAccounts, - }, - ], - }; - } - - const ethChainIds = getPermittedEthChainIds(caip25CaveatValue); - - if (ethChainIds.length > 0) { - permissions[EndowmentTypes.PermittedChains] = { - ...caip25Endowment, - parentCapability: EndowmentTypes.PermittedChains, - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ethChainIds, - }, - ], - }; - } - } - - res.result = Object.values(permissions); - return end(); -} diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts deleted file mode 100644 index 1f1e2efd1af..00000000000 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { JsonRpcRequest } from '@metamask/utils'; - -import { walletGetSession } from './wallet-getSession'; -import * as PermissionAdapterSessionScopes from '../adapters/caip-permission-adapter-session-scopes'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; - -jest.mock('../adapters/caip-permission-adapter-session-scopes', () => ({ - getSessionScopes: jest.fn(), -})); -const MockPermissionAdapterSessionScopes = jest.mocked( - PermissionAdapterSessionScopes, -); - -const baseRequest: JsonRpcRequest & { origin: string } = { - origin: 'http://test.com', - jsonrpc: '2.0' as const, - method: 'wallet_getSession', - params: {}, - id: 1, -}; - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const getNonEvmSupportedMethods = jest.fn(); - const getCaveatForOrigin = jest.fn().mockReturnValue({ - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - }, - }); - const response = { - result: { - sessionScopes: {}, - }, - id: 1, - jsonrpc: '2.0' as const, - }; - const handler = (request: JsonRpcRequest & { origin: string }) => - walletGetSession.implementation(request, response, next, end, { - getCaveatForOrigin, - getNonEvmSupportedMethods, - }); - - return { - next, - response, - end, - getCaveatForOrigin, - getNonEvmSupportedMethods, - handler, - }; -}; - -describe('wallet_getSession', () => { - it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { - const { handler, getCaveatForOrigin } = createMockedHandler(); - - await handler(baseRequest); - expect(getCaveatForOrigin).toHaveBeenCalledWith( - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - }); - - it('returns empty scopes if the CAIP-25 endowment permission does not exist', async () => { - const { handler, response, getCaveatForOrigin } = createMockedHandler(); - getCaveatForOrigin.mockImplementation(() => { - throw new Error('permission not found'); - }); - - await handler(baseRequest); - expect(response.result).toStrictEqual({ - sessionScopes: {}, - }); - }); - - it('gets the session scopes from the CAIP-25 caveat value', async () => { - const { handler, getNonEvmSupportedMethods } = createMockedHandler(); - - await handler(baseRequest); - expect( - MockPermissionAdapterSessionScopes.getSessionScopes, - ).toHaveBeenCalledWith( - { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - }); - - it('returns the session scopes', async () => { - const { handler, response } = createMockedHandler(); - - MockPermissionAdapterSessionScopes.getSessionScopes.mockReturnValue({ - 'eip155:1': { - methods: ['eth_call', 'net_version'], - notifications: ['chainChanged'], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - }); - - await handler(baseRequest); - expect(response.result).toStrictEqual({ - sessionScopes: { - 'eip155:1': { - methods: ['eth_call', 'net_version'], - notifications: ['chainChanged'], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - }, - }); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts deleted file mode 100644 index 72fd0326dc0..00000000000 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Caveat } from '@metamask/permission-controller'; -import type { - CaipChainId, - JsonRpcRequest, - JsonRpcSuccess, -} from '@metamask/utils'; - -import { getSessionScopes } from '../adapters/caip-permission-adapter-session-scopes'; -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import type { NormalizedScopesObject } from '../scope/types'; - -/** - * Handler for the `wallet_getSession` RPC method as specified by [CAIP-312](https://chainagnostic.org/CAIPs/caip-312). - * The implementation below deviates from the linked spec in that it ignores the `sessionId` param entirely, - * and that an empty object is returned for the `sessionScopes` result rather than throwing an error if there - * is no active session for the origin. - * - * @param _request - The request object. - * @param response - The response object. - * @param _next - The next middleware function. Unused. - * @param end - The end function. - * @param hooks - The hooks object. - * @param hooks.getCaveatForOrigin - Function to retrieve a caveat for the origin. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - */ -async function walletGetSessionHandler( - _request: JsonRpcRequest & { origin: string }, - response: JsonRpcSuccess<{ sessionScopes: NormalizedScopesObject }>, - _next: () => void, - end: () => void, - hooks: { - getCaveatForOrigin: ( - endowmentPermissionName: string, - caveatType: string, - ) => Caveat; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) { - let caveat; - try { - caveat = hooks.getCaveatForOrigin( - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - } catch (e) { - // noop - } - - if (!caveat) { - response.result = { sessionScopes: {} }; - return end(); - } - - response.result = { - sessionScopes: getSessionScopes(caveat.value, { - getNonEvmSupportedMethods: hooks.getNonEvmSupportedMethods, - }), - }; - return end(); -} - -export const walletGetSession = { - methodNames: ['wallet_getSession'], - implementation: walletGetSessionHandler, - hookNames: { - getCaveatForOrigin: true, - getNonEvmSupportedMethods: true, - }, -}; diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts deleted file mode 100644 index 3b5048cbc92..00000000000 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; - -import type { WalletInvokeMethodRequest } from './wallet-invokeMethod'; -import { walletInvokeMethod } from './wallet-invokeMethod'; -import * as PermissionAdapterSessionScopes from '../adapters/caip-permission-adapter-session-scopes'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; - -jest.mock('../adapters/caip-permission-adapter-session-scopes', () => ({ - getSessionScopes: jest.fn(), -})); -const MockPermissionAdapterSessionScopes = jest.mocked( - PermissionAdapterSessionScopes, -); - -const createMockedRequest = () => ({ - jsonrpc: '2.0' as const, - id: 0, - origin: 'http://test.com', - method: 'wallet_invokeMethod', - params: { - scope: 'eip155:1', - request: { - method: 'eth_call', - params: { - foo: 'bar', - }, - }, - }, -}); - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const getCaveatForOrigin = jest.fn().mockReturnValue({ - value: { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - const findNetworkClientIdByChainId = jest.fn().mockReturnValue('mainnet'); - const getSelectedNetworkClientId = jest - .fn() - .mockReturnValue('selectedNetworkClientId'); - const getNonEvmSupportedMethods = jest.fn().mockReturnValue([]); - const handleNonEvmRequestForOrigin = jest.fn().mockResolvedValue(null); - const response = { jsonrpc: '2.0' as const, id: 1 }; - const handler = (request: WalletInvokeMethodRequest) => - walletInvokeMethod.implementation(request, response, next, end, { - getCaveatForOrigin, - findNetworkClientIdByChainId, - getSelectedNetworkClientId, - getNonEvmSupportedMethods, - handleNonEvmRequestForOrigin, - }); - - return { - response, - next, - end, - getCaveatForOrigin, - findNetworkClientIdByChainId, - getSelectedNetworkClientId, - getNonEvmSupportedMethods, - handleNonEvmRequestForOrigin, - handler, - }; -}; - -describe('wallet_invokeMethod', () => { - beforeEach(() => { - MockPermissionAdapterSessionScopes.getSessionScopes.mockReturnValue({ - 'eip155:1': { - methods: ['eth_call', 'net_version'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - 'wallet:eip155': { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - 'nonevm:scope': { - methods: ['foobar'], - notifications: [], - accounts: ['nonevm:scope:0x1'], - }, - }); - }); - - it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { - const request = createMockedRequest(); - const { handler, getCaveatForOrigin } = createMockedHandler(); - await handler(request); - expect(getCaveatForOrigin).toHaveBeenCalledWith( - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - }); - - it('gets the session scopes from the CAIP-25 caveat value', async () => { - const request = createMockedRequest(); - const { handler, getNonEvmSupportedMethods } = createMockedHandler(); - await handler(request); - expect( - MockPermissionAdapterSessionScopes.getSessionScopes, - ).toHaveBeenCalledWith( - { - requiredScopes: { - 'eip155:1': { - accounts: [], - }, - 'eip155:5': { - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - accounts: [], - }, - wallet: { - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - { - getNonEvmSupportedMethods, - }, - ); - }); - - it('throws an unauthorized error when there is no CAIP-25 endowment permission', async () => { - const request = createMockedRequest(); - const { handler, getCaveatForOrigin, end } = createMockedHandler(); - getCaveatForOrigin.mockImplementation(() => { - throw new Error('permission not found'); - }); - await handler(request); - expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); - }); - - it('throws an unauthorized error when the CAIP-25 endowment permission was not granted from the multichain flow', async () => { - const request = createMockedRequest(); - const { handler, getCaveatForOrigin, end } = createMockedHandler(); - getCaveatForOrigin.mockReturnValue({ - value: { - isMultichainOrigin: false, - }, - }); - await handler(request); - expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); - }); - - it('throws an unauthorized error if the requested scope is not authorized', async () => { - const request = createMockedRequest(); - const { handler, end } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'eip155:999', - }, - }); - expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); - }); - - it('throws an unauthorized error if the requested scope method is not authorized', async () => { - const request = createMockedRequest(); - const { handler, end } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - request: { - ...request.params.request, - method: 'unauthorized_method', - }, - }, - }); - expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); - }); - - describe('ethereum scope', () => { - it('gets the networkClientId for the chainId', async () => { - const request = createMockedRequest(); - const { handler, findNetworkClientIdByChainId } = createMockedHandler(); - - await handler(request); - expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); - }); - - it('throws an internal error if a networkClientId does not exist for the chainId', async () => { - const request = createMockedRequest(); - const { handler, findNetworkClientIdByChainId, end } = - createMockedHandler(); - findNetworkClientIdByChainId.mockReturnValue(undefined); - - await handler(request); - expect(end).toHaveBeenCalledWith(rpcErrors.internal()); - }); - - it('sets the networkClientId and unwraps the CAIP-27 request', async () => { - const request = createMockedRequest(); - const { handler, next } = createMockedHandler(); - - await handler(request); - expect(request).toStrictEqual({ - jsonrpc: '2.0' as const, - id: 0, - scope: 'eip155:1', - origin: 'http://test.com', - networkClientId: 'mainnet', - method: 'eth_call', - params: { - foo: 'bar', - }, - }); - expect(next).toHaveBeenCalled(); - }); - }); - - describe('wallet scope', () => { - it('gets the networkClientId for the globally selected network', async () => { - const request = createMockedRequest(); - const { handler, getSelectedNetworkClientId } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'wallet', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }); - expect(getSelectedNetworkClientId).toHaveBeenCalled(); - }); - - it('throws an internal error if a networkClientId cannot be retrieved for the globally selected network', async () => { - const request = createMockedRequest(); - const { handler, getSelectedNetworkClientId, end } = - createMockedHandler(); - getSelectedNetworkClientId.mockReturnValue(undefined); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'wallet', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }); - expect(end).toHaveBeenCalledWith(rpcErrors.internal()); - }); - - it('sets the networkClientId and unwraps the CAIP-27 request', async () => { - const request = createMockedRequest(); - const { handler, next } = createMockedHandler(); - - const walletRequest = { - ...request, - params: { - ...request.params, - scope: 'wallet', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }; - await handler(walletRequest); - expect(walletRequest).toStrictEqual({ - jsonrpc: '2.0' as const, - id: 0, - scope: 'wallet', - origin: 'http://test.com', - networkClientId: 'selectedNetworkClientId', - method: 'wallet_watchAsset', - params: { - foo: 'bar', - }, - }); - expect(next).toHaveBeenCalled(); - }); - }); - - describe("'wallet:eip155' scope", () => { - it('gets the networkClientId for the globally selected network', async () => { - const request = createMockedRequest(); - const { handler, getSelectedNetworkClientId } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'wallet:eip155', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }); - expect(getSelectedNetworkClientId).toHaveBeenCalled(); - }); - - it('throws an internal error if a networkClientId cannot be retrieved for the globally selected network', async () => { - const request = createMockedRequest(); - const { handler, getSelectedNetworkClientId, end } = - createMockedHandler(); - getSelectedNetworkClientId.mockReturnValue(undefined); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'wallet:eip155', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }); - expect(end).toHaveBeenCalledWith(rpcErrors.internal()); - }); - - it('sets the networkClientId and unwraps the CAIP-27 request', async () => { - const request = createMockedRequest(); - const { handler, next } = createMockedHandler(); - - const walletRequest = { - ...request, - params: { - ...request.params, - scope: 'wallet:eip155', - request: { - ...request.params.request, - method: 'wallet_watchAsset', - }, - }, - }; - await handler(walletRequest); - expect(walletRequest).toStrictEqual({ - jsonrpc: '2.0' as const, - id: 0, - scope: 'wallet:eip155', - origin: 'http://test.com', - networkClientId: 'selectedNetworkClientId', - method: 'wallet_watchAsset', - params: { - foo: 'bar', - }, - }); - expect(next).toHaveBeenCalled(); - }); - }); - - describe('non-evm scope', () => { - it('forwards the unwrapped CAIP-27 request for authorized non-evm scopes to handleNonEvmRequestForOrigin', async () => { - const request = createMockedRequest(); - const { handler, handleNonEvmRequestForOrigin } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'nonevm:scope', - request: { - ...request.params.request, - method: 'foobar', - }, - }, - }); - - expect(handleNonEvmRequestForOrigin).toHaveBeenCalledWith({ - connectedAddresses: ['nonevm:scope:0x1'], - scope: 'nonevm:scope', - request: { - id: 0, - jsonrpc: '2.0', - method: 'foobar', - origin: 'http://test.com', - params: { - foo: 'bar', - }, - scope: 'nonevm:scope', - }, - }); - }); - - it('sets response.result to the return value from handleNonEvmRequestForOrigin', async () => { - const request = createMockedRequest(); - const { handler, handleNonEvmRequestForOrigin, end, response } = - createMockedHandler(); - handleNonEvmRequestForOrigin.mockResolvedValue('nonEvmResult'); - await handler({ - ...request, - params: { - ...request.params, - scope: 'nonevm:scope', - request: { - ...request.params.request, - method: 'foobar', - }, - }, - }); - - expect(response).toStrictEqual({ - jsonrpc: '2.0', - id: 1, - result: 'nonEvmResult', - }); - expect(end).toHaveBeenCalledWith(); - }); - - it('returns an error if handleNonEvmRequestForOrigin throws', async () => { - const request = createMockedRequest(); - const { handler, handleNonEvmRequestForOrigin, end } = - createMockedHandler(); - handleNonEvmRequestForOrigin.mockRejectedValue( - new Error('handleNonEvemRequest failed'), - ); - await handler({ - ...request, - params: { - ...request.params, - scope: 'nonevm:scope', - request: { - ...request.params.request, - method: 'foobar', - }, - }, - }); - - expect(end).toHaveBeenCalledWith( - new Error('handleNonEvemRequest failed'), - ); - }); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts deleted file mode 100644 index df2e3f205d7..00000000000 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ /dev/null @@ -1,157 +0,0 @@ -import type { NetworkClientId } from '@metamask/network-controller'; -import type { Caveat } from '@metamask/permission-controller'; -import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; -import type { - CaipAccountId, - CaipChainId, - Hex, - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; -import { KnownCaipNamespace, numberToHex } from '@metamask/utils'; - -import { getSessionScopes } from '../adapters/caip-permission-adapter-session-scopes'; -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { assertIsInternalScopeString } from '../scope/assert'; -import type { ExternalScopeString } from '../scope/types'; -import { parseScopeString } from '../scope/types'; - -export type WalletInvokeMethodRequest = JsonRpcRequest & { - origin: string; - params: { - scope: ExternalScopeString; - request: Pick; - }; -}; - -/** - * Handler for the `wallet_invokeMethod` RPC method as specified by [CAIP-27](https://chainagnostic.org/CAIPs/caip-27). - * The implementation below deviates from the linked spec in that it ignores the `sessionId` param - * and instead uses the singular session for the origin if available. - * - * @param request - The request object. - * @param response - The response object. Unused. - * @param next - The next middleware function. - * @param end - The end function. - * @param hooks - The hooks object. - * @param hooks.getCaveatForOrigin - the hook for getting a caveat from a permission for an origin. - * @param hooks.findNetworkClientIdByChainId - the hook for finding the networkClientId for a chainId. - * @param hooks.getSelectedNetworkClientId - the hook for getting the current globally selected networkClientId. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @param hooks.handleNonEvmRequestForOrigin - A function that sends a request to the MultichainRouter for processing. - */ -async function walletInvokeMethodHandler( - request: WalletInvokeMethodRequest, - response: PendingJsonRpcResponse, - next: () => void, - end: (error?: Error) => void, - hooks: { - getCaveatForOrigin: ( - endowmentPermissionName: string, - caveatType: string, - ) => Caveat; - findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId | undefined; - getSelectedNetworkClientId: () => NetworkClientId; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - handleNonEvmRequestForOrigin: (params: { - connectedAddresses: CaipAccountId[]; - scope: CaipChainId; - request: JsonRpcRequest; - }) => Promise; - }, -) { - const { scope, request: wrappedRequest } = request.params; - - assertIsInternalScopeString(scope); - - let caveat; - try { - caveat = hooks.getCaveatForOrigin( - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - } catch (e) { - // noop - } - if (!caveat?.value?.isMultichainOrigin) { - return end(providerErrors.unauthorized()); - } - - const scopeObject = getSessionScopes(caveat.value, { - getNonEvmSupportedMethods: hooks.getNonEvmSupportedMethods, - })[scope]; - - if (!scopeObject?.methods?.includes(wrappedRequest.method)) { - return end(providerErrors.unauthorized()); - } - - const { namespace, reference } = parseScopeString(scope); - - const isEvmRequest = - (namespace === KnownCaipNamespace.Wallet && - (!reference || reference === KnownCaipNamespace.Eip155)) || - namespace === KnownCaipNamespace.Eip155; - - const unwrappedRequest = { - ...request, - scope, - method: wrappedRequest.method, - params: wrappedRequest.params, - }; - - if (isEvmRequest) { - let networkClientId; - if (namespace === KnownCaipNamespace.Wallet) { - networkClientId = hooks.getSelectedNetworkClientId(); - } else if (namespace === KnownCaipNamespace.Eip155) { - if (reference) { - networkClientId = hooks.findNetworkClientIdByChainId( - numberToHex(parseInt(reference, 10)), - ); - } - } - - if (!networkClientId) { - console.error( - 'failed to resolve network client for wallet_invokeMethod', - request, - ); - return end(rpcErrors.internal()); - } - - Object.assign(request, { - ...unwrappedRequest, - networkClientId, - }); - return next(); - } - - try { - response.result = await hooks.handleNonEvmRequestForOrigin({ - connectedAddresses: scopeObject.accounts, - // Type assertion: We know that scope is not "wallet" by now because it - // is already being handled above. - scope: scope as CaipChainId, - request: unwrappedRequest, - }); - } catch (err) { - return end(err as Error); - } - return end(); -} -export const walletInvokeMethod = { - methodNames: ['wallet_invokeMethod'], - implementation: walletInvokeMethodHandler, - hookNames: { - getCaveatForOrigin: true, - findNetworkClientIdByChainId: true, - getSelectedNetworkClientId: true, - getNonEvmSupportedMethods: true, - handleNonEvmRequestForOrigin: true, - }, -}; diff --git a/packages/multichain/src/handlers/wallet-requestPermissions.test.ts b/packages/multichain/src/handlers/wallet-requestPermissions.test.ts deleted file mode 100644 index 2b890d6bba1..00000000000 --- a/packages/multichain/src/handlers/wallet-requestPermissions.test.ts +++ /dev/null @@ -1,592 +0,0 @@ -import { - invalidParams, - type RequestedPermissions, -} from '@metamask/permission-controller'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - -import { requestPermissionsHandler } from './wallet-requestPermissions'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { - CaveatTypes, - EndowmentTypes, - RestrictedMethods, -} from '../constants/permissions'; - -const getBaseRequest = (overrides = {}) => ({ - jsonrpc: '2.0' as const, - id: 0, - method: 'wallet_requestPermissions', - networkClientId: 'mainnet', - origin: 'http://test.com', - params: [ - { - eth_accounts: {}, - }, - ], - ...overrides, -}); - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const requestPermissionsForOrigin = jest - .fn() - .mockResolvedValue([{ [Caip25EndowmentPermissionName]: {} }]); - const getAccounts = jest.fn().mockReturnValue([]); - const getCaip25PermissionFromLegacyPermissionsForOrigin = jest - .fn() - .mockReturnValue({}); - - const response: PendingJsonRpcResponse = { - jsonrpc: '2.0' as const, - id: 0, - }; - const handler = (request: unknown) => - requestPermissionsHandler.implementation( - request as JsonRpcRequest<[RequestedPermissions]> & { origin: string }, - response, - next, - end, - { - getAccounts, - requestPermissionsForOrigin, - getCaip25PermissionFromLegacyPermissionsForOrigin, - }, - ); - - return { - response, - next, - end, - getAccounts, - requestPermissionsForOrigin, - getCaip25PermissionFromLegacyPermissionsForOrigin, - handler, - }; -}; - -describe('requestPermissionsHandler', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it('returns an error if params is malformed', async () => { - const { handler, end } = createMockedHandler(); - - const malformedRequest = getBaseRequest({ params: [] }); - await handler(malformedRequest); - expect(end).toHaveBeenCalledWith( - invalidParams({ data: { request: malformedRequest } }), - ); - }); - - describe('only other permissions (non CAIP-25 equivalent) requested', () => { - it('requests the permission for the other permissions', async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - - await handler( - getBaseRequest({ - params: [ - { - otherPermissionA: {}, - otherPermissionB: {}, - }, - ], - }), - ); - - expect(requestPermissionsForOrigin).toHaveBeenCalledWith({ - otherPermissionA: {}, - otherPermissionB: {}, - }); - }); - - it('returns the other permissions that are granted', async () => { - const { handler, requestPermissionsForOrigin, response } = - createMockedHandler(); - - requestPermissionsForOrigin.mockResolvedValue([ - { - otherPermissionA: { foo: 'bar' }, - otherPermissionB: { hello: true }, - }, - ]); - - await handler( - getBaseRequest({ - params: [ - { - otherPermissionA: {}, - otherPermissionB: {}, - }, - ], - }), - ); - - expect(response.result).toStrictEqual([{ foo: 'bar' }, { hello: true }]); - }); - }); - - describe('only CAIP-25 "endowment:caip25" permissions requested', () => { - it('should call "requestPermissionsForOrigin" hook with empty object', async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - - await handler( - getBaseRequest({ - params: [ - { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:5': { accounts: ['eip155:5:0xdead'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }, - ], - }), - ); - - expect(requestPermissionsForOrigin).toHaveBeenCalledWith({}); - }); - }); - - describe('only CAIP-25 equivalent permissions ("eth_accounts" and/or "endowment:permittedChains") requested', () => { - it('requests the CAIP-25 permission using eth_accounts when only eth_accounts is specified in params', async () => { - const mockedRequestedPermissions = { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'wallet:eip155': { accounts: ['wallet:eip155:foo'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }; - - const { - handler, - getCaip25PermissionFromLegacyPermissionsForOrigin, - requestPermissionsForOrigin, - getAccounts, - } = createMockedHandler(); - getCaip25PermissionFromLegacyPermissionsForOrigin.mockReturnValue( - mockedRequestedPermissions, - ); - requestPermissionsForOrigin.mockResolvedValue([ - mockedRequestedPermissions, - ]); - getAccounts.mockReturnValue(['foo']); - - await handler( - getBaseRequest({ - params: [ - { - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - }, - ], - }), - ); - - expect( - getCaip25PermissionFromLegacyPermissionsForOrigin, - ).toHaveBeenCalledWith({ - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - }); - }); - - it('requests the CAIP-25 permission for permittedChains when only permittedChains is specified in params', async () => { - const mockedRequestedPermissions = { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:100': { accounts: [] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }; - - const { - handler, - requestPermissionsForOrigin, - getCaip25PermissionFromLegacyPermissionsForOrigin, - } = createMockedHandler(); - - getCaip25PermissionFromLegacyPermissionsForOrigin.mockReturnValue( - mockedRequestedPermissions, - ); - requestPermissionsForOrigin.mockResolvedValue([ - mockedRequestedPermissions, - ]); - - await handler( - getBaseRequest({ - params: [ - { - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }, - ], - }), - ); - - expect( - getCaip25PermissionFromLegacyPermissionsForOrigin, - ).toHaveBeenCalledWith({ - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }); - }); - - it('requests the CAIP-25 permission for eth_accounts and permittedChains when both are specified in params', async () => { - const mockedRequestedPermissions = { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:100': { accounts: ['bar'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }; - - const { - handler, - requestPermissionsForOrigin, - getAccounts, - getCaip25PermissionFromLegacyPermissionsForOrigin, - } = createMockedHandler(); - - requestPermissionsForOrigin.mockResolvedValue([ - mockedRequestedPermissions, - ]); - getAccounts.mockReturnValue(['bar']); - getCaip25PermissionFromLegacyPermissionsForOrigin.mockReturnValue( - mockedRequestedPermissions, - ); - - await handler( - getBaseRequest({ - params: [ - { - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }, - ], - }), - ); - - expect( - getCaip25PermissionFromLegacyPermissionsForOrigin, - ).toHaveBeenCalledWith({ - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }); - }); - }); - - describe('CAIP-25 equivalent permissions ("eth_accounts" and/or "endowment:permittedChains") alongside "endowment:caip25" requested', () => { - it('requests the CAIP-25 permission only for eth_accounts and permittedChains when both are specified in params (ignores "endowment:caip25")', async () => { - const mockedRequestedPermissions = { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:100': { accounts: ['bar'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }; - - const { - handler, - requestPermissionsForOrigin, - getAccounts, - getCaip25PermissionFromLegacyPermissionsForOrigin, - } = createMockedHandler(); - - requestPermissionsForOrigin.mockResolvedValue([ - mockedRequestedPermissions, - ]); - getAccounts.mockReturnValue(['bar']); - getCaip25PermissionFromLegacyPermissionsForOrigin.mockReturnValue( - mockedRequestedPermissions, - ); - - await handler( - getBaseRequest({ - params: [ - { - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:5': { accounts: ['eip155:5:0xdead'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }, - ], - }), - ); - - expect( - getCaip25PermissionFromLegacyPermissionsForOrigin, - ).toHaveBeenCalledWith({ - [RestrictedMethods.EthAccounts]: { - foo: 'bar', - }, - [EndowmentTypes.PermittedChains]: { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x64'], - }, - ], - }, - }); - }); - }); - - describe('both CAIP-25 equivalent and other permissions requested', () => { - describe('both CAIP-25 equivalent permissions and other permissions are approved', () => { - it('returns eth_accounts, permittedChains, and other permissions that were granted', async () => { - const mockedRequestedPermissions = { - otherPermissionA: { foo: 'bar' }, - otherPermissionB: { hello: true }, - [Caip25EndowmentPermissionName]: { - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: { - 'eip155:1': { accounts: ['eip155:1:0xdeadbeef'] }, - 'eip155:5': { accounts: ['eip155:5:0xdeadbeef'] }, - }, - isMultichainOrigin: false, - }, - }, - ], - }, - }; - - const { - handler, - requestPermissionsForOrigin, - getAccounts, - getCaip25PermissionFromLegacyPermissionsForOrigin, - response, - } = createMockedHandler(); - - requestPermissionsForOrigin.mockResolvedValue([ - mockedRequestedPermissions, - ]); - - getAccounts.mockReturnValue(['0xdeadbeef']); - - getCaip25PermissionFromLegacyPermissionsForOrigin.mockReturnValue( - mockedRequestedPermissions, - ); - - await handler( - getBaseRequest({ - params: [ - { - eth_accounts: {}, - 'endowment:permitted-chains': {}, - otherPermissionA: {}, - otherPermissionB: {}, - }, - ], - }), - ); - expect(response.result).toStrictEqual([ - { foo: 'bar' }, - { hello: true }, - { - caveats: [ - { - type: CaveatTypes.RestrictReturnedAccounts, - value: ['0xdeadbeef'], - }, - ], - parentCapability: RestrictedMethods.EthAccounts, - }, - { - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ['0x1', '0x5'], - }, - ], - parentCapability: EndowmentTypes.PermittedChains, - }, - ]); - }); - }); - - describe('CAIP-25 equivalent permissions are approved, but other permissions are not approved', () => { - it('returns an error that the other permissions were not approved', async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - requestPermissionsForOrigin.mockRejectedValue( - new Error('other permissions rejected'), - ); - - await expect( - handler( - getBaseRequest({ - params: [ - { - eth_accounts: {}, - 'endowment:permitted-chains': {}, - otherPermissionA: {}, - otherPermissionB: {}, - }, - ], - }), - ), - ).rejects.toThrow('other permissions rejected'); - }); - }); - }); - - describe('no permissions requested', () => { - it('returns an error by requesting empty permissions in params from the PermissionController if no permissions specified', async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - requestPermissionsForOrigin.mockRejectedValue( - new Error('failed to request unexpected permission'), - ); - - await expect( - handler( - getBaseRequest({ - params: [{}], - }), - ), - ).rejects.toThrow('failed to request unexpected permission'); - }); - - it("returns an error if requestPermissionsForOrigin hook doesn't return a valid CAIP-25 permission", async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - requestPermissionsForOrigin.mockResolvedValue([{ foo: 'bar' }]); - - await expect( - handler( - getBaseRequest({ - params: [{ eth_accounts: {}, 'endowment:permitted-chains': {} }], - }), - ), - ).rejects.toThrow( - `could not find ${Caip25EndowmentPermissionName} permission.`, - ); - }); - - it('returns an error if requestPermissionsForOrigin hook returns a an invalid CAIP-25 permission (with no CAIP-25 caveat value)', async () => { - const { handler, requestPermissionsForOrigin } = createMockedHandler(); - requestPermissionsForOrigin.mockResolvedValue([ - { - [Caip25EndowmentPermissionName]: { - caveats: [{ type: 'foo', value: 'bar' }], - }, - }, - ]); - - await expect( - handler( - getBaseRequest({ - params: [{ eth_accounts: {}, 'endowment:permitted-chains': {} }], - }), - ), - ).rejects.toThrow( - `could not find ${Caip25CaveatType} in granted ${Caip25EndowmentPermissionName} permission.`, - ); - }); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-requestPermissions.ts b/packages/multichain/src/handlers/wallet-requestPermissions.ts deleted file mode 100644 index 5803103c621..00000000000 --- a/packages/multichain/src/handlers/wallet-requestPermissions.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { isPlainObject } from '@metamask/controller-utils'; -import type { - AsyncJsonRpcEngineNextCallback, - JsonRpcEngineEndCallback, -} from '@metamask/json-rpc-engine'; -import { - type Caveat, - type CaveatSpecificationConstraint, - invalidParams, - MethodNames, - type PermissionController, - type PermissionSpecificationConstraint, - type RequestedPermissions, - type ValidPermission, -} from '@metamask/permission-controller'; -import type { - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; -import { pick } from 'lodash'; - -import { getPermittedEthChainIds } from '../adapters/caip-permission-adapter-permittedChains'; -import { - Caip25CaveatType, - type Caip25CaveatValue, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { - CaveatTypes, - EndowmentTypes, - RestrictedMethods, -} from '../constants/permissions'; - -export const requestPermissionsHandler = { - methodNames: [MethodNames.RequestPermissions], - implementation: requestPermissionsImplementation, - hookNames: { - getAccounts: true, - requestPermissionsForOrigin: true, - getCaip25PermissionFromLegacyPermissionsForOrigin: true, - }, -}; - -type AbstractPermissionController = PermissionController< - PermissionSpecificationConstraint, - CaveatSpecificationConstraint ->; - -type GrantedPermissions = Awaited< - ReturnType ->[0]; - -/** - * Request Permissions implementation to be used in JsonRpcEngine middleware, specifically for `wallet_requestPermissions` RPC method. - * The request object is expected to contain a CAIP-25 endowment permission. - * - * @param req - The JsonRpcEngine request - * @param res - The JsonRpcEngine result object - * @param _next - JsonRpcEngine next() callback - unused - * @param end - JsonRpcEngine end() callback - * @param options - Method hooks passed to the method implementation - * @param options.getAccounts - A hook that returns the permitted eth accounts for the origin sorted by lastSelected. - * @param options.getCaip25PermissionFromLegacyPermissionsForOrigin - A hook that returns a CAIP-25 permission from a legacy `eth_accounts` and `endowment:permitted-chains` permission. - * @param options.requestPermissionsForOrigin - A hook that requests CAIP-25 permissions for the origin. - * @returns A promise that resolves to nothing - */ -async function requestPermissionsImplementation( - req: JsonRpcRequest<[RequestedPermissions]> & { origin: string }, - res: PendingJsonRpcResponse, - _next: AsyncJsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - { - getAccounts, - requestPermissionsForOrigin, - getCaip25PermissionFromLegacyPermissionsForOrigin, - }: { - getAccounts: () => string[]; - requestPermissionsForOrigin: ( - requestedPermissions: RequestedPermissions, - ) => Promise<[GrantedPermissions]>; - getCaip25PermissionFromLegacyPermissionsForOrigin: ( - requestedPermissions?: RequestedPermissions, - ) => RequestedPermissions; - }, -) { - const { params } = req; - - if (!Array.isArray(params) || !isPlainObject(params[0])) { - return end(invalidParams({ data: { request: req } })); - } - - let [requestedPermissions] = params; - delete requestedPermissions[Caip25EndowmentPermissionName]; - - const caip25EquivalentPermissions: Partial< - Pick - > = pick(requestedPermissions, [ - RestrictedMethods.EthAccounts, - EndowmentTypes.PermittedChains, - ]); - delete requestedPermissions[RestrictedMethods.EthAccounts]; - delete requestedPermissions[EndowmentTypes.PermittedChains]; - - const hasCaip25EquivalentPermissions = - Object.keys(caip25EquivalentPermissions).length > 0; - - if (hasCaip25EquivalentPermissions) { - const caip25Permission = getCaip25PermissionFromLegacyPermissionsForOrigin( - caip25EquivalentPermissions, - ); - requestedPermissions = { ...requestedPermissions, ...caip25Permission }; - } - - let grantedPermissions: GrantedPermissions = {}; - - const [frozenGrantedPermissions] = - await requestPermissionsForOrigin(requestedPermissions); - - grantedPermissions = { ...frozenGrantedPermissions }; - - if (hasCaip25EquivalentPermissions) { - const caip25Endowment = grantedPermissions[Caip25EndowmentPermissionName]; - - if (!caip25Endowment) { - throw new Error( - `could not find ${Caip25EndowmentPermissionName} permission.`, - ); - } - - const caip25CaveatValue = caip25Endowment.caveats?.find( - ({ type }) => type === Caip25CaveatType, - )?.value as Caip25CaveatValue | undefined; - if (!caip25CaveatValue) { - throw new Error( - `could not find ${Caip25CaveatType} in granted ${Caip25EndowmentPermissionName} permission.`, - ); - } - - delete grantedPermissions[Caip25EndowmentPermissionName]; - // We cannot derive correct eth_accounts value directly from the CAIP-25 permission - // because the accounts will not be in order of lastSelected - const ethAccounts = getAccounts(); - - grantedPermissions[RestrictedMethods.EthAccounts] = { - ...caip25Endowment, - parentCapability: RestrictedMethods.EthAccounts, - caveats: [ - { - type: CaveatTypes.RestrictReturnedAccounts, - value: ethAccounts, - }, - ], - }; - - const ethChainIds = getPermittedEthChainIds(caip25CaveatValue); - - if (ethChainIds.length > 0) { - grantedPermissions[EndowmentTypes.PermittedChains] = { - ...caip25Endowment, - parentCapability: EndowmentTypes.PermittedChains, - caveats: [ - { - type: CaveatTypes.RestrictNetworkSwitching, - value: ethChainIds, - }, - ], - }; - } - } - - res.result = Object.values(grantedPermissions).filter( - ( - permission: ValidPermission> | undefined, - ): permission is ValidPermission> => - permission !== undefined, - ); - return end(); -} diff --git a/packages/multichain/src/handlers/wallet-revokePermissions.test.ts b/packages/multichain/src/handlers/wallet-revokePermissions.test.ts deleted file mode 100644 index e888cba23ca..00000000000 --- a/packages/multichain/src/handlers/wallet-revokePermissions.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { invalidParams } from '@metamask/permission-controller'; -import type { - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; - -import { revokePermissionsHandler } from './wallet-revokePermissions'; -import { Caip25EndowmentPermissionName } from '../caip25Permission'; -import { EndowmentTypes, RestrictedMethods } from '../constants/permissions'; - -const baseRequest = { - jsonrpc: '2.0' as const, - id: 0, - method: 'wallet_revokePermissions', - params: [ - { - [Caip25EndowmentPermissionName]: {}, - otherPermission: {}, - }, - ], -}; - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const revokePermissionsForOrigin = jest.fn(); - - const response: PendingJsonRpcResponse = { - jsonrpc: '2.0' as const, - id: 0, - }; - const handler = (request: JsonRpcRequest) => - revokePermissionsHandler.implementation(request, response, next, end, { - revokePermissionsForOrigin, - }); - - return { - response, - next, - end, - revokePermissionsForOrigin, - handler, - }; -}; - -describe('revokePermissionsHandler', () => { - it('returns an error if params is malformed', () => { - const { handler, end } = createMockedHandler(); - - const malformedRequest = { - ...baseRequest, - params: [], - }; - handler(malformedRequest); - expect(end).toHaveBeenCalledWith( - invalidParams({ data: { request: malformedRequest } }), - ); - }); - - it('returns an error if params are empty', () => { - const { handler, end } = createMockedHandler(); - - const emptyRequest = { - ...baseRequest, - params: [{}], - }; - handler(emptyRequest); - expect(end).toHaveBeenCalledWith( - invalidParams({ data: { request: emptyRequest } }), - ); - }); - - it('returns an error if params only contains the CAIP-25 permission', () => { - const { handler, end } = createMockedHandler(); - - const emptyRequest = { - ...baseRequest, - params: [ - { - [Caip25EndowmentPermissionName]: {}, - }, - ], - }; - handler(emptyRequest); - expect(end).toHaveBeenCalledWith( - invalidParams({ data: { request: emptyRequest } }), - ); - }); - - describe.each([ - [RestrictedMethods.EthAccounts], - [EndowmentTypes.PermittedChains], - ])('%s permission is specified', (permission: string) => { - it('revokes the CAIP-25 endowment permission', () => { - const { handler, revokePermissionsForOrigin } = createMockedHandler(); - - handler({ - ...baseRequest, - params: [ - { - [permission]: {}, - }, - ], - }); - expect(revokePermissionsForOrigin).toHaveBeenCalledWith([ - Caip25EndowmentPermissionName, - ]); - }); - - it('revokes other permissions specified', () => { - const { handler, revokePermissionsForOrigin } = createMockedHandler(); - - handler({ - ...baseRequest, - params: [ - { - [permission]: {}, - otherPermission: {}, - }, - ], - }); - expect(revokePermissionsForOrigin).toHaveBeenCalledWith([ - 'otherPermission', - Caip25EndowmentPermissionName, - ]); - }); - }); - - it('revokes permissions other than eth_accounts, permittedChains, CAIP-25 if specified', () => { - const { handler, revokePermissionsForOrigin } = createMockedHandler(); - - handler({ - ...baseRequest, - params: [ - { - [Caip25EndowmentPermissionName]: {}, - otherPermission: {}, - }, - ], - }); - expect(revokePermissionsForOrigin).toHaveBeenCalledWith([ - 'otherPermission', - ]); - }); - - it('returns null', () => { - const { handler, response } = createMockedHandler(); - - handler(baseRequest); - expect(response.result).toBeNull(); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-revokePermissions.ts b/packages/multichain/src/handlers/wallet-revokePermissions.ts deleted file mode 100644 index fc779a27329..00000000000 --- a/packages/multichain/src/handlers/wallet-revokePermissions.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { - AsyncJsonRpcEngineNextCallback, - JsonRpcEngineEndCallback, -} from '@metamask/json-rpc-engine'; -import { invalidParams, MethodNames } from '@metamask/permission-controller'; -import { - isNonEmptyArray, - type Json, - type JsonRpcRequest, - type PendingJsonRpcResponse, -} from '@metamask/utils'; - -import { Caip25EndowmentPermissionName } from '../caip25Permission'; -import { EndowmentTypes, RestrictedMethods } from '../constants/permissions'; - -export const revokePermissionsHandler = { - methodNames: [MethodNames.RevokePermissions], - implementation: revokePermissionsImplementation, - hookNames: { - revokePermissionsForOrigin: true, - updateCaveat: true, - }, -}; - -/** - * Revoke Permissions implementation to be used in JsonRpcEngine middleware. - * - * @param req - The JsonRpcEngine request - * @param res - The JsonRpcEngine result object - * @param _next - JsonRpcEngine next() callback - unused - * @param end - JsonRpcEngine end() callback - * @param options - Method hooks passed to the method implementation - * @param options.revokePermissionsForOrigin - A hook that revokes given permission keys for an origin - * @returns Nothing. - */ -function revokePermissionsImplementation( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - _next: AsyncJsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - { - revokePermissionsForOrigin, - }: { - revokePermissionsForOrigin: (permissionKeys: string[]) => void; - }, -) { - const { params } = req; - - const param = params?.[0]; - - if (!param) { - return end(invalidParams({ data: { request: req } })); - } - - // For now, this API revokes the entire permission key - // even if caveats are specified. - const permissionKeys = Object.keys(param).filter( - (name) => name !== Caip25EndowmentPermissionName, - ); - - if (!isNonEmptyArray(permissionKeys)) { - return end(invalidParams({ data: { request: req } })); - } - - const caip25EquivalentPermissions: string[] = [ - RestrictedMethods.EthAccounts, - EndowmentTypes.PermittedChains, - ]; - const relevantPermissionKeys = permissionKeys.filter( - (name: string) => !caip25EquivalentPermissions.includes(name), - ); - - const shouldRevokeLegacyPermission = - relevantPermissionKeys.length !== permissionKeys.length; - - if (shouldRevokeLegacyPermission) { - relevantPermissionKeys.push(Caip25EndowmentPermissionName); - } - - revokePermissionsForOrigin(relevantPermissionKeys); - - res.result = null; - - return end(); -} diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.ts b/packages/multichain/src/handlers/wallet-revokeSession.test.ts deleted file mode 100644 index c74c95a1f7c..00000000000 --- a/packages/multichain/src/handlers/wallet-revokeSession.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - PermissionDoesNotExistError, - UnrecognizedSubjectError, -} from '@metamask/permission-controller'; -import { rpcErrors } from '@metamask/rpc-errors'; -import type { JsonRpcRequest } from '@metamask/utils'; - -import { Caip25EndowmentPermissionName } from '../caip25Permission'; -import { walletRevokeSession } from './wallet-revokeSession'; - -const baseRequest: JsonRpcRequest & { origin: string } = { - origin: 'http://test.com', - params: {}, - jsonrpc: '2.0' as const, - id: 1, - method: 'wallet_revokeSession', -}; - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const revokePermissionForOrigin = jest.fn(); - const response = { - result: true, - id: 1, - jsonrpc: '2.0' as const, - }; - const handler = (request: JsonRpcRequest & { origin: string }) => - walletRevokeSession.implementation(request, response, next, end, { - revokePermissionForOrigin, - }); - - return { - next, - response, - end, - revokePermissionForOrigin, - handler, - }; -}; - -describe('wallet_revokeSession', () => { - it('revokes the the CAIP-25 endowment permission', async () => { - const { handler, revokePermissionForOrigin } = createMockedHandler(); - - await handler(baseRequest); - expect(revokePermissionForOrigin).toHaveBeenCalledWith( - Caip25EndowmentPermissionName, - ); - }); - - it('returns true if the CAIP-25 endowment permission does not exist', async () => { - const { handler, response, revokePermissionForOrigin } = createMockedHandler(); - revokePermissionForOrigin.mockImplementation(() => { - throw new PermissionDoesNotExistError( - 'foo.com', - Caip25EndowmentPermissionName, - ); - }); - - await handler(baseRequest); - expect(response.result).toBe(true); - }); - - it('returns true if the subject does not exist', async () => { - const { handler, response, revokePermissionForOrigin } = createMockedHandler(); - revokePermissionForOrigin.mockImplementation(() => { - throw new UnrecognizedSubjectError('foo.com'); - }); - - await handler(baseRequest); - expect(response.result).toBe(true); - }); - - it('throws an internal RPC error if something unexpected goes wrong with revoking the permission', async () => { - const { handler, revokePermissionForOrigin, end } = createMockedHandler(); - revokePermissionForOrigin.mockImplementation(() => { - throw new Error('revoke failed'); - }); - - await handler(baseRequest); - expect(end).toHaveBeenCalledWith(rpcErrors.internal()); - }); - - it('returns true if the permission was revoked', async () => { - const { handler, response } = createMockedHandler(); - - await handler(baseRequest); - expect(response.result).toBe(true); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-revokeSession.ts b/packages/multichain/src/handlers/wallet-revokeSession.ts deleted file mode 100644 index 963cd6432f4..00000000000 --- a/packages/multichain/src/handlers/wallet-revokeSession.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { - JsonRpcEngineNextCallback, - JsonRpcEngineEndCallback, -} from '@metamask/json-rpc-engine'; -import { - PermissionDoesNotExistError, - UnrecognizedSubjectError, -} from '@metamask/permission-controller'; -import { rpcErrors } from '@metamask/rpc-errors'; -import type { JsonRpcSuccess, JsonRpcRequest } from '@metamask/utils'; - -import { Caip25EndowmentPermissionName } from '../caip25Permission'; - -/** - * Handler for the `wallet_revokeSession` RPC method as specified by [CAIP-285](https://chainagnostic.org/CAIPs/caip-285). - * The implementation below deviates from the linked spec in that it ignores the `sessionId` param - * and instead revokes the singular session for the origin if available. Additionally, - * the handler also does not return an error if there is currently no active session and instead - * returns true which is the same result returned if an active session was actually revoked. - * - * @param request - The JSON-RPC request object. - * @param response - The JSON-RPC response object. - * @param _next - The next middleware function. Unused. - * @param end - The end callback function. - * @param hooks - The hooks object. - * @param hooks.revokePermissionForOrigin - The hook for revoking a permission for an origin function. - */ -async function walletRevokeSessionHandler( - request: JsonRpcRequest & { origin: string }, - response: JsonRpcSuccess, - _next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - hooks: { - revokePermissionForOrigin: (permissionName: string) => void; - }, -) { - try { - hooks.revokePermissionForOrigin(Caip25EndowmentPermissionName); - } catch (err) { - if ( - !(err instanceof UnrecognizedSubjectError) && - !(err instanceof PermissionDoesNotExistError) - ) { - console.error(err); - return end(rpcErrors.internal()); - } - } - - response.result = true; - return end(); -} -export const walletRevokeSession = { - methodNames: ['wallet_revokeSession'], - implementation: walletRevokeSessionHandler, - hookNames: { - revokePermissionForOrigin: true, - }, -}; diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts deleted file mode 100644 index 391bd480685..00000000000 --- a/packages/multichain/src/index.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as allExports from '.'; - -describe('@metamask/multichain', () => { - it('has expected JavaScript exports', () => { - expect(Object.keys(allExports)).toMatchInlineSnapshot(` - Array [ - "getEthAccounts", - "setEthAccounts", - "getPermittedEthChainIds", - "addPermittedEthChainId", - "setPermittedEthChainIds", - "getInternalScopesObject", - "getSessionScopes", - "getPermissionsHandler", - "requestPermissionsHandler", - "revokePermissionsHandler", - "walletGetSession", - "walletInvokeMethod", - "walletRevokeSession", - "multichainMethodCallValidatorMiddleware", - "MultichainMiddlewareManager", - "MultichainSubscriptionManager", - "validateAndNormalizeScopes", - "bucketScopes", - "KnownWalletRpcMethods", - "KnownRpcMethods", - "KnownWalletNamespaceRpcMethods", - "KnownNotifications", - "KnownWalletScopeString", - "getSupportedScopeObjects", - "parseScopeString", - "normalizeScope", - "mergeScopeObject", - "mergeNormalizedScopes", - "mergeInternalScopes", - "normalizeAndMergeScopes", - "caip25CaveatBuilder", - "Caip25CaveatType", - "createCaip25Caveat", - "Caip25EndowmentPermissionName", - "caip25EndowmentBuilder", - "Caip25CaveatMutators", - ] - `); - }); -}); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts deleted file mode 100644 index eff0ee0b01e..00000000000 --- a/packages/multichain/src/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -export { - getEthAccounts, - setEthAccounts, -} from './adapters/caip-permission-adapter-eth-accounts'; -export { - getPermittedEthChainIds, - addPermittedEthChainId, - setPermittedEthChainIds, -} from './adapters/caip-permission-adapter-permittedChains'; -export { - getInternalScopesObject, - getSessionScopes, -} from './adapters/caip-permission-adapter-session-scopes'; - -export { getPermissionsHandler } from './handlers/wallet-getPermissions'; -export { requestPermissionsHandler } from './handlers/wallet-requestPermissions'; -export { revokePermissionsHandler } from './handlers/wallet-revokePermissions'; - -export { walletGetSession } from './handlers/wallet-getSession'; -export { walletInvokeMethod } from './handlers/wallet-invokeMethod'; -export { walletRevokeSession } from './handlers/wallet-revokeSession'; - -export { multichainMethodCallValidatorMiddleware } from './middlewares/multichainMethodCallValidator'; -export { MultichainMiddlewareManager } from './middlewares/MultichainMiddlewareManager'; -export { MultichainSubscriptionManager } from './middlewares/MultichainSubscriptionManager'; - -export type { Caip25Authorization } from './scope/authorization'; -export { - validateAndNormalizeScopes, - bucketScopes, -} from './scope/authorization'; -export { - KnownWalletRpcMethods, - KnownRpcMethods, - KnownWalletNamespaceRpcMethods, - KnownNotifications, - KnownWalletScopeString, -} from './scope/constants'; -export { getSupportedScopeObjects } from './scope/filter'; -export type { - ExternalScopeString, - ExternalScopeObject, - ExternalScopesObject, - InternalScopeString, - InternalScopeObject, - InternalScopesObject, - NormalizedScopeObject, - NormalizedScopesObject, - ScopedProperties, - NonWalletKnownCaipNamespace, -} from './scope/types'; -export { parseScopeString } from './scope/types'; -export { - normalizeScope, - mergeScopeObject, - mergeNormalizedScopes, - mergeInternalScopes, - normalizeAndMergeScopes, -} from './scope/transform'; - -export type { Caip25CaveatValue } from './caip25Permission'; -export { - caip25CaveatBuilder, - Caip25CaveatType, - createCaip25Caveat, - Caip25EndowmentPermissionName, - caip25EndowmentBuilder, - Caip25CaveatMutators, -} from './caip25Permission'; diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts deleted file mode 100644 index afb57036e8c..00000000000 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { rpcErrors } from '@metamask/rpc-errors'; - -import type { ExtendedJsonRpcMiddleware } from './MultichainMiddlewareManager'; -import { MultichainMiddlewareManager } from './MultichainMiddlewareManager'; - -const scope = 'eip155:1'; -const origin = 'example.com'; -const tabId = 123; - -describe('MultichainMiddlewareManager', () => { - it('should add middleware and get called for the scope, origin, and tabId if request is "eth_subscribe', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_subscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).toHaveBeenCalledWith( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_subscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).not.toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should add middleware and get called for the scope, origin, and tabId if request is "eth_unsubscribe', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_unsubscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).toHaveBeenCalledWith( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_unsubscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).not.toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should add middleware and call next if called for the scope, origin, and tabId but request is not "eth_subscribe" or "eth_unsubscribe"', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled() - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('call next if no middleware exists for scope, origin, and tabId and request is not "eth_subscribe" or "eth_unsubscribe', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('return error if no middleware exists for scope, origin, and tabId and request is "eth_subscribe"', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_subscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).not.toHaveBeenCalled(); - expect(endSpy).toHaveBeenCalledWith(rpcErrors.methodNotFound()); - }); - - it('return error if no middleware exists for scope, origin, and tabId and request is "eth_unsubscribe"', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'eth_unsubscribe', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).not.toHaveBeenCalled(); - expect(endSpy).toHaveBeenCalledWith(rpcErrors.methodNotFound()); - }); - - it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed and the middleware has no destroy function', async () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - await middleware.destroy?.(); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed and the middleware destroy function resolves', async () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - // eslint-disable-next-line jest/prefer-spy-on - middlewareSpy.destroy = jest.fn().mockResolvedValue(undefined); - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - await middleware.destroy?.(); - - expect(middlewareSpy.destroy).toHaveBeenCalled(); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed and the middleware destroy function rejects', async () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - // eslint-disable-next-line jest/prefer-spy-on - middlewareSpy.destroy = jest - .fn() - .mockRejectedValue( - new Error('failed to destroy the actual underlying middleware'), - ); - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - await middleware.destroy?.(); - - expect(middlewareSpy.destroy).toHaveBeenCalled(); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by scope', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - multichainMiddlewareManager.removeMiddlewareByScope(scope); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by scope and origin', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - multichainMiddlewareManager.removeMiddlewareByScopeAndOrigin(scope, origin); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by origin and tabId', () => { - const multichainMiddlewareManager = new MultichainMiddlewareManager(); - const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; - multichainMiddlewareManager.addMiddleware({ - scope, - origin, - tabId, - middleware: middlewareSpy, - }); - - multichainMiddlewareManager.removeMiddlewareByOriginAndTabId(origin, tabId); - - const middleware = - multichainMiddlewareManager.generateMultichainMiddlewareForOriginAndTabId( - origin, - 123, - ); - - const nextSpy = jest.fn(); - const endSpy = jest.fn(); - - middleware( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(middlewareSpy).not.toHaveBeenCalled(); - expect(nextSpy).toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts deleted file mode 100644 index 2af37012d07..00000000000 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { - JsonRpcEngineEndCallback, - JsonRpcEngineNextCallback, -} from '@metamask/json-rpc-engine'; -import { rpcErrors } from '@metamask/rpc-errors'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - -import type { ExternalScopeString } from '../scope/types'; - -export type ExtendedJsonRpcMiddleware = { - ( - req: JsonRpcRequest & { scope: string }, - res: PendingJsonRpcResponse, - next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - ): void; - destroy?: () => void | Promise; -}; - -type MiddlewareKey = { - scope: ExternalScopeString; - origin: string; - tabId?: number; -}; -type MiddlewareEntry = MiddlewareKey & { - middleware: ExtendedJsonRpcMiddleware; -}; - -// Methods related to eth_subscriptions -const SubscriptionMethods = ['eth_subscribe', 'eth_unsubscribe']; - -/** - * A helper that facilates registering and calling of provided middleware instances - * in the RPC pipeline based on the incoming request's scope, origin, and tabId. - * The core purpose of this class is to enable and manage multichain subscriptions - * (i.e. eth_subscribe called accross different chains and domains). - * - * Note that only one middleware instance can be registered per scope, origin, tabId key. - */ -export class MultichainMiddlewareManager { - #middlewares: MiddlewareEntry[] = []; - - #getMiddlewareEntry({ - scope, - origin, - tabId, - }: MiddlewareKey): MiddlewareEntry | undefined { - return this.#middlewares.find((middlewareEntry) => { - return ( - middlewareEntry.scope === scope && - middlewareEntry.origin === origin && - middlewareEntry.tabId === tabId - ); - }); - } - - #removeMiddlewareEntry({ scope, origin, tabId }: MiddlewareEntry) { - this.#middlewares = this.#middlewares.filter((middlewareEntry) => { - return ( - middlewareEntry.scope !== scope || - middlewareEntry.origin !== origin || - middlewareEntry.tabId !== tabId - ); - }); - } - - addMiddleware(middlewareEntry: MiddlewareEntry) { - const { scope, origin, tabId } = middlewareEntry; - if (!this.#getMiddlewareEntry({ scope, origin, tabId })) { - this.#middlewares.push(middlewareEntry); - } - } - - #removeMiddleware(middlewareEntry: MiddlewareEntry) { - // When the destroy function on the middleware is async, - // we don't need to wait for it complete - Promise.resolve(middlewareEntry.middleware.destroy?.()).catch(() => { - // do nothing - }); - - this.#removeMiddlewareEntry(middlewareEntry); - } - - removeMiddlewareByScope(scope: ExternalScopeString) { - this.#middlewares.forEach((middlewareEntry) => { - if (middlewareEntry.scope === scope) { - this.#removeMiddleware(middlewareEntry); - } - }); - } - - removeMiddlewareByScopeAndOrigin(scope: ExternalScopeString, origin: string) { - this.#middlewares.forEach((middlewareEntry) => { - if ( - middlewareEntry.scope === scope && - middlewareEntry.origin === origin - ) { - this.#removeMiddleware(middlewareEntry); - } - }); - } - - removeMiddlewareByOriginAndTabId(origin: string, tabId?: number) { - this.#middlewares.forEach((middlewareEntry) => { - if ( - middlewareEntry.origin === origin && - middlewareEntry.tabId === tabId - ) { - this.#removeMiddleware(middlewareEntry); - } - }); - } - - generateMultichainMiddlewareForOriginAndTabId( - origin: string, - tabId?: number, - ) { - const middleware: ExtendedJsonRpcMiddleware = (req, res, next, end) => { - const { scope } = req; - const middlewareEntry = this.#getMiddlewareEntry({ - scope, - origin, - tabId, - }); - - if (SubscriptionMethods.includes(req.method)) { - if (middlewareEntry) { - middlewareEntry.middleware(req, res, next, end); - } else { - // TODO: Temporary safety guard to prevent requests with these methods - // from being forwarded to the RPC endpoint even though this scenario - // should not be possible. - return end(rpcErrors.methodNotFound()); - } - } else { - return next(); - } - return undefined; - }; - middleware.destroy = this.removeMiddlewareByOriginAndTabId.bind( - this, - origin, - tabId, - ); - - return middleware; - } -} diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts deleted file mode 100644 index 75c6d3df05f..00000000000 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; -import type SafeEventEmitter from '@metamask/safe-event-emitter'; - -import { MultichainSubscriptionManager } from './MultichainSubscriptionManager'; - -jest.mock('@metamask/eth-json-rpc-filters/subscriptionManager', () => - jest.fn(), -); -const MockCreateSubscriptionManager = jest.mocked(createSubscriptionManager); - -const newHeadsNotificationMock = { - method: 'eth_subscription', - params: { - result: { - difficulty: '0x15d9223a23aa', - extraData: '0xd983010305844765746887676f312e342e328777696e646f7773', - gasLimit: '0x47e7c4', - gasUsed: '0x38658', - logsBloom: - '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - miner: '0xf8b483dba2c3b7176a3da549ad41a48bb3121069', - nonce: '0x084149998194cc5f', - number: '0x1348c9', - parentHash: - '0x7736fab79e05dc611604d22470dadad26f56fe494421b5b333de816ce1f25701', - receiptRoot: - '0x2fab35823ad00c7bb388595cb46652fe7886e00660a01e867824d3dceb1c8d36', - sha3Uncles: - '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - stateRoot: - '0xb3346685172db67de536d8765c43c31009d0eb3bd9c501c9be3229203f15f378', - timestamp: '0x56ffeff8', - }, - }, -}; - -const scope = 'eip155:1'; -const origin = 'example.com'; -const tabId = 123; - -const createMultichainSubscriptionManager = () => { - const mockFindNetworkClientIdByChainId = jest.fn(); - const mockGetNetworkClientById = jest.fn().mockImplementation(() => ({ - blockTracker: {}, - provider: {}, - })); - const multichainSubscriptionManager = new MultichainSubscriptionManager({ - findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, - getNetworkClientById: mockGetNetworkClientById, - }); - - return { multichainSubscriptionManager }; -}; - -const createMockSubscriptionManager = () => ({ - events: { - on: jest.fn(), - } as unknown as jest.Mocked, - destroy: jest.fn(), - middleware: { - destroy: jest.fn(), - }, -}); - -describe('MultichainSubscriptionManager', () => { - let mockSubscriptionManager = createMockSubscriptionManager(); - - beforeEach(() => { - mockSubscriptionManager = createMockSubscriptionManager(); - MockCreateSubscriptionManager.mockReturnValue(mockSubscriptionManager); - }); - - it('should not create a new subscriptionManager if one matches the passed in subscriptionKey', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - - const firstSubscription = multichainSubscriptionManager.subscribe({ - scope, - origin, - tabId, - }); - - const secondSubscription = multichainSubscriptionManager.subscribe({ - scope, - origin, - tabId, - }); - - expect(secondSubscription).toBe(firstSubscription); - expect(MockCreateSubscriptionManager).toHaveBeenCalledTimes(1); - }); - - it('should subscribe to a scope, origin, and tabId', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - const notifySpy = jest.fn(); - multichainSubscriptionManager.on('notification', notifySpy); - - mockSubscriptionManager.events.on.mock.calls[0][1]( - newHeadsNotificationMock, - ); - - expect(notifySpy).toHaveBeenCalledWith(origin, tabId, { - method: 'wallet_notify', - params: { - scope, - notification: newHeadsNotificationMock, - }, - }); - }); - - it('should unsubscribe from a scope', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - multichainSubscriptionManager.unsubscribeByScope(scope); - - expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); - }); - - it('should unsubscribe from a scope and origin', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - multichainSubscriptionManager.unsubscribeByScopeAndOrigin(scope, origin); - - expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); - }); - - it('should do nothing if an unsubscribe call does not match an existing subscription', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - multichainSubscriptionManager.unsubscribeByScope('eip155:10'); - multichainSubscriptionManager.unsubscribeByScopeAndOrigin( - scope, - 'other-origin', - ); - multichainSubscriptionManager.unsubscribeByOriginAndTabId( - 'other-origin', - 123, - ); - - expect(mockSubscriptionManager.destroy).not.toHaveBeenCalled(); - }); - - it('should unsubscribe from a origin and tabId', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); - - expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); - }); - - it('should unsubscribe when the middleware is destroyed', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - mockSubscriptionManager.middleware.destroy(); - - expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); - }); -}); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts deleted file mode 100644 index 9df0bb48518..00000000000 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { toHex } from '@metamask/controller-utils'; -import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; -import type { NetworkController } from '@metamask/network-controller'; -import SafeEventEmitter from '@metamask/safe-event-emitter'; -import type { CaipChainId, Hex } from '@metamask/utils'; -import { parseCaipChainId } from '@metamask/utils'; - -import type { ExternalScopeString } from '../scope/types'; -import type { ExtendedJsonRpcMiddleware } from './MultichainMiddlewareManager'; - -export type SubscriptionManager = { - events: SafeEventEmitter; - destroy?: () => void; - middleware: ExtendedJsonRpcMiddleware; -}; - -type SubscriptionNotificationEvent = { - jsonrpc: '2.0'; - method: 'eth_subscription'; - params: { - subscription: Hex; - result: unknown; - }; -}; - -type SubscriptionKey = { - scope: ExternalScopeString; - origin: string; - tabId?: number; -}; -type SubscriptionEntry = SubscriptionKey & { - subscriptionManager: SubscriptionManager; -}; - -type MultichainSubscriptionManagerOptions = { - findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; - getNetworkClientById: NetworkController['getNetworkClientById']; -}; - -/** - * A helper that facilates the lifecycle of a SubscriptionManager instance that - * is meant to handle subscriptons for only one specific scope, origin, and tabId combination. - */ -export class MultichainSubscriptionManager extends SafeEventEmitter { - #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; - - #getNetworkClientById: NetworkController['getNetworkClientById']; - - #subscriptions: SubscriptionEntry[] = []; - - /** - * Construct a MultichainSubscriptionManager. - * - * @param options - The controller options. - * @param options.findNetworkClientIdByChainId - The hook to get the networkClientId from a chainId. - * @param options.getNetworkClientById - The hook to get the network client instance by its networkClientId. - */ - constructor(options: MultichainSubscriptionManagerOptions) { - super(); - this.#findNetworkClientIdByChainId = options.findNetworkClientIdByChainId; - this.#getNetworkClientById = options.getNetworkClientById; - } - - notify( - { scope, origin, tabId }: SubscriptionKey, - { method, params }: SubscriptionNotificationEvent, - ) { - this.emit('notification', origin, tabId, { - method: 'wallet_notify', - params: { - scope, - notification: { method, params }, - }, - }); - } - - #getSubscriptionEntry({ - scope, - origin, - tabId, - }: SubscriptionKey): SubscriptionEntry | undefined { - return this.#subscriptions.find((subscriptionEntry) => { - return ( - subscriptionEntry.scope === scope && - subscriptionEntry.origin === origin && - subscriptionEntry.tabId === tabId - ); - }); - } - - #removeSubscriptionEntry({ scope, origin, tabId }: SubscriptionEntry) { - this.#subscriptions = this.#subscriptions.filter((subscriptionEntry) => { - return ( - subscriptionEntry.scope !== scope || - subscriptionEntry.origin !== origin || - subscriptionEntry.tabId !== tabId - ); - }); - } - - subscribe(subscriptionKey: SubscriptionKey) { - const subscriptionEntry = this.#getSubscriptionEntry(subscriptionKey); - if (subscriptionEntry) { - return subscriptionEntry.subscriptionManager; - } - - const networkClientId = this.#findNetworkClientIdByChainId( - toHex(parseCaipChainId(subscriptionKey.scope as CaipChainId).reference), - ); - const networkClient = this.#getNetworkClientById(networkClientId); - const subscriptionManager = createSubscriptionManager({ - blockTracker: networkClient.blockTracker, - provider: networkClient.provider, - }); - - subscriptionManager.events.on( - 'notification', - (message: SubscriptionNotificationEvent) => { - this.notify(subscriptionKey, message); - }, - ); - - const newSubscriptionManagerEntry = { - ...subscriptionKey, - subscriptionManager, - }; - subscriptionManager.destroy = subscriptionManager.middleware.destroy; - subscriptionManager.middleware.destroy = this.#unsubscribe.bind( - this, - newSubscriptionManagerEntry, - ); - - this.#subscriptions.push(newSubscriptionManagerEntry); - - return subscriptionManager; - } - - #unsubscribe(subscriptionEntry: SubscriptionEntry) { - subscriptionEntry.subscriptionManager.destroy?.(); - - this.#removeSubscriptionEntry(subscriptionEntry); - } - - unsubscribeByScope(scope: ExternalScopeString) { - this.#subscriptions.forEach((subscriptionEntry) => { - if (subscriptionEntry.scope === scope) { - this.#unsubscribe(subscriptionEntry); - } - }); - } - - unsubscribeByScopeAndOrigin(scope: ExternalScopeString, origin: string) { - this.#subscriptions.forEach((subscriptionEntry) => { - if ( - subscriptionEntry.scope === scope && - subscriptionEntry.origin === origin - ) { - this.#unsubscribe(subscriptionEntry); - } - }); - } - - unsubscribeByOriginAndTabId(origin: string, tabId?: number) { - this.#subscriptions.forEach((subscriptionEntry) => { - if ( - subscriptionEntry.origin === origin && - subscriptionEntry.tabId === tabId - ) { - this.#unsubscribe(subscriptionEntry); - } - }); - } -} diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts deleted file mode 100644 index 3ba8bd4b4a2..00000000000 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts +++ /dev/null @@ -1,474 +0,0 @@ -import type { - JsonRpcError, - JsonRpcRequest, - JsonRpcResponse, -} from '@metamask/utils'; - -import { multichainMethodCallValidatorMiddleware } from './multichainMethodCallValidator'; - -describe('multichainMethodCallValidatorMiddleware', () => { - const mockNext = jest.fn(); - - describe('"wallet_invokeMethod" request', () => { - it('should pass validation and call next when passed a valid "wallet_invokeMethod" request', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'wallet_invokeMethod', - params: { - scope: 'test', - request: { - method: 'test_method', - params: { - test: 'test', - }, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - reject(error); - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - it('should throw an error when passed a "wallet_invokeMethod" request with no scope', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'wallet_invokeMethod', - params: { - request: { - method: 'test_method', - params: { - test: 'test', - }, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - const rpcError = error as JsonRpcError & { data: JsonRpcError[] }; - expect(rpcError.message).toBe('Invalid method parameter(s).'); - expect(rpcError.code).toBe(-32602); - expect(rpcError.data[0].data).toStrictEqual({ - got: undefined, - param: 'scope', - path: [], - schema: { - pattern: '[-a-z0-9]{3,8}(:[-_a-zA-Z0-9]{1,32})?', - type: 'string', - }, - }); - expect(rpcError.data[0].message).toBe( - 'scope is required, but is undefined', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - it('should throw an error for a "wallet_invokeMethod" request without a nested request object', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'wallet_invokeMethod', - params: { - scope: 'test', - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - const rpcError = error as JsonRpcError & { data: JsonRpcError[] }; - expect(rpcError.message).toBe('Invalid method parameter(s).'); - expect(rpcError.code).toBe(-32602); - expect(rpcError.data[0].data).toStrictEqual({ - got: undefined, - param: 'request', - path: [], - schema: { - properties: { - method: { - type: 'string', - }, - params: true, - }, - type: 'object', - }, - }); - expect(rpcError.data[0].message).toBe( - 'request is required, but is undefined', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - it('should throw an error for an invalidly formatted "wallet_invokeMethod" request', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'wallet_invokeMethod', - params: { - scope: 'test', - request: { - method: {}, // expected to be a string - params: { - test: 'test', - }, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - const rpcError = error as JsonRpcError & { data: JsonRpcError[] }; - expect(rpcError.message).toBe('Invalid method parameter(s).'); - expect(rpcError.code).toBe(-32602); - expect(rpcError.data[0].data).toStrictEqual({ - got: { - method: {}, - params: { - test: 'test', - }, - }, - param: 'request', - path: ['method'], - schema: { - type: 'string', - }, - }); - expect(rpcError.data[0].message).toBe( - 'request.method is not of a type(s) string', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - }); - - describe('"wallet_notify" request', () => { - it('should pass validation for a "wallet_notify" request and call next', async () => { - const request: JsonRpcRequest = { - id: 2, - jsonrpc: '2.0', - method: 'wallet_notify', - params: { - scope: 'test_scope', - notification: { - method: 'test_method', - params: { - data: { - key: 'value', - }, - }, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - reject(error); - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - - it('should throw an error for a "wallet_notify" request with invalid params', async () => { - const request: JsonRpcRequest = { - id: 2, - jsonrpc: '2.0', - method: 'wallet_notify', - params: { - scope: 'test_scope', - request: { - data: {}, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - const rpcError = error as JsonRpcError & { data: JsonRpcError[] }; - expect(rpcError.message).toBe('Invalid method parameter(s).'); - expect(rpcError.code).toBe(-32602); - expect(rpcError.data[0].data).toStrictEqual({ - got: undefined, - param: 'notification', - path: [], - schema: { - properties: { - method: { - type: 'string', - }, - params: true, - }, - type: 'object', - }, - }); - expect(rpcError.data[0].message).toBe( - 'notification is required, but is undefined', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - }); - - describe('"wallet_revokeSession" request', () => { - it('should pass validation and call next when passed a valid "wallet_revokeSession" request', async () => { - const request: JsonRpcRequest = { - id: 3, - jsonrpc: '2.0', - method: 'wallet_revokeSession', - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - reject(error); - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - }); - - describe('"wallet_getSession" request', () => { - it('should pass validation and call next when passed a valid "wallet_getSession" request', async () => { - const request: JsonRpcRequest = { - id: 5, - jsonrpc: '2.0', - method: 'wallet_getSession', - params: {}, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - reject(error); - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - }); - - it('should throw an error if the top level params are not an object', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'wallet_invokeMethod', - params: ['test'], - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - expect(error).toBeDefined(); - expect((error as JsonRpcError).code).toBe(-32602); - expect((error as JsonRpcError).message).toBe( - 'Invalid method parameter(s).', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); - - it('should throw an error when passed an unknown method at the top level', async () => { - const request: JsonRpcRequest = { - id: 1, - jsonrpc: '2.0', - method: 'unknown_method', - params: { - request: { - method: 'test_method', - params: { - test: 'test', - }, - }, - }, - }; - const response = {} as JsonRpcResponse; - - await new Promise((resolve, reject) => { - multichainMethodCallValidatorMiddleware( - request, - response, - mockNext, - (error) => { - try { - const rpcError = error as JsonRpcError & { data: JsonRpcError[] }; - expect(rpcError.message).toBe('Invalid method parameter(s).'); - expect(rpcError.code).toBe(-32602); - expect(rpcError.data[0].data).toStrictEqual({ - method: 'unknown_method', - }); - expect(rpcError.data[0].message).toBe( - 'The method does not exist / is not available.', - ); - resolve(); - } catch (e) { - reject(e); - } - }, - ); - - process.nextTick(() => { - try { - expect(mockNext).not.toHaveBeenCalled(); - resolve(); - } catch (error) { - reject(error); - } - }); - }); - }); -}); diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts deleted file mode 100644 index 77977930849..00000000000 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { MultiChainOpenRPCDocument } from '@metamask/api-specs'; -import { createAsyncMiddleware } from '@metamask/json-rpc-engine'; -import { rpcErrors } from '@metamask/rpc-errors'; -import { isObject } from '@metamask/utils'; -import type { JsonRpcError, JsonRpcParams } from '@metamask/utils'; -import type { - ContentDescriptorObject, - MethodObject, - OpenrpcDocument, - ReferenceObject, -} from '@open-rpc/meta-schema'; -import dereferenceDocument from '@open-rpc/schema-utils-js/build/dereference-document'; -import { makeCustomResolver } from '@open-rpc/schema-utils-js/build/parse-open-rpc-document'; -import type { Schema, ValidationError } from 'jsonschema'; -import { Validator } from 'jsonschema'; - -const transformError = ( - error: ValidationError, - param: ContentDescriptorObject, - got: unknown, -) => { - // if there is a path, add it to the message - const message = `${param.name}${ - error.path.length > 0 ? `.${error.path.join('.')}` : '' - } ${error.message}`; - - return rpcErrors.invalidParams({ - message, - data: { - param: param.name, - path: error.path, - schema: error.schema, - got, - }, - }); -}; - -const v = new Validator(); - -const dereffedPromise = dereferenceDocument( - MultiChainOpenRPCDocument as unknown as OpenrpcDocument, - makeCustomResolver({}), -); - -/** - * Helper that utilizes the Multichain method specifications from `@metamask/api-specs` - * to validate the params of a Multichain request. - * - * @param method - The request's method. - * @param params - The request's optional JsonRpcParams object. - * @returns an array of error objects for each validation error or an empty array if no errors. - */ -const multichainMethodCallValidator = async ( - method: string, - params: JsonRpcParams | undefined, -) => { - const dereffed = await dereffedPromise; - - const methodToCheck = dereffed.methods.find( - (m: MethodObject | ReferenceObject) => (m as MethodObject).name === method, - ) as MethodObject | undefined; - - if ( - !methodToCheck || - !isObject(methodToCheck) || - !('params' in methodToCheck) - ) { - return [rpcErrors.methodNotFound({ data: { method } })] as JsonRpcError[]; - } - - const errors: JsonRpcError[] = []; - for (const param of methodToCheck.params) { - if (!isObject(params)) { - return [rpcErrors.invalidParams()] as JsonRpcError[]; - } - const p = param as ContentDescriptorObject; - const paramToCheck = params[p.name]; - - const result = v.validate(paramToCheck, p.schema as unknown as Schema, { - required: p.required, - }); - if (result.errors) { - errors.push( - ...result.errors.map((e) => { - return transformError(e, p, paramToCheck) as JsonRpcError; - }), - ); - } - } - return errors; -}; - -/** - * Middleware that validates the params of a Multichain method request - * using the specifications from `@metamask/api-specs`. - */ -export const multichainMethodCallValidatorMiddleware = createAsyncMiddleware( - async (request, _response, next) => { - const errors = await multichainMethodCallValidator( - request.method, - request.params, - ); - if (errors.length > 0) { - throw rpcErrors.invalidParams({ data: errors }); - } - return await next(); - }, -); diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts deleted file mode 100644 index 5197f472f03..00000000000 --- a/packages/multichain/src/scope/assert.test.ts +++ /dev/null @@ -1,627 +0,0 @@ -import * as Utils from '@metamask/utils'; - -import { - assertScopeSupported, - assertScopesSupported, - assertIsExternalScopesObject, - assertIsInternalScopesObject, - assertIsInternalScopeString, -} from './assert'; -import { Caip25Errors } from './errors'; -import * as Supported from './supported'; -import type { NormalizedScopeObject } from './types'; - -jest.mock('./supported', () => ({ - isSupportedScopeString: jest.fn(), - isSupportedNotification: jest.fn(), - isSupportedMethod: jest.fn(), -})); - -jest.mock('@metamask/utils', () => ({ - ...jest.requireActual('@metamask/utils'), - isCaipChainId: jest.fn(), - isCaipReference: jest.fn(), - isCaipAccountId: jest.fn(), -})); - -const MockSupported = jest.mocked(Supported); -const MockUtils = jest.mocked(Utils); - -const validScopeObject: NormalizedScopeObject = { - methods: [], - notifications: [], - accounts: [], -}; - -describe('Scope Assert', () => { - beforeEach(() => { - MockUtils.isCaipChainId.mockImplementation(() => true); - MockUtils.isCaipReference.mockImplementation(() => true); - MockUtils.isCaipAccountId.mockImplementation(() => true); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('assertScopeSupported', () => { - const isEvmChainIdSupported = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - const getNonEvmSupportedMethods = jest.fn(); - - describe('scopeString', () => { - it('checks if the scopeString is supported', () => { - try { - assertScopeSupported('scopeString', validScopeObject, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - } catch (err) { - // noop - } - expect(MockSupported.isSupportedScopeString).toHaveBeenCalledWith( - 'scopeString', - { isEvmChainIdSupported, isNonEvmScopeSupported }, - ); - }); - - it('throws an error if the scopeString is not supported', () => { - MockSupported.isSupportedScopeString.mockReturnValue(false); - expect(() => { - assertScopeSupported('scopeString', validScopeObject, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - }).toThrow(Caip25Errors.requestedChainsNotSupportedError()); - }); - }); - - describe('scopeObject', () => { - beforeEach(() => { - MockSupported.isSupportedScopeString.mockReturnValue(true); - }); - - it('checks if the methods are supported', () => { - try { - assertScopeSupported( - 'scopeString', - { - ...validScopeObject, - methods: ['eth_chainId'], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - } catch (err) { - // noop - } - - expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( - 'scopeString', - 'eth_chainId', - { - getNonEvmSupportedMethods, - }, - ); - }); - - it('throws an error if there are unsupported methods', () => { - MockSupported.isSupportedMethod.mockReturnValue(false); - expect(() => { - assertScopeSupported( - 'scopeString', - { - ...validScopeObject, - methods: ['eth_chainId'], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }).toThrow(Caip25Errors.requestedMethodsNotSupportedError()); - }); - - it('checks if the notifications are supported', () => { - MockSupported.isSupportedMethod.mockReturnValue(true); - try { - assertScopeSupported( - 'scopeString', - { - ...validScopeObject, - notifications: ['chainChanged'], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - } catch (err) { - // noop - } - - expect(MockSupported.isSupportedNotification).toHaveBeenCalledWith( - 'scopeString', - 'chainChanged', - ); - }); - - it('throws an error if there are unsupported notifications', () => { - MockSupported.isSupportedMethod.mockReturnValue(true); - MockSupported.isSupportedNotification.mockReturnValue(false); - expect(() => { - assertScopeSupported( - 'scopeString', - { - ...validScopeObject, - notifications: ['chainChanged'], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }).toThrow(Caip25Errors.requestedNotificationsNotSupportedError()); - }); - - it('does not throw if the scopeObject is valid', () => { - MockSupported.isSupportedMethod.mockReturnValue(true); - MockSupported.isSupportedNotification.mockReturnValue(true); - expect( - assertScopeSupported( - 'scopeString', - { - ...validScopeObject, - methods: ['eth_chainId'], - notifications: ['chainChanged'], - accounts: ['eip155:1:0xdeadbeef'], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ), - ).toBeUndefined(); - }); - }); - }); - - describe('assertScopesSupported', () => { - const isEvmChainIdSupported = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - const getNonEvmSupportedMethods = jest.fn(); - - it('does not throw an error if no scopes are defined', () => { - expect( - assertScopesSupported( - {}, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ), - ).toBeUndefined(); - }); - - it('throws an error if any scope is invalid', () => { - MockSupported.isSupportedScopeString.mockReturnValue(false); - - expect(() => { - assertScopesSupported( - { - 'eip155:1': validScopeObject, - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }).toThrow(Caip25Errors.requestedChainsNotSupportedError()); - }); - - it('does not throw an error if all scopes are valid', () => { - MockSupported.isSupportedScopeString.mockReturnValue(true); - - expect( - assertScopesSupported( - { - 'eip155:1': validScopeObject, - 'eip155:2': validScopeObject, - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ), - ).toBeUndefined(); - }); - }); - - describe('assertIsExternalScopesObject', () => { - it('does not throw if passed obj is a valid ExternalScopesObject with all valid properties', () => { - const obj = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => assertIsExternalScopesObject(obj)).not.toThrow(); - }); - - it('does not throw if passed obj is a valid ExternalScopesObject with some optional properties missing', () => { - const obj = { - accounts: ['eip155:1:0x1234'], - methods: ['method1'], - }; - expect(() => assertIsExternalScopesObject(obj)).not.toThrow(); - }); - - it('throws an error if passed obj is not an object', () => { - expect(() => assertIsExternalScopesObject(null)).toThrow( - 'ExternalScopesObject must be an object', - ); - expect(() => assertIsExternalScopesObject(123)).toThrow( - 'ExternalScopesObject must be an object', - ); - expect(() => assertIsExternalScopesObject('string')).toThrow( - 'ExternalScopesObject must be an object', - ); - }); - - it('throws and error if passed an object with an ExternalScopeObject value that is not an object', () => { - expect(() => assertIsExternalScopesObject({ 'eip155:1': 123 })).toThrow( - 'ExternalScopeObject must be an object', - ); - }); - - it('throws an error if passed an object with a key that is not a valid ExternalScopeString', () => { - MockUtils.isCaipChainId.mockReturnValue(false); - - expect(() => - assertIsExternalScopesObject({ 'invalid-scope-string': {} }), - ).toThrow('scopeString is not a valid ExternalScopeString'); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a references property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: 'not-an-array', - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.references must be an array of CaipReference', - ); - }); - - it('throws an error if references contains invalid CaipReference', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['invalidRef'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - jest - .spyOn(Utils, 'isCaipReference') - .mockImplementation((ref) => ref !== 'invalidRef'); - - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.references must be an array of CaipReference', - ); - }); - - it('throws an error if passed an object with an ExternalScopeObject with an accounts property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: 'not-an-array', - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.accounts must be an array of CaipAccountId', - ); - }); - - it('throws an error if accounts contains invalid CaipAccountId', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234', 'invalidAccount'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - MockUtils.isCaipAccountId.mockImplementation( - (id) => id !== 'invalidAccount', - ); - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.accounts must be an array of CaipAccountId', - ); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a methods property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: 'not-an-array', - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.methods must be an array of strings'); - }); - - it('throws an error if methods contains non-string elements', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 123], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.methods must be an array of strings'); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a notifications property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: 'not-an-array', - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.notifications must be an array of strings', - ); - }); - - it('throws an error if notifications contains non-string elements', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1', false], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow( - 'ExternalScopeObject.notifications must be an array of strings', - ); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a rpcDocuments property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: 'not-an-array', - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.rpcDocuments must be an array of strings'); - }); - - it('throws an error if rpcDocuments contains non-string elements', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1', 456], - rpcEndpoints: ['endpoint1'], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.rpcDocuments must be an array of strings'); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a rpcEndpoints property that is not an array', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: 'not-an-array', - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.rpcEndpoints must be an array of strings'); - }); - - it('throws an error if passed an object with an ExternalScopeObject with a rpcEndpoints property that contains non-string elements', () => { - const invalidExternalScopeObject = { - 'eip155:1': { - references: ['reference1', 'reference2'], - accounts: ['eip155:1:0x1234'], - methods: ['method1', 'method2'], - notifications: ['notification1'], - rpcDocuments: ['doc1'], - rpcEndpoints: ['endpoint1', null], - }, - }; - expect(() => - assertIsExternalScopesObject(invalidExternalScopeObject), - ).toThrow('ExternalScopeObject.rpcEndpoints must be an array of strings'); - }); - }); - - describe('assertIsInternalScopeString', () => { - it('throws an error if the value is not a string', () => { - expect(() => assertIsInternalScopeString({})).toThrow( - 'scopeString is not a valid InternalScopeString', - ); - expect(() => assertIsInternalScopeString(123)).toThrow( - 'scopeString is not a valid InternalScopeString', - ); - expect(() => assertIsInternalScopeString(undefined)).toThrow( - 'scopeString is not a valid InternalScopeString', - ); - expect(() => assertIsInternalScopeString(null)).toThrow( - 'scopeString is not a valid InternalScopeString', - ); - }); - - it("does not throw an error if the value is 'wallet'", () => { - expect(assertIsInternalScopeString('wallet')).toBeUndefined(); - expect(MockUtils.isCaipChainId).not.toHaveBeenCalled(); - }); - - it('does not throw an error if the value is a valid CAIP-2 Chain ID', () => { - MockUtils.isCaipChainId.mockReturnValue(true); - - expect(assertIsInternalScopeString('scopeString')).toBeUndefined(); - expect(MockUtils.isCaipChainId).toHaveBeenCalledWith('scopeString'); - }); - - it('throws an error if the value is not a valid CAIP-2 Chain ID', () => { - MockUtils.isCaipChainId.mockReturnValue(false); - - expect(() => assertIsInternalScopeString('scopeString')).toThrow( - 'scopeString is not a valid InternalScopeString', - ); - expect(MockUtils.isCaipChainId).toHaveBeenCalledWith('scopeString'); - }); - }); - - describe('assertIsInternalScopesObject', () => { - it('does not throw if passed obj is a valid InternalScopesObject with all valid properties', () => { - const obj = { - 'eip155:1': { - accounts: ['eip155:1:0x1234'], - }, - }; - expect(() => assertIsInternalScopesObject(obj)).not.toThrow(); - }); - - it('throws an error if passed obj is not an object', () => { - expect(() => assertIsInternalScopesObject(null)).toThrow( - 'InternalScopesObject must be an object', - ); - expect(() => assertIsInternalScopesObject(123)).toThrow( - 'InternalScopesObject must be an object', - ); - expect(() => assertIsInternalScopesObject('string')).toThrow( - 'InternalScopesObject must be an object', - ); - }); - - it('throws an error if passed an object with an InternalScopeObject value that is not an object', () => { - expect(() => assertIsInternalScopesObject({ 'eip155:1': 123 })).toThrow( - 'InternalScopeObject must be an object', - ); - }); - - it('throws an error if passed an object with a key that is not a valid InternalScopeString', () => { - MockUtils.isCaipChainId.mockReturnValue(false); - - expect(() => - assertIsInternalScopesObject({ 'invalid-scope-string': {} }), - ).toThrow('scopeString is not a valid InternalScopeString'); - }); - - it('throws an error if passed an object with an InternalScopeObject without an accounts property', () => { - const invalidInternalScopeObject = { - 'eip155:1': {}, - }; - expect(() => - assertIsInternalScopesObject(invalidInternalScopeObject), - ).toThrow( - 'InternalScopeObject.accounts must be an array of CaipAccountId', - ); - }); - - it('throws an error if passed an object with an InternalScopeObject with an accounts property that is not an array', () => { - const invalidInternalScopeObject = { - 'eip155:1': { - accounts: 'not-an-array', - }, - }; - expect(() => - assertIsInternalScopesObject(invalidInternalScopeObject), - ).toThrow( - 'InternalScopeObject.accounts must be an array of CaipAccountId', - ); - }); - - it('throws an error if accounts contains invalid CaipAccountId', () => { - const invalidInternalScopeObject = { - 'eip155:1': { - accounts: ['eip155:1:0x1234', 'invalidAccount'], - }, - }; - MockUtils.isCaipAccountId.mockImplementation( - (id) => id !== 'invalidAccount', - ); - expect(() => - assertIsInternalScopesObject(invalidInternalScopeObject), - ).toThrow( - 'InternalScopeObject.accounts must be an array of CaipAccountId', - ); - }); - }); -}); diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts deleted file mode 100644 index 69edf4cc028..00000000000 --- a/packages/multichain/src/scope/assert.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { - type CaipChainId, - hasProperty, - isCaipAccountId, - isCaipChainId, - isCaipNamespace, - isCaipReference, - KnownCaipNamespace, - type Hex, -} from '@metamask/utils'; - -import { Caip25Errors } from './errors'; -import { - isSupportedMethod, - isSupportedNotification, - isSupportedScopeString, -} from './supported'; -import type { - ExternalScopeObject, - ExternalScopesObject, - ExternalScopeString, - InternalScopeObject, - InternalScopesObject, - InternalScopeString, - NormalizedScopeObject, - NormalizedScopesObject, -} from './types'; - -/** - * Asserts that a scope string and its associated scope object are supported. - * - * @param scopeString - The scope string against which to assert support. - * @param scopeObject - The scope object against which to assert support. - * @param hooks - An object containing the following properties: - * @param hooks.isEvmChainIdSupported - A predicate that determines if an EVM chainID is supported. - * @param hooks.isNonEvmScopeSupported - A predicate that determines if an non EVM scopeString is supported. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - */ -export const assertScopeSupported = ( - scopeString: string, - scopeObject: NormalizedScopeObject, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }: { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - const { methods, notifications } = scopeObject; - if ( - !isSupportedScopeString(scopeString, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }) - ) { - throw Caip25Errors.requestedChainsNotSupportedError(); - } - - const allMethodsSupported = methods.every((method) => - isSupportedMethod(scopeString, method, { getNonEvmSupportedMethods }), - ); - - if (!allMethodsSupported) { - throw Caip25Errors.requestedMethodsNotSupportedError(); - } - - if ( - notifications && - !notifications.every((notification) => - isSupportedNotification(scopeString, notification), - ) - ) { - throw Caip25Errors.requestedNotificationsNotSupportedError(); - } -}; - -/** - * Asserts that all scope strings and their associated scope objects are supported. - * - * @param scopes - The scopes object against which to assert support. - * @param hooks - An object containing the following properties: - * @param hooks.isEvmChainIdSupported - A predicate that determines if an EVM chainID is supported. - * @param hooks.isNonEvmScopeSupported - A predicate that determines if an non EVM scopeString is supported. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - */ -export const assertScopesSupported = ( - scopes: NormalizedScopesObject, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }: { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - for (const [scopeString, scopeObject] of Object.entries(scopes)) { - assertScopeSupported(scopeString, scopeObject, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - } -}; -/** - * Asserts that an object is a valid ExternalScopeObject. - * - * @param obj - The object to assert. - */ -function assertIsExternalScopeObject( - obj: unknown, -): asserts obj is ExternalScopeObject { - if (typeof obj !== 'object' || obj === null) { - throw new Error('ExternalScopeObject must be an object'); - } - - if (hasProperty(obj, 'references')) { - if ( - !Array.isArray(obj.references) || - !obj.references.every(isCaipReference) - ) { - throw new Error( - 'ExternalScopeObject.references must be an array of CaipReference', - ); - } - } - - if (hasProperty(obj, 'accounts')) { - if (!Array.isArray(obj.accounts) || !obj.accounts.every(isCaipAccountId)) { - throw new Error( - 'ExternalScopeObject.accounts must be an array of CaipAccountId', - ); - } - } - - if (hasProperty(obj, 'methods')) { - if ( - !Array.isArray(obj.methods) || - !obj.methods.every((method) => typeof method === 'string') - ) { - throw new Error( - 'ExternalScopeObject.methods must be an array of strings', - ); - } - } - - if (hasProperty(obj, 'notifications')) { - if ( - !Array.isArray(obj.notifications) || - !obj.notifications.every( - (notification) => typeof notification === 'string', - ) - ) { - throw new Error( - 'ExternalScopeObject.notifications must be an array of strings', - ); - } - } - - if (hasProperty(obj, 'rpcDocuments')) { - if ( - !Array.isArray(obj.rpcDocuments) || - !obj.rpcDocuments.every((doc) => typeof doc === 'string') - ) { - throw new Error( - 'ExternalScopeObject.rpcDocuments must be an array of strings', - ); - } - } - - if (hasProperty(obj, 'rpcEndpoints')) { - if ( - !Array.isArray(obj.rpcEndpoints) || - !obj.rpcEndpoints.every((endpoint) => typeof endpoint === 'string') - ) { - throw new Error( - 'ExternalScopeObject.rpcEndpoints must be an array of strings', - ); - } - } -} - -/** - * Asserts that a scope string is a valid ExternalScopeString. - * - * @param scopeString - The scope string to assert. - */ -function assertIsExternalScopeString( - scopeString: unknown, -): asserts scopeString is ExternalScopeString { - if ( - typeof scopeString !== 'string' || - (!isCaipNamespace(scopeString) && !isCaipChainId(scopeString)) - ) { - throw new Error('scopeString is not a valid ExternalScopeString'); - } -} - -/** - * Asserts that an object is a valid ExternalScopesObject. - * - * @param obj - The object to assert. - */ -export function assertIsExternalScopesObject( - obj: unknown, -): asserts obj is ExternalScopesObject { - if (typeof obj !== 'object' || obj === null) { - throw new Error('ExternalScopesObject must be an object'); - } - - for (const [scopeString, scopeObject] of Object.entries(obj)) { - assertIsExternalScopeString(scopeString); - assertIsExternalScopeObject(scopeObject); - } -} - -/** - * Asserts that an object is a valid InternalScopeObject. - * - * @param obj - The object to assert. - */ -function assertIsInternalScopeObject( - obj: unknown, -): asserts obj is InternalScopeObject { - if (typeof obj !== 'object' || obj === null) { - throw new Error('InternalScopeObject must be an object'); - } - - if ( - !hasProperty(obj, 'accounts') || - !Array.isArray(obj.accounts) || - !obj.accounts.every(isCaipAccountId) - ) { - throw new Error( - 'InternalScopeObject.accounts must be an array of CaipAccountId', - ); - } -} - -/** - * Asserts that a scope string is a valid InternalScopeString. - * - * @param scopeString - The scope string to assert. - */ -export function assertIsInternalScopeString( - scopeString: unknown, -): asserts scopeString is InternalScopeString { - if ( - typeof scopeString !== 'string' || - (scopeString !== KnownCaipNamespace.Wallet && !isCaipChainId(scopeString)) - ) { - throw new Error('scopeString is not a valid InternalScopeString'); - } -} - -/** - * Asserts that an object is a valid InternalScopesObject. - * - * @param obj - The object to assert. - */ -export function assertIsInternalScopesObject( - obj: unknown, -): asserts obj is InternalScopesObject { - if (typeof obj !== 'object' || obj === null) { - throw new Error('InternalScopesObject must be an object'); - } - - for (const [scopeString, scopeObject] of Object.entries(obj)) { - assertIsInternalScopeString(scopeString); - assertIsInternalScopeObject(scopeObject); - } -} diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts deleted file mode 100644 index 08c8454c3fa..00000000000 --- a/packages/multichain/src/scope/authorization.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { bucketScopes, validateAndNormalizeScopes } from './authorization'; -import * as Filter from './filter'; -import * as Transform from './transform'; -import type { ExternalScopeObject } from './types'; -import * as Validation from './validation'; - -jest.mock('./filter', () => ({ - bucketScopesBySupport: jest.fn(), -})); -const MockFilter = jest.mocked(Filter); - -jest.mock('./validation', () => ({ - getValidScopes: jest.fn(), -})); -const MockValidation = jest.mocked(Validation); - -jest.mock('./transform', () => ({ - normalizeAndMergeScopes: jest.fn(), -})); -const MockTransform = jest.mocked(Transform); - -const validScopeObject: ExternalScopeObject = { - methods: [], - notifications: [], -}; - -describe('Scope Authorization', () => { - describe('validateAndNormalizeScopes', () => { - it('validates the scopes', () => { - MockValidation.getValidScopes.mockReturnValue({ - validRequiredScopes: {}, - validOptionalScopes: {}, - }); - validateAndNormalizeScopes( - { - 'eip155:1': validScopeObject, - }, - { - 'eip155:5': validScopeObject, - }, - ); - expect(MockValidation.getValidScopes).toHaveBeenCalledWith( - { - 'eip155:1': validScopeObject, - }, - { - 'eip155:5': validScopeObject, - }, - ); - }); - - it('normalizes and merges the validated scopes', () => { - MockValidation.getValidScopes.mockReturnValue({ - validRequiredScopes: { - 'eip155:1': validScopeObject, - }, - validOptionalScopes: { - 'eip155:5': validScopeObject, - }, - }); - - validateAndNormalizeScopes({}, {}); - expect(MockTransform.normalizeAndMergeScopes).toHaveBeenCalledWith({ - 'eip155:1': validScopeObject, - }); - expect(MockTransform.normalizeAndMergeScopes).toHaveBeenCalledWith({ - 'eip155:5': validScopeObject, - }); - }); - - it('returns the normalized and merged scopes', () => { - MockValidation.getValidScopes.mockReturnValue({ - validRequiredScopes: { - 'eip155:1': validScopeObject, - }, - validOptionalScopes: { - 'eip155:5': validScopeObject, - }, - }); - MockTransform.normalizeAndMergeScopes.mockImplementation((value) => ({ - ...value, - transformed: true, - })); - - expect(validateAndNormalizeScopes({}, {})).toStrictEqual({ - normalizedRequiredScopes: { - 'eip155:1': validScopeObject, - transformed: true, - }, - normalizedOptionalScopes: { - 'eip155:5': validScopeObject, - transformed: true, - }, - }); - }); - }); - - describe('bucketScopes', () => { - const isEvmChainIdSupported = jest.fn(); - const isEvmChainIdSupportable = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - const getNonEvmSupportedMethods = jest.fn(); - - beforeEach(() => { - let callCount = 0; - MockFilter.bucketScopesBySupport.mockImplementation(() => { - callCount += 1; - return { - supportedScopes: { - 'mock:A': { - methods: [`mock_method_${callCount}`], - notifications: [], - accounts: [], - }, - }, - unsupportedScopes: { - 'mock:B': { - methods: [`mock_method_${callCount}`], - notifications: [], - accounts: [], - }, - }, - }; - }); - }); - - it('buckets the scopes by supported', () => { - const isChainIdSupported = jest.fn(); - bucketScopes( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - - expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }); - - it('buckets the maybe supportable scopes', () => { - const isChainIdSupportable = jest.fn(); - bucketScopes( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - - expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( - { - 'mock:B': { - methods: [`mock_method_1`], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported: isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }); - - it('returns the bucketed scopes', () => { - expect( - bucketScopes( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ), - ).toStrictEqual({ - supportedScopes: { - 'mock:A': { - methods: [`mock_method_1`], - notifications: [], - accounts: [], - }, - }, - supportableScopes: { - 'mock:A': { - methods: [`mock_method_2`], - notifications: [], - accounts: [], - }, - }, - unsupportableScopes: { - 'mock:B': { - methods: [`mock_method_2`], - notifications: [], - accounts: [], - }, - }, - }); - }); - }); -}); diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts deleted file mode 100644 index 2fa5ceaa781..00000000000 --- a/packages/multichain/src/scope/authorization.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { CaipChainId, Hex, Json } from '@metamask/utils'; - -import { bucketScopesBySupport } from './filter'; -import { normalizeAndMergeScopes } from './transform'; -import type { - ExternalScopesObject, - ExternalScopeString, - NormalizedScopesObject, -} from './types'; -import { getValidScopes } from './validation'; - -/** - * Represents the parameters of a [CAIP-25](https://chainagnostic.org/CAIPs/caip-25) request. - */ -export type Caip25Authorization = ( - | { - requiredScopes: ExternalScopesObject; - optionalScopes?: ExternalScopesObject; - } - | { - requiredScopes?: ExternalScopesObject; - optionalScopes: ExternalScopesObject; - } -) & { - sessionProperties?: Record; - scopedProperties?: Record; -}; - -/** - * Validates and normalizes a set of scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. - * - * @param requiredScopes - The required scopes to validate and normalize. - * @param optionalScopes - The optional scopes to validate and normalize. - * @returns An object containing the normalized required scopes and normalized optional scopes. - */ -export const validateAndNormalizeScopes = ( - requiredScopes: ExternalScopesObject, - optionalScopes: ExternalScopesObject, -): { - normalizedRequiredScopes: NormalizedScopesObject; - normalizedOptionalScopes: NormalizedScopesObject; -} => { - const { validRequiredScopes, validOptionalScopes } = getValidScopes( - requiredScopes, - optionalScopes, - ); - - const normalizedRequiredScopes = normalizeAndMergeScopes(validRequiredScopes); - const normalizedOptionalScopes = normalizeAndMergeScopes(validOptionalScopes); - - return { - normalizedRequiredScopes, - normalizedOptionalScopes, - }; -}; - -/** - * Groups a NormalizedScopesObject into three separate - * NormalizedScopesObjects for supported scopes, - * supportable scopes, and unsupportable scopes. - * - * @param scopes - The NormalizedScopesObject to group. - * @param hooks - The hooks. - * @param hooks.isEvmChainIdSupported - A helper that returns true if an eth chainId is currently supported by the wallet. - * @param hooks.isEvmChainIdSupportable - A helper that returns true if an eth chainId could be supported by the wallet. - * @param hooks.isNonEvmScopeSupported - A predicate that determines if an non EVM scopeString is supported. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns an object with three NormalizedScopesObjects separated by support. - */ -export const bucketScopes = ( - scopes: NormalizedScopesObject, - { - isEvmChainIdSupported, - isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }: { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isEvmChainIdSupportable: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -): { - supportedScopes: NormalizedScopesObject; - supportableScopes: NormalizedScopesObject; - unsupportableScopes: NormalizedScopesObject; -} => { - const { supportedScopes, unsupportedScopes: maybeSupportableScopes } = - bucketScopesBySupport(scopes, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - - const { - supportedScopes: supportableScopes, - unsupportedScopes: unsupportableScopes, - } = bucketScopesBySupport(maybeSupportableScopes, { - isEvmChainIdSupported: isEvmChainIdSupportable, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - - return { supportedScopes, supportableScopes, unsupportableScopes }; -}; diff --git a/packages/multichain/src/scope/constants.test.ts b/packages/multichain/src/scope/constants.test.ts deleted file mode 100644 index 82915fbe7d0..00000000000 --- a/packages/multichain/src/scope/constants.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { KnownRpcMethods } from './constants'; - -describe('KnownRpcMethods', () => { - it('should match the snapshot', () => { - expect(KnownRpcMethods).toMatchInlineSnapshot(` - Object { - "bip122": Array [], - "eip155": Array [ - "personal_sign", - "eth_signTypedData_v4", - "wallet_watchAsset", - "wallet_sendCalls", - "wallet_getCallsStatus", - "wallet_getCapabilities", - "eth_sendTransaction", - "eth_decrypt", - "eth_getEncryptionPublicKey", - "web3_clientVersion", - "eth_subscribe", - "eth_unsubscribe", - "eth_blockNumber", - "eth_call", - "eth_chainId", - "eth_estimateGas", - "eth_feeHistory", - "eth_gasPrice", - "eth_getBalance", - "eth_getBlockByHash", - "eth_getBlockByNumber", - "eth_getBlockTransactionCountByHash", - "eth_getBlockTransactionCountByNumber", - "eth_getCode", - "eth_getFilterChanges", - "eth_getFilterLogs", - "eth_getLogs", - "eth_getProof", - "eth_getStorageAt", - "eth_getTransactionByBlockHashAndIndex", - "eth_getTransactionByBlockNumberAndIndex", - "eth_getTransactionByHash", - "eth_getTransactionCount", - "eth_getTransactionReceipt", - "eth_getUncleCountByBlockHash", - "eth_getUncleCountByBlockNumber", - "eth_newBlockFilter", - "eth_newFilter", - "eth_newPendingTransactionFilter", - "eth_sendRawTransaction", - "eth_syncing", - "eth_uninstallFilter", - ], - "solana": Array [], - } - `); - }); -}); diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts deleted file mode 100644 index 8ad272a7a65..00000000000 --- a/packages/multichain/src/scope/constants.ts +++ /dev/null @@ -1,91 +0,0 @@ -import MetaMaskOpenRPCDocument from '@metamask/api-specs'; - -import type { NonWalletKnownCaipNamespace } from './types'; - -/** - * ScopeStrings for offchain methods that are not specific to a chainId but are specific to a CAIP namespace. - */ -export enum KnownWalletScopeString { - Eip155 = 'wallet:eip155', -} - -/** - * Regexes defining how references must be formed for non-wallet known CAIP namespaces - */ -export const CaipReferenceRegexes: Record = - { - eip155: /^(0|[1-9][0-9]*)$/u, - bip122: /.*/u, - solana: /.*/u, - }; - -/** - * Methods that do not belong exclusively to any CAIP namespace. - */ -export const KnownWalletRpcMethods: string[] = [ - 'wallet_registerOnboarding', - 'wallet_scanQRCode', -]; - -/** - * Methods that belong to the `wallet:eip155` scope. - */ -const WalletEip155Methods = ['wallet_addEthereumChain']; - -/** - * Methods that are only supported via the EIP-1193 API. - */ -export const Eip1193OnlyMethods = [ - 'wallet_switchEthereumChain', - 'wallet_getPermissions', - 'wallet_requestPermissions', - 'wallet_revokePermissions', - 'eth_requestAccounts', - 'eth_accounts', - 'eth_coinbase', - 'net_version', - 'metamask_logWeb3ShimUsage', - 'metamask_getProviderState', - 'metamask_sendDomainMetadata', - 'wallet_registerOnboarding', -]; - -/** - * All MetaMask methods, except for ones we have specified in the constants above. - */ -const Eip155Methods = MetaMaskOpenRPCDocument.methods - .map(({ name }: { name: string }) => name) - .filter((method: string) => !WalletEip155Methods.includes(method)) - .filter((method: string) => !KnownWalletRpcMethods.includes(method)) - .filter((method: string) => !Eip1193OnlyMethods.includes(method)); - -/** - * Methods by ecosystem that are chain specific. - */ -export const KnownRpcMethods: Record = { - eip155: Eip155Methods, - bip122: [], - solana: [], -}; - -/** - * Methods for CAIP namespaces that aren't chain specific. - */ -export const KnownWalletNamespaceRpcMethods: Record< - NonWalletKnownCaipNamespace, - string[] -> = { - eip155: WalletEip155Methods, - bip122: [], - solana: [], -}; - -/** - * Notifications for known CAIP namespaces. - */ -export const KnownNotifications: Record = - { - eip155: ['eth_subscription'], - bip122: [], - solana: [], - }; diff --git a/packages/multichain/src/scope/errors.test.ts b/packages/multichain/src/scope/errors.test.ts deleted file mode 100644 index f176cd36d8a..00000000000 --- a/packages/multichain/src/scope/errors.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Caip25Errors } from './errors'; - -describe('Caip25Errors', () => { - it('requestedChainsNotSupportedError', () => { - expect(Caip25Errors.requestedChainsNotSupportedError().message).toBe( - 'Requested chains are not supported', - ); - expect(Caip25Errors.requestedChainsNotSupportedError().code).toBe(5100); - }); - - it('requestedMethodsNotSupportedError', () => { - expect(Caip25Errors.requestedMethodsNotSupportedError().message).toBe( - 'Requested methods are not supported', - ); - expect(Caip25Errors.requestedMethodsNotSupportedError().code).toBe(5101); - }); - - it('requestedNotificationsNotSupportedError', () => { - expect(Caip25Errors.requestedNotificationsNotSupportedError().message).toBe( - 'Requested notifications are not supported', - ); - expect(Caip25Errors.requestedNotificationsNotSupportedError().code).toBe( - 5102, - ); - }); - - it('unknownMethodsRequestedError', () => { - expect(Caip25Errors.unknownMethodsRequestedError().message).toBe( - 'Unknown method(s) requested', - ); - expect(Caip25Errors.unknownMethodsRequestedError().code).toBe(5201); - }); - - it('unknownNotificationsRequestedError', () => { - expect(Caip25Errors.unknownNotificationsRequestedError().message).toBe( - 'Unknown notification(s) requested', - ); - expect(Caip25Errors.unknownNotificationsRequestedError().code).toBe(5202); - }); -}); diff --git a/packages/multichain/src/scope/errors.ts b/packages/multichain/src/scope/errors.ts deleted file mode 100644 index 97ff9c9872c..00000000000 --- a/packages/multichain/src/scope/errors.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { JsonRpcError } from '@metamask/rpc-errors'; - -/** - * CAIP25 Errors. - */ -export const Caip25Errors = { - /** - * Thrown when chains requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * @returns A new JsonRpcError instance. - */ - requestedChainsNotSupportedError: () => - new JsonRpcError(5100, 'Requested chains are not supported'), - - /** - * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * TODO: consider throwing the more generic version of this error (UNKNOWN_METHODS_REQUESTED_ERROR) unless in a DevMode build of the wallet - * @returns A new JsonRpcError instance. - */ - requestedMethodsNotSupportedError: () => - new JsonRpcError(5101, 'Requested methods are not supported'), - - /** - * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * TODO: consider throwing the more generic version of this error (UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR) unless in a DevMode build of the wallet - * @returns A new JsonRpcError instance. - */ - requestedNotificationsNotSupportedError: () => - new JsonRpcError(5102, 'Requested notifications are not supported'), - - /** - * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * @returns A new JsonRpcError instance. - */ - unknownMethodsRequestedError: () => - new JsonRpcError(5201, 'Unknown method(s) requested'), - - /** - * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * @returns A new JsonRpcError instance. - */ - unknownNotificationsRequestedError: () => - new JsonRpcError(5202, 'Unknown notification(s) requested'), -}; diff --git a/packages/multichain/src/scope/filter.test.ts b/packages/multichain/src/scope/filter.test.ts deleted file mode 100644 index 336af7d3a98..00000000000 --- a/packages/multichain/src/scope/filter.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import * as Assert from './assert'; -import { bucketScopesBySupport, getSupportedScopeObjects } from './filter'; -import * as Supported from './supported'; - -jest.mock('./assert', () => ({ - ...jest.requireActual('./assert'), - assertScopeSupported: jest.fn(), -})); -const MockAssert = jest.mocked(Assert); - -jest.mock('./supported', () => ({ - ...jest.requireActual('./supported'), - isSupportedMethod: jest.fn(), - isSupportedNotification: jest.fn(), -})); -const MockSupported = jest.mocked(Supported); - -describe('filter', () => { - describe('bucketScopesBySupport', () => { - const isEvmChainIdSupported = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - const getNonEvmSupportedMethods = jest.fn(); - - it('checks if each scope is supported', () => { - bucketScopesBySupport( - { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:1', - { - methods: ['a'], - notifications: [], - accounts: [], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:5', - { - methods: ['b'], - notifications: [], - accounts: [], - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ); - }); - - it('returns supported and unsupported scopes', () => { - MockAssert.assertScopeSupported.mockImplementation((scopeString) => { - if (scopeString === 'eip155:1') { - throw new Error('scope not supported'); - } - }); - - expect( - bucketScopesBySupport( - { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }, - ), - ).toStrictEqual({ - supportedScopes: { - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - unsupportedScopes: { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - }, - }); - }); - }); - - describe('getSupportedScopeObjects', () => { - const getNonEvmSupportedMethods = jest.fn(); - - it('checks if each scopeObject method is supported', () => { - getSupportedScopeObjects( - { - 'eip155:1': { - methods: ['method1', 'method2'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['methodA', 'methodB'], - notifications: [], - accounts: [], - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(MockSupported.isSupportedMethod).toHaveBeenCalledTimes(4); - expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( - 'eip155:1', - 'method1', - { - getNonEvmSupportedMethods, - }, - ); - expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( - 'eip155:1', - 'method2', - { - getNonEvmSupportedMethods, - }, - ); - expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( - 'eip155:5', - 'methodA', - { - getNonEvmSupportedMethods, - }, - ); - expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( - 'eip155:5', - 'methodB', - { - getNonEvmSupportedMethods, - }, - ); - }); - - it('returns only supported methods', () => { - MockSupported.isSupportedMethod.mockImplementation( - (scopeString, method) => { - if (scopeString === 'eip155:1' && method === 'method1') { - return false; - } - if (scopeString === 'eip155:5' && method === 'methodB') { - return false; - } - return true; - }, - ); - - const result = getSupportedScopeObjects( - { - 'eip155:1': { - methods: ['method1', 'method2'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['methodA', 'methodB'], - notifications: [], - accounts: [], - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'eip155:1': { - methods: ['method2'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['methodA'], - notifications: [], - accounts: [], - }, - }); - }); - - it('checks if each scopeObject notification is supported', () => { - getSupportedScopeObjects( - { - 'eip155:1': { - methods: [], - notifications: ['notification1', 'notification2'], - accounts: [], - }, - 'eip155:5': { - methods: [], - notifications: ['notificationA', 'notificationB'], - accounts: [], - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(MockSupported.isSupportedNotification).toHaveBeenCalledTimes(4); - expect(MockSupported.isSupportedNotification).toHaveBeenCalledWith( - 'eip155:1', - 'notification1', - ); - expect(MockSupported.isSupportedNotification).toHaveBeenCalledWith( - 'eip155:1', - 'notification2', - ); - expect(MockSupported.isSupportedNotification).toHaveBeenCalledWith( - 'eip155:5', - 'notificationA', - ); - expect(MockSupported.isSupportedNotification).toHaveBeenCalledWith( - 'eip155:5', - 'notificationB', - ); - }); - - it('returns only supported notifications', () => { - MockSupported.isSupportedNotification.mockImplementation( - (scopeString, notification) => { - if (scopeString === 'eip155:1' && notification === 'notification1') { - return false; - } - if (scopeString === 'eip155:5' && notification === 'notificationB') { - return false; - } - return true; - }, - ); - - const result = getSupportedScopeObjects( - { - 'eip155:1': { - methods: [], - notifications: ['notification1', 'notification2'], - accounts: [], - }, - 'eip155:5': { - methods: [], - notifications: ['notificationA', 'notificationB'], - accounts: [], - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'eip155:1': { - methods: [], - notifications: ['notification2'], - accounts: [], - }, - 'eip155:5': { - methods: [], - notifications: ['notificationA'], - accounts: [], - }, - }); - }); - - it('does not modify accounts', () => { - const result = getSupportedScopeObjects( - { - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0xdeadbeef'], - }, - 'eip155:5': { - methods: [], - notifications: [], - accounts: ['eip155:5:0xdeadbeef'], - }, - }, - { - getNonEvmSupportedMethods, - }, - ); - - expect(result).toStrictEqual({ - 'eip155:1': { - methods: [], - notifications: [], - accounts: ['eip155:1:0xdeadbeef'], - }, - 'eip155:5': { - methods: [], - notifications: [], - accounts: ['eip155:5:0xdeadbeef'], - }, - }); - }); - }); -}); diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts deleted file mode 100644 index daa371d5121..00000000000 --- a/packages/multichain/src/scope/filter.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { CaipChainId, Hex } from '@metamask/utils'; - -import { assertIsInternalScopeString, assertScopeSupported } from './assert'; -import { isSupportedMethod, isSupportedNotification } from './supported'; -import type { - InternalScopeString, - NormalizedScopeObject, - NormalizedScopesObject, -} from './types'; - -/** - * Groups a NormalizedScopesObject into two separate - * NormalizedScopesObject with supported scopes in one - * and unsupported scopes in the other. - * - * @param scopes - The NormalizedScopesObject to group. - * @param hooks - An object containing the following properties: - * @param hooks.isEvmChainIdSupported - A predicate that determines if an EVM chainID is supported. - * @param hooks.isNonEvmScopeSupported - A predicate that determines if an non EVM scopeString is supported. - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - */ -export const bucketScopesBySupport = ( - scopes: NormalizedScopesObject, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }: { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - const supportedScopes: NormalizedScopesObject = {}; - const unsupportedScopes: NormalizedScopesObject = {}; - - for (const [scopeString, scopeObject] of Object.entries(scopes)) { - assertIsInternalScopeString(scopeString); - try { - assertScopeSupported(scopeString, scopeObject, { - isEvmChainIdSupported, - isNonEvmScopeSupported, - getNonEvmSupportedMethods, - }); - supportedScopes[scopeString] = scopeObject; - } catch (err) { - unsupportedScopes[scopeString] = scopeObject; - } - } - - return { supportedScopes, unsupportedScopes }; -}; - -/** - * Returns a NormalizedScopeObject with - * unsupported methods and notifications removed. - * - * @param scopeString - The InternalScopeString for the scopeObject. - * @param scopeObject - The NormalizedScopeObject to filter. - * @param hooks - An object containing the following properties: - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns a NormalizedScopeObject with only methods and notifications that are currently supported. - */ -const getSupportedScopeObject = ( - scopeString: InternalScopeString, - scopeObject: NormalizedScopeObject, - { - getNonEvmSupportedMethods, - }: { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - const { methods, notifications } = scopeObject; - - const supportedMethods = methods.filter((method) => - isSupportedMethod(scopeString, method, { getNonEvmSupportedMethods }), - ); - - const supportedNotifications = notifications.filter((notification) => - isSupportedNotification(scopeString, notification), - ); - - return { - ...scopeObject, - methods: supportedMethods, - notifications: supportedNotifications, - }; -}; - -/** - * Returns a NormalizedScopesObject with - * unsupported methods and notifications removed from scopeObjects. - * - * @param scopes - The NormalizedScopesObject to filter. - * @param hooks - An object containing the following properties: - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns a NormalizedScopesObject with only methods, and notifications that are currently supported. - */ -export const getSupportedScopeObjects = ( - scopes: NormalizedScopesObject, - { - getNonEvmSupportedMethods, - }: { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -) => { - const filteredScopesObject: NormalizedScopesObject = {}; - - for (const [scopeString, scopeObject] of Object.entries(scopes)) { - assertIsInternalScopeString(scopeString); - filteredScopesObject[scopeString] = getSupportedScopeObject( - scopeString, - scopeObject, - { getNonEvmSupportedMethods }, - ); - } - - return filteredScopesObject; -}; diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts deleted file mode 100644 index ccd55afc7a1..00000000000 --- a/packages/multichain/src/scope/supported.test.ts +++ /dev/null @@ -1,493 +0,0 @@ -import { - KnownNotifications, - KnownRpcMethods, - KnownWalletNamespaceRpcMethods, - KnownWalletRpcMethods, -} from './constants'; -import { - isSupportedAccount, - isSupportedMethod, - isSupportedNotification, - isSupportedScopeString, -} from './supported'; - -describe('Scope Support', () => { - describe('isSupportedNotification', () => { - it.each(Object.entries(KnownNotifications))( - 'returns true for each %s scope method', - (scopeString: string, notifications: string[]) => { - notifications.forEach((notification) => { - expect(isSupportedNotification(scopeString, notification)).toBe(true); - }); - }, - ); - - it('returns false otherwise', () => { - expect(isSupportedNotification('eip155', 'anything else')).toBe(false); - expect(isSupportedNotification('', '')).toBe(false); - }); - - it('returns false for unknown namespaces', () => { - expect(isSupportedNotification('unknown', 'anything else')).toBe(false); - }); - - it('returns false for wallet namespace', () => { - expect(isSupportedNotification('wallet', 'anything else')).toBe(false); - }); - }); - - describe('isSupportedMethod', () => { - const getNonEvmSupportedMethods = jest.fn(); - - beforeEach(() => { - getNonEvmSupportedMethods.mockReturnValue([]); - }); - - it('returns true for each eip155 scoped method', () => { - KnownRpcMethods.eip155.forEach((method) => { - expect( - isSupportedMethod(`eip155:1`, method, { getNonEvmSupportedMethods }), - ).toBe(true); - }); - }); - - it('returns true for each wallet scoped method', () => { - KnownWalletRpcMethods.forEach((method) => { - expect( - isSupportedMethod('wallet', method, { getNonEvmSupportedMethods }), - ).toBe(true); - }); - }); - - it('returns true for each wallet:eip155 scoped method', () => { - KnownWalletNamespaceRpcMethods.eip155.forEach((method) => { - expect( - isSupportedMethod(`wallet:eip155`, method, { - getNonEvmSupportedMethods, - }), - ).toBe(true); - }); - }); - - it('gets the supported method list from isSupportedNonEvmMethod for non-evm wallet scoped methods', () => { - isSupportedMethod(`wallet:nonevm`, 'nonEvmMethod', { - getNonEvmSupportedMethods, - }); - expect(getNonEvmSupportedMethods).toHaveBeenCalledWith('wallet:nonevm'); - }); - - it('returns true for non-evm wallet scoped methods if they are returned by isSupportedNonEvmMethod', () => { - getNonEvmSupportedMethods.mockReturnValue(['foo', 'bar', 'nonEvmMethod']); - - expect( - isSupportedMethod(`wallet:nonevm`, 'nonEvmMethod', { - getNonEvmSupportedMethods, - }), - ).toBe(true); - }); - - it('returns false for non-evm wallet scoped methods if they are not returned by isSupportedNonEvmMethod', () => { - getNonEvmSupportedMethods.mockReturnValue(['foo', 'bar', 'nonEvmMethod']); - - expect( - isSupportedMethod(`wallet:nonevm`, 'unsupportedMethod', { - getNonEvmSupportedMethods, - }), - ).toBe(false); - }); - - it('gets the supported method list from isSupportedNonEvmMethod for non-evm scoped methods', () => { - isSupportedMethod(`nonevm:123`, 'nonEvmMethod', { - getNonEvmSupportedMethods, - }); - expect(getNonEvmSupportedMethods).toHaveBeenCalledWith('nonevm:123'); - }); - - it('returns true for non-evm scoped methods if they are returned by isSupportedNonEvmMethod', () => { - getNonEvmSupportedMethods.mockReturnValue(['foo', 'bar', 'nonEvmMethod']); - - expect( - isSupportedMethod(`nonevm:123`, 'nonEvmMethod', { - getNonEvmSupportedMethods, - }), - ).toBe(true); - }); - - it('returns false for non-evm scoped methods if they are not returned by isSupportedNonEvmMethod', () => { - getNonEvmSupportedMethods.mockReturnValue(['foo', 'bar', 'nonEvmMethod']); - - expect( - isSupportedMethod(`nonevm:123`, 'unsupportedMethod', { - getNonEvmSupportedMethods, - }), - ).toBe(false); - }); - - it('returns false otherwise', () => { - expect( - isSupportedMethod('eip155', 'anything else', { - getNonEvmSupportedMethods, - }), - ).toBe(false); - expect( - isSupportedMethod('wallet:wallet', 'anything else', { - getNonEvmSupportedMethods, - }), - ).toBe(false); - expect(isSupportedMethod('', '', { getNonEvmSupportedMethods })).toBe( - false, - ); - }); - }); - - describe('isSupportedScopeString', () => { - const isEvmChainIdSupported = jest.fn(); - const isNonEvmScopeSupported = jest.fn(); - - it('returns true for the wallet namespace', () => { - expect( - isSupportedScopeString('wallet', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - - it('calls isNonEvmScopeSupported for the wallet namespace with a non-evm reference', () => { - isSupportedScopeString('wallet:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }); - - expect(isNonEvmScopeSupported).toHaveBeenCalledWith('wallet:someref'); - }); - - it('returns true for the wallet namespace when a non-evm reference is included if isNonEvmScopeSupported returns true', () => { - isNonEvmScopeSupported.mockReturnValue(true); - expect( - isSupportedScopeString('wallet:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - it('returns false for the wallet namespace when a non-evm reference is included if isNonEvmScopeSupported returns false', () => { - isNonEvmScopeSupported.mockReturnValue(false); - expect( - isSupportedScopeString('wallet:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - }); - - it('returns true for the ethereum namespace', () => { - expect( - isSupportedScopeString('eip155', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - - it('returns true for the wallet namespace with eip155 reference', () => { - expect( - isSupportedScopeString('wallet:eip155', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - - it('returns true for the ethereum namespace when a network client exists for the reference', () => { - isEvmChainIdSupported.mockReturnValue(true); - expect( - isSupportedScopeString('eip155:1', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - - it('returns false for the ethereum namespace when a network client does not exist for the reference', () => { - isEvmChainIdSupported.mockReturnValue(false); - expect( - isSupportedScopeString('eip155:1', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - }); - - it('returns false for the ethereum namespace when the reference is malformed', () => { - isEvmChainIdSupported.mockReturnValue(true); - expect( - isSupportedScopeString('eip155:01', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - expect( - isSupportedScopeString('eip155:1e1', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - }); - - it('returns false for non-evm namespace without a reference', () => { - expect( - isSupportedScopeString('nonevm', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - }); - - it('calls isNonEvmScopeSupported for non-evm namespace', () => { - isSupportedScopeString('nonevm:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }); - - expect(isNonEvmScopeSupported).toHaveBeenCalledWith('nonevm:someref'); - }); - - it('returns true for non-evm namespace if isNonEvmScopeSupported returns true', () => { - isNonEvmScopeSupported.mockReturnValue(true); - expect( - isSupportedScopeString('nonevm:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(true); - }); - it('returns false for non-evm namespace if isNonEvmScopeSupported returns false', () => { - isNonEvmScopeSupported.mockReturnValue(false); - expect( - isSupportedScopeString('nonevm:someref', { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }), - ).toBe(false); - }); - }); - - describe('isSupportedAccount', () => { - const getEvmInternalAccounts = jest.fn(); - const getNonEvmAccountAddresses = jest.fn(); - - beforeEach(() => { - getEvmInternalAccounts.mockReturnValue([]); - getNonEvmAccountAddresses.mockReturnValue([]); - }); - - it('returns true if eoa account matching eip155 namespaced address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:eoa', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('eip155:1:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if eoa account matching eip155 namespaced address with different casing exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:eoa', - address: '0xdeadBEEF', - }, - ]); - expect( - isSupportedAccount('eip155:1:0xDEADbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if erc4337 account matching eip155 namespaced address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:erc4337', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('eip155:1:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if erc4337 account matching eip155 namespaced address with different casing exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:erc4337', - address: '0xdeadBEEF', - }, - ]); - expect( - isSupportedAccount('eip155:1:0xDEADbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns false if neither eoa or erc4337 account matching eip155 namespaced address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'other', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('eip155:1:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(false); - }); - - it('returns true if eoa account matching wallet:eip155 address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:eoa', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('wallet:eip155:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if eoa account matching wallet:eip155 address with different casing exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:eoa', - address: '0xdeadBEEF', - }, - ]); - expect( - isSupportedAccount('wallet:eip155:0xDEADbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if erc4337 account matching wallet:eip155 address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:erc4337', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('wallet:eip155:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns true if erc4337 account matching wallet:eip155 address with different casing exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'eip155:erc4337', - address: '0xdeadBEEF', - }, - ]); - expect( - isSupportedAccount('wallet:eip155:0xDEADbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('returns false if neither eoa or erc4337 account matching wallet:eip155 address exists', () => { - getEvmInternalAccounts.mockReturnValue([ - { - type: 'other', - address: '0xdeadbeef', - }, - ]); - expect( - isSupportedAccount('wallet:eip155:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(false); - }); - - it('gets the non-evm account addresses for the scope if wallet namespace with non-evm reference', () => { - isSupportedAccount('wallet:nonevm:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }); - - expect(getNonEvmAccountAddresses).toHaveBeenCalledWith('wallet:nonevm'); - }); - - it('returns false if wallet namespace with non-evm reference and account is not returned by getNonEvmAccountAddresses', () => { - getNonEvmAccountAddresses.mockReturnValue(['wallet:other:123']); - expect( - isSupportedAccount('wallet:nonevm:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(false); - }); - - it('returns true if wallet namespace with non-evm reference and account is returned by getNonEvmAccountAddresses', () => { - getNonEvmAccountAddresses.mockReturnValue(['wallet:nonevm:0xdeadbeef']); - expect( - isSupportedAccount('wallet:nonevm:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - - it('gets the non-evm account addresses for the scope if non-evm namespace', () => { - isSupportedAccount('foo:bar:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }); - - expect(getNonEvmAccountAddresses).toHaveBeenCalledWith('foo:bar'); - }); - - it('returns false if non-evm namespace and account is not returned by getNonEvmAccountAddresses', () => { - getNonEvmAccountAddresses.mockReturnValue(['wallet:other:123']); - expect( - isSupportedAccount('foo:bar:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(false); - }); - - it('returns true if non-evm namespace and account is returned by getNonEvmAccountAddresses', () => { - getNonEvmAccountAddresses.mockReturnValue(['foo:bar:0xdeadbeef']); - expect( - isSupportedAccount('foo:bar:0xdeadbeef', { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }), - ).toBe(true); - }); - }); -}); diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts deleted file mode 100644 index 62c7237e363..00000000000 --- a/packages/multichain/src/scope/supported.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { toHex, isEqualCaseInsensitive } from '@metamask/controller-utils'; -import type { CaipAccountId, CaipChainId, Hex } from '@metamask/utils'; -import { - isCaipChainId, - KnownCaipNamespace, - parseCaipAccountId, -} from '@metamask/utils'; - -import { - CaipReferenceRegexes, - KnownNotifications, - KnownRpcMethods, - KnownWalletNamespaceRpcMethods, - KnownWalletRpcMethods, -} from './constants'; -import type { ExternalScopeString } from './types'; -import { parseScopeString } from './types'; - -/** - * Determines if a scope string is supported. - * - * @param scopeString - The scope string to check. - * @param hooks - An object containing the following properties: - * @param hooks.isEvmChainIdSupported - A predicate that determines if an EVM chainID is supported. - * @param hooks.isNonEvmScopeSupported - A predicate that determines if an non EVM scopeString is supported. - * @returns A boolean indicating if the scope string is supported. - */ -export const isSupportedScopeString = ( - scopeString: string, - { - isEvmChainIdSupported, - isNonEvmScopeSupported, - }: { - isEvmChainIdSupported: (chainId: Hex) => boolean; - isNonEvmScopeSupported: (scope: CaipChainId) => boolean; - }, -) => { - const { namespace, reference } = parseScopeString(scopeString); - - switch (namespace) { - case KnownCaipNamespace.Wallet: - if ( - isCaipChainId(scopeString) && - reference !== KnownCaipNamespace.Eip155 - ) { - return isNonEvmScopeSupported(scopeString); - } - return true; - case KnownCaipNamespace.Eip155: - return ( - !reference || - (CaipReferenceRegexes.eip155.test(reference) && - isEvmChainIdSupported(toHex(reference))) - ); - default: - return isCaipChainId(scopeString) - ? isNonEvmScopeSupported(scopeString) - : false; - } -}; - -/** - * Determines if an account is supported by the wallet (i.e. on a keyring known to the wallet). - * - * @param account - The CAIP account ID to check. - * @param hooks - An object containing the following properties: - * @param hooks.getEvmInternalAccounts - A function that returns the EVM internal accounts. - * @param hooks.getNonEvmAccountAddresses - A function that returns the supported CAIP-10 account addresses for a non EVM scope. - * @returns A boolean indicating if the account is supported by the wallet. - */ -export const isSupportedAccount = ( - account: CaipAccountId, - { - getEvmInternalAccounts, - getNonEvmAccountAddresses, - }: { - getEvmInternalAccounts: () => { type: string; address: Hex }[]; - getNonEvmAccountAddresses: (scope: CaipChainId) => string[]; - }, -) => { - const { - address, - chainId, - chain: { namespace, reference }, - } = parseCaipAccountId(account); - - const isSupportedEip155Account = () => - getEvmInternalAccounts().some( - (internalAccount) => - ['eip155:eoa', 'eip155:erc4337'].includes(internalAccount.type) && - isEqualCaseInsensitive(address, internalAccount.address), - ); - - const isSupportedNonEvmAccount = () => - getNonEvmAccountAddresses(chainId).includes(account); - - switch (namespace) { - case KnownCaipNamespace.Wallet: - if (reference === KnownCaipNamespace.Eip155) { - return isSupportedEip155Account(); - } - return isSupportedNonEvmAccount(); - case KnownCaipNamespace.Eip155: - return isSupportedEip155Account(); - default: - return isSupportedNonEvmAccount(); - } -}; - -/** - * Determines if a method is supported by the wallet. - * - * @param scopeString - The scope string to check. - * @param method - The method to check. - * @param hooks - An object containing the following properties: - * @param hooks.getNonEvmSupportedMethods - A function that returns the supported methods for a non EVM scope. - * @returns A boolean indicating if the method is supported by the wallet. - */ -export const isSupportedMethod = ( - scopeString: ExternalScopeString, - method: string, - { - getNonEvmSupportedMethods, - }: { - getNonEvmSupportedMethods: (scope: CaipChainId) => string[]; - }, -): boolean => { - const { namespace, reference } = parseScopeString(scopeString); - - if (!namespace) { - return false; - } - - const isSupportedNonEvmMethod = () => - isCaipChainId(scopeString) && - getNonEvmSupportedMethods(scopeString).includes(method); - - if (namespace === KnownCaipNamespace.Wallet) { - if (!reference) { - return KnownWalletRpcMethods.includes(method); - } - - if (reference === KnownCaipNamespace.Eip155) { - return KnownWalletNamespaceRpcMethods[reference].includes(method); - } - - return isSupportedNonEvmMethod(); - } - - if (namespace === KnownCaipNamespace.Eip155) { - return KnownRpcMethods[namespace].includes(method); - } - - return isSupportedNonEvmMethod(); -}; - -/** - * Determines if a notification is supported by the wallet. - * - * @param scopeString - The scope string to check. - * @param notification - The notification to check. - * @returns A boolean indicating if the notification is supported by the wallet. - */ -export const isSupportedNotification = ( - scopeString: ExternalScopeString, - notification: string, -): boolean => { - const { namespace } = parseScopeString(scopeString); - - if (namespace === KnownCaipNamespace.Eip155) { - return KnownNotifications[namespace].includes(notification); - } - - return false; -}; diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts deleted file mode 100644 index 7d8e33715a5..00000000000 --- a/packages/multichain/src/scope/transform.test.ts +++ /dev/null @@ -1,537 +0,0 @@ -import { - normalizeScope, - mergeNormalizedScopes, - mergeInternalScopes, - mergeScopeObject, - normalizeAndMergeScopes, -} from './transform'; -import type { - ExternalScopeObject, - NormalizedScopeObject, - InternalScopesObject, -} from './types'; - -const externalScopeObject: ExternalScopeObject = { - methods: [], - notifications: [], -}; - -const validScopeObject: NormalizedScopeObject = { - methods: [], - notifications: [], - accounts: [], -}; - -describe('Scope Transform', () => { - describe('normalizeScope', () => { - describe('scopeString is chain scoped', () => { - it('returns the scope with empty accounts array when accounts are not defined', () => { - expect(normalizeScope('eip155:1', externalScopeObject)).toStrictEqual({ - 'eip155:1': { - ...externalScopeObject, - accounts: [], - }, - }); - }); - - it('returns the scope unchanged when accounts are defined', () => { - expect( - normalizeScope('eip155:1', { ...externalScopeObject, accounts: [] }), - ).toStrictEqual({ - 'eip155:1': { - ...externalScopeObject, - accounts: [], - }, - }); - }); - }); - - describe('scopeString is namespace scoped', () => { - it('returns the scope as is when `references` is not defined', () => { - expect(normalizeScope('eip155', validScopeObject)).toStrictEqual({ - eip155: validScopeObject, - }); - }); - - it('returns one scope per `references` element with `references` excluded from the scopeObject', () => { - expect( - normalizeScope('eip155', { - ...validScopeObject, - references: ['1', '5', '64'], - }), - ).toStrictEqual({ - 'eip155:1': validScopeObject, - 'eip155:5': validScopeObject, - 'eip155:64': validScopeObject, - }); - }); - - it('returns one deep cloned scope per `references` element', () => { - const normalizedScopes = normalizeScope('eip155', { - ...validScopeObject, - references: ['1', '5'], - }); - - expect(normalizedScopes['eip155:1']).not.toBe( - normalizedScopes['eip155:5'], - ); - expect(normalizedScopes['eip155:1'].methods).not.toBe( - normalizedScopes['eip155:5'].methods, - ); - }); - - it('returns the scope as is when `references` is an empty array', () => { - expect( - normalizeScope('eip155', { ...validScopeObject, references: [] }), - ).toStrictEqual({ - eip155: validScopeObject, - }); - }); - }); - }); - - describe('mergeScopeObject', () => { - it('returns an object with the unique set of methods', () => { - expect( - mergeScopeObject( - { - ...validScopeObject, - methods: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - methods: ['b', 'c', 'd'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - methods: ['a', 'b', 'c', 'd'], - }); - }); - - it('returns an object with the unique set of notifications', () => { - expect( - mergeScopeObject( - { - ...validScopeObject, - notifications: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - notifications: ['b', 'c', 'd'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - notifications: ['a', 'b', 'c', 'd'], - }); - }); - - it('returns an object with the unique set of accounts', () => { - expect( - mergeScopeObject( - { - ...validScopeObject, - accounts: ['eip155:1:a', 'eip155:1:b', 'eip155:1:c'], - }, - { - ...validScopeObject, - accounts: ['eip155:1:b', 'eip155:1:c', 'eip155:1:d'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - accounts: ['eip155:1:a', 'eip155:1:b', 'eip155:1:c', 'eip155:1:d'], - }); - - expect( - mergeScopeObject( - { - ...validScopeObject, - accounts: ['eip155:1:a', 'eip155:1:b', 'eip155:1:c'], - }, - { - ...validScopeObject, - }, - ), - ).toStrictEqual({ - ...validScopeObject, - accounts: ['eip155:1:a', 'eip155:1:b', 'eip155:1:c'], - }); - }); - - it('returns an object with the unique set of rpcDocuments', () => { - expect( - mergeScopeObject( - { - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - rpcDocuments: ['b', 'c', 'd'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c', 'd'], - }); - - expect( - mergeScopeObject( - { - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c'], - }); - - expect( - mergeScopeObject( - { - ...validScopeObject, - }, - { - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcDocuments: ['a', 'b', 'c'], - }); - }); - - it('returns an object with the unique set of rpcEndpoints', () => { - expect( - mergeScopeObject( - { - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - rpcEndpoints: ['b', 'c', 'd'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c', 'd'], - }); - - expect( - mergeScopeObject( - { - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c'], - }, - { - ...validScopeObject, - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c'], - }); - - expect( - mergeScopeObject( - { - ...validScopeObject, - }, - { - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c'], - }, - ), - ).toStrictEqual({ - ...validScopeObject, - rpcEndpoints: ['a', 'b', 'c'], - }); - }); - }); - - describe('mergeInternalScopes', () => { - describe('incremental request existing scope with a new account', () => { - it('should return merged scope with existing chain and both accounts', () => { - const leftValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }; - - const rightValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xbeef'], - }, - }; - - const expectedMergedValue: InternalScopesObject = { - 'eip155:1': { accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'] }, - }; - - const mergedValue = mergeInternalScopes(leftValue, rightValue); - - expect(mergedValue).toStrictEqual(expectedMergedValue); - }); - }); - - describe('incremental request a whole new scope without accounts', () => { - it('should return merged scope with previously existing chain and accounts, plus new requested chain with no accounts', () => { - const leftValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }; - - const rightValue: InternalScopesObject = { - 'eip155:10': { - accounts: [], - }, - }; - - const expectedMergedValue: InternalScopesObject = { - 'eip155:1': { accounts: ['eip155:1:0xdead'] }, - 'eip155:10': { - accounts: [], - }, - }; - - const mergedValue = mergeInternalScopes(leftValue, rightValue); - - expect(mergedValue).toStrictEqual(expectedMergedValue); - }); - }); - - describe('incremental request a whole new scope with accounts', () => { - it('should return merged scope with previously existing chain and accounts, plus new requested chain with new account', () => { - const leftValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }; - - const rightValue: InternalScopesObject = { - 'eip155:10': { - accounts: ['eip155:10:0xbeef'], - }, - }; - - const expectedMergedValue: InternalScopesObject = { - 'eip155:1': { accounts: ['eip155:1:0xdead'] }, - 'eip155:10': { accounts: ['eip155:10:0xbeef'] }, - }; - - const mergedValue = mergeInternalScopes(leftValue, rightValue); - - expect(mergedValue).toStrictEqual(expectedMergedValue); - }); - }); - - describe('incremental request an existing scope with new accounts, and whole new scope with accounts', () => { - it('should return merged scope with previously existing chain and accounts, plus new requested chain with new accounts', () => { - const leftValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }; - - const rightValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }; - - const expectedMergedValue: InternalScopesObject = { - 'eip155:1': { accounts: ['eip155:1:0xdead', 'eip155:1:0xbeef'] }, - 'eip155:10': { - accounts: ['eip155:10:0xdead', 'eip155:10:0xbeef'], - }, - }; - - const mergedValue = mergeInternalScopes(leftValue, rightValue); - - expect(mergedValue).toStrictEqual(expectedMergedValue); - }); - }); - - describe('incremental request an existing scope with new accounts, and 2 whole new scope with accounts', () => { - it('should return merged scope with previously existing chain and accounts, plus new requested chains with new accounts', () => { - const leftValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead'], - }, - }; - - const rightValue: InternalScopesObject = { - 'eip155:1': { - accounts: ['eip155:1:0xdead', 'eip155:1:0xbadd'], - }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef', 'eip155:10:0xbadd'], - }, - 'eip155:426161': { - accounts: [ - 'eip155:426161:0xdead', - 'eip155:426161:0xbeef', - 'eip155:426161:0xbadd', - ], - }, - }; - - const expectedMergedValue: InternalScopesObject = { - 'eip155:1': { accounts: ['eip155:1:0xdead', 'eip155:1:0xbadd'] }, - 'eip155:10': { - accounts: ['eip155:10:0xbeef', 'eip155:10:0xbadd'], - }, - 'eip155:426161': { - accounts: [ - 'eip155:426161:0xdead', - 'eip155:426161:0xbeef', - 'eip155:426161:0xbadd', - ], - }, - }; - - const mergedValue = mergeInternalScopes(leftValue, rightValue); - - expect(mergedValue).toStrictEqual(expectedMergedValue); - }); - }); - }); - - describe('mergeNormalizedScopes', () => { - it('merges the scopeObjects with matching scopeString', () => { - expect( - mergeNormalizedScopes( - { - 'eip155:1': { - methods: ['a', 'b', 'c'], - notifications: ['foo'], - accounts: [], - }, - }, - { - 'eip155:1': { - methods: ['c', 'd'], - notifications: ['bar'], - accounts: [], - }, - }, - ), - ).toStrictEqual({ - 'eip155:1': { - methods: ['a', 'b', 'c', 'd'], - notifications: ['foo', 'bar'], - accounts: [], - }, - }); - }); - - it('preserves the scopeObjects with no matching scopeString', () => { - expect( - mergeNormalizedScopes( - { - 'eip155:1': { - methods: ['a', 'b', 'c'], - notifications: ['foo'], - accounts: [], - }, - }, - { - 'eip155:2': { - methods: ['c', 'd'], - notifications: ['bar'], - accounts: [], - }, - 'eip155:3': { - methods: [], - notifications: [], - accounts: [], - }, - }, - ), - ).toStrictEqual({ - 'eip155:1': { - methods: ['a', 'b', 'c'], - notifications: ['foo'], - accounts: [], - }, - 'eip155:2': { - methods: ['c', 'd'], - notifications: ['bar'], - accounts: [], - }, - 'eip155:3': { - methods: [], - notifications: [], - accounts: [], - }, - }); - }); - it('returns an empty object when no scopes are provided', () => { - expect(mergeNormalizedScopes({}, {})).toStrictEqual({}); - }); - - it('returns an unchanged scope when two identical scopeObjects are provided', () => { - expect( - mergeNormalizedScopes( - { 'eip155:1': validScopeObject }, - { 'eip155:1': validScopeObject }, - ), - ).toStrictEqual({ 'eip155:1': validScopeObject }); - }); - }); - - describe('normalizeAndMergeScopes', () => { - it('normalizes scopes and merges any overlapping scopeStrings', () => { - expect( - normalizeAndMergeScopes({ - eip155: { - ...validScopeObject, - methods: ['a', 'b'], - references: ['1', '5'], - }, - 'eip155:1': { - ...validScopeObject, - methods: ['b', 'c', 'd'], - }, - }), - ).toStrictEqual({ - 'eip155:1': { - ...validScopeObject, - methods: ['a', 'b', 'c', 'd'], - }, - 'eip155:5': { - ...validScopeObject, - methods: ['a', 'b'], - }, - }); - }); - it('returns an empty object when no scopes are provided', () => { - expect(normalizeAndMergeScopes({})).toStrictEqual({}); - }); - it('return an unchanged scope when scopeObjects are already normalized (i.e. none contain references to flatten)', () => { - expect( - normalizeAndMergeScopes({ - 'eip155:1': validScopeObject, - 'eip155:2': validScopeObject, - 'eip155:3': validScopeObject, - }), - ).toStrictEqual({ - 'eip155:1': validScopeObject, - 'eip155:2': validScopeObject, - 'eip155:3': validScopeObject, - }); - }); - }); -}); diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts deleted file mode 100644 index a44d510474d..00000000000 --- a/packages/multichain/src/scope/transform.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { CaipReference } from '@metamask/utils'; -import { cloneDeep } from 'lodash'; - -import type { - ExternalScopeObject, - ExternalScopesObject, - InternalScopesObject, - NormalizedScopeObject, - NormalizedScopesObject, -} from './types'; -import { parseScopeString } from './types'; - -/** - * Returns a list of unique items - * - * @param list - The list of items to filter - * @returns A list of unique items - */ -export const getUniqueArrayItems = (list: Value[]): Value[] => { - return Array.from(new Set(list)); -}; - -/** - * Normalizes a ScopeString and ExternalScopeObject into a separate - * InternalScopeString and NormalizedScopeObject for each reference in the `references` - * value if defined and adds an empty `accounts` array if not defined. - * - * @param scopeString - The string representing the scope - * @param externalScopeObject - The object that defines the scope - * @returns a map of caipChainId to ScopeObjects - */ -export const normalizeScope = ( - scopeString: string, - externalScopeObject: ExternalScopeObject, -): NormalizedScopesObject => { - const { references, ...scopeObject } = externalScopeObject; - const { namespace, reference } = parseScopeString(scopeString); - - const normalizedScopeObject: NormalizedScopeObject = { - accounts: [], - ...scopeObject, - }; - - const shouldFlatten = - namespace && - !reference && - references !== undefined && - references.length > 0; - - if (shouldFlatten) { - return Object.fromEntries( - references.map((ref: CaipReference) => [ - `${namespace}:${ref}`, - cloneDeep(normalizedScopeObject), - ]), - ); - } - return { [scopeString]: normalizedScopeObject }; -}; - -/** - * Merges two NormalizedScopeObjects - * - * @param scopeObjectA - The first scope object to merge. - * @param scopeObjectB - The second scope object to merge. - * @returns The merged scope object. - */ -export const mergeScopeObject = ( - scopeObjectA: NormalizedScopeObject, - scopeObjectB: NormalizedScopeObject, -) => { - const mergedScopeObject: NormalizedScopeObject = { - methods: getUniqueArrayItems([ - ...scopeObjectA.methods, - ...scopeObjectB.methods, - ]), - notifications: getUniqueArrayItems([ - ...scopeObjectA.notifications, - ...scopeObjectB.notifications, - ]), - accounts: getUniqueArrayItems([ - ...scopeObjectA.accounts, - ...scopeObjectB.accounts, - ]), - }; - - if (scopeObjectA.rpcDocuments || scopeObjectB.rpcDocuments) { - mergedScopeObject.rpcDocuments = getUniqueArrayItems([ - ...(scopeObjectA.rpcDocuments ?? []), - ...(scopeObjectB.rpcDocuments ?? []), - ]); - } - - if (scopeObjectA.rpcEndpoints || scopeObjectB.rpcEndpoints) { - mergedScopeObject.rpcEndpoints = getUniqueArrayItems([ - ...(scopeObjectA.rpcEndpoints ?? []), - ...(scopeObjectB.rpcEndpoints ?? []), - ]); - } - - return mergedScopeObject; -}; - -/** - * Merges two NormalizedScopeObjects - * - * @param scopeA - The first normalized scope object to merge. - * @param scopeB - The second normalized scope object to merge. - * @returns The merged normalized scope object from the [CAIP-25](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) request. - */ -export const mergeNormalizedScopes = ( - scopeA: NormalizedScopesObject, - scopeB: NormalizedScopesObject, -): NormalizedScopesObject => { - const scope: NormalizedScopesObject = {}; - - Object.entries(scopeA).forEach(([_scopeString, scopeObjectA]) => { - // Cast needed because index type is returned as `string` by `Object.entries` - const scopeString = _scopeString as keyof typeof scopeA; - const scopeObjectB = scopeB[scopeString]; - - scope[scopeString] = scopeObjectB - ? mergeScopeObject(scopeObjectA, scopeObjectB) - : scopeObjectA; - }); - - Object.entries(scopeB).forEach(([_scopeString, scopeObjectB]) => { - // Cast needed because index type is returned as `string` by `Object.entries` - const scopeString = _scopeString as keyof typeof scopeB; - const scopeObjectA = scopeA[scopeString]; - - if (!scopeObjectA) { - scope[scopeString] = scopeObjectB; - } - }); - - return scope; -}; - -/** - * Merges two InternalScopeObjects - * - * @param scopeA - The first internal scope object to merge. - * @param scopeB - The second internal scope object to merge. - * @returns The merged internal scope object from the [CAIP-25](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) request. - */ -export const mergeInternalScopes = ( - scopeA: InternalScopesObject, - scopeB: InternalScopesObject, -): InternalScopesObject => { - const resultScope = cloneDeep(scopeA); - - Object.entries(scopeB).forEach(([scopeString, rightScopeObject]) => { - const internalScopeString = scopeString as keyof typeof scopeB; - const leftRequiredScopeObject = resultScope[internalScopeString]; - if (!leftRequiredScopeObject) { - resultScope[internalScopeString] = rightScopeObject; - } else { - resultScope[internalScopeString] = { - accounts: getUniqueArrayItems([ - ...leftRequiredScopeObject.accounts, - ...rightScopeObject.accounts, - ]), - }; - } - }); - - return resultScope; -}; - -/** - * Normalizes and merges a set of ExternalScopesObjects into a NormalizedScopesObject (i.e. a set of NormalizedScopeObjects where references are flattened). - * - * @param scopes - The external scopes to normalize and merge. - * @returns The normalized and merged scopes. - */ -export const normalizeAndMergeScopes = ( - scopes: ExternalScopesObject, -): NormalizedScopesObject => { - let mergedScopes: NormalizedScopesObject = {}; - Object.keys(scopes).forEach((scopeString) => { - const normalizedScopes = normalizeScope(scopeString, scopes[scopeString]); - mergedScopes = mergeNormalizedScopes(mergedScopes, normalizedScopes); - }); - - return mergedScopes; -}; diff --git a/packages/multichain/src/scope/types.test.ts b/packages/multichain/src/scope/types.test.ts deleted file mode 100644 index 1b6149b3f2e..00000000000 --- a/packages/multichain/src/scope/types.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { parseScopeString } from './types'; - -describe('Scope', () => { - describe('parseScopeString', () => { - it('returns only the namespace if scopeString is namespace', () => { - expect(parseScopeString('abc')).toStrictEqual({ namespace: 'abc' }); - }); - - it('returns the namespace and reference if scopeString is a CAIP chain ID', () => { - expect(parseScopeString('abc:foo')).toStrictEqual({ - namespace: 'abc', - reference: 'foo', - }); - }); - - it('returns empty object if scopeString is invalid', () => { - expect(parseScopeString('')).toStrictEqual({}); - expect(parseScopeString('a:')).toStrictEqual({}); - expect(parseScopeString(':b')).toStrictEqual({}); - expect(parseScopeString('a:b:c')).toStrictEqual({}); - }); - }); -}); diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts deleted file mode 100644 index 8993eb0cabb..00000000000 --- a/packages/multichain/src/scope/types.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { - isCaipNamespace, - isCaipChainId, - parseCaipChainId, -} from '@metamask/utils'; -import type { - CaipChainId, - CaipReference, - CaipAccountId, - KnownCaipNamespace, - CaipNamespace, - Json, -} from '@metamask/utils'; - -/** - * Represents a `scopeString` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). - */ -export type ExternalScopeString = CaipChainId | CaipNamespace; -/** - * Represents a `scopeObject` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). - */ -export type ExternalScopeObject = Omit & { - references?: CaipReference[]; - accounts?: CaipAccountId[]; -}; -/** - * Represents a `scope` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). - * TODO update the language in CAIP-217 to use "scope" instead of "scopeObject" for this full record type. - */ -export type ExternalScopesObject = Record< - ExternalScopeString, - ExternalScopeObject ->; - -/** - * Represents a `scopeString` as defined in - * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that - * CAIP namespaces without a reference (aside from "wallet") are disallowed for our internal representations of CAIP-25 session scopes - */ -export type InternalScopeString = CaipChainId | KnownCaipNamespace.Wallet; - -/** - * A trimmed down version of a [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) defined scopeObject that is stored in a `endowment:caip25` permission. - * The only property from the original CAIP-25 scopeObject that we use for permissioning is `accounts`. - */ -export type InternalScopeObject = { - accounts: CaipAccountId[]; -}; - -/** - * A trimmed down version of a [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) scope that is stored in a `endowment:caip25` permission. - * Accounts arrays are mapped to CAIP-2 chainIds. These are currently the only properties used by the permission system. - */ -export type InternalScopesObject = Record & { - [KnownCaipNamespace.Wallet]?: InternalScopeObject; -}; - -/** - * Represents a `scopeObject` as defined in - * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that - * we resolve the `references` property into a scopeObject per reference and - * assign an empty array to the `accounts` property if not already defined - * to more easily perform support checks for `wallet_createSession` requests. - * Also used as the return type for `wallet_createSession` and `wallet_sessionChanged`. - */ -export type NormalizedScopeObject = { - methods: string[]; - notifications: string[]; - accounts: CaipAccountId[]; - rpcDocuments?: string[]; - rpcEndpoints?: string[]; -}; -/** - * Represents a keyed `scopeObject` as defined in - * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that - * we resolve the `references` property into a scopeObject per reference and - * assign an empty array to the `accounts` property if not already defined - * to more easily perform support checks for `wallet_createSession` requests. - * Also used as the return type for `wallet_createSession` and `wallet_sessionChanged`. - */ -export type NormalizedScopesObject = Record< - CaipChainId, - NormalizedScopeObject -> & { - [KnownCaipNamespace.Wallet]?: NormalizedScopeObject; -}; - -export type ScopedProperties = Record> & { - [KnownCaipNamespace.Wallet]?: Record; -}; - -/** - * Parses a scope string into a namespace and reference. - * - * @param scopeString - The scope string to parse. - * @returns An object containing the namespace and reference. - */ -export const parseScopeString = ( - scopeString: string, -): { - namespace?: string; - reference?: string; -} => { - if (isCaipNamespace(scopeString)) { - return { - namespace: scopeString, - }; - } - if (isCaipChainId(scopeString)) { - return parseCaipChainId(scopeString); - } - - return {}; -}; - -/** - * CAIP namespaces excluding "wallet" currently supported by/known to the wallet. - */ -export type NonWalletKnownCaipNamespace = Exclude< - KnownCaipNamespace, - KnownCaipNamespace.Wallet ->; diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts deleted file mode 100644 index 6871b01069b..00000000000 --- a/packages/multichain/src/scope/validation.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { ExternalScopeObject } from './types'; -import { isValidScope, getValidScopes } from './validation'; - -const validScopeString = 'eip155:1'; -const validScopeObject: ExternalScopeObject = { - methods: [], - notifications: [], -}; - -describe('Scope Validation', () => { - describe('isValidScope', () => { - it('returns false when the scopeString is neither a CAIP namespace or CAIP chainId', () => { - expect( - isValidScope('not a namespace or a caip chain id', validScopeObject), - ).toBe(false); - }); - - it('returns true when the scopeString is "wallet" and the scopeObject does not contain references', () => { - expect(isValidScope('wallet', validScopeObject)).toBe(true); - }); - - it('returns true when the scopeString is a valid CAIP chainId and the scopeObject is valid', () => { - expect(isValidScope('eip155:1', validScopeObject)).toBe(true); - }); - - it('returns false when the scopeString is a valid CAIP namespace but references are invalid CAIP references', () => { - expect( - isValidScope('eip155', { - ...validScopeObject, - references: ['@'], - }), - ).toBe(false); - }); - - it('returns false when the scopeString is a CAIP chainId but references is defined', () => { - expect( - isValidScope('eip155:1', { - ...validScopeObject, - references: [], - }), - ).toBe(false); - }); - - it('returns false when the scopeString is a valid CAIP namespace (other than "wallet") but references is an empty array', () => { - expect( - isValidScope('eip155', { ...validScopeObject, references: [] }), - ).toBe(false); - }); - - it('returns false when the scopeString is a valid CAIP namespace (other than "wallet") but references is undefined', () => { - expect(isValidScope('eip155', validScopeObject)).toBe(false); - }); - - it('returns false when methods contains empty string', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - methods: [''], - }), - ).toBe(false); - }); - - it('returns false when methods contains non-string', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - // @ts-expect-error Intentionally invalid input - methods: [{ foo: 'bar' }], - }), - ).toBe(false); - }); - - it('returns true when methods contains only strings', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - methods: ['method1', 'method2'], - }), - ).toBe(true); - }); - - it('returns false when notifications contains empty string', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - notifications: [''], - }), - ).toBe(false); - }); - - it('returns false when notifications contains non-string', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - // @ts-expect-error Intentionally invalid input - notifications: [{ foo: 'bar' }], - }), - ).toBe(false); - }); - - it('returns false when unexpected properties are defined', () => { - expect( - isValidScope(validScopeString, { - ...validScopeObject, - // @ts-expect-error Intentionally invalid input - unexpectedParam: 'foobar', - }), - ).toBe(false); - }); - - it('returns true when only expected properties are defined', () => { - expect( - isValidScope(validScopeString, { - methods: [], - notifications: [], - accounts: [], - rpcDocuments: [], - rpcEndpoints: [], - }), - ).toBe(true); - - expect( - isValidScope('eip155', { - ...validScopeObject, - references: ['1'], - }), - ).toBe(true); - }); - }); - - describe('getValidScopes', () => { - const validScopeObjectWithAccounts = { - ...validScopeObject, - accounts: [], - }; - - it('does not throw an error if required scopes are defined but none are valid', () => { - expect( - getValidScopes( - // @ts-expect-error Intentionally invalid input - { 'eip155:1': {} }, - undefined, - ), - ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); - }); - - it('does not throw an error if optional scopes are defined but none are valid', () => { - expect( - getValidScopes(undefined, { - // @ts-expect-error Intentionally invalid input - 'eip155:1': {}, - }), - ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); - }); - - it('returns the valid required and optional scopes', () => { - expect( - getValidScopes( - { - 'eip155:1': validScopeObjectWithAccounts, - // @ts-expect-error Intentionally invalid input - 'eip155:64': {}, - }, - { - 'eip155:2': {}, - 'eip155:5': validScopeObjectWithAccounts, - }, - ), - ).toStrictEqual({ - validRequiredScopes: { - 'eip155:1': validScopeObjectWithAccounts, - }, - validOptionalScopes: { - 'eip155:5': validScopeObjectWithAccounts, - }, - }); - }); - }); -}); diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts deleted file mode 100644 index 26e96fdc656..00000000000 --- a/packages/multichain/src/scope/validation.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { isCaipReference } from '@metamask/utils'; - -import type { - ExternalScopeString, - ExternalScopeObject, - ExternalScopesObject, -} from './types'; -import { parseScopeString } from './types'; - -/** - * Validates a scope object according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. - * @param scopeString - The scope string to validate. - * @param scopeObject - The scope object to validate. - * @returns A boolean indicating if the scope object is valid according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. - */ -export const isValidScope = ( - scopeString: ExternalScopeString, - scopeObject: ExternalScopeObject, -): boolean => { - const { namespace, reference } = parseScopeString(scopeString); - - // Namespace is required - if (!namespace) { - return false; - } - - const { - references, - methods, - notifications, - accounts, - rpcDocuments, - rpcEndpoints, - ...extraProperties - } = scopeObject; - - // Methods and notifications are required - if (!methods || !notifications) { - return false; - } - - // For namespaces other than 'wallet', either reference or non-empty references array must be present - if ( - namespace !== 'wallet' && - !reference && - (!references || references.length === 0) - ) { - return false; - } - - // If references are present, reference must be absent and all references must be valid - if (references) { - if (reference) { - return false; - } - - const areReferencesValid = references.every((nestedReference) => - isCaipReference(nestedReference), - ); - - if (!areReferencesValid) { - return false; - } - } - - const areMethodsValid = methods.every( - (method) => typeof method === 'string' && method.trim() !== '', - ); - - if (!areMethodsValid) { - return false; - } - - const areNotificationsValid = notifications.every( - (notification) => - typeof notification === 'string' && notification.trim() !== '', - ); - - if (!areNotificationsValid) { - return false; - } - - // Ensure no unexpected properties are present in the scope object - if (Object.keys(extraProperties).length > 0) { - return false; - } - - return true; -}; - -/** - * Filters out invalid scopes and returns valid sets of required and optional scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. - * @param requiredScopes - The required scopes to validate. - * @param optionalScopes - The optional scopes to validate. - * @returns An object containing valid required scopes and optional scopes. - */ -export const getValidScopes = ( - requiredScopes?: ExternalScopesObject, - optionalScopes?: ExternalScopesObject, -) => { - const validRequiredScopes: ExternalScopesObject = {}; - for (const [scopeString, scopeObject] of Object.entries( - requiredScopes || {}, - )) { - if (isValidScope(scopeString, scopeObject)) { - validRequiredScopes[scopeString] = { - accounts: [], - ...scopeObject, - }; - } - } - - const validOptionalScopes: ExternalScopesObject = {}; - for (const [scopeString, scopeObject] of Object.entries( - optionalScopes || {}, - )) { - if (isValidScope(scopeString, scopeObject)) { - validOptionalScopes[scopeString] = { - accounts: [], - ...scopeObject, - }; - } - } - - return { - validRequiredScopes, - validOptionalScopes, - }; -}; diff --git a/packages/multichain/tsconfig.build.json b/packages/multichain/tsconfig.build.json deleted file mode 100644 index f2108df2764..00000000000 --- a/packages/multichain/tsconfig.build.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../tsconfig.packages.build.json", - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist", - "rootDir": "./src", - "resolveJsonModule": true - }, - "references": [ - { - "path": "../network-controller/tsconfig.build.json" - }, - { - "path": "../permission-controller/tsconfig.build.json" - } - ], - "include": ["../../types", "./src"] -} diff --git a/packages/multichain/tsconfig.json b/packages/multichain/tsconfig.json deleted file mode 100644 index 34e1d4a7218..00000000000 --- a/packages/multichain/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.packages.json", - "compilerOptions": { - "baseUrl": "./" - }, - "references": [ - { - "path": "../network-controller" - }, - { - "path": "../permission-controller" - } - ], - "include": ["../../types", "./src"] -} diff --git a/packages/multichain/typedoc.json b/packages/multichain/typedoc.json deleted file mode 100644 index c9da015dbf8..00000000000 --- a/packages/multichain/typedoc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "entryPoints": ["./src/index.ts"], - "excludePrivate": true, - "hideGenerator": true, - "out": "docs", - "tsconfig": "./tsconfig.build.json" -} diff --git a/teams.json b/teams.json index 68c0d879fe2..725d81fd5b0 100644 --- a/teams.json +++ b/teams.json @@ -23,7 +23,6 @@ "metamask/keyring-controller": "team-accounts", "metamask/logging-controller": "team-confirmations", "metamask/message-manager": "team-confirmations", - "metamask/multichain": "team-wallet-api-platform", "metamask/multichain-api-middleware": "team-wallet-api-platform", "metamask/multichain-network-controller": "team-wallet-api-platform", "metamask/name-controller": "team-confirmations", diff --git a/tsconfig.build.json b/tsconfig.build.json index 312bbf40103..c5d38c31dae 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -32,7 +32,6 @@ { "path": "./packages/multichain-transactions-controller/tsconfig.build.json" }, - { "path": "./packages/multichain/tsconfig.build.json" }, { "path": "./packages/name-controller/tsconfig.build.json" }, { "path": "./packages/network-controller/tsconfig.build.json" }, { diff --git a/tsconfig.json b/tsconfig.json index b4268e7d965..5d6254f9b89 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,6 @@ { "path": "./packages/json-rpc-middleware-stream" }, { "path": "./packages/keyring-controller" }, { "path": "./packages/message-manager" }, - { "path": "./packages/multichain" }, { "path": "./packages/multichain-api-middleware" }, { "path": "./packages/multichain-network-controller" }, { "path": "./packages/multichain-transactions-controller" }, diff --git a/yarn.lock b/yarn.lock index 29d3d198197..7086adbdc7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3867,37 +3867,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/multichain@workspace:packages/multichain": - version: 0.0.0-use.local - resolution: "@metamask/multichain@workspace:packages/multichain" - dependencies: - "@metamask/api-specs": "npm:^0.14.0" - "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.10.0" - "@metamask/eth-json-rpc-filters": "npm:^9.0.0" - "@metamask/json-rpc-engine": "npm:^10.0.3" - "@metamask/network-controller": "npm:^23.6.0" - "@metamask/permission-controller": "npm:^11.0.6" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^11.2.0" - "@open-rpc/meta-schema": "npm:^1.14.6" - "@open-rpc/schema-utils-js": "npm:^2.0.5" - "@types/jest": "npm:^27.4.1" - deepmerge: "npm:^4.2.2" - jest: "npm:^27.5.1" - jsonschema: "npm:^1.4.1" - lodash: "npm:^4.17.21" - ts-jest: "npm:^27.1.4" - typedoc: "npm:^0.24.8" - typedoc-plugin-missing-exports: "npm:^2.0.0" - typescript: "npm:~5.2.2" - peerDependencies: - "@metamask/network-controller": ^23.0.0 - "@metamask/permission-controller": ^11.0.0 - languageName: unknown - linkType: soft - "@metamask/name-controller@workspace:packages/name-controller": version: 0.0.0-use.local resolution: "@metamask/name-controller@workspace:packages/name-controller"