From bc5610499b0e4a4323bba69ff796edb0a432191e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 14:02:49 -0700 Subject: [PATCH 01/80] Initial package --- README.md | 28 +- packages/multichain/CHANGELOG.md | 10 + packages/multichain/LICENSE | 20 + packages/multichain/README.md | 15 + packages/multichain/jest.config.js | 26 + packages/multichain/package.json | 59 ++ ...ip-permission-adapter-eth-accounts.test.ts | 208 ++++++ .../caip-permission-adapter-eth-accounts.ts | 97 +++ .../caip-permission-adapter-middleware.js | 50 ++ ...caip-permission-adapter-middleware.test.js | 134 ++++ ...permission-adapter-permittedChains.test.ts | 314 ++++++++ ...caip-permission-adapter-permittedChains.ts | 103 +++ .../multichain/src/caip25permissions.test.ts | 688 ++++++++++++++++++ packages/multichain/src/caip25permissions.ts | 251 +++++++ .../src/handlers/wallet-getSession.js | 37 + .../src/handlers/wallet-getSession.test.js | 99 +++ .../src/handlers/wallet-invokeMethod.js | 78 ++ .../src/handlers/wallet-invokeMethod.test.js | 262 +++++++ .../src/handlers/wallet-revokeSession.js | 29 + .../src/handlers/wallet-revokeSession.test.js | 80 ++ packages/multichain/src/index.test.ts | 9 + packages/multichain/src/index.ts | 9 + .../MultichainMiddlewareManager.test.ts | 173 +++++ .../MultichainMiddlewareManager.ts | 123 ++++ .../MultichainSubscriptionManager.test.ts | 124 ++++ .../MultichainSubscriptionManager.ts | 160 ++++ .../multichainMethodCallValidator.ts | 98 +++ packages/multichain/tsconfig.build.json | 10 + packages/multichain/tsconfig.json | 8 + packages/multichain/typedoc.json | 7 + tsconfig.build.json | 1 + tsconfig.json | 1 + yarn.lock | 15 + 33 files changed, 3315 insertions(+), 11 deletions(-) create mode 100644 packages/multichain/CHANGELOG.md create mode 100644 packages/multichain/LICENSE create mode 100644 packages/multichain/README.md create mode 100644 packages/multichain/jest.config.js create mode 100644 packages/multichain/package.json create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.js create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts create mode 100644 packages/multichain/src/caip25permissions.test.ts create mode 100644 packages/multichain/src/caip25permissions.ts create mode 100644 packages/multichain/src/handlers/wallet-getSession.js create mode 100644 packages/multichain/src/handlers/wallet-getSession.test.js create mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.js create mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.test.js create mode 100644 packages/multichain/src/handlers/wallet-revokeSession.js create mode 100644 packages/multichain/src/handlers/wallet-revokeSession.test.js create mode 100644 packages/multichain/src/index.test.ts create mode 100644 packages/multichain/src/index.ts create mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts create mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.ts create mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts create mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.ts create mode 100644 packages/multichain/src/middlewares/multichainMethodCallValidator.ts create mode 100644 packages/multichain/tsconfig.build.json create mode 100644 packages/multichain/tsconfig.json create mode 100644 packages/multichain/typedoc.json diff --git a/README.md b/README.md index e5961edab2..30b0d17682 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This repository contains the following packages [^fn1]: - [`@metamask/keyring-controller`](packages/keyring-controller) - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) +- [`@metamask/multichain`](packages/multichain) - [`@metamask/name-controller`](packages/name-controller) - [`@metamask/network-controller`](packages/network-controller) - [`@metamask/notification-controller`](packages/notification-controller) @@ -71,6 +72,7 @@ linkStyle default opacity:0.5 keyring_controller(["@metamask/keyring-controller"]); logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); + multichain(["@metamask/multichain"]); name_controller(["@metamask/name-controller"]); network_controller(["@metamask/network-controller"]); notification_controller(["@metamask/notification-controller"]); @@ -93,14 +95,15 @@ linkStyle default opacity:0.5 address_book_controller --> controller_utils; announcement_controller --> base_controller; approval_controller --> base_controller; - assets_controllers --> accounts_controller; - assets_controllers --> approval_controller; assets_controllers --> base_controller; assets_controllers --> controller_utils; + assets_controllers --> polling_controller; + assets_controllers --> accounts_controller; + assets_controllers --> approval_controller; assets_controllers --> keyring_controller; assets_controllers --> network_controller; - assets_controllers --> polling_controller; assets_controllers --> preferences_controller; + base_controller --> json_rpc_engine; chain_controller --> base_controller; composable_controller --> base_controller; composable_controller --> json_rpc_engine; @@ -110,8 +113,8 @@ linkStyle default opacity:0.5 eth_json_rpc_provider --> json_rpc_engine; gas_fee_controller --> base_controller; gas_fee_controller --> controller_utils; - gas_fee_controller --> network_controller; gas_fee_controller --> polling_controller; + gas_fee_controller --> network_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; keyring_controller --> message_manager; @@ -145,6 +148,9 @@ linkStyle default opacity:0.5 preferences_controller --> controller_utils; preferences_controller --> keyring_controller; profile_sync_controller --> base_controller; + profile_sync_controller --> keyring_controller; + profile_sync_controller --> accounts_controller; + profile_sync_controller --> network_controller; queued_request_controller --> base_controller; queued_request_controller --> controller_utils; queued_request_controller --> json_rpc_engine; @@ -155,26 +161,26 @@ linkStyle default opacity:0.5 selected_network_controller --> json_rpc_engine; selected_network_controller --> network_controller; selected_network_controller --> permission_controller; - signature_controller --> approval_controller; signature_controller --> base_controller; signature_controller --> controller_utils; + signature_controller --> message_manager; + signature_controller --> approval_controller; signature_controller --> keyring_controller; signature_controller --> logging_controller; - signature_controller --> message_manager; - transaction_controller --> accounts_controller; - transaction_controller --> approval_controller; transaction_controller --> base_controller; transaction_controller --> controller_utils; + transaction_controller --> accounts_controller; + transaction_controller --> approval_controller; + transaction_controller --> eth_json_rpc_provider; transaction_controller --> gas_fee_controller; transaction_controller --> network_controller; - transaction_controller --> eth_json_rpc_provider; - user_operation_controller --> approval_controller; user_operation_controller --> base_controller; user_operation_controller --> controller_utils; + user_operation_controller --> polling_controller; + user_operation_controller --> approval_controller; user_operation_controller --> gas_fee_controller; user_operation_controller --> keyring_controller; user_operation_controller --> network_controller; - user_operation_controller --> polling_controller; user_operation_controller --> transaction_controller; ``` diff --git a/packages/multichain/CHANGELOG.md b/packages/multichain/CHANGELOG.md new file mode 100644 index 0000000000..b518709c7b --- /dev/null +++ b/packages/multichain/CHANGELOG.md @@ -0,0 +1,10 @@ +# 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] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain/LICENSE b/packages/multichain/LICENSE new file mode 100644 index 0000000000..6f8bff03fc --- /dev/null +++ b/packages/multichain/LICENSE @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000000..dc89e0fade --- /dev/null +++ b/packages/multichain/README.md @@ -0,0 +1,15 @@ +# `@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 new file mode 100644 index 0000000000..ca08413339 --- /dev/null +++ b/packages/multichain/jest.config.js @@ -0,0 +1,26 @@ +/* + * 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 new file mode 100644 index 0000000000..b7f05dcc3e --- /dev/null +++ b/packages/multichain/package.json @@ -0,0 +1,59 @@ +{ + "name": "@metamask/multichain", + "version": "0.0.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": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain", + "publish:preview": "yarn npm publish --tag preview", + "test": "jest --reporters=jest-silent-reporter", + "test:clean": "jest --clearCache", + "test:verbose": "jest --verbose", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@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" + }, + "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 new file mode 100644 index 0000000000..b7014fe78e --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -0,0 +1,208 @@ +import { Caip25CaveatValue } from '../caip25permissions'; +import { + getEthAccounts, + setEthAccounts, +} from './caip-permission-adapter-eth-accounts'; + +describe('CAIP-25 eth_accounts adapters', () => { + describe('getEthAccounts', () => { + it('returns the unique set of EIP155 accounts from the CAIP-25 caveat value', () => { + const ethAccounts = getEthAccounts({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0x2', 'eip155:1:0x3'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + accounts: [ + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x4'], + }, + 'eip155:10': { + methods: [], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + accounts: ['wallet:eip155:0x5'], + }, + wallet: { + methods: [], + notifications: [], + accounts: ['wallet:eip155:0x6'], + }, + }, + isMultichainOrigin: false, + }); + + expect(ethAccounts).toStrictEqual([ + '0x1', + '0x2', + '0x4', + '0x3', + '0x100', + '0x5', + '0x6', + ]); + }); + }); + + 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': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0x2', 'eip155:1:0x3'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + accounts: [ + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x4'], + }, + 'eip155:10': { + methods: [], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, + wallet: { + methods: [], + notifications: [], + }, + }, + isMultichainOrigin: false, + }; + + const result = setEthAccounts(input, ['0x1', '0x2', '0x3']); + expect(result).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2', 'eip155:1:0x3'], + }, + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0x1', 'eip155:5:0x2', 'eip155:5:0x3'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + accounts: [ + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2', 'eip155:1:0x3'], + }, + 'eip155:10': { + methods: [], + notifications: [], + accounts: ['eip155:10:0x1', 'eip155:10:0x2', 'eip155:10:0x3'], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x1', 'eip155:100:0x2', 'eip155:100:0x3'], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + accounts: [ + 'wallet:eip155:0x1', + 'wallet:eip155:0x2', + 'wallet:eip155:0x3', + ], + }, + wallet: { + methods: [], + notifications: [], + accounts: [ + 'wallet:eip155:0x1', + 'wallet:eip155:0x2', + 'wallet:eip155:0x3', + ], + }, + }, + isMultichainOrigin: false, + }); + }); + + it('does not modify the input CAIP-25 caveat value object in place', () => { + const input: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: [], + }, + }, + optionalScopes: {}, + isMultichainOrigin: false, + }; + + const result = setEthAccounts(input, ['0x1', '0x2', '0x3']); + expect(input).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + 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 new file mode 100644 index 0000000000..7f515b5ec2 --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -0,0 +1,97 @@ +import { + CaipAccountId, + Hex, + KnownCaipNamespace, + parseCaipAccountId, +} from '@metamask/utils'; +import { Caip25CaveatValue } from '../caip25permissions'; +import { + mergeScopes, + parseScopeString, + ScopesObject, + ScopeString, +} from '../scope'; + +const isEip155ScopeString = (scopeString: ScopeString) => { + const { namespace, reference } = parseScopeString(scopeString); + + return ( + namespace === KnownCaipNamespace.Eip155 || + (namespace === KnownCaipNamespace.Wallet && + reference === KnownCaipNamespace.Eip155) + ); +}; + +export const getEthAccounts = (caip25CaveatValue: Caip25CaveatValue) => { + const ethAccounts: string[] = []; + const sessionScopes = mergeScopes( + caip25CaveatValue.requiredScopes, + caip25CaveatValue.optionalScopes, + ); + + Object.entries(sessionScopes).forEach(([_, { accounts }]) => { + accounts?.forEach((account) => { + const { address, chainId } = parseCaipAccountId(account); + + if (isEip155ScopeString(chainId)) { + ethAccounts.push(address); + } + }); + }); + + return Array.from(new Set(ethAccounts)); +}; + +const setEthAccountsForScopesObject = ( + scopesObject: ScopesObject, + accounts: Hex[], +) => { + const updatedScopesObject: ScopesObject = {}; + + Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { + const { namespace, reference } = parseScopeString(scopeString); + + const isWalletNamespace = + namespace === KnownCaipNamespace.Wallet && reference === undefined; + + if ( + !isEip155ScopeString(scopeString as ScopeString) && + !isWalletNamespace + ) { + updatedScopesObject[scopeString as ScopeString] = scopeObject; + return; + } + + const caipAccounts = accounts.map( + (account) => + (isWalletNamespace + ? `wallet:eip155:${account}` + : `${scopeString}:${account}`) as CaipAccountId, + ); + + updatedScopesObject[scopeString as ScopeString] = { + ...scopeObject, + accounts: caipAccounts, + }; + }); + + return updatedScopesObject; +}; + +// This helper must be called with existing eip155 scopes +export const setEthAccounts = ( + caip25CaveatValue: Caip25CaveatValue, + accounts: Hex[], +) => { + return { + ...caip25CaveatValue, + requiredScopes: setEthAccountsForScopesObject( + caip25CaveatValue.requiredScopes, + accounts, + ), + optionalScopes: setEthAccountsForScopesObject( + caip25CaveatValue.optionalScopes, + accounts, + ), + }; +}; diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.js b/packages/multichain/src/adapters/caip-permission-adapter-middleware.js new file mode 100644 index 0000000000..867288eb95 --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.js @@ -0,0 +1,50 @@ +import { providerErrors } from '@metamask/rpc-errors'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25permissions'; +import { mergeScopes } from '../scope'; + +export async function CaipPermissionAdapterMiddleware( + request, + _response, + next, + end, + hooks, +) { + const { networkClientId, method } = request; + + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (err) { + // noop + } + if (!caveat?.value?.isMultichainOrigin) { + return next(); + } + + const { chainId } = + hooks.getNetworkConfigurationByNetworkClientId(networkClientId); + + const scope = `eip155:${parseInt(chainId, 16)}`; + + const scopesObject = mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + ); + + if ( + !scopesObject[scope]?.methods?.includes(method) && + !scopesObject['wallet:eip155']?.methods?.includes(method) && + !scopesObject.wallet?.methods?.includes(method) + ) { + return end(providerErrors.unauthorized()); + } + + return next(); +} diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js new file mode 100644 index 0000000000..f8c0f98137 --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js @@ -0,0 +1,134 @@ +import { providerErrors } from '@metamask/rpc-errors'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25permissions'; +import { CaipPermissionAdapterMiddleware } from './caip-permission-adapter-middleware'; + +const baseRequest = { + origin: 'http://test.com', + networkClientId: 'mainnet', + method: 'eth_call', + params: { + foo: 'bar', + }, +}; + +const createMockedHandler = () => { + const next = jest.fn(); + const end = jest.fn(); + const getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + }, + unhandled: { + methods: ['foobar'], + notifications: [], + }, + }, + isMultichainOrigin: true, + }, + }); + const getNetworkConfigurationByNetworkClientId = jest + .fn() + .mockImplementation((networkClientId) => { + const chainId = + { + mainnet: '0x1', + goerli: '0x5', + }[networkClientId] || '0x999'; + return { + chainId, + }; + }); + const handler = (request) => + CaipPermissionAdapterMiddleware(request, {}, next, end, { + getCaveat, + getNetworkConfigurationByNetworkClientId, + }); + + return { + next, + end, + getCaveat, + getNetworkConfigurationByNetworkClientId, + handler, + }; +}; + +describe('CaipPermissionAdapterMiddleware', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const { handler, getCaveat } = createMockedHandler(); + await handler(baseRequest); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('allows the request when there is no CAIP-25 endowment permission', async () => { + const { handler, getCaveat, next } = createMockedHandler(); + getCaveat.mockImplementation(() => { + throw new Error('permission not found'); + }); + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + + it('allows the request when the CAIP-25 endowment permission was not granted from the multichain API', async () => { + const { handler, getCaveat, next } = createMockedHandler(); + getCaveat.mockReturnValue({ + value: { + isMultichainOrigin: false, + }, + }); + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + + it('gets the chainId for the request networkClientId', async () => { + const { handler, getNetworkConfigurationByNetworkClientId } = + createMockedHandler(); + await handler(baseRequest); + expect(getNetworkConfigurationByNetworkClientId).toHaveBeenCalledWith( + 'mainnet', + ); + }); + + describe('when the CAIP-25 endowment permission was granted over the multichain API', () => { + it('throws an error if the requested method is not authorized for the scope specified in the request', async () => { + const { handler, end } = createMockedHandler(); + + await handler({ + ...baseRequest, + method: 'unauthorized_method', + }); + expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); + }); + + it('allows the request if the requested scope method is authorized in the current scope', async () => { + const { handler, next } = createMockedHandler(); + + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts new file mode 100644 index 0000000000..aa125193ce --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -0,0 +1,314 @@ +import { Caip25CaveatValue } from '../caip25permissions'; +import { KnownNotifications, KnownRpcMethods } from '../scope'; +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': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0x2', 'eip155:1:0x3'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + accounts: [ + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x4'], + }, + 'eip155:10': { + methods: [], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + }, + isMultichainOrigin: false, + }); + + expect(ethChainIds).toStrictEqual(['0x1', '0x5', '0xa', '0x64']); + }); + }); + + describe('addPermittedEthChainId', () => { + it('adds an optional scope for the chainId if it does not already exist in required or optional scopes', () => { + const result = addPermittedEthChainId( + { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + }, + isMultichainOrigin: false, + }, + '0x65', + ); + + expect(result).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + 'eip155:101': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: [], + }, + }, + isMultichainOrigin: false, + }); + }); + + it('does not modify the input CAIP-25 caveat value object', () => { + const input: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: {}, + isMultichainOrigin: false, + }; + + const result = addPermittedEthChainId(input, '0x65'); + + expect(input).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + 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': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + 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': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + 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': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + }, + isMultichainOrigin: false, + }, + ['0x1'], + ); + + expect(result).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + }, + }, + isMultichainOrigin: false, + }); + }); + + it('returns a CAIP-25 caveat value with optional scopes added for missing chainIds', () => { + const result = setPermittedEthChainIds( + { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + }, + isMultichainOrigin: false, + }, + ['0x1', '0x64', '0x65'], + ); + + expect(result).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + }, + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + 'eip155:101': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: [], + }, + }, + isMultichainOrigin: false, + }); + }); + + it('does not modify the input CAIP-25 caveat value object', () => { + const input: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: {}, + isMultichainOrigin: false, + }; + + const result = setPermittedEthChainIds(input, ['0x1', '0x2', '0x3']); + + expect(input).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + 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 new file mode 100644 index 0000000000..8e840c6c32 --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -0,0 +1,103 @@ +import { Hex, KnownCaipNamespace } from '@metamask/utils'; +import { toHex } from '@metamask/controller-utils'; +import { Caip25CaveatValue } from '../caip25permissions'; +import { + KnownNotifications, + KnownRpcMethods, + mergeScopes, + parseScopeString, + ScopesObject, + ScopeString, +} from '../scope'; + +export const getPermittedEthChainIds = ( + caip25CaveatValue: Caip25CaveatValue, +) => { + const ethChainIds: Hex[] = []; + const sessionScopes = mergeScopes( + caip25CaveatValue.requiredScopes, + caip25CaveatValue.optionalScopes, + ); + + Object.keys(sessionScopes).forEach((scopeString) => { + const { namespace, reference } = parseScopeString(scopeString); + if (namespace === KnownCaipNamespace.Eip155 && reference) { + ethChainIds.push(toHex(reference)); + } + }); + + return Array.from(new Set(ethChainIds)); +}; + +export const addPermittedEthChainId = ( + caip25CaveatValue: Caip25CaveatValue, + chainId: Hex, +) => { + const scopeString = `eip155:${parseInt(chainId, 16)}`; + if ( + Object.keys(caip25CaveatValue.requiredScopes).includes(scopeString) || + Object.keys(caip25CaveatValue.optionalScopes).includes(scopeString) + ) { + return caip25CaveatValue; + } + + return { + ...caip25CaveatValue, + optionalScopes: { + ...caip25CaveatValue.optionalScopes, + [scopeString]: { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: [], + }, + }, + }; +}; + +const filterEthScopesObjectByChainId = ( + scopesObject: ScopesObject, + chainIds: Hex[], +) => { + const updatedScopesObject: ScopesObject = {}; + + Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { + const { namespace, reference } = parseScopeString(scopeString); + if (!reference) { + updatedScopesObject[scopeString as ScopeString] = scopeObject; + return; + } + if (namespace === KnownCaipNamespace.Eip155) { + const chainId = toHex(reference); + if (chainIds.includes(chainId)) { + updatedScopesObject[scopeString as ScopeString] = scopeObject; + } + } else { + updatedScopesObject[scopeString as ScopeString] = scopeObject; + } + }); + + return updatedScopesObject; +}; + +export const setPermittedEthChainIds = ( + caip25CaveatValue: Caip25CaveatValue, + chainIds: Hex[], +) => { + 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/caip25permissions.test.ts b/packages/multichain/src/caip25permissions.test.ts new file mode 100644 index 0000000000..97fce8f631 --- /dev/null +++ b/packages/multichain/src/caip25permissions.test.ts @@ -0,0 +1,688 @@ +import { + CaveatConstraint, + CaveatMutatorOperation, + PermissionType, + SubjectType, +} from '@metamask/permission-controller'; +import { NonEmptyArray } from '@metamask/controller-utils'; +import * as Scope from './scope'; +import { + Caip25CaveatType, + Caip25CaveatValue, + caip25EndowmentBuilder, + Caip25EndowmentPermissionName, + Caip25CaveatMutatorFactories, + removeScope, +} from './caip25permissions'; + +jest.mock('./scope', () => ({ + validateAndFlattenScopes: jest.fn(), + assertScopesSupported: jest.fn(), +})); +const MockScope = jest.mocked(Scope); + +const { removeAccount } = Caip25CaveatMutatorFactories[Caip25CaveatType]; + +describe('endowment:caip25', () => { + beforeEach(() => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: {}, + flattenedOptionalScopes: {}, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('builds the expected permission specification', () => { + const specification = caip25EndowmentBuilder.specificationBuilder({ + methodHooks: { + findNetworkClientIdByChainId: jest.fn(), + }, + }); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetName: Caip25EndowmentPermissionName, + endowmentGetter: expect.any(Function), + allowedCaveats: [Caip25CaveatType], + subjectTypes: [SubjectType.Website], + validator: expect.any(Function), + }); + + expect(specification.endowmentGetter()).toBeNull(); + }); + + describe('caveat mutator removeScope', () => { + it('can remove a caveat', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope('eip155:5', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.updateValue, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + }, + }, + optionalScopes: {}, + }, + }); + }); + + it('can revoke the entire permission when a requiredScope is removed', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope('eip155:1', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.revokePermission, + }); + }); + + it('can noop when nothing is removed', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope('eip155:2', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.noop, + }); + }); + }); + + describe('caveat mutator removeAccount', () => { + it('can remove an account', () => { + const ethereumGoerliCaveat: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: {}, + isMultichainOrigin: true, + }; + const result = removeAccount('0x1', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.updateValue, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x2'], + }, + }, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }); + + it('can remove an account in multiple scopes in optional and required', () => { + const ethereumGoerliCaveat: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + 'eip155:2': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:2:0x1', 'eip155:2:0x2'], + }, + }, + optionalScopes: { + 'eip155:3': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:3:0x1', 'eip155:3:0x2'], + }, + }, + isMultichainOrigin: true, + }; + const result = removeAccount('0x1', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.updateValue, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x2'], + }, + 'eip155:2': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:2:0x2'], + }, + }, + optionalScopes: { + 'eip155:3': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:3:0x2'], + }, + }, + isMultichainOrigin: true, + }, + }); + }); + + it('can noop when nothing is removed', () => { + const ethereumGoerliCaveat: Caip25CaveatValue = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + }, + }, + isMultichainOrigin: true, + }; + const result = removeAccount('0x3', ethereumGoerliCaveat); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.noop, + }); + }); + }); + + describe('permission validator', () => { + const findNetworkClientIdByChainId = jest.fn(); + const { validator } = caip25EndowmentBuilder.specificationBuilder({ + findNetworkClientIdByChainId, + }); + + 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({ + caveats: [] as unknown as NonEmptyArray, + 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}".`, + ), + ); + }); + + it('throws an error if the CAIP-25 caveat is malformed', () => { + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + missingRequiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + missingOptionalScopes: {}, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: 'NotABoolean', + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ), + ); + }); + + it('validates and flattens the ScopesObjects', () => { + try { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + } catch (err) { + // noop + } + expect(MockScope.validateAndFlattenScopes).toHaveBeenCalledWith( + { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + ); + }); + + it('asserts the validated and flattened required scopes are supported', () => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: { + 'eip155:1': { + methods: ['flattened_required'], + notifications: [], + }, + }, + flattenedOptionalScopes: { + 'eip155:1': { + methods: ['flattened_optional'], + notifications: [], + }, + }, + }); + try { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + } catch (err) { + // noop + } + expect(MockScope.assertScopesSupported).toHaveBeenCalledWith( + { + 'eip155:1': { + methods: ['flattened_required'], + notifications: [], + }, + }, + expect.objectContaining({ + isChainIdSupported: expect.any(Function), + }), + ); + const isChainIdSupportedBody = + MockScope.assertScopesSupported.mock.calls[0][1].isChainIdSupported.toString(); + expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); + }); + + it('asserts the validated and flattened optional scopes are supported', () => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: { + 'eip155:1': { + methods: ['flattened_required'], + notifications: [], + }, + }, + flattenedOptionalScopes: { + 'eip155:1': { + methods: ['flattened_optional'], + notifications: [], + }, + }, + }); + try { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + } catch (err) { + // noop + } + expect(MockScope.assertScopesSupported).toHaveBeenCalledWith( + { + 'eip155:1': { + methods: ['flattened_optional'], + notifications: [], + }, + }, + expect.objectContaining({ + isChainIdSupported: expect.any(Function), + }), + ); + const isChainIdSupportedBody = + MockScope.assertScopesSupported.mock.calls[1][1].isChainIdSupported.toString(); + expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); + }); + + it('throws if the input requiredScopes does not match the output of validateAndFlattenScopes', () => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: {}, + flattenedOptionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + }); + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow(/Expected values to be strictly deep-equal/u); + }); + + it('throws if the input optionalScopes does not match the output of validateAndFlattenScopes', () => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + flattenedOptionalScopes: {}, + }); + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow(/Expected values to be strictly deep-equal/u); + }); + + it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and flattened', () => { + MockScope.validateAndFlattenScopes.mockReturnValue({ + flattenedRequiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + flattenedOptionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + }); + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }); + }); +}); diff --git a/packages/multichain/src/caip25permissions.ts b/packages/multichain/src/caip25permissions.ts new file mode 100644 index 0000000000..335f17113a --- /dev/null +++ b/packages/multichain/src/caip25permissions.ts @@ -0,0 +1,251 @@ +import { strict as assert } from 'assert'; +import type { + PermissionSpecificationBuilder, + EndowmentGetterParams, + ValidPermissionSpecification, + PermissionValidatorConstraint, + PermissionConstraint, +} from '@metamask/permission-controller'; +import { + CaveatMutatorOperation, + PermissionType, + SubjectType, +} from '@metamask/permission-controller'; +import { + CaipAccountId, + Json, + parseCaipAccountId, + type Hex, + type NonEmptyArray, +} from '@metamask/utils'; +import { NetworkClientId } from '@metamask/network-controller'; +import { cloneDeep, isEqual } from 'lodash'; +import { + ExternalScopeString, + validateAndFlattenScopes, + ScopesObject, + ScopeObject, + assertScopesSupported, +} from './scope'; + +export type Caip25CaveatValue = { + requiredScopes: ScopesObject; + optionalScopes: ScopesObject; + sessionProperties?: Record; + isMultichainOrigin: boolean; +}; + +export const Caip25CaveatType = 'authorizedScopes'; + +export const Caip25CaveatFactoryFn = (value: Caip25CaveatValue) => { + return { + type: Caip25CaveatType, + value, + }; +}; + +export const Caip25EndowmentPermissionName = 'endowment:caip25'; + +type Caip25EndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof Caip25EndowmentPermissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + validator: PermissionValidatorConstraint; + allowedCaveats: Readonly> | null; +}>; + +/** + * `endowment:caip25` returns nothing atm; + * + * @param builderOptions - The specification builder options. + * @param builderOptions.findNetworkClientIdByChainId + * @returns The specification for the `caip25` endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + // TODO: FIX THIS + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + Caip25EndowmentSpecification +> = ({ + findNetworkClientIdByChainId, +}: { + findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; +}) => { + return { + permissionType: PermissionType.Endowment, + targetName: Caip25EndowmentPermissionName, + allowedCaveats: [Caip25CaveatType], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + subjectTypes: [SubjectType.Website], + validator: (permission: PermissionConstraint) => { + const caip25Caveat = permission.caveats?.[0]; + if ( + permission.caveats?.length !== 1 || + caip25Caveat?.type !== Caip25CaveatType + ) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Invalid caveats. There must be a single caveat of type "${Caip25CaveatType}".`, + ); + } + + // TODO: FIX THIS TYPE + const { requiredScopes, optionalScopes, isMultichainOrigin } = ( + caip25Caveat as unknown as { value: Caip25CaveatValue } + ).value; + + if ( + !requiredScopes || + !optionalScopes || + typeof isMultichainOrigin !== 'boolean' + ) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, + ); + } + + const { flattenedRequiredScopes, flattenedOptionalScopes } = + validateAndFlattenScopes(requiredScopes, optionalScopes); + + const isChainIdSupported = (chainId: Hex) => { + try { + findNetworkClientIdByChainId(chainId); + return true; + } catch (err) { + return false; + } + }; + + assertScopesSupported(flattenedRequiredScopes, { + isChainIdSupported, + }); + assertScopesSupported(flattenedOptionalScopes, { + isChainIdSupported, + }); + + assert.deepEqual(requiredScopes, flattenedRequiredScopes); + assert.deepEqual(optionalScopes, flattenedOptionalScopes); + }, + }; +}; + +export const caip25EndowmentBuilder = Object.freeze({ + targetName: Caip25EndowmentPermissionName, + specificationBuilder, +} as const); + +/** + * Factories that construct caveat mutator functions that are passed to + * PermissionController.updatePermissionsByCaveat. + */ +export const Caip25CaveatMutatorFactories = { + [Caip25CaveatType]: { + removeScope, + removeAccount, + }, +}; + +const reduceKeysHelper = ( + acc: Record, + [key, value]: [K, V], +) => { + return { + ...acc, + [key]: value, + }; +}; + +function removeAccountFilterFn(targetAddress: string) { + return (account: CaipAccountId) => { + const parsed = parseCaipAccountId(account); + return parsed.address !== targetAddress; + }; +} + +function removeAccountOnScope(targetAddress: string, scopeObject: ScopeObject) { + if (scopeObject.accounts) { + scopeObject.accounts = scopeObject.accounts.filter( + removeAccountFilterFn(targetAddress), + ); + } +} + +function removeAccount( + targetAddress: string, // non caip-10 formatted address + existingScopes: Caip25CaveatValue, +) { + // copy existing scopes + const copyOfExistingScopes = cloneDeep(existingScopes); + + [ + copyOfExistingScopes.requiredScopes, + copyOfExistingScopes.optionalScopes, + ].forEach((scopes) => { + Object.entries(scopes).forEach(([, scopeObject]) => { + removeAccountOnScope(targetAddress, scopeObject); + }); + }); + + // deep equal check for changes + const noChange = isEqual(copyOfExistingScopes, existingScopes); + + if (noChange) { + return { + operation: CaveatMutatorOperation.noop, + }; + } + + return { + operation: CaveatMutatorOperation.updateValue, + value: copyOfExistingScopes, + }; +} + +/** + * Removes the target account from the value arrays of all + * `endowment:caip25` caveats. No-ops if the target scopeString is not in + * the existing scopes,. + * + * @param targetScopeString - The scope that is being removed. + * @param caip25CaveatValue - The CAIP-25 permission caveat value to remove the scope from. + */ +export function removeScope( + targetScopeString: ExternalScopeString, + caip25CaveatValue: Caip25CaveatValue, +) { + 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) { + return { + operation: CaveatMutatorOperation.revokePermission, + }; + } + + if (optionalScopesRemoved) { + return { + operation: CaveatMutatorOperation.updateValue, + value: { + requiredScopes: newRequiredScopes.reduce(reduceKeysHelper, {}), + optionalScopes: newOptionalScopes.reduce(reduceKeysHelper, {}), + }, + }; + } + + return { + operation: CaveatMutatorOperation.noop, + }; +} diff --git a/packages/multichain/src/handlers/wallet-getSession.js b/packages/multichain/src/handlers/wallet-getSession.js new file mode 100644 index 0000000000..e10e278125 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-getSession.js @@ -0,0 +1,37 @@ +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25permissions'; +import { mergeScopes } from './scope'; + +export async function walletGetSessionHandler( + request, + response, + _next, + end, + hooks, +) { + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (e) { + // noop + } + + if (!caveat) { + response.result = { sessionScopes: {} }; + return end(); + } + + response.result = { + sessionScopes: mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + ), + }; + return end(); +} diff --git a/packages/multichain/src/handlers/wallet-getSession.test.js b/packages/multichain/src/handlers/wallet-getSession.test.js new file mode 100644 index 0000000000..de51365869 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-getSession.test.js @@ -0,0 +1,99 @@ +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25permissions'; +import { walletGetSessionHandler } from './wallet-getSession'; + +const baseRequest = { + origin: 'http://test.com', + params: {}, +}; + +const createMockedHandler = () => { + const next = jest.fn(); + const end = jest.fn(); + const getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: ['chainChanged'], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + }, + }, + }, + }); + const response = {}; + const handler = (request) => + walletGetSessionHandler(request, response, next, end, { + getCaveat, + }); + + return { + next, + response, + end, + getCaveat, + handler, + }; +}; + +describe('wallet_getSession', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const { handler, getCaveat } = createMockedHandler(); + + await handler(baseRequest); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('returns empty scopes if the CAIP-25 endowment permission does not exist', async () => { + const { handler, response, getCaveat } = createMockedHandler(); + getCaveat.mockImplementation(() => { + throw new Error('permission not found'); + }); + + await handler(baseRequest); + expect(response.result).toStrictEqual({ + sessionScopes: {}, + }); + }); + + it('returns the merged scopes', async () => { + const { handler, response } = createMockedHandler(); + + await handler(baseRequest); + expect(response.result).toStrictEqual({ + sessionScopes: { + 'eip155:1': { + methods: ['eth_call', 'net_version'], + notifications: ['chainChanged'], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + }, + }, + }); + }); +}); diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.js b/packages/multichain/src/handlers/wallet-invokeMethod.js new file mode 100644 index 0000000000..14b2043726 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-invokeMethod.js @@ -0,0 +1,78 @@ +import { numberToHex } from '@metamask/utils'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from './caip25permissions'; +import { mergeScopes, parseScopeString } from './scope'; + +export async function walletInvokeMethodHandler( + request, + _response, + next, + end, + hooks, +) { + const { scope, request: wrappedRequest } = request.params; + + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (e) { + // noop + } + if (!caveat?.value?.isMultichainOrigin) { + return end(providerErrors.unauthorized()); + } + + const scopeObject = mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + )[scope]; + + if (!scopeObject?.methods?.includes(wrappedRequest.method)) { + return end(providerErrors.unauthorized()); + } + + const { namespace, reference } = parseScopeString(scope); + + let networkClientId; + switch (namespace) { + case 'wallet': + networkClientId = hooks.getSelectedNetworkClientId(); + break; + case 'eip155': + if (reference) { + networkClientId = hooks.findNetworkClientIdByChainId( + numberToHex(parseInt(reference, 10)), + ); + } + break; + default: + console.error( + 'failed to resolve namespace for wallet_invokeMethod', + request, + ); + return end(rpcErrors.internal()); + } + + if (!networkClientId) { + console.error( + 'failed to resolve network client for wallet_invokeMethod', + request, + ); + return end(rpcErrors.internal()); + } + + Object.assign(request, { + scope, + networkClientId, + method: wrappedRequest.method, + params: wrappedRequest.params, + }); + return next(); +} diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.js b/packages/multichain/src/handlers/wallet-invokeMethod.test.js new file mode 100644 index 0000000000..dcf0d5f4ac --- /dev/null +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.js @@ -0,0 +1,262 @@ +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from './caip25permissions'; +import { walletInvokeMethodHandler } from './wallet-invokeMethod'; + +const createMockedRequest = () => ({ + origin: 'http://test.com', + params: { + scope: 'eip155:1', + request: { + method: 'eth_call', + params: { + foo: 'bar', + }, + }, + }, +}); + +const createMockedHandler = () => { + const next = jest.fn(); + const end = jest.fn(); + const getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + }, + unhandled: { + methods: ['foobar'], + notifications: [], + }, + }, + isMultichainOrigin: true, + }, + }); + const findNetworkClientIdByChainId = jest.fn().mockReturnValue('mainnet'); + const getSelectedNetworkClientId = jest + .fn() + .mockReturnValue('selectedNetworkClientId'); + const handler = (request) => + walletInvokeMethodHandler(request, {}, next, end, { + getCaveat, + findNetworkClientIdByChainId, + getSelectedNetworkClientId, + }); + + return { + next, + end, + getCaveat, + findNetworkClientIdByChainId, + getSelectedNetworkClientId, + handler, + }; +}; + +describe('wallet_invokeMethod', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const request = createMockedRequest(); + const { handler, getCaveat } = createMockedHandler(); + await handler(request); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('throws an unauthorized error when there is no CAIP-25 endowment permission', async () => { + const request = createMockedRequest(); + const { handler, getCaveat, end } = createMockedHandler(); + getCaveat.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, getCaveat, end } = createMockedHandler(); + getCaveat.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()); + }); + + it('throws an internal error for authorized but unhandled scopes', async () => { + const request = createMockedRequest(); + const { handler, end } = createMockedHandler(); + + await handler({ + ...request, + params: { + ...request.params, + scope: 'unhandled', + request: { + ...request.params.request, + method: 'foobar', + }, + }, + }); + + expect(end).toHaveBeenCalledWith(rpcErrors.internal()); + }); + + 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({ + 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({ + scope: 'wallet', + origin: 'http://test.com', + networkClientId: 'selectedNetworkClientId', + method: 'wallet_watchAsset', + params: { + foo: 'bar', + }, + }); + expect(next).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/multichain/src/handlers/wallet-revokeSession.js b/packages/multichain/src/handlers/wallet-revokeSession.js new file mode 100644 index 0000000000..e0425cf326 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-revokeSession.js @@ -0,0 +1,29 @@ +import { + PermissionDoesNotExistError, + UnrecognizedSubjectError, +} from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import { Caip25EndowmentPermissionName } from '../caip25permissions'; + +export async function walletRevokeSessionHandler( + request, + response, + _next, + end, + hooks, +) { + try { + hooks.revokePermission(request.origin, Caip25EndowmentPermissionName); + } catch (err) { + if ( + !(err instanceof UnrecognizedSubjectError) && + !(err instanceof PermissionDoesNotExistError) + ) { + console.error(err); + return end(rpcErrors.internal()); + } + } + + response.result = true; + return end(); +} diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.js b/packages/multichain/src/handlers/wallet-revokeSession.test.js new file mode 100644 index 0000000000..8acd84ac3d --- /dev/null +++ b/packages/multichain/src/handlers/wallet-revokeSession.test.js @@ -0,0 +1,80 @@ +import { + PermissionDoesNotExistError, + UnrecognizedSubjectError, +} from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import { Caip25EndowmentPermissionName } from '../caip25permissions'; +import { walletRevokeSessionHandler } from './wallet-revokeSession'; + +const baseRequest = { + origin: 'http://test.com', + params: {}, +}; + +const createMockedHandler = () => { + const next = jest.fn(); + const end = jest.fn(); + const revokePermission = jest.fn(); + const response = {}; + const handler = (request) => + walletRevokeSessionHandler(request, response, next, end, { + revokePermission, + }); + + return { + next, + response, + end, + revokePermission, + handler, + }; +}; + +describe('wallet_revokeSession', () => { + it('revokes the the CAIP-25 endowment permission', async () => { + const { handler, revokePermission } = createMockedHandler(); + + await handler(baseRequest); + expect(revokePermission).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + ); + }); + + it('returns true if the CAIP-25 endowment permission does not exist', async () => { + const { handler, response, revokePermission } = createMockedHandler(); + revokePermission.mockImplementation(() => { + throw new PermissionDoesNotExistError(); + }); + + await handler(baseRequest); + expect(response.result).toStrictEqual(true); + }); + + it('returns true if the subject does not exist', async () => { + const { handler, response, revokePermission } = createMockedHandler(); + revokePermission.mockImplementation(() => { + throw new UnrecognizedSubjectError(); + }); + + await handler(baseRequest); + expect(response.result).toStrictEqual(true); + }); + + it('throws an internal RPC error if something unexpected goes wrong with revoking the permission', async () => { + const { handler, revokePermission, end } = createMockedHandler(); + revokePermission.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).toStrictEqual(true); + }); +}); diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts new file mode 100644 index 0000000000..bc062d3694 --- /dev/null +++ b/packages/multichain/src/index.test.ts @@ -0,0 +1,9 @@ +import greeter from '.'; + +describe('Test', () => { + it('greets', () => { + const name = 'Huey'; + const result = greeter(name); + expect(result).toBe('Hello, Huey!'); + }); +}); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts new file mode 100644 index 0000000000..6972c11729 --- /dev/null +++ b/packages/multichain/src/index.ts @@ -0,0 +1,9 @@ +/** + * Example function that returns a greeting for the given name. + * + * @param name - The name to greet. + * @returns The greeting. + */ +export default function greeter(name: string): string { + return `Hello, ${name}!`; +} diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts new file mode 100644 index 0000000000..099e09537e --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts @@ -0,0 +1,173 @@ +import { JsonRpcRequest } from 'json-rpc-engine'; +import MultichainMiddlewareManager, { + ExtendedJsonRpcMiddleware, +} 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', () => { + 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( + { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0', id: 0 }, + nextSpy, + endSpy, + ); + expect(middlewareSpy).toHaveBeenCalledWith( + { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0', id: 0 }, + nextSpy, + endSpy, + ); + expect(nextSpy).not.toHaveBeenCalled(); + expect(endSpy).not.toHaveBeenCalled(); + }); + + it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed', () => { + 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, + ); + + middleware.destroy?.(); + + const nextSpy = jest.fn(); + const endSpy = jest.fn(); + + middleware( + { scope } as unknown as JsonRpcRequest, + { 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( + { scope } as unknown as JsonRpcRequest, + { 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( + { scope } as unknown as JsonRpcRequest, + { 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( + { scope } as unknown as JsonRpcRequest, + { 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 new file mode 100644 index 0000000000..f0b52b655e --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -0,0 +1,123 @@ +import { JsonRpcMiddleware } from 'json-rpc-engine'; +import { ExternalScopeString } from './scope'; + +// Extend JsonRpcMiddleware to include the destroy method +// this was introduced in 7.0.0 of json-rpc-engine: https://github.com/MetaMask/json-rpc-engine/blob/v7.0.0/src/JsonRpcEngine.ts#L29-L40 +export type ExtendedJsonRpcMiddleware = JsonRpcMiddleware & { + destroy?: () => void; +}; + +type MiddlewareKey = { + scope: ExternalScopeString; + origin: string; + tabId?: number; +}; +type MiddlewareEntry = MiddlewareKey & { + middleware: ExtendedJsonRpcMiddleware; +}; + +export default 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 }: MiddlewareKey) { + 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(middlewareKey: MiddlewareKey) { + const existingMiddlewareEntry = this.#getMiddlewareEntry(middlewareKey); + if (!existingMiddlewareEntry) { + return; + } + + existingMiddlewareEntry.middleware.destroy?.(); + + this.#removeMiddlewareEntry(middlewareKey); + } + + 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 r = req as unknown as { + scope: string; + }; + const { scope } = r; + const middlewareEntry = this.#getMiddlewareEntry({ + scope, + origin, + tabId, + }); + + if (middlewareEntry) { + middlewareEntry.middleware(req, res, next, end); + } else { + next(); + } + }; + 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 new file mode 100644 index 0000000000..f5e3c0147c --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -0,0 +1,124 @@ +import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; +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, + }); + const onNotificationSpy = jest.fn(); + + multichainSubscriptionManager.on('notification', onNotificationSpy); + + return { multichainSubscriptionManager, onNotificationSpy }; +}; + +describe('MultichainSubscriptionManager', () => { + const mockSubscriptionManager = { + events: { + on: jest.fn(), + }, + destroy: jest.fn(), + }; + + beforeEach(() => { + MockCreateSubscriptionManager.mockReturnValue(mockSubscriptionManager); + }); + + it('should subscribe to a scope, origin, and tabId', () => { + const { multichainSubscriptionManager, onNotificationSpy } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(onNotificationSpy).toHaveBeenCalledWith(origin, tabId, { + method: 'wallet_notify', + params: { + scope, + notification: newHeadsNotificationMock, + }, + }); + }); + + it('should unsubscribe from a scope', () => { + const { multichainSubscriptionManager, onNotificationSpy } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + multichainSubscriptionManager.unsubscribeByScope(scope); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(onNotificationSpy).not.toHaveBeenCalled(); + }); + + it('should unsubscribe from a scope and origin', () => { + const { multichainSubscriptionManager, onNotificationSpy } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + multichainSubscriptionManager.unsubscribeByScopeAndOrigin(scope, origin); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(onNotificationSpy).not.toHaveBeenCalled(); + }); + + it('should unsubscribe from a origin and tabId', () => { + const { multichainSubscriptionManager, onNotificationSpy } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(onNotificationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts new file mode 100644 index 0000000000..5cf94f0597 --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -0,0 +1,160 @@ +import EventEmitter from 'events'; +import { NetworkController } from '@metamask/network-controller'; +import SafeEventEmitter from '@metamask/safe-event-emitter'; +import { Hex, parseCaipChainId } from '@metamask/utils'; +import { toHex } from '@metamask/controller-utils'; +import { ExternalScopeString, ScopeString } from './scope'; + +export type SubscriptionManager = { + events: EventEmitter; + destroy?: () => void; +}; + +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; +}; + +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires +const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager'); + +type MultichainSubscriptionManagerOptions = { + findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + getNetworkClientById: NetworkController['getNetworkClientById']; +}; + +export default class MultichainSubscriptionManager extends SafeEventEmitter { + #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + + #getNetworkClientById: NetworkController['getNetworkClientById']; + + #subscriptions: SubscriptionEntry[] = []; + + constructor(options: MultichainSubscriptionManagerOptions) { + super(); + this.#findNetworkClientIdByChainId = options.findNetworkClientIdByChainId; + this.#getNetworkClientById = options.getNetworkClientById; + } + + onNotification( + { 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 }: SubscriptionKey) { + 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).reference), + ); + const networkClient = this.#getNetworkClientById(networkClientId); + const subscriptionManager = createSubscriptionManager({ + blockTracker: networkClient.blockTracker, + provider: networkClient.provider, + }); + + subscriptionManager.events.on( + 'notification', + (message: SubscriptionNotificationEvent) => { + this.onNotification(subscriptionKey, message); + }, + ); + + this.#subscriptions.push({ + ...subscriptionKey, + subscriptionManager, + }); + + return subscriptionManager; + } + + #unsubscribe(subscriptionKey: SubscriptionKey) { + const existingSubscriptionEntry = + this.#getSubscriptionEntry(subscriptionKey); + if (!existingSubscriptionEntry) { + return; + } + + existingSubscriptionEntry.subscriptionManager.destroy?.(); + + this.#removeSubscriptionEntry(subscriptionKey); + } + + unsubscribeByScope(scope: ScopeString) { + this.#subscriptions.forEach((subscriptionEntry) => { + if (subscriptionEntry.scope === scope) { + this.#unsubscribe(subscriptionEntry); + } + }); + } + + unsubscribeByScopeAndOrigin(scope: ScopeString, 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.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts new file mode 100644 index 0000000000..cff2841ecf --- /dev/null +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -0,0 +1,98 @@ +import { MultiChainOpenRPCDocument } from '@metamask/api-specs'; +import { rpcErrors } from '@metamask/rpc-errors'; +import { + JsonRpcError, + JsonRpcParams, + JsonRpcRequest, + isObject, +} from '@metamask/utils'; +import { + ContentDescriptorObject, + MethodObject, + OpenrpcDocument, +} 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 { Json, JsonRpcMiddleware } from 'json-rpc-engine'; +import { Schema, ValidationError, 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 { + code: -32602, // TODO: could be a different error code or not wrapped in json-rpc error, since this will also be wrapped in a -32602 invalid params error + 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({}), +); +export const multichainMethodCallValidator = async ( + method: string, + params: JsonRpcParams | undefined, +) => { + const dereffed = await dereffedPromise; + const methodToCheck = dereffed.methods.find( + (m) => (m as unknown as ContentDescriptorObject).name === method, + ); + const errors: JsonRpcError[] = []; + // check each param and aggregate errors + (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { + let paramToCheck: Json | undefined; + const p = param as ContentDescriptorObject; + if (isObject(params)) { + paramToCheck = params[p.name]; + } else if (params && Array.isArray(params)) { + paramToCheck = params[i]; + } else { + paramToCheck = undefined; + } + 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; + }), + ); + } + }); + if (errors.length > 0) { + return errors; + } + // feels like this should return true to indicate that its valid but i'd rather check the falsy value since errors + // would be an array and return true if it's empty + return false; +}; + +export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< + JsonRpcRequest, + void +> = function (request, _response, next, end) { + multichainMethodCallValidator(request.method, request.params).then( + (errors) => { + if (errors) { + return end(rpcErrors.invalidParams({ data: errors })); + } + return next(); + }, + ); +}; diff --git a/packages/multichain/tsconfig.build.json b/packages/multichain/tsconfig.build.json new file mode 100644 index 0000000000..02a0eea03f --- /dev/null +++ b/packages/multichain/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain/tsconfig.json b/packages/multichain/tsconfig.json new file mode 100644 index 0000000000..025ba2ef7f --- /dev/null +++ b/packages/multichain/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain/typedoc.json b/packages/multichain/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/multichain/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 4e485ea189..6102878c56 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -18,6 +18,7 @@ { "path": "./packages/keyring-controller/tsconfig.build.json" }, { "path": "./packages/logging-controller/tsconfig.build.json" }, { "path": "./packages/message-manager/tsconfig.build.json" }, + { "path": "./packages/multichain/tsconfig.build.json" }, { "path": "./packages/name-controller/tsconfig.build.json" }, { "path": "./packages/network-controller/tsconfig.build.json" }, { "path": "./packages/notification-controller/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index f886671a63..127a643b9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ { "path": "./packages/json-rpc-middleware-stream" }, { "path": "./packages/keyring-controller" }, { "path": "./packages/message-manager" }, + { "path": "./packages/multichain" }, { "path": "./packages/name-controller" }, { "path": "./packages/network-controller" }, { "path": "./packages/notification-controller" }, diff --git a/yarn.lock b/yarn.lock index 06a9e70928..d59a9c957f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3010,6 +3010,21 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain@workspace:packages/multichain": + version: 0.0.0-use.local + resolution: "@metamask/multichain@workspace:packages/multichain" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + 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" From a4b52ad4a567414d4b25098ddd37339a586937f4 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 14:17:39 -0700 Subject: [PATCH 02/80] Fix caip25Permission --- packages/multichain/package.json | 9 +++++++++ ...ermissions.test.ts => caip25Permission.test.ts} | 14 +++++++------- .../{caip25permissions.ts => caip25Permission.ts} | 10 +++++----- packages/multichain/tsconfig.json | 9 ++++++++- yarn.lock | 6 ++++++ 5 files changed, 35 insertions(+), 13 deletions(-) rename packages/multichain/src/{caip25permissions.test.ts => caip25Permission.test.ts} (98%) rename packages/multichain/src/{caip25permissions.ts => caip25Permission.ts} (96%) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index b7f05dcc3e..9d65fa1eab 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -39,8 +39,13 @@ "test:verbose": "jest --verbose", "test:watch": "jest --watch" }, + "dependencies": { + "lodash": "^4.17.21" + }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/network-controller": "^21.0.1", + "@metamask/permission-controller": "^11.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", @@ -49,6 +54,10 @@ "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "~5.2.2" }, + "peerDependencies": { + "@metamask/network-controller": "^21.0.0", + "@metamask/permission-controller": "^11.0.0" + }, "engines": { "node": "^18.18 || >=20" }, diff --git a/packages/multichain/src/caip25permissions.test.ts b/packages/multichain/src/caip25Permission.test.ts similarity index 98% rename from packages/multichain/src/caip25permissions.test.ts rename to packages/multichain/src/caip25Permission.test.ts index 97fce8f631..cf63cf0a0c 100644 --- a/packages/multichain/src/caip25permissions.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -13,7 +13,7 @@ import { Caip25EndowmentPermissionName, Caip25CaveatMutatorFactories, removeScope, -} from './caip25permissions'; +} from './caip25Permission'; jest.mock('./scope', () => ({ validateAndFlattenScopes: jest.fn(), @@ -73,7 +73,7 @@ describe('endowment:caip25', () => { }; const result = removeScope('eip155:5', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.updateValue, + operation: CaveatMutatorOperation.UpdateValue, value: { requiredScopes: { 'eip155:1': { @@ -105,7 +105,7 @@ describe('endowment:caip25', () => { }; const result = removeScope('eip155:1', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.revokePermission, + operation: CaveatMutatorOperation.RevokePermission, }); }); @@ -128,7 +128,7 @@ describe('endowment:caip25', () => { }; const result = removeScope('eip155:2', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.noop, + operation: CaveatMutatorOperation.Noop, }); }); }); @@ -148,7 +148,7 @@ describe('endowment:caip25', () => { }; const result = removeAccount('0x1', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.updateValue, + operation: CaveatMutatorOperation.UpdateValue, value: { requiredScopes: { 'eip155:1': { @@ -188,7 +188,7 @@ describe('endowment:caip25', () => { }; const result = removeAccount('0x1', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.updateValue, + operation: CaveatMutatorOperation.UpdateValue, value: { requiredScopes: { 'eip155:1': { @@ -233,7 +233,7 @@ describe('endowment:caip25', () => { }; const result = removeAccount('0x3', ethereumGoerliCaveat); expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.noop, + operation: CaveatMutatorOperation.Noop, }); }); }); diff --git a/packages/multichain/src/caip25permissions.ts b/packages/multichain/src/caip25Permission.ts similarity index 96% rename from packages/multichain/src/caip25permissions.ts rename to packages/multichain/src/caip25Permission.ts index 335f17113a..35312b2e11 100644 --- a/packages/multichain/src/caip25permissions.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -191,12 +191,12 @@ function removeAccount( if (noChange) { return { - operation: CaveatMutatorOperation.noop, + operation: CaveatMutatorOperation.Noop, }; } return { - operation: CaveatMutatorOperation.updateValue, + operation: CaveatMutatorOperation.UpdateValue, value: copyOfExistingScopes, }; } @@ -231,13 +231,13 @@ export function removeScope( if (requiredScopesRemoved) { return { - operation: CaveatMutatorOperation.revokePermission, + operation: CaveatMutatorOperation.RevokePermission, }; } if (optionalScopesRemoved) { return { - operation: CaveatMutatorOperation.updateValue, + operation: CaveatMutatorOperation.UpdateValue, value: { requiredScopes: newRequiredScopes.reduce(reduceKeysHelper, {}), optionalScopes: newOptionalScopes.reduce(reduceKeysHelper, {}), @@ -246,6 +246,6 @@ export function removeScope( } return { - operation: CaveatMutatorOperation.noop, + operation: CaveatMutatorOperation.Noop, }; } diff --git a/packages/multichain/tsconfig.json b/packages/multichain/tsconfig.json index 025ba2ef7f..34e1d4a721 100644 --- a/packages/multichain/tsconfig.json +++ b/packages/multichain/tsconfig.json @@ -3,6 +3,13 @@ "compilerOptions": { "baseUrl": "./" }, - "references": [], + "references": [ + { + "path": "../network-controller" + }, + { + "path": "../permission-controller" + } + ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index d59a9c957f..93b7cecb5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3015,13 +3015,19 @@ __metadata: resolution: "@metamask/multichain@workspace:packages/multichain" dependencies: "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/network-controller": "npm:^21.0.1" + "@metamask/permission-controller": "npm:^11.0.2" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.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": ^21.0.0 + "@metamask/permission-controller": ^11.0.0 languageName: unknown linkType: soft From 86ab58a2690b7808304a29840ddc94d672c7420c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 14:40:07 -0700 Subject: [PATCH 03/80] scopes --- packages/multichain/package.json | 3 + packages/multichain/src/scope/assert.test.ts | 209 +++++++++++++ packages/multichain/src/scope/assert.ts | 73 +++++ .../src/scope/authorization.test.ts | 219 ++++++++++++++ .../multichain/src/scope/authorization.ts | 68 +++++ packages/multichain/src/scope/filter.test.ts | 153 ++++++++++ packages/multichain/src/scope/filter.ts | 43 +++ packages/multichain/src/scope/index.ts | 7 + packages/multichain/src/scope/scope.test.ts | 23 ++ packages/multichain/src/scope/scope.ts | 96 ++++++ .../multichain/src/scope/supported.test.ts | 96 ++++++ packages/multichain/src/scope/supported.ts | 126 ++++++++ .../multichain/src/scope/transform.test.ts | 283 ++++++++++++++++++ packages/multichain/src/scope/transform.ts | 119 ++++++++ .../multichain/src/scope/validation.test.ts | 177 +++++++++++ packages/multichain/src/scope/validation.ts | 101 +++++++ yarn.lock | 10 + 17 files changed, 1806 insertions(+) create mode 100644 packages/multichain/src/scope/assert.test.ts create mode 100644 packages/multichain/src/scope/assert.ts create mode 100644 packages/multichain/src/scope/authorization.test.ts create mode 100644 packages/multichain/src/scope/authorization.ts create mode 100644 packages/multichain/src/scope/filter.test.ts create mode 100644 packages/multichain/src/scope/filter.ts create mode 100644 packages/multichain/src/scope/index.ts create mode 100644 packages/multichain/src/scope/scope.test.ts create mode 100644 packages/multichain/src/scope/scope.ts create mode 100644 packages/multichain/src/scope/supported.test.ts create mode 100644 packages/multichain/src/scope/supported.ts create mode 100644 packages/multichain/src/scope/transform.test.ts create mode 100644 packages/multichain/src/scope/transform.ts create mode 100644 packages/multichain/src/scope/validation.test.ts create mode 100644 packages/multichain/src/scope/validation.ts diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 9d65fa1eab..c284234ed5 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -40,10 +40,13 @@ "test:watch": "jest --watch" }, "dependencies": { + "@metamask/api-specs": "^0.10.12", + "@metamask/rpc-errors": "^6.3.1", "lodash": "^4.17.21" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/json-rpc-engine": "^9.0.3", "@metamask/network-controller": "^21.0.1", "@metamask/permission-controller": "^11.0.2", "@types/jest": "^27.4.1", diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts new file mode 100644 index 0000000000..919b6e6a38 --- /dev/null +++ b/packages/multichain/src/scope/assert.test.ts @@ -0,0 +1,209 @@ +import { JsonRpcError } from '@metamask/rpc-errors'; +import { assertScopeSupported, assertScopesSupported } from './assert'; +import { ScopeObject } from './scope'; +import * as Supported from './supported'; + +jest.mock('./supported', () => ({ + isSupportedScopeString: jest.fn(), + isSupportedNotification: jest.fn(), + isSupportedMethod: jest.fn(), +})); +const MockSupported = jest.mocked(Supported); + +const validScopeObject: ScopeObject = { + methods: [], + notifications: [], +}; + +describe('Scope Assert', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('assertScopeSupported', () => { + const isChainIdSupported = jest.fn(); + + describe('scopeString', () => { + it('checks if the scopeString is supported', () => { + try { + assertScopeSupported('scopeString', validScopeObject, { + isChainIdSupported, + }); + } catch (err) { + // noop + } + expect(MockSupported.isSupportedScopeString).toHaveBeenCalledWith( + 'scopeString', + isChainIdSupported, + ); + }); + + it('throws an error if the scopeString is not supported', () => { + MockSupported.isSupportedScopeString.mockReturnValue(false); + expect(() => { + assertScopeSupported('scopeString', validScopeObject, { + isChainIdSupported, + }); + }).toThrow( + new JsonRpcError(5100, 'Requested chains are not supported'), + ); + }); + }); + + describe('scopeObject', () => { + beforeEach(() => { + MockSupported.isSupportedScopeString.mockReturnValue(true); + }); + + it('checks if the methods are supported', () => { + try { + assertScopeSupported( + 'scopeString', + { + ...validScopeObject, + methods: ['eth_chainId'], + }, + { + isChainIdSupported, + }, + ); + } catch (err) { + // noop + } + + expect(MockSupported.isSupportedMethod).toHaveBeenCalledWith( + 'scopeString', + 'eth_chainId', + ); + }); + + it('throws an error if there are unsupported methods', () => { + MockSupported.isSupportedMethod.mockReturnValue(false); + expect(() => { + assertScopeSupported( + 'scopeString', + { + ...validScopeObject, + methods: ['eth_chainId'], + }, + { + isChainIdSupported, + }, + ); + }).toThrow( + new JsonRpcError(5101, 'Requested methods are not supported'), + ); + }); + + it('checks if the notifications are supported', () => { + MockSupported.isSupportedMethod.mockReturnValue(true); + try { + assertScopeSupported( + 'scopeString', + { + ...validScopeObject, + notifications: ['chainChanged'], + }, + { + isChainIdSupported, + }, + ); + } 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'], + }, + { + isChainIdSupported, + }, + ); + }).toThrow( + new JsonRpcError( + 5102, + 'Requested notifications are not supported', + ), + ); + }); + + 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'], + }, + { + isChainIdSupported, + }, + ), + ).toBeUndefined(); + }); + }); + }); + + describe('assertScopesSupported', () => { + const isChainIdSupported = jest.fn(); + + it('does not throw an error if no scopes are defined', () => { + assertScopesSupported( + {}, + { + isChainIdSupported, + }, + ); + }); + + it('throws an error if any scope is invalid', () => { + MockSupported.isSupportedScopeString.mockReturnValue(false); + + expect(() => { + assertScopesSupported( + { + 'eip155:1': validScopeObject, + }, + { + isChainIdSupported, + }, + ); + }).toThrow( + new JsonRpcError(5100, 'Requested chains are not supported'), + ); + }); + + it('does not throw an error if all scopes are valid', () => { + MockSupported.isSupportedScopeString.mockReturnValue(true); + + expect( + assertScopesSupported( + { + 'eip155:1': validScopeObject, + 'eip155:2': validScopeObject, + }, + { + isChainIdSupported, + }, + ), + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts new file mode 100644 index 0000000000..2724ecd221 --- /dev/null +++ b/packages/multichain/src/scope/assert.ts @@ -0,0 +1,73 @@ +import { Hex } from '@metamask/utils'; +import { JsonRpcError } from '@metamask/rpc-errors'; +import { + isSupportedMethod, + isSupportedNotification, + isSupportedScopeString, +} from './supported'; +import { ScopeObject, ScopesObject } from './scope'; + +export const assertScopeSupported = ( + scopeString: string, + scopeObject: ScopeObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + const { methods, notifications } = scopeObject; + if (!isSupportedScopeString(scopeString, isChainIdSupported)) { + throw new JsonRpcError(5100, 'Requested chains are not supported'); + } + + const allMethodsSupported = methods.every((method) => + isSupportedMethod(scopeString, method), + ); + + if (!allMethodsSupported) { + // not sure which one of these to use + // When provider evaluates requested methods to not be supported + // code = 5101 + // message = "Requested methods are not supported" + // When provider does not recognize one or more requested method(s) + // code = 5201 + // message = "Unknown method(s) requested" + + throw new JsonRpcError(5101, 'Requested methods are not supported'); + } + + if ( + notifications && + !notifications.every((notification) => + isSupportedNotification(scopeString, notification), + ) + ) { + // not sure which one of these to use + // When provider evaluates requested notifications to not be supported + // code = 5102 + // message = "Requested notifications are not supported" + // When provider does not recognize one or more requested notification(s) + // code = 5202 + // message = "Unknown notification(s) requested" + throw new JsonRpcError( + 5102, + 'Requested notifications are not supported', + ); + } +}; + +export const assertScopesSupported = ( + scopes: ScopesObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + for (const [scopeString, scopeObject] of Object.entries(scopes)) { + assertScopeSupported(scopeString, scopeObject, { + isChainIdSupported, + }); + } +}; diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts new file mode 100644 index 0000000000..69e57dc32c --- /dev/null +++ b/packages/multichain/src/scope/authorization.test.ts @@ -0,0 +1,219 @@ +import * as Validation from './validation'; +import * as Transform from './transform'; +import * as Filter from './filter'; +import { + bucketScopes, + validateAndFlattenScopes, +} from './authorization'; +import { ExternalScopeObject } from './scope'; + +jest.mock('./validation', () => ({ + validateScopes: jest.fn(), +})); +const MockValidation = jest.mocked(Validation); + +jest.mock('./transform', () => ({ + flattenMergeScopes: jest.fn(), +})); +const MockTransform = jest.mocked(Transform); + +jest.mock('./filter', () => ({ + bucketScopesBySupport: jest.fn(), +})); +const MockFilter = jest.mocked(Filter); + +const validScopeObject: ExternalScopeObject = { + methods: [], + notifications: [], +}; + +describe('Scope Authorization', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('validateAndFlattenScopes', () => { + it('validates the scopes', () => { + try { + validateAndFlattenScopes( + { + 'eip155:1': validScopeObject, + }, + { + 'eip155:5': validScopeObject, + }, + ); + } catch (err) { + // noop + } + expect(MockValidation.validateScopes).toHaveBeenCalledWith( + { + 'eip155:1': validScopeObject, + }, + { + 'eip155:5': validScopeObject, + }, + ); + }); + + it('flatten and merges the validated scopes', () => { + MockValidation.validateScopes.mockReturnValue({ + validRequiredScopes: { + 'eip155:1': validScopeObject, + }, + validOptionalScopes: { + 'eip155:5': validScopeObject, + }, + }); + + validateAndFlattenScopes({}, {}); + expect(MockTransform.flattenMergeScopes).toHaveBeenCalledWith({ + 'eip155:1': validScopeObject, + }); + expect(MockTransform.flattenMergeScopes).toHaveBeenCalledWith({ + 'eip155:5': validScopeObject, + }); + }); + + it('returns the flattened and merged scopes', () => { + MockValidation.validateScopes.mockReturnValue({ + validRequiredScopes: { + 'eip155:1': validScopeObject, + }, + validOptionalScopes: { + 'eip155:5': validScopeObject, + }, + }); + MockTransform.flattenMergeScopes.mockImplementation((value) => ({ + ...value, + transformed: true, + })); + + expect(validateAndFlattenScopes({}, {})).toStrictEqual({ + flattenedRequiredScopes: { + 'eip155:1': validScopeObject, + transformed: true, + }, + flattenedOptionalScopes: { + 'eip155:5': validScopeObject, + transformed: true, + }, + }); + }); + }); + + describe('bucketScopes', () => { + beforeEach(() => { + let callCount = 0; + MockFilter.bucketScopesBySupport.mockImplementation(() => { + callCount += 1; + return { + supportedScopes: { + 'mock:A': { + methods: [`mock_method_${callCount}`], + notifications: [], + }, + }, + unsupportedScopes: { + 'mock:B': { + methods: [`mock_method_${callCount}`], + notifications: [], + }, + }, + }; + }); + }); + + it('buckets the scopes by supported', () => { + const isChainIdSupported = jest.fn(); + bucketScopes( + { + wallet: { + methods: [], + notifications: [], + }, + }, + { + isChainIdSupported, + isChainIdSupportable: jest.fn(), + }, + ); + + expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( + { + wallet: { + methods: [], + notifications: [], + }, + }, + { + isChainIdSupported, + }, + ); + }); + + it('buckets the mayble supportable scopes', () => { + const isChainIdSupportable = jest.fn(); + bucketScopes( + { + wallet: { + methods: [], + notifications: [], + }, + }, + { + isChainIdSupported: jest.fn(), + isChainIdSupportable, + }, + ); + + expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( + { + 'mock:B': { + methods: [`mock_method_1`], + notifications: [], + }, + }, + { + isChainIdSupported: isChainIdSupportable, + }, + ); + }); + + it('returns the bucketed scopes', () => { + expect( + bucketScopes( + { + wallet: { + methods: [], + notifications: [], + }, + }, + { + isChainIdSupported: jest.fn(), + isChainIdSupportable: jest.fn(), + }, + ), + ).toStrictEqual({ + supportedScopes: { + 'mock:A': { + methods: [`mock_method_1`], + notifications: [], + }, + }, + supportableScopes: { + 'mock:A': { + methods: [`mock_method_2`], + notifications: [], + }, + }, + unsupportableScopes: { + 'mock:B': { + methods: [`mock_method_2`], + notifications: [], + }, + }, + }); + }); + }); +}); diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts new file mode 100644 index 0000000000..b6c83cb1cf --- /dev/null +++ b/packages/multichain/src/scope/authorization.ts @@ -0,0 +1,68 @@ +import { validateScopes } from './validation'; +import { ExternalScopesObject, ScopesObject, ScopedProperties } from './scope'; +import { flattenMergeScopes } from './transform'; +import { bucketScopesBySupport } from './filter'; +import { Hex } from '@metamask/utils'; + +export type Caip25Authorization = + | { + requiredScopes: ExternalScopesObject; + optionalScopes?: ExternalScopesObject; + sessionProperties?: Record; + } + | ({ + requiredScopes?: ExternalScopesObject; + optionalScopes: ExternalScopesObject; + } & { + sessionProperties?: Record; + }); + +export const validateAndFlattenScopes = ( + requiredScopes: ExternalScopesObject, + optionalScopes: ExternalScopesObject, +): { + flattenedRequiredScopes: ScopesObject; + flattenedOptionalScopes: ScopesObject; +} => { + const { validRequiredScopes, validOptionalScopes } = validateScopes( + requiredScopes, + optionalScopes, + ); + + const flattenedRequiredScopes = flattenMergeScopes(validRequiredScopes); + const flattenedOptionalScopes = flattenMergeScopes(validOptionalScopes); + + return { + flattenedRequiredScopes, + flattenedOptionalScopes, + }; +}; + +export const bucketScopes = ( + scopes: ScopesObject, + { + isChainIdSupported, + isChainIdSupportable, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + isChainIdSupportable: (chainId: Hex) => boolean; + }, +): { + supportedScopes: ScopesObject; + supportableScopes: ScopesObject; + unsupportableScopes: ScopesObject; +} => { + const { supportedScopes, unsupportedScopes: maybeSupportableScopes } = + bucketScopesBySupport(scopes, { + isChainIdSupported, + }); + + const { + supportedScopes: supportableScopes, + unsupportedScopes: unsupportableScopes, + } = bucketScopesBySupport(maybeSupportableScopes, { + isChainIdSupported: isChainIdSupportable, + }); + + return { supportedScopes, supportableScopes, unsupportableScopes }; +}; diff --git a/packages/multichain/src/scope/filter.test.ts b/packages/multichain/src/scope/filter.test.ts new file mode 100644 index 0000000000..cf7c492583 --- /dev/null +++ b/packages/multichain/src/scope/filter.test.ts @@ -0,0 +1,153 @@ +import * as Assert from './assert'; +import { filterScopesSupported, bucketScopesBySupport } from './filter'; + +jest.mock('./assert', () => ({ + assertScopeSupported: jest.fn(), +})); +const MockAssert = jest.mocked(Assert); + +describe('filter', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('filterScopesSupported', () => { + const isChainIdSupported = jest.fn(); + + it('checks if each scope is supported', () => { + filterScopesSupported( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }, + { isChainIdSupported }, + ); + + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:1', + { + methods: ['a'], + notifications: [], + }, + { isChainIdSupported }, + ); + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:5', + { + methods: ['b'], + notifications: [], + }, + { isChainIdSupported }, + ); + }); + + it('returns only supported scopes', () => { + MockAssert.assertScopeSupported.mockImplementation((scopeString) => { + if (scopeString === 'eip155:1') { + throw new Error('scope not supported'); + } + }); + + expect( + filterScopesSupported( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }, + { isChainIdSupported }, + ), + ).toStrictEqual({ + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }); + }); + }); + + describe('bucketScopesBySupport', () => { + const isChainIdSupported = jest.fn(); + + it('checks if each scope is supported', () => { + bucketScopesBySupport( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }, + { isChainIdSupported }, + ); + + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:1', + { + methods: ['a'], + notifications: [], + }, + { isChainIdSupported }, + ); + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:5', + { + methods: ['b'], + notifications: [], + }, + { isChainIdSupported }, + ); + }); + + 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: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }, + { isChainIdSupported }, + ), + ).toStrictEqual({ + supportedScopes: { + 'eip155:5': { + methods: ['b'], + notifications: [], + }, + }, + unsupportedScopes: { + 'eip155:1': { + methods: ['a'], + notifications: [], + }, + }, + }); + }); + }); +}); diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts new file mode 100644 index 0000000000..06b9795c49 --- /dev/null +++ b/packages/multichain/src/scope/filter.ts @@ -0,0 +1,43 @@ +import { CaipChainId, Hex } from '@metamask/utils'; +import { ScopesObject } from './scope'; +import { assertScopeSupported } from './assert'; + +export const bucketScopesBySupport = ( + scopes: ScopesObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + const supportedScopes: ScopesObject = {}; + const unsupportedScopes: ScopesObject = {}; + + for (const [scopeString, scopeObject] of Object.entries(scopes)) { + try { + assertScopeSupported(scopeString, scopeObject, { + isChainIdSupported, + }); + supportedScopes[scopeString as CaipChainId] = scopeObject; + } catch (err) { + unsupportedScopes[scopeString as CaipChainId] = scopeObject; + } + } + + return { supportedScopes, unsupportedScopes }; +}; + +export const filterScopesSupported = ( + scopes: ScopesObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + const { supportedScopes } = bucketScopesBySupport(scopes, { + isChainIdSupported, + }); + + return supportedScopes; +}; diff --git a/packages/multichain/src/scope/index.ts b/packages/multichain/src/scope/index.ts new file mode 100644 index 0000000000..c1b804efec --- /dev/null +++ b/packages/multichain/src/scope/index.ts @@ -0,0 +1,7 @@ +export * from './assert'; +export * from './authorization'; +export * from './filter'; +export * from './scope'; +export * from './supported'; +export * from './transform'; +export * from './validation'; diff --git a/packages/multichain/src/scope/scope.test.ts b/packages/multichain/src/scope/scope.test.ts new file mode 100644 index 0000000000..2441c41c34 --- /dev/null +++ b/packages/multichain/src/scope/scope.test.ts @@ -0,0 +1,23 @@ +import { parseScopeString } from './scope'; + +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/scope.ts b/packages/multichain/src/scope/scope.ts new file mode 100644 index 0000000000..ae452ee653 --- /dev/null +++ b/packages/multichain/src/scope/scope.ts @@ -0,0 +1,96 @@ +import MetaMaskOpenRPCDocument from '@metamask/api-specs'; +import { + CaipChainId, + CaipReference, + CaipAccountId, + isCaipNamespace, + isCaipChainId, + parseCaipChainId, + KnownCaipNamespace, + CaipNamespace, +} from '@metamask/utils'; + +export type NonWalletKnownCaipNamespace = Exclude< + KnownCaipNamespace, + KnownCaipNamespace.Wallet +>; + +export const KnownWalletRpcMethods: string[] = [ + 'wallet_registerOnboarding', + 'wallet_scanQRCode', +]; +const WalletEip155Methods = ['wallet_addEthereumChain']; + +const Eip155Methods = MetaMaskOpenRPCDocument.methods + .map(({ name }: { name: string}) => name) + .filter((method: string) => !WalletEip155Methods.includes(method)) + .filter((method: string) => !KnownWalletRpcMethods.includes(method)); + +export const KnownRpcMethods: Record = { + eip155: Eip155Methods, +}; + +export const KnownWalletNamespaceRpcMethods: Record< + NonWalletKnownCaipNamespace, + string[] +> = { + eip155: WalletEip155Methods, +}; + +export const KnownNotifications: Record = + { + eip155: ['accountsChanged', 'chainChanged', 'eth_subscription'], + }; + +// These External prefixed types represent the CAIP-217 +// Scope and ScopeObject as defined in the spec. +export type ExternalScopeString = CaipChainId | CaipNamespace; +export type ExternalScopeObject = ScopeObject & { + references?: CaipReference[]; +}; +export type ExternalScopesObject = Record< + ExternalScopeString, + ExternalScopeObject +>; + +// These non-prefixed types represent CAIP-217 Scope and +// ScopeObject as defined by the spec but without +// namespace-only Scopes (except for "wallet") and without +// the `references` array of CAIP References on the ScopeObject. +// These deviations from the spec are necessary as MetaMask +// does not support wildcarded Scopes, i.e. Scopes that only +// specify a namespace but no specific reference. +export type ScopeString = CaipChainId | KnownCaipNamespace.Wallet; +export type ScopeObject = { + methods: string[]; + notifications: string[]; + accounts?: CaipAccountId[]; + rpcDocuments?: string[]; + rpcEndpoints?: string[]; +}; +export type ScopesObject = Record & { + [KnownCaipNamespace.Wallet]?: ScopeObject; +}; + +export const parseScopeString = ( + scopeString: string, +): { + namespace?: string; + reference?: string; +} => { + if (isCaipNamespace(scopeString)) { + return { + namespace: scopeString, + }; + } + if (isCaipChainId(scopeString)) { + return parseCaipChainId(scopeString); + } + + return {}; +}; + +export type ScopedProperties = Record< + ExternalScopeString, + Record +>; diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts new file mode 100644 index 0000000000..30b8549107 --- /dev/null +++ b/packages/multichain/src/scope/supported.test.ts @@ -0,0 +1,96 @@ +import { + isSupportedMethod, + isSupportedNotification, + isSupportedScopeString, +} from './supported'; +import { + KnownNotifications, + KnownRpcMethods, + KnownWalletNamespaceRpcMethods, + KnownWalletRpcMethods, +} from './scope'; + +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), + ).toStrictEqual(true); + }); + }, + ); + + it('returns false otherwise', () => { + expect(isSupportedNotification('eip155', 'anything else')).toStrictEqual( + false, + ); + expect(isSupportedNotification('', '')).toStrictEqual(false); + }); + }); + + describe('isSupportedMethod', () => { + it.each(Object.entries(KnownRpcMethods))( + 'returns true for each %s scoped method', + (scopeString: string, methods: string[]) => { + methods.forEach((method) => { + expect(isSupportedMethod(scopeString, method)).toStrictEqual(true); + }); + }, + ); + + it('returns true for each wallet scoped method', () => { + KnownWalletRpcMethods.forEach((method) => { + expect(isSupportedMethod('wallet', method)).toStrictEqual(true); + }); + }); + + it.each(Object.entries(KnownWalletNamespaceRpcMethods))( + 'returns true for each wallet:%s scoped method', + (scopeString: string, methods: string[]) => { + methods.forEach((method) => { + expect( + isSupportedMethod(`wallet:${scopeString}`, method), + ).toStrictEqual(true); + }); + }, + ); + + it('returns false otherwise', () => { + expect(isSupportedMethod('eip155', 'anything else')).toStrictEqual(false); + expect(isSupportedMethod('', '')).toStrictEqual(false); + }); + }); + + describe('isSupportedScopeString', () => { + it('returns true for the wallet namespace', () => { + expect(isSupportedScopeString('wallet', jest.fn())).toStrictEqual(true); + }); + + it('returns false for the wallet namespace when a reference is included', () => { + expect(isSupportedScopeString('wallet:someref', jest.fn())).toStrictEqual( + false, + ); + }); + + it('returns true for the ethereum namespace', () => { + expect(isSupportedScopeString('eip155', jest.fn())).toStrictEqual(true); + }); + + it('returns true for the ethereum namespace when a network client exists for the reference', () => { + const isChainIdSupportedMock = jest.fn().mockReturnValue(true); + expect( + isSupportedScopeString('eip155:1', isChainIdSupportedMock), + ).toStrictEqual(true); + }); + + it('returns false for the ethereum namespace when a network client does not exist for the reference', () => { + const isChainIdSupportedMock = jest.fn().mockReturnValue(false); + expect( + isSupportedScopeString('eip155:1', isChainIdSupportedMock), + ).toStrictEqual(false); + }); + }); +}); diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts new file mode 100644 index 0000000000..9ca98be6e2 --- /dev/null +++ b/packages/multichain/src/scope/supported.ts @@ -0,0 +1,126 @@ +import { + CaipAccountId, + Hex, + isCaipChainId, + isCaipNamespace, + KnownCaipNamespace, + parseCaipAccountId, + parseCaipChainId, +} from '@metamask/utils'; +import { toHex } from '@metamask/controller-utils'; +import { InternalAccount } from '@metamask/keyring-api'; +import { + KnownNotifications, + KnownRpcMethods, + KnownWalletNamespaceRpcMethods, + KnownWalletRpcMethods, + NonWalletKnownCaipNamespace, + parseScopeString, + ExternalScopeString, +} from './scope'; + +// TODO Maybe this gets DRY'ed into utils?.. It's used in TokenDetectionController too +function isEqualCaseInsensitive( + value1: string, + value2: string, +): boolean { + if (typeof value1 !== 'string' || typeof value2 !== 'string') { + return false; + } + return value1.toLowerCase() === value2.toLowerCase(); +} + +export const isSupportedScopeString = ( + scopeString: string, + isChainIdSupported: (chainId: Hex) => boolean, +) => { + const isNamespaceScoped = isCaipNamespace(scopeString); + const isChainScoped = isCaipChainId(scopeString); + + if (isNamespaceScoped) { + switch (scopeString) { + case KnownCaipNamespace.Wallet: + return true; + case KnownCaipNamespace.Eip155: + return true; + default: + return false; + } + } + + if (isChainScoped) { + const { namespace, reference } = parseCaipChainId(scopeString); + switch (namespace) { + case KnownCaipNamespace.Wallet: + if (reference === KnownCaipNamespace.Eip155) { + return true; + } + return false; + case KnownCaipNamespace.Eip155: + return isChainIdSupported(toHex(reference)); + default: + return false; + } + } + + return false; +}; + +export const isSupportedAccount = ( + account: CaipAccountId, + getInternalAccounts: () => InternalAccount[], +) => { + const { + address, + chain: { namespace }, + } = parseCaipAccountId(account); + switch (namespace) { + case KnownCaipNamespace.Eip155: + try { + return getInternalAccounts().some( + (internalAccount) => + ['eip155:eoa', 'eip155:erc4337'].includes(internalAccount.type) && + isEqualCaseInsensitive(address, internalAccount.address), + ); + } catch (err) { + console.log('failed to check if account is supported by wallet', err); + } + return false; + default: + return false; + } +}; + +export const isSupportedMethod = ( + scopeString: ExternalScopeString, + method: string, +): boolean => { + const { namespace, reference } = parseScopeString(scopeString); + + if (namespace === KnownCaipNamespace.Wallet) { + if (reference) { + return ( + KnownWalletNamespaceRpcMethods[ + reference as NonWalletKnownCaipNamespace + ] || [] + ).includes(method); + } + + return KnownWalletRpcMethods.includes(method); + } + + return ( + KnownRpcMethods[namespace as NonWalletKnownCaipNamespace] || [] + ).includes(method); +}; + +export const isSupportedNotification = ( + scopeString: ExternalScopeString, + notification: string, +): boolean => { + const { namespace } = parseScopeString(scopeString); + + return ( + KnownNotifications[namespace as NonWalletKnownCaipNamespace] || [] + ).includes(notification); +}; diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts new file mode 100644 index 0000000000..df0b529822 --- /dev/null +++ b/packages/multichain/src/scope/transform.test.ts @@ -0,0 +1,283 @@ +import { ExternalScopeObject } from './scope'; +import { + flattenScope, + mergeScopes, + mergeScopeObject, + flattenMergeScopes, +} from './transform'; + +const validScopeObject: ExternalScopeObject = { + methods: [], + notifications: [], +}; + +describe('Scope Transform', () => { + describe('flattenScope', () => { + it('returns the scope as is when the scopeString is chain scoped', () => { + expect(flattenScope('eip155:1', validScopeObject)).toStrictEqual({ + 'eip155:1': validScopeObject, + }); + }); + + describe('scopeString is namespace scoped', () => { + it('returns the scope as is when `references` is not defined', () => { + expect(flattenScope('eip155', validScopeObject)).toStrictEqual({ + eip155: validScopeObject, + }); + }); + + it('returns one scope per `references` element with `references` excluded from the scopeObject', () => { + expect( + flattenScope('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 flattenedScopes = flattenScope('eip155', { + ...validScopeObject, + references: ['1', '5'], + }); + + expect(flattenedScopes['eip155:1']).not.toBe( + flattenedScopes['eip155:5'], + ); + expect(flattenedScopes['eip155:1'].methods).not.toBe( + flattenedScopes['eip155:5'].methods, + ); + }); + }); + }); + + 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'], + }); + }); + + 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'], + }); + }); + }); + + describe('mergeScopes', () => { + it('merges the scopeObjects with matching scopeString', () => { + expect( + mergeScopes( + { + 'eip155:1': { + methods: ['a', 'b', 'c'], + notifications: ['foo'], + }, + }, + { + 'eip155:1': { + methods: ['c', 'd'], + notifications: ['bar'], + }, + }, + ), + ).toStrictEqual({ + 'eip155:1': { + methods: ['a', 'b', 'c', 'd'], + notifications: ['foo', 'bar'], + }, + }); + }); + + it('preserves the scopeObjects with no matching scopeString', () => { + expect( + mergeScopes( + { + 'eip155:1': { + methods: ['a', 'b', 'c'], + notifications: ['foo'], + }, + }, + { + 'eip155:2': { + methods: ['c', 'd'], + notifications: ['bar'], + }, + 'eip155:3': { + methods: [], + notifications: [], + }, + }, + ), + ).toStrictEqual({ + 'eip155:1': { + methods: ['a', 'b', 'c'], + notifications: ['foo'], + }, + 'eip155:2': { + methods: ['c', 'd'], + notifications: ['bar'], + }, + 'eip155:3': { + methods: [], + notifications: [], + }, + }); + }); + }); + + describe('flattenMergeScopes', () => { + it('flattens scopes and merges any overlapping scopeStrings', () => { + expect( + flattenMergeScopes({ + 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'], + }, + }); + }); + }); +}); diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts new file mode 100644 index 0000000000..a31faf2d34 --- /dev/null +++ b/packages/multichain/src/scope/transform.ts @@ -0,0 +1,119 @@ +import { CaipReference } from '@metamask/utils'; +import { cloneDeep } from 'lodash'; +import { + ExternalScopeObject, + ExternalScopesObject, + ScopeString, + ScopeObject, + ScopesObject, + parseScopeString, +} from './scope'; + +// DRY THIS +function unique(list: T[]): T[] { + return Array.from(new Set(list)); +} + +/** + * Flattens a ScopeString and ScopeObject into a separate + * ScopeString and ScopeObject for each reference in the `references` + * value if defined. Returns the ScopeString and ScopeObject + * unmodified if it cannot be flattened + * + * @param scopeString - The string representing the scopeObject + * @param scopeObject - The object that defines the scope + * @returns a map of caipChainId to ScopeObjects + */ +export const flattenScope = ( + scopeString: string, + scopeObject: ExternalScopeObject, +): ScopesObject => { + const { references, ...restScopeObject } = scopeObject; + const { namespace, reference } = parseScopeString(scopeString); + + // Scope is already a CAIP-2 ID and has no references to flatten + if (reference || !references) { + return { [scopeString]: scopeObject }; + } + + const scopeMap: ScopesObject = {}; + references.forEach((nestedReference: CaipReference) => { + scopeMap[`${namespace}:${nestedReference}`] = cloneDeep(restScopeObject); + }); + return scopeMap; +}; + +export const mergeScopeObject = ( + scopeObjectA: ScopeObject, + scopeObjectB: ScopeObject, +) => { + const mergedScopeObject: ScopeObject = { + methods: unique([...scopeObjectA.methods, ...scopeObjectB.methods]), + notifications: unique([ + ...scopeObjectA.notifications, + ...scopeObjectB.notifications, + ]), + }; + + if (scopeObjectA.accounts || scopeObjectB.accounts) { + mergedScopeObject.accounts = unique([ + ...(scopeObjectA.accounts ?? []), + ...(scopeObjectB.accounts ?? []), + ]); + } + + if (scopeObjectA.rpcDocuments || scopeObjectB.rpcDocuments) { + mergedScopeObject.rpcDocuments = unique([ + ...(scopeObjectA.rpcDocuments ?? []), + ...(scopeObjectB.rpcDocuments ?? []), + ]); + } + + if (scopeObjectA.rpcEndpoints || scopeObjectB.rpcEndpoints) { + mergedScopeObject.rpcEndpoints = unique([ + ...(scopeObjectA.rpcEndpoints ?? []), + ...(scopeObjectB.rpcEndpoints ?? []), + ]); + } + + return mergedScopeObject; +}; + +export const mergeScopes = ( + scopeA: ScopesObject, + scopeB: ScopesObject, +): ScopesObject => { + const scope: ScopesObject = {}; + + Object.entries(scopeA).forEach(([_scopeString, scopeObjectA]) => { + const scopeString = _scopeString as ScopeString; + const scopeObjectB = scopeB[scopeString]; + + scope[scopeString] = scopeObjectB + ? mergeScopeObject(scopeObjectA, scopeObjectB) + : scopeObjectA; + }); + + Object.entries(scopeB).forEach(([_scopeString, scopeObjectB]) => { + const scopeString = _scopeString as ScopeString; + const scopeObjectA = scopeA[scopeString]; + + if (!scopeObjectA) { + scope[scopeString] = scopeObjectB; + } + }); + + return scope; +}; + +export const flattenMergeScopes = ( + scopes: ExternalScopesObject, +): ScopesObject => { + let flattenedScopes: ScopesObject = {}; + Object.keys(scopes).forEach((scopeString) => { + const flattenedScopeMap = flattenScope(scopeString, scopes[scopeString]); + flattenedScopes = mergeScopes(flattenedScopes, flattenedScopeMap); + }); + + return flattenedScopes; +}; diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts new file mode 100644 index 0000000000..507f24b328 --- /dev/null +++ b/packages/multichain/src/scope/validation.test.ts @@ -0,0 +1,177 @@ +import { ExternalScopeObject } from './scope'; +import { + isValidScope, + validateScopes, +} from './validation'; + +const validScopeString = 'eip155:1'; +const validScopeObject: ExternalScopeObject = { + methods: [], + notifications: [], +}; + +describe('Scope Validation', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('isValidScope', () => { + it.each([ + [ + false, + 'the scopeString is neither a CAIP namespace or CAIP chainId', + 'not a namespace or a caip chain id', + validScopeObject, + ], + [ + true, + 'the scopeString is a valid CAIP namespace and the scopeObject is valid', + 'eip155', + validScopeObject, + ], + [ + true, + 'the scopeString is a valid CAIP chainId and the scopeObject is valid', + 'eip155:1', + validScopeObject, + ], + [ + false, + 'the scopeString is a CAIP chainId but references is nonempty', + 'eip155:1', + { + ...validScopeObject, + references: ['5'], + }, + ], + [ + false, + 'methods contains empty string', + validScopeString, + { + ...validScopeObject, + methods: [''], + }, + ], + [ + false, + 'methods contains non-string', + validScopeString, + { + ...validScopeObject, + methods: [{ foo: 'bar' }], + }, + ], + [ + true, + 'methods contains only strings', + validScopeString, + { + ...validScopeObject, + methods: ['method1', 'method2'], + }, + ], + [ + false, + 'notifications contains empty string', + validScopeString, + { + ...validScopeObject, + notifications: [''], + }, + ], + [ + false, + 'notifications contains non-string', + validScopeString, + { + ...validScopeObject, + notifications: [{ foo: 'bar' }], + }, + ], + [ + false, + 'notifications contains non-string', + 'eip155:1', + { + ...validScopeObject, + notifications: [{ foo: 'bar' }], + }, + ], + [ + false, + 'unexpected properties are defined', + validScopeString, + { + ...validScopeObject, + unexpectedParam: 'foobar', + }, + ], + [ + true, + 'only expected properties are defined', + validScopeString, + { + references: [], + methods: [], + notifications: [], + accounts: [], + rpcDocuments: [], + rpcEndpoints: [], + }, + ], + ])( + 'returns %s when %s', + ( + expected: boolean, + _scenario: string, + scopeString: string, + scopeObject: unknown, + ) => { + expect(isValidScope(scopeString, scopeObject as ExternalScopeObject)).toStrictEqual(expected); + }, + ); + }); + + describe('validateScopes', () => { + const validScopeObjectWithAccounts = { + ...validScopeObject, + accounts: [], + }; + + it('does not throw an error if required scopes are defined but none are valid', () => { + validateScopes( + { 'eip155:1': {} as unknown as ExternalScopeObject }, + undefined, + ); + }); + + it('does not throw an error if optional scopes are defined but none are valid', () => { + validateScopes(undefined, { + 'eip155:1': {} as unknown as ExternalScopeObject, + }); + }); + + it('returns the valid required and optional scopes', () => { + expect( + validateScopes( + { + 'eip155:1': validScopeObjectWithAccounts, + 'eip155:64': {} as unknown as ExternalScopeObject, + }, + { + 'eip155:2': {} as unknown as ExternalScopeObject, + '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 new file mode 100644 index 0000000000..8a5ab1a1cd --- /dev/null +++ b/packages/multichain/src/scope/validation.ts @@ -0,0 +1,101 @@ +import { isCaipReference } from '@metamask/utils'; +import { + ExternalScopeString, + parseScopeString, + ExternalScopeObject, + ExternalScopesObject, +} from './scope'; + +export const isValidScope = ( + scopeString: ExternalScopeString, + scopeObject: ExternalScopeObject, +): boolean => { + const { namespace, reference } = parseScopeString(scopeString); + + if (!namespace && !reference) { + return false; + } + + const { + references, + methods, + notifications, + accounts, + rpcDocuments, + rpcEndpoints, + ...restScopeObject + } = scopeObject; + + if (!methods || !notifications) { + return false; + } + + // These assume that the namespace has a notion of chainIds + if (reference && references && references.length > 0) { + return false; + } + if (namespace && references) { + const areReferencesValid = references.every((nestedReference) => { + return isCaipReference(nestedReference); + }); + + if (!areReferencesValid) { + return false; + } + } + + const areMethodsValid = methods.every( + (method) => typeof method === 'string' && method !== '', + ); + if (!areMethodsValid) { + return false; + } + + const areNotificationsValid = notifications.every( + (notification) => typeof notification === 'string' && notification !== '', + ); + if (!areNotificationsValid) { + return false; + } + + // unexpected properties found on scopeObject + if (Object.keys(restScopeObject).length !== 0) { + return false; + } + + return true; +}; + +export const validateScopes = ( + 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/yarn.lock b/yarn.lock index 93b7cecb5f..a3e97ad38d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2103,6 +2103,13 @@ __metadata: languageName: unknown linkType: soft +"@metamask/api-specs@npm:^0.10.12": + version: 0.10.12 + resolution: "@metamask/api-specs@npm:0.10.12" + checksum: 10/e592f27f350994688d3d54a8a8db16de033011ef665efe3283a77431914d8d69d1c3312fad33e4245b4984e1223b04c98da3d0a68c7f9577cf8290ba441c52ee + languageName: node + linkType: hard + "@metamask/approval-controller@npm:^7.0.2, @metamask/approval-controller@npm:^7.1.0, @metamask/approval-controller@workspace:packages/approval-controller": version: 0.0.0-use.local resolution: "@metamask/approval-controller@workspace:packages/approval-controller" @@ -3014,9 +3021,12 @@ __metadata: version: 0.0.0-use.local resolution: "@metamask/multichain@workspace:packages/multichain" dependencies: + "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" + "@metamask/rpc-errors": "npm:^6.3.1" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" From 186d4b34a2bd4e430ab0bb72023c3fb54a57710a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 15:03:16 -0700 Subject: [PATCH 04/80] Fix middlewares --- packages/multichain/package.json | 3 + .../MultichainMiddlewareManager.test.ts | 14 +- .../MultichainMiddlewareManager.ts | 7 +- .../MultichainSubscriptionManager.test.ts | 25 ++- .../MultichainSubscriptionManager.ts | 10 +- .../multichainMethodCallValidator.ts | 5 +- types/@metamask/eth-json-rpc-filters.d.ts | 1 + yarn.lock | 145 +++++++++++++++++- 8 files changed, 175 insertions(+), 35 deletions(-) create mode 100644 types/@metamask/eth-json-rpc-filters.d.ts diff --git a/packages/multichain/package.json b/packages/multichain/package.json index c284234ed5..8511906670 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -41,7 +41,9 @@ }, "dependencies": { "@metamask/api-specs": "^0.10.12", + "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^6.3.1", + "@open-rpc/schema-utils-js": "^2.0.5", "lodash": "^4.17.21" }, "devDependencies": { @@ -49,6 +51,7 @@ "@metamask/json-rpc-engine": "^9.0.3", "@metamask/network-controller": "^21.0.1", "@metamask/permission-controller": "^11.0.2", + "@open-rpc/meta-schema": "^1.14.6", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts index 099e09537e..b00cd7ab4e 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts @@ -1,4 +1,4 @@ -import { JsonRpcRequest } from 'json-rpc-engine'; +import { JsonRpcRequest } from '@metamask/utils'; import MultichainMiddlewareManager, { ExtendedJsonRpcMiddleware, } from './MultichainMiddlewareManager'; @@ -28,13 +28,13 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, ); expect(middlewareSpy).toHaveBeenCalledWith( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -65,7 +65,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -97,7 +97,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -129,7 +129,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -161,7 +161,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { scope } as unknown as JsonRpcRequest, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts index f0b52b655e..e4c2663099 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -1,9 +1,10 @@ -import { JsonRpcMiddleware } from 'json-rpc-engine'; -import { ExternalScopeString } from './scope'; +import { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import { ExternalScopeString } from '../scope'; +import { Json, JsonRpcParams } from '@metamask/utils'; // Extend JsonRpcMiddleware to include the destroy method // this was introduced in 7.0.0 of json-rpc-engine: https://github.com/MetaMask/json-rpc-engine/blob/v7.0.0/src/JsonRpcEngine.ts#L29-L40 -export type ExtendedJsonRpcMiddleware = JsonRpcMiddleware & { +export type ExtendedJsonRpcMiddleware = JsonRpcMiddleware & { destroy?: () => void; }; diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts index f5e3c0147c..86fda171ce 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -46,11 +46,8 @@ const createMultichainSubscriptionManager = () => { findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, getNetworkClientById: mockGetNetworkClientById, }); - const onNotificationSpy = jest.fn(); - multichainSubscriptionManager.on('notification', onNotificationSpy); - - return { multichainSubscriptionManager, onNotificationSpy }; + return { multichainSubscriptionManager }; }; describe('MultichainSubscriptionManager', () => { @@ -66,9 +63,11 @@ describe('MultichainSubscriptionManager', () => { }); it('should subscribe to a scope, origin, and tabId', () => { - const { multichainSubscriptionManager, onNotificationSpy } = + const { multichainSubscriptionManager} = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + const onNotificationSpy = jest.fn(); + multichainSubscriptionManager.on('notification', onNotificationSpy); mockSubscriptionManager.events.on.mock.calls[0][1]( newHeadsNotificationMock, @@ -84,20 +83,16 @@ describe('MultichainSubscriptionManager', () => { }); it('should unsubscribe from a scope', () => { - const { multichainSubscriptionManager, onNotificationSpy } = + const { multichainSubscriptionManager} = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByScope(scope); - mockSubscriptionManager.events.on.mock.calls[0][1]( - newHeadsNotificationMock, - ); - - expect(onNotificationSpy).not.toHaveBeenCalled(); + expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); }); it('should unsubscribe from a scope and origin', () => { - const { multichainSubscriptionManager, onNotificationSpy } = + const { multichainSubscriptionManager} = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByScopeAndOrigin(scope, origin); @@ -106,11 +101,11 @@ describe('MultichainSubscriptionManager', () => { newHeadsNotificationMock, ); - expect(onNotificationSpy).not.toHaveBeenCalled(); + expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); }); it('should unsubscribe from a origin and tabId', () => { - const { multichainSubscriptionManager, onNotificationSpy } = + const { multichainSubscriptionManager} = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); @@ -119,6 +114,6 @@ describe('MultichainSubscriptionManager', () => { newHeadsNotificationMock, ); - expect(onNotificationSpy).not.toHaveBeenCalled(); + expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); }); }); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 5cf94f0597..668aa431aa 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -1,9 +1,9 @@ import EventEmitter from 'events'; import { NetworkController } from '@metamask/network-controller'; import SafeEventEmitter from '@metamask/safe-event-emitter'; -import { Hex, parseCaipChainId } from '@metamask/utils'; +import { CaipChainId, Hex, parseCaipChainId } from '@metamask/utils'; import { toHex } from '@metamask/controller-utils'; -import { ExternalScopeString, ScopeString } from './scope'; +import { ExternalScopeString } from '../scope'; export type SubscriptionManager = { events: EventEmitter; @@ -93,7 +93,7 @@ export default class MultichainSubscriptionManager extends SafeEventEmitter { } const networkClientId = this.#findNetworkClientIdByChainId( - toHex(parseCaipChainId(subscriptionKey.scope).reference), + toHex(parseCaipChainId(subscriptionKey.scope as CaipChainId).reference), ); const networkClient = this.#getNetworkClientById(networkClientId); const subscriptionManager = createSubscriptionManager({ @@ -128,7 +128,7 @@ export default class MultichainSubscriptionManager extends SafeEventEmitter { this.#removeSubscriptionEntry(subscriptionKey); } - unsubscribeByScope(scope: ScopeString) { + unsubscribeByScope(scope: ExternalScopeString) { this.#subscriptions.forEach((subscriptionEntry) => { if (subscriptionEntry.scope === scope) { this.#unsubscribe(subscriptionEntry); @@ -136,7 +136,7 @@ export default class MultichainSubscriptionManager extends SafeEventEmitter { }); } - unsubscribeByScopeAndOrigin(scope: ScopeString, origin: string) { + unsubscribeByScopeAndOrigin(scope: ExternalScopeString, origin: string) { this.#subscriptions.forEach((subscriptionEntry) => { if ( subscriptionEntry.scope === scope && diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index cff2841ecf..7c68c56b91 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -13,7 +13,8 @@ import { } 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 { Json, JsonRpcMiddleware } from 'json-rpc-engine'; +import { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import { Json } from '@metamask/utils'; import { Schema, ValidationError, Validator } from 'jsonschema'; const transformError = ( @@ -85,7 +86,7 @@ export const multichainMethodCallValidator = async ( export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< JsonRpcRequest, - void + Json > = function (request, _response, next, end) { multichainMethodCallValidator(request.method, request.params).then( (errors) => { diff --git a/types/@metamask/eth-json-rpc-filters.d.ts b/types/@metamask/eth-json-rpc-filters.d.ts new file mode 100644 index 0000000000..5a51785b82 --- /dev/null +++ b/types/@metamask/eth-json-rpc-filters.d.ts @@ -0,0 +1 @@ +declare module '@metamask/eth-json-rpc-filters/subscriptionManager'; diff --git a/yarn.lock b/yarn.lock index a3e97ad38d..b43e7ce698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1931,6 +1931,48 @@ __metadata: languageName: node linkType: hard +"@json-schema-spec/json-pointer@npm:^0.1.2": + version: 0.1.2 + resolution: "@json-schema-spec/json-pointer@npm:0.1.2" + checksum: 10/2a691ffc11f1a266ca4d0c9e2c99791679d580f343ef69746fad623d1abcf4953adde987890e41f906767d7729604c0182341e9012388b73a44d5b21fb296453 + languageName: node + linkType: hard + +"@json-schema-tools/dereferencer@npm:^1.6.3": + version: 1.6.3 + resolution: "@json-schema-tools/dereferencer@npm:1.6.3" + dependencies: + "@json-schema-tools/reference-resolver": "npm:^1.2.6" + "@json-schema-tools/traverse": "npm:^1.10.4" + fast-safe-stringify: "npm:^2.1.1" + checksum: 10/da6ef5b82a8a9c3a7e62ffcab5c04c581f1e0f8165c0debdb272bb1e08ccd726107ee194487b8fa736cac00fb390b8df74bc1ad1b200eddbe25c98ee0d3d000b + languageName: node + linkType: hard + +"@json-schema-tools/meta-schema@npm:^1.7.5": + version: 1.7.5 + resolution: "@json-schema-tools/meta-schema@npm:1.7.5" + checksum: 10/707dc3a285c26c37d00f418e9d0ef8a2ad1c23d4936ad5aab0ce94c9ae36a7a6125c4ca5048513af64b7e6e527b5472a1701d1f709c379acdd7ad12f6409d2cd + languageName: node + linkType: hard + +"@json-schema-tools/reference-resolver@npm:^1.2.6": + version: 1.2.6 + resolution: "@json-schema-tools/reference-resolver@npm:1.2.6" + dependencies: + "@json-schema-spec/json-pointer": "npm:^0.1.2" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/91d6b4b2ac43f8163fd27bde6d826f29f339e9c7ce3b7e2b73b85e891fa78e3702fd487deda143a0701879cbc2fe28c53a4efce4cd2d2dd2fe6e82b64bbd9c9c + languageName: node + linkType: hard + +"@json-schema-tools/traverse@npm:^1.10.4": + version: 1.10.4 + resolution: "@json-schema-tools/traverse@npm:1.10.4" + checksum: 10/0027bc90df01c5eeee0833e722b7320b53be8b5ce3f4e0e4a6e45713a38e6f88f21aba31e3dd973093ef75cd21a40c07fe8f112da8f49a7919b1c0e44c904d20 + languageName: node + linkType: hard + "@keystonehq/alias-sampling@npm:^0.1.1": version: 0.1.2 resolution: "@keystonehq/alias-sampling@npm:0.1.2" @@ -2534,6 +2576,19 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-json-rpc-filters@npm:^7.0.0": + version: 7.0.1 + resolution: "@metamask/eth-json-rpc-filters@npm:7.0.1" + dependencies: + "@metamask/eth-query": "npm:^4.0.0" + "@metamask/json-rpc-engine": "npm:^8.0.2" + "@metamask/safe-event-emitter": "npm:^3.0.0" + async-mutex: "npm:^0.5.0" + pify: "npm:^5.0.0" + checksum: 10/5200f75cee48dfd79deba5e4f1b16ff6827e606da617891f5cb7b59c43ae4ac8420cb9a6a9ca31705c47d2c3d32a3754e052b30f61fd293cc37f009c4fe20c12 + languageName: node + linkType: hard + "@metamask/eth-json-rpc-infura@npm:^9.1.0": version: 9.1.0 resolution: "@metamask/eth-json-rpc-infura@npm:9.1.0" @@ -2863,6 +2918,17 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^8.0.2": + version: 8.0.2 + resolution: "@metamask/json-rpc-engine@npm:8.0.2" + dependencies: + "@metamask/rpc-errors": "npm:^6.2.1" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^8.3.0" + checksum: 10/f088f4b648b9b55875b56e8237853e7282f13302a9db6a1f9bba06314dfd6cd0a23b3d27f8fde05a157b97ebb03b67bc2699ba455c99553dfb2ecccd73ab3474 + languageName: node + linkType: hard + "@metamask/json-rpc-engine@npm:^9.0.0, @metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2, @metamask/json-rpc-engine@npm:^9.0.3, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" @@ -3023,10 +3089,13 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eth-json-rpc-filters": "npm:^7.0.0" "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" "@metamask/rpc-errors": "npm:^6.3.1" + "@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" @@ -3979,6 +4048,31 @@ __metadata: languageName: node linkType: hard +"@open-rpc/meta-schema@npm:^1.14.6, @open-rpc/meta-schema@npm:^1.14.9": + version: 1.14.9 + resolution: "@open-rpc/meta-schema@npm:1.14.9" + checksum: 10/51505dcf7aa1a2285c78953c9b33711cede5f2765aa37dcb9ee7756d689e2ff2a89cfc6039504f0569c52a805fb9aa18f30a7c02ad7a06e793c801e43b419104 + languageName: node + linkType: hard + +"@open-rpc/schema-utils-js@npm:^2.0.5": + version: 2.0.5 + resolution: "@open-rpc/schema-utils-js@npm:2.0.5" + dependencies: + "@json-schema-tools/dereferencer": "npm:^1.6.3" + "@json-schema-tools/meta-schema": "npm:^1.7.5" + "@json-schema-tools/reference-resolver": "npm:^1.2.6" + "@open-rpc/meta-schema": "npm:^1.14.9" + ajv: "npm:^6.10.0" + detect-node: "npm:^2.0.4" + fast-safe-stringify: "npm:^2.0.7" + fs-extra: "npm:^10.1.0" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/9e10215606e9a00a47b082c9cfd70d05bf0d38de6cf1c147246c545c6997375d94cd3caafe919b71178df58b5facadfd0dcc8b6857bf5e79c40e5e33683dd3d5 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -4919,7 +5013,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4": +"ajv@npm:^6.10.0, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6315,6 +6409,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:^2.0.4": + version: 2.1.0 + resolution: "detect-node@npm:2.1.0" + checksum: 10/832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e + languageName: node + linkType: hard + "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -7323,7 +7424,7 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:^2.0.6": +"fast-safe-stringify@npm:^2.0.6, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 @@ -7563,6 +7664,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^10.1.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/05ce2c3b59049bcb7b52001acd000e44b3c4af4ec1f8839f383ef41ec0048e3cfa7fd8a637b1bddfefad319145db89be91f4b7c1db2908205d38bf91e7d1d3b7 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -7856,7 +7968,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -8532,6 +8644,13 @@ __metadata: languageName: node linkType: hard +"is-url@npm:^1.2.4": + version: 1.2.4 + resolution: "is-url@npm:1.2.4" + checksum: 10/100e74b3b1feab87a43ef7653736e88d997eb7bd32e71fd3ebc413e58c1cbe56269699c776aaea84244b0567f2a7d68dfaa512a062293ed2f9fdecb394148432 + languageName: node + linkType: hard + "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -9468,6 +9587,19 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 + languageName: node + linkType: hard + "jsonschema@npm:^1.2.4": version: 1.4.1 resolution: "jsonschema@npm:1.4.1" @@ -12295,6 +12427,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.0": version: 1.1.0 resolution: "update-browserslist-db@npm:1.1.0" From 6a93d1d3425252c625b0c2c4cb5c8cb11030c3ac Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 15:48:05 -0700 Subject: [PATCH 05/80] Fix adapters except permission middleware --- .../src/adapters/caip-permission-adapter-eth-accounts.test.ts | 2 +- .../src/adapters/caip-permission-adapter-eth-accounts.ts | 2 +- .../adapters/caip-permission-adapter-permittedChains.test.ts | 2 +- .../src/adapters/caip-permission-adapter-permittedChains.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 index b7014fe78e..04aba6a330 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -1,4 +1,4 @@ -import { Caip25CaveatValue } from '../caip25permissions'; +import { Caip25CaveatValue } from '../caip25Permission'; import { getEthAccounts, setEthAccounts, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 7f515b5ec2..d7291f42b8 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -4,7 +4,7 @@ import { KnownCaipNamespace, parseCaipAccountId, } from '@metamask/utils'; -import { Caip25CaveatValue } from '../caip25permissions'; +import { Caip25CaveatValue } from '../caip25Permission'; import { mergeScopes, parseScopeString, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index aa125193ce..e83562f7dc 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -1,4 +1,4 @@ -import { Caip25CaveatValue } from '../caip25permissions'; +import { Caip25CaveatValue } from '../caip25Permission'; import { KnownNotifications, KnownRpcMethods } from '../scope'; import { addPermittedEthChainId, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 8e840c6c32..39b2b86bea 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -1,6 +1,6 @@ import { Hex, KnownCaipNamespace } from '@metamask/utils'; import { toHex } from '@metamask/controller-utils'; -import { Caip25CaveatValue } from '../caip25permissions'; +import { Caip25CaveatValue } from '../caip25Permission'; import { KnownNotifications, KnownRpcMethods, From 231bcaafad7f37d7b6590b3b16d6200e9178c1c5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 15:48:57 -0700 Subject: [PATCH 06/80] Rename handlers js to ts --- packages/multichain/package.json | 1 + ...js => caip-permission-adapter-middleware.ts} | 0 ...ession.test.js => wallet-getSession.test.ts} | 2 +- ...allet-getSession.js => wallet-getSession.ts} | 4 ++-- ...thod.test.js => wallet-invokeMethod.test.ts} | 2 +- ...t-invokeMethod.js => wallet-invokeMethod.ts} | 4 ++-- ...ion.test.js => wallet-revokeSession.test.ts} | 2 +- ...revokeSession.js => wallet-revokeSession.ts} | 17 +++++++++++------ 8 files changed, 19 insertions(+), 13 deletions(-) rename packages/multichain/src/adapters/{caip-permission-adapter-middleware.js => caip-permission-adapter-middleware.ts} (100%) rename packages/multichain/src/handlers/{wallet-getSession.test.js => wallet-getSession.test.ts} (98%) rename packages/multichain/src/handlers/{wallet-getSession.js => wallet-getSession.ts} (89%) rename packages/multichain/src/handlers/{wallet-invokeMethod.test.js => wallet-invokeMethod.test.ts} (99%) rename packages/multichain/src/handlers/{wallet-invokeMethod.js => wallet-invokeMethod.ts} (95%) rename packages/multichain/src/handlers/{wallet-revokeSession.test.js => wallet-revokeSession.test.ts} (97%) rename packages/multichain/src/handlers/{wallet-revokeSession.js => wallet-revokeSession.ts} (53%) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 8511906670..bfb1d8dd29 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -42,6 +42,7 @@ "dependencies": { "@metamask/api-specs": "^0.10.12", "@metamask/eth-json-rpc-filters": "^7.0.0", + "@metamask/json-rpc-engine": "^9.0.3", "@metamask/rpc-errors": "^6.3.1", "@open-rpc/schema-utils-js": "^2.0.5", "lodash": "^4.17.21" diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.js b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts similarity index 100% rename from packages/multichain/src/adapters/caip-permission-adapter-middleware.js rename to packages/multichain/src/adapters/caip-permission-adapter-middleware.ts diff --git a/packages/multichain/src/handlers/wallet-getSession.test.js b/packages/multichain/src/handlers/wallet-getSession.test.ts similarity index 98% rename from packages/multichain/src/handlers/wallet-getSession.test.js rename to packages/multichain/src/handlers/wallet-getSession.test.ts index de51365869..bbbad820a2 100644 --- a/packages/multichain/src/handlers/wallet-getSession.test.js +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -1,7 +1,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from '../caip25permissions'; +} from '../caip25Permission'; import { walletGetSessionHandler } from './wallet-getSession'; const baseRequest = { diff --git a/packages/multichain/src/handlers/wallet-getSession.js b/packages/multichain/src/handlers/wallet-getSession.ts similarity index 89% rename from packages/multichain/src/handlers/wallet-getSession.js rename to packages/multichain/src/handlers/wallet-getSession.ts index e10e278125..5bcefe0705 100644 --- a/packages/multichain/src/handlers/wallet-getSession.js +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -1,8 +1,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from '../caip25permissions'; -import { mergeScopes } from './scope'; +} from '../caip25Permission'; +import { mergeScopes } from '../scope'; export async function walletGetSessionHandler( request, diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.js b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts similarity index 99% rename from packages/multichain/src/handlers/wallet-invokeMethod.test.js rename to packages/multichain/src/handlers/wallet-invokeMethod.test.ts index dcf0d5f4ac..56d46d1c02 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.js +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -2,7 +2,7 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from './caip25permissions'; +} from '../caip25Permission'; import { walletInvokeMethodHandler } from './wallet-invokeMethod'; const createMockedRequest = () => ({ diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.js b/packages/multichain/src/handlers/wallet-invokeMethod.ts similarity index 95% rename from packages/multichain/src/handlers/wallet-invokeMethod.js rename to packages/multichain/src/handlers/wallet-invokeMethod.ts index 14b2043726..1ca8ff5b1e 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.js +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -3,8 +3,8 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from './caip25permissions'; -import { mergeScopes, parseScopeString } from './scope'; +} from '../caip25Permission'; +import { mergeScopes, parseScopeString } from '../scope'; export async function walletInvokeMethodHandler( request, diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.js b/packages/multichain/src/handlers/wallet-revokeSession.test.ts similarity index 97% rename from packages/multichain/src/handlers/wallet-revokeSession.test.js rename to packages/multichain/src/handlers/wallet-revokeSession.test.ts index 8acd84ac3d..6a6add8023 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.test.js +++ b/packages/multichain/src/handlers/wallet-revokeSession.test.ts @@ -3,7 +3,7 @@ import { UnrecognizedSubjectError, } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import { Caip25EndowmentPermissionName } from '../caip25permissions'; +import { Caip25EndowmentPermissionName } from '../caip25Permission'; import { walletRevokeSessionHandler } from './wallet-revokeSession'; const baseRequest = { diff --git a/packages/multichain/src/handlers/wallet-revokeSession.js b/packages/multichain/src/handlers/wallet-revokeSession.ts similarity index 53% rename from packages/multichain/src/handlers/wallet-revokeSession.js rename to packages/multichain/src/handlers/wallet-revokeSession.ts index e0425cf326..d76994de90 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.js +++ b/packages/multichain/src/handlers/wallet-revokeSession.ts @@ -1,16 +1,21 @@ +import type { JsonRpcEngineNextCallback, JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import { PermissionDoesNotExistError, UnrecognizedSubjectError, + PermissionController } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import { Caip25EndowmentPermissionName } from '../caip25permissions'; +import { Caip25EndowmentPermissionName } from '../caip25Permission'; +import { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; export async function walletRevokeSessionHandler( - request, - response, - _next, - end, - hooks, + request: JsonRpcRequest, + response: JsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + hooks: { + revokePermission: PermissionController['revokePermission'] + }, ) { try { hooks.revokePermission(request.origin, Caip25EndowmentPermissionName); From 4e52fc0d5f20a31e1a262ff3e4a6fafc48aecb75 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 15:54:26 -0700 Subject: [PATCH 07/80] permission middleware test js ts rename --- ...dleware.test.js => caip-permission-adapter-middleware.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/multichain/src/adapters/{caip-permission-adapter-middleware.test.js => caip-permission-adapter-middleware.test.ts} (100%) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts similarity index 100% rename from packages/multichain/src/adapters/caip-permission-adapter-middleware.test.js rename to packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts From 151eaacfb089d4441928a1bcb4448daedf50d595 Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 11 Oct 2024 13:00:59 -0400 Subject: [PATCH 08/80] fix: typescript + linting (#4788) ## Explanation This PR fixes a lot of the linting and typescript errors. still some left but this covers a lot of it. ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Jiexi Luan --- packages/multichain/jest.config.js | 8 +-- packages/multichain/package.json | 30 ++++++---- ...ip-permission-adapter-eth-accounts.test.ts | 2 +- .../caip-permission-adapter-eth-accounts.ts | 15 ++--- ...caip-permission-adapter-middleware.test.ts | 19 ++++-- .../caip-permission-adapter-middleware.ts | 43 +++++++++++--- ...permission-adapter-permittedChains.test.ts | 2 +- ...caip-permission-adapter-permittedChains.ts | 9 +-- .../multichain/src/caip25Permission.test.ts | 59 ++++++++++--------- packages/multichain/src/caip25Permission.ts | 45 +++++++++----- .../src/handlers/wallet-getSession.test.ts | 17 +++++- .../src/handlers/wallet-getSession.ts | 31 ++++++++-- .../src/handlers/wallet-invokeMethod.test.ts | 13 +++- .../src/handlers/wallet-invokeMethod.ts | 46 ++++++++++++--- .../src/handlers/wallet-revokeSession.test.ts | 28 ++++++--- .../src/handlers/wallet-revokeSession.ts | 25 ++++++-- .../MultichainMiddlewareManager.test.ts | 22 ++++--- .../MultichainMiddlewareManager.ts | 41 ++++++++----- .../MultichainSubscriptionManager.test.ts | 9 +-- .../MultichainSubscriptionManager.ts | 12 ++-- .../multichainMethodCallValidator.ts | 14 +++-- packages/multichain/src/scope/assert.test.ts | 26 ++++---- packages/multichain/src/scope/assert.ts | 10 ++-- .../src/scope/authorization.test.ts | 11 ++-- .../multichain/src/scope/authorization.ts | 9 +-- packages/multichain/src/scope/filter.ts | 5 +- packages/multichain/src/scope/scope.test.ts | 2 +- packages/multichain/src/scope/scope.ts | 14 +++-- .../multichain/src/scope/supported.test.ts | 52 +++++++--------- packages/multichain/src/scope/supported.ts | 24 ++++---- .../multichain/src/scope/transform.test.ts | 2 +- packages/multichain/src/scope/transform.ts | 19 ++++-- .../multichain/src/scope/validation.test.ts | 29 +++++---- packages/multichain/src/scope/validation.ts | 5 +- packages/multichain/tsconfig.build.json | 12 +++- yarn.lock | 4 ++ 36 files changed, 452 insertions(+), 262 deletions(-) diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js index ca08413339..f8be8cb30e 100644 --- a/packages/multichain/jest.config.js +++ b/packages/multichain/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, + branches: 83.05, + functions: 87.37, + lines: 86.73, + statements: 87.17, }, }, }); diff --git a/packages/multichain/package.json b/packages/multichain/package.json index bfb1d8dd29..e633a1bb2b 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -18,33 +18,43 @@ "sideEffects": false, "exports": { ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/types/index.d.ts" + "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.js", - "types": "./dist/types/index.d.ts", + "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", - "test": "jest --reporters=jest-silent-reporter", - "test:clean": "jest --clearCache", - "test:verbose": "jest --verbose", - "test:watch": "jest --watch" + "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.10.12", + "@metamask/controller-utils": "^11.3.0", "@metamask/eth-json-rpc-filters": "^7.0.0", - "@metamask/json-rpc-engine": "^9.0.3", "@metamask/rpc-errors": "^6.3.1", + "@metamask/safe-event-emitter": "^3.0.0", + "@metamask/utils": "^9.1.0", "@open-rpc/schema-utils-js": "^2.0.5", + "jsonschema": "^1.2.4", "lodash": "^4.17.21" }, "devDependencies": { 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 index 04aba6a330..9434fab81d 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -1,4 +1,4 @@ -import { Caip25CaveatValue } from '../caip25Permission'; +import type { Caip25CaveatValue } from '../caip25Permission'; import { getEthAccounts, setEthAccounts, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index d7291f42b8..95cb5dd0ec 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -1,16 +1,13 @@ import { - CaipAccountId, - Hex, + type CaipAccountId, + type Hex, KnownCaipNamespace, parseCaipAccountId, } from '@metamask/utils'; -import { Caip25CaveatValue } from '../caip25Permission'; -import { - mergeScopes, - parseScopeString, - ScopesObject, - ScopeString, -} from '../scope'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import type { ScopesObject } from '../scope'; +import { mergeScopes, parseScopeString, type ScopeString } from '../scope'; const isEip155ScopeString = (scopeString: ScopeString) => { const { namespace, reference } = parseScopeString(scopeString); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts index f8c0f98137..ea6318074a 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts @@ -1,11 +1,15 @@ import { providerErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from '../caip25permissions'; -import { CaipPermissionAdapterMiddleware } from './caip-permission-adapter-middleware'; +} from '../caip25Permission'; +import { caipPermissionAdapterMiddleware } from './caip-permission-adapter-middleware'; const baseRequest = { + id: 1, + jsonrpc: '2.0' as const, origin: 'http://test.com', networkClientId: 'mainnet', method: 'eth_call', @@ -48,7 +52,7 @@ const createMockedHandler = () => { }); const getNetworkConfigurationByNetworkClientId = jest .fn() - .mockImplementation((networkClientId) => { + .mockImplementation((networkClientId: string) => { const chainId = { mainnet: '0x1', @@ -58,8 +62,13 @@ const createMockedHandler = () => { chainId, }; }); - const handler = (request) => - CaipPermissionAdapterMiddleware(request, {}, next, end, { + const handler = ( + request: JsonRpcRequest & { + networkClientId: string; + origin: string; + }, + ) => + caipPermissionAdapterMiddleware(request, {}, next, end, { getCaveat, getNetworkConfigurationByNetworkClientId, }); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 867288eb95..d92e0292e2 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -1,16 +1,43 @@ +import type { NetworkConfiguration } from '@metamask/network-controller'; +import type { Caveat } from '@metamask/permission-controller'; import { providerErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; import { Caip25CaveatType, Caip25EndowmentPermissionName, -} from '../caip25permissions'; +} from '../caip25Permission'; +import type { ScopeString } from '../scope'; import { mergeScopes } from '../scope'; -export async function CaipPermissionAdapterMiddleware( - request, - _response, - next, - end, - hooks, +/** + * Middleware to handle CAIP-25 permission requests. + * + * @param request - The request object. + * @param _response - The response object. + * @param next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - Function to retrieve a caveat. + * @param hooks.getNetworkConfigurationByNetworkClientId - Function to retrieve a network configuration. + */ +export async function caipPermissionAdapterMiddleware( + request: JsonRpcRequest & { + networkClientId: string; + origin: string; + }, + _response: unknown, + next: () => Promise, + end: (error?: Error) => void, + hooks: { + getCaveat: ( + ...args: unknown[] + ) => Caveat; + getNetworkConfigurationByNetworkClientId: ( + networkClientId: string, + ) => NetworkConfiguration; + }, ) { const { networkClientId, method } = request; @@ -31,7 +58,7 @@ export async function CaipPermissionAdapterMiddleware( const { chainId } = hooks.getNetworkConfigurationByNetworkClientId(networkClientId); - const scope = `eip155:${parseInt(chainId, 16)}`; + const scope: ScopeString = `eip155:${parseInt(chainId, 16)}`; const scopesObject = mergeScopes( caveat.value.requiredScopes, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index e83562f7dc..a740207807 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -1,4 +1,4 @@ -import { Caip25CaveatValue } from '../caip25Permission'; +import type { Caip25CaveatValue } from '../caip25Permission'; import { KnownNotifications, KnownRpcMethods } from '../scope'; import { addPermittedEthChainId, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 39b2b86bea..cfbbdedd29 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -1,13 +1,14 @@ -import { Hex, KnownCaipNamespace } from '@metamask/utils'; import { toHex } from '@metamask/controller-utils'; -import { Caip25CaveatValue } from '../caip25Permission'; +import type { Hex } from '@metamask/utils'; +import { KnownCaipNamespace } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import type { ScopesObject, ScopeString } from '../scope'; import { KnownNotifications, KnownRpcMethods, mergeScopes, parseScopeString, - ScopesObject, - ScopeString, } from '../scope'; export const getPermittedEthChainIds = ( diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index cf63cf0a0c..7e6d6f243d 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -1,19 +1,20 @@ +import type { NonEmptyArray } from '@metamask/controller-utils'; +import type { CaveatConstraint } from '@metamask/permission-controller'; import { - CaveatConstraint, CaveatMutatorOperation, PermissionType, SubjectType, } from '@metamask/permission-controller'; -import { NonEmptyArray } from '@metamask/controller-utils'; -import * as Scope from './scope'; + +import type { Caip25CaveatValue } from './caip25Permission'; import { Caip25CaveatType, - Caip25CaveatValue, caip25EndowmentBuilder, Caip25EndowmentPermissionName, Caip25CaveatMutatorFactories, removeScope, } from './caip25Permission'; +import * as Scope from './scope'; jest.mock('./scope', () => ({ validateAndFlattenScopes: jest.fn(), @@ -655,34 +656,36 @@ describe('endowment:caip25', () => { }, }, }); - validator({ - caveats: [ - { - type: Caip25CaveatType, - value: { - requiredScopes: { - 'eip155:1': { - methods: ['eth_chainId'], - notifications: [], - accounts: ['eip155:1:0xdead'], + expect( + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, }, - }, - optionalScopes: { - 'eip155:5': { - methods: [], - notifications: [], - accounts: ['eip155:5:0xbeef'], + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, }, + isMultichainOrigin: true, }, - isMultichainOrigin: true, }, - }, - ], - date: 1234, - id: '1', - invoker: 'test.com', - parentCapability: Caip25EndowmentPermissionName, - }); + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }), + ).toBeUndefined(); }); }); }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 35312b2e11..d4cb66428c 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -1,4 +1,4 @@ -import { strict as assert } from 'assert'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { PermissionSpecificationBuilder, EndowmentGetterParams, @@ -11,22 +11,17 @@ import { PermissionType, SubjectType, } from '@metamask/permission-controller'; +import type { CaipAccountId, Json } from '@metamask/utils'; import { - CaipAccountId, - Json, parseCaipAccountId, type Hex, type NonEmptyArray, } from '@metamask/utils'; -import { NetworkClientId } from '@metamask/network-controller'; +import { strict as assert } from 'assert'; import { cloneDeep, isEqual } from 'lodash'; -import { - ExternalScopeString, - validateAndFlattenScopes, - ScopesObject, - ScopeObject, - assertScopesSupported, -} from './scope'; + +import type { ExternalScopeString, ScopesObject, ScopeObject } from './scope'; +import { validateAndFlattenScopes, assertScopesSupported } from './scope'; export type Caip25CaveatValue = { requiredScopes: ScopesObject; @@ -58,7 +53,7 @@ type Caip25EndowmentSpecification = ValidPermissionSpecification<{ * `endowment:caip25` returns nothing atm; * * @param builderOptions - The specification builder options. - * @param builderOptions.findNetworkClientIdByChainId + * @param builderOptions.findNetworkClientIdByChainId - The hook to find the networkClientId for a chainId. * @returns The specification for the `caip25` endowment. */ const specificationBuilder: PermissionSpecificationBuilder< @@ -145,9 +140,9 @@ export const Caip25CaveatMutatorFactories = { }, }; -const reduceKeysHelper = ( - acc: Record, - [key, value]: [K, V], +const reduceKeysHelper = ( + acc: Record, + [key, value]: [Key, Value], ) => { return { ...acc, @@ -155,6 +150,12 @@ const reduceKeysHelper = ( }; }; +/** + * 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); @@ -162,6 +163,12 @@ function removeAccountFilterFn(targetAddress: string) { }; } +/** + * Removes the account from the scope object. + * + * @param targetAddress - The address to remove from the scope object. + * @param scopeObject - The scope object to remove the account from. + */ function removeAccountOnScope(targetAddress: string, scopeObject: ScopeObject) { if (scopeObject.accounts) { scopeObject.accounts = scopeObject.accounts.filter( @@ -170,6 +177,13 @@ function removeAccountOnScope(targetAddress: string, scopeObject: ScopeObject) { } } +/** + * Removes the target account from the scope object. + * + * @param targetAddress - The address to remove from the scope object. + * @param existingScopes - The scope object to remove the account from. + * @returns The updated scope object. + */ function removeAccount( targetAddress: string, // non caip-10 formatted address existingScopes: Caip25CaveatValue, @@ -208,6 +222,7 @@ function removeAccount( * * @param targetScopeString - The scope that is being removed. * @param caip25CaveatValue - The CAIP-25 permission caveat value to remove the scope from. + * @returns The updated CAIP-25 permission caveat value. */ export function removeScope( targetScopeString: ExternalScopeString, diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts index bbbad820a2..ebee666967 100644 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -1,12 +1,17 @@ +import type { JsonRpcRequest } from '@metamask/utils'; + import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; import { walletGetSessionHandler } from './wallet-getSession'; -const baseRequest = { +const baseRequest: JsonRpcRequest & { origin: string } = { origin: 'http://test.com', + jsonrpc: '2.0' as const, + method: 'wallet_getSession', params: {}, + id: 1, }; const createMockedHandler = () => { @@ -36,8 +41,14 @@ const createMockedHandler = () => { }, }, }); - const response = {}; - const handler = (request) => + const response = { + result: { + sessionScopes: {}, + }, + id: 1, + jsonrpc: '2.0' as const, + }; + const handler = (request: JsonRpcRequest & { origin: string }) => walletGetSessionHandler(request, response, next, end, { getCaveat, }); diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index 5bcefe0705..7f0032d00d 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -1,15 +1,36 @@ +import type { Caveat } from '@metamask/permission-controller'; +import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; +import type { ScopesObject } from '../scope'; import { mergeScopes } from '../scope'; +/** + * Handler for the `wallet_getSession` RPC method. + * + * @param request - The request object. + * @param response - The response object. + * @param _next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - Function to retrieve a caveat. + */ export async function walletGetSessionHandler( - request, - response, - _next, - end, - hooks, + request: JsonRpcRequest & { origin: string }, + response: JsonRpcSuccess<{ sessionScopes: ScopesObject }>, + _next: () => void, + end: () => void, + hooks: { + getCaveat: ( + origin: string, + endowmentPermissionName: string, + caveatType: string, + ) => Caveat; + }, ) { let caveat; try { diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts index 56d46d1c02..ebffb0ece8 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -1,4 +1,6 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + import { Caip25CaveatType, Caip25EndowmentPermissionName, @@ -6,7 +8,10 @@ import { import { walletInvokeMethodHandler } from './wallet-invokeMethod'; const createMockedRequest = () => ({ + jsonrpc: '2.0' as const, + id: 0, origin: 'http://test.com', + method: 'wallet_invokeMethod', params: { scope: 'eip155:1', request: { @@ -54,8 +59,8 @@ const createMockedHandler = () => { const getSelectedNetworkClientId = jest .fn() .mockReturnValue('selectedNetworkClientId'); - const handler = (request) => - walletInvokeMethodHandler(request, {}, next, end, { + const handler = (request: JsonRpcRequest & { origin: string }) => + walletInvokeMethodHandler(request, { jsonrpc: '2.0', id: 1 }, next, end, { getCaveat, findNetworkClientIdByChainId, getSelectedNetworkClientId, @@ -180,6 +185,8 @@ describe('wallet_invokeMethod', () => { await handler(request); expect(request).toStrictEqual({ + jsonrpc: '2.0' as const, + id: 0, scope: 'eip155:1', origin: 'http://test.com', networkClientId: 'mainnet', @@ -248,6 +255,8 @@ describe('wallet_invokeMethod', () => { }; await handler(walletRequest); expect(walletRequest).toStrictEqual({ + jsonrpc: '2.0' as const, + id: 0, scope: 'wallet', origin: 'http://test.com', networkClientId: 'selectedNetworkClientId', diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index 1ca8ff5b1e..55f6006083 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -1,19 +1,51 @@ -import { numberToHex } from '@metamask/utils'; +import type { Caveat } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { + Json, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; +import { numberToHex } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; +import type { ScopeString } from '../scope'; import { mergeScopes, parseScopeString } from '../scope'; +/** + * Handler for the `wallet_invokeMethod` RPC method. + * + * @param request - The request object. + * @param _response - The response object. + * @param next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - 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. + */ export async function walletInvokeMethodHandler( - request, - _response, - next, - end, - hooks, + request: JsonRpcRequest & { origin: string }, + _response: PendingJsonRpcResponse, + next: () => void, + end: (error: Error) => void, + hooks: { + getCaveat: ( + origin: string, + endowmentPermissionName: string, + caveatType: string, + ) => Caveat; + findNetworkClientIdByChainId: (chainId: string) => string | undefined; + getSelectedNetworkClientId: () => string; + }, ) { - const { scope, request: wrappedRequest } = request.params; + const { scope, request: wrappedRequest } = request.params as { + scope: ScopeString; + request: JsonRpcRequest; + }; let caveat; try { diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.ts b/packages/multichain/src/handlers/wallet-revokeSession.test.ts index 6a6add8023..695d0eb430 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.test.ts +++ b/packages/multichain/src/handlers/wallet-revokeSession.test.ts @@ -3,20 +3,29 @@ import { UnrecognizedSubjectError, } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + import { Caip25EndowmentPermissionName } from '../caip25Permission'; import { walletRevokeSessionHandler } from './wallet-revokeSession'; -const baseRequest = { +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 revokePermission = jest.fn(); - const response = {}; - const handler = (request) => + const response = { + result: true, + id: 1, + jsonrpc: '2.0' as const, + }; + const handler = (request: JsonRpcRequest & { origin: string }) => walletRevokeSessionHandler(request, response, next, end, { revokePermission, }); @@ -44,21 +53,24 @@ describe('wallet_revokeSession', () => { it('returns true if the CAIP-25 endowment permission does not exist', async () => { const { handler, response, revokePermission } = createMockedHandler(); revokePermission.mockImplementation(() => { - throw new PermissionDoesNotExistError(); + throw new PermissionDoesNotExistError( + 'foo.com', + Caip25EndowmentPermissionName, + ); }); await handler(baseRequest); - expect(response.result).toStrictEqual(true); + expect(response.result).toBe(true); }); it('returns true if the subject does not exist', async () => { const { handler, response, revokePermission } = createMockedHandler(); revokePermission.mockImplementation(() => { - throw new UnrecognizedSubjectError(); + throw new UnrecognizedSubjectError('foo.com'); }); await handler(baseRequest); - expect(response.result).toStrictEqual(true); + expect(response.result).toBe(true); }); it('throws an internal RPC error if something unexpected goes wrong with revoking the permission', async () => { @@ -75,6 +87,6 @@ describe('wallet_revokeSession', () => { const { handler, response } = createMockedHandler(); await handler(baseRequest); - expect(response.result).toStrictEqual(true); + expect(response.result).toBe(true); }); }); diff --git a/packages/multichain/src/handlers/wallet-revokeSession.ts b/packages/multichain/src/handlers/wallet-revokeSession.ts index d76994de90..1aec0b7245 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.ts +++ b/packages/multichain/src/handlers/wallet-revokeSession.ts @@ -1,20 +1,33 @@ -import type { JsonRpcEngineNextCallback, JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineNextCallback, + JsonRpcEngineEndCallback, +} from '@metamask/json-rpc-engine'; import { PermissionDoesNotExistError, UnrecognizedSubjectError, - PermissionController } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; +import type { JsonRpcSuccess, Json, JsonRpcRequest } from '@metamask/utils'; + import { Caip25EndowmentPermissionName } from '../caip25Permission'; -import { JsonRpcRequest, JsonRpcResponse } from '@metamask/utils'; +/** + * Handles the `wallet_revokeSession` RPC method. + * + * @param request - The JSON-RPC request object. + * @param response - The JSON-RPC response object. + * @param _next - The next middleware function. + * @param end - The end callback function. + * @param hooks - The hooks object. + * @param hooks.revokePermission - The revokePermission function. + */ export async function walletRevokeSessionHandler( - request: JsonRpcRequest, - response: JsonRpcResponse, + request: JsonRpcRequest & { origin: string }, + response: JsonRpcSuccess, _next: JsonRpcEngineNextCallback, end: JsonRpcEngineEndCallback, hooks: { - revokePermission: PermissionController['revokePermission'] + revokePermission: (origin: string, permissionName: string) => void; }, ) { try { diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts index b00cd7ab4e..c609752994 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts @@ -1,7 +1,5 @@ -import { JsonRpcRequest } from '@metamask/utils'; -import MultichainMiddlewareManager, { - ExtendedJsonRpcMiddleware, -} from './MultichainMiddlewareManager'; +import type { ExtendedJsonRpcMiddleware } from './MultichainMiddlewareManager'; +import MultichainMiddlewareManager from './MultichainMiddlewareManager'; const scope = 'eip155:1'; const origin = 'example.com'; @@ -28,13 +26,13 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, ); expect(middlewareSpy).toHaveBeenCalledWith( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -43,7 +41,7 @@ describe('MultichainMiddlewareManager', () => { expect(endSpy).not.toHaveBeenCalled(); }); - it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed', () => { + it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed', async () => { const multichainMiddlewareManager = new MultichainMiddlewareManager(); const middlewareSpy = jest.fn() as unknown as ExtendedJsonRpcMiddleware; multichainMiddlewareManager.addMiddleware({ @@ -59,13 +57,13 @@ describe('MultichainMiddlewareManager', () => { 123, ); - middleware.destroy?.(); + await middleware.destroy?.(); const nextSpy = jest.fn(); const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -97,7 +95,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -129,7 +127,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, @@ -161,7 +159,7 @@ describe('MultichainMiddlewareManager', () => { const endSpy = jest.fn(); middleware( - { scope } as unknown as JsonRpcRequest, + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, { jsonrpc: '2.0', id: 0 }, nextSpy, endSpy, diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts index e4c2663099..205c6a6751 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -1,11 +1,23 @@ -import { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { ExternalScopeString } from '../scope'; -import { Json, JsonRpcParams } from '@metamask/utils'; - -// Extend JsonRpcMiddleware to include the destroy method -// this was introduced in 7.0.0 of json-rpc-engine: https://github.com/MetaMask/json-rpc-engine/blob/v7.0.0/src/JsonRpcEngine.ts#L29-L40 -export type ExtendedJsonRpcMiddleware = JsonRpcMiddleware & { - destroy?: () => void; +import type { + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, +} from '@metamask/json-rpc-engine'; +import type { + Json, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; + +import type { ExternalScopeString } from '../scope'; + +export type ExtendedJsonRpcMiddleware = { + ( + req: JsonRpcRequest & { scope: string }, + res: PendingJsonRpcResponse, + next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + ): void; + destroy?: () => void | Promise; }; type MiddlewareKey = { @@ -57,7 +69,10 @@ export default class MultichainMiddlewareManager { return; } - existingMiddlewareEntry.middleware.destroy?.(); + // When the destroy function on the middleware is async, + // we don't need to wait for it complete + // eslint-disable-next-line no-void + void existingMiddlewareEntry.middleware.destroy?.(); this.#removeMiddlewareEntry(middlewareKey); } @@ -97,10 +112,7 @@ export default class MultichainMiddlewareManager { tabId?: number, ) { const middleware: ExtendedJsonRpcMiddleware = (req, res, next, end) => { - const r = req as unknown as { - scope: string; - }; - const { scope } = r; + const { scope } = req; const middlewareEntry = this.#getMiddlewareEntry({ scope, origin, @@ -110,8 +122,9 @@ export default class MultichainMiddlewareManager { if (middlewareEntry) { middlewareEntry.middleware(req, res, next, end); } else { - next(); + return next(); } + return undefined; }; middleware.destroy = this.removeMiddlewareByOriginAndTabId.bind( this, diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts index 86fda171ce..c951b711af 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -1,4 +1,5 @@ import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; + import MultichainSubscriptionManager from './MultichainSubscriptionManager'; jest.mock('@metamask/eth-json-rpc-filters/subscriptionManager', () => @@ -63,7 +64,7 @@ describe('MultichainSubscriptionManager', () => { }); it('should subscribe to a scope, origin, and tabId', () => { - const { multichainSubscriptionManager} = + const { multichainSubscriptionManager } = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); const onNotificationSpy = jest.fn(); @@ -83,7 +84,7 @@ describe('MultichainSubscriptionManager', () => { }); it('should unsubscribe from a scope', () => { - const { multichainSubscriptionManager} = + const { multichainSubscriptionManager } = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByScope(scope); @@ -92,7 +93,7 @@ describe('MultichainSubscriptionManager', () => { }); it('should unsubscribe from a scope and origin', () => { - const { multichainSubscriptionManager} = + const { multichainSubscriptionManager } = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByScopeAndOrigin(scope, origin); @@ -105,7 +106,7 @@ describe('MultichainSubscriptionManager', () => { }); it('should unsubscribe from a origin and tabId', () => { - const { multichainSubscriptionManager} = + const { multichainSubscriptionManager } = createMultichainSubscriptionManager(); multichainSubscriptionManager.subscribe({ scope, origin, tabId }); multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 668aa431aa..7ffb75b723 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -1,9 +1,11 @@ -import EventEmitter from 'events'; -import { NetworkController } from '@metamask/network-controller'; -import SafeEventEmitter from '@metamask/safe-event-emitter'; -import { CaipChainId, Hex, parseCaipChainId } from '@metamask/utils'; import { toHex } from '@metamask/controller-utils'; -import { ExternalScopeString } from '../scope'; +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 EventEmitter from 'events'; + +import type { ExternalScopeString } from '../scope'; export type SubscriptionManager = { events: EventEmitter; diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index 7c68c56b91..a57ac7835a 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -1,21 +1,22 @@ import { MultiChainOpenRPCDocument } from '@metamask/api-specs'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; import { rpcErrors } from '@metamask/rpc-errors'; -import { +import { isObject } from '@metamask/utils'; +import type { + Json, JsonRpcError, JsonRpcParams, JsonRpcRequest, - isObject, } from '@metamask/utils'; -import { +import type { ContentDescriptorObject, MethodObject, OpenrpcDocument, } 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 { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { Json } from '@metamask/utils'; -import { Schema, ValidationError, Validator } from 'jsonschema'; +import type { Schema, ValidationError } from 'jsonschema'; +import { Validator } from 'jsonschema'; const transformError = ( error: ValidationError, @@ -88,6 +89,7 @@ export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< JsonRpcRequest, Json > = function (request, _response, next, end) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises multichainMethodCallValidator(request.method, request.params).then( (errors) => { if (errors) { diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 919b6e6a38..75485352ba 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,6 +1,7 @@ import { JsonRpcError } from '@metamask/rpc-errors'; + import { assertScopeSupported, assertScopesSupported } from './assert'; -import { ScopeObject } from './scope'; +import type { ScopeObject } from './scope'; import * as Supported from './supported'; jest.mock('./supported', () => ({ @@ -133,10 +134,7 @@ describe('Scope Assert', () => { }, ); }).toThrow( - new JsonRpcError( - 5102, - 'Requested notifications are not supported', - ), + new JsonRpcError(5102, 'Requested notifications are not supported'), ); }); @@ -165,12 +163,14 @@ describe('Scope Assert', () => { const isChainIdSupported = jest.fn(); it('does not throw an error if no scopes are defined', () => { - assertScopesSupported( - {}, - { - isChainIdSupported, - }, - ); + expect( + assertScopesSupported( + {}, + { + isChainIdSupported, + }, + ), + ).toBeUndefined(); }); it('throws an error if any scope is invalid', () => { @@ -185,9 +185,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow( - new JsonRpcError(5100, 'Requested chains are not supported'), - ); + }).toThrow(new JsonRpcError(5100, 'Requested chains are not supported')); }); it('does not throw an error if all scopes are valid', () => { diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 2724ecd221..ea436fb909 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,11 +1,12 @@ -import { Hex } from '@metamask/utils'; import { JsonRpcError } from '@metamask/rpc-errors'; +import type { Hex } from '@metamask/utils'; + +import type { ScopeObject, ScopesObject } from './scope'; import { isSupportedMethod, isSupportedNotification, isSupportedScopeString, } from './supported'; -import { ScopeObject, ScopesObject } from './scope'; export const assertScopeSupported = ( scopeString: string, @@ -50,10 +51,7 @@ export const assertScopeSupported = ( // When provider does not recognize one or more requested notification(s) // code = 5202 // message = "Unknown notification(s) requested" - throw new JsonRpcError( - 5102, - 'Requested notifications are not supported', - ); + throw new JsonRpcError(5102, 'Requested notifications are not supported'); } }; diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 69e57dc32c..318718133d 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,11 +1,8 @@ -import * as Validation from './validation'; -import * as Transform from './transform'; +import { bucketScopes, validateAndFlattenScopes } from './authorization'; import * as Filter from './filter'; -import { - bucketScopes, - validateAndFlattenScopes, -} from './authorization'; -import { ExternalScopeObject } from './scope'; +import type { ExternalScopeObject } from './scope'; +import * as Transform from './transform'; +import * as Validation from './validation'; jest.mock('./validation', () => ({ validateScopes: jest.fn(), diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index b6c83cb1cf..3b4f5e0619 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,8 +1,9 @@ -import { validateScopes } from './validation'; -import { ExternalScopesObject, ScopesObject, ScopedProperties } from './scope'; -import { flattenMergeScopes } from './transform'; +import type { Hex } from '@metamask/utils'; + import { bucketScopesBySupport } from './filter'; -import { Hex } from '@metamask/utils'; +import type { ExternalScopesObject, ScopesObject } from './scope'; +import { flattenMergeScopes } from './transform'; +import { validateScopes } from './validation'; export type Caip25Authorization = | { diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts index 06b9795c49..ab5e889af1 100644 --- a/packages/multichain/src/scope/filter.ts +++ b/packages/multichain/src/scope/filter.ts @@ -1,6 +1,7 @@ -import { CaipChainId, Hex } from '@metamask/utils'; -import { ScopesObject } from './scope'; +import type { CaipChainId, Hex } from '@metamask/utils'; + import { assertScopeSupported } from './assert'; +import type { ScopesObject } from './scope'; export const bucketScopesBySupport = ( scopes: ScopesObject, diff --git a/packages/multichain/src/scope/scope.test.ts b/packages/multichain/src/scope/scope.test.ts index 2441c41c34..d3a58b3221 100644 --- a/packages/multichain/src/scope/scope.test.ts +++ b/packages/multichain/src/scope/scope.test.ts @@ -6,7 +6,7 @@ describe('Scope', () => { expect(parseScopeString('abc')).toStrictEqual({ namespace: 'abc' }); }); - it('returns the namespace and reference if scopeString is a CAIP chain ID ', () => { + it('returns the namespace and reference if scopeString is a CAIP chain ID', () => { expect(parseScopeString('abc:foo')).toStrictEqual({ namespace: 'abc', reference: 'foo', diff --git a/packages/multichain/src/scope/scope.ts b/packages/multichain/src/scope/scope.ts index ae452ee653..c899fa8a40 100644 --- a/packages/multichain/src/scope/scope.ts +++ b/packages/multichain/src/scope/scope.ts @@ -1,18 +1,20 @@ import MetaMaskOpenRPCDocument from '@metamask/api-specs'; -import { +import type { CaipChainId, CaipReference, CaipAccountId, + KnownCaipNamespace, + CaipNamespace, +} from '@metamask/utils'; +import { isCaipNamespace, isCaipChainId, parseCaipChainId, - KnownCaipNamespace, - CaipNamespace, } from '@metamask/utils'; -export type NonWalletKnownCaipNamespace = Exclude< +export type NonWalletKnownCaipNamespace = Extract< KnownCaipNamespace, - KnownCaipNamespace.Wallet + KnownCaipNamespace.Eip155 >; export const KnownWalletRpcMethods: string[] = [ @@ -22,7 +24,7 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; const Eip155Methods = MetaMaskOpenRPCDocument.methods - .map(({ name }: { name: string}) => name) + .map(({ name }: { name: string }) => name) .filter((method: string) => !WalletEip155Methods.includes(method)) .filter((method: string) => !KnownWalletRpcMethods.includes(method)); diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index 30b8549107..b8146ebd15 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -1,14 +1,14 @@ -import { - isSupportedMethod, - isSupportedNotification, - isSupportedScopeString, -} from './supported'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, } from './scope'; +import { + isSupportedMethod, + isSupportedNotification, + isSupportedScopeString, +} from './supported'; describe('Scope Support', () => { describe('isSupportedNotification', () => { @@ -16,18 +16,14 @@ describe('Scope Support', () => { 'returns true for each %s scope method', (scopeString: string, notifications: string[]) => { notifications.forEach((notification) => { - expect( - isSupportedNotification(scopeString, notification), - ).toStrictEqual(true); + expect(isSupportedNotification(scopeString, notification)).toBe(true); }); }, ); it('returns false otherwise', () => { - expect(isSupportedNotification('eip155', 'anything else')).toStrictEqual( - false, - ); - expect(isSupportedNotification('', '')).toStrictEqual(false); + expect(isSupportedNotification('eip155', 'anything else')).toBe(false); + expect(isSupportedNotification('', '')).toBe(false); }); }); @@ -36,14 +32,14 @@ describe('Scope Support', () => { 'returns true for each %s scoped method', (scopeString: string, methods: string[]) => { methods.forEach((method) => { - expect(isSupportedMethod(scopeString, method)).toStrictEqual(true); + expect(isSupportedMethod(scopeString, method)).toBe(true); }); }, ); it('returns true for each wallet scoped method', () => { KnownWalletRpcMethods.forEach((method) => { - expect(isSupportedMethod('wallet', method)).toStrictEqual(true); + expect(isSupportedMethod('wallet', method)).toBe(true); }); }); @@ -51,46 +47,42 @@ describe('Scope Support', () => { 'returns true for each wallet:%s scoped method', (scopeString: string, methods: string[]) => { methods.forEach((method) => { - expect( - isSupportedMethod(`wallet:${scopeString}`, method), - ).toStrictEqual(true); + expect(isSupportedMethod(`wallet:${scopeString}`, method)).toBe(true); }); }, ); it('returns false otherwise', () => { - expect(isSupportedMethod('eip155', 'anything else')).toStrictEqual(false); - expect(isSupportedMethod('', '')).toStrictEqual(false); + expect(isSupportedMethod('eip155', 'anything else')).toBe(false); + expect(isSupportedMethod('', '')).toBe(false); }); }); describe('isSupportedScopeString', () => { it('returns true for the wallet namespace', () => { - expect(isSupportedScopeString('wallet', jest.fn())).toStrictEqual(true); + expect(isSupportedScopeString('wallet', jest.fn())).toBe(true); }); it('returns false for the wallet namespace when a reference is included', () => { - expect(isSupportedScopeString('wallet:someref', jest.fn())).toStrictEqual( - false, - ); + expect(isSupportedScopeString('wallet:someref', jest.fn())).toBe(false); }); it('returns true for the ethereum namespace', () => { - expect(isSupportedScopeString('eip155', jest.fn())).toStrictEqual(true); + expect(isSupportedScopeString('eip155', jest.fn())).toBe(true); }); it('returns true for the ethereum namespace when a network client exists for the reference', () => { const isChainIdSupportedMock = jest.fn().mockReturnValue(true); - expect( - isSupportedScopeString('eip155:1', isChainIdSupportedMock), - ).toStrictEqual(true); + expect(isSupportedScopeString('eip155:1', isChainIdSupportedMock)).toBe( + true, + ); }); it('returns false for the ethereum namespace when a network client does not exist for the reference', () => { const isChainIdSupportedMock = jest.fn().mockReturnValue(false); - expect( - isSupportedScopeString('eip155:1', isChainIdSupportedMock), - ).toStrictEqual(false); + expect(isSupportedScopeString('eip155:1', isChainIdSupportedMock)).toBe( + false, + ); }); }); }); diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 9ca98be6e2..80ceb961f4 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -1,29 +1,31 @@ +import { toHex } from '@metamask/controller-utils'; +import type { CaipAccountId, Hex } from '@metamask/utils'; import { - CaipAccountId, - Hex, isCaipChainId, isCaipNamespace, KnownCaipNamespace, parseCaipAccountId, parseCaipChainId, } from '@metamask/utils'; -import { toHex } from '@metamask/controller-utils'; -import { InternalAccount } from '@metamask/keyring-api'; + +import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './scope'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, - NonWalletKnownCaipNamespace, parseScopeString, - ExternalScopeString, } from './scope'; // TODO Maybe this gets DRY'ed into utils?.. It's used in TokenDetectionController too -function isEqualCaseInsensitive( - value1: string, - value2: string, -): boolean { +/** + * Checks if two strings are equal, ignoring case. + * + * @param value1 - The first string to compare. + * @param value2 - The second string to compare. + * @returns `true` if the strings are equal, ignoring case; otherwise, `false`. + */ +function isEqualCaseInsensitive(value1: string, value2: string): boolean { if (typeof value1 !== 'string' || typeof value2 !== 'string') { return false; } @@ -68,7 +70,7 @@ export const isSupportedScopeString = ( export const isSupportedAccount = ( account: CaipAccountId, - getInternalAccounts: () => InternalAccount[], + getInternalAccounts: () => { type: string; address: string }[], ) => { const { address, diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index df0b529822..d092735eb6 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -1,4 +1,4 @@ -import { ExternalScopeObject } from './scope'; +import type { ExternalScopeObject } from './scope'; import { flattenScope, mergeScopes, diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index a31faf2d34..097ad725d8 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -1,16 +1,23 @@ -import { CaipReference } from '@metamask/utils'; +import type { CaipReference } from '@metamask/utils'; import { cloneDeep } from 'lodash'; -import { + +import type { ExternalScopeObject, ExternalScopesObject, ScopeString, ScopeObject, ScopesObject, - parseScopeString, } from './scope'; +import { parseScopeString } from './scope'; -// DRY THIS -function unique(list: T[]): T[] { +// TODO: DRY THIS +/** + * Returns a list of unique items + * + * @param list - The list of items to filter + * @returns A list of unique items + */ +function unique(list: Value[]): Value[] { return Array.from(new Set(list)); } @@ -32,7 +39,7 @@ export const flattenScope = ( const { namespace, reference } = parseScopeString(scopeString); // Scope is already a CAIP-2 ID and has no references to flatten - if (reference || !references) { + if (!namespace || reference || !references) { return { [scopeString]: scopeObject }; } diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index 507f24b328..f4f4ae63e3 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -1,8 +1,5 @@ -import { ExternalScopeObject } from './scope'; -import { - isValidScope, - validateScopes, -} from './validation'; +import type { ExternalScopeObject } from './scope'; +import { isValidScope, validateScopes } from './validation'; const validScopeString = 'eip155:1'; const validScopeObject: ExternalScopeObject = { @@ -128,7 +125,9 @@ describe('Scope Validation', () => { scopeString: string, scopeObject: unknown, ) => { - expect(isValidScope(scopeString, scopeObject as ExternalScopeObject)).toStrictEqual(expected); + expect( + isValidScope(scopeString, scopeObject as ExternalScopeObject), + ).toStrictEqual(expected); }, ); }); @@ -140,16 +139,20 @@ describe('Scope Validation', () => { }; it('does not throw an error if required scopes are defined but none are valid', () => { - validateScopes( - { 'eip155:1': {} as unknown as ExternalScopeObject }, - undefined, - ); + expect( + validateScopes( + { 'eip155:1': {} as unknown as ExternalScopeObject }, + undefined, + ), + ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); }); it('does not throw an error if optional scopes are defined but none are valid', () => { - validateScopes(undefined, { - 'eip155:1': {} as unknown as ExternalScopeObject, - }); + expect( + validateScopes(undefined, { + 'eip155:1': {} as unknown as ExternalScopeObject, + }), + ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); }); it('returns the valid required and optional scopes', () => { diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index 8a5ab1a1cd..69bc3e1bb9 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -1,10 +1,11 @@ import { isCaipReference } from '@metamask/utils'; -import { + +import type { ExternalScopeString, - parseScopeString, ExternalScopeObject, ExternalScopesObject, } from './scope'; +import { parseScopeString } from './scope'; export const isValidScope = ( scopeString: ExternalScopeString, diff --git a/packages/multichain/tsconfig.build.json b/packages/multichain/tsconfig.build.json index 02a0eea03f..f2108df276 100644 --- a/packages/multichain/tsconfig.build.json +++ b/packages/multichain/tsconfig.build.json @@ -3,8 +3,16 @@ "compilerOptions": { "baseUrl": "./", "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "resolveJsonModule": true }, - "references": [], + "references": [ + { + "path": "../network-controller/tsconfig.build.json" + }, + { + "path": "../permission-controller/tsconfig.build.json" + } + ], "include": ["../../types", "./src"] } diff --git a/yarn.lock b/yarn.lock index b43e7ce698..88a64433d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3089,16 +3089,20 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/safe-event-emitter": "npm:^3.0.0" + "@metamask/utils": "npm:^9.1.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.2.4" lodash: "npm:^4.17.21" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" From 85a723e5a18ed07307f9e1467e17f4da47dd2336 Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 11 Oct 2024 16:18:32 -0400 Subject: [PATCH 09/80] Added exports for multichain package (#4789) ## Explanation Added ESM exports for multichain package ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- packages/multichain/package.json | 90 ++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index e633a1bb2b..a1ad56d292 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -27,6 +27,96 @@ "default": "./dist/index.cjs" } }, + "./caip25Permission": { + "import": { + "types": "./dist/caip25Permission.d.mts", + "default": "./dist/caip25Permission.mjs" + } + }, + "./adapters/caip-permission-adapter-eth-accounts": { + "import": { + "types": "./dist/adapters/caip-permission-adapter-eth-accounts.d.mts", + "default": "./dist/adapters/caip-permission-adapter-eth-accounts.mjs" + } + }, + "./adapters/caip-permission-adapter-middleware": { + "import": { + "types": "./dist/adapters/caip-permission-adapter-middleware.d.mts", + "default": "./dist/adapters/caip-permission-adapter-middleware.mjs" + } + }, + "./handlers/wallet-getSession": { + "import": { + "types": "./dist/handlers/wallet-getSession.d.mts", + "default": "./dist/handlers/wallet-getSession.mjs" + } + }, + "./handlers/wallet-invokeMethod": { + "import": { + "types": "./dist/handlers/wallet-invokeMethod.d.mts", + "default": "./dist/handlers/wallet-invokeMethod.mjs" + } + }, + "./handlers/wallet-revokeSession": { + "import": { + "types": "./dist/handlers/wallet-revokeSession.d.mts", + "default": "./dist/handlers/wallet-revokeSession.mjs" + } + }, + "./middlewares/MultichainMethodCallValidator": { + "import": { + "types": "./dist/middlewares/MultichainMethodCallValidator.d.mts", + "default": "./dist/middlewares/MultichainMethodCallValidator.mjs" + } + }, + "./middlewares/MultichainMiddlewareManager": { + "import": { + "types": "./dist/middlewares/MultichainMiddlewareManager.d.mts", + "default": "./dist/middlewares/MultichainMiddlewareManager.mjs" + } + }, + "./middlewares/MultichainSubscriptionManager": { + "import": { + "types": "./dist/middlewares/MultichainSubscriptionManager.d.mts", + "default": "./dist/middlewares/MultichainSubscriptionManager.mjs" + } + }, + "./scope/authorization": { + "import": { + "types": "./dist/scope/authorization.d.mts", + "default": "./dist/scope/authorization.mjs" + } + }, + "./scope/filter": { + "import": { + "types": "./dist/scope/filter.d.mts", + "default": "./dist/scope/filter.mjs" + } + }, + "./scope/scope": { + "import": { + "types": "./dist/scope/scope.d.mts", + "default": "./dist/scope/scope.mjs" + } + }, + "./scope/supported": { + "import": { + "types": "./dist/scope/supported.d.mts", + "default": "./dist/scope/supported.mjs" + } + }, + "./scope/transform": { + "import": { + "types": "./dist/scope/transform.d.mts", + "default": "./dist/scope/transform.mjs" + } + }, + "./scope/validation": { + "import": { + "types": "./dist/scope/validation.d.mts", + "default": "./dist/scope/validation.mjs" + } + }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", From a0bb278a2a6c68f7d2f787d2598318d1b225f065 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 11 Oct 2024 13:43:24 -0700 Subject: [PATCH 10/80] Add requires --- packages/multichain/package.json | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index a1ad56d292..56d08e2992 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -31,90 +31,150 @@ "import": { "types": "./dist/caip25Permission.d.mts", "default": "./dist/caip25Permission.mjs" + }, + "require": { + "types": "./dist/caip25Permission.d.cts", + "default": "./dist/caip25Permission.cjs" } }, "./adapters/caip-permission-adapter-eth-accounts": { "import": { "types": "./dist/adapters/caip-permission-adapter-eth-accounts.d.mts", "default": "./dist/adapters/caip-permission-adapter-eth-accounts.mjs" + }, + "require": { + "types": "./dist/adapters/caip-permission-adapter-eth-accounts.d.cts", + "default": "./dist/adapters/caip-permission-adapter-eth-accounts.cjs" } }, "./adapters/caip-permission-adapter-middleware": { "import": { "types": "./dist/adapters/caip-permission-adapter-middleware.d.mts", "default": "./dist/adapters/caip-permission-adapter-middleware.mjs" + }, + "require": { + "types": "./dist/adapters/caip-permission-adapter-middleware.d.cts", + "default": "./dist/adapters/caip-permission-adapter-middleware.cjs" } }, "./handlers/wallet-getSession": { "import": { "types": "./dist/handlers/wallet-getSession.d.mts", "default": "./dist/handlers/wallet-getSession.mjs" + }, + "require": { + "types": "./dist/handlers/wallet-getSession.d.cts", + "default": "./dist/handlers/wallet-getSession.cjs" } }, "./handlers/wallet-invokeMethod": { "import": { "types": "./dist/handlers/wallet-invokeMethod.d.mts", "default": "./dist/handlers/wallet-invokeMethod.mjs" + }, + "require": { + "types": "./dist/handlers/wallet-invokeMethod.d.cts", + "default": "./dist/handlers/wallet-invokeMethod.cjs" } }, "./handlers/wallet-revokeSession": { "import": { "types": "./dist/handlers/wallet-revokeSession.d.mts", "default": "./dist/handlers/wallet-revokeSession.mjs" + }, + "require": { + "types": "./dist/handlers/wallet-revokeSession.d.cts", + "default": "./dist/handlers/wallet-revokeSession.cjs" } }, "./middlewares/MultichainMethodCallValidator": { "import": { "types": "./dist/middlewares/MultichainMethodCallValidator.d.mts", "default": "./dist/middlewares/MultichainMethodCallValidator.mjs" + }, + "require": { + "types": "./dist/middlewares/MultichainMethodCallValidator.d.cts", + "default": "./dist/middlewares/MultichainMethodCallValidator.cjs" } }, "./middlewares/MultichainMiddlewareManager": { "import": { "types": "./dist/middlewares/MultichainMiddlewareManager.d.mts", "default": "./dist/middlewares/MultichainMiddlewareManager.mjs" + }, + "require": { + "types": "./dist/middlewares/MultichainMiddlewareManager.d.cts", + "default": "./dist/middlewares/MultichainMiddlewareManager.cjs" } }, "./middlewares/MultichainSubscriptionManager": { "import": { "types": "./dist/middlewares/MultichainSubscriptionManager.d.mts", "default": "./dist/middlewares/MultichainSubscriptionManager.mjs" + }, + "require": { + "types": "./dist/middlewares/MultichainSubscriptionManager.d.cts", + "default": "./dist/middlewares/MultichainSubscriptionManager.cjs" } }, "./scope/authorization": { "import": { "types": "./dist/scope/authorization.d.mts", "default": "./dist/scope/authorization.mjs" + }, + "require": { + "types": "./dist/scope/authorization.d.cts", + "default": "./dist/scope/authorization.cjs" } }, "./scope/filter": { "import": { "types": "./dist/scope/filter.d.mts", "default": "./dist/scope/filter.mjs" + }, + "require": { + "types": "./dist/scope/filter.d.cts", + "default": "./dist/scope/filter.cjs" } }, "./scope/scope": { "import": { "types": "./dist/scope/scope.d.mts", "default": "./dist/scope/scope.mjs" + }, + "require": { + "types": "./dist/scope/scope.d.cts", + "default": "./dist/scope/scope.cjs" } }, "./scope/supported": { "import": { "types": "./dist/scope/supported.d.mts", "default": "./dist/scope/supported.mjs" + }, + "require": { + "types": "./dist/scope/supported.d.cts", + "default": "./dist/scope/supported.cjs" } }, "./scope/transform": { "import": { "types": "./dist/scope/transform.d.mts", "default": "./dist/scope/transform.mjs" + }, + "require": { + "types": "./dist/scope/transform.d.cts", + "default": "./dist/scope/transform.cjs" } }, "./scope/validation": { "import": { "types": "./dist/scope/validation.d.mts", "default": "./dist/scope/validation.mjs" + }, + "require": { + "types": "./dist/scope/validation.d.cts", + "default": "./dist/scope/validation.cjs" } }, "./package.json": "./package.json" From 1cd8ef288acfce170984e0d1ff5e4783dec6f8d5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 11 Oct 2024 14:11:37 -0700 Subject: [PATCH 11/80] add permittedChains adapter to exports --- packages/multichain/package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 56d08e2992..547a73d957 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -57,6 +57,16 @@ "default": "./dist/adapters/caip-permission-adapter-middleware.cjs" } }, + "./adapters/caip-permission-adapter-permittedChains": { + "import": { + "types": "./dist/adapters/caip-permission-adapter-permittedChains.d.mts", + "default": "./dist/adapters/caip-permission-adapter-permittedChains.mjs" + }, + "require": { + "types": "./dist/adapters/caip-permission-adapter-permittedChains.d.cts", + "default": "./dist/adapters/caip-permission-adapter-permittedChains.cjs" + } + }, "./handlers/wallet-getSession": { "import": { "types": "./dist/handlers/wallet-getSession.d.mts", From c636def90f3c48c780337b6d5fa2a0ad4c240e25 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 11 Oct 2024 14:56:35 -0700 Subject: [PATCH 12/80] kill scope index barrel --- packages/multichain/jest.config.js | 4 +-- .../caip-permission-adapter-eth-accounts.ts | 5 +-- .../caip-permission-adapter-middleware.ts | 4 +-- ...permission-adapter-permittedChains.test.ts | 2 +- ...caip-permission-adapter-permittedChains.ts | 6 ++-- .../multichain/src/caip25Permission.test.ts | 35 +++++++++++-------- packages/multichain/src/caip25Permission.ts | 9 +++-- .../src/handlers/wallet-getSession.ts | 4 +-- .../src/handlers/wallet-invokeMethod.ts | 5 +-- .../MultichainMiddlewareManager.ts | 2 +- .../MultichainSubscriptionManager.ts | 2 +- packages/multichain/src/scope/index.ts | 7 ---- 12 files changed, 46 insertions(+), 39 deletions(-) delete mode 100644 packages/multichain/src/scope/index.ts diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js index f8be8cb30e..2f651a645e 100644 --- a/packages/multichain/jest.config.js +++ b/packages/multichain/jest.config.js @@ -19,8 +19,8 @@ module.exports = merge(baseConfig, { global: { branches: 83.05, functions: 87.37, - lines: 86.73, - statements: 87.17, + lines: 86.65, + statements: 87.09, }, }, }); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 95cb5dd0ec..d47957cbc8 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -6,8 +6,9 @@ import { } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import type { ScopesObject } from '../scope'; -import { mergeScopes, parseScopeString, type ScopeString } from '../scope'; +import type { ScopesObject, ScopeString } from '../scope/scope'; +import { parseScopeString } from '../scope/scope'; +import { mergeScopes } from '../scope/transform'; const isEip155ScopeString = (scopeString: ScopeString) => { const { namespace, reference } = parseScopeString(scopeString); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index d92e0292e2..594d0bf7ae 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -8,8 +8,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopeString } from '../scope'; -import { mergeScopes } from '../scope'; +import type { ScopeString } from '../scope/scope'; +import { mergeScopes } from '../scope/transform'; /** * Middleware to handle CAIP-25 permission requests. diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index a740207807..665fc4779c 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -1,5 +1,5 @@ import type { Caip25CaveatValue } from '../caip25Permission'; -import { KnownNotifications, KnownRpcMethods } from '../scope'; +import { KnownNotifications, KnownRpcMethods } from '../scope/scope'; import { addPermittedEthChainId, getPermittedEthChainIds, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index cfbbdedd29..6ec08ecd5c 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,13 +3,13 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import type { ScopesObject, ScopeString } from '../scope'; +import type { ScopesObject, ScopeString } from '../scope/scope'; import { KnownNotifications, KnownRpcMethods, - mergeScopes, parseScopeString, -} from '../scope'; +} from '../scope/scope'; +import { mergeScopes } from '../scope/transform'; export const getPermittedEthChainIds = ( caip25CaveatValue: Caip25CaveatValue, diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 7e6d6f243d..eba34e350c 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -14,19 +14,24 @@ import { Caip25CaveatMutatorFactories, removeScope, } from './caip25Permission'; -import * as Scope from './scope'; +import * as ScopeAssert from './scope/assert'; +import * as ScopeAuthorization from './scope/authorization'; -jest.mock('./scope', () => ({ +jest.mock('./scope/authorization', () => ({ validateAndFlattenScopes: jest.fn(), +})); +const MockScopeAuthorization = jest.mocked(ScopeAuthorization); + +jest.mock('./scope/assert', () => ({ assertScopesSupported: jest.fn(), })); -const MockScope = jest.mocked(Scope); +const MockScopeAssert = jest.mocked(ScopeAssert); const { removeAccount } = Caip25CaveatMutatorFactories[Caip25CaveatType]; describe('endowment:caip25', () => { beforeEach(() => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: {}, flattenedOptionalScopes: {}, }); @@ -409,7 +414,9 @@ describe('endowment:caip25', () => { } catch (err) { // noop } - expect(MockScope.validateAndFlattenScopes).toHaveBeenCalledWith( + expect( + MockScopeAuthorization.validateAndFlattenScopes, + ).toHaveBeenCalledWith( { 'eip155:1': { methods: ['eth_chainId'], @@ -428,7 +435,7 @@ describe('endowment:caip25', () => { }); it('asserts the validated and flattened required scopes are supported', () => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: { 'eip155:1': { methods: ['flattened_required'], @@ -474,7 +481,7 @@ describe('endowment:caip25', () => { } catch (err) { // noop } - expect(MockScope.assertScopesSupported).toHaveBeenCalledWith( + expect(MockScopeAssert.assertScopesSupported).toHaveBeenCalledWith( { 'eip155:1': { methods: ['flattened_required'], @@ -486,12 +493,12 @@ describe('endowment:caip25', () => { }), ); const isChainIdSupportedBody = - MockScope.assertScopesSupported.mock.calls[0][1].isChainIdSupported.toString(); + MockScopeAssert.assertScopesSupported.mock.calls[0][1].isChainIdSupported.toString(); expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); }); it('asserts the validated and flattened optional scopes are supported', () => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: { 'eip155:1': { methods: ['flattened_required'], @@ -537,7 +544,7 @@ describe('endowment:caip25', () => { } catch (err) { // noop } - expect(MockScope.assertScopesSupported).toHaveBeenCalledWith( + expect(MockScopeAssert.assertScopesSupported).toHaveBeenCalledWith( { 'eip155:1': { methods: ['flattened_optional'], @@ -549,12 +556,12 @@ describe('endowment:caip25', () => { }), ); const isChainIdSupportedBody = - MockScope.assertScopesSupported.mock.calls[1][1].isChainIdSupported.toString(); + MockScopeAssert.assertScopesSupported.mock.calls[1][1].isChainIdSupported.toString(); expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); }); it('throws if the input requiredScopes does not match the output of validateAndFlattenScopes', () => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: {}, flattenedOptionalScopes: { 'eip155:5': { @@ -597,7 +604,7 @@ describe('endowment:caip25', () => { }); it('throws if the input optionalScopes does not match the output of validateAndFlattenScopes', () => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], @@ -640,7 +647,7 @@ describe('endowment:caip25', () => { }); it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and flattened', () => { - MockScope.validateAndFlattenScopes.mockReturnValue({ + MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ flattenedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index d4cb66428c..2dd7b97739 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -20,8 +20,13 @@ import { import { strict as assert } from 'assert'; import { cloneDeep, isEqual } from 'lodash'; -import type { ExternalScopeString, ScopesObject, ScopeObject } from './scope'; -import { validateAndFlattenScopes, assertScopesSupported } from './scope'; +import { assertScopesSupported } from './scope/assert'; +import { validateAndFlattenScopes } from './scope/authorization'; +import type { + ExternalScopeString, + ScopeObject, + ScopesObject, +} from './scope/scope'; export type Caip25CaveatValue = { requiredScopes: ScopesObject; diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index 7f0032d00d..be5295b4ff 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -6,8 +6,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopesObject } from '../scope'; -import { mergeScopes } from '../scope'; +import type { ScopesObject } from '../scope/scope'; +import { mergeScopes } from '../scope/transform'; /** * Handler for the `wallet_getSession` RPC method. diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index 55f6006083..eea6d6a2b1 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -12,8 +12,9 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopeString } from '../scope'; -import { mergeScopes, parseScopeString } from '../scope'; +import type { ScopeString } from '../scope/scope'; +import { parseScopeString } from '../scope/scope'; +import { mergeScopes } from '../scope/transform'; /** * Handler for the `wallet_invokeMethod` RPC method. diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts index 205c6a6751..336b33978a 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -8,7 +8,7 @@ import type { PendingJsonRpcResponse, } from '@metamask/utils'; -import type { ExternalScopeString } from '../scope'; +import type { ExternalScopeString } from '../scope/scope'; export type ExtendedJsonRpcMiddleware = { ( diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 7ffb75b723..66f3ae550a 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -5,7 +5,7 @@ import type { CaipChainId, Hex } from '@metamask/utils'; import { parseCaipChainId } from '@metamask/utils'; import type EventEmitter from 'events'; -import type { ExternalScopeString } from '../scope'; +import type { ExternalScopeString } from '../scope/scope'; export type SubscriptionManager = { events: EventEmitter; diff --git a/packages/multichain/src/scope/index.ts b/packages/multichain/src/scope/index.ts deleted file mode 100644 index c1b804efec..0000000000 --- a/packages/multichain/src/scope/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './assert'; -export * from './authorization'; -export * from './filter'; -export * from './scope'; -export * from './supported'; -export * from './transform'; -export * from './validation'; From a6aa7c1a5f6ad9074f155d71665af74781963f20 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 08:30:45 -0700 Subject: [PATCH 13/80] barrel --- packages/multichain/package.json | 160 ------------------ .../caip-permission-adapter-eth-accounts.ts | 4 +- .../caip-permission-adapter-middleware.ts | 2 +- ...permission-adapter-permittedChains.test.ts | 2 +- ...caip-permission-adapter-permittedChains.ts | 4 +- packages/multichain/src/caip25Permission.ts | 2 +- .../src/handlers/wallet-getSession.ts | 2 +- .../src/handlers/wallet-invokeMethod.ts | 4 +- packages/multichain/src/index.test.ts | 52 +++++- packages/multichain/src/index.ts | 30 +++- .../MultichainMiddlewareManager.test.ts | 2 +- .../MultichainMiddlewareManager.ts | 4 +- .../MultichainSubscriptionManager.test.ts | 2 +- .../MultichainSubscriptionManager.ts | 4 +- .../multichainMethodCallValidator.ts | 2 +- packages/multichain/src/scope/assert.test.ts | 2 +- packages/multichain/src/scope/assert.ts | 2 +- .../src/scope/authorization.test.ts | 2 +- .../multichain/src/scope/authorization.ts | 2 +- packages/multichain/src/scope/filter.ts | 2 +- packages/multichain/src/scope/scope.test.ts | 2 +- .../multichain/src/scope/supported.test.ts | 2 +- packages/multichain/src/scope/supported.ts | 4 +- .../multichain/src/scope/transform.test.ts | 2 +- packages/multichain/src/scope/transform.ts | 4 +- .../src/scope/{scope.ts => types.ts} | 0 .../multichain/src/scope/validation.test.ts | 2 +- packages/multichain/src/scope/validation.ts | 4 +- 28 files changed, 99 insertions(+), 207 deletions(-) rename packages/multichain/src/scope/{scope.ts => types.ts} (100%) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 547a73d957..e633a1bb2b 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -27,166 +27,6 @@ "default": "./dist/index.cjs" } }, - "./caip25Permission": { - "import": { - "types": "./dist/caip25Permission.d.mts", - "default": "./dist/caip25Permission.mjs" - }, - "require": { - "types": "./dist/caip25Permission.d.cts", - "default": "./dist/caip25Permission.cjs" - } - }, - "./adapters/caip-permission-adapter-eth-accounts": { - "import": { - "types": "./dist/adapters/caip-permission-adapter-eth-accounts.d.mts", - "default": "./dist/adapters/caip-permission-adapter-eth-accounts.mjs" - }, - "require": { - "types": "./dist/adapters/caip-permission-adapter-eth-accounts.d.cts", - "default": "./dist/adapters/caip-permission-adapter-eth-accounts.cjs" - } - }, - "./adapters/caip-permission-adapter-middleware": { - "import": { - "types": "./dist/adapters/caip-permission-adapter-middleware.d.mts", - "default": "./dist/adapters/caip-permission-adapter-middleware.mjs" - }, - "require": { - "types": "./dist/adapters/caip-permission-adapter-middleware.d.cts", - "default": "./dist/adapters/caip-permission-adapter-middleware.cjs" - } - }, - "./adapters/caip-permission-adapter-permittedChains": { - "import": { - "types": "./dist/adapters/caip-permission-adapter-permittedChains.d.mts", - "default": "./dist/adapters/caip-permission-adapter-permittedChains.mjs" - }, - "require": { - "types": "./dist/adapters/caip-permission-adapter-permittedChains.d.cts", - "default": "./dist/adapters/caip-permission-adapter-permittedChains.cjs" - } - }, - "./handlers/wallet-getSession": { - "import": { - "types": "./dist/handlers/wallet-getSession.d.mts", - "default": "./dist/handlers/wallet-getSession.mjs" - }, - "require": { - "types": "./dist/handlers/wallet-getSession.d.cts", - "default": "./dist/handlers/wallet-getSession.cjs" - } - }, - "./handlers/wallet-invokeMethod": { - "import": { - "types": "./dist/handlers/wallet-invokeMethod.d.mts", - "default": "./dist/handlers/wallet-invokeMethod.mjs" - }, - "require": { - "types": "./dist/handlers/wallet-invokeMethod.d.cts", - "default": "./dist/handlers/wallet-invokeMethod.cjs" - } - }, - "./handlers/wallet-revokeSession": { - "import": { - "types": "./dist/handlers/wallet-revokeSession.d.mts", - "default": "./dist/handlers/wallet-revokeSession.mjs" - }, - "require": { - "types": "./dist/handlers/wallet-revokeSession.d.cts", - "default": "./dist/handlers/wallet-revokeSession.cjs" - } - }, - "./middlewares/MultichainMethodCallValidator": { - "import": { - "types": "./dist/middlewares/MultichainMethodCallValidator.d.mts", - "default": "./dist/middlewares/MultichainMethodCallValidator.mjs" - }, - "require": { - "types": "./dist/middlewares/MultichainMethodCallValidator.d.cts", - "default": "./dist/middlewares/MultichainMethodCallValidator.cjs" - } - }, - "./middlewares/MultichainMiddlewareManager": { - "import": { - "types": "./dist/middlewares/MultichainMiddlewareManager.d.mts", - "default": "./dist/middlewares/MultichainMiddlewareManager.mjs" - }, - "require": { - "types": "./dist/middlewares/MultichainMiddlewareManager.d.cts", - "default": "./dist/middlewares/MultichainMiddlewareManager.cjs" - } - }, - "./middlewares/MultichainSubscriptionManager": { - "import": { - "types": "./dist/middlewares/MultichainSubscriptionManager.d.mts", - "default": "./dist/middlewares/MultichainSubscriptionManager.mjs" - }, - "require": { - "types": "./dist/middlewares/MultichainSubscriptionManager.d.cts", - "default": "./dist/middlewares/MultichainSubscriptionManager.cjs" - } - }, - "./scope/authorization": { - "import": { - "types": "./dist/scope/authorization.d.mts", - "default": "./dist/scope/authorization.mjs" - }, - "require": { - "types": "./dist/scope/authorization.d.cts", - "default": "./dist/scope/authorization.cjs" - } - }, - "./scope/filter": { - "import": { - "types": "./dist/scope/filter.d.mts", - "default": "./dist/scope/filter.mjs" - }, - "require": { - "types": "./dist/scope/filter.d.cts", - "default": "./dist/scope/filter.cjs" - } - }, - "./scope/scope": { - "import": { - "types": "./dist/scope/scope.d.mts", - "default": "./dist/scope/scope.mjs" - }, - "require": { - "types": "./dist/scope/scope.d.cts", - "default": "./dist/scope/scope.cjs" - } - }, - "./scope/supported": { - "import": { - "types": "./dist/scope/supported.d.mts", - "default": "./dist/scope/supported.mjs" - }, - "require": { - "types": "./dist/scope/supported.d.cts", - "default": "./dist/scope/supported.cjs" - } - }, - "./scope/transform": { - "import": { - "types": "./dist/scope/transform.d.mts", - "default": "./dist/scope/transform.mjs" - }, - "require": { - "types": "./dist/scope/transform.d.cts", - "default": "./dist/scope/transform.cjs" - } - }, - "./scope/validation": { - "import": { - "types": "./dist/scope/validation.d.mts", - "default": "./dist/scope/validation.mjs" - }, - "require": { - "types": "./dist/scope/validation.d.cts", - "default": "./dist/scope/validation.cjs" - } - }, "./package.json": "./package.json" }, "main": "./dist/index.cjs", diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index d47957cbc8..db459460cc 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -6,8 +6,8 @@ import { } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import type { ScopesObject, ScopeString } from '../scope/scope'; -import { parseScopeString } from '../scope/scope'; +import type { ScopesObject, ScopeString } from '../scope/types'; +import { parseScopeString } from '../scope/types'; import { mergeScopes } from '../scope/transform'; const isEip155ScopeString = (scopeString: ScopeString) => { diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 594d0bf7ae..1ce16cee3b 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -8,7 +8,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopeString } from '../scope/scope'; +import type { ScopeString } from '../scope/types'; import { mergeScopes } from '../scope/transform'; /** diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 665fc4779c..127a15cb40 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -1,5 +1,5 @@ import type { Caip25CaveatValue } from '../caip25Permission'; -import { KnownNotifications, KnownRpcMethods } from '../scope/scope'; +import { KnownNotifications, KnownRpcMethods } from '../scope/types'; import { addPermittedEthChainId, getPermittedEthChainIds, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 6ec08ecd5c..5913a1f61c 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,12 +3,12 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import type { ScopesObject, ScopeString } from '../scope/scope'; +import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownNotifications, KnownRpcMethods, parseScopeString, -} from '../scope/scope'; +} from '../scope/types'; import { mergeScopes } from '../scope/transform'; export const getPermittedEthChainIds = ( diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 2dd7b97739..b3ad6e8a8b 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -26,7 +26,7 @@ import type { ExternalScopeString, ScopeObject, ScopesObject, -} from './scope/scope'; +} from './scope/types'; export type Caip25CaveatValue = { requiredScopes: ScopesObject; diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index be5295b4ff..8df6526260 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -6,7 +6,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopesObject } from '../scope/scope'; +import type { ScopesObject } from '../scope/types'; import { mergeScopes } from '../scope/transform'; /** diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index eea6d6a2b1..8faaef066c 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -12,8 +12,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopeString } from '../scope/scope'; -import { parseScopeString } from '../scope/scope'; +import type { ScopeString } from '../scope/types'; +import { parseScopeString } from '../scope/types'; import { mergeScopes } from '../scope/transform'; /** diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index bc062d3694..ffb2b2b8b7 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -1,9 +1,49 @@ -import greeter from '.'; +import * as allExports from '.'; -describe('Test', () => { - it('greets', () => { - const name = 'Huey'; - const result = greeter(name); - expect(result).toBe('Hello, Huey!'); +describe('@metamask/multichain', () => { + it('has expected JavaScript exports', () => { + expect(Object.keys(allExports)).toMatchInlineSnapshot(` + Array [ + "getEthAccounts", + "setEthAccounts", + "caipPermissionAdapterMiddleware", + "getPermittedEthChainIds", + "addPermittedEthChainId", + "setPermittedEthChainIds", + "walletGetSessionHandler", + "walletInvokeMethodHandler", + "walletRevokeSessionHandler", + "multichainMethodCallValidatorMiddleware", + "MultichainMiddlewareManager", + "MultichainSubscriptionManager", + "Caip25CaveatType", + "Caip25CaveatFactoryFn", + "Caip25EndowmentPermissionName", + "caip25EndowmentBuilder", + "Caip25CaveatMutatorFactories", + "removeScope", + "assertScopeSupported", + "assertScopesSupported", + "validateAndFlattenScopes", + "bucketScopes", + "bucketScopesBySupport", + "filterScopesSupported", + "isSupportedScopeString", + "isSupportedAccount", + "isSupportedMethod", + "isSupportedNotification", + "flattenScope", + "mergeScopeObject", + "mergeScopes", + "flattenMergeScopes", + "isValidScope", + "validateScopes", + "KnownWalletRpcMethods", + "KnownRpcMethods", + "KnownWalletNamespaceRpcMethods", + "KnownNotifications", + "parseScopeString", + ] + `); }); }); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 6972c11729..e9caf04f5a 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -1,9 +1,21 @@ -/** - * Example function that returns a greeting for the given name. - * - * @param name - The name to greet. - * @returns The greeting. - */ -export default function greeter(name: string): string { - return `Hello, ${name}!`; -} +export { getEthAccounts, setEthAccounts } from './adapters/caip-permission-adapter-eth-accounts' +export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware' +export { getPermittedEthChainIds, addPermittedEthChainId, setPermittedEthChainIds} from './adapters/caip-permission-adapter-permittedChains' + +export { walletGetSessionHandler } from './handlers/wallet-getSession' +export { walletInvokeMethodHandler } from './handlers/wallet-invokeMethod' +export { walletRevokeSessionHandler } from './handlers/wallet-revokeSession' + +export { multichainMethodCallValidatorMiddleware } from './middlewares/multichainMethodCallValidator' +export { MultichainMiddlewareManager } from './middlewares/MultichainMiddlewareManager' +export { MultichainSubscriptionManager } from './middlewares/MultichainSubscriptionManager' + +export {assertScopeSupported, assertScopesSupported} from './scope/assert' +export { Caip25Authorization, validateAndFlattenScopes, bucketScopes } from './scope/authorization' +export {bucketScopesBySupport, filterScopesSupported} from './scope/filter' +export * from './scope/types' +export {isSupportedScopeString, isSupportedAccount, isSupportedMethod, isSupportedNotification} from './scope/supported' +export {flattenScope, mergeScopeObject, mergeScopes, flattenMergeScopes } from './scope/transform' +export {isValidScope, validateScopes} from './scope/validation' + +export { Caip25CaveatValue, Caip25CaveatType, Caip25CaveatFactoryFn, Caip25EndowmentPermissionName, caip25EndowmentBuilder, Caip25CaveatMutatorFactories, removeScope } from './caip25Permission' diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts index c609752994..4a358896fc 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts @@ -1,5 +1,5 @@ import type { ExtendedJsonRpcMiddleware } from './MultichainMiddlewareManager'; -import MultichainMiddlewareManager from './MultichainMiddlewareManager'; +import { MultichainMiddlewareManager } from './MultichainMiddlewareManager'; const scope = 'eip155:1'; const origin = 'example.com'; diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts index 336b33978a..d1e52f9385 100644 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -8,7 +8,7 @@ import type { PendingJsonRpcResponse, } from '@metamask/utils'; -import type { ExternalScopeString } from '../scope/scope'; +import type { ExternalScopeString } from '../scope/types'; export type ExtendedJsonRpcMiddleware = { ( @@ -29,7 +29,7 @@ type MiddlewareEntry = MiddlewareKey & { middleware: ExtendedJsonRpcMiddleware; }; -export default class MultichainMiddlewareManager { +export class MultichainMiddlewareManager { #middlewares: MiddlewareEntry[] = []; #getMiddlewareEntry({ diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts index c951b711af..cf05aa5c5a 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -1,6 +1,6 @@ import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; -import MultichainSubscriptionManager from './MultichainSubscriptionManager'; +import { MultichainSubscriptionManager } from './MultichainSubscriptionManager'; jest.mock('@metamask/eth-json-rpc-filters/subscriptionManager', () => jest.fn(), diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 66f3ae550a..0fed86cfd9 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -5,7 +5,7 @@ import type { CaipChainId, Hex } from '@metamask/utils'; import { parseCaipChainId } from '@metamask/utils'; import type EventEmitter from 'events'; -import type { ExternalScopeString } from '../scope/scope'; +import type { ExternalScopeString } from '../scope/types'; export type SubscriptionManager = { events: EventEmitter; @@ -38,7 +38,7 @@ type MultichainSubscriptionManagerOptions = { getNetworkClientById: NetworkController['getNetworkClientById']; }; -export default class MultichainSubscriptionManager extends SafeEventEmitter { +export class MultichainSubscriptionManager extends SafeEventEmitter { #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; #getNetworkClientById: NetworkController['getNetworkClientById']; diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index a57ac7835a..d62b2328eb 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -46,7 +46,7 @@ const dereffedPromise = dereferenceDocument( MultiChainOpenRPCDocument as unknown as OpenrpcDocument, makeCustomResolver({}), ); -export const multichainMethodCallValidator = async ( +const multichainMethodCallValidator = async ( method: string, params: JsonRpcParams | undefined, ) => { diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 75485352ba..2f8d934127 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,7 +1,7 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import { assertScopeSupported, assertScopesSupported } from './assert'; -import type { ScopeObject } from './scope'; +import type { ScopeObject } from './types'; import * as Supported from './supported'; jest.mock('./supported', () => ({ diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index ea436fb909..9d7b090665 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,7 +1,7 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; -import type { ScopeObject, ScopesObject } from './scope'; +import type { ScopeObject, ScopesObject } from './types'; import { isSupportedMethod, isSupportedNotification, diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 318718133d..6b5b9cfc14 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,6 +1,6 @@ import { bucketScopes, validateAndFlattenScopes } from './authorization'; import * as Filter from './filter'; -import type { ExternalScopeObject } from './scope'; +import type { ExternalScopeObject } from './types'; import * as Transform from './transform'; import * as Validation from './validation'; diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index 3b4f5e0619..b44e7f5544 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,7 +1,7 @@ import type { Hex } from '@metamask/utils'; import { bucketScopesBySupport } from './filter'; -import type { ExternalScopesObject, ScopesObject } from './scope'; +import type { ExternalScopesObject, ScopesObject } from './types'; import { flattenMergeScopes } from './transform'; import { validateScopes } from './validation'; diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts index ab5e889af1..58157bd602 100644 --- a/packages/multichain/src/scope/filter.ts +++ b/packages/multichain/src/scope/filter.ts @@ -1,7 +1,7 @@ import type { CaipChainId, Hex } from '@metamask/utils'; import { assertScopeSupported } from './assert'; -import type { ScopesObject } from './scope'; +import type { ScopesObject } from './types'; export const bucketScopesBySupport = ( scopes: ScopesObject, diff --git a/packages/multichain/src/scope/scope.test.ts b/packages/multichain/src/scope/scope.test.ts index d3a58b3221..1b6149b3f2 100644 --- a/packages/multichain/src/scope/scope.test.ts +++ b/packages/multichain/src/scope/scope.test.ts @@ -1,4 +1,4 @@ -import { parseScopeString } from './scope'; +import { parseScopeString } from './types'; describe('Scope', () => { describe('parseScopeString', () => { diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index b8146ebd15..bba7e40f20 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -3,7 +3,7 @@ import { KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, -} from './scope'; +} from './types'; import { isSupportedMethod, isSupportedNotification, diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 80ceb961f4..364e2cd249 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -8,14 +8,14 @@ import { parseCaipChainId, } from '@metamask/utils'; -import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './scope'; +import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './types'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, parseScopeString, -} from './scope'; +} from './types'; // TODO Maybe this gets DRY'ed into utils?.. It's used in TokenDetectionController too /** diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index d092735eb6..e57e85f565 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -1,4 +1,4 @@ -import type { ExternalScopeObject } from './scope'; +import type { ExternalScopeObject } from './types'; import { flattenScope, mergeScopes, diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 097ad725d8..a5bb2e9070 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -7,8 +7,8 @@ import type { ScopeString, ScopeObject, ScopesObject, -} from './scope'; -import { parseScopeString } from './scope'; +} from './types'; +import { parseScopeString } from './types'; // TODO: DRY THIS /** diff --git a/packages/multichain/src/scope/scope.ts b/packages/multichain/src/scope/types.ts similarity index 100% rename from packages/multichain/src/scope/scope.ts rename to packages/multichain/src/scope/types.ts diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index f4f4ae63e3..e8cfb96280 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -1,4 +1,4 @@ -import type { ExternalScopeObject } from './scope'; +import type { ExternalScopeObject } from './types'; import { isValidScope, validateScopes } from './validation'; const validScopeString = 'eip155:1'; diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index 69bc3e1bb9..3dbb7cfa25 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -4,8 +4,8 @@ import type { ExternalScopeString, ExternalScopeObject, ExternalScopesObject, -} from './scope'; -import { parseScopeString } from './scope'; +} from './types'; +import { parseScopeString } from './types'; export const isValidScope = ( scopeString: ExternalScopeString, From a13b9c75d85294f117965d7d36c1cc5cdf3181e8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 09:38:42 -0700 Subject: [PATCH 14/80] remove subjectTypes from CAIP-25 permission --- packages/multichain/src/caip25Permission.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index b3ad6e8a8b..cbf6bf3932 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -9,7 +9,6 @@ import type { import { CaveatMutatorOperation, PermissionType, - SubjectType, } from '@metamask/permission-controller'; import type { CaipAccountId, Json } from '@metamask/utils'; import { @@ -77,7 +76,6 @@ const specificationBuilder: PermissionSpecificationBuilder< targetName: Caip25EndowmentPermissionName, allowedCaveats: [Caip25CaveatType], endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, - subjectTypes: [SubjectType.Website], validator: (permission: PermissionConstraint) => { const caip25Caveat = permission.caveats?.[0]; if ( From 81db87755a680def5847ace9fd07cbc80dfb0528 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 09:52:51 -0700 Subject: [PATCH 15/80] update eth accounts adapter with empty wallet and wallet:eip155 ScopeObjects --- ...ip-permission-adapter-eth-accounts.test.ts | 34 +++++++++++++++++++ .../caip-permission-adapter-eth-accounts.ts | 12 ++++++- 2 files changed, 45 insertions(+), 1 deletion(-) 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 index 9434fab81d..76fe6d2880 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -177,6 +177,40 @@ describe('CAIP-25 eth_accounts adapters', () => { }); }); + it('returns a CAIP-25 caveat value with "wallet" and "wallet:eip155" scopes with CAIP-10 account addresses formed from the accounts param when the "wallet" or "wallet:eip155" are not defined in optional scopes', () => { + const input: Caip25CaveatValue = { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: false, + }; + + const result = setEthAccounts(input, ['0x1', '0x2', '0x3']); + expect(result).toStrictEqual({ + requiredScopes: {}, + optionalScopes: { + wallet: { + methods: [], + notifications: [], + accounts: [ + 'wallet:eip155:0x1', + 'wallet:eip155:0x2', + 'wallet:eip155:0x3', + ], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + accounts: [ + 'wallet:eip155:0x1', + 'wallet:eip155:0x2', + 'wallet:eip155:0x3', + ], + }, + }, + isMultichainOrigin: false, + }); + }); + it('does not modify the input CAIP-25 caveat value object in place', () => { const input: Caip25CaveatValue = { requiredScopes: { diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index db459460cc..569e3e9380 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -88,7 +88,17 @@ export const setEthAccounts = ( accounts, ), optionalScopes: setEthAccountsForScopesObject( - caip25CaveatValue.optionalScopes, + { + wallet: { + methods: [], + notifications: [], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, + ...caip25CaveatValue.optionalScopes, + }, accounts, ), }; From a3fc2639dc767d8447195efa3d62f06dbfd397af Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 10:37:04 -0700 Subject: [PATCH 16/80] fix caip25permission spec --- packages/multichain/src/caip25Permission.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index eba34e350c..af1b92745c 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -3,7 +3,6 @@ import type { CaveatConstraint } from '@metamask/permission-controller'; import { CaveatMutatorOperation, PermissionType, - SubjectType, } from '@metamask/permission-controller'; import type { Caip25CaveatValue } from './caip25Permission'; @@ -52,7 +51,6 @@ describe('endowment:caip25', () => { targetName: Caip25EndowmentPermissionName, endowmentGetter: expect.any(Function), allowedCaveats: [Caip25CaveatType], - subjectTypes: [SubjectType.Website], validator: expect.any(Function), }); From afe5cb9307792f20a9a8668a91ffc43f6d7ada3a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 10:37:21 -0700 Subject: [PATCH 17/80] upsert empty wallet:eip155 when setting permittedChains --- ...permission-adapter-permittedChains.test.ts | 62 +++++++++++++++++++ ...caip-permission-adapter-permittedChains.ts | 4 ++ 2 files changed, 66 insertions(+) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 127a15cb40..2ca86004f3 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -69,6 +69,10 @@ describe('CAIP-25 permittedChains adapters', () => { notifications: [], accounts: ['eip155:100:0x100'], }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, }, isMultichainOrigin: false, }, @@ -94,6 +98,60 @@ describe('CAIP-25 permittedChains adapters', () => { notifications: KnownNotifications.eip155, accounts: [], }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, + }, + isMultichainOrigin: false, + }); + }); + + it('adds an optional scope for "wallet:eip155" if it does not already exist in the optional scopes', () => { + const result = addPermittedEthChainId( + { + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + }, + isMultichainOrigin: false, + }, + '0x65', + ); + + expect(result).toStrictEqual({ + requiredScopes: { + 'eip155:1': { + methods: [], + notifications: [], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + }, + }, + optionalScopes: { + 'eip155:100': { + methods: [], + notifications: [], + accounts: ['eip155:100:0x100'], + }, + 'eip155:101': { + methods: KnownRpcMethods.eip155, + notifications: KnownNotifications.eip155, + accounts: [], + }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, }, isMultichainOrigin: false, }); @@ -277,6 +335,10 @@ describe('CAIP-25 permittedChains adapters', () => { notifications: KnownNotifications.eip155, accounts: [], }, + 'wallet:eip155': { + methods: [], + notifications: [], + }, }, isMultichainOrigin: false, }); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 5913a1f61c..7a0ad8196f 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -45,6 +45,10 @@ export const addPermittedEthChainId = ( return { ...caip25CaveatValue, optionalScopes: { + 'wallet:eip155': { + methods: [], + notifications: [], + }, ...caip25CaveatValue.optionalScopes, [scopeString]: { methods: KnownRpcMethods.eip155, From 9ea78bee99e6d4eaf0a8778632599f0135559960 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 10:47:29 -0700 Subject: [PATCH 18/80] lint --- .../caip-permission-adapter-eth-accounts.ts | 2 +- .../caip-permission-adapter-middleware.ts | 2 +- ...caip-permission-adapter-permittedChains.ts | 2 +- .../src/handlers/wallet-getSession.ts | 2 +- .../src/handlers/wallet-invokeMethod.ts | 2 +- packages/multichain/src/index.ts | 60 +++++++++++++------ packages/multichain/src/scope/assert.test.ts | 2 +- packages/multichain/src/scope/assert.ts | 2 +- .../src/scope/authorization.test.ts | 2 +- .../multichain/src/scope/authorization.ts | 2 +- .../multichain/src/scope/supported.test.ts | 10 ++-- .../multichain/src/scope/transform.test.ts | 2 +- 12 files changed, 58 insertions(+), 32 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 569e3e9380..5646bab107 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -6,9 +6,9 @@ import { } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { parseScopeString } from '../scope/types'; -import { mergeScopes } from '../scope/transform'; const isEip155ScopeString = (scopeString: ScopeString) => { const { namespace, reference } = parseScopeString(scopeString); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 1ce16cee3b..6c8378b40d 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -8,8 +8,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopeString } from '../scope/types'; import { mergeScopes } from '../scope/transform'; +import type { ScopeString } from '../scope/types'; /** * Middleware to handle CAIP-25 permission requests. diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 7a0ad8196f..c0042dbfe9 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,13 +3,13 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownNotifications, KnownRpcMethods, parseScopeString, } from '../scope/types'; -import { mergeScopes } from '../scope/transform'; export const getPermittedEthChainIds = ( caip25CaveatValue: Caip25CaveatValue, diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index 8df6526260..13c9e4f017 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -6,8 +6,8 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import type { ScopesObject } from '../scope/types'; import { mergeScopes } from '../scope/transform'; +import type { ScopesObject } from '../scope/types'; /** * Handler for the `wallet_getSession` RPC method. diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index 8faaef066c..239d65195b 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -12,9 +12,9 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; import type { ScopeString } from '../scope/types'; import { parseScopeString } from '../scope/types'; -import { mergeScopes } from '../scope/transform'; /** * Handler for the `wallet_invokeMethod` RPC method. diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index e9caf04f5a..55a4cd3822 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -1,21 +1,47 @@ -export { getEthAccounts, setEthAccounts } from './adapters/caip-permission-adapter-eth-accounts' -export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware' -export { getPermittedEthChainIds, addPermittedEthChainId, setPermittedEthChainIds} from './adapters/caip-permission-adapter-permittedChains' +export { + getEthAccounts, + setEthAccounts, +} from './adapters/caip-permission-adapter-eth-accounts'; +export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware'; +export { + getPermittedEthChainIds, + addPermittedEthChainId, + setPermittedEthChainIds, +} from './adapters/caip-permission-adapter-permittedChains'; -export { walletGetSessionHandler } from './handlers/wallet-getSession' -export { walletInvokeMethodHandler } from './handlers/wallet-invokeMethod' -export { walletRevokeSessionHandler } from './handlers/wallet-revokeSession' +export { walletGetSessionHandler } from './handlers/wallet-getSession'; +export { walletInvokeMethodHandler } from './handlers/wallet-invokeMethod'; +export { walletRevokeSessionHandler } from './handlers/wallet-revokeSession'; -export { multichainMethodCallValidatorMiddleware } from './middlewares/multichainMethodCallValidator' -export { MultichainMiddlewareManager } from './middlewares/MultichainMiddlewareManager' -export { MultichainSubscriptionManager } from './middlewares/MultichainSubscriptionManager' +export { multichainMethodCallValidatorMiddleware } from './middlewares/multichainMethodCallValidator'; +export { MultichainMiddlewareManager } from './middlewares/MultichainMiddlewareManager'; +export { MultichainSubscriptionManager } from './middlewares/MultichainSubscriptionManager'; -export {assertScopeSupported, assertScopesSupported} from './scope/assert' -export { Caip25Authorization, validateAndFlattenScopes, bucketScopes } from './scope/authorization' -export {bucketScopesBySupport, filterScopesSupported} from './scope/filter' -export * from './scope/types' -export {isSupportedScopeString, isSupportedAccount, isSupportedMethod, isSupportedNotification} from './scope/supported' -export {flattenScope, mergeScopeObject, mergeScopes, flattenMergeScopes } from './scope/transform' -export {isValidScope, validateScopes} from './scope/validation' +export { assertScopeSupported, assertScopesSupported } from './scope/assert'; +export type { Caip25Authorization } from './scope/authorization'; +export { validateAndFlattenScopes, bucketScopes } from './scope/authorization'; +export { bucketScopesBySupport, filterScopesSupported } from './scope/filter'; +export * from './scope/types'; +export { + isSupportedScopeString, + isSupportedAccount, + isSupportedMethod, + isSupportedNotification, +} from './scope/supported'; +export { + flattenScope, + mergeScopeObject, + mergeScopes, + flattenMergeScopes, +} from './scope/transform'; +export { isValidScope, validateScopes } from './scope/validation'; -export { Caip25CaveatValue, Caip25CaveatType, Caip25CaveatFactoryFn, Caip25EndowmentPermissionName, caip25EndowmentBuilder, Caip25CaveatMutatorFactories, removeScope } from './caip25Permission' +export type { Caip25CaveatValue } from './caip25Permission'; +export { + Caip25CaveatType, + Caip25CaveatFactoryFn, + Caip25EndowmentPermissionName, + caip25EndowmentBuilder, + Caip25CaveatMutatorFactories, + removeScope, +} from './caip25Permission'; diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 2f8d934127..92e936a741 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,8 +1,8 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import { assertScopeSupported, assertScopesSupported } from './assert'; -import type { ScopeObject } from './types'; import * as Supported from './supported'; +import type { ScopeObject } from './types'; jest.mock('./supported', () => ({ isSupportedScopeString: jest.fn(), diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 9d7b090665..77a9dd6205 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,12 +1,12 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; -import type { ScopeObject, ScopesObject } from './types'; import { isSupportedMethod, isSupportedNotification, isSupportedScopeString, } from './supported'; +import type { ScopeObject, ScopesObject } from './types'; export const assertScopeSupported = ( scopeString: string, diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 6b5b9cfc14..44e6323f38 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,7 +1,7 @@ import { bucketScopes, validateAndFlattenScopes } from './authorization'; import * as Filter from './filter'; -import type { ExternalScopeObject } from './types'; import * as Transform from './transform'; +import type { ExternalScopeObject } from './types'; import * as Validation from './validation'; jest.mock('./validation', () => ({ diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index b44e7f5544..9b377a8b13 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,8 +1,8 @@ import type { Hex } from '@metamask/utils'; import { bucketScopesBySupport } from './filter'; -import type { ExternalScopesObject, ScopesObject } from './types'; import { flattenMergeScopes } from './transform'; +import type { ExternalScopesObject, ScopesObject } from './types'; import { validateScopes } from './validation'; export type Caip25Authorization = diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index bba7e40f20..72faf0e50d 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -1,14 +1,14 @@ +import { + isSupportedMethod, + isSupportedNotification, + isSupportedScopeString, +} from './supported'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, } from './types'; -import { - isSupportedMethod, - isSupportedNotification, - isSupportedScopeString, -} from './supported'; describe('Scope Support', () => { describe('isSupportedNotification', () => { diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index e57e85f565..acfc694e7d 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -1,10 +1,10 @@ -import type { ExternalScopeObject } from './types'; import { flattenScope, mergeScopes, mergeScopeObject, flattenMergeScopes, } from './transform'; +import type { ExternalScopeObject } from './types'; const validScopeObject: ExternalScopeObject = { methods: [], From 5650d31fca65e609d778e4ac5fa8eb97e1787913 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 14 Oct 2024 10:52:53 -0700 Subject: [PATCH 19/80] Rename scope.test.ts to types.test.ts --- packages/multichain/src/scope/{scope.test.ts => types.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/multichain/src/scope/{scope.test.ts => types.test.ts} (100%) diff --git a/packages/multichain/src/scope/scope.test.ts b/packages/multichain/src/scope/types.test.ts similarity index 100% rename from packages/multichain/src/scope/scope.test.ts rename to packages/multichain/src/scope/types.test.ts From df5167ebef9fffb60b8caf57544346554edc7ae5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 08:17:01 -0700 Subject: [PATCH 20/80] add networkClientId type to permission-adapter-middleware --- .../src/adapters/caip-permission-adapter-middleware.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 6c8378b40d..811978697f 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -1,4 +1,7 @@ -import type { NetworkConfiguration } from '@metamask/network-controller'; +import type { + NetworkConfiguration, + NetworkClientId, +} from '@metamask/network-controller'; import type { Caveat } from '@metamask/permission-controller'; import { providerErrors } from '@metamask/rpc-errors'; import type { JsonRpcRequest } from '@metamask/utils'; @@ -24,7 +27,7 @@ import type { ScopeString } from '../scope/types'; */ export async function caipPermissionAdapterMiddleware( request: JsonRpcRequest & { - networkClientId: string; + networkClientId: NetworkClientId; origin: string; }, _response: unknown, @@ -35,7 +38,7 @@ export async function caipPermissionAdapterMiddleware( ...args: unknown[] ) => Caveat; getNetworkConfigurationByNetworkClientId: ( - networkClientId: string, + networkClientId: NetworkClientId, ) => NetworkConfiguration; }, ) { From 9446b0b12714b3bfbdf0d6802ad21fb1d9450785 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 08:17:07 -0700 Subject: [PATCH 21/80] fix snapshot --- packages/multichain/src/index.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index ffb2b2b8b7..4bae121c2d 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -16,12 +16,6 @@ describe('@metamask/multichain', () => { "multichainMethodCallValidatorMiddleware", "MultichainMiddlewareManager", "MultichainSubscriptionManager", - "Caip25CaveatType", - "Caip25CaveatFactoryFn", - "Caip25EndowmentPermissionName", - "caip25EndowmentBuilder", - "Caip25CaveatMutatorFactories", - "removeScope", "assertScopeSupported", "assertScopesSupported", "validateAndFlattenScopes", @@ -38,6 +32,12 @@ describe('@metamask/multichain', () => { "flattenMergeScopes", "isValidScope", "validateScopes", + "Caip25CaveatType", + "Caip25CaveatFactoryFn", + "Caip25EndowmentPermissionName", + "caip25EndowmentBuilder", + "Caip25CaveatMutatorFactories", + "removeScope", "KnownWalletRpcMethods", "KnownRpcMethods", "KnownWalletNamespaceRpcMethods", From 941849da0e93d4be7da0e771ffa464a957dba306 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 08:31:08 -0700 Subject: [PATCH 22/80] add KnownWalletScopeString enum --- packages/multichain/jest.config.js | 2 +- .../caip-permission-adapter-eth-accounts.ts | 17 ++++++----------- .../caip-permission-adapter-middleware.ts | 4 ++-- .../caip-permission-adapter-permittedChains.ts | 5 +++-- packages/multichain/src/index.test.ts | 1 + packages/multichain/src/scope/types.ts | 4 ++++ 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js index 2f651a645e..0413429bc3 100644 --- a/packages/multichain/jest.config.js +++ b/packages/multichain/jest.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 83.05, + branches: 82.95, functions: 87.37, lines: 86.65, statements: 87.09, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 5646bab107..375e483c5e 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -8,15 +8,14 @@ import { import type { Caip25CaveatValue } from '../caip25Permission'; import { mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; -import { parseScopeString } from '../scope/types'; +import { KnownWalletScopeString, parseScopeString } from '../scope/types'; const isEip155ScopeString = (scopeString: ScopeString) => { - const { namespace, reference } = parseScopeString(scopeString); + const { namespace } = parseScopeString(scopeString); return ( namespace === KnownCaipNamespace.Eip155 || - (namespace === KnownCaipNamespace.Wallet && - reference === KnownCaipNamespace.Eip155) + scopeString === KnownWalletScopeString.Eip155 ); }; @@ -47,10 +46,7 @@ const setEthAccountsForScopesObject = ( const updatedScopesObject: ScopesObject = {}; Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { - const { namespace, reference } = parseScopeString(scopeString); - - const isWalletNamespace = - namespace === KnownCaipNamespace.Wallet && reference === undefined; + const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet if ( !isEip155ScopeString(scopeString as ScopeString) && @@ -63,7 +59,7 @@ const setEthAccountsForScopesObject = ( const caipAccounts = accounts.map( (account) => (isWalletNamespace - ? `wallet:eip155:${account}` + ? `${KnownWalletScopeString.Eip155}:${account}` : `${scopeString}:${account}`) as CaipAccountId, ); @@ -76,7 +72,6 @@ const setEthAccountsForScopesObject = ( return updatedScopesObject; }; -// This helper must be called with existing eip155 scopes export const setEthAccounts = ( caip25CaveatValue: Caip25CaveatValue, accounts: Hex[], @@ -93,7 +88,7 @@ export const setEthAccounts = ( methods: [], notifications: [], }, - 'wallet:eip155': { + [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], }, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 811978697f..865a4cccfc 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -12,7 +12,7 @@ import { Caip25EndowmentPermissionName, } from '../caip25Permission'; import { mergeScopes } from '../scope/transform'; -import type { ScopeString } from '../scope/types'; +import { KnownWalletScopeString, type ScopeString } from '../scope/types'; /** * Middleware to handle CAIP-25 permission requests. @@ -70,7 +70,7 @@ export async function caipPermissionAdapterMiddleware( if ( !scopesObject[scope]?.methods?.includes(method) && - !scopesObject['wallet:eip155']?.methods?.includes(method) && + !scopesObject[KnownWalletScopeString.Eip155]?.methods?.includes(method) && !scopesObject.wallet?.methods?.includes(method) ) { return end(providerErrors.unauthorized()); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index c0042dbfe9..74e6b5a555 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,13 +3,14 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownNotifications, KnownRpcMethods, + KnownWalletScopeString, parseScopeString, } from '../scope/types'; +import { mergeScopes } from '../scope/transform'; export const getPermittedEthChainIds = ( caip25CaveatValue: Caip25CaveatValue, @@ -45,7 +46,7 @@ export const addPermittedEthChainId = ( return { ...caip25CaveatValue, optionalScopes: { - 'wallet:eip155': { + [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], }, diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 4bae121c2d..9afd6a5722 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -38,6 +38,7 @@ describe('@metamask/multichain', () => { "caip25EndowmentBuilder", "Caip25CaveatMutatorFactories", "removeScope", + "KnownWalletScopeString", "KnownWalletRpcMethods", "KnownRpcMethods", "KnownWalletNamespaceRpcMethods", diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index c899fa8a40..16633b63e7 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -12,6 +12,10 @@ import { parseCaipChainId, } from '@metamask/utils'; +export enum KnownWalletScopeString { + Eip155 = "wallet:eip155", +} + export type NonWalletKnownCaipNamespace = Extract< KnownCaipNamespace, KnownCaipNamespace.Eip155 From a250de21f9e0fdb55f0b4a3bcc90298628d73979 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 09:32:37 -0700 Subject: [PATCH 23/80] stop upserting wallet scope in setEthAccounts() --- ...ip-permission-adapter-eth-accounts.test.ts | 30 +------------------ .../caip-permission-adapter-eth-accounts.ts | 6 +--- 2 files changed, 2 insertions(+), 34 deletions(-) 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 index 76fe6d2880..427034e46c 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -47,11 +47,6 @@ describe('CAIP-25 eth_accounts adapters', () => { notifications: [], accounts: ['wallet:eip155:0x5'], }, - wallet: { - methods: [], - notifications: [], - accounts: ['wallet:eip155:0x6'], - }, }, isMultichainOrigin: false, }); @@ -63,7 +58,6 @@ describe('CAIP-25 eth_accounts adapters', () => { '0x3', '0x100', '0x5', - '0x6', ]); }); }); @@ -109,10 +103,6 @@ describe('CAIP-25 eth_accounts adapters', () => { methods: [], notifications: [], }, - wallet: { - methods: [], - notifications: [], - }, }, isMultichainOrigin: false, }; @@ -163,21 +153,12 @@ describe('CAIP-25 eth_accounts adapters', () => { 'wallet:eip155:0x3', ], }, - wallet: { - methods: [], - notifications: [], - accounts: [ - 'wallet:eip155:0x1', - 'wallet:eip155:0x2', - 'wallet:eip155:0x3', - ], - }, }, isMultichainOrigin: false, }); }); - it('returns a CAIP-25 caveat value with "wallet" and "wallet:eip155" scopes with CAIP-10 account addresses formed from the accounts param when the "wallet" or "wallet:eip155" are not defined in optional scopes', () => { + it('returns a CAIP-25 caveat value with upserted "wallet:eip155" optional scope with CAIP-10 account addresses formed from the accounts param', () => { const input: Caip25CaveatValue = { requiredScopes: {}, optionalScopes: {}, @@ -188,15 +169,6 @@ describe('CAIP-25 eth_accounts adapters', () => { expect(result).toStrictEqual({ requiredScopes: {}, optionalScopes: { - wallet: { - methods: [], - notifications: [], - accounts: [ - 'wallet:eip155:0x1', - 'wallet:eip155:0x2', - 'wallet:eip155:0x3', - ], - }, 'wallet:eip155': { methods: [], notifications: [], diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 375e483c5e..52e61aacc1 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -46,7 +46,7 @@ const setEthAccountsForScopesObject = ( const updatedScopesObject: ScopesObject = {}; Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { - const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet + const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet; if ( !isEip155ScopeString(scopeString as ScopeString) && @@ -84,10 +84,6 @@ export const setEthAccounts = ( ), optionalScopes: setEthAccountsForScopesObject( { - wallet: { - methods: [], - notifications: [], - }, [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], From aa7ba39a6e141a09bdec32a296ff3c96a68dba9b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 09:32:44 -0700 Subject: [PATCH 24/80] lint --- packages/multichain/jest.config.js | 2 +- .../src/adapters/caip-permission-adapter-permittedChains.ts | 4 ++-- packages/multichain/src/scope/types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js index 0413429bc3..1cadcfe8b2 100644 --- a/packages/multichain/jest.config.js +++ b/packages/multichain/jest.config.js @@ -17,7 +17,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 82.95, + branches: 82.38, functions: 87.37, lines: 86.65, statements: 87.09, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 74e6b5a555..b5d6cdd1f4 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,6 +3,7 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownNotifications, @@ -10,7 +11,6 @@ import { KnownWalletScopeString, parseScopeString, } from '../scope/types'; -import { mergeScopes } from '../scope/transform'; export const getPermittedEthChainIds = ( caip25CaveatValue: Caip25CaveatValue, @@ -46,7 +46,7 @@ export const addPermittedEthChainId = ( return { ...caip25CaveatValue, optionalScopes: { - [KnownWalletScopeString.Eip155]: { + [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], }, diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index 16633b63e7..d9c40b8652 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -13,7 +13,7 @@ import { } from '@metamask/utils'; export enum KnownWalletScopeString { - Eip155 = "wallet:eip155", + Eip155 = 'wallet:eip155', } export type NonWalletKnownCaipNamespace = Extract< From 8415be6ec267fcd4f8c9f9df77bb2adaf6f0a476 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 09:44:01 -0700 Subject: [PATCH 25/80] update removeScope mutator to not revoke permission if requiredScope is removed --- .../multichain/src/caip25Permission.test.ts | 23 ------------------- packages/multichain/src/caip25Permission.ts | 8 +------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index af1b92745c..417e342e80 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -90,29 +90,6 @@ describe('endowment:caip25', () => { }); }); - it('can revoke the entire permission when a requiredScope is removed', () => { - const ethereumGoerliCaveat = { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: ['chainChanged'], - }, - }, - optionalScopes: { - 'eip155:5': { - methods: ['eth_call'], - notifications: ['accountsChanged'], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope('eip155:1', ethereumGoerliCaveat); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.RevokePermission, - }); - }); - it('can noop when nothing is removed', () => { const ethereumGoerliCaveat = { requiredScopes: { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index cbf6bf3932..f92456f528 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -247,13 +247,7 @@ export function removeScope( newOptionalScopes.length !== Object.keys(caip25CaveatValue.optionalScopes).length; - if (requiredScopesRemoved) { - return { - operation: CaveatMutatorOperation.RevokePermission, - }; - } - - if (optionalScopesRemoved) { + if (requiredScopesRemoved || optionalScopesRemoved) { return { operation: CaveatMutatorOperation.UpdateValue, value: { From c7f510ea3bd3219ae6a3a517e00412139e479f62 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 10:00:07 -0700 Subject: [PATCH 26/80] Fix specificationBuilder jsdoc and typing --- .../multichain/src/caip25Permission.test.ts | 4 +++- packages/multichain/src/caip25Permission.ts | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 417e342e80..982a1434c9 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -222,7 +222,9 @@ describe('endowment:caip25', () => { describe('permission validator', () => { const findNetworkClientIdByChainId = jest.fn(); const { validator } = caip25EndowmentBuilder.specificationBuilder({ - findNetworkClientIdByChainId, + methodHooks: { + findNetworkClientIdByChainId, + }, }); it('throws an error if there is not exactly one caveat', () => { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index f92456f528..8345590f45 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -53,24 +53,25 @@ type Caip25EndowmentSpecification = ValidPermissionSpecification<{ allowedCaveats: Readonly> | null; }>; +type Caip25EndowmentSpecificationBuilderOptions = { + methodHooks: { + findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; + }; +}; + /** - * `endowment:caip25` returns nothing atm; + * Helper that returns a `endowment:caip25` specification that + * can be passed into the PermissionController constructor. * * @param builderOptions - The specification builder options. - * @param builderOptions.findNetworkClientIdByChainId - The hook to find the networkClientId for a chainId. + * @param builderOptions.methodHooks - The RPC method hooks needed by the method implementation. * @returns The specification for the `caip25` endowment. */ const specificationBuilder: PermissionSpecificationBuilder< PermissionType.Endowment, - // TODO: FIX THIS - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any, + Caip25EndowmentSpecificationBuilderOptions, Caip25EndowmentSpecification -> = ({ - findNetworkClientIdByChainId, -}: { - findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; -}) => { +> = ({ methodHooks }: Caip25EndowmentSpecificationBuilderOptions) => { return { permissionType: PermissionType.Endowment, targetName: Caip25EndowmentPermissionName, @@ -107,7 +108,7 @@ const specificationBuilder: PermissionSpecificationBuilder< const isChainIdSupported = (chainId: Hex) => { try { - findNetworkClientIdByChainId(chainId); + methodHooks.findNetworkClientIdByChainId(chainId); return true; } catch (err) { return false; From 4afa8f81bd022e0c7282c6bc88c13f66edab4899 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 10:02:47 -0700 Subject: [PATCH 27/80] Fix caip25permission type --- packages/multichain/src/caip25Permission.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 8345590f45..0e5e163668 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -88,9 +88,8 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - // TODO: FIX THIS TYPE const { requiredScopes, optionalScopes, isMultichainOrigin } = ( - caip25Caveat as unknown as { value: Caip25CaveatValue } + caip25Caveat.value as { value: Caip25CaveatValue } ).value; if ( From 597e837720641ec9d567622e2a61343aaa762b55 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 10:15:20 -0700 Subject: [PATCH 28/80] Fix caip25permission type --- packages/multichain/src/caip25Permission.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 0e5e163668..dc1b900092 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -88,9 +88,8 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - const { requiredScopes, optionalScopes, isMultichainOrigin } = ( - caip25Caveat.value as { value: Caip25CaveatValue } - ).value; + const { requiredScopes, optionalScopes, isMultichainOrigin } = + caip25Caveat.value as Caip25CaveatValue; if ( !requiredScopes || From 187218863d0f4c1bca9eab79f7b085a3f443cd76 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 13:32:21 -0500 Subject: [PATCH 29/80] small dry todo fix --- .../src/TokenDetectionController.ts | 26 ++++--------------- packages/controller-utils/src/index.ts | 1 + packages/controller-utils/src/util.test.ts | 26 +++++++++++++++++++ packages/controller-utils/src/util.ts | 17 ++++++++++++ packages/multichain/src/scope/supported.ts | 17 +----------- 5 files changed, 50 insertions(+), 37 deletions(-) diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 2459baea38..76400bc7cc 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -9,7 +9,11 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import contractMap from '@metamask/contract-metadata'; -import { ChainId, safelyExecute } from '@metamask/controller-utils'; +import { + ChainId, + safelyExecute, + isEqualCaseInsensitive, +} from '@metamask/controller-utils'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -44,26 +48,6 @@ import type { const DEFAULT_INTERVAL = 180000; -/** - * Compare 2 given strings and return boolean - * eg: "foo" and "FOO" => true - * eg: "foo" and "bar" => false - * eg: "foo" and 123 => false - * - * @param value1 - first string to compare - * @param value2 - first string to compare - * @returns true if 2 strings are identical when they are lowercase - */ -export function isEqualCaseInsensitive( - value1: string, - value2: string, -): boolean { - if (typeof value1 !== 'string' || typeof value2 !== 'string') { - return false; - } - return value1.toLowerCase() === value2.toLowerCase(); -} - type LegacyToken = { name: string; logo: `${string}.svg`; diff --git a/packages/controller-utils/src/index.ts b/packages/controller-utils/src/index.ts index 265872e620..3d35d62c0a 100644 --- a/packages/controller-utils/src/index.ts +++ b/packages/controller-utils/src/index.ts @@ -27,6 +27,7 @@ export { toChecksumHexAddress, toHex, weiHexToGweiDec, + isEqualCaseInsensitive, } from './util'; export * from './types'; export * from './siwe'; diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts index 71dd33e90d..3126fb7ef1 100644 --- a/packages/controller-utils/src/util.test.ts +++ b/packages/controller-utils/src/util.test.ts @@ -611,3 +611,29 @@ describe('util', () => { }); }); }); + +describe('isEqualCaseInsensitive', () => { + it('returns false for non-string values', () => { + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(null, 'test')).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive('test', null)).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(5, 'test')).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive('test', 5)).toBe(false); + }); + + it('returns false for strings that are not equal', () => { + expect(util.isEqualCaseInsensitive('test', 'test1')).toBe(false); + expect(util.isEqualCaseInsensitive('test1', 'test')).toBe(false); + }); + + it('returns true for strings that are equal', () => { + expect(util.isEqualCaseInsensitive('test', 'TEST')).toBe(true); + expect(util.isEqualCaseInsensitive('test', 'test')).toBe(true); + expect(util.isEqualCaseInsensitive('TEST', 'TEST')).toBe(true); + expect(util.isEqualCaseInsensitive('test', 'Test')).toBe(true); + expect(util.isEqualCaseInsensitive('Test', 'test')).toBe(true); + }); +}); diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts index 4d14f71e6f..4d53b069d9 100644 --- a/packages/controller-utils/src/util.ts +++ b/packages/controller-utils/src/util.ts @@ -619,3 +619,20 @@ function logOrRethrowError(error: unknown, codesToCatch: number[] = []) { throw error; } } + +/** + * Checks if two strings are equal, ignoring case. + * + * @param value1 - The first string to compare. + * @param value2 - The second string to compare. + * @returns `true` if the strings are equal, ignoring case; otherwise, `false`. + */ +export function isEqualCaseInsensitive( + value1: string, + value2: string, +): boolean { + if (typeof value1 !== 'string' || typeof value2 !== 'string') { + return false; + } + return value1.toLowerCase() === value2.toLowerCase(); +} diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 364e2cd249..52f351ea6c 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -1,4 +1,4 @@ -import { toHex } from '@metamask/controller-utils'; +import { toHex, isEqualCaseInsensitive } from '@metamask/controller-utils'; import type { CaipAccountId, Hex } from '@metamask/utils'; import { isCaipChainId, @@ -17,21 +17,6 @@ import { parseScopeString, } from './types'; -// TODO Maybe this gets DRY'ed into utils?.. It's used in TokenDetectionController too -/** - * Checks if two strings are equal, ignoring case. - * - * @param value1 - The first string to compare. - * @param value2 - The second string to compare. - * @returns `true` if the strings are equal, ignoring case; otherwise, `false`. - */ -function isEqualCaseInsensitive(value1: string, value2: string): boolean { - if (typeof value1 !== 'string' || typeof value2 !== 'string') { - return false; - } - return value1.toLowerCase() === value2.toLowerCase(); -} - export const isSupportedScopeString = ( scopeString: string, isChainIdSupported: (chainId: Hex) => boolean, From c5d80056f20a39d67b8ef6935440936ba57cae3b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 15 Oct 2024 13:37:13 -0500 Subject: [PATCH 30/80] another small dry --- .../adapters/caip-permission-adapter-eth-accounts.ts | 4 ++-- .../caip-permission-adapter-permittedChains.ts | 4 ++-- packages/multichain/src/scope/transform.ts | 11 +++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 52e61aacc1..e0ef4038bf 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -6,7 +6,7 @@ import { } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; +import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownWalletScopeString, parseScopeString } from '../scope/types'; @@ -36,7 +36,7 @@ export const getEthAccounts = (caip25CaveatValue: Caip25CaveatValue) => { }); }); - return Array.from(new Set(ethAccounts)); + return getUniqueArrayItems(ethAccounts); }; const setEthAccountsForScopesObject = ( diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index b5d6cdd1f4..25bd63a208 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,7 +3,7 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; +import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; import { KnownNotifications, @@ -28,7 +28,7 @@ export const getPermittedEthChainIds = ( } }); - return Array.from(new Set(ethChainIds)); + return getUniqueArrayItems(ethChainIds); }; export const addPermittedEthChainId = ( diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index a5bb2e9070..72d90bbb7a 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -10,14 +10,13 @@ import type { } from './types'; import { parseScopeString } from './types'; -// TODO: DRY THIS /** * Returns a list of unique items * * @param list - The list of items to filter * @returns A list of unique items */ -function unique(list: Value[]): Value[] { +export function getUniqueArrayItems(list: Value[]): Value[] { return Array.from(new Set(list)); } @@ -56,28 +55,28 @@ export const mergeScopeObject = ( ) => { const mergedScopeObject: ScopeObject = { methods: unique([...scopeObjectA.methods, ...scopeObjectB.methods]), - notifications: unique([ + notifications: getUniqueArrayItems([ ...scopeObjectA.notifications, ...scopeObjectB.notifications, ]), }; if (scopeObjectA.accounts || scopeObjectB.accounts) { - mergedScopeObject.accounts = unique([ + mergedScopeObject.accounts = getUniqueArrayItems([ ...(scopeObjectA.accounts ?? []), ...(scopeObjectB.accounts ?? []), ]); } if (scopeObjectA.rpcDocuments || scopeObjectB.rpcDocuments) { - mergedScopeObject.rpcDocuments = unique([ + mergedScopeObject.rpcDocuments = getUniqueArrayItems([ ...(scopeObjectA.rpcDocuments ?? []), ...(scopeObjectB.rpcDocuments ?? []), ]); } if (scopeObjectA.rpcEndpoints || scopeObjectB.rpcEndpoints) { - mergedScopeObject.rpcEndpoints = unique([ + mergedScopeObject.rpcEndpoints = getUniqueArrayItems([ ...(scopeObjectA.rpcEndpoints ?? []), ...(scopeObjectB.rpcEndpoints ?? []), ]); From 54f7c497e47d7770d19ca54f5a4cc88655c978a2 Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 15 Oct 2024 16:01:25 -0400 Subject: [PATCH 31/80] Added handler wrappers to better integrate with existing middleware (#4796) Add handler wrappers to better integrate with existing middleware in extension --------- Co-authored-by: Jiexi Luan --- .../src/handlers/wallet-getSession.test.ts | 4 ++-- .../src/handlers/wallet-getSession.ts | 10 +++++++++- .../src/handlers/wallet-invokeMethod.test.ts | 18 ++++++++++++------ .../src/handlers/wallet-invokeMethod.ts | 11 ++++++++++- .../src/handlers/wallet-revokeSession.test.ts | 4 ++-- .../src/handlers/wallet-revokeSession.ts | 9 ++++++++- packages/multichain/src/index.test.ts | 6 +++--- packages/multichain/src/index.ts | 6 +++--- packages/multichain/src/scope/transform.ts | 9 ++++++--- 9 files changed, 55 insertions(+), 22 deletions(-) diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts index ebee666967..68c6044119 100644 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -4,7 +4,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { walletGetSessionHandler } from './wallet-getSession'; +import { walletGetSession } from './wallet-getSession'; const baseRequest: JsonRpcRequest & { origin: string } = { origin: 'http://test.com', @@ -49,7 +49,7 @@ const createMockedHandler = () => { jsonrpc: '2.0' as const, }; const handler = (request: JsonRpcRequest & { origin: string }) => - walletGetSessionHandler(request, response, next, end, { + walletGetSession.implementation(request, response, next, end, { getCaveat, }); diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index 13c9e4f017..ad3e4e5569 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -19,7 +19,7 @@ import type { ScopesObject } from '../scope/types'; * @param hooks - The hooks object. * @param hooks.getCaveat - Function to retrieve a caveat. */ -export async function walletGetSessionHandler( +async function walletGetSessionHandler( request: JsonRpcRequest & { origin: string }, response: JsonRpcSuccess<{ sessionScopes: ScopesObject }>, _next: () => void, @@ -56,3 +56,11 @@ export async function walletGetSessionHandler( }; return end(); } + +export const walletGetSession = { + methodNames: ['wallet_getSession'], + implementation: walletGetSessionHandler, + hookNames: { + getCaveat: true, + }, +}; diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts index ebffb0ece8..fce6550e72 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -5,7 +5,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { walletInvokeMethodHandler } from './wallet-invokeMethod'; +import { walletInvokeMethod } from './wallet-invokeMethod'; const createMockedRequest = () => ({ jsonrpc: '2.0' as const, @@ -60,11 +60,17 @@ const createMockedHandler = () => { .fn() .mockReturnValue('selectedNetworkClientId'); const handler = (request: JsonRpcRequest & { origin: string }) => - walletInvokeMethodHandler(request, { jsonrpc: '2.0', id: 1 }, next, end, { - getCaveat, - findNetworkClientIdByChainId, - getSelectedNetworkClientId, - }); + walletInvokeMethod.implementation( + request, + { jsonrpc: '2.0', id: 1 }, + next, + end, + { + getCaveat, + findNetworkClientIdByChainId, + getSelectedNetworkClientId, + }, + ); return { next, diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index 239d65195b..3df3ca8346 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -28,7 +28,7 @@ import { parseScopeString } from '../scope/types'; * @param hooks.findNetworkClientIdByChainId - the hook for finding the networkClientId for a chainId. * @param hooks.getSelectedNetworkClientId - the hook for getting the current globally selected networkClientId. */ -export async function walletInvokeMethodHandler( +async function walletInvokeMethodHandler( request: JsonRpcRequest & { origin: string }, _response: PendingJsonRpcResponse, next: () => void, @@ -109,3 +109,12 @@ export async function walletInvokeMethodHandler( }); return next(); } +export const walletInvokeMethod = { + methodNames: ['wallet_invokeMethod'], + implementation: walletInvokeMethodHandler, + hookNames: { + getCaveat: true, + findNetworkClientIdByChainId: true, + getSelectedNetworkClientId: true, + }, +}; diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.ts b/packages/multichain/src/handlers/wallet-revokeSession.test.ts index 695d0eb430..e11b89f42c 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.test.ts +++ b/packages/multichain/src/handlers/wallet-revokeSession.test.ts @@ -6,7 +6,7 @@ import { rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcRequest } from '@metamask/utils'; import { Caip25EndowmentPermissionName } from '../caip25Permission'; -import { walletRevokeSessionHandler } from './wallet-revokeSession'; +import { walletRevokeSession } from './wallet-revokeSession'; const baseRequest: JsonRpcRequest & { origin: string } = { origin: 'http://test.com', @@ -26,7 +26,7 @@ const createMockedHandler = () => { jsonrpc: '2.0' as const, }; const handler = (request: JsonRpcRequest & { origin: string }) => - walletRevokeSessionHandler(request, response, next, end, { + walletRevokeSession.implementation(request, response, next, end, { revokePermission, }); diff --git a/packages/multichain/src/handlers/wallet-revokeSession.ts b/packages/multichain/src/handlers/wallet-revokeSession.ts index 1aec0b7245..51e31073f0 100644 --- a/packages/multichain/src/handlers/wallet-revokeSession.ts +++ b/packages/multichain/src/handlers/wallet-revokeSession.ts @@ -21,7 +21,7 @@ import { Caip25EndowmentPermissionName } from '../caip25Permission'; * @param hooks - The hooks object. * @param hooks.revokePermission - The revokePermission function. */ -export async function walletRevokeSessionHandler( +async function walletRevokeSessionHandler( request: JsonRpcRequest & { origin: string }, response: JsonRpcSuccess, _next: JsonRpcEngineNextCallback, @@ -45,3 +45,10 @@ export async function walletRevokeSessionHandler( response.result = true; return end(); } +export const walletRevokeSession = { + methodNames: ['wallet_revokeSession'], + implementation: walletRevokeSessionHandler, + hookNames: { + revokePermission: true, + }, +}; diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 9afd6a5722..cca93222f7 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -10,9 +10,9 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", - "walletGetSessionHandler", - "walletInvokeMethodHandler", - "walletRevokeSessionHandler", + "walletGetSession", + "walletInvokeMethod", + "walletRevokeSession", "multichainMethodCallValidatorMiddleware", "MultichainMiddlewareManager", "MultichainSubscriptionManager", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 55a4cd3822..9211d69967 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -9,9 +9,9 @@ export { setPermittedEthChainIds, } from './adapters/caip-permission-adapter-permittedChains'; -export { walletGetSessionHandler } from './handlers/wallet-getSession'; -export { walletInvokeMethodHandler } from './handlers/wallet-invokeMethod'; -export { walletRevokeSessionHandler } from './handlers/wallet-revokeSession'; +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'; diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 72d90bbb7a..5aeecc6930 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -16,9 +16,9 @@ import { parseScopeString } from './types'; * @param list - The list of items to filter * @returns A list of unique items */ -export function getUniqueArrayItems(list: Value[]): Value[] { +export const getUniqueArrayItems = (list: Value[]): Value[] => { return Array.from(new Set(list)); -} +}; /** * Flattens a ScopeString and ScopeObject into a separate @@ -54,7 +54,10 @@ export const mergeScopeObject = ( scopeObjectB: ScopeObject, ) => { const mergedScopeObject: ScopeObject = { - methods: unique([...scopeObjectA.methods, ...scopeObjectB.methods]), + methods: getUniqueArrayItems([ + ...scopeObjectA.methods, + ...scopeObjectB.methods, + ]), notifications: getUniqueArrayItems([ ...scopeObjectA.notifications, ...scopeObjectB.notifications, From ada451daaff2700b7acc1916b1c5c4d48310e40e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 13:52:55 -0700 Subject: [PATCH 32/80] change subscriptionManager require to import --- .../src/middlewares/MultichainSubscriptionManager.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 0fed86cfd9..fa5b1397a2 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -4,6 +4,7 @@ import SafeEventEmitter from '@metamask/safe-event-emitter'; import type { CaipChainId, Hex } from '@metamask/utils'; import { parseCaipChainId } from '@metamask/utils'; import type EventEmitter from 'events'; +import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import type { ExternalScopeString } from '../scope/types'; @@ -30,9 +31,6 @@ type SubscriptionEntry = SubscriptionKey & { subscriptionManager: SubscriptionManager; }; -// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager'); - type MultichainSubscriptionManagerOptions = { findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; getNetworkClientById: NetworkController['getNetworkClientById']; From 2322e39df6590dae6514ab14e37dbd3f58a1f2ed Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 15 Oct 2024 13:59:10 -0700 Subject: [PATCH 33/80] lint --- .../multichain/src/middlewares/MultichainSubscriptionManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index fa5b1397a2..6494ed24a5 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -1,10 +1,10 @@ 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 EventEmitter from 'events'; -import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; import type { ExternalScopeString } from '../scope/types'; From 00a41cb81920d2cfd6af1b64fce60649e0c02528 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 16 Oct 2024 12:37:52 -0700 Subject: [PATCH 34/80] Jl/caip multichain/update scope object account types (#4803) ## Explanation * Make `accounts` required on `ScopeObject` * Make `flattenScope` also add empty `accounts` array if missing * Rename `flattenScope` to `normalizeScope` * Rename `validateAndFlattenScopes` to `validateAndNormalizeScopes` * Rename `flattenMergeScopes` to `normalizeAndMergeScopes` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- ...ip-permission-adapter-eth-accounts.test.ts | 3 + .../caip-permission-adapter-eth-accounts.ts | 1 + ...caip-permission-adapter-middleware.test.ts | 5 ++ ...permission-adapter-permittedChains.test.ts | 11 +++ ...caip-permission-adapter-permittedChains.ts | 1 + .../multichain/src/caip25Permission.test.ts | 76 +++++++++++-------- packages/multichain/src/caip25Permission.ts | 14 ++-- .../src/handlers/wallet-getSession.test.ts | 7 ++ .../src/handlers/wallet-invokeMethod.test.ts | 5 ++ packages/multichain/src/index.test.ts | 6 +- packages/multichain/src/index.ts | 9 ++- packages/multichain/src/scope/assert.test.ts | 1 + .../src/scope/authorization.test.ts | 36 +++++---- .../multichain/src/scope/authorization.ts | 16 ++-- packages/multichain/src/scope/filter.test.ts | 15 ++++ .../multichain/src/scope/transform.test.ts | 55 +++++++++----- packages/multichain/src/scope/transform.ts | 47 ++++++------ packages/multichain/src/scope/types.ts | 5 +- 18 files changed, 206 insertions(+), 107 deletions(-) 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 index 427034e46c..eb55966678 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -36,6 +36,7 @@ describe('CAIP-25 eth_accounts adapters', () => { 'eip155:10': { methods: [], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -93,6 +94,7 @@ describe('CAIP-25 eth_accounts adapters', () => { 'eip155:10': { methods: [], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -102,6 +104,7 @@ describe('CAIP-25 eth_accounts adapters', () => { 'wallet:eip155': { methods: [], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index e0ef4038bf..6c72f1d07b 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -87,6 +87,7 @@ export const setEthAccounts = ( [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], + accounts: [], }, ...caip25CaveatValue.optionalScopes, }, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts index ea6318074a..c044c73b0f 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts @@ -27,24 +27,29 @@ const createMockedHandler = () => { 'eip155:1': { methods: ['eth_call'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, }, optionalScopes: { 'eip155:1': { methods: ['net_version'], notifications: [], + accounts: [], }, wallet: { methods: ['wallet_watchAsset'], notifications: [], + accounts: [], }, unhandled: { methods: ['foobar'], notifications: [], + accounts: [], }, }, isMultichainOrigin: true, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 2ca86004f3..4020c2442b 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -38,6 +38,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'eip155:10': { methods: [], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -72,6 +73,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'wallet:eip155': { methods: [], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, @@ -101,6 +103,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'wallet:eip155': { methods: [], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, @@ -151,6 +154,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'wallet:eip155': { methods: [], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, @@ -246,12 +250,14 @@ describe('CAIP-25 permittedChains adapters', () => { 'bip122:000000000019d6689c085ae165831e93': { methods: [], notifications: [], + accounts: [], }, }, optionalScopes: { 'eip155:1': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -274,12 +280,14 @@ describe('CAIP-25 permittedChains adapters', () => { 'bip122:000000000019d6689c085ae165831e93': { methods: [], notifications: [], + accounts: [], }, }, optionalScopes: { 'eip155:1': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, @@ -300,6 +308,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'eip155:1': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -324,6 +333,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'eip155:1': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, 'eip155:100': { methods: [], @@ -338,6 +348,7 @@ describe('CAIP-25 permittedChains adapters', () => { 'wallet:eip155': { methods: [], notifications: [], + accounts: [], }, }, isMultichainOrigin: false, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 25bd63a208..6a59efa916 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -49,6 +49,7 @@ export const addPermittedEthChainId = ( [KnownWalletScopeString.Eip155]: { methods: [], notifications: [], + accounts: [], }, ...caip25CaveatValue.optionalScopes, [scopeString]: { diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 982a1434c9..818035ff8a 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -17,7 +17,7 @@ import * as ScopeAssert from './scope/assert'; import * as ScopeAuthorization from './scope/authorization'; jest.mock('./scope/authorization', () => ({ - validateAndFlattenScopes: jest.fn(), + validateAndNormalizeScopes: jest.fn(), })); const MockScopeAuthorization = jest.mocked(ScopeAuthorization); @@ -30,9 +30,9 @@ const { removeAccount } = Caip25CaveatMutatorFactories[Caip25CaveatType]; describe('endowment:caip25', () => { beforeEach(() => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: {}, - flattenedOptionalScopes: {}, + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: {}, + normalizedOptionalScopes: {}, }); }); @@ -64,12 +64,14 @@ describe('endowment:caip25', () => { 'eip155:1': { methods: ['eth_call'], notifications: ['chainChanged'], + accounts: [], }, }, optionalScopes: { 'eip155:5': { methods: ['eth_call'], notifications: ['accountsChanged'], + accounts: [], }, }, sessionProperties: {}, @@ -83,6 +85,7 @@ describe('endowment:caip25', () => { 'eip155:1': { methods: ['eth_call'], notifications: ['chainChanged'], + accounts: [], }, }, optionalScopes: {}, @@ -96,12 +99,14 @@ describe('endowment:caip25', () => { 'eip155:1': { methods: ['eth_call'], notifications: ['chainChanged'], + accounts: [], }, }, optionalScopes: { 'eip155:5': { methods: ['eth_call'], notifications: ['accountsChanged'], + accounts: [], }, }, sessionProperties: {}, @@ -208,6 +213,7 @@ describe('endowment:caip25', () => { 'eip155:5': { methods: ['eth_call'], notifications: ['accountsChanged'], + accounts: [], }, }, isMultichainOrigin: true, @@ -358,7 +364,7 @@ describe('endowment:caip25', () => { ); }); - it('validates and flattens the ScopesObjects', () => { + it('validates and normalizes the ScopesObjects', () => { try { validator({ caveats: [ @@ -392,7 +398,7 @@ describe('endowment:caip25', () => { // noop } expect( - MockScopeAuthorization.validateAndFlattenScopes, + MockScopeAuthorization.validateAndNormalizeScopes, ).toHaveBeenCalledWith( { 'eip155:1': { @@ -411,18 +417,20 @@ describe('endowment:caip25', () => { ); }); - it('asserts the validated and flattened required scopes are supported', () => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: { + it('asserts the validated and normalized required scopes are supported', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { 'eip155:1': { - methods: ['flattened_required'], + methods: ['normalized_required'], notifications: [], + accounts: [], }, }, - flattenedOptionalScopes: { + normalizedOptionalScopes: { 'eip155:1': { - methods: ['flattened_optional'], + methods: ['normalized_optional'], notifications: [], + accounts: [], }, }, }); @@ -461,8 +469,9 @@ describe('endowment:caip25', () => { expect(MockScopeAssert.assertScopesSupported).toHaveBeenCalledWith( { 'eip155:1': { - methods: ['flattened_required'], + methods: ['normalized_required'], notifications: [], + accounts: [], }, }, expect.objectContaining({ @@ -474,18 +483,20 @@ describe('endowment:caip25', () => { expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); }); - it('asserts the validated and flattened optional scopes are supported', () => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: { + it('asserts the validated and normalized optional scopes are supported', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { 'eip155:1': { - methods: ['flattened_required'], + methods: ['normalized_required'], notifications: [], + accounts: [], }, }, - flattenedOptionalScopes: { + normalizedOptionalScopes: { 'eip155:1': { - methods: ['flattened_optional'], + methods: ['normalized_optional'], notifications: [], + accounts: [], }, }, }); @@ -524,8 +535,9 @@ describe('endowment:caip25', () => { expect(MockScopeAssert.assertScopesSupported).toHaveBeenCalledWith( { 'eip155:1': { - methods: ['flattened_optional'], + methods: ['normalized_optional'], notifications: [], + accounts: [], }, }, expect.objectContaining({ @@ -537,10 +549,10 @@ describe('endowment:caip25', () => { expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); }); - it('throws if the input requiredScopes does not match the output of validateAndFlattenScopes', () => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: {}, - flattenedOptionalScopes: { + it('throws if the input requiredScopes does not match the output of validateAndNormalizeScopes', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: {}, + normalizedOptionalScopes: { 'eip155:5': { methods: [], notifications: [], @@ -580,16 +592,16 @@ describe('endowment:caip25', () => { }).toThrow(/Expected values to be strictly deep-equal/u); }); - it('throws if the input optionalScopes does not match the output of validateAndFlattenScopes', () => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: { + it('throws if the input optionalScopes does not match the output of validateAndNormalizeScopes', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], notifications: [], accounts: ['eip155:1:0xdead'], }, }, - flattenedOptionalScopes: {}, + normalizedOptionalScopes: {}, }); expect(() => { validator({ @@ -623,16 +635,16 @@ describe('endowment:caip25', () => { }).toThrow(/Expected values to be strictly deep-equal/u); }); - it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and flattened', () => { - MockScopeAuthorization.validateAndFlattenScopes.mockReturnValue({ - flattenedRequiredScopes: { + it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and normalized', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], notifications: [], accounts: ['eip155:1:0xdead'], }, }, - flattenedOptionalScopes: { + normalizedOptionalScopes: { 'eip155:5': { methods: [], notifications: [], diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index dc1b900092..f4f7ec88c1 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -20,7 +20,7 @@ import { strict as assert } from 'assert'; import { cloneDeep, isEqual } from 'lodash'; import { assertScopesSupported } from './scope/assert'; -import { validateAndFlattenScopes } from './scope/authorization'; +import { validateAndNormalizeScopes } from './scope/authorization'; import type { ExternalScopeString, ScopeObject, @@ -101,8 +101,8 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - const { flattenedRequiredScopes, flattenedOptionalScopes } = - validateAndFlattenScopes(requiredScopes, optionalScopes); + const { normalizedRequiredScopes, normalizedOptionalScopes } = + validateAndNormalizeScopes(requiredScopes, optionalScopes); const isChainIdSupported = (chainId: Hex) => { try { @@ -113,15 +113,15 @@ const specificationBuilder: PermissionSpecificationBuilder< } }; - assertScopesSupported(flattenedRequiredScopes, { + assertScopesSupported(normalizedRequiredScopes, { isChainIdSupported, }); - assertScopesSupported(flattenedOptionalScopes, { + assertScopesSupported(normalizedOptionalScopes, { isChainIdSupported, }); - assert.deepEqual(requiredScopes, flattenedRequiredScopes); - assert.deepEqual(optionalScopes, flattenedOptionalScopes); + assert.deepEqual(requiredScopes, normalizedRequiredScopes); + assert.deepEqual(optionalScopes, normalizedOptionalScopes); }, }; }; diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts index 68c6044119..ca74cc9b90 100644 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -23,20 +23,24 @@ const createMockedHandler = () => { 'eip155:1': { methods: ['eth_call'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, }, optionalScopes: { 'eip155:1': { methods: ['net_version'], notifications: ['chainChanged'], + accounts: [], }, wallet: { methods: ['wallet_watchAsset'], notifications: [], + accounts: [], }, }, }, @@ -95,14 +99,17 @@ describe('wallet_getSession', () => { '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-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts index fce6550e72..208bccc337 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -32,24 +32,29 @@ const createMockedHandler = () => { 'eip155:1': { methods: ['eth_call'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['eth_chainId'], notifications: [], + accounts: [], }, }, optionalScopes: { 'eip155:1': { methods: ['net_version'], notifications: [], + accounts: [], }, wallet: { methods: ['wallet_watchAsset'], notifications: [], + accounts: [], }, unhandled: { methods: ['foobar'], notifications: [], + accounts: [], }, }, isMultichainOrigin: true, diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index cca93222f7..d06cd6094e 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -18,7 +18,7 @@ describe('@metamask/multichain', () => { "MultichainSubscriptionManager", "assertScopeSupported", "assertScopesSupported", - "validateAndFlattenScopes", + "validateAndNormalizeScopes", "bucketScopes", "bucketScopesBySupport", "filterScopesSupported", @@ -26,10 +26,10 @@ describe('@metamask/multichain', () => { "isSupportedAccount", "isSupportedMethod", "isSupportedNotification", - "flattenScope", + "normalizeScope", "mergeScopeObject", "mergeScopes", - "flattenMergeScopes", + "normalizeAndMergeScopes", "isValidScope", "validateScopes", "Caip25CaveatType", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 9211d69967..f764ea5b3e 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -19,7 +19,10 @@ export { MultichainSubscriptionManager } from './middlewares/MultichainSubscript export { assertScopeSupported, assertScopesSupported } from './scope/assert'; export type { Caip25Authorization } from './scope/authorization'; -export { validateAndFlattenScopes, bucketScopes } from './scope/authorization'; +export { + validateAndNormalizeScopes, + bucketScopes, +} from './scope/authorization'; export { bucketScopesBySupport, filterScopesSupported } from './scope/filter'; export * from './scope/types'; export { @@ -29,10 +32,10 @@ export { isSupportedNotification, } from './scope/supported'; export { - flattenScope, + normalizeScope, mergeScopeObject, mergeScopes, - flattenMergeScopes, + normalizeAndMergeScopes, } from './scope/transform'; export { isValidScope, validateScopes } from './scope/validation'; diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 92e936a741..9b09aa6437 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -14,6 +14,7 @@ const MockSupported = jest.mocked(Supported); const validScopeObject: ScopeObject = { methods: [], notifications: [], + accounts: [], }; describe('Scope Assert', () => { diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 44e6323f38..4c8bc65e7f 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,4 +1,4 @@ -import { bucketScopes, validateAndFlattenScopes } from './authorization'; +import { bucketScopes, validateAndNormalizeScopes } from './authorization'; import * as Filter from './filter'; import * as Transform from './transform'; import type { ExternalScopeObject } from './types'; @@ -10,7 +10,7 @@ jest.mock('./validation', () => ({ const MockValidation = jest.mocked(Validation); jest.mock('./transform', () => ({ - flattenMergeScopes: jest.fn(), + normalizeAndMergeScopes: jest.fn(), })); const MockTransform = jest.mocked(Transform); @@ -29,10 +29,10 @@ describe('Scope Authorization', () => { jest.resetAllMocks(); }); - describe('validateAndFlattenScopes', () => { + describe('validateAndNormalizeScopes', () => { it('validates the scopes', () => { try { - validateAndFlattenScopes( + validateAndNormalizeScopes( { 'eip155:1': validScopeObject, }, @@ -53,7 +53,7 @@ describe('Scope Authorization', () => { ); }); - it('flatten and merges the validated scopes', () => { + it('normalized and merges the validated scopes', () => { MockValidation.validateScopes.mockReturnValue({ validRequiredScopes: { 'eip155:1': validScopeObject, @@ -63,16 +63,16 @@ describe('Scope Authorization', () => { }, }); - validateAndFlattenScopes({}, {}); - expect(MockTransform.flattenMergeScopes).toHaveBeenCalledWith({ + validateAndNormalizeScopes({}, {}); + expect(MockTransform.normalizeAndMergeScopes).toHaveBeenCalledWith({ 'eip155:1': validScopeObject, }); - expect(MockTransform.flattenMergeScopes).toHaveBeenCalledWith({ + expect(MockTransform.normalizeAndMergeScopes).toHaveBeenCalledWith({ 'eip155:5': validScopeObject, }); }); - it('returns the flattened and merged scopes', () => { + it('returns the normalized and merged scopes', () => { MockValidation.validateScopes.mockReturnValue({ validRequiredScopes: { 'eip155:1': validScopeObject, @@ -81,17 +81,17 @@ describe('Scope Authorization', () => { 'eip155:5': validScopeObject, }, }); - MockTransform.flattenMergeScopes.mockImplementation((value) => ({ + MockTransform.normalizeAndMergeScopes.mockImplementation((value) => ({ ...value, transformed: true, })); - expect(validateAndFlattenScopes({}, {})).toStrictEqual({ - flattenedRequiredScopes: { + expect(validateAndNormalizeScopes({}, {})).toStrictEqual({ + normalizedRequiredScopes: { 'eip155:1': validScopeObject, transformed: true, }, - flattenedOptionalScopes: { + normalizedOptionalScopes: { 'eip155:5': validScopeObject, transformed: true, }, @@ -109,12 +109,14 @@ describe('Scope Authorization', () => { 'mock:A': { methods: [`mock_method_${callCount}`], notifications: [], + accounts: [], }, }, unsupportedScopes: { 'mock:B': { methods: [`mock_method_${callCount}`], notifications: [], + accounts: [], }, }, }; @@ -128,6 +130,7 @@ describe('Scope Authorization', () => { wallet: { methods: [], notifications: [], + accounts: [], }, }, { @@ -141,6 +144,7 @@ describe('Scope Authorization', () => { wallet: { methods: [], notifications: [], + accounts: [], }, }, { @@ -156,6 +160,7 @@ describe('Scope Authorization', () => { wallet: { methods: [], notifications: [], + accounts: [], }, }, { @@ -169,6 +174,7 @@ describe('Scope Authorization', () => { 'mock:B': { methods: [`mock_method_1`], notifications: [], + accounts: [], }, }, { @@ -184,6 +190,7 @@ describe('Scope Authorization', () => { wallet: { methods: [], notifications: [], + accounts: [], }, }, { @@ -196,18 +203,21 @@ describe('Scope Authorization', () => { '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 index 9b377a8b13..3dcbef7e40 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,7 +1,7 @@ import type { Hex } from '@metamask/utils'; import { bucketScopesBySupport } from './filter'; -import { flattenMergeScopes } from './transform'; +import { normalizeAndMergeScopes } from './transform'; import type { ExternalScopesObject, ScopesObject } from './types'; import { validateScopes } from './validation'; @@ -18,24 +18,24 @@ export type Caip25Authorization = sessionProperties?: Record; }); -export const validateAndFlattenScopes = ( +export const validateAndNormalizeScopes = ( requiredScopes: ExternalScopesObject, optionalScopes: ExternalScopesObject, ): { - flattenedRequiredScopes: ScopesObject; - flattenedOptionalScopes: ScopesObject; + normalizedRequiredScopes: ScopesObject; + normalizedOptionalScopes: ScopesObject; } => { const { validRequiredScopes, validOptionalScopes } = validateScopes( requiredScopes, optionalScopes, ); - const flattenedRequiredScopes = flattenMergeScopes(validRequiredScopes); - const flattenedOptionalScopes = flattenMergeScopes(validOptionalScopes); + const normalizedRequiredScopes = normalizeAndMergeScopes(validRequiredScopes); + const normalizedOptionalScopes = normalizeAndMergeScopes(validOptionalScopes); return { - flattenedRequiredScopes, - flattenedOptionalScopes, + normalizedRequiredScopes, + normalizedOptionalScopes, }; }; diff --git a/packages/multichain/src/scope/filter.test.ts b/packages/multichain/src/scope/filter.test.ts index cf7c492583..c49c739786 100644 --- a/packages/multichain/src/scope/filter.test.ts +++ b/packages/multichain/src/scope/filter.test.ts @@ -20,10 +20,12 @@ describe('filter', () => { 'eip155:1': { methods: ['a'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }, { isChainIdSupported }, @@ -34,6 +36,7 @@ describe('filter', () => { { methods: ['a'], notifications: [], + accounts: [], }, { isChainIdSupported }, ); @@ -42,6 +45,7 @@ describe('filter', () => { { methods: ['b'], notifications: [], + accounts: [], }, { isChainIdSupported }, ); @@ -60,10 +64,12 @@ describe('filter', () => { 'eip155:1': { methods: ['a'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }, { isChainIdSupported }, @@ -72,6 +78,7 @@ describe('filter', () => { 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }); }); @@ -86,10 +93,12 @@ describe('filter', () => { 'eip155:1': { methods: ['a'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }, { isChainIdSupported }, @@ -100,6 +109,7 @@ describe('filter', () => { { methods: ['a'], notifications: [], + accounts: [], }, { isChainIdSupported }, ); @@ -108,6 +118,7 @@ describe('filter', () => { { methods: ['b'], notifications: [], + accounts: [], }, { isChainIdSupported }, ); @@ -126,10 +137,12 @@ describe('filter', () => { 'eip155:1': { methods: ['a'], notifications: [], + accounts: [], }, 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }, { isChainIdSupported }, @@ -139,12 +152,14 @@ describe('filter', () => { 'eip155:5': { methods: ['b'], notifications: [], + accounts: [], }, }, unsupportedScopes: { 'eip155:1': { methods: ['a'], notifications: [], + accounts: [], }, }, }); diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index acfc694e7d..afdd3ae2a3 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -1,34 +1,46 @@ import { - flattenScope, + normalizeScope, mergeScopes, mergeScopeObject, - flattenMergeScopes, + normalizeAndMergeScopes, } from './transform'; -import type { ExternalScopeObject } from './types'; +import type { ExternalScopeObject, ScopeObject } from './types'; -const validScopeObject: ExternalScopeObject = { +const externalScopeObject: ExternalScopeObject = { methods: [], notifications: [], }; +const validScopeObject: ScopeObject = { + methods: [], + notifications: [], + accounts: [], +}; + describe('Scope Transform', () => { - describe('flattenScope', () => { - it('returns the scope as is when the scopeString is chain scoped', () => { - expect(flattenScope('eip155:1', validScopeObject)).toStrictEqual({ + describe('normalizeScope', () => { + it('returns the scope with empty accounts array when the scopeString is chain scoped when accounts are not defined', () => { + expect(normalizeScope('eip155:1', externalScopeObject)).toStrictEqual({ + 'eip155:1': validScopeObject, + }); + }); + + it('returns the scope as is when the scopeString is chain scoped and accounts are defined', () => { + expect(normalizeScope('eip155:1', validScopeObject)).toStrictEqual({ 'eip155:1': validScopeObject, }); }); describe('scopeString is namespace scoped', () => { it('returns the scope as is when `references` is not defined', () => { - expect(flattenScope('eip155', validScopeObject)).toStrictEqual({ + expect(normalizeScope('eip155', validScopeObject)).toStrictEqual({ eip155: validScopeObject, }); }); it('returns one scope per `references` element with `references` excluded from the scopeObject', () => { expect( - flattenScope('eip155', { + normalizeScope('eip155', { ...validScopeObject, references: ['1', '5', '64'], }), @@ -40,16 +52,16 @@ describe('Scope Transform', () => { }); it('returns one deep cloned scope per `references` element', () => { - const flattenedScopes = flattenScope('eip155', { + const noramlizedScopes = normalizeScope('eip155', { ...validScopeObject, references: ['1', '5'], }); - expect(flattenedScopes['eip155:1']).not.toBe( - flattenedScopes['eip155:5'], + expect(noramlizedScopes['eip155:1']).not.toBe( + noramlizedScopes['eip155:5'], ); - expect(flattenedScopes['eip155:1'].methods).not.toBe( - flattenedScopes['eip155:5'].methods, + expect(noramlizedScopes['eip155:1'].methods).not.toBe( + noramlizedScopes['eip155:5'].methods, ); }); }); @@ -200,12 +212,14 @@ describe('Scope Transform', () => { 'eip155:1': { methods: ['a', 'b', 'c'], notifications: ['foo'], + accounts: [], }, }, { 'eip155:1': { methods: ['c', 'd'], notifications: ['bar'], + accounts: [], }, }, ), @@ -213,6 +227,7 @@ describe('Scope Transform', () => { 'eip155:1': { methods: ['a', 'b', 'c', 'd'], notifications: ['foo', 'bar'], + accounts: [], }, }); }); @@ -224,16 +239,19 @@ describe('Scope Transform', () => { 'eip155:1': { methods: ['a', 'b', 'c'], notifications: ['foo'], + accounts: [], }, }, { 'eip155:2': { methods: ['c', 'd'], notifications: ['bar'], + accounts: [], }, 'eip155:3': { methods: [], notifications: [], + accounts: [], }, }, ), @@ -241,23 +259,26 @@ describe('Scope Transform', () => { 'eip155:1': { methods: ['a', 'b', 'c'], notifications: ['foo'], + accounts: [], }, 'eip155:2': { methods: ['c', 'd'], notifications: ['bar'], + accounts: [], }, 'eip155:3': { methods: [], notifications: [], + accounts: [], }, }); }); }); - describe('flattenMergeScopes', () => { - it('flattens scopes and merges any overlapping scopeStrings', () => { + describe('normalizeAndMergeScopes', () => { + it('normalizes scopes and merges any overlapping scopeStrings', () => { expect( - flattenMergeScopes({ + normalizeAndMergeScopes({ eip155: { ...validScopeObject, methods: ['a', 'b'], diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 5aeecc6930..94f69d060d 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -21,30 +21,36 @@ export const getUniqueArrayItems = (list: Value[]): Value[] => { }; /** - * Flattens a ScopeString and ScopeObject into a separate + * Normalizes a ScopeString and ExternalScopeObject into a separate * ScopeString and ScopeObject for each reference in the `references` - * value if defined. Returns the ScopeString and ScopeObject - * unmodified if it cannot be flattened + * value if defined and adds an empty `accounts` array if not defined. * - * @param scopeString - The string representing the scopeObject - * @param scopeObject - The object that defines the scope + * @param scopeString - The string representing the scope + * @param externalScopeObject - The object that defines the scope * @returns a map of caipChainId to ScopeObjects */ -export const flattenScope = ( +export const normalizeScope = ( scopeString: string, - scopeObject: ExternalScopeObject, + externalScopeObject: ExternalScopeObject, ): ScopesObject => { - const { references, ...restScopeObject } = scopeObject; + const { references, ...scopeObject } = externalScopeObject; const { namespace, reference } = parseScopeString(scopeString); + const normalizedScopeObject = { + accounts: [], + ...scopeObject, + }; + // Scope is already a CAIP-2 ID and has no references to flatten if (!namespace || reference || !references) { - return { [scopeString]: scopeObject }; + return { [scopeString]: normalizedScopeObject }; } const scopeMap: ScopesObject = {}; references.forEach((nestedReference: CaipReference) => { - scopeMap[`${namespace}:${nestedReference}`] = cloneDeep(restScopeObject); + scopeMap[`${namespace}:${nestedReference}`] = cloneDeep( + normalizedScopeObject, + ); }); return scopeMap; }; @@ -62,15 +68,12 @@ export const mergeScopeObject = ( ...scopeObjectA.notifications, ...scopeObjectB.notifications, ]), + accounts: getUniqueArrayItems([ + ...scopeObjectA.accounts, + ...scopeObjectB.accounts, + ]), }; - if (scopeObjectA.accounts || scopeObjectB.accounts) { - mergedScopeObject.accounts = getUniqueArrayItems([ - ...(scopeObjectA.accounts ?? []), - ...(scopeObjectB.accounts ?? []), - ]); - } - if (scopeObjectA.rpcDocuments || scopeObjectB.rpcDocuments) { mergedScopeObject.rpcDocuments = getUniqueArrayItems([ ...(scopeObjectA.rpcDocuments ?? []), @@ -115,14 +118,14 @@ export const mergeScopes = ( return scope; }; -export const flattenMergeScopes = ( +export const normalizeAndMergeScopes = ( scopes: ExternalScopesObject, ): ScopesObject => { - let flattenedScopes: ScopesObject = {}; + let mergedScopes: ScopesObject = {}; Object.keys(scopes).forEach((scopeString) => { - const flattenedScopeMap = flattenScope(scopeString, scopes[scopeString]); - flattenedScopes = mergeScopes(flattenedScopes, flattenedScopeMap); + const normalizedScopes = normalizeScope(scopeString, scopes[scopeString]); + mergedScopes = mergeScopes(mergedScopes, normalizedScopes); }); - return flattenedScopes; + return mergedScopes; }; diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index d9c40b8652..0d5c87fbb1 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -51,8 +51,9 @@ export const KnownNotifications: Record = // These External prefixed types represent the CAIP-217 // Scope and ScopeObject as defined in the spec. export type ExternalScopeString = CaipChainId | CaipNamespace; -export type ExternalScopeObject = ScopeObject & { +export type ExternalScopeObject = Omit & { references?: CaipReference[]; + accounts?: CaipAccountId[]; }; export type ExternalScopesObject = Record< ExternalScopeString, @@ -70,7 +71,7 @@ export type ScopeString = CaipChainId | KnownCaipNamespace.Wallet; export type ScopeObject = { methods: string[]; notifications: string[]; - accounts?: CaipAccountId[]; + accounts: CaipAccountId[]; rpcDocuments?: string[]; rpcEndpoints?: string[]; }; From f30dd8bcf7a0936c970a5e1c1b3920b057e01a8b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 17 Oct 2024 10:25:29 -0500 Subject: [PATCH 35/80] update @metamask/rpc-errors version --- packages/multichain/package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index e633a1bb2b..044b203f0b 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -50,7 +50,7 @@ "@metamask/api-specs": "^0.10.12", "@metamask/controller-utils": "^11.3.0", "@metamask/eth-json-rpc-filters": "^7.0.0", - "@metamask/rpc-errors": "^6.3.1", + "@metamask/rpc-errors": "^7.0.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^9.1.0", "@open-rpc/schema-utils-js": "^2.0.5", diff --git a/yarn.lock b/yarn.lock index 5646ea327a..f2d4d6fc21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3094,7 +3094,7 @@ __metadata: "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" - "@metamask/rpc-errors": "npm:^6.3.1" + "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^9.1.0" "@open-rpc/meta-schema": "npm:^1.14.6" From 644f4d0c55cb10ece1a776a80a121ce8506faf95 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 12:35:41 -0700 Subject: [PATCH 36/80] Add isEqualCaseInsensitive to controller-utils --- .../src/TokenDetectionController.ts | 26 ++++--------------- packages/controller-utils/src/index.ts | 1 + packages/controller-utils/src/util.test.ts | 26 +++++++++++++++++++ packages/controller-utils/src/util.ts | 17 ++++++++++++ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 2459baea38..76400bc7cc 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -9,7 +9,11 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import contractMap from '@metamask/contract-metadata'; -import { ChainId, safelyExecute } from '@metamask/controller-utils'; +import { + ChainId, + safelyExecute, + isEqualCaseInsensitive, +} from '@metamask/controller-utils'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -44,26 +48,6 @@ import type { const DEFAULT_INTERVAL = 180000; -/** - * Compare 2 given strings and return boolean - * eg: "foo" and "FOO" => true - * eg: "foo" and "bar" => false - * eg: "foo" and 123 => false - * - * @param value1 - first string to compare - * @param value2 - first string to compare - * @returns true if 2 strings are identical when they are lowercase - */ -export function isEqualCaseInsensitive( - value1: string, - value2: string, -): boolean { - if (typeof value1 !== 'string' || typeof value2 !== 'string') { - return false; - } - return value1.toLowerCase() === value2.toLowerCase(); -} - type LegacyToken = { name: string; logo: `${string}.svg`; diff --git a/packages/controller-utils/src/index.ts b/packages/controller-utils/src/index.ts index 265872e620..3d35d62c0a 100644 --- a/packages/controller-utils/src/index.ts +++ b/packages/controller-utils/src/index.ts @@ -27,6 +27,7 @@ export { toChecksumHexAddress, toHex, weiHexToGweiDec, + isEqualCaseInsensitive, } from './util'; export * from './types'; export * from './siwe'; diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts index 71dd33e90d..3126fb7ef1 100644 --- a/packages/controller-utils/src/util.test.ts +++ b/packages/controller-utils/src/util.test.ts @@ -611,3 +611,29 @@ describe('util', () => { }); }); }); + +describe('isEqualCaseInsensitive', () => { + it('returns false for non-string values', () => { + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(null, 'test')).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive('test', null)).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(5, 'test')).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive('test', 5)).toBe(false); + }); + + it('returns false for strings that are not equal', () => { + expect(util.isEqualCaseInsensitive('test', 'test1')).toBe(false); + expect(util.isEqualCaseInsensitive('test1', 'test')).toBe(false); + }); + + it('returns true for strings that are equal', () => { + expect(util.isEqualCaseInsensitive('test', 'TEST')).toBe(true); + expect(util.isEqualCaseInsensitive('test', 'test')).toBe(true); + expect(util.isEqualCaseInsensitive('TEST', 'TEST')).toBe(true); + expect(util.isEqualCaseInsensitive('test', 'Test')).toBe(true); + expect(util.isEqualCaseInsensitive('Test', 'test')).toBe(true); + }); +}); diff --git a/packages/controller-utils/src/util.ts b/packages/controller-utils/src/util.ts index 4d14f71e6f..4d53b069d9 100644 --- a/packages/controller-utils/src/util.ts +++ b/packages/controller-utils/src/util.ts @@ -619,3 +619,20 @@ function logOrRethrowError(error: unknown, codesToCatch: number[] = []) { throw error; } } + +/** + * Checks if two strings are equal, ignoring case. + * + * @param value1 - The first string to compare. + * @param value2 - The second string to compare. + * @returns `true` if the strings are equal, ignoring case; otherwise, `false`. + */ +export function isEqualCaseInsensitive( + value1: string, + value2: string, +): boolean { + if (typeof value1 !== 'string' || typeof value2 !== 'string') { + return false; + } + return value1.toLowerCase() === value2.toLowerCase(); +} From 1b78bbc9a4e6f9dd3cab4a2ffded4b013830c2f2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 12:45:33 -0700 Subject: [PATCH 37/80] create initial multichain package --- README.md | 30 +++++++------ packages/multichain/CHANGELOG.md | 10 +++++ packages/multichain/LICENSE | 20 +++++++++ packages/multichain/README.md | 15 +++++++ packages/multichain/jest.config.js | 26 +++++++++++ packages/multichain/package.json | 60 +++++++++++++++++++++++++ packages/multichain/src/index.test.ts | 9 ++++ packages/multichain/src/index.ts | 9 ++++ packages/multichain/tsconfig.build.json | 10 +++++ packages/multichain/tsconfig.json | 8 ++++ packages/multichain/typedoc.json | 7 +++ tsconfig.build.json | 1 + tsconfig.json | 1 + yarn.lock | 15 +++++++ 14 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 packages/multichain/CHANGELOG.md create mode 100644 packages/multichain/LICENSE create mode 100644 packages/multichain/README.md create mode 100644 packages/multichain/jest.config.js create mode 100644 packages/multichain/package.json create mode 100644 packages/multichain/src/index.test.ts create mode 100644 packages/multichain/src/index.ts create mode 100644 packages/multichain/tsconfig.build.json create mode 100644 packages/multichain/tsconfig.json create mode 100644 packages/multichain/typedoc.json diff --git a/README.md b/README.md index e5961edab2..d13cf34247 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ This repository contains the following packages [^fn1]: - [`@metamask/keyring-controller`](packages/keyring-controller) - [`@metamask/logging-controller`](packages/logging-controller) - [`@metamask/message-manager`](packages/message-manager) +- [`@metamask/multichain`](packages/multichain) - [`@metamask/name-controller`](packages/name-controller) - [`@metamask/network-controller`](packages/network-controller) - [`@metamask/notification-controller`](packages/notification-controller) @@ -71,6 +72,7 @@ linkStyle default opacity:0.5 keyring_controller(["@metamask/keyring-controller"]); logging_controller(["@metamask/logging-controller"]); message_manager(["@metamask/message-manager"]); + multichain(["@metamask/multichain"]); name_controller(["@metamask/name-controller"]); network_controller(["@metamask/network-controller"]); notification_controller(["@metamask/notification-controller"]); @@ -93,14 +95,15 @@ linkStyle default opacity:0.5 address_book_controller --> controller_utils; announcement_controller --> base_controller; approval_controller --> base_controller; - assets_controllers --> accounts_controller; - assets_controllers --> approval_controller; assets_controllers --> base_controller; assets_controllers --> controller_utils; + assets_controllers --> polling_controller; + assets_controllers --> accounts_controller; + assets_controllers --> approval_controller; assets_controllers --> keyring_controller; assets_controllers --> network_controller; - assets_controllers --> polling_controller; assets_controllers --> preferences_controller; + base_controller --> json_rpc_engine; chain_controller --> base_controller; composable_controller --> base_controller; composable_controller --> json_rpc_engine; @@ -110,8 +113,8 @@ linkStyle default opacity:0.5 eth_json_rpc_provider --> json_rpc_engine; gas_fee_controller --> base_controller; gas_fee_controller --> controller_utils; - gas_fee_controller --> network_controller; gas_fee_controller --> polling_controller; + gas_fee_controller --> network_controller; json_rpc_middleware_stream --> json_rpc_engine; keyring_controller --> base_controller; keyring_controller --> message_manager; @@ -124,7 +127,6 @@ linkStyle default opacity:0.5 network_controller --> base_controller; network_controller --> controller_utils; network_controller --> eth_json_rpc_provider; - network_controller --> json_rpc_engine; notification_controller --> base_controller; notification_services_controller --> base_controller; notification_services_controller --> controller_utils; @@ -132,7 +134,6 @@ linkStyle default opacity:0.5 notification_services_controller --> profile_sync_controller; permission_controller --> base_controller; permission_controller --> controller_utils; - permission_controller --> json_rpc_engine; permission_controller --> approval_controller; permission_log_controller --> base_controller; permission_log_controller --> json_rpc_engine; @@ -145,9 +146,11 @@ linkStyle default opacity:0.5 preferences_controller --> controller_utils; preferences_controller --> keyring_controller; profile_sync_controller --> base_controller; + profile_sync_controller --> keyring_controller; + profile_sync_controller --> accounts_controller; + profile_sync_controller --> network_controller; queued_request_controller --> base_controller; queued_request_controller --> controller_utils; - queued_request_controller --> json_rpc_engine; queued_request_controller --> network_controller; queued_request_controller --> selected_network_controller; rate_limit_controller --> base_controller; @@ -155,26 +158,25 @@ linkStyle default opacity:0.5 selected_network_controller --> json_rpc_engine; selected_network_controller --> network_controller; selected_network_controller --> permission_controller; - signature_controller --> approval_controller; signature_controller --> base_controller; signature_controller --> controller_utils; + signature_controller --> approval_controller; signature_controller --> keyring_controller; signature_controller --> logging_controller; - signature_controller --> message_manager; - transaction_controller --> accounts_controller; - transaction_controller --> approval_controller; transaction_controller --> base_controller; transaction_controller --> controller_utils; + transaction_controller --> accounts_controller; + transaction_controller --> approval_controller; + transaction_controller --> eth_json_rpc_provider; transaction_controller --> gas_fee_controller; transaction_controller --> network_controller; - transaction_controller --> eth_json_rpc_provider; - user_operation_controller --> approval_controller; user_operation_controller --> base_controller; user_operation_controller --> controller_utils; + user_operation_controller --> polling_controller; + user_operation_controller --> approval_controller; user_operation_controller --> gas_fee_controller; user_operation_controller --> keyring_controller; user_operation_controller --> network_controller; - user_operation_controller --> polling_controller; user_operation_controller --> transaction_controller; ``` diff --git a/packages/multichain/CHANGELOG.md b/packages/multichain/CHANGELOG.md new file mode 100644 index 0000000000..b518709c7b --- /dev/null +++ b/packages/multichain/CHANGELOG.md @@ -0,0 +1,10 @@ +# 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] + +[Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/multichain/LICENSE b/packages/multichain/LICENSE new file mode 100644 index 0000000000..6f8bff03fc --- /dev/null +++ b/packages/multichain/LICENSE @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000000..dc89e0fade --- /dev/null +++ b/packages/multichain/README.md @@ -0,0 +1,15 @@ +# `@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 new file mode 100644 index 0000000000..ca08413339 --- /dev/null +++ b/packages/multichain/jest.config.js @@ -0,0 +1,26 @@ +/* + * 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 new file mode 100644 index 0000000000..8bb2159698 --- /dev/null +++ b/packages/multichain/package.json @@ -0,0 +1,60 @@ +{ + "name": "@metamask/multichain", + "version": "0.0.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": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./dist/types/index.d.ts" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "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", + "test": "jest --reporters=jest-silent-reporter", + "test:clean": "jest --clearCache", + "test:verbose": "jest --verbose", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@metamask/auto-changelog": "^3.4.4", + "@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" + }, + "engines": { + "node": "^18.18 || >=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts new file mode 100644 index 0000000000..bc062d3694 --- /dev/null +++ b/packages/multichain/src/index.test.ts @@ -0,0 +1,9 @@ +import greeter from '.'; + +describe('Test', () => { + it('greets', () => { + const name = 'Huey'; + const result = greeter(name); + expect(result).toBe('Hello, Huey!'); + }); +}); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts new file mode 100644 index 0000000000..6972c11729 --- /dev/null +++ b/packages/multichain/src/index.ts @@ -0,0 +1,9 @@ +/** + * Example function that returns a greeting for the given name. + * + * @param name - The name to greet. + * @returns The greeting. + */ +export default function greeter(name: string): string { + return `Hello, ${name}!`; +} diff --git a/packages/multichain/tsconfig.build.json b/packages/multichain/tsconfig.build.json new file mode 100644 index 0000000000..02a0eea03f --- /dev/null +++ b/packages/multichain/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain/tsconfig.json b/packages/multichain/tsconfig.json new file mode 100644 index 0000000000..025ba2ef7f --- /dev/null +++ b/packages/multichain/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./" + }, + "references": [], + "include": ["../../types", "./src"] +} diff --git a/packages/multichain/typedoc.json b/packages/multichain/typedoc.json new file mode 100644 index 0000000000..c9da015dbf --- /dev/null +++ b/packages/multichain/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 4e485ea189..6102878c56 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -18,6 +18,7 @@ { "path": "./packages/keyring-controller/tsconfig.build.json" }, { "path": "./packages/logging-controller/tsconfig.build.json" }, { "path": "./packages/message-manager/tsconfig.build.json" }, + { "path": "./packages/multichain/tsconfig.build.json" }, { "path": "./packages/name-controller/tsconfig.build.json" }, { "path": "./packages/network-controller/tsconfig.build.json" }, { "path": "./packages/notification-controller/tsconfig.build.json" }, diff --git a/tsconfig.json b/tsconfig.json index f886671a63..127a643b9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ { "path": "./packages/json-rpc-middleware-stream" }, { "path": "./packages/keyring-controller" }, { "path": "./packages/message-manager" }, + { "path": "./packages/multichain" }, { "path": "./packages/name-controller" }, { "path": "./packages/network-controller" }, { "path": "./packages/notification-controller" }, diff --git a/yarn.lock b/yarn.lock index ed66bdca2d..10990344a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3021,6 +3021,21 @@ __metadata: languageName: node linkType: hard +"@metamask/multichain@workspace:packages/multichain": + version: 0.0.0-use.local + resolution: "@metamask/multichain@workspace:packages/multichain" + dependencies: + "@metamask/auto-changelog": "npm:^3.4.4" + "@types/jest": "npm:^27.4.1" + deepmerge: "npm:^4.2.2" + jest: "npm:^27.5.1" + ts-jest: "npm:^27.1.4" + typedoc: "npm:^0.24.8" + typedoc-plugin-missing-exports: "npm:^2.0.0" + typescript: "npm:~5.2.2" + 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" From fc28c89c17de6e9593c6551cd159ebd10587489c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:07:43 -0700 Subject: [PATCH 38/80] remove API related logic --- .../src/handlers/wallet-getSession.test.ts | 117 -------- .../src/handlers/wallet-getSession.ts | 66 ---- .../src/handlers/wallet-invokeMethod.test.ts | 282 ------------------ .../src/handlers/wallet-invokeMethod.ts | 120 -------- .../src/handlers/wallet-revokeSession.test.ts | 92 ------ .../src/handlers/wallet-revokeSession.ts | 54 ---- packages/multichain/src/index.test.ts | 11 - packages/multichain/src/index.ts | 11 - .../MultichainMiddlewareManager.test.ts | 171 ----------- .../MultichainMiddlewareManager.ts | 137 --------- .../MultichainSubscriptionManager.test.ts | 120 -------- .../MultichainSubscriptionManager.ts | 160 ---------- .../multichainMethodCallValidator.ts | 101 ------- .../src/scope/authorization.test.ts | 133 +-------- .../multichain/src/scope/authorization.ts | 32 -- packages/multichain/src/scope/filter.test.ts | 168 ----------- packages/multichain/src/scope/filter.ts | 44 --- 17 files changed, 1 insertion(+), 1818 deletions(-) delete mode 100644 packages/multichain/src/handlers/wallet-getSession.test.ts delete mode 100644 packages/multichain/src/handlers/wallet-getSession.ts delete mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.test.ts delete mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.ts delete mode 100644 packages/multichain/src/handlers/wallet-revokeSession.test.ts delete mode 100644 packages/multichain/src/handlers/wallet-revokeSession.ts delete mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts delete mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.ts delete mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts delete mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.ts delete mode 100644 packages/multichain/src/middlewares/multichainMethodCallValidator.ts delete mode 100644 packages/multichain/src/scope/filter.test.ts delete mode 100644 packages/multichain/src/scope/filter.ts 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 ca74cc9b90..0000000000 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { JsonRpcRequest } from '@metamask/utils'; - -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { walletGetSession } from './wallet-getSession'; - -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 getCaveat = jest.fn().mockReturnValue({ - value: { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - methods: ['net_version'], - notifications: ['chainChanged'], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - }, - }, - }); - const response = { - result: { - sessionScopes: {}, - }, - id: 1, - jsonrpc: '2.0' as const, - }; - const handler = (request: JsonRpcRequest & { origin: string }) => - walletGetSession.implementation(request, response, next, end, { - getCaveat, - }); - - return { - next, - response, - end, - getCaveat, - handler, - }; -}; - -describe('wallet_getSession', () => { - it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { - const { handler, getCaveat } = createMockedHandler(); - - await handler(baseRequest); - expect(getCaveat).toHaveBeenCalledWith( - 'http://test.com', - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - }); - - it('returns empty scopes if the CAIP-25 endowment permission does not exist', async () => { - const { handler, response, getCaveat } = createMockedHandler(); - getCaveat.mockImplementation(() => { - throw new Error('permission not found'); - }); - - await handler(baseRequest); - expect(response.result).toStrictEqual({ - sessionScopes: {}, - }); - }); - - it('returns the merged scopes', async () => { - const { handler, response } = createMockedHandler(); - - 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 ad3e4e5569..0000000000 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Caveat } from '@metamask/permission-controller'; -import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import type { ScopesObject } from '../scope/types'; - -/** - * Handler for the `wallet_getSession` RPC method. - * - * @param request - The request object. - * @param response - The response object. - * @param _next - The next middleware function. - * @param end - The end function. - * @param hooks - The hooks object. - * @param hooks.getCaveat - Function to retrieve a caveat. - */ -async function walletGetSessionHandler( - request: JsonRpcRequest & { origin: string }, - response: JsonRpcSuccess<{ sessionScopes: ScopesObject }>, - _next: () => void, - end: () => void, - hooks: { - getCaveat: ( - origin: string, - endowmentPermissionName: string, - caveatType: string, - ) => Caveat; - }, -) { - let caveat; - try { - caveat = hooks.getCaveat( - request.origin, - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - } catch (e) { - // noop - } - - if (!caveat) { - response.result = { sessionScopes: {} }; - return end(); - } - - response.result = { - sessionScopes: mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - ), - }; - return end(); -} - -export const walletGetSession = { - methodNames: ['wallet_getSession'], - implementation: walletGetSessionHandler, - hookNames: { - getCaveat: 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 208bccc337..0000000000 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; -import type { JsonRpcRequest } from '@metamask/utils'; - -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { walletInvokeMethod } from './wallet-invokeMethod'; - -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 getCaveat = jest.fn().mockReturnValue({ - value: { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - methods: ['net_version'], - notifications: [], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - unhandled: { - methods: ['foobar'], - notifications: [], - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - const findNetworkClientIdByChainId = jest.fn().mockReturnValue('mainnet'); - const getSelectedNetworkClientId = jest - .fn() - .mockReturnValue('selectedNetworkClientId'); - const handler = (request: JsonRpcRequest & { origin: string }) => - walletInvokeMethod.implementation( - request, - { jsonrpc: '2.0', id: 1 }, - next, - end, - { - getCaveat, - findNetworkClientIdByChainId, - getSelectedNetworkClientId, - }, - ); - - return { - next, - end, - getCaveat, - findNetworkClientIdByChainId, - getSelectedNetworkClientId, - handler, - }; -}; - -describe('wallet_invokeMethod', () => { - it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { - const request = createMockedRequest(); - const { handler, getCaveat } = createMockedHandler(); - await handler(request); - expect(getCaveat).toHaveBeenCalledWith( - 'http://test.com', - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - }); - - it('throws an unauthorized error when there is no CAIP-25 endowment permission', async () => { - const request = createMockedRequest(); - const { handler, getCaveat, end } = createMockedHandler(); - getCaveat.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, getCaveat, end } = createMockedHandler(); - getCaveat.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()); - }); - - it('throws an internal error for authorized but unhandled scopes', async () => { - const request = createMockedRequest(); - const { handler, end } = createMockedHandler(); - - await handler({ - ...request, - params: { - ...request.params, - scope: 'unhandled', - request: { - ...request.params.request, - method: 'foobar', - }, - }, - }); - - expect(end).toHaveBeenCalledWith(rpcErrors.internal()); - }); - - 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(); - }); - }); -}); diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts deleted file mode 100644 index 3df3ca8346..0000000000 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { Caveat } from '@metamask/permission-controller'; -import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; -import type { - Json, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; -import { numberToHex } from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import type { ScopeString } from '../scope/types'; -import { parseScopeString } from '../scope/types'; - -/** - * Handler for the `wallet_invokeMethod` RPC method. - * - * @param request - The request object. - * @param _response - The response object. - * @param next - The next middleware function. - * @param end - The end function. - * @param hooks - The hooks object. - * @param hooks.getCaveat - 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. - */ -async function walletInvokeMethodHandler( - request: JsonRpcRequest & { origin: string }, - _response: PendingJsonRpcResponse, - next: () => void, - end: (error: Error) => void, - hooks: { - getCaveat: ( - origin: string, - endowmentPermissionName: string, - caveatType: string, - ) => Caveat; - findNetworkClientIdByChainId: (chainId: string) => string | undefined; - getSelectedNetworkClientId: () => string; - }, -) { - const { scope, request: wrappedRequest } = request.params as { - scope: ScopeString; - request: JsonRpcRequest; - }; - - let caveat; - try { - caveat = hooks.getCaveat( - request.origin, - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - } catch (e) { - // noop - } - if (!caveat?.value?.isMultichainOrigin) { - return end(providerErrors.unauthorized()); - } - - const scopeObject = mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - )[scope]; - - if (!scopeObject?.methods?.includes(wrappedRequest.method)) { - return end(providerErrors.unauthorized()); - } - - const { namespace, reference } = parseScopeString(scope); - - let networkClientId; - switch (namespace) { - case 'wallet': - networkClientId = hooks.getSelectedNetworkClientId(); - break; - case 'eip155': - if (reference) { - networkClientId = hooks.findNetworkClientIdByChainId( - numberToHex(parseInt(reference, 10)), - ); - } - break; - default: - console.error( - 'failed to resolve namespace for wallet_invokeMethod', - request, - ); - return end(rpcErrors.internal()); - } - - if (!networkClientId) { - console.error( - 'failed to resolve network client for wallet_invokeMethod', - request, - ); - return end(rpcErrors.internal()); - } - - Object.assign(request, { - scope, - networkClientId, - method: wrappedRequest.method, - params: wrappedRequest.params, - }); - return next(); -} -export const walletInvokeMethod = { - methodNames: ['wallet_invokeMethod'], - implementation: walletInvokeMethodHandler, - hookNames: { - getCaveat: true, - findNetworkClientIdByChainId: true, - getSelectedNetworkClientId: true, - }, -}; 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 e11b89f42c..0000000000 --- a/packages/multichain/src/handlers/wallet-revokeSession.test.ts +++ /dev/null @@ -1,92 +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 revokePermission = 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, { - revokePermission, - }); - - return { - next, - response, - end, - revokePermission, - handler, - }; -}; - -describe('wallet_revokeSession', () => { - it('revokes the the CAIP-25 endowment permission', async () => { - const { handler, revokePermission } = createMockedHandler(); - - await handler(baseRequest); - expect(revokePermission).toHaveBeenCalledWith( - 'http://test.com', - Caip25EndowmentPermissionName, - ); - }); - - it('returns true if the CAIP-25 endowment permission does not exist', async () => { - const { handler, response, revokePermission } = createMockedHandler(); - revokePermission.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, revokePermission } = createMockedHandler(); - revokePermission.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, revokePermission, end } = createMockedHandler(); - revokePermission.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 51e31073f0..0000000000 --- a/packages/multichain/src/handlers/wallet-revokeSession.ts +++ /dev/null @@ -1,54 +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, Json, JsonRpcRequest } from '@metamask/utils'; - -import { Caip25EndowmentPermissionName } from '../caip25Permission'; - -/** - * Handles the `wallet_revokeSession` RPC method. - * - * @param request - The JSON-RPC request object. - * @param response - The JSON-RPC response object. - * @param _next - The next middleware function. - * @param end - The end callback function. - * @param hooks - The hooks object. - * @param hooks.revokePermission - The revokePermission function. - */ -async function walletRevokeSessionHandler( - request: JsonRpcRequest & { origin: string }, - response: JsonRpcSuccess, - _next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - hooks: { - revokePermission: (origin: string, permissionName: string) => void; - }, -) { - try { - hooks.revokePermission(request.origin, 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: { - revokePermission: true, - }, -}; diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index d06cd6094e..7cf4f39321 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -10,18 +10,7 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", - "walletGetSession", - "walletInvokeMethod", - "walletRevokeSession", - "multichainMethodCallValidatorMiddleware", - "MultichainMiddlewareManager", - "MultichainSubscriptionManager", - "assertScopeSupported", - "assertScopesSupported", "validateAndNormalizeScopes", - "bucketScopes", - "bucketScopesBySupport", - "filterScopesSupported", "isSupportedScopeString", "isSupportedAccount", "isSupportedMethod", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index f764ea5b3e..5c10c8f8fa 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -9,21 +9,10 @@ export { setPermittedEthChainIds, } from './adapters/caip-permission-adapter-permittedChains'; -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 { assertScopeSupported, assertScopesSupported } from './scope/assert'; export type { Caip25Authorization } from './scope/authorization'; export { validateAndNormalizeScopes, - bucketScopes, } from './scope/authorization'; -export { bucketScopesBySupport, filterScopesSupported } from './scope/filter'; export * from './scope/types'; export { isSupportedScopeString, diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts deleted file mode 100644 index 4a358896fc..0000000000 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -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', () => { - 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).toHaveBeenCalledWith( - { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, - { jsonrpc: '2.0', id: 0 }, - nextSpy, - endSpy, - ); - expect(nextSpy).not.toHaveBeenCalled(); - expect(endSpy).not.toHaveBeenCalled(); - }); - - it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed', 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 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 d1e52f9385..0000000000 --- a/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { - JsonRpcEngineEndCallback, - JsonRpcEngineNextCallback, -} from '@metamask/json-rpc-engine'; -import type { - Json, - 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; -}; - -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 }: MiddlewareKey) { - 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(middlewareKey: MiddlewareKey) { - const existingMiddlewareEntry = this.#getMiddlewareEntry(middlewareKey); - if (!existingMiddlewareEntry) { - return; - } - - // When the destroy function on the middleware is async, - // we don't need to wait for it complete - // eslint-disable-next-line no-void - void existingMiddlewareEntry.middleware.destroy?.(); - - this.#removeMiddlewareEntry(middlewareKey); - } - - 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 (middlewareEntry) { - middlewareEntry.middleware(req, res, next, end); - } 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 cf05aa5c5a..0000000000 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; - -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 }; -}; - -describe('MultichainSubscriptionManager', () => { - const mockSubscriptionManager = { - events: { - on: jest.fn(), - }, - destroy: jest.fn(), - }; - - beforeEach(() => { - MockCreateSubscriptionManager.mockReturnValue(mockSubscriptionManager); - }); - - it('should subscribe to a scope, origin, and tabId', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - const onNotificationSpy = jest.fn(); - multichainSubscriptionManager.on('notification', onNotificationSpy); - - mockSubscriptionManager.events.on.mock.calls[0][1]( - newHeadsNotificationMock, - ); - - expect(onNotificationSpy).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); - - mockSubscriptionManager.events.on.mock.calls[0][1]( - newHeadsNotificationMock, - ); - - expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); - }); - - it('should unsubscribe from a origin and tabId', () => { - const { multichainSubscriptionManager } = - createMultichainSubscriptionManager(); - multichainSubscriptionManager.subscribe({ scope, origin, tabId }); - multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); - - mockSubscriptionManager.events.on.mock.calls[0][1]( - newHeadsNotificationMock, - ); - - 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 6494ed24a5..0000000000 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ /dev/null @@ -1,160 +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 EventEmitter from 'events'; - -import type { ExternalScopeString } from '../scope/types'; - -export type SubscriptionManager = { - events: EventEmitter; - destroy?: () => void; -}; - -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']; -}; - -export class MultichainSubscriptionManager extends SafeEventEmitter { - #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; - - #getNetworkClientById: NetworkController['getNetworkClientById']; - - #subscriptions: SubscriptionEntry[] = []; - - constructor(options: MultichainSubscriptionManagerOptions) { - super(); - this.#findNetworkClientIdByChainId = options.findNetworkClientIdByChainId; - this.#getNetworkClientById = options.getNetworkClientById; - } - - onNotification( - { 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 }: SubscriptionKey) { - 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.onNotification(subscriptionKey, message); - }, - ); - - this.#subscriptions.push({ - ...subscriptionKey, - subscriptionManager, - }); - - return subscriptionManager; - } - - #unsubscribe(subscriptionKey: SubscriptionKey) { - const existingSubscriptionEntry = - this.#getSubscriptionEntry(subscriptionKey); - if (!existingSubscriptionEntry) { - return; - } - - existingSubscriptionEntry.subscriptionManager.destroy?.(); - - this.#removeSubscriptionEntry(subscriptionKey); - } - - 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.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts deleted file mode 100644 index d62b2328eb..0000000000 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { MultiChainOpenRPCDocument } from '@metamask/api-specs'; -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { rpcErrors } from '@metamask/rpc-errors'; -import { isObject } from '@metamask/utils'; -import type { - Json, - JsonRpcError, - JsonRpcParams, - JsonRpcRequest, -} from '@metamask/utils'; -import type { - ContentDescriptorObject, - MethodObject, - OpenrpcDocument, -} 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 { - code: -32602, // TODO: could be a different error code or not wrapped in json-rpc error, since this will also be wrapped in a -32602 invalid params error - 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({}), -); -const multichainMethodCallValidator = async ( - method: string, - params: JsonRpcParams | undefined, -) => { - const dereffed = await dereffedPromise; - const methodToCheck = dereffed.methods.find( - (m) => (m as unknown as ContentDescriptorObject).name === method, - ); - const errors: JsonRpcError[] = []; - // check each param and aggregate errors - (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { - let paramToCheck: Json | undefined; - const p = param as ContentDescriptorObject; - if (isObject(params)) { - paramToCheck = params[p.name]; - } else if (params && Array.isArray(params)) { - paramToCheck = params[i]; - } else { - paramToCheck = undefined; - } - 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; - }), - ); - } - }); - if (errors.length > 0) { - return errors; - } - // feels like this should return true to indicate that its valid but i'd rather check the falsy value since errors - // would be an array and return true if it's empty - return false; -}; - -export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< - JsonRpcRequest, - Json -> = function (request, _response, next, end) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - multichainMethodCallValidator(request.method, request.params).then( - (errors) => { - if (errors) { - return end(rpcErrors.invalidParams({ data: errors })); - } - return next(); - }, - ); -}; diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 4c8bc65e7f..55a184b364 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,5 +1,4 @@ -import { bucketScopes, validateAndNormalizeScopes } from './authorization'; -import * as Filter from './filter'; +import { validateAndNormalizeScopes } from './authorization'; import * as Transform from './transform'; import type { ExternalScopeObject } from './types'; import * as Validation from './validation'; @@ -14,11 +13,6 @@ jest.mock('./transform', () => ({ })); const MockTransform = jest.mocked(Transform); -jest.mock('./filter', () => ({ - bucketScopesBySupport: jest.fn(), -})); -const MockFilter = jest.mocked(Filter); - const validScopeObject: ExternalScopeObject = { methods: [], notifications: [], @@ -98,129 +92,4 @@ describe('Scope Authorization', () => { }); }); }); - - describe('bucketScopes', () => { - 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: [], - }, - }, - { - isChainIdSupported, - isChainIdSupportable: jest.fn(), - }, - ); - - expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isChainIdSupported, - }, - ); - }); - - it('buckets the mayble supportable scopes', () => { - const isChainIdSupportable = jest.fn(); - bucketScopes( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isChainIdSupported: jest.fn(), - isChainIdSupportable, - }, - ); - - expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( - { - 'mock:B': { - methods: [`mock_method_1`], - notifications: [], - accounts: [], - }, - }, - { - isChainIdSupported: isChainIdSupportable, - }, - ); - }); - - it('returns the bucketed scopes', () => { - expect( - bucketScopes( - { - wallet: { - methods: [], - notifications: [], - accounts: [], - }, - }, - { - isChainIdSupported: jest.fn(), - isChainIdSupportable: jest.fn(), - }, - ), - ).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 index 3dcbef7e40..c7b98a8357 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,6 +1,3 @@ -import type { Hex } from '@metamask/utils'; - -import { bucketScopesBySupport } from './filter'; import { normalizeAndMergeScopes } from './transform'; import type { ExternalScopesObject, ScopesObject } from './types'; import { validateScopes } from './validation'; @@ -38,32 +35,3 @@ export const validateAndNormalizeScopes = ( normalizedOptionalScopes, }; }; - -export const bucketScopes = ( - scopes: ScopesObject, - { - isChainIdSupported, - isChainIdSupportable, - }: { - isChainIdSupported: (chainId: Hex) => boolean; - isChainIdSupportable: (chainId: Hex) => boolean; - }, -): { - supportedScopes: ScopesObject; - supportableScopes: ScopesObject; - unsupportableScopes: ScopesObject; -} => { - const { supportedScopes, unsupportedScopes: maybeSupportableScopes } = - bucketScopesBySupport(scopes, { - isChainIdSupported, - }); - - const { - supportedScopes: supportableScopes, - unsupportedScopes: unsupportableScopes, - } = bucketScopesBySupport(maybeSupportableScopes, { - isChainIdSupported: isChainIdSupportable, - }); - - return { supportedScopes, supportableScopes, unsupportableScopes }; -}; diff --git a/packages/multichain/src/scope/filter.test.ts b/packages/multichain/src/scope/filter.test.ts deleted file mode 100644 index c49c739786..0000000000 --- a/packages/multichain/src/scope/filter.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import * as Assert from './assert'; -import { filterScopesSupported, bucketScopesBySupport } from './filter'; - -jest.mock('./assert', () => ({ - assertScopeSupported: jest.fn(), -})); -const MockAssert = jest.mocked(Assert); - -describe('filter', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - describe('filterScopesSupported', () => { - const isChainIdSupported = jest.fn(); - - it('checks if each scope is supported', () => { - filterScopesSupported( - { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - { isChainIdSupported }, - ); - - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:1', - { - methods: ['a'], - notifications: [], - accounts: [], - }, - { isChainIdSupported }, - ); - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:5', - { - methods: ['b'], - notifications: [], - accounts: [], - }, - { isChainIdSupported }, - ); - }); - - it('returns only supported scopes', () => { - MockAssert.assertScopeSupported.mockImplementation((scopeString) => { - if (scopeString === 'eip155:1') { - throw new Error('scope not supported'); - } - }); - - expect( - filterScopesSupported( - { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - { isChainIdSupported }, - ), - ).toStrictEqual({ - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }); - }); - }); - - describe('bucketScopesBySupport', () => { - const isChainIdSupported = jest.fn(); - - it('checks if each scope is supported', () => { - bucketScopesBySupport( - { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - { isChainIdSupported }, - ); - - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:1', - { - methods: ['a'], - notifications: [], - accounts: [], - }, - { isChainIdSupported }, - ); - expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( - 'eip155:5', - { - methods: ['b'], - notifications: [], - accounts: [], - }, - { isChainIdSupported }, - ); - }); - - 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: [], - }, - }, - { isChainIdSupported }, - ), - ).toStrictEqual({ - supportedScopes: { - 'eip155:5': { - methods: ['b'], - notifications: [], - accounts: [], - }, - }, - unsupportedScopes: { - 'eip155:1': { - methods: ['a'], - notifications: [], - accounts: [], - }, - }, - }); - }); - }); -}); diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts deleted file mode 100644 index 58157bd602..0000000000 --- a/packages/multichain/src/scope/filter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CaipChainId, Hex } from '@metamask/utils'; - -import { assertScopeSupported } from './assert'; -import type { ScopesObject } from './types'; - -export const bucketScopesBySupport = ( - scopes: ScopesObject, - { - isChainIdSupported, - }: { - isChainIdSupported: (chainId: Hex) => boolean; - }, -) => { - const supportedScopes: ScopesObject = {}; - const unsupportedScopes: ScopesObject = {}; - - for (const [scopeString, scopeObject] of Object.entries(scopes)) { - try { - assertScopeSupported(scopeString, scopeObject, { - isChainIdSupported, - }); - supportedScopes[scopeString as CaipChainId] = scopeObject; - } catch (err) { - unsupportedScopes[scopeString as CaipChainId] = scopeObject; - } - } - - return { supportedScopes, unsupportedScopes }; -}; - -export const filterScopesSupported = ( - scopes: ScopesObject, - { - isChainIdSupported, - }: { - isChainIdSupported: (chainId: Hex) => boolean; - }, -) => { - const { supportedScopes } = bucketScopesBySupport(scopes, { - isChainIdSupported, - }); - - return supportedScopes; -}; From 4ceb11850f8bc7340290940204f3e2a4a4f30db9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:12:11 -0700 Subject: [PATCH 39/80] remove api related deps --- packages/multichain/package.json | 5 -- yarn.lock | 123 +------------------------------ 2 files changed, 3 insertions(+), 125 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 044b203f0b..313c94a52f 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -51,18 +51,13 @@ "@metamask/controller-utils": "^11.3.0", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.0", - "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^9.1.0", - "@open-rpc/schema-utils-js": "^2.0.5", - "jsonschema": "^1.2.4", "lodash": "^4.17.21" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/json-rpc-engine": "^9.0.3", "@metamask/network-controller": "^21.0.1", "@metamask/permission-controller": "^11.0.2", - "@open-rpc/meta-schema": "^1.14.6", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/yarn.lock b/yarn.lock index 31ab2d0d9e..b16e5d8f3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1931,48 +1931,6 @@ __metadata: languageName: node linkType: hard -"@json-schema-spec/json-pointer@npm:^0.1.2": - version: 0.1.2 - resolution: "@json-schema-spec/json-pointer@npm:0.1.2" - checksum: 10/2a691ffc11f1a266ca4d0c9e2c99791679d580f343ef69746fad623d1abcf4953adde987890e41f906767d7729604c0182341e9012388b73a44d5b21fb296453 - languageName: node - linkType: hard - -"@json-schema-tools/dereferencer@npm:^1.6.3": - version: 1.6.3 - resolution: "@json-schema-tools/dereferencer@npm:1.6.3" - dependencies: - "@json-schema-tools/reference-resolver": "npm:^1.2.6" - "@json-schema-tools/traverse": "npm:^1.10.4" - fast-safe-stringify: "npm:^2.1.1" - checksum: 10/da6ef5b82a8a9c3a7e62ffcab5c04c581f1e0f8165c0debdb272bb1e08ccd726107ee194487b8fa736cac00fb390b8df74bc1ad1b200eddbe25c98ee0d3d000b - languageName: node - linkType: hard - -"@json-schema-tools/meta-schema@npm:^1.7.5": - version: 1.7.5 - resolution: "@json-schema-tools/meta-schema@npm:1.7.5" - checksum: 10/707dc3a285c26c37d00f418e9d0ef8a2ad1c23d4936ad5aab0ce94c9ae36a7a6125c4ca5048513af64b7e6e527b5472a1701d1f709c379acdd7ad12f6409d2cd - languageName: node - linkType: hard - -"@json-schema-tools/reference-resolver@npm:^1.2.6": - version: 1.2.6 - resolution: "@json-schema-tools/reference-resolver@npm:1.2.6" - dependencies: - "@json-schema-spec/json-pointer": "npm:^0.1.2" - isomorphic-fetch: "npm:^3.0.0" - checksum: 10/91d6b4b2ac43f8163fd27bde6d826f29f339e9c7ce3b7e2b73b85e891fa78e3702fd487deda143a0701879cbc2fe28c53a4efce4cd2d2dd2fe6e82b64bbd9c9c - languageName: node - linkType: hard - -"@json-schema-tools/traverse@npm:^1.10.4": - version: 1.10.4 - resolution: "@json-schema-tools/traverse@npm:1.10.4" - checksum: 10/0027bc90df01c5eeee0833e722b7320b53be8b5ce3f4e0e4a6e45713a38e6f88f21aba31e3dd973093ef75cd21a40c07fe8f112da8f49a7919b1c0e44c904d20 - languageName: node - linkType: hard - "@keystonehq/alias-sampling@npm:^0.1.1": version: 0.1.2 resolution: "@keystonehq/alias-sampling@npm:0.1.2" @@ -3102,18 +3060,13 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" "@metamask/rpc-errors": "npm:^7.0.0" - "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^9.1.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.2.4" lodash: "npm:^4.17.21" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" @@ -4075,31 +4028,6 @@ __metadata: languageName: node linkType: hard -"@open-rpc/meta-schema@npm:^1.14.6, @open-rpc/meta-schema@npm:^1.14.9": - version: 1.14.9 - resolution: "@open-rpc/meta-schema@npm:1.14.9" - checksum: 10/51505dcf7aa1a2285c78953c9b33711cede5f2765aa37dcb9ee7756d689e2ff2a89cfc6039504f0569c52a805fb9aa18f30a7c02ad7a06e793c801e43b419104 - languageName: node - linkType: hard - -"@open-rpc/schema-utils-js@npm:^2.0.5": - version: 2.0.5 - resolution: "@open-rpc/schema-utils-js@npm:2.0.5" - dependencies: - "@json-schema-tools/dereferencer": "npm:^1.6.3" - "@json-schema-tools/meta-schema": "npm:^1.7.5" - "@json-schema-tools/reference-resolver": "npm:^1.2.6" - "@open-rpc/meta-schema": "npm:^1.14.9" - ajv: "npm:^6.10.0" - detect-node: "npm:^2.0.4" - fast-safe-stringify: "npm:^2.0.7" - fs-extra: "npm:^10.1.0" - is-url: "npm:^1.2.4" - isomorphic-fetch: "npm:^3.0.0" - checksum: 10/9e10215606e9a00a47b082c9cfd70d05bf0d38de6cf1c147246c545c6997375d94cd3caafe919b71178df58b5facadfd0dcc8b6857bf5e79c40e5e33683dd3d5 - languageName: node - linkType: hard - "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -5040,7 +4968,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.4": +"ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6436,13 +6364,6 @@ __metadata: languageName: node linkType: hard -"detect-node@npm:^2.0.4": - version: 2.1.0 - resolution: "detect-node@npm:2.1.0" - checksum: 10/832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e - languageName: node - linkType: hard - "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -7451,7 +7372,7 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:^2.0.6, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": +"fast-safe-stringify@npm:^2.0.6": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 @@ -7691,17 +7612,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^10.1.0": - version: 10.1.0 - resolution: "fs-extra@npm:10.1.0" - dependencies: - graceful-fs: "npm:^4.2.0" - jsonfile: "npm:^6.0.1" - universalify: "npm:^2.0.0" - checksum: 10/05ce2c3b59049bcb7b52001acd000e44b3c4af4ec1f8839f383ef41ec0048e3cfa7fd8a637b1bddfefad319145db89be91f4b7c1db2908205d38bf91e7d1d3b7 - languageName: node - linkType: hard - "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -7995,7 +7905,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -8671,13 +8581,6 @@ __metadata: languageName: node linkType: hard -"is-url@npm:^1.2.4": - version: 1.2.4 - resolution: "is-url@npm:1.2.4" - checksum: 10/100e74b3b1feab87a43ef7653736e88d997eb7bd32e71fd3ebc413e58c1cbe56269699c776aaea84244b0567f2a7d68dfaa512a062293ed2f9fdecb394148432 - languageName: node - linkType: hard - "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -9614,19 +9517,6 @@ __metadata: languageName: node linkType: hard -"jsonfile@npm:^6.0.1": - version: 6.1.0 - resolution: "jsonfile@npm:6.1.0" - dependencies: - graceful-fs: "npm:^4.1.6" - universalify: "npm:^2.0.0" - dependenciesMeta: - graceful-fs: - optional: true - checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 - languageName: node - linkType: hard - "jsonschema@npm:^1.2.4": version: 1.4.1 resolution: "jsonschema@npm:1.4.1" @@ -12454,13 +12344,6 @@ __metadata: languageName: node linkType: hard -"universalify@npm:^2.0.0": - version: 2.0.1 - resolution: "universalify@npm:2.0.1" - checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.1.0": version: 1.1.0 resolution: "update-browserslist-db@npm:1.1.0" From a36ec4d1c6fc11e23bf13f36a43d8f9501403758 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:12:44 -0700 Subject: [PATCH 40/80] Revert "remove API related logic" This reverts commit fc28c89c17de6e9593c6551cd159ebd10587489c. --- .../src/handlers/wallet-getSession.test.ts | 117 ++++++++ .../src/handlers/wallet-getSession.ts | 66 ++++ .../src/handlers/wallet-invokeMethod.test.ts | 282 ++++++++++++++++++ .../src/handlers/wallet-invokeMethod.ts | 120 ++++++++ .../src/handlers/wallet-revokeSession.test.ts | 92 ++++++ .../src/handlers/wallet-revokeSession.ts | 54 ++++ packages/multichain/src/index.test.ts | 11 + packages/multichain/src/index.ts | 11 + .../MultichainMiddlewareManager.test.ts | 171 +++++++++++ .../MultichainMiddlewareManager.ts | 137 +++++++++ .../MultichainSubscriptionManager.test.ts | 120 ++++++++ .../MultichainSubscriptionManager.ts | 160 ++++++++++ .../multichainMethodCallValidator.ts | 101 +++++++ .../src/scope/authorization.test.ts | 133 ++++++++- .../multichain/src/scope/authorization.ts | 32 ++ packages/multichain/src/scope/filter.test.ts | 168 +++++++++++ packages/multichain/src/scope/filter.ts | 44 +++ 17 files changed, 1818 insertions(+), 1 deletion(-) create mode 100644 packages/multichain/src/handlers/wallet-getSession.test.ts create mode 100644 packages/multichain/src/handlers/wallet-getSession.ts create mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.test.ts create mode 100644 packages/multichain/src/handlers/wallet-invokeMethod.ts create mode 100644 packages/multichain/src/handlers/wallet-revokeSession.test.ts create mode 100644 packages/multichain/src/handlers/wallet-revokeSession.ts create mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts create mode 100644 packages/multichain/src/middlewares/MultichainMiddlewareManager.ts create mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts create mode 100644 packages/multichain/src/middlewares/MultichainSubscriptionManager.ts create mode 100644 packages/multichain/src/middlewares/multichainMethodCallValidator.ts create mode 100644 packages/multichain/src/scope/filter.test.ts create mode 100644 packages/multichain/src/scope/filter.ts diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts new file mode 100644 index 0000000000..ca74cc9b90 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -0,0 +1,117 @@ +import type { JsonRpcRequest } from '@metamask/utils'; + +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { walletGetSession } from './wallet-getSession'; + +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 getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + accounts: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: ['chainChanged'], + accounts: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + accounts: [], + }, + }, + }, + }); + const response = { + result: { + sessionScopes: {}, + }, + id: 1, + jsonrpc: '2.0' as const, + }; + const handler = (request: JsonRpcRequest & { origin: string }) => + walletGetSession.implementation(request, response, next, end, { + getCaveat, + }); + + return { + next, + response, + end, + getCaveat, + handler, + }; +}; + +describe('wallet_getSession', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const { handler, getCaveat } = createMockedHandler(); + + await handler(baseRequest); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('returns empty scopes if the CAIP-25 endowment permission does not exist', async () => { + const { handler, response, getCaveat } = createMockedHandler(); + getCaveat.mockImplementation(() => { + throw new Error('permission not found'); + }); + + await handler(baseRequest); + expect(response.result).toStrictEqual({ + sessionScopes: {}, + }); + }); + + it('returns the merged scopes', async () => { + const { handler, response } = createMockedHandler(); + + 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 new file mode 100644 index 0000000000..ad3e4e5569 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -0,0 +1,66 @@ +import type { Caveat } from '@metamask/permission-controller'; +import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; +import type { ScopesObject } from '../scope/types'; + +/** + * Handler for the `wallet_getSession` RPC method. + * + * @param request - The request object. + * @param response - The response object. + * @param _next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - Function to retrieve a caveat. + */ +async function walletGetSessionHandler( + request: JsonRpcRequest & { origin: string }, + response: JsonRpcSuccess<{ sessionScopes: ScopesObject }>, + _next: () => void, + end: () => void, + hooks: { + getCaveat: ( + origin: string, + endowmentPermissionName: string, + caveatType: string, + ) => Caveat; + }, +) { + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (e) { + // noop + } + + if (!caveat) { + response.result = { sessionScopes: {} }; + return end(); + } + + response.result = { + sessionScopes: mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + ), + }; + return end(); +} + +export const walletGetSession = { + methodNames: ['wallet_getSession'], + implementation: walletGetSessionHandler, + hookNames: { + getCaveat: true, + }, +}; diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts new file mode 100644 index 0000000000..208bccc337 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -0,0 +1,282 @@ +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { walletInvokeMethod } from './wallet-invokeMethod'; + +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 getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + accounts: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: [], + accounts: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + accounts: [], + }, + unhandled: { + methods: ['foobar'], + notifications: [], + accounts: [], + }, + }, + isMultichainOrigin: true, + }, + }); + const findNetworkClientIdByChainId = jest.fn().mockReturnValue('mainnet'); + const getSelectedNetworkClientId = jest + .fn() + .mockReturnValue('selectedNetworkClientId'); + const handler = (request: JsonRpcRequest & { origin: string }) => + walletInvokeMethod.implementation( + request, + { jsonrpc: '2.0', id: 1 }, + next, + end, + { + getCaveat, + findNetworkClientIdByChainId, + getSelectedNetworkClientId, + }, + ); + + return { + next, + end, + getCaveat, + findNetworkClientIdByChainId, + getSelectedNetworkClientId, + handler, + }; +}; + +describe('wallet_invokeMethod', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const request = createMockedRequest(); + const { handler, getCaveat } = createMockedHandler(); + await handler(request); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('throws an unauthorized error when there is no CAIP-25 endowment permission', async () => { + const request = createMockedRequest(); + const { handler, getCaveat, end } = createMockedHandler(); + getCaveat.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, getCaveat, end } = createMockedHandler(); + getCaveat.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()); + }); + + it('throws an internal error for authorized but unhandled scopes', async () => { + const request = createMockedRequest(); + const { handler, end } = createMockedHandler(); + + await handler({ + ...request, + params: { + ...request.params, + scope: 'unhandled', + request: { + ...request.params.request, + method: 'foobar', + }, + }, + }); + + expect(end).toHaveBeenCalledWith(rpcErrors.internal()); + }); + + 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(); + }); + }); +}); diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts new file mode 100644 index 0000000000..3df3ca8346 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -0,0 +1,120 @@ +import type { Caveat } from '@metamask/permission-controller'; +import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import type { + Json, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; +import { numberToHex } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; +import type { ScopeString } from '../scope/types'; +import { parseScopeString } from '../scope/types'; + +/** + * Handler for the `wallet_invokeMethod` RPC method. + * + * @param request - The request object. + * @param _response - The response object. + * @param next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - 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. + */ +async function walletInvokeMethodHandler( + request: JsonRpcRequest & { origin: string }, + _response: PendingJsonRpcResponse, + next: () => void, + end: (error: Error) => void, + hooks: { + getCaveat: ( + origin: string, + endowmentPermissionName: string, + caveatType: string, + ) => Caveat; + findNetworkClientIdByChainId: (chainId: string) => string | undefined; + getSelectedNetworkClientId: () => string; + }, +) { + const { scope, request: wrappedRequest } = request.params as { + scope: ScopeString; + request: JsonRpcRequest; + }; + + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (e) { + // noop + } + if (!caveat?.value?.isMultichainOrigin) { + return end(providerErrors.unauthorized()); + } + + const scopeObject = mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + )[scope]; + + if (!scopeObject?.methods?.includes(wrappedRequest.method)) { + return end(providerErrors.unauthorized()); + } + + const { namespace, reference } = parseScopeString(scope); + + let networkClientId; + switch (namespace) { + case 'wallet': + networkClientId = hooks.getSelectedNetworkClientId(); + break; + case 'eip155': + if (reference) { + networkClientId = hooks.findNetworkClientIdByChainId( + numberToHex(parseInt(reference, 10)), + ); + } + break; + default: + console.error( + 'failed to resolve namespace for wallet_invokeMethod', + request, + ); + return end(rpcErrors.internal()); + } + + if (!networkClientId) { + console.error( + 'failed to resolve network client for wallet_invokeMethod', + request, + ); + return end(rpcErrors.internal()); + } + + Object.assign(request, { + scope, + networkClientId, + method: wrappedRequest.method, + params: wrappedRequest.params, + }); + return next(); +} +export const walletInvokeMethod = { + methodNames: ['wallet_invokeMethod'], + implementation: walletInvokeMethodHandler, + hookNames: { + getCaveat: true, + findNetworkClientIdByChainId: true, + getSelectedNetworkClientId: true, + }, +}; diff --git a/packages/multichain/src/handlers/wallet-revokeSession.test.ts b/packages/multichain/src/handlers/wallet-revokeSession.test.ts new file mode 100644 index 0000000000..e11b89f42c --- /dev/null +++ b/packages/multichain/src/handlers/wallet-revokeSession.test.ts @@ -0,0 +1,92 @@ +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 revokePermission = 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, { + revokePermission, + }); + + return { + next, + response, + end, + revokePermission, + handler, + }; +}; + +describe('wallet_revokeSession', () => { + it('revokes the the CAIP-25 endowment permission', async () => { + const { handler, revokePermission } = createMockedHandler(); + + await handler(baseRequest); + expect(revokePermission).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + ); + }); + + it('returns true if the CAIP-25 endowment permission does not exist', async () => { + const { handler, response, revokePermission } = createMockedHandler(); + revokePermission.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, revokePermission } = createMockedHandler(); + revokePermission.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, revokePermission, end } = createMockedHandler(); + revokePermission.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 new file mode 100644 index 0000000000..51e31073f0 --- /dev/null +++ b/packages/multichain/src/handlers/wallet-revokeSession.ts @@ -0,0 +1,54 @@ +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, Json, JsonRpcRequest } from '@metamask/utils'; + +import { Caip25EndowmentPermissionName } from '../caip25Permission'; + +/** + * Handles the `wallet_revokeSession` RPC method. + * + * @param request - The JSON-RPC request object. + * @param response - The JSON-RPC response object. + * @param _next - The next middleware function. + * @param end - The end callback function. + * @param hooks - The hooks object. + * @param hooks.revokePermission - The revokePermission function. + */ +async function walletRevokeSessionHandler( + request: JsonRpcRequest & { origin: string }, + response: JsonRpcSuccess, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + hooks: { + revokePermission: (origin: string, permissionName: string) => void; + }, +) { + try { + hooks.revokePermission(request.origin, 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: { + revokePermission: true, + }, +}; diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 7cf4f39321..d06cd6094e 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -10,7 +10,18 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", + "walletGetSession", + "walletInvokeMethod", + "walletRevokeSession", + "multichainMethodCallValidatorMiddleware", + "MultichainMiddlewareManager", + "MultichainSubscriptionManager", + "assertScopeSupported", + "assertScopesSupported", "validateAndNormalizeScopes", + "bucketScopes", + "bucketScopesBySupport", + "filterScopesSupported", "isSupportedScopeString", "isSupportedAccount", "isSupportedMethod", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 47a6b90661..f46f4c97c7 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -10,10 +10,21 @@ export { setPermittedEthChainIds, } from './adapters/caip-permission-adapter-permittedChains'; +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 { assertScopeSupported, assertScopesSupported } from './scope/assert'; export type { Caip25Authorization } from './scope/authorization'; export { validateAndNormalizeScopes, + bucketScopes, } from './scope/authorization'; +export { bucketScopesBySupport, filterScopesSupported } from './scope/filter'; export * from './scope/types'; export { isSupportedScopeString, diff --git a/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts new file mode 100644 index 0000000000..4a358896fc --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.test.ts @@ -0,0 +1,171 @@ +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', () => { + 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).toHaveBeenCalledWith( + { jsonrpc: '2.0' as const, id: 0, method: 'method', scope }, + { jsonrpc: '2.0', id: 0 }, + nextSpy, + endSpy, + ); + expect(nextSpy).not.toHaveBeenCalled(); + expect(endSpy).not.toHaveBeenCalled(); + }); + + it('should remove middleware by origin and tabId when the multiplexing middleware is destroyed', 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 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 new file mode 100644 index 0000000000..d1e52f9385 --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainMiddlewareManager.ts @@ -0,0 +1,137 @@ +import type { + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, +} from '@metamask/json-rpc-engine'; +import type { + Json, + 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; +}; + +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 }: MiddlewareKey) { + 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(middlewareKey: MiddlewareKey) { + const existingMiddlewareEntry = this.#getMiddlewareEntry(middlewareKey); + if (!existingMiddlewareEntry) { + return; + } + + // When the destroy function on the middleware is async, + // we don't need to wait for it complete + // eslint-disable-next-line no-void + void existingMiddlewareEntry.middleware.destroy?.(); + + this.#removeMiddlewareEntry(middlewareKey); + } + + 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 (middlewareEntry) { + middlewareEntry.middleware(req, res, next, end); + } 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 new file mode 100644 index 0000000000..cf05aa5c5a --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -0,0 +1,120 @@ +import createSubscriptionManager from '@metamask/eth-json-rpc-filters/subscriptionManager'; + +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 }; +}; + +describe('MultichainSubscriptionManager', () => { + const mockSubscriptionManager = { + events: { + on: jest.fn(), + }, + destroy: jest.fn(), + }; + + beforeEach(() => { + MockCreateSubscriptionManager.mockReturnValue(mockSubscriptionManager); + }); + + it('should subscribe to a scope, origin, and tabId', () => { + const { multichainSubscriptionManager } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + const onNotificationSpy = jest.fn(); + multichainSubscriptionManager.on('notification', onNotificationSpy); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(onNotificationSpy).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); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); + }); + + it('should unsubscribe from a origin and tabId', () => { + const { multichainSubscriptionManager } = + createMultichainSubscriptionManager(); + multichainSubscriptionManager.subscribe({ scope, origin, tabId }); + multichainSubscriptionManager.unsubscribeByOriginAndTabId(origin, tabId); + + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(mockSubscriptionManager.destroy).toHaveBeenCalled(); + }); +}); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts new file mode 100644 index 0000000000..6494ed24a5 --- /dev/null +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -0,0 +1,160 @@ +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 EventEmitter from 'events'; + +import type { ExternalScopeString } from '../scope/types'; + +export type SubscriptionManager = { + events: EventEmitter; + destroy?: () => void; +}; + +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']; +}; + +export class MultichainSubscriptionManager extends SafeEventEmitter { + #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + + #getNetworkClientById: NetworkController['getNetworkClientById']; + + #subscriptions: SubscriptionEntry[] = []; + + constructor(options: MultichainSubscriptionManagerOptions) { + super(); + this.#findNetworkClientIdByChainId = options.findNetworkClientIdByChainId; + this.#getNetworkClientById = options.getNetworkClientById; + } + + onNotification( + { 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 }: SubscriptionKey) { + 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.onNotification(subscriptionKey, message); + }, + ); + + this.#subscriptions.push({ + ...subscriptionKey, + subscriptionManager, + }); + + return subscriptionManager; + } + + #unsubscribe(subscriptionKey: SubscriptionKey) { + const existingSubscriptionEntry = + this.#getSubscriptionEntry(subscriptionKey); + if (!existingSubscriptionEntry) { + return; + } + + existingSubscriptionEntry.subscriptionManager.destroy?.(); + + this.#removeSubscriptionEntry(subscriptionKey); + } + + 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.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts new file mode 100644 index 0000000000..d62b2328eb --- /dev/null +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -0,0 +1,101 @@ +import { MultiChainOpenRPCDocument } from '@metamask/api-specs'; +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import { rpcErrors } from '@metamask/rpc-errors'; +import { isObject } from '@metamask/utils'; +import type { + Json, + JsonRpcError, + JsonRpcParams, + JsonRpcRequest, +} from '@metamask/utils'; +import type { + ContentDescriptorObject, + MethodObject, + OpenrpcDocument, +} 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 { + code: -32602, // TODO: could be a different error code or not wrapped in json-rpc error, since this will also be wrapped in a -32602 invalid params error + 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({}), +); +const multichainMethodCallValidator = async ( + method: string, + params: JsonRpcParams | undefined, +) => { + const dereffed = await dereffedPromise; + const methodToCheck = dereffed.methods.find( + (m) => (m as unknown as ContentDescriptorObject).name === method, + ); + const errors: JsonRpcError[] = []; + // check each param and aggregate errors + (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { + let paramToCheck: Json | undefined; + const p = param as ContentDescriptorObject; + if (isObject(params)) { + paramToCheck = params[p.name]; + } else if (params && Array.isArray(params)) { + paramToCheck = params[i]; + } else { + paramToCheck = undefined; + } + 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; + }), + ); + } + }); + if (errors.length > 0) { + return errors; + } + // feels like this should return true to indicate that its valid but i'd rather check the falsy value since errors + // would be an array and return true if it's empty + return false; +}; + +export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< + JsonRpcRequest, + Json +> = function (request, _response, next, end) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + multichainMethodCallValidator(request.method, request.params).then( + (errors) => { + if (errors) { + return end(rpcErrors.invalidParams({ data: errors })); + } + return next(); + }, + ); +}; diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 55a184b364..4c8bc65e7f 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,4 +1,5 @@ -import { validateAndNormalizeScopes } from './authorization'; +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'; @@ -13,6 +14,11 @@ jest.mock('./transform', () => ({ })); const MockTransform = jest.mocked(Transform); +jest.mock('./filter', () => ({ + bucketScopesBySupport: jest.fn(), +})); +const MockFilter = jest.mocked(Filter); + const validScopeObject: ExternalScopeObject = { methods: [], notifications: [], @@ -92,4 +98,129 @@ describe('Scope Authorization', () => { }); }); }); + + describe('bucketScopes', () => { + 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: [], + }, + }, + { + isChainIdSupported, + isChainIdSupportable: jest.fn(), + }, + ); + + expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( + { + wallet: { + methods: [], + notifications: [], + accounts: [], + }, + }, + { + isChainIdSupported, + }, + ); + }); + + it('buckets the mayble supportable scopes', () => { + const isChainIdSupportable = jest.fn(); + bucketScopes( + { + wallet: { + methods: [], + notifications: [], + accounts: [], + }, + }, + { + isChainIdSupported: jest.fn(), + isChainIdSupportable, + }, + ); + + expect(MockFilter.bucketScopesBySupport).toHaveBeenCalledWith( + { + 'mock:B': { + methods: [`mock_method_1`], + notifications: [], + accounts: [], + }, + }, + { + isChainIdSupported: isChainIdSupportable, + }, + ); + }); + + it('returns the bucketed scopes', () => { + expect( + bucketScopes( + { + wallet: { + methods: [], + notifications: [], + accounts: [], + }, + }, + { + isChainIdSupported: jest.fn(), + isChainIdSupportable: jest.fn(), + }, + ), + ).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 index c7b98a8357..3dcbef7e40 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,3 +1,6 @@ +import type { Hex } from '@metamask/utils'; + +import { bucketScopesBySupport } from './filter'; import { normalizeAndMergeScopes } from './transform'; import type { ExternalScopesObject, ScopesObject } from './types'; import { validateScopes } from './validation'; @@ -35,3 +38,32 @@ export const validateAndNormalizeScopes = ( normalizedOptionalScopes, }; }; + +export const bucketScopes = ( + scopes: ScopesObject, + { + isChainIdSupported, + isChainIdSupportable, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + isChainIdSupportable: (chainId: Hex) => boolean; + }, +): { + supportedScopes: ScopesObject; + supportableScopes: ScopesObject; + unsupportableScopes: ScopesObject; +} => { + const { supportedScopes, unsupportedScopes: maybeSupportableScopes } = + bucketScopesBySupport(scopes, { + isChainIdSupported, + }); + + const { + supportedScopes: supportableScopes, + unsupportedScopes: unsupportableScopes, + } = bucketScopesBySupport(maybeSupportableScopes, { + isChainIdSupported: isChainIdSupportable, + }); + + return { supportedScopes, supportableScopes, unsupportableScopes }; +}; diff --git a/packages/multichain/src/scope/filter.test.ts b/packages/multichain/src/scope/filter.test.ts new file mode 100644 index 0000000000..c49c739786 --- /dev/null +++ b/packages/multichain/src/scope/filter.test.ts @@ -0,0 +1,168 @@ +import * as Assert from './assert'; +import { filterScopesSupported, bucketScopesBySupport } from './filter'; + +jest.mock('./assert', () => ({ + assertScopeSupported: jest.fn(), +})); +const MockAssert = jest.mocked(Assert); + +describe('filter', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('filterScopesSupported', () => { + const isChainIdSupported = jest.fn(); + + it('checks if each scope is supported', () => { + filterScopesSupported( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + accounts: [], + }, + }, + { isChainIdSupported }, + ); + + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:1', + { + methods: ['a'], + notifications: [], + accounts: [], + }, + { isChainIdSupported }, + ); + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:5', + { + methods: ['b'], + notifications: [], + accounts: [], + }, + { isChainIdSupported }, + ); + }); + + it('returns only supported scopes', () => { + MockAssert.assertScopeSupported.mockImplementation((scopeString) => { + if (scopeString === 'eip155:1') { + throw new Error('scope not supported'); + } + }); + + expect( + filterScopesSupported( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + accounts: [], + }, + }, + { isChainIdSupported }, + ), + ).toStrictEqual({ + 'eip155:5': { + methods: ['b'], + notifications: [], + accounts: [], + }, + }); + }); + }); + + describe('bucketScopesBySupport', () => { + const isChainIdSupported = jest.fn(); + + it('checks if each scope is supported', () => { + bucketScopesBySupport( + { + 'eip155:1': { + methods: ['a'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['b'], + notifications: [], + accounts: [], + }, + }, + { isChainIdSupported }, + ); + + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:1', + { + methods: ['a'], + notifications: [], + accounts: [], + }, + { isChainIdSupported }, + ); + expect(MockAssert.assertScopeSupported).toHaveBeenCalledWith( + 'eip155:5', + { + methods: ['b'], + notifications: [], + accounts: [], + }, + { isChainIdSupported }, + ); + }); + + 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: [], + }, + }, + { isChainIdSupported }, + ), + ).toStrictEqual({ + supportedScopes: { + 'eip155:5': { + methods: ['b'], + notifications: [], + accounts: [], + }, + }, + unsupportedScopes: { + 'eip155:1': { + methods: ['a'], + notifications: [], + accounts: [], + }, + }, + }); + }); + }); +}); diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts new file mode 100644 index 0000000000..58157bd602 --- /dev/null +++ b/packages/multichain/src/scope/filter.ts @@ -0,0 +1,44 @@ +import type { CaipChainId, Hex } from '@metamask/utils'; + +import { assertScopeSupported } from './assert'; +import type { ScopesObject } from './types'; + +export const bucketScopesBySupport = ( + scopes: ScopesObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + const supportedScopes: ScopesObject = {}; + const unsupportedScopes: ScopesObject = {}; + + for (const [scopeString, scopeObject] of Object.entries(scopes)) { + try { + assertScopeSupported(scopeString, scopeObject, { + isChainIdSupported, + }); + supportedScopes[scopeString as CaipChainId] = scopeObject; + } catch (err) { + unsupportedScopes[scopeString as CaipChainId] = scopeObject; + } + } + + return { supportedScopes, unsupportedScopes }; +}; + +export const filterScopesSupported = ( + scopes: ScopesObject, + { + isChainIdSupported, + }: { + isChainIdSupported: (chainId: Hex) => boolean; + }, +) => { + const { supportedScopes } = bucketScopesBySupport(scopes, { + isChainIdSupported, + }); + + return supportedScopes; +}; From 95dfbc9b795520346238bd2f1ebe978c9cf3127a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:12:56 -0700 Subject: [PATCH 41/80] Revert "remove api related deps" This reverts commit 4ceb11850f8bc7340290940204f3e2a4a4f30db9. --- packages/multichain/package.json | 5 ++ yarn.lock | 123 ++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 313c94a52f..044b203f0b 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -51,13 +51,18 @@ "@metamask/controller-utils": "^11.3.0", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.0", + "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^9.1.0", + "@open-rpc/schema-utils-js": "^2.0.5", + "jsonschema": "^1.2.4", "lodash": "^4.17.21" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", + "@metamask/json-rpc-engine": "^9.0.3", "@metamask/network-controller": "^21.0.1", "@metamask/permission-controller": "^11.0.2", + "@open-rpc/meta-schema": "^1.14.6", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/yarn.lock b/yarn.lock index b16e5d8f3e..31ab2d0d9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1931,6 +1931,48 @@ __metadata: languageName: node linkType: hard +"@json-schema-spec/json-pointer@npm:^0.1.2": + version: 0.1.2 + resolution: "@json-schema-spec/json-pointer@npm:0.1.2" + checksum: 10/2a691ffc11f1a266ca4d0c9e2c99791679d580f343ef69746fad623d1abcf4953adde987890e41f906767d7729604c0182341e9012388b73a44d5b21fb296453 + languageName: node + linkType: hard + +"@json-schema-tools/dereferencer@npm:^1.6.3": + version: 1.6.3 + resolution: "@json-schema-tools/dereferencer@npm:1.6.3" + dependencies: + "@json-schema-tools/reference-resolver": "npm:^1.2.6" + "@json-schema-tools/traverse": "npm:^1.10.4" + fast-safe-stringify: "npm:^2.1.1" + checksum: 10/da6ef5b82a8a9c3a7e62ffcab5c04c581f1e0f8165c0debdb272bb1e08ccd726107ee194487b8fa736cac00fb390b8df74bc1ad1b200eddbe25c98ee0d3d000b + languageName: node + linkType: hard + +"@json-schema-tools/meta-schema@npm:^1.7.5": + version: 1.7.5 + resolution: "@json-schema-tools/meta-schema@npm:1.7.5" + checksum: 10/707dc3a285c26c37d00f418e9d0ef8a2ad1c23d4936ad5aab0ce94c9ae36a7a6125c4ca5048513af64b7e6e527b5472a1701d1f709c379acdd7ad12f6409d2cd + languageName: node + linkType: hard + +"@json-schema-tools/reference-resolver@npm:^1.2.6": + version: 1.2.6 + resolution: "@json-schema-tools/reference-resolver@npm:1.2.6" + dependencies: + "@json-schema-spec/json-pointer": "npm:^0.1.2" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/91d6b4b2ac43f8163fd27bde6d826f29f339e9c7ce3b7e2b73b85e891fa78e3702fd487deda143a0701879cbc2fe28c53a4efce4cd2d2dd2fe6e82b64bbd9c9c + languageName: node + linkType: hard + +"@json-schema-tools/traverse@npm:^1.10.4": + version: 1.10.4 + resolution: "@json-schema-tools/traverse@npm:1.10.4" + checksum: 10/0027bc90df01c5eeee0833e722b7320b53be8b5ce3f4e0e4a6e45713a38e6f88f21aba31e3dd973093ef75cd21a40c07fe8f112da8f49a7919b1c0e44c904d20 + languageName: node + linkType: hard + "@keystonehq/alias-sampling@npm:^0.1.1": version: 0.1.2 resolution: "@keystonehq/alias-sampling@npm:0.1.2" @@ -3060,13 +3102,18 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" + "@metamask/json-rpc-engine": "npm:^9.0.3" "@metamask/network-controller": "npm:^21.0.1" "@metamask/permission-controller": "npm:^11.0.2" "@metamask/rpc-errors": "npm:^7.0.0" + "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^9.1.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.2.4" lodash: "npm:^4.17.21" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" @@ -4028,6 +4075,31 @@ __metadata: languageName: node linkType: hard +"@open-rpc/meta-schema@npm:^1.14.6, @open-rpc/meta-schema@npm:^1.14.9": + version: 1.14.9 + resolution: "@open-rpc/meta-schema@npm:1.14.9" + checksum: 10/51505dcf7aa1a2285c78953c9b33711cede5f2765aa37dcb9ee7756d689e2ff2a89cfc6039504f0569c52a805fb9aa18f30a7c02ad7a06e793c801e43b419104 + languageName: node + linkType: hard + +"@open-rpc/schema-utils-js@npm:^2.0.5": + version: 2.0.5 + resolution: "@open-rpc/schema-utils-js@npm:2.0.5" + dependencies: + "@json-schema-tools/dereferencer": "npm:^1.6.3" + "@json-schema-tools/meta-schema": "npm:^1.7.5" + "@json-schema-tools/reference-resolver": "npm:^1.2.6" + "@open-rpc/meta-schema": "npm:^1.14.9" + ajv: "npm:^6.10.0" + detect-node: "npm:^2.0.4" + fast-safe-stringify: "npm:^2.0.7" + fs-extra: "npm:^10.1.0" + is-url: "npm:^1.2.4" + isomorphic-fetch: "npm:^3.0.0" + checksum: 10/9e10215606e9a00a47b082c9cfd70d05bf0d38de6cf1c147246c545c6997375d94cd3caafe919b71178df58b5facadfd0dcc8b6857bf5e79c40e5e33683dd3d5 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -4968,7 +5040,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.4": +"ajv@npm:^6.10.0, ajv@npm:^6.12.4": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -6364,6 +6436,13 @@ __metadata: languageName: node linkType: hard +"detect-node@npm:^2.0.4": + version: 2.1.0 + resolution: "detect-node@npm:2.1.0" + checksum: 10/832184ec458353e41533ac9c622f16c19f7c02d8b10c303dfd3a756f56be93e903616c0bb2d4226183c9351c15fc0b3dba41a17a2308262afabcfa3776e6ae6e + languageName: node + linkType: hard + "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -7372,7 +7451,7 @@ __metadata: languageName: node linkType: hard -"fast-safe-stringify@npm:^2.0.6": +"fast-safe-stringify@npm:^2.0.6, fast-safe-stringify@npm:^2.0.7, fast-safe-stringify@npm:^2.1.1": version: 2.1.1 resolution: "fast-safe-stringify@npm:2.1.1" checksum: 10/dc1f063c2c6ac9533aee14d406441f86783a8984b2ca09b19c2fe281f9ff59d315298bc7bc22fd1f83d26fe19ef2f20e2ddb68e96b15040292e555c5ced0c1e4 @@ -7612,6 +7691,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^10.1.0": + version: 10.1.0 + resolution: "fs-extra@npm:10.1.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/05ce2c3b59049bcb7b52001acd000e44b3c4af4ec1f8839f383ef41ec0048e3cfa7fd8a637b1bddfefad319145db89be91f4b7c1db2908205d38bf91e7d1d3b7 + languageName: node + linkType: hard + "fs-minipass@npm:^2.0.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -7905,7 +7995,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -8581,6 +8671,13 @@ __metadata: languageName: node linkType: hard +"is-url@npm:^1.2.4": + version: 1.2.4 + resolution: "is-url@npm:1.2.4" + checksum: 10/100e74b3b1feab87a43ef7653736e88d997eb7bd32e71fd3ebc413e58c1cbe56269699c776aaea84244b0567f2a7d68dfaa512a062293ed2f9fdecb394148432 + languageName: node + linkType: hard + "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -9517,6 +9614,19 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10/03014769e7dc77d4cf05fa0b534907270b60890085dd5e4d60a382ff09328580651da0b8b4cdf44d91e4c8ae64d91791d965f05707beff000ed494a38b6fec85 + languageName: node + linkType: hard + "jsonschema@npm:^1.2.4": version: 1.4.1 resolution: "jsonschema@npm:1.4.1" @@ -12344,6 +12454,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10/ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.0": version: 1.1.0 resolution: "update-browserslist-db@npm:1.1.0" From 9bcf86965d48a8499bdfe12d279e3dbc1edeb4f2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:18:08 -0700 Subject: [PATCH 42/80] Fix package.json --- packages/multichain/package.json | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 8bb2159698..4fa4f7ccfc 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -18,14 +18,19 @@ "sideEffects": false, "exports": { ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/types/index.d.ts" + "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.js", - "types": "./dist/types/index.d.ts", + "main": "./dist/index.cjs", + "types": "./dist/index.d.cts", "files": [ "dist/" ], @@ -35,10 +40,11 @@ "changelog:update": "../../scripts/update-changelog.sh @metamask/multichain", "changelog:validate": "../../scripts/validate-changelog.sh @metamask/multichain", "publish:preview": "yarn npm publish --tag preview", - "test": "jest --reporters=jest-silent-reporter", - "test:clean": "jest --clearCache", - "test:verbose": "jest --verbose", - "test:watch": "jest --watch" + "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" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", From afb2e2c0d8c0facca91b693444135641ef693d03 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:34:09 -0700 Subject: [PATCH 43/80] fix bad merge --- packages/multichain/src/index.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 47a6b90661..03bbb44262 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -1,4 +1,3 @@ -<<<<<<< HEAD export { getEthAccounts, setEthAccounts, @@ -11,9 +10,7 @@ export { } from './adapters/caip-permission-adapter-permittedChains'; export type { Caip25Authorization } from './scope/authorization'; -export { - validateAndNormalizeScopes, -} from './scope/authorization'; +export { validateAndNormalizeScopes } from './scope/authorization'; export * from './scope/types'; export { isSupportedScopeString, @@ -38,15 +35,3 @@ export { Caip25CaveatMutatorFactories, removeScope, } from './caip25Permission'; -||||||| 8fb04fc2 -======= -/** - * Example function that returns a greeting for the given name. - * - * @param name - The name to greet. - * @returns The greeting. - */ -export default function greeter(name: string): string { - return `Hello, ${name}!`; -} ->>>>>>> initialize-caip-multichain From 7c97afe00a77879aa5dcf01089180045635e8dde Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:37:57 -0700 Subject: [PATCH 44/80] fix bad merge --- packages/multichain/src/index.ts | 13 ------------- .../middlewares/multichainMethodCallValidator.ts | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index f46f4c97c7..f764ea5b3e 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -1,4 +1,3 @@ -<<<<<<< HEAD export { getEthAccounts, setEthAccounts, @@ -49,15 +48,3 @@ export { Caip25CaveatMutatorFactories, removeScope, } from './caip25Permission'; -||||||| 8fb04fc2 -======= -/** - * Example function that returns a greeting for the given name. - * - * @param name - The name to greet. - * @returns The greeting. - */ -export default function greeter(name: string): string { - return `Hello, ${name}!`; -} ->>>>>>> initialize-caip-multichain diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index d62b2328eb..cc075140e2 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -24,8 +24,8 @@ const transformError = ( got: unknown, ) => { // if there is a path, add it to the message - const message = `${ - param.name + (error.path.length > 0 ? `.${error.path.join('.')}` : '') + const message = `${param.name}${ + error.path.length > 0 ? `.${error.path.join('.')}` : '' } ${error.message}`; return { From 109a7bf5a25ef4f9b15a3ffe081cacac286d0114 Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 17 Oct 2024 14:21:11 -0700 Subject: [PATCH 45/80] Update packages/controller-utils/src/util.test.ts Co-authored-by: Mark Stacey --- packages/controller-utils/src/util.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/controller-utils/src/util.test.ts b/packages/controller-utils/src/util.test.ts index 3126fb7ef1..a28cc2df18 100644 --- a/packages/controller-utils/src/util.test.ts +++ b/packages/controller-utils/src/util.test.ts @@ -614,6 +614,10 @@ describe('util', () => { describe('isEqualCaseInsensitive', () => { it('returns false for non-string values', () => { + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(null, null)).toBe(false); + // @ts-expect-error Invalid type for testing purposes + expect(util.isEqualCaseInsensitive(5, 5)).toBe(false); // @ts-expect-error Invalid type for testing purposes expect(util.isEqualCaseInsensitive(null, 'test')).toBe(false); // @ts-expect-error Invalid type for testing purposes From dae4f73d4e54c88a492112977146f99d20365e4c Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 17 Oct 2024 15:48:26 -0700 Subject: [PATCH 46/80] add account support check in validator (#4816) ## Explanation Mirrors the [wallet_createSession handler ](https://github.com/MetaMask/metamask-extension/pull/27782/files#diff-107459889087f2776c6db636bd45498bef6749302f9d2dc633b4de17fede40a3R96-R108) in how eth account support is checked/asserted. Opted to do this rather than modify `assertScopeSupported` because the `bucketScopes` helper also relies on `assertScopedSupported` but doesn't care about accounts (which is why eth accounts are checked outside of assertScopeSupported in the wallet_createSession handler currently) ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Alex Donesky --- .../multichain/src/caip25Permission.test.ts | 71 ++++++++++++++++++- packages/multichain/src/caip25Permission.ts | 22 ++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 818035ff8a..900b89c907 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -44,6 +44,7 @@ describe('endowment:caip25', () => { const specification = caip25EndowmentBuilder.specificationBuilder({ methodHooks: { findNetworkClientIdByChainId: jest.fn(), + listAccounts: jest.fn(), }, }); expect(specification).toStrictEqual({ @@ -227,9 +228,11 @@ describe('endowment:caip25', () => { describe('permission validator', () => { const findNetworkClientIdByChainId = jest.fn(); + const listAccounts = jest.fn(); const { validator } = caip25EndowmentBuilder.specificationBuilder({ methodHooks: { findNetworkClientIdByChainId, + listAccounts, }, }); @@ -493,7 +496,7 @@ describe('endowment:caip25', () => { }, }, normalizedOptionalScopes: { - 'eip155:1': { + 'eip155:5': { methods: ['normalized_optional'], notifications: [], accounts: [], @@ -534,7 +537,7 @@ describe('endowment:caip25', () => { } expect(MockScopeAssert.assertScopesSupported).toHaveBeenCalledWith( { - 'eip155:1': { + 'eip155:5': { methods: ['normalized_optional'], notifications: [], accounts: [], @@ -549,6 +552,61 @@ describe('endowment:caip25', () => { expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); }); + it('throws if the eth accounts specified in the normalized scopeObjects are not found in the wallet keyring', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + normalizedOptionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + }); + listAccounts.mockReturnValue([{ address: '0xdead' }]); // missing '0xbeef' + + expect(() => { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, + ), + ); + }); + it('throws if the input requiredScopes does not match the output of validateAndNormalizeScopes', () => { MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: {}, @@ -560,6 +618,8 @@ describe('endowment:caip25', () => { }, }, }); + listAccounts.mockReturnValue([{ address: '0xbeef' }]); + expect(() => { validator({ caveats: [ @@ -603,6 +663,8 @@ describe('endowment:caip25', () => { }, normalizedOptionalScopes: {}, }); + listAccounts.mockReturnValue([{ address: '0xdead' }]); + expect(() => { validator({ caveats: [ @@ -652,6 +714,11 @@ describe('endowment:caip25', () => { }, }, }); + listAccounts.mockReturnValue([ + { address: '0xdead' }, + { address: '0xbeef' }, + ]); + expect( validator({ caveats: [ diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index f4f7ec88c1..e58355c1f4 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -19,6 +19,7 @@ import { import { strict as assert } from 'assert'; import { cloneDeep, isEqual } from 'lodash'; +import { getEthAccounts } from './adapters/caip-permission-adapter-eth-accounts'; import { assertScopesSupported } from './scope/assert'; import { validateAndNormalizeScopes } from './scope/authorization'; import type { @@ -56,6 +57,7 @@ type Caip25EndowmentSpecification = ValidPermissionSpecification<{ type Caip25EndowmentSpecificationBuilderOptions = { methodHooks: { findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId; + listAccounts: () => { address: Hex }[]; }; }; @@ -120,6 +122,26 @@ const specificationBuilder: PermissionSpecificationBuilder< isChainIdSupported, }); + // Fetch EVM accounts from native wallet keyring + // These addresses are lowercased already + const existingEvmAddresses = methodHooks + .listAccounts() + .map((account) => account.address); + const ethAccounts = getEthAccounts({ + requiredScopes: normalizedRequiredScopes, + optionalScopes: normalizedOptionalScopes, + isMultichainOrigin, + }).map((address) => address.toLowerCase() as Hex); + + const allEthAccountsSupported = ethAccounts.every((address) => + existingEvmAddresses.includes(address), + ); + if (!allEthAccountsSupported) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Received eip155 account value(s) for caveat of type "${Caip25CaveatType}" that were not found in the wallet keyring.`, + ); + } + assert.deepEqual(requiredScopes, normalizedRequiredScopes); assert.deepEqual(optionalScopes, normalizedOptionalScopes); }, From fad338dc1c9dd3c575673c7e7c8040bbd8dae074 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 08:49:52 -0700 Subject: [PATCH 47/80] remove caip-permission-adapter-middleware --- ...caip-permission-adapter-middleware.test.ts | 148 ------------------ .../caip-permission-adapter-middleware.ts | 80 ---------- 2 files changed, 228 deletions(-) delete mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts delete mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.ts diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts deleted file mode 100644 index c044c73b0f..0000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { providerErrors } from '@metamask/rpc-errors'; -import type { JsonRpcRequest } from '@metamask/utils'; - -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { caipPermissionAdapterMiddleware } from './caip-permission-adapter-middleware'; - -const baseRequest = { - id: 1, - jsonrpc: '2.0' as const, - origin: 'http://test.com', - networkClientId: 'mainnet', - method: 'eth_call', - params: { - foo: 'bar', - }, -}; - -const createMockedHandler = () => { - const next = jest.fn(); - const end = jest.fn(); - const getCaveat = jest.fn().mockReturnValue({ - value: { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: [], - accounts: [], - }, - 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], - accounts: [], - }, - }, - optionalScopes: { - 'eip155:1': { - methods: ['net_version'], - notifications: [], - accounts: [], - }, - wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - unhandled: { - methods: ['foobar'], - notifications: [], - accounts: [], - }, - }, - isMultichainOrigin: true, - }, - }); - const getNetworkConfigurationByNetworkClientId = jest - .fn() - .mockImplementation((networkClientId: string) => { - const chainId = - { - mainnet: '0x1', - goerli: '0x5', - }[networkClientId] || '0x999'; - return { - chainId, - }; - }); - const handler = ( - request: JsonRpcRequest & { - networkClientId: string; - origin: string; - }, - ) => - caipPermissionAdapterMiddleware(request, {}, next, end, { - getCaveat, - getNetworkConfigurationByNetworkClientId, - }); - - return { - next, - end, - getCaveat, - getNetworkConfigurationByNetworkClientId, - handler, - }; -}; - -describe('CaipPermissionAdapterMiddleware', () => { - it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { - const { handler, getCaveat } = createMockedHandler(); - await handler(baseRequest); - expect(getCaveat).toHaveBeenCalledWith( - 'http://test.com', - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - }); - - it('allows the request when there is no CAIP-25 endowment permission', async () => { - const { handler, getCaveat, next } = createMockedHandler(); - getCaveat.mockImplementation(() => { - throw new Error('permission not found'); - }); - await handler(baseRequest); - expect(next).toHaveBeenCalled(); - }); - - it('allows the request when the CAIP-25 endowment permission was not granted from the multichain API', async () => { - const { handler, getCaveat, next } = createMockedHandler(); - getCaveat.mockReturnValue({ - value: { - isMultichainOrigin: false, - }, - }); - await handler(baseRequest); - expect(next).toHaveBeenCalled(); - }); - - it('gets the chainId for the request networkClientId', async () => { - const { handler, getNetworkConfigurationByNetworkClientId } = - createMockedHandler(); - await handler(baseRequest); - expect(getNetworkConfigurationByNetworkClientId).toHaveBeenCalledWith( - 'mainnet', - ); - }); - - describe('when the CAIP-25 endowment permission was granted over the multichain API', () => { - it('throws an error if the requested method is not authorized for the scope specified in the request', async () => { - const { handler, end } = createMockedHandler(); - - await handler({ - ...baseRequest, - method: 'unauthorized_method', - }); - expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); - }); - - it('allows the request if the requested scope method is authorized in the current scope', async () => { - const { handler, next } = createMockedHandler(); - - await handler(baseRequest); - expect(next).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts deleted file mode 100644 index 865a4cccfc..0000000000 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { - NetworkConfiguration, - NetworkClientId, -} from '@metamask/network-controller'; -import type { Caveat } from '@metamask/permission-controller'; -import { providerErrors } from '@metamask/rpc-errors'; -import type { JsonRpcRequest } from '@metamask/utils'; - -import type { Caip25CaveatValue } from '../caip25Permission'; -import { - Caip25CaveatType, - Caip25EndowmentPermissionName, -} from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import { KnownWalletScopeString, type ScopeString } from '../scope/types'; - -/** - * Middleware to handle CAIP-25 permission requests. - * - * @param request - The request object. - * @param _response - The response object. - * @param next - The next middleware function. - * @param end - The end function. - * @param hooks - The hooks object. - * @param hooks.getCaveat - Function to retrieve a caveat. - * @param hooks.getNetworkConfigurationByNetworkClientId - Function to retrieve a network configuration. - */ -export async function caipPermissionAdapterMiddleware( - request: JsonRpcRequest & { - networkClientId: NetworkClientId; - origin: string; - }, - _response: unknown, - next: () => Promise, - end: (error?: Error) => void, - hooks: { - getCaveat: ( - ...args: unknown[] - ) => Caveat; - getNetworkConfigurationByNetworkClientId: ( - networkClientId: NetworkClientId, - ) => NetworkConfiguration; - }, -) { - const { networkClientId, method } = request; - - let caveat; - try { - caveat = hooks.getCaveat( - request.origin, - Caip25EndowmentPermissionName, - Caip25CaveatType, - ); - } catch (err) { - // noop - } - if (!caveat?.value?.isMultichainOrigin) { - return next(); - } - - const { chainId } = - hooks.getNetworkConfigurationByNetworkClientId(networkClientId); - - const scope: ScopeString = `eip155:${parseInt(chainId, 16)}`; - - const scopesObject = mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - ); - - if ( - !scopesObject[scope]?.methods?.includes(method) && - !scopesObject[KnownWalletScopeString.Eip155]?.methods?.includes(method) && - !scopesObject.wallet?.methods?.includes(method) - ) { - return end(providerErrors.unauthorized()); - } - - return next(); -} From 8208931de69ae4ccf964257702a6b6a26a4b2e3d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 08:50:09 -0700 Subject: [PATCH 48/80] Revert "remove caip-permission-adapter-middleware" This reverts commit fad338dc1c9dd3c575673c7e7c8040bbd8dae074. --- ...caip-permission-adapter-middleware.test.ts | 148 ++++++++++++++++++ .../caip-permission-adapter-middleware.ts | 80 ++++++++++ 2 files changed, 228 insertions(+) create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-middleware.ts diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts new file mode 100644 index 0000000000..c044c73b0f --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.test.ts @@ -0,0 +1,148 @@ +import { providerErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { caipPermissionAdapterMiddleware } from './caip-permission-adapter-middleware'; + +const baseRequest = { + id: 1, + jsonrpc: '2.0' as const, + origin: 'http://test.com', + networkClientId: 'mainnet', + method: 'eth_call', + params: { + foo: 'bar', + }, +}; + +const createMockedHandler = () => { + const next = jest.fn(); + const end = jest.fn(); + const getCaveat = jest.fn().mockReturnValue({ + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: [], + accounts: [], + }, + 'eip155:5': { + methods: ['eth_chainId'], + notifications: [], + accounts: [], + }, + }, + optionalScopes: { + 'eip155:1': { + methods: ['net_version'], + notifications: [], + accounts: [], + }, + wallet: { + methods: ['wallet_watchAsset'], + notifications: [], + accounts: [], + }, + unhandled: { + methods: ['foobar'], + notifications: [], + accounts: [], + }, + }, + isMultichainOrigin: true, + }, + }); + const getNetworkConfigurationByNetworkClientId = jest + .fn() + .mockImplementation((networkClientId: string) => { + const chainId = + { + mainnet: '0x1', + goerli: '0x5', + }[networkClientId] || '0x999'; + return { + chainId, + }; + }); + const handler = ( + request: JsonRpcRequest & { + networkClientId: string; + origin: string; + }, + ) => + caipPermissionAdapterMiddleware(request, {}, next, end, { + getCaveat, + getNetworkConfigurationByNetworkClientId, + }); + + return { + next, + end, + getCaveat, + getNetworkConfigurationByNetworkClientId, + handler, + }; +}; + +describe('CaipPermissionAdapterMiddleware', () => { + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { + const { handler, getCaveat } = createMockedHandler(); + await handler(baseRequest); + expect(getCaveat).toHaveBeenCalledWith( + 'http://test.com', + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + }); + + it('allows the request when there is no CAIP-25 endowment permission', async () => { + const { handler, getCaveat, next } = createMockedHandler(); + getCaveat.mockImplementation(() => { + throw new Error('permission not found'); + }); + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + + it('allows the request when the CAIP-25 endowment permission was not granted from the multichain API', async () => { + const { handler, getCaveat, next } = createMockedHandler(); + getCaveat.mockReturnValue({ + value: { + isMultichainOrigin: false, + }, + }); + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + + it('gets the chainId for the request networkClientId', async () => { + const { handler, getNetworkConfigurationByNetworkClientId } = + createMockedHandler(); + await handler(baseRequest); + expect(getNetworkConfigurationByNetworkClientId).toHaveBeenCalledWith( + 'mainnet', + ); + }); + + describe('when the CAIP-25 endowment permission was granted over the multichain API', () => { + it('throws an error if the requested method is not authorized for the scope specified in the request', async () => { + const { handler, end } = createMockedHandler(); + + await handler({ + ...baseRequest, + method: 'unauthorized_method', + }); + expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); + }); + + it('allows the request if the requested scope method is authorized in the current scope', async () => { + const { handler, next } = createMockedHandler(); + + await handler(baseRequest); + expect(next).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts new file mode 100644 index 0000000000..865a4cccfc --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -0,0 +1,80 @@ +import type { + NetworkConfiguration, + NetworkClientId, +} from '@metamask/network-controller'; +import type { Caveat } from '@metamask/permission-controller'; +import { providerErrors } from '@metamask/rpc-errors'; +import type { JsonRpcRequest } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import { + Caip25CaveatType, + Caip25EndowmentPermissionName, +} from '../caip25Permission'; +import { mergeScopes } from '../scope/transform'; +import { KnownWalletScopeString, type ScopeString } from '../scope/types'; + +/** + * Middleware to handle CAIP-25 permission requests. + * + * @param request - The request object. + * @param _response - The response object. + * @param next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.getCaveat - Function to retrieve a caveat. + * @param hooks.getNetworkConfigurationByNetworkClientId - Function to retrieve a network configuration. + */ +export async function caipPermissionAdapterMiddleware( + request: JsonRpcRequest & { + networkClientId: NetworkClientId; + origin: string; + }, + _response: unknown, + next: () => Promise, + end: (error?: Error) => void, + hooks: { + getCaveat: ( + ...args: unknown[] + ) => Caveat; + getNetworkConfigurationByNetworkClientId: ( + networkClientId: NetworkClientId, + ) => NetworkConfiguration; + }, +) { + const { networkClientId, method } = request; + + let caveat; + try { + caveat = hooks.getCaveat( + request.origin, + Caip25EndowmentPermissionName, + Caip25CaveatType, + ); + } catch (err) { + // noop + } + if (!caveat?.value?.isMultichainOrigin) { + return next(); + } + + const { chainId } = + hooks.getNetworkConfigurationByNetworkClientId(networkClientId); + + const scope: ScopeString = `eip155:${parseInt(chainId, 16)}`; + + const scopesObject = mergeScopes( + caveat.value.requiredScopes, + caveat.value.optionalScopes, + ); + + if ( + !scopesObject[scope]?.methods?.includes(method) && + !scopesObject[KnownWalletScopeString.Eip155]?.methods?.includes(method) && + !scopesObject.wallet?.methods?.includes(method) + ) { + return end(providerErrors.unauthorized()); + } + + return next(); +} From c5980c62e07251cb62abde2849f48ee2908cabc3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 08:51:58 -0700 Subject: [PATCH 49/80] remove caip-permission-adapter-middleware from exports --- packages/multichain/src/index.test.ts | 1 - packages/multichain/src/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 7cf4f39321..664c35f6ea 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -6,7 +6,6 @@ describe('@metamask/multichain', () => { Array [ "getEthAccounts", "setEthAccounts", - "caipPermissionAdapterMiddleware", "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 03bbb44262..6c4d641f55 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -2,7 +2,6 @@ export { getEthAccounts, setEthAccounts, } from './adapters/caip-permission-adapter-eth-accounts'; -export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware'; export { getPermittedEthChainIds, addPermittedEthChainId, From 5c6bb8f5ce2e76991ee772b8b43073d299e0b088 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 08:52:14 -0700 Subject: [PATCH 50/80] Revert "remove caip-permission-adapter-middleware from exports" This reverts commit c5980c62e07251cb62abde2849f48ee2908cabc3. --- packages/multichain/src/index.test.ts | 1 + packages/multichain/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index ff29ce8f43..d06cd6094e 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -6,6 +6,7 @@ describe('@metamask/multichain', () => { Array [ "getEthAccounts", "setEthAccounts", + "caipPermissionAdapterMiddleware", "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index c203b78cfc..f764ea5b3e 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -2,6 +2,7 @@ export { getEthAccounts, setEthAccounts, } from './adapters/caip-permission-adapter-eth-accounts'; +export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware'; export { getPermittedEthChainIds, addPermittedEthChainId, From a7a8e6e4db0ccbd97124e201f5e0a49136354c5c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 09:06:56 -0700 Subject: [PATCH 51/80] remove types/@metamask/eth-json-rpc-filters.d.ts --- types/@metamask/eth-json-rpc-filters.d.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 types/@metamask/eth-json-rpc-filters.d.ts diff --git a/types/@metamask/eth-json-rpc-filters.d.ts b/types/@metamask/eth-json-rpc-filters.d.ts deleted file mode 100644 index 5a51785b82..0000000000 --- a/types/@metamask/eth-json-rpc-filters.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@metamask/eth-json-rpc-filters/subscriptionManager'; From 54829e006408c1758be0b67f3c5055ba6a7f7c0d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 09:07:14 -0700 Subject: [PATCH 52/80] Revert "remove types/@metamask/eth-json-rpc-filters.d.ts" This reverts commit a7a8e6e4db0ccbd97124e201f5e0a49136354c5c. --- types/@metamask/eth-json-rpc-filters.d.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 types/@metamask/eth-json-rpc-filters.d.ts diff --git a/types/@metamask/eth-json-rpc-filters.d.ts b/types/@metamask/eth-json-rpc-filters.d.ts new file mode 100644 index 0000000000..5a51785b82 --- /dev/null +++ b/types/@metamask/eth-json-rpc-filters.d.ts @@ -0,0 +1 @@ +declare module '@metamask/eth-json-rpc-filters/subscriptionManager'; From 42297f964fdb1623bc9da75e377c5ed9c662cd3f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Oct 2024 14:22:58 -0500 Subject: [PATCH 53/80] add back readme content --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d13cf34247..d01734fa5b 100644 --- a/README.md +++ b/README.md @@ -122,11 +122,15 @@ linkStyle default opacity:0.5 logging_controller --> controller_utils; message_manager --> base_controller; message_manager --> controller_utils; + multichain --> controller_utils; + multichain --> network_controller; + multichain --> permission_controller; name_controller --> base_controller; name_controller --> controller_utils; network_controller --> base_controller; network_controller --> controller_utils; network_controller --> eth_json_rpc_provider; + network_controller --> json_rpc_engine; notification_controller --> base_controller; notification_services_controller --> base_controller; notification_services_controller --> controller_utils; @@ -134,6 +138,7 @@ linkStyle default opacity:0.5 notification_services_controller --> profile_sync_controller; permission_controller --> base_controller; permission_controller --> controller_utils; + permission_controller --> json_rpc_engine; permission_controller --> approval_controller; permission_log_controller --> base_controller; permission_log_controller --> json_rpc_engine; @@ -151,6 +156,7 @@ linkStyle default opacity:0.5 profile_sync_controller --> network_controller; queued_request_controller --> base_controller; queued_request_controller --> controller_utils; + queued_request_controller --> json_rpc_engine; queued_request_controller --> network_controller; queued_request_controller --> selected_network_controller; rate_limit_controller --> base_controller; From 56176d172c4b9d58ae923d95e6afd29fce149cf9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 15:01:33 -0700 Subject: [PATCH 54/80] yarn lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 83909d3835..9277381a56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2925,7 +2925,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2": +"@metamask/json-rpc-engine@npm:^9.0.1, @metamask/json-rpc-engine@npm:^9.0.2, @metamask/json-rpc-engine@npm:^9.0.3": version: 9.0.3 resolution: "@metamask/json-rpc-engine@npm:9.0.3" dependencies: From 72033583c9b3168993306b51ae03221666944082 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 22 Oct 2024 09:49:49 -0700 Subject: [PATCH 55/80] bump network-controller dep --- packages/multichain/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 313c94a52f..c06fe39396 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^21.0.1", + "@metamask/network-controller": "^21.1.0", "@metamask/permission-controller": "^11.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", diff --git a/yarn.lock b/yarn.lock index dee0817049..893ed6048b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3037,7 +3037,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.3.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/network-controller": "npm:^21.0.1" + "@metamask/network-controller": "npm:^21.1.0" "@metamask/permission-controller": "npm:^11.0.2" "@metamask/rpc-errors": "npm:^7.0.0" "@metamask/utils": "npm:^9.1.0" @@ -3074,7 +3074,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^21.0.1, @metamask/network-controller@npm:^21.1.0, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^21.1.0, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: From 046133a766b36ea9fb5da499b8899cbd3deccf0d Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 22 Oct 2024 09:50:46 -0700 Subject: [PATCH 56/80] add Scoped Properties to Caip25Authorization type --- .../multichain/src/scope/authorization.ts | 21 ++++++++++++------- packages/multichain/src/scope/types.ts | 16 +++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index c7b98a8357..2b97a231e1 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,19 +1,26 @@ +import type { Json } from '@metamask/utils'; + import { normalizeAndMergeScopes } from './transform'; -import type { ExternalScopesObject, ScopesObject } from './types'; +import type { + ExternalScopesObject, + ExternalScopeString, + ScopesObject, +} from './types'; import { validateScopes } from './validation'; -export type Caip25Authorization = +export type Caip25Authorization = ( | { requiredScopes: ExternalScopesObject; optionalScopes?: ExternalScopesObject; - sessionProperties?: Record; } - | ({ + | { requiredScopes?: ExternalScopesObject; optionalScopes: ExternalScopesObject; - } & { - sessionProperties?: Record; - }); + } +) & { + sessionProperties?: Record; + scopedProperties?: Record; +}; export const validateAndNormalizeScopes = ( requiredScopes: ExternalScopesObject, diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index 0d5c87fbb1..ddd4695628 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -1,15 +1,16 @@ import MetaMaskOpenRPCDocument from '@metamask/api-specs'; +import { + isCaipNamespace, + isCaipChainId, + parseCaipChainId, +} from '@metamask/utils'; import type { CaipChainId, CaipReference, CaipAccountId, KnownCaipNamespace, CaipNamespace, -} from '@metamask/utils'; -import { - isCaipNamespace, - isCaipChainId, - parseCaipChainId, + Json, } from '@metamask/utils'; export enum KnownWalletScopeString { @@ -97,7 +98,4 @@ export const parseScopeString = ( return {}; }; -export type ScopedProperties = Record< - ExternalScopeString, - Record ->; +export type ScopedProperties = Record>; From 67b222e3ba7f436cb10d788fb32a177facb98387 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 22 Oct 2024 11:07:01 -0700 Subject: [PATCH 57/80] Fix ScopedProperties type --- packages/multichain/src/scope/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index ddd4695628..bc15442ed4 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -98,4 +98,6 @@ export const parseScopeString = ( return {}; }; -export type ScopedProperties = Record>; +export type ScopedProperties = Record> & { + [KnownCaipNamespace.Wallet]?: Record; +}; From 009fb4aa59f7d92cefe386018aec307e765a2026 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 22 Oct 2024 13:00:30 -0700 Subject: [PATCH 58/80] Loosen get adapter param types (#4835) ## Explanation Loosen `getEthAccounts` and `getPermittedEthChainIds` param type ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../adapters/caip-permission-adapter-eth-accounts.test.ts | 1 - .../src/adapters/caip-permission-adapter-eth-accounts.ts | 7 ++++++- .../caip-permission-adapter-permittedChains.test.ts | 1 - .../adapters/caip-permission-adapter-permittedChains.ts | 5 ++++- packages/multichain/src/caip25Permission.ts | 1 - 5 files changed, 10 insertions(+), 5 deletions(-) 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 index eb55966678..6f043ed763 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -49,7 +49,6 @@ describe('CAIP-25 eth_accounts adapters', () => { accounts: ['wallet:eip155:0x5'], }, }, - isMultichainOrigin: false, }); expect(ethAccounts).toStrictEqual([ diff --git a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts index 6c72f1d07b..87848250b5 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -19,7 +19,12 @@ const isEip155ScopeString = (scopeString: ScopeString) => { ); }; -export const getEthAccounts = (caip25CaveatValue: Caip25CaveatValue) => { +export const getEthAccounts = ( + caip25CaveatValue: Pick< + Caip25CaveatValue, + 'requiredScopes' | 'optionalScopes' + >, +) => { const ethAccounts: string[] = []; const sessionScopes = mergeScopes( caip25CaveatValue.requiredScopes, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 4020c2442b..6f6fdc78d4 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -46,7 +46,6 @@ describe('CAIP-25 permittedChains adapters', () => { accounts: ['eip155:100:0x100'], }, }, - isMultichainOrigin: false, }); expect(ethChainIds).toStrictEqual(['0x1', '0x5', '0xa', '0x64']); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 6a59efa916..0528a14ea7 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -13,7 +13,10 @@ import { } from '../scope/types'; export const getPermittedEthChainIds = ( - caip25CaveatValue: Caip25CaveatValue, + caip25CaveatValue: Pick< + Caip25CaveatValue, + 'requiredScopes' | 'optionalScopes' + >, ) => { const ethChainIds: Hex[] = []; const sessionScopes = mergeScopes( diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index e58355c1f4..5914f1687a 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -130,7 +130,6 @@ const specificationBuilder: PermissionSpecificationBuilder< const ethAccounts = getEthAccounts({ requiredScopes: normalizedRequiredScopes, optionalScopes: normalizedOptionalScopes, - isMultichainOrigin, }).map((address) => address.toLowerCase() as Hex); const allEthAccountsSupported = ethAccounts.every((address) => From 0631c487e9f16efb966f7c969d861143031133af Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 08:53:57 -0700 Subject: [PATCH 59/80] Seperate types and constants --- ...permission-adapter-permittedChains.test.ts | 2 +- ...caip-permission-adapter-permittedChains.ts | 8 +-- packages/multichain/src/index.test.ts | 12 ++--- packages/multichain/src/index.ts | 18 ++++++- packages/multichain/src/scope/constants.ts | 47 ++++++++++++++++ .../multichain/src/scope/supported.test.ts | 12 ++--- packages/multichain/src/scope/supported.ts | 6 +-- packages/multichain/src/scope/types.ts | 54 +++++-------------- 8 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 packages/multichain/src/scope/constants.ts diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 6f6fdc78d4..1d55cf6f60 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -1,5 +1,5 @@ import type { Caip25CaveatValue } from '../caip25Permission'; -import { KnownNotifications, KnownRpcMethods } from '../scope/types'; +import { KnownNotifications, KnownRpcMethods } from '../scope/constants'; import { addPermittedEthChainId, getPermittedEthChainIds, diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 0528a14ea7..f994418946 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,14 +3,10 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; +import { KnownNotifications, KnownRpcMethods } from '../scope/constants'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { ScopesObject, ScopeString } from '../scope/types'; -import { - KnownNotifications, - KnownRpcMethods, - KnownWalletScopeString, - parseScopeString, -} from '../scope/types'; +import { KnownWalletScopeString, parseScopeString } from '../scope/types'; export const getPermittedEthChainIds = ( caip25CaveatValue: Pick< diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 664c35f6ea..7b61fec8af 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -10,6 +10,12 @@ describe('@metamask/multichain', () => { "addPermittedEthChainId", "setPermittedEthChainIds", "validateAndNormalizeScopes", + "KnownWalletRpcMethods", + "KnownRpcMethods", + "KnownWalletNamespaceRpcMethods", + "KnownNotifications", + "parseScopeString", + "KnownWalletScopeString", "isSupportedScopeString", "isSupportedAccount", "isSupportedMethod", @@ -26,12 +32,6 @@ describe('@metamask/multichain', () => { "caip25EndowmentBuilder", "Caip25CaveatMutatorFactories", "removeScope", - "KnownWalletScopeString", - "KnownWalletRpcMethods", - "KnownRpcMethods", - "KnownWalletNamespaceRpcMethods", - "KnownNotifications", - "parseScopeString", ] `); }); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 6c4d641f55..a70fce6508 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -10,7 +10,23 @@ export { export type { Caip25Authorization } from './scope/authorization'; export { validateAndNormalizeScopes } from './scope/authorization'; -export * from './scope/types'; +export { + KnownWalletRpcMethods, + KnownRpcMethods, + KnownWalletNamespaceRpcMethods, + KnownNotifications, +} from './scope/constants'; +export type { + ExternalScopeString, + ExternalScopeObject, + ExternalScopesObject, + ScopeString, + ScopeObject, + ScopesObject, + ScopedProperties, + NonWalletKnownCaipNamespace, +} from './scope/types'; +export { parseScopeString, KnownWalletScopeString } from './scope/types'; export { isSupportedScopeString, isSupportedAccount, diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts new file mode 100644 index 0000000000..ac1e4829d7 --- /dev/null +++ b/packages/multichain/src/scope/constants.ts @@ -0,0 +1,47 @@ +import MetaMaskOpenRPCDocument from '@metamask/api-specs'; +import type { KnownCaipNamespace } from '@metamask/utils'; + +// ScopeString for ecosystems that aren't chain specific +export enum KnownWalletScopeString { + Eip155 = 'wallet:eip155', +} + +// Known CAIP Namespaces excluding "wallet" +export type NonWalletKnownCaipNamespace = Exclude< + KnownCaipNamespace, + KnownCaipNamespace.Wallet +>; + +// Methods that do not belong to an ecosystem +export const KnownWalletRpcMethods: string[] = [ + 'wallet_registerOnboarding', + 'wallet_scanQRCode', +]; + +const WalletEip155Methods = ['wallet_addEthereumChain']; + +// 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)); + +// Methods for ecosystem that are chain specific +export const KnownRpcMethods: Record = { + eip155: Eip155Methods, +}; + +// Methods for ecosystems that aren't chain specific +export const KnownWalletNamespaceRpcMethods: Record< + NonWalletKnownCaipNamespace, + string[] +> = { + eip155: WalletEip155Methods, +}; + +// Notifications +export const KnownNotifications: Record = + { + eip155: ['accountsChanged', 'chainChanged', 'eth_subscription'], + }; diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index 72faf0e50d..cec4acf4aa 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -1,14 +1,14 @@ -import { - isSupportedMethod, - isSupportedNotification, - isSupportedScopeString, -} from './supported'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, -} from './types'; +} from './constants'; +import { + isSupportedMethod, + isSupportedNotification, + isSupportedScopeString, +} from './supported'; describe('Scope Support', () => { describe('isSupportedNotification', () => { diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 52f351ea6c..4cfe3fdbef 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -8,14 +8,14 @@ import { parseCaipChainId, } from '@metamask/utils'; -import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './types'; import { KnownNotifications, KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, - parseScopeString, -} from './types'; +} from './constants'; +import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './types'; +import { parseScopeString } from './types'; export const isSupportedScopeString = ( scopeString: string, diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index bc15442ed4..2742bc5457 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -1,4 +1,3 @@ -import MetaMaskOpenRPCDocument from '@metamask/api-specs'; import { isCaipNamespace, isCaipChainId, @@ -13,42 +12,6 @@ import type { Json, } from '@metamask/utils'; -export enum KnownWalletScopeString { - Eip155 = 'wallet:eip155', -} - -export type NonWalletKnownCaipNamespace = Extract< - KnownCaipNamespace, - KnownCaipNamespace.Eip155 ->; - -export const KnownWalletRpcMethods: string[] = [ - 'wallet_registerOnboarding', - 'wallet_scanQRCode', -]; -const WalletEip155Methods = ['wallet_addEthereumChain']; - -const Eip155Methods = MetaMaskOpenRPCDocument.methods - .map(({ name }: { name: string }) => name) - .filter((method: string) => !WalletEip155Methods.includes(method)) - .filter((method: string) => !KnownWalletRpcMethods.includes(method)); - -export const KnownRpcMethods: Record = { - eip155: Eip155Methods, -}; - -export const KnownWalletNamespaceRpcMethods: Record< - NonWalletKnownCaipNamespace, - string[] -> = { - eip155: WalletEip155Methods, -}; - -export const KnownNotifications: Record = - { - eip155: ['accountsChanged', 'chainChanged', 'eth_subscription'], - }; - // These External prefixed types represent the CAIP-217 // Scope and ScopeObject as defined in the spec. export type ExternalScopeString = CaipChainId | CaipNamespace; @@ -80,6 +43,10 @@ export type ScopesObject = Record & { [KnownCaipNamespace.Wallet]?: ScopeObject; }; +export type ScopedProperties = Record> & { + [KnownCaipNamespace.Wallet]?: Record; +}; + export const parseScopeString = ( scopeString: string, ): { @@ -98,6 +65,13 @@ export const parseScopeString = ( return {}; }; -export type ScopedProperties = Record> & { - [KnownCaipNamespace.Wallet]?: Record; -}; +// ScopeString for ecosystems that aren't chain specific +export enum KnownWalletScopeString { + Eip155 = 'wallet:eip155', +} + +// Known CAIP Namespaces excluding "wallet" +export type NonWalletKnownCaipNamespace = Exclude< + KnownCaipNamespace, + KnownCaipNamespace.Wallet +>; From 18d1112b95e756334fbf06d7dd5b9ec24a8783f7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 08:55:08 -0700 Subject: [PATCH 60/80] remove jest.resetAllMocks --- packages/multichain/src/caip25Permission.test.ts | 4 ---- packages/multichain/src/scope/assert.test.ts | 4 ---- packages/multichain/src/scope/authorization.test.ts | 4 ---- packages/multichain/src/scope/validation.test.ts | 4 ---- 4 files changed, 16 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 900b89c907..e2655c7288 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -36,10 +36,6 @@ describe('endowment:caip25', () => { }); }); - afterEach(() => { - jest.resetAllMocks(); - }); - it('builds the expected permission specification', () => { const specification = caip25EndowmentBuilder.specificationBuilder({ methodHooks: { diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 9b09aa6437..e333cde866 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -18,10 +18,6 @@ const validScopeObject: ScopeObject = { }; describe('Scope Assert', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - describe('assertScopeSupported', () => { const isChainIdSupported = jest.fn(); diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 55a184b364..c4a9c3463d 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -19,10 +19,6 @@ const validScopeObject: ExternalScopeObject = { }; describe('Scope Authorization', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - describe('validateAndNormalizeScopes', () => { it('validates the scopes', () => { try { diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index e8cfb96280..33bb343f3d 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -8,10 +8,6 @@ const validScopeObject: ExternalScopeObject = { }; describe('Scope Validation', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - describe('isValidScope', () => { it.each([ [ From 1ba77c78b753ac9d410f48b84a58f63ab7c1ecfc Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 08:55:38 -0700 Subject: [PATCH 61/80] yarn lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 3493fb766f..63495cca4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: From bb50a193e8e684a4f885cc71a2319940b7259bd0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 09:11:38 -0700 Subject: [PATCH 62/80] yarn --- packages/multichain/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index c06fe39396..e0ce902781 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@metamask/api-specs": "^0.10.12", - "@metamask/controller-utils": "^11.3.0", + "@metamask/controller-utils": "^11.4.0", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.0", "@metamask/utils": "^9.1.0", diff --git a/yarn.lock b/yarn.lock index 63495cca4d..05ba2455fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.3.0, @metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.0, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3050,7 +3050,7 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.3.0" + "@metamask/controller-utils": "npm:^11.4.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" "@metamask/network-controller": "npm:^21.1.0" "@metamask/permission-controller": "npm:^11.0.2" From 7d32a0b28934ab979e4de515a0e709dac4825673 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 10:36:49 -0700 Subject: [PATCH 63/80] 100% coverage --- packages/multichain/jest.config.js | 8 +- ...ip-permission-adapter-eth-accounts.test.ts | 14 ++ ...permission-adapter-permittedChains.test.ts | 10 + .../multichain/src/caip25Permission.test.ts | 94 +++++++++- .../multichain/src/scope/supported.test.ts | 172 ++++++++++++++++++ packages/multichain/src/scope/supported.ts | 71 +++----- .../multichain/src/scope/transform.test.ts | 30 +++ .../multichain/src/scope/validation.test.ts | 9 + 8 files changed, 351 insertions(+), 57 deletions(-) diff --git a/packages/multichain/jest.config.js b/packages/multichain/jest.config.js index 1cadcfe8b2..ca08413339 100644 --- a/packages/multichain/jest.config.js +++ b/packages/multichain/jest.config.js @@ -17,10 +17,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 82.38, - functions: 87.37, - lines: 86.65, - statements: 87.09, + branches: 100, + functions: 100, + lines: 100, + statements: 100, }, }, }); 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 index 6f043ed763..f779f1f576 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts @@ -105,6 +105,11 @@ describe('CAIP-25 eth_accounts adapters', () => { notifications: [], accounts: [], }, + wallet: { + methods: [], + notifications: [], + accounts: [], + }, }, isMultichainOrigin: false, }; @@ -155,6 +160,15 @@ describe('CAIP-25 eth_accounts adapters', () => { 'wallet:eip155:0x3', ], }, + wallet: { + methods: [], + notifications: [], + accounts: [ + 'wallet:eip155:0x1', + 'wallet:eip155:0x2', + 'wallet:eip155:0x3', + ], + }, }, isMultichainOrigin: false, }); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts index 1d55cf6f60..016067199a 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -253,6 +253,11 @@ describe('CAIP-25 permittedChains adapters', () => { }, }, optionalScopes: { + wallet: { + methods: [], + notifications: [], + accounts: [], + }, 'eip155:1': { methods: ['eth_chainId'], notifications: [], @@ -283,6 +288,11 @@ describe('CAIP-25 permittedChains adapters', () => { }, }, optionalScopes: { + wallet: { + methods: [], + notifications: [], + accounts: [], + }, 'eip155:1': { methods: ['eth_chainId'], notifications: [], diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index e2655c7288..72929ba7c1 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -12,6 +12,7 @@ import { Caip25EndowmentPermissionName, Caip25CaveatMutatorFactories, removeScope, + Caip25CaveatFactoryFn, } from './caip25Permission'; import * as ScopeAssert from './scope/assert'; import * as ScopeAuthorization from './scope/authorization'; @@ -54,6 +55,23 @@ describe('endowment:caip25', () => { expect(specification.endowmentGetter()).toBeNull(); }); + it('builds the caveat', () => { + expect( + Caip25CaveatFactoryFn({ + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }), + ).toStrictEqual({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }); + }); + describe('caveat mutator removeScope', () => { it('can remove a caveat', () => { const ethereumGoerliCaveat = { @@ -477,9 +495,11 @@ describe('endowment:caip25', () => { isChainIdSupported: expect.any(Function), }), ); - const isChainIdSupportedBody = - MockScopeAssert.assertScopesSupported.mock.calls[0][1].isChainIdSupported.toString(); - expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); + + MockScopeAssert.assertScopesSupported.mock.calls[0][1].isChainIdSupported( + '0x1', + ); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); }); it('asserts the validated and normalized optional scopes are supported', () => { @@ -543,9 +563,71 @@ describe('endowment:caip25', () => { isChainIdSupported: expect.any(Function), }), ); - const isChainIdSupportedBody = - MockScopeAssert.assertScopesSupported.mock.calls[1][1].isChainIdSupported.toString(); - expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); + MockScopeAssert.assertScopesSupported.mock.calls[1][1].isChainIdSupported( + '0x1', + ); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); + }); + + it('does not throw if unable to find a network client for the chainId', () => { + MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ + normalizedRequiredScopes: { + 'eip155:1': { + methods: ['normalized_required'], + notifications: [], + accounts: [], + }, + }, + normalizedOptionalScopes: { + 'eip155:5': { + methods: ['normalized_optional'], + notifications: [], + accounts: [], + }, + }, + }); + findNetworkClientIdByChainId.mockImplementation(() => { + throw new Error('unable to find network client'); + }); + try { + validator({ + caveats: [ + { + type: Caip25CaveatType, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_chainId'], + notifications: [], + accounts: ['eip155:1:0xdead'], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: [], + notifications: [], + accounts: ['eip155:5:0xbeef'], + }, + }, + isMultichainOrigin: true, + }, + }, + ], + date: 1234, + id: '1', + invoker: 'test.com', + parentCapability: Caip25EndowmentPermissionName, + }); + } catch (err) { + // noop + } + + expect( + MockScopeAssert.assertScopesSupported.mock.calls[0][1].isChainIdSupported( + '0x1', + ), + ).toBe(false); + expect(findNetworkClientIdByChainId).toHaveBeenCalledWith('0x1'); }); it('throws if the eth accounts specified in the normalized scopeObjects are not found in the wallet keyring', () => { diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index cec4acf4aa..76f41b9a44 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -1,3 +1,5 @@ +import type { CaipAccountId } from '@metamask/utils'; + import { KnownNotifications, KnownRpcMethods, @@ -5,6 +7,7 @@ import { KnownWalletRpcMethods, } from './constants'; import { + isSupportedAccount, isSupportedMethod, isSupportedNotification, isSupportedScopeString, @@ -54,6 +57,7 @@ describe('Scope Support', () => { it('returns false otherwise', () => { expect(isSupportedMethod('eip155', 'anything else')).toBe(false); + expect(isSupportedMethod('wallet:unknown', 'anything else')).toBe(false); expect(isSupportedMethod('', '')).toBe(false); }); }); @@ -71,6 +75,18 @@ describe('Scope Support', () => { expect(isSupportedScopeString('eip155', jest.fn())).toBe(true); }); + it('returns false for unknown namespaces', () => { + expect(isSupportedScopeString('unknown', jest.fn())).toBe(false); + }); + + it('returns true for the wallet namespace with eip155 reference', () => { + expect(isSupportedScopeString('wallet:eip155', jest.fn())).toBe(true); + }); + + it('returns false for the wallet namespace with eip155 reference', () => { + expect(isSupportedScopeString('wallet:eip155', jest.fn())).toBe(true); + }); + it('returns true for the ethereum namespace when a network client exists for the reference', () => { const isChainIdSupportedMock = jest.fn().mockReturnValue(true); expect(isSupportedScopeString('eip155:1', isChainIdSupportedMock)).toBe( @@ -85,4 +101,160 @@ describe('Scope Support', () => { ); }); }); + + describe('isSupportedAccount', () => { + it.each([ + [ + true, + 'eoa account matching eip155 namespaced address exists', + 'eip155:1:0xdeadbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + ], + ], + [ + true, + 'eoa account matching eip155 namespaced address with different casing exists', + 'eip155:1:0xDEADbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadBEEF', + }, + ], + ], + [ + true, + 'erc4337 account matching eip155 namespaced address exists', + 'eip155:1:0xdeadbeef', + [ + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ], + ], + [ + true, + 'erc4337 account matching eip155 namespaced address with different casing exists', + 'eip155:1:0xDEADbeef', + [ + { + type: 'eip155:erc4337', + address: '0xdeadBEEF', + }, + ], + ], + [ + false, + 'neither eoa or erc4337 account matching eip155 namespaced address exists', + 'eip155:1:0xdeadbeef', + [ + { + type: 'other', + address: '0xdeadbeef', + }, + ], + ], + + [ + true, + 'eoa account matching wallet:eip155 address exists', + 'wallet:eip155:0xdeadbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + ], + ], + [ + true, + 'eoa account matching wallet:eip155 address with different casing exists', + 'wallet:eip155:0xDEADbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadBEEF', + }, + ], + ], + [ + true, + 'erc4337 account matching wallet:eip155 address exists', + 'wallet:eip155:0xdeadbeef', + [ + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ], + ], + [ + true, + 'erc4337 account matching wallet:eip155 address with different casing exists', + 'wallet:eip155:0xDEADbeef', + [ + { + type: 'eip155:erc4337', + address: '0xdeadBEEF', + }, + ], + ], + [ + false, + 'neither eoa or erc4337 account matching wallet:eip155 address exists', + 'wallet:eip155:0xdeadbeef', + [ + { + type: 'other', + address: '0xdeadbeef', + }, + ], + ], + [ + false, + 'wallet namespace with unknown reference', + 'wallet:foobar:0xdeadbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ], + ], + [ + false, + 'unknown namespace', + 'foo:bar:0xdeadbeef', + [ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ], + ], + ])( + 'returns %s if %s', + (result, _desc, account, getInternalAccountsValue) => { + const getInternalAccounts = jest + .fn() + .mockReturnValue(getInternalAccountsValue); + expect( + isSupportedAccount(account as CaipAccountId, getInternalAccounts), + ).toBe(result); + }, + ); + }); }); diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 4cfe3fdbef..42f6ccd318 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -1,12 +1,6 @@ import { toHex, isEqualCaseInsensitive } from '@metamask/controller-utils'; -import type { CaipAccountId, Hex } from '@metamask/utils'; -import { - isCaipChainId, - isCaipNamespace, - KnownCaipNamespace, - parseCaipAccountId, - parseCaipChainId, -} from '@metamask/utils'; +import type { CaipAccountId, Hex, CaipChainId } from '@metamask/utils'; +import { KnownCaipNamespace, parseCaipAccountId } from '@metamask/utils'; import { KnownNotifications, @@ -21,36 +15,16 @@ export const isSupportedScopeString = ( scopeString: string, isChainIdSupported: (chainId: Hex) => boolean, ) => { - const isNamespaceScoped = isCaipNamespace(scopeString); - const isChainScoped = isCaipChainId(scopeString); - - if (isNamespaceScoped) { - switch (scopeString) { - case KnownCaipNamespace.Wallet: - return true; - case KnownCaipNamespace.Eip155: - return true; - default: - return false; - } - } + const { namespace, reference } = parseScopeString(scopeString as CaipChainId); - if (isChainScoped) { - const { namespace, reference } = parseCaipChainId(scopeString); - switch (namespace) { - case KnownCaipNamespace.Wallet: - if (reference === KnownCaipNamespace.Eip155) { - return true; - } - return false; - case KnownCaipNamespace.Eip155: - return isChainIdSupported(toHex(reference)); - default: - return false; - } + switch (namespace) { + case KnownCaipNamespace.Wallet: + return !reference || reference === KnownCaipNamespace.Eip155; + case KnownCaipNamespace.Eip155: + return !reference || isChainIdSupported(toHex(reference)); + default: + return false; } - - return false; }; export const isSupportedAccount = ( @@ -59,20 +33,23 @@ export const isSupportedAccount = ( ) => { const { address, - chain: { namespace }, + chain: { namespace, reference }, } = parseCaipAccountId(account); + + const isSupportedEip155Account = () => + getInternalAccounts().some( + (internalAccount) => + ['eip155:eoa', 'eip155:erc4337'].includes(internalAccount.type) && + isEqualCaseInsensitive(address, internalAccount.address), + ); + switch (namespace) { + case KnownCaipNamespace.Wallet: + return reference === KnownCaipNamespace.Eip155 + ? isSupportedEip155Account() + : false; case KnownCaipNamespace.Eip155: - try { - return getInternalAccounts().some( - (internalAccount) => - ['eip155:eoa', 'eip155:erc4337'].includes(internalAccount.type) && - isEqualCaseInsensitive(address, internalAccount.address), - ); - } catch (err) { - console.log('failed to check if account is supported by wallet', err); - } - return false; + return isSupportedEip155Account(); default: return false; } diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index afdd3ae2a3..ea19dab97f 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -168,6 +168,21 @@ describe('Scope Transform', () => { ...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', () => { @@ -201,6 +216,21 @@ describe('Scope Transform', () => { ...validScopeObject, rpcEndpoints: ['a', 'b', 'c'], }); + + expect( + mergeScopeObject( + { + ...validScopeObject, + }, + { + ...validScopeObject, + rpcEndpoints: ['a', 'b', 'c'], + }, + ), + ).toStrictEqual({ + ...validScopeObject, + rpcEndpoints: ['a', 'b', 'c'], + }); }); }); diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index 33bb343f3d..253e54a99b 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -37,6 +37,15 @@ describe('Scope Validation', () => { references: ['5'], }, ], + [ + false, + 'the scopeString is a valid CAIP namespace but references are invalid CAIP references', + 'eip155', + { + ...validScopeObject, + references: ['@'], + }, + ], [ false, 'methods contains empty string', From f0953702046b47ca3108c8874fc187aee9f24e69 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 23 Oct 2024 11:10:13 -0700 Subject: [PATCH 64/80] Remove accountsChanged and chainChanged from KnownNotifications (since they are implicitly granted now) --- packages/multichain/src/scope/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index ac1e4829d7..81fbd4db63 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -43,5 +43,5 @@ export const KnownWalletNamespaceRpcMethods: Record< // Notifications export const KnownNotifications: Record = { - eip155: ['accountsChanged', 'chainChanged', 'eth_subscription'], + eip155: ['eth_subscription'], }; From 491552f21512e222c8b387124039e240b25640de Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 24 Oct 2024 15:11:57 -0700 Subject: [PATCH 65/80] Update packages/multichain/src/scope/transform.test.ts Co-authored-by: Elliot Winkler --- packages/multichain/src/scope/transform.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index ea19dab97f..a98c2986dd 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -52,16 +52,16 @@ describe('Scope Transform', () => { }); it('returns one deep cloned scope per `references` element', () => { - const noramlizedScopes = normalizeScope('eip155', { + const normalizedScopes = normalizeScope('eip155', { ...validScopeObject, references: ['1', '5'], }); - expect(noramlizedScopes['eip155:1']).not.toBe( - noramlizedScopes['eip155:5'], + expect(normalizedScopes['eip155:1']).not.toBe( + normalizedScopes['eip155:5'], ); - expect(noramlizedScopes['eip155:1'].methods).not.toBe( - noramlizedScopes['eip155:5'].methods, + expect(normalizedScopes['eip155:1'].methods).not.toBe( + normalizedScopes['eip155:5'].methods, ); }); }); From 0d167a3d4239ed5734f38ac906b8226985ba35d0 Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 21 Nov 2024 14:26:01 -0800 Subject: [PATCH 66/80] CAIP Multichain API with permission refactor changes (#4961) ## Explanation ~~This diff is a bit messy since it also brings in the [core PR that refactors the CAIP-25 permission](https://github.com/MetaMask/core/pull/4950) which the [caip-multichain-api](https://github.com/MetaMask/core/tree/caip-multichain-api) branch doesn't have yet.~~ The main thing this PR does is adds `getSessionScopes()` which massages the new CAIP-25 permission into a `NormalizedScopesObject` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --------- Co-authored-by: Alex Donesky --- packages/multichain/package.json | 2 +- .../caip-permission-adapter-middleware.ts | 18 ++-- ...-permission-adapter-session-scopes.test.ts | 87 +++++++++++++++++++ .../caip-permission-adapter-session-scopes.ts | 78 +++++++++++++++++ .../src/handlers/wallet-getSession.test.ts | 66 ++++++++++++-- .../src/handlers/wallet-getSession.ts | 11 +-- .../src/handlers/wallet-invokeMethod.test.ts | 81 +++++++++++++---- .../src/handlers/wallet-invokeMethod.ts | 14 +-- packages/multichain/src/index.test.ts | 17 ++++ packages/multichain/src/index.ts | 4 +- packages/multichain/src/scope/assert.test.ts | 45 +++++++++- packages/multichain/src/scope/assert.ts | 2 +- .../src/scope/authorization.test.ts | 6 ++ .../multichain/src/scope/authorization.ts | 10 +-- packages/multichain/src/scope/filter.ts | 10 +-- packages/multichain/src/scope/types.ts | 6 +- yarn.lock | 15 +--- 17 files changed, 393 insertions(+), 79 deletions(-) create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts create mode 100644 packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 5a90288e99..1daaab643f 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -59,7 +59,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/json-rpc-engine": "^9.0.3", + "@metamask/json-rpc-engine": "^10.0.1", "@metamask/network-controller": "^22.0.2", "@metamask/permission-controller": "^11.0.3", "@open-rpc/meta-schema": "^1.14.6", diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 865a4cccfc..12402b17f9 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -11,8 +11,9 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import { KnownWalletScopeString, type ScopeString } from '../scope/types'; +import { KnownWalletScopeString } from '../scope/constants'; +import type { InternalScopeString } from '../scope/types'; +import { getSessionScopes } from './caip-permission-adapter-session-scopes'; /** * Middleware to handle CAIP-25 permission requests. @@ -61,17 +62,14 @@ export async function caipPermissionAdapterMiddleware( const { chainId } = hooks.getNetworkConfigurationByNetworkClientId(networkClientId); - const scope: ScopeString = `eip155:${parseInt(chainId, 16)}`; + const scope: InternalScopeString = `eip155:${parseInt(chainId, 16)}`; - const scopesObject = mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - ); + const sesionScopes = getSessionScopes(caveat.value); if ( - !scopesObject[scope]?.methods?.includes(method) && - !scopesObject[KnownWalletScopeString.Eip155]?.methods?.includes(method) && - !scopesObject.wallet?.methods?.includes(method) + !sesionScopes[scope]?.methods?.includes(method) && + !sesionScopes[KnownWalletScopeString.Eip155]?.methods?.includes(method) && + !sesionScopes.wallet?.methods?.includes(method) ) { return end(providerErrors.unauthorized()); } 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 new file mode 100644 index 0000000000..05f924d942 --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts @@ -0,0 +1,87 @@ +import { + KnownNotifications, + KnownRpcMethods, + KnownWalletNamespaceRpcMethods, + KnownWalletRpcMethods, +} from '../scope/constants'; +import { getSessionScopes } from './caip-permission-adapter-session-scopes'; + +describe('CAIP-25 session scopes adapters', () => { + describe('getSessionScopes', () => { + it('returns a NormalizedScopesObject for the wallet scope', () => { + const result = getSessionScopes({ + requiredScopes: {}, + optionalScopes: { + wallet: { + accounts: [], + }, + }, + }); + + 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'], + }, + }, + }); + + expect(result).toStrictEqual({ + 'wallet:eip155': { + methods: KnownWalletNamespaceRpcMethods.eip155, + notifications: [], + accounts: ['wallet:eip155:0xdeadbeef'], + }, + }); + }); + + it('returns a NormalizedScopesObject with empty methods and notifications for scope with wallet namespace and unknown reference', () => { + const result = getSessionScopes({ + requiredScopes: {}, + optionalScopes: { + 'wallet:foobar': { + accounts: ['wallet:foobar:0xdeadbeef'], + }, + }, + }); + + expect(result).toStrictEqual({ + 'wallet:foobar': { + methods: [], + notifications: [], + accounts: ['wallet:foobar:0xdeadbeef'], + }, + }); + }); + + it('returns a NormalizedScopesObject for a eip155 namespaced scope', () => { + const result = getSessionScopes({ + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + accounts: ['eip155:1:0xdeadbeef'], + }, + }, + }); + + 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 new file mode 100644 index 0000000000..8de3698bbb --- /dev/null +++ b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts @@ -0,0 +1,78 @@ +import { KnownCaipNamespace } from '@metamask/utils'; + +import type { Caip25CaveatValue } from '../caip25Permission'; +import { + KnownNotifications, + KnownRpcMethods, + KnownWalletNamespaceRpcMethods, + KnownWalletRpcMethods, +} from '../scope/constants'; +import { mergeScopes } from '../scope/transform'; +import type { + InternalScopesObject, + NonWalletKnownCaipNamespace, + NormalizedScopesObject, +} from '../scope/types'; +import { parseScopeString } from '../scope/types'; + +/** + * Converts an InternalScopesObject to a NormalizedScopesObject. + * @param internalScopesObject - The InternalScopesObject to convert. + * @returns A NormalizedScopesObject. + */ +const getNormalizedScopesObject = ( + internalScopesObject: InternalScopesObject, +) => { + 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 (namespace === KnownCaipNamespace.Wallet) { + if (reference) { + methods = + KnownWalletNamespaceRpcMethods[ + reference as NonWalletKnownCaipNamespace + ] ?? []; + } else { + methods = KnownWalletRpcMethods; + } + } else { + methods = + KnownRpcMethods[namespace as NonWalletKnownCaipNamespace] ?? []; + notifications = + KnownNotifications[namespace as NonWalletKnownCaipNamespace] ?? []; + } + + 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. + * @returns A NormalizedScopesObject. + */ +export const getSessionScopes = ( + caip25CaveatValue: Pick< + Caip25CaveatValue, + 'requiredScopes' | 'optionalScopes' + >, +) => { + return mergeScopes( + getNormalizedScopesObject(caip25CaveatValue.requiredScopes), + getNormalizedScopesObject(caip25CaveatValue.optionalScopes), + ); +}; diff --git a/packages/multichain/src/handlers/wallet-getSession.test.ts b/packages/multichain/src/handlers/wallet-getSession.test.ts index ca74cc9b90..2583394203 100644 --- a/packages/multichain/src/handlers/wallet-getSession.test.ts +++ b/packages/multichain/src/handlers/wallet-getSession.test.ts @@ -1,11 +1,19 @@ import type { JsonRpcRequest } from '@metamask/utils'; +import * as PermissionAdapterSessionScopes from '../adapters/caip-permission-adapter-session-scopes'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; import { walletGetSession } from './wallet-getSession'; +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, @@ -21,25 +29,17 @@ const createMockedHandler = () => { value: { requiredScopes: { 'eip155:1': { - methods: ['eth_call'], - notifications: [], accounts: [], }, 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], accounts: [], }, }, optionalScopes: { 'eip155:1': { - methods: ['net_version'], - notifications: ['chainChanged'], accounts: [], }, wallet: { - methods: ['wallet_watchAsset'], - notifications: [], accounts: [], }, }, @@ -67,6 +67,10 @@ const createMockedHandler = () => { }; describe('wallet_getSession', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { const { handler, getCaveat } = createMockedHandler(); @@ -90,9 +94,53 @@ describe('wallet_getSession', () => { }); }); - it('returns the merged scopes', async () => { + it('gets the session scopes from the CAIP-25 caveat value', async () => { + const { handler } = createMockedHandler(); + + await handler(baseRequest); + expect( + MockPermissionAdapterSessionScopes.getSessionScopes, + ).toHaveBeenCalledWith({ + requiredScopes: { + 'eip155:1': { + accounts: [], + }, + 'eip155:5': { + accounts: [], + }, + }, + optionalScopes: { + 'eip155:1': { + accounts: [], + }, + wallet: { + accounts: [], + }, + }, + }); + }); + + 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: { diff --git a/packages/multichain/src/handlers/wallet-getSession.ts b/packages/multichain/src/handlers/wallet-getSession.ts index ad3e4e5569..6a8f4bf2fb 100644 --- a/packages/multichain/src/handlers/wallet-getSession.ts +++ b/packages/multichain/src/handlers/wallet-getSession.ts @@ -1,13 +1,13 @@ import type { Caveat } from '@metamask/permission-controller'; import type { JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; +import type { NormalizedScopesObject } from 'src/scope/types'; +import { getSessionScopes } from '../adapters/caip-permission-adapter-session-scopes'; import type { Caip25CaveatValue } from '../caip25Permission'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import type { ScopesObject } from '../scope/types'; /** * Handler for the `wallet_getSession` RPC method. @@ -21,7 +21,7 @@ import type { ScopesObject } from '../scope/types'; */ async function walletGetSessionHandler( request: JsonRpcRequest & { origin: string }, - response: JsonRpcSuccess<{ sessionScopes: ScopesObject }>, + response: JsonRpcSuccess<{ sessionScopes: NormalizedScopesObject }>, _next: () => void, end: () => void, hooks: { @@ -49,10 +49,7 @@ async function walletGetSessionHandler( } response.result = { - sessionScopes: mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - ), + sessionScopes: getSessionScopes(caveat.value), }; return end(); } diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts index 208bccc337..a608e2866f 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.test.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.test.ts @@ -1,12 +1,20 @@ import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { JsonRpcRequest } from '@metamask/utils'; +import * as PermissionAdapterSessionScopes from '../adapters/caip-permission-adapter-session-scopes'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; import { walletInvokeMethod } from './wallet-invokeMethod'; +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, @@ -30,30 +38,17 @@ const createMockedHandler = () => { value: { requiredScopes: { 'eip155:1': { - methods: ['eth_call'], - notifications: [], accounts: [], }, 'eip155:5': { - methods: ['eth_chainId'], - notifications: [], accounts: [], }, }, optionalScopes: { 'eip155:1': { - methods: ['net_version'], - notifications: [], accounts: [], }, wallet: { - methods: ['wallet_watchAsset'], - notifications: [], - accounts: [], - }, - unhandled: { - methods: ['foobar'], - notifications: [], accounts: [], }, }, @@ -88,6 +83,35 @@ const createMockedHandler = () => { }; 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: [], + }, + 'unknown:scope': { + methods: ['foobar'], + notifications: [], + accounts: [], + }, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + it('gets the authorized scopes from the CAIP-25 endowment permission', async () => { const request = createMockedRequest(); const { handler, getCaveat } = createMockedHandler(); @@ -99,6 +123,33 @@ describe('wallet_invokeMethod', () => { ); }); + it('gets the session scopes from the CAIP-25 caveat value', async () => { + const request = createMockedRequest(); + const { handler } = createMockedHandler(); + await handler(request); + expect( + MockPermissionAdapterSessionScopes.getSessionScopes, + ).toHaveBeenCalledWith({ + requiredScopes: { + 'eip155:1': { + accounts: [], + }, + 'eip155:5': { + accounts: [], + }, + }, + optionalScopes: { + 'eip155:1': { + accounts: [], + }, + wallet: { + accounts: [], + }, + }, + isMultichainOrigin: true, + }); + }); + it('throws an unauthorized error when there is no CAIP-25 endowment permission', async () => { const request = createMockedRequest(); const { handler, getCaveat, end } = createMockedHandler(); @@ -152,7 +203,7 @@ describe('wallet_invokeMethod', () => { expect(end).toHaveBeenCalledWith(providerErrors.unauthorized()); }); - it('throws an internal error for authorized but unhandled scopes', async () => { + it('throws an internal error for authorized but unsupported scopes', async () => { const request = createMockedRequest(); const { handler, end } = createMockedHandler(); @@ -160,7 +211,7 @@ describe('wallet_invokeMethod', () => { ...request, params: { ...request.params, - scope: 'unhandled', + scope: 'unknown:scope', request: { ...request.params.request, method: 'foobar', diff --git a/packages/multichain/src/handlers/wallet-invokeMethod.ts b/packages/multichain/src/handlers/wallet-invokeMethod.ts index 3df3ca8346..4e9f6002bd 100644 --- a/packages/multichain/src/handlers/wallet-invokeMethod.ts +++ b/packages/multichain/src/handlers/wallet-invokeMethod.ts @@ -7,13 +7,14 @@ import type { } from '@metamask/utils'; import { numberToHex } from '@metamask/utils'; +import { getSessionScopes } from '../adapters/caip-permission-adapter-session-scopes'; import type { Caip25CaveatValue } from '../caip25Permission'; import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { mergeScopes } from '../scope/transform'; -import type { ScopeString } from '../scope/types'; +import { assertIsInternalScopeString } from '../scope/assert'; +import type { ExternalScopeString } from '../scope/types'; import { parseScopeString } from '../scope/types'; /** @@ -44,10 +45,12 @@ async function walletInvokeMethodHandler( }, ) { const { scope, request: wrappedRequest } = request.params as { - scope: ScopeString; + scope: ExternalScopeString; request: JsonRpcRequest; }; + assertIsInternalScopeString(scope); + let caveat; try { caveat = hooks.getCaveat( @@ -62,10 +65,7 @@ async function walletInvokeMethodHandler( return end(providerErrors.unauthorized()); } - const scopeObject = mergeScopes( - caveat.value.requiredScopes, - caveat.value.optionalScopes, - )[scope]; + const scopeObject = getSessionScopes(caveat.value)[scope]; if (!scopeObject?.methods?.includes(wrappedRequest.method)) { return end(providerErrors.unauthorized()); diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 61f0fdcc42..296dbb912b 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -9,17 +9,34 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", + "walletGetSession", + "walletInvokeMethod", + "walletRevokeSession", + "multichainMethodCallValidatorMiddleware", + "MultichainMiddlewareManager", + "MultichainSubscriptionManager", + "assertScopeSupported", + "assertScopesSupported", "validateAndNormalizeScopes", + "bucketScopes", + "bucketScopesBySupport", + "filterScopesSupported", "KnownWalletRpcMethods", "KnownRpcMethods", "KnownWalletNamespaceRpcMethods", "KnownNotifications", "KnownWalletScopeString", "parseScopeString", + "isSupportedScopeString", + "isSupportedAccount", + "isSupportedMethod", + "isSupportedNotification", "normalizeScope", "mergeScopeObject", "mergeScopes", "normalizeAndMergeScopes", + "isValidScope", + "getValidScopes", "Caip25CaveatType", "createCaip25Caveat", "Caip25EndowmentPermissionName", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 82281c9198..481def28f1 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -42,7 +42,7 @@ export type { ScopedProperties, NonWalletKnownCaipNamespace, } from './scope/types'; -export { parseScopeString, KnownWalletScopeString } from './scope/types'; +export { parseScopeString } from './scope/types'; // Do these need to be exported? export { isSupportedScopeString, @@ -57,7 +57,7 @@ export { normalizeAndMergeScopes, } from './scope/transform'; // does this need to be exported? -export { isValidScope, validateScopes } from './scope/validation'; +export { isValidScope, getValidScopes } from './scope/validation'; export type { Caip25CaveatValue } from './caip25Permission'; export { diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 0fd23b5bf6..2b27abd672 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -5,6 +5,7 @@ import { assertScopesSupported, assertIsExternalScopesObject, assertIsInternalScopesObject, + assertIsInternalScopeString, } from './assert'; import { Caip25Errors } from './errors'; import * as Supported from './supported'; @@ -18,6 +19,7 @@ jest.mock('./supported', () => ({ jest.mock('@metamask/utils', () => ({ ...jest.requireActual('@metamask/utils'), + isCaipChainId: jest.fn(), isCaipReference: jest.fn(), isCaipAccountId: jest.fn(), })); @@ -33,6 +35,7 @@ const validScopeObject: NormalizedScopeObject = { describe('Scope Assert', () => { beforeEach(() => { + MockUtils.isCaipChainId.mockImplementation(() => true); MockUtils.isCaipReference.mockImplementation(() => true); MockUtils.isCaipAccountId.mockImplementation(() => true); }); @@ -261,7 +264,7 @@ describe('Scope Assert', () => { }); it('throws an error if passed an object with a key that is not a valid ExternalScopeString', () => { - jest.spyOn(Utils, 'isCaipReference').mockImplementation(() => false); + MockUtils.isCaipChainId.mockReturnValue(false); expect(() => assertIsExternalScopesObject({ 'invalid-scope-string': {} }), @@ -480,6 +483,44 @@ describe('Scope Assert', () => { }); }); + 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 = { @@ -509,7 +550,7 @@ describe('Scope Assert', () => { }); it('throws an error if passed an object with a key that is not a valid InternalScopeString', () => { - jest.spyOn(Utils, 'isCaipReference').mockImplementation(() => false); + MockUtils.isCaipChainId.mockReturnValue(false); expect(() => assertIsInternalScopesObject({ 'invalid-scope-string': {} }), diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 0d2c8c16cb..873c577575 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -219,7 +219,7 @@ function assertIsInternalScopeObject( * Asserts that a scope string is a valid InternalScopeString. * @param scopeString - The scope string to assert. */ -function assertIsInternalScopeString( +export function assertIsInternalScopeString( scopeString: unknown, ): asserts scopeString is InternalScopeString { if ( diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index 57555dfbce..2514c630dc 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -1,8 +1,14 @@ 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(), })); diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index f26e4798d4..b3f00cba09 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -1,5 +1,6 @@ import type { Hex, Json } from '@metamask/utils'; +import { bucketScopesBySupport } from './filter'; import { normalizeAndMergeScopes } from './transform'; import type { ExternalScopesObject, @@ -7,7 +8,6 @@ import type { NormalizedScopesObject, } from './types'; import { getValidScopes } from './validation'; -import { bucketScopesBySupport } from './filter'; /** * Represents the parameters of a [CAIP-25](https://chainagnostic.org/CAIPs/caip-25) request. @@ -54,7 +54,7 @@ export const validateAndNormalizeScopes = ( }; export const bucketScopes = ( - scopes: InternalScopesObject, + scopes: NormalizedScopesObject, { isChainIdSupported, isChainIdSupportable, @@ -63,9 +63,9 @@ export const bucketScopes = ( isChainIdSupportable: (chainId: Hex) => boolean; }, ): { - supportedScopes: InternalScopesObject; - supportableScopes: InternalScopesObject; - unsupportableScopes: InternalScopesObject; + supportedScopes: NormalizedScopesObject; + supportableScopes: NormalizedScopesObject; + unsupportableScopes: NormalizedScopesObject; } => { const { supportedScopes, unsupportedScopes: maybeSupportableScopes } = bucketScopesBySupport(scopes, { diff --git a/packages/multichain/src/scope/filter.ts b/packages/multichain/src/scope/filter.ts index 6854458398..b8ab92f805 100644 --- a/packages/multichain/src/scope/filter.ts +++ b/packages/multichain/src/scope/filter.ts @@ -1,18 +1,18 @@ import type { CaipChainId, Hex } from '@metamask/utils'; import { assertScopeSupported } from './assert'; -import type { InternalScopesObject } from './types'; +import type { NormalizedScopesObject } from './types'; export const bucketScopesBySupport = ( - scopes: InternalScopesObject, + scopes: NormalizedScopesObject, { isChainIdSupported, }: { isChainIdSupported: (chainId: Hex) => boolean; }, ) => { - const supportedScopes: InternalScopesObject = {}; - const unsupportedScopes: InternalScopesObject = {}; + const supportedScopes: NormalizedScopesObject = {}; + const unsupportedScopes: NormalizedScopesObject = {}; for (const [scopeString, scopeObject] of Object.entries(scopes)) { try { @@ -29,7 +29,7 @@ export const bucketScopesBySupport = ( }; export const filterScopesSupported = ( - scopes: InternalScopesObject, + scopes: NormalizedScopesObject, { isChainIdSupported, }: { diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index f639c58d2e..b13b5edae7 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -60,7 +60,8 @@ export type InternalScopesObject = Record & { * [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 read chain specific permissions. + * 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[]; @@ -74,7 +75,8 @@ export type NormalizedScopeObject = { * [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 read chain specific permissions. + * 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, diff --git a/yarn.lock b/yarn.lock index da7bc2a801..8c1a8b9861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2932,17 +2932,6 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^9.0.3": - version: 9.0.3 - resolution: "@metamask/json-rpc-engine@npm:9.0.3" - dependencies: - "@metamask/rpc-errors": "npm:^6.3.1" - "@metamask/safe-event-emitter": "npm:^3.0.0" - "@metamask/utils": "npm:^9.1.0" - checksum: 10/23a3cafb5869f6d5867105e3570ac4e214a72dda0b4b428cde6bae8856ec838c822b174f8cea054108122531d662cf93a65e92e1ee07da0485d5d0c0e5a1fca6 - languageName: node - linkType: hard - "@metamask/json-rpc-middleware-stream@npm:^8.0.5, @metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream": version: 0.0.0-use.local resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" @@ -3087,7 +3076,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/json-rpc-engine": "npm:^9.0.3" + "@metamask/json-rpc-engine": "npm:^10.0.1" "@metamask/network-controller": "npm:^22.0.2" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" @@ -3495,7 +3484,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/rpc-errors@npm:^6.2.1, @metamask/rpc-errors@npm:^6.3.1": +"@metamask/rpc-errors@npm:^6.2.1": version: 6.3.1 resolution: "@metamask/rpc-errors@npm:6.3.1" dependencies: From 17af3953b1595d7ec7466e07aabc38b467903f3e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 21 Nov 2024 15:44:03 -0800 Subject: [PATCH 67/80] export caipPermissionAdapterMiddleware --- packages/multichain/src/index.test.ts | 1 + packages/multichain/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 296dbb912b..654d802d8f 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -9,6 +9,7 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", + "caipPermissionAdapterMiddleware", "walletGetSession", "walletInvokeMethod", "walletRevokeSession", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 481def28f1..0260e26976 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -7,6 +7,7 @@ export { addPermittedEthChainId, setPermittedEthChainIds, } from './adapters/caip-permission-adapter-permittedChains'; +export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware'; export { walletGetSession } from './handlers/wallet-getSession'; export { walletInvokeMethod } from './handlers/wallet-invokeMethod'; From 62975b62dfcbd7baff08d27b9eccf5b67cae667a Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 21 Nov 2024 15:46:57 -0800 Subject: [PATCH 68/80] Caip multichain api normalized to internal (#4964) ## Explanation * Add getInternalScopesObject * Export getInternalScopesObject and getSessionScopes --------- Co-authored-by: Alex Donesky --- ...-permission-adapter-session-scopes.test.ts | 31 ++++++++++++++++++- .../caip-permission-adapter-session-scopes.ts | 23 ++++++++++++++ packages/multichain/src/index.test.ts | 2 ++ packages/multichain/src/index.ts | 4 +++ 4 files changed, 59 insertions(+), 1 deletion(-) 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 index 05f924d942..8dd377f0ed 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.test.ts @@ -4,9 +4,38 @@ import { KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, } from '../scope/constants'; -import { getSessionScopes } from './caip-permission-adapter-session-scopes'; +import { + getInternalScopesObject, + getSessionScopes, +} from './caip-permission-adapter-session-scopes'; 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', () => { it('returns a NormalizedScopesObject for the wallet scope', () => { const result = getSessionScopes({ diff --git a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts index 8de3698bbb..7e05eb01ad 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-session-scopes.ts @@ -15,6 +15,29 @@ import type { } 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. diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 654d802d8f..d7b6fed382 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -9,6 +9,8 @@ describe('@metamask/multichain', () => { "getPermittedEthChainIds", "addPermittedEthChainId", "setPermittedEthChainIds", + "getInternalScopesObject", + "getSessionScopes", "caipPermissionAdapterMiddleware", "walletGetSession", "walletInvokeMethod", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 0260e26976..f227f2817a 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -7,6 +7,10 @@ export { addPermittedEthChainId, setPermittedEthChainIds, } from './adapters/caip-permission-adapter-permittedChains'; +export { + getInternalScopesObject, + getSessionScopes, +} from './adapters/caip-permission-adapter-session-scopes'; export { caipPermissionAdapterMiddleware } from './adapters/caip-permission-adapter-middleware'; export { walletGetSession } from './handlers/wallet-getSession'; From 19a5983c4482d2c58f2dccf2f2880e9550e9fbaa Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 22 Nov 2024 08:46:21 -0800 Subject: [PATCH 69/80] Filter out eip1193 only methods from Eip155Methods --- packages/multichain/src/scope/constants.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index 6a9427fd4f..fc66b28e64 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -28,13 +28,25 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; +const Eip1193OnlyMethods = [ + 'wallet_switchEthereumChain', + 'wallet_getPermissions', + 'wallet_requestPermissions', + 'wallet_revokePermissions', + 'eth_requestAccounts', + 'eth_accounts', + 'eth_coinbase', + 'net_version', +] + /** * 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) => !KnownWalletRpcMethods.includes(method)) + .filter((method: string) => !Eip1193OnlyMethods.includes(method)) /** * Methods by ecosystem that are chain specific. From 7d61af678e5ffbf10f4c62ecbfa176df0a140bb9 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 22 Nov 2024 10:31:08 -0800 Subject: [PATCH 70/80] export Eip1193OnlyMethods --- packages/multichain/src/index.test.ts | 1 + packages/multichain/src/index.ts | 1 + packages/multichain/src/scope/constants.test.ts | 7 ------- packages/multichain/src/scope/constants.ts | 2 +- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index d7b6fed382..7d929e5f55 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -29,6 +29,7 @@ describe('@metamask/multichain', () => { "KnownWalletNamespaceRpcMethods", "KnownNotifications", "KnownWalletScopeString", + "Eip1193OnlyMethods", "parseScopeString", "isSupportedScopeString", "isSupportedAccount", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index f227f2817a..e14f0b2e47 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -34,6 +34,7 @@ export { KnownWalletNamespaceRpcMethods, KnownNotifications, KnownWalletScopeString, + Eip1193OnlyMethods, } from './scope/constants'; export type { ExternalScopeString, diff --git a/packages/multichain/src/scope/constants.test.ts b/packages/multichain/src/scope/constants.test.ts index 8369ec721a..362bead5a7 100644 --- a/packages/multichain/src/scope/constants.test.ts +++ b/packages/multichain/src/scope/constants.test.ts @@ -6,15 +6,9 @@ describe('KnownRpcMethods', () => { Object { "bip122": Array [], "eip155": Array [ - "wallet_switchEthereumChain", - "wallet_getPermissions", - "wallet_requestPermissions", - "wallet_revokePermissions", "personal_sign", "eth_signTypedData_v4", "wallet_watchAsset", - "eth_requestAccounts", - "eth_accounts", "eth_sendTransaction", "eth_decrypt", "eth_getEncryptionPublicKey", @@ -24,7 +18,6 @@ describe('KnownRpcMethods', () => { "eth_blockNumber", "eth_call", "eth_chainId", - "eth_coinbase", "eth_estimateGas", "eth_feeHistory", "eth_gasPrice", diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index fc66b28e64..1f9400c892 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -28,7 +28,7 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; -const Eip1193OnlyMethods = [ +export const Eip1193OnlyMethods = [ 'wallet_switchEthereumChain', 'wallet_getPermissions', 'wallet_requestPermissions', From 97b4a095f89b71370bb378d2a6a370eaa9550fb2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 22 Nov 2024 10:37:25 -0800 Subject: [PATCH 71/80] Revert "export Eip1193OnlyMethods" This reverts commit 7d61af678e5ffbf10f4c62ecbfa176df0a140bb9. --- packages/multichain/src/index.test.ts | 1 - packages/multichain/src/index.ts | 1 - packages/multichain/src/scope/constants.test.ts | 7 +++++++ packages/multichain/src/scope/constants.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 7d929e5f55..d7b6fed382 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -29,7 +29,6 @@ describe('@metamask/multichain', () => { "KnownWalletNamespaceRpcMethods", "KnownNotifications", "KnownWalletScopeString", - "Eip1193OnlyMethods", "parseScopeString", "isSupportedScopeString", "isSupportedAccount", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index e14f0b2e47..f227f2817a 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -34,7 +34,6 @@ export { KnownWalletNamespaceRpcMethods, KnownNotifications, KnownWalletScopeString, - Eip1193OnlyMethods, } from './scope/constants'; export type { ExternalScopeString, diff --git a/packages/multichain/src/scope/constants.test.ts b/packages/multichain/src/scope/constants.test.ts index 362bead5a7..8369ec721a 100644 --- a/packages/multichain/src/scope/constants.test.ts +++ b/packages/multichain/src/scope/constants.test.ts @@ -6,9 +6,15 @@ describe('KnownRpcMethods', () => { Object { "bip122": Array [], "eip155": Array [ + "wallet_switchEthereumChain", + "wallet_getPermissions", + "wallet_requestPermissions", + "wallet_revokePermissions", "personal_sign", "eth_signTypedData_v4", "wallet_watchAsset", + "eth_requestAccounts", + "eth_accounts", "eth_sendTransaction", "eth_decrypt", "eth_getEncryptionPublicKey", @@ -18,6 +24,7 @@ describe('KnownRpcMethods', () => { "eth_blockNumber", "eth_call", "eth_chainId", + "eth_coinbase", "eth_estimateGas", "eth_feeHistory", "eth_gasPrice", diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index 1f9400c892..fc66b28e64 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -28,7 +28,7 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; -export const Eip1193OnlyMethods = [ +const Eip1193OnlyMethods = [ 'wallet_switchEthereumChain', 'wallet_getPermissions', 'wallet_requestPermissions', From 6732cd9ae6774395cf9cc46e107decde53aea3b3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 22 Nov 2024 10:38:55 -0800 Subject: [PATCH 72/80] use Eip1193OnlyMethods in adapter middleware --- .../src/adapters/caip-permission-adapter-middleware.ts | 5 +++-- packages/multichain/src/scope/constants.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts index 12402b17f9..89b47e8f81 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-middleware.ts @@ -11,7 +11,7 @@ import { Caip25CaveatType, Caip25EndowmentPermissionName, } from '../caip25Permission'; -import { KnownWalletScopeString } from '../scope/constants'; +import { Eip1193OnlyMethods, KnownWalletScopeString } from '../scope/constants'; import type { InternalScopeString } from '../scope/types'; import { getSessionScopes } from './caip-permission-adapter-session-scopes'; @@ -69,7 +69,8 @@ export async function caipPermissionAdapterMiddleware( if ( !sesionScopes[scope]?.methods?.includes(method) && !sesionScopes[KnownWalletScopeString.Eip155]?.methods?.includes(method) && - !sesionScopes.wallet?.methods?.includes(method) + !sesionScopes.wallet?.methods?.includes(method) && + !Eip1193OnlyMethods.includes(method) ) { return end(providerErrors.unauthorized()); } diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index fc66b28e64..c121aeeff7 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -28,7 +28,7 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; -const Eip1193OnlyMethods = [ +export const Eip1193OnlyMethods = [ 'wallet_switchEthereumChain', 'wallet_getPermissions', 'wallet_requestPermissions', @@ -37,7 +37,7 @@ const Eip1193OnlyMethods = [ 'eth_accounts', 'eth_coinbase', 'net_version', -] +]; /** * All MetaMask methods, except for ones we have specified in the constants above. @@ -46,7 +46,7 @@ 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)) + .filter((method: string) => !Eip1193OnlyMethods.includes(method)); /** * Methods by ecosystem that are chain specific. From e03033c83610ba0371b665fc16555b59831234a0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 22 Nov 2024 10:58:31 -0800 Subject: [PATCH 73/80] expand Eip1193OnlyMethods to include meta methods --- packages/multichain/src/scope/constants.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index c121aeeff7..ea1291f7b9 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -37,6 +37,10 @@ export const Eip1193OnlyMethods = [ 'eth_accounts', 'eth_coinbase', 'net_version', + 'metamask_logWeb3ShimUsage', + 'metamask_getProviderState', + 'metamask_sendDomainMetadata', + 'wallet_registerOnboarding', ]; /** From e1793ff003e960530bd4c8d30ffd30d749094933 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 10:57:35 -0600 Subject: [PATCH 74/80] cleanup + add tests for MultichainSubscriptionManager --- .../MultichainSubscriptionManager.test.ts | 40 +++++++++++++++++++ .../MultichainSubscriptionManager.ts | 15 +++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts index cf05aa5c5a..f08182d026 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.test.ts @@ -63,6 +63,26 @@ describe('MultichainSubscriptionManager', () => { 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(); @@ -105,6 +125,26 @@ describe('MultichainSubscriptionManager', () => { 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, + ); + mockSubscriptionManager.events.on.mock.calls[0][1]( + newHeadsNotificationMock, + ); + + expect(mockSubscriptionManager.destroy).not.toHaveBeenCalled(); + }); + it('should unsubscribe from a origin and tabId', () => { const { multichainSubscriptionManager } = createMultichainSubscriptionManager(); diff --git a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts index 6494ed24a5..e3b9cbfb76 100644 --- a/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts +++ b/packages/multichain/src/middlewares/MultichainSubscriptionManager.ts @@ -76,7 +76,7 @@ export class MultichainSubscriptionManager extends SafeEventEmitter { }); } - #removeSubscriptionEntry({ scope, origin, tabId }: SubscriptionKey) { + #removeSubscriptionEntry({ scope, origin, tabId }: SubscriptionEntry) { this.#subscriptions = this.#subscriptions.filter((subscriptionEntry) => { return ( subscriptionEntry.scope !== scope || @@ -116,16 +116,10 @@ export class MultichainSubscriptionManager extends SafeEventEmitter { return subscriptionManager; } - #unsubscribe(subscriptionKey: SubscriptionKey) { - const existingSubscriptionEntry = - this.#getSubscriptionEntry(subscriptionKey); - if (!existingSubscriptionEntry) { - return; - } - - existingSubscriptionEntry.subscriptionManager.destroy?.(); + #unsubscribe(subscriptionEntry: SubscriptionEntry) { + subscriptionEntry.subscriptionManager.destroy?.(); - this.#removeSubscriptionEntry(subscriptionKey); + this.#removeSubscriptionEntry(subscriptionEntry); } unsubscribeByScope(scope: ExternalScopeString) { @@ -137,6 +131,7 @@ export class MultichainSubscriptionManager extends SafeEventEmitter { } unsubscribeByScopeAndOrigin(scope: ExternalScopeString, origin: string) { + console.log('unsubscribing by scope and origin', scope, origin); this.#subscriptions.forEach((subscriptionEntry) => { if ( subscriptionEntry.scope === scope && From 4c9be0e0d823dd16a1e200314330aa39f24d4166 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 14:08:02 -0600 Subject: [PATCH 75/80] adding tests for multichainMethodCallValidator --- .../multichainMethodCallValidator.test.ts | 411 ++++++++++++++++++ .../multichainMethodCallValidator.ts | 28 +- .../multichain/src/scope/constants.test.ts | 7 - packages/multichain/src/scope/types.ts | 58 +++ 4 files changed, 496 insertions(+), 8 deletions(-) create mode 100644 packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts new file mode 100644 index 0000000000..c7e9a89d99 --- /dev/null +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts @@ -0,0 +1,411 @@ +import type { + JsonRpcError, + JsonRpcRequest, + JsonRpcResponse, +} from '@metamask/utils'; + +import type { + Caip27Params, + Caip285Params, + Caip319Params, +} from '../scope/types'; +import { multichainMethodCallValidatorMiddleware } from './multichainMethodCallValidator'; + +describe('multichainMethodCallValidatorMiddleware', () => { + const mockNext = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('"wallet_invokeMethod" request', () => { + it('should pass validation for a "wallet_invokeMethod" request and call next', 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 for a a "wallet_invokeMethod" request with no scope', async () => { + const request: JsonRpcRequest = { + id: 1, + jsonrpc: '2.0', + method: 'wallet_invokeMethod', + // @ts-expect-error test + params: { + request: { + method: 'test_method', + params: { + test: 'test', + }, + }, + }, + }; + const response = {} as JsonRpcResponse; + + await new Promise((resolve, reject) => { + multichainMethodCallValidatorMiddleware( + request, + response, + mockNext, + (error) => { + try { + expect(error).toBeDefined(); + expect((error as JsonRpcError).message).toBe( + 'Invalid method parameter(s).', + ); + expect((error as JsonRpcError).code).toBe(-32602); + expect((error as JsonRpcError).data).toStrictEqual([ + { + code: -32602, + message: 'scope is required, but is undefined', + data: { + param: 'scope', + path: [], + schema: { + pattern: '[-a-z0-9]{3,8}(:[-_a-zA-Z0-9]{1,32})?', + type: 'string', + }, + got: 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', + // @ts-expect-error test + params: { + scope: 'test', + }, + }; + const response = {} as JsonRpcResponse; + + await new Promise((resolve, reject) => { + multichainMethodCallValidatorMiddleware( + request, + response, + mockNext, + (error) => { + try { + expect(error).toBeDefined(); + expect((error as JsonRpcError).message).toBe( + 'Invalid method parameter(s).', + ); + expect((error as JsonRpcError).code).toBe(-32602); + expect((error as JsonRpcError).data).toStrictEqual([ + { + code: -32602, + data: { + got: undefined, + param: 'request', + path: [], + schema: { + properties: { + method: { + type: 'string', + }, + params: true, + }, + type: 'object', + }, + }, + message: 'request is required, but is undefined', + }, + ]); + 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: { + // Missing required parameters or invalid structure + scope: 'test_scope', + request: { + // @ts-expect-error test + event: '', + data: {}, + }, + }, + }; + 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).', + ); + expect((error as JsonRpcError).data).toStrictEqual([ + { + code: -32602, + data: { + got: undefined, + param: 'notification', + path: [], + schema: { + properties: { + method: { + type: 'string', + }, + params: true, + }, + type: 'object', + }, + }, + message: '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 for a "wallet_revokeSession" request and call next', 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 for a "wallet_getSession" request and call next', async () => { + const request: JsonRpcRequest = { + id: 5, + jsonrpc: '2.0', + method: 'wallet_getSession', + // @ts-expect-error TODO figure out why this type is not working + 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 is passed an unknown method', async () => { + const request: JsonRpcRequest = { + id: 1, + jsonrpc: '2.0', + method: 'unknown_method', + // @ts-expect-error test + params: { + request: { + method: 'test_method', + params: { + test: 'test', + }, + }, + }, + }; + const response = {} as JsonRpcResponse; + + await new Promise((resolve, reject) => { + multichainMethodCallValidatorMiddleware( + request, + response, + mockNext, + (error) => { + try { + expect(error).toBeDefined(); + console.log('error in test', error); + expect((error as JsonRpcError).message).toBe( + 'Invalid method parameter(s).', + ); + expect((error as JsonRpcError).code).toBe(-32602); + expect((error as JsonRpcError).data).toStrictEqual([ + { + code: -32601, + message: 'The method does not exist / is not available.', + data: { + method: 'unknown_method', + }, + }, + ]); + 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 index cc075140e2..7748957bf0 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -18,6 +18,8 @@ import { makeCustomResolver } from '@open-rpc/schema-utils-js/build/parse-open-r import type { Schema, ValidationError } from 'jsonschema'; import { Validator } from 'jsonschema'; +import type { Caip27Params, Caip285Params, Caip319Params } from '../scope/types'; + const transformError = ( error: ValidationError, param: ContentDescriptorObject, @@ -54,10 +56,28 @@ const multichainMethodCallValidator = async ( const methodToCheck = dereffed.methods.find( (m) => (m as unknown as ContentDescriptorObject).name === method, ); + const errors: JsonRpcError[] = []; + if ( + !methodToCheck || + !isObject(methodToCheck) || + !('params' in methodToCheck) + ) { + return [ + { + code: -32601, + message: 'The method does not exist / is not available.', + data: { + method, + }, + }, + ]; + } + // check each param and aggregate errors (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { let paramToCheck: Json | undefined; + console.log('param', param); const p = param as ContentDescriptorObject; if (isObject(params)) { paramToCheck = params[p.name]; @@ -66,6 +86,8 @@ const multichainMethodCallValidator = async ( } else { paramToCheck = undefined; } + console.log('paramToCheck', paramToCheck); + console.log('p.schema', p.schema); const result = v.validate(paramToCheck, p.schema as unknown as Schema, { required: p.required, }); @@ -77,22 +99,26 @@ const multichainMethodCallValidator = async ( ); } }); + console.log('errors', errors); if (errors.length > 0) { return errors; } + console.log('no errors'); // feels like this should return true to indicate that its valid but i'd rather check the falsy value since errors // would be an array and return true if it's empty return false; }; export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< - JsonRpcRequest, + JsonRpcRequest | Caip27Params | Caip319Params | Caip285Params, Json > = function (request, _response, next, end) { // eslint-disable-next-line @typescript-eslint/no-floating-promises multichainMethodCallValidator(request.method, request.params).then( (errors) => { + console.log('errors', errors); if (errors) { + console.log('errors', errors); return end(rpcErrors.invalidParams({ data: errors })); } return next(); diff --git a/packages/multichain/src/scope/constants.test.ts b/packages/multichain/src/scope/constants.test.ts index 8369ec721a..362bead5a7 100644 --- a/packages/multichain/src/scope/constants.test.ts +++ b/packages/multichain/src/scope/constants.test.ts @@ -6,15 +6,9 @@ describe('KnownRpcMethods', () => { Object { "bip122": Array [], "eip155": Array [ - "wallet_switchEthereumChain", - "wallet_getPermissions", - "wallet_requestPermissions", - "wallet_revokePermissions", "personal_sign", "eth_signTypedData_v4", "wallet_watchAsset", - "eth_requestAccounts", - "eth_accounts", "eth_sendTransaction", "eth_decrypt", "eth_getEncryptionPublicKey", @@ -24,7 +18,6 @@ describe('KnownRpcMethods', () => { "eth_blockNumber", "eth_call", "eth_chainId", - "eth_coinbase", "eth_estimateGas", "eth_feeHistory", "eth_gasPrice", diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index b13b5edae7..c5bc5e7a15 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -10,6 +10,8 @@ import type { KnownCaipNamespace, CaipNamespace, Json, + JsonRpcRequest, + JsonRpcParams, } from '@metamask/utils'; /** @@ -119,3 +121,59 @@ export type NonWalletKnownCaipNamespace = Exclude< KnownCaipNamespace, KnownCaipNamespace.Wallet >; + +// { +// "id": 1, +// "jsonrpc": "2.0", +// "method": "wallet_invokeMethod", +// "params": { +// "sessionId": "0xdeadbeef", +// "scope": "eip155:1", +// "request": { +// "method": "eth_sendTransaction", +// "params": [ +// { +// "to": "0x4B0897b0513FdBeEc7C469D9aF4fA6C0752aBea7", +// "from": "0xDeaDbeefdEAdbeefdEadbEEFdeadbeefDEADbEEF", +// "gas": "0x76c0", +// "value": "0x8ac7230489e80000", +// "data": "0x", +// "gasPrice": "0x4a817c800" +// } +// ] +// } +// } +// } + +/** + * Parameters for the `wallet_invokeMethod` method as defined in CAIP-27. + */ +export type Caip27Params = { + scope: string; + request: { + method: string; + params: JsonRpcParams; + }; +}; + +/** + * Parameters for the `wallet_notify` method as defined in CAIP-319. + */ +export type Caip319Params = { + scope: string; + notification: { + method: string; + params: JsonRpcParams; + }; +}; + +/** + * Parameters for the `wallet_revokeSession` method as defined in CAIP-285. + */ +export type Caip285Params = { + scope: string; + request: { + method: string; + params: Record; + }; +}; From a25c05d11dbc3761dd8799d692e6619f2a7dd229 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 15:28:18 -0600 Subject: [PATCH 76/80] cleanup --- .../middlewares/multichainMethodCallValidator.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index 7748957bf0..badafa8099 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -18,7 +18,11 @@ import { makeCustomResolver } from '@open-rpc/schema-utils-js/build/parse-open-r import type { Schema, ValidationError } from 'jsonschema'; import { Validator } from 'jsonschema'; -import type { Caip27Params, Caip285Params, Caip319Params } from '../scope/types'; +import type { + Caip27Params, + Caip285Params, + Caip319Params, +} from '../scope/types'; const transformError = ( error: ValidationError, @@ -77,7 +81,6 @@ const multichainMethodCallValidator = async ( // check each param and aggregate errors (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { let paramToCheck: Json | undefined; - console.log('param', param); const p = param as ContentDescriptorObject; if (isObject(params)) { paramToCheck = params[p.name]; @@ -86,8 +89,6 @@ const multichainMethodCallValidator = async ( } else { paramToCheck = undefined; } - console.log('paramToCheck', paramToCheck); - console.log('p.schema', p.schema); const result = v.validate(paramToCheck, p.schema as unknown as Schema, { required: p.required, }); @@ -99,11 +100,9 @@ const multichainMethodCallValidator = async ( ); } }); - console.log('errors', errors); if (errors.length > 0) { return errors; } - console.log('no errors'); // feels like this should return true to indicate that its valid but i'd rather check the falsy value since errors // would be an array and return true if it's empty return false; @@ -116,9 +115,7 @@ export const multichainMethodCallValidatorMiddleware: JsonRpcMiddleware< // eslint-disable-next-line @typescript-eslint/no-floating-promises multichainMethodCallValidator(request.method, request.params).then( (errors) => { - console.log('errors', errors); if (errors) { - console.log('errors', errors); return end(rpcErrors.invalidParams({ data: errors })); } return next(); From b35bdc369a979e15db5e18e35c69e75428430201 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 15:36:20 -0600 Subject: [PATCH 77/80] lint --- packages/multichain/src/scope/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index c5bc5e7a15..4d426be6a1 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -10,7 +10,6 @@ import type { KnownCaipNamespace, CaipNamespace, Json, - JsonRpcRequest, JsonRpcParams, } from '@metamask/utils'; From 2ad05a38ac3221841e4508896a274088e11c3dae Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 16:03:41 -0600 Subject: [PATCH 78/80] question --- .../src/middlewares/multichainMethodCallValidator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index badafa8099..e96d6fdeaf 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -57,6 +57,7 @@ const multichainMethodCallValidator = async ( params: JsonRpcParams | undefined, ) => { const dereffed = await dereffedPromise; + const methodToCheck = dereffed.methods.find( (m) => (m as unknown as ContentDescriptorObject).name === method, ); @@ -77,13 +78,13 @@ const multichainMethodCallValidator = async ( }, ]; } - // check each param and aggregate errors (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { let paramToCheck: Json | undefined; const p = param as ContentDescriptorObject; if (isObject(params)) { paramToCheck = params[p.name]; + // TODO: according to the spec all of the multichain method params are objects, should this be removed? } else if (params && Array.isArray(params)) { paramToCheck = params[i]; } else { From 79acd26d2f538e8caad2511f68b4edb7ad246b88 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 16:18:41 -0600 Subject: [PATCH 79/80] small cleanup --- .../multichainMethodCallValidator.test.ts | 16 +++++++--------- .../middlewares/multichainMethodCallValidator.ts | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts index c7e9a89d99..f022e0c826 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts @@ -18,7 +18,7 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); describe('"wallet_invokeMethod" request', () => { - it('should pass validation for a "wallet_invokeMethod" request and call next', async () => { + it('should pass validation and call next when passed a valid "wallet_invokeMethod" request', async () => { const request: JsonRpcRequest = { id: 1, jsonrpc: '2.0', @@ -55,7 +55,7 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); }); }); - it('should throw an error for a a "wallet_invokeMethod" request with no scope', async () => { + it('should throw an error when passed a "wallet_invokeMethod" request with no scope', async () => { const request: JsonRpcRequest = { id: 1, jsonrpc: '2.0', @@ -221,16 +221,14 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); it('should throw an error for a "wallet_notify" request with invalid params', async () => { - const request: JsonRpcRequest = { + const request: JsonRpcRequest = { id: 2, jsonrpc: '2.0', method: 'wallet_notify', + // @ts-expect-error test params: { - // Missing required parameters or invalid structure scope: 'test_scope', request: { - // @ts-expect-error test - event: '', data: {}, }, }, @@ -289,7 +287,7 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); describe('"wallet_revokeSession" request', () => { - it('should pass validation for a "wallet_revokeSession" request and call next', async () => { + it('should pass validation and call next when passed a valid "wallet_revokeSession" request', async () => { const request: JsonRpcRequest = { id: 3, jsonrpc: '2.0', @@ -320,7 +318,7 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); describe('"wallet_getSession" request', () => { - it('should pass validation for a "wallet_getSession" request and call next', async () => { + it('should pass validation and call next when passed a valid "wallet_getSession" request', async () => { const request: JsonRpcRequest = { id: 5, jsonrpc: '2.0', @@ -352,7 +350,7 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); }); - it('should throw an error is passed an unknown method', async () => { + it('should throw an error when passed an unknown method', async () => { const request: JsonRpcRequest = { id: 1, jsonrpc: '2.0', diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index e96d6fdeaf..a3e3337f67 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -84,7 +84,7 @@ const multichainMethodCallValidator = async ( const p = param as ContentDescriptorObject; if (isObject(params)) { paramToCheck = params[p.name]; - // TODO: according to the spec all of the multichain method params are objects, should this be removed? + // TODO: according to the spec all of the multichain method params values should be objects, should this be removed? } else if (params && Array.isArray(params)) { paramToCheck = params[i]; } else { From 32aa80736ec13a17e47757376fb9098e74b61251 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 22 Nov 2024 16:51:25 -0600 Subject: [PATCH 80/80] more cleanup --- .../multichainMethodCallValidator.test.ts | 42 ++++++++++++++++++- .../multichainMethodCallValidator.ts | 18 ++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts index f022e0c826..14903fe7d0 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.test.ts @@ -350,7 +350,47 @@ describe('multichainMethodCallValidatorMiddleware', () => { }); }); - it('should throw an error when passed an unknown method', async () => { + 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', + // @ts-expect-error test + 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', diff --git a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts index a3e3337f67..edd9a0fda2 100644 --- a/packages/multichain/src/middlewares/multichainMethodCallValidator.ts +++ b/packages/multichain/src/middlewares/multichainMethodCallValidator.ts @@ -79,17 +79,17 @@ const multichainMethodCallValidator = async ( ]; } // check each param and aggregate errors - (methodToCheck as unknown as MethodObject).params.forEach((param, i) => { - let paramToCheck: Json | undefined; + (methodToCheck as unknown as MethodObject).params.forEach((param) => { const p = param as ContentDescriptorObject; - if (isObject(params)) { - paramToCheck = params[p.name]; - // TODO: according to the spec all of the multichain method params values should be objects, should this be removed? - } else if (params && Array.isArray(params)) { - paramToCheck = params[i]; - } else { - paramToCheck = undefined; + if (!isObject(params)) { + errors.push({ + code: -32602, + message: 'Invalid method parameter(s).', + }); + return; } + const paramToCheck = params[p.name]; + const result = v.validate(paramToCheck, p.schema as unknown as Schema, { required: p.required, });