From bc5610499b0e4a4323bba69ff796edb0a432191e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 10 Oct 2024 14:02:49 -0700 Subject: [PATCH 001/144] 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 002/144] 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 003/144] 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 004/144] 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 005/144] 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 006/144] 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 007/144] 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 008/144] 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 009/144] 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 010/144] 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 011/144] 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 012/144] 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 013/144] 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 014/144] 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 015/144] 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 016/144] 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 017/144] 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 018/144] 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 019/144] 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 020/144] 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 021/144] 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 022/144] 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 023/144] 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 024/144] 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 025/144] 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 026/144] 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 027/144] 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 028/144] 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 029/144] 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 030/144] 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 031/144] 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 032/144] 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 033/144] 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 034/144] 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 035/144] 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 036/144] 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 037/144] 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 038/144] 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 039/144] 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 9bcf86965d48a8499bdfe12d279e3dbc1edeb4f2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Thu, 17 Oct 2024 13:18:08 -0700 Subject: [PATCH 040/144] 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 041/144] 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 109a7bf5a25ef4f9b15a3ffe081cacac286d0114 Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 17 Oct 2024 14:21:11 -0700 Subject: [PATCH 042/144] 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 043/144] 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 044/144] 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 c5980c62e07251cb62abde2849f48ee2908cabc3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 08:51:58 -0700 Subject: [PATCH 045/144] 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 a7a8e6e4db0ccbd97124e201f5e0a49136354c5c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 21 Oct 2024 09:06:56 -0700 Subject: [PATCH 046/144] 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 42297f964fdb1623bc9da75e377c5ed9c662cd3f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Oct 2024 14:22:58 -0500 Subject: [PATCH 047/144] 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 72033583c9b3168993306b51ae03221666944082 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 22 Oct 2024 09:49:49 -0700 Subject: [PATCH 048/144] 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 049/144] 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 050/144] 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 051/144] 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 052/144] 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 053/144] 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 054/144] 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 055/144] 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 056/144] 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 057/144] 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 058/144] 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 757b177a4162b19c761edbdcfdb8fece654c2523 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:32:59 -0700 Subject: [PATCH 059/144] align deps --- packages/multichain/package.json | 8 ++++---- yarn.lock | 35 +++++--------------------------- 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index e0ce902781..027022c411 100644 --- a/packages/multichain/package.json +++ b/packages/multichain/package.json @@ -50,13 +50,13 @@ "@metamask/api-specs": "^0.10.12", "@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", + "@metamask/rpc-errors": "^7.0.1", + "@metamask/utils": "^10.0.0", "lodash": "^4.17.21" }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^21.1.0", + "@metamask/network-controller": "^22.0.0", "@metamask/permission-controller": "^11.0.2", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", @@ -67,7 +67,7 @@ "typescript": "~5.2.2" }, "peerDependencies": { - "@metamask/network-controller": "^21.0.0", + "@metamask/network-controller": "^22.0.0", "@metamask/permission-controller": "^11.0.0" }, "engines": { diff --git a/yarn.lock b/yarn.lock index cff8c44aa4..f3c8ed9452 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: @@ -3052,10 +3052,10 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/controller-utils": "npm:^11.4.0" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/network-controller": "npm:^21.1.0" + "@metamask/network-controller": "npm:^22.0.0" "@metamask/permission-controller": "npm:^11.0.2" - "@metamask/rpc-errors": "npm:^7.0.0" - "@metamask/utils": "npm:^9.1.0" + "@metamask/rpc-errors": "npm:^7.0.1" + "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" @@ -3065,7 +3065,7 @@ __metadata: typedoc-plugin-missing-exports: "npm:^2.0.0" typescript: "npm:~5.2.2" peerDependencies: - "@metamask/network-controller": ^21.0.0 + "@metamask/network-controller": ^22.0.0 "@metamask/permission-controller": ^11.0.0 languageName: unknown linkType: soft @@ -3089,31 +3089,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^21.1.0": - version: 21.1.0 - resolution: "@metamask/network-controller@npm:21.1.0" - dependencies: - "@metamask/base-controller": "npm:^7.0.1" - "@metamask/controller-utils": "npm:^11.3.0" - "@metamask/eth-block-tracker": "npm:^11.0.2" - "@metamask/eth-json-rpc-infura": "npm:^10.0.0" - "@metamask/eth-json-rpc-middleware": "npm:^15.0.0" - "@metamask/eth-json-rpc-provider": "npm:^4.1.5" - "@metamask/eth-query": "npm:^4.0.0" - "@metamask/json-rpc-engine": "npm:^10.0.0" - "@metamask/rpc-errors": "npm:^7.0.0" - "@metamask/swappable-obj-proxy": "npm:^2.2.0" - "@metamask/utils": "npm:^9.1.0" - async-mutex: "npm:^0.5.0" - immer: "npm:^9.0.6" - loglevel: "npm:^1.8.1" - reselect: "npm:^5.1.1" - uri-js: "npm:^4.4.1" - uuid: "npm:^8.3.2" - checksum: 10/142011625377599c448a2f219dc3288843e5623bfbe3704ca9f7b670eff06efc80574458832892231698fecdd40aa88dd20e1f9c6f554a028f9d972037eb35e8 - languageName: node - linkType: hard - "@metamask/network-controller@npm:^22.0.0, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" From dcb593b6df608bb36d2c096545fe78f6319ca6d4 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:33:26 -0700 Subject: [PATCH 060/144] rename Caip25CaveatFactoryFn to createCaip25Caveat --- packages/multichain/src/caip25Permission.test.ts | 4 ++-- packages/multichain/src/caip25Permission.ts | 2 +- packages/multichain/src/index.test.ts | 2 +- packages/multichain/src/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 72929ba7c1..4f6fc9e307 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -12,7 +12,7 @@ import { Caip25EndowmentPermissionName, Caip25CaveatMutatorFactories, removeScope, - Caip25CaveatFactoryFn, + createCaip25Caveat, } from './caip25Permission'; import * as ScopeAssert from './scope/assert'; import * as ScopeAuthorization from './scope/authorization'; @@ -57,7 +57,7 @@ describe('endowment:caip25', () => { it('builds the caveat', () => { expect( - Caip25CaveatFactoryFn({ + createCaip25Caveat({ requiredScopes: {}, optionalScopes: {}, isMultichainOrigin: true, diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 5914f1687a..bb7bdccb1e 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -37,7 +37,7 @@ export type Caip25CaveatValue = { export const Caip25CaveatType = 'authorizedScopes'; -export const Caip25CaveatFactoryFn = (value: Caip25CaveatValue) => { +export const createCaip25Caveat = (value: Caip25CaveatValue) => { return { type: Caip25CaveatType, value, diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 7b61fec8af..d7dcbd5d01 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -27,7 +27,7 @@ describe('@metamask/multichain', () => { "isValidScope", "validateScopes", "Caip25CaveatType", - "Caip25CaveatFactoryFn", + "createCaip25Caveat", "Caip25EndowmentPermissionName", "caip25EndowmentBuilder", "Caip25CaveatMutatorFactories", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index a70fce6508..0449def0fe 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -44,7 +44,7 @@ export { isValidScope, validateScopes } from './scope/validation'; export type { Caip25CaveatValue } from './caip25Permission'; export { Caip25CaveatType, - Caip25CaveatFactoryFn, + createCaip25Caveat, Caip25EndowmentPermissionName, caip25EndowmentBuilder, Caip25CaveatMutatorFactories, From 16aa6862cbbe2c005940f13175f2ab1aa38a0d33 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:44:25 -0700 Subject: [PATCH 061/144] switch removeScope param order --- packages/multichain/src/caip25Permission.test.ts | 4 ++-- packages/multichain/src/caip25Permission.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 4f6fc9e307..7da94095ba 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -92,7 +92,7 @@ describe('endowment:caip25', () => { sessionProperties: {}, isMultichainOrigin: true, }; - const result = removeScope('eip155:5', ethereumGoerliCaveat); + const result = removeScope(ethereumGoerliCaveat, 'eip155:5'); expect(result).toStrictEqual({ operation: CaveatMutatorOperation.UpdateValue, value: { @@ -127,7 +127,7 @@ describe('endowment:caip25', () => { sessionProperties: {}, isMultichainOrigin: true, }; - const result = removeScope('eip155:2', ethereumGoerliCaveat); + const result = removeScope(ethereumGoerliCaveat, 'eip155:2'); expect(result).toStrictEqual({ operation: CaveatMutatorOperation.Noop, }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index bb7bdccb1e..510e9a4804 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -243,13 +243,13 @@ function removeAccount( * `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. + * @param targetScopeString - The scope that is being removed. * @returns The updated CAIP-25 permission caveat value. */ export function removeScope( - targetScopeString: ExternalScopeString, caip25CaveatValue: Caip25CaveatValue, + targetScopeString: ExternalScopeString, ) { const newRequiredScopes = Object.entries( caip25CaveatValue.requiredScopes, From f8b887c99eef428ebb07903086a802be685f5c4f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:44:41 -0700 Subject: [PATCH 062/144] switch removeAccount param order --- packages/multichain/src/caip25Permission.test.ts | 6 +++--- packages/multichain/src/caip25Permission.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 7da94095ba..c969699c1c 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -147,7 +147,7 @@ describe('endowment:caip25', () => { optionalScopes: {}, isMultichainOrigin: true, }; - const result = removeAccount('0x1', ethereumGoerliCaveat); + const result = removeAccount(ethereumGoerliCaveat, '0x1'); expect(result).toStrictEqual({ operation: CaveatMutatorOperation.UpdateValue, value: { @@ -187,7 +187,7 @@ describe('endowment:caip25', () => { }, isMultichainOrigin: true, }; - const result = removeAccount('0x1', ethereumGoerliCaveat); + const result = removeAccount(ethereumGoerliCaveat, '0x1'); expect(result).toStrictEqual({ operation: CaveatMutatorOperation.UpdateValue, value: { @@ -233,7 +233,7 @@ describe('endowment:caip25', () => { }, isMultichainOrigin: true, }; - const result = removeAccount('0x3', ethereumGoerliCaveat); + const result = removeAccount(ethereumGoerliCaveat, '0x3'); expect(result).toStrictEqual({ operation: CaveatMutatorOperation.Noop, }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 510e9a4804..1ce35f8981 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -203,13 +203,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. + * @param targetAddress - The address to remove from the scope object. * @returns The updated scope object. */ function removeAccount( - targetAddress: string, // non caip-10 formatted address existingScopes: Caip25CaveatValue, + targetAddress: string, // non caip-10 formatted address ) { // copy existing scopes const copyOfExistingScopes = cloneDeep(existingScopes); From 632db4b7be608074dab0d773d057a40c7aa4cdfb Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:50:55 -0700 Subject: [PATCH 063/144] do not export removeScope directly. fix Caip25CaveatMutatorFactories test --- packages/multichain/src/caip25Permission.test.ts | 3 +-- packages/multichain/src/caip25Permission.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index c969699c1c..6efa2a0177 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -11,7 +11,6 @@ import { caip25EndowmentBuilder, Caip25EndowmentPermissionName, Caip25CaveatMutatorFactories, - removeScope, createCaip25Caveat, } from './caip25Permission'; import * as ScopeAssert from './scope/assert'; @@ -27,7 +26,7 @@ jest.mock('./scope/assert', () => ({ })); const MockScopeAssert = jest.mocked(ScopeAssert); -const { removeAccount } = Caip25CaveatMutatorFactories[Caip25CaveatType]; +const { removeAccount, removeScope } = Caip25CaveatMutatorFactories[Caip25CaveatType]; describe('endowment:caip25', () => { beforeEach(() => { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 1ce35f8981..3202e9dbc6 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -247,7 +247,7 @@ function removeAccount( * @param targetScopeString - The scope that is being removed. * @returns The updated CAIP-25 permission caveat value. */ -export function removeScope( +function removeScope( caip25CaveatValue: Caip25CaveatValue, targetScopeString: ExternalScopeString, ) { From 628a38ca7e1f458020790f622edfd03bd5ca551e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:53:13 -0700 Subject: [PATCH 064/144] reorder removeAccountOnScope params --- packages/multichain/src/caip25Permission.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 3202e9dbc6..7fa2bd3631 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -189,10 +189,13 @@ 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. + * @param targetAddress - The address to remove from the scope object. */ -function removeAccountOnScope(targetAddress: string, scopeObject: ScopeObject) { +function removeAccountOnScope( + scopeObject: ScopeObject, + targetAddress: string, +) { if (scopeObject.accounts) { scopeObject.accounts = scopeObject.accounts.filter( removeAccountFilterFn(targetAddress), @@ -219,7 +222,7 @@ function removeAccount( copyOfExistingScopes.optionalScopes, ].forEach((scopes) => { Object.entries(scopes).forEach(([, scopeObject]) => { - removeAccountOnScope(targetAddress, scopeObject); + removeAccountOnScope(scopeObject, targetAddress); }); }); From 6346955aea105ec079852117b2f831bc7456fb74 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 14:55:13 -0700 Subject: [PATCH 065/144] rename Caip25CaveatMutatorFactories to Caip25CaveatMutators --- packages/multichain/src/caip25Permission.test.ts | 4 ++-- packages/multichain/src/caip25Permission.ts | 2 +- packages/multichain/src/index.test.ts | 2 +- packages/multichain/src/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 6efa2a0177..74bfaaf10e 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -10,7 +10,7 @@ import { Caip25CaveatType, caip25EndowmentBuilder, Caip25EndowmentPermissionName, - Caip25CaveatMutatorFactories, + Caip25CaveatMutators, createCaip25Caveat, } from './caip25Permission'; import * as ScopeAssert from './scope/assert'; @@ -26,7 +26,7 @@ jest.mock('./scope/assert', () => ({ })); const MockScopeAssert = jest.mocked(ScopeAssert); -const { removeAccount, removeScope } = Caip25CaveatMutatorFactories[Caip25CaveatType]; +const { removeAccount, removeScope } = Caip25CaveatMutators[Caip25CaveatType]; describe('endowment:caip25', () => { beforeEach(() => { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 7fa2bd3631..019936533f 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -156,7 +156,7 @@ export const caip25EndowmentBuilder = Object.freeze({ * Factories that construct caveat mutator functions that are passed to * PermissionController.updatePermissionsByCaveat. */ -export const Caip25CaveatMutatorFactories = { +export const Caip25CaveatMutators = { [Caip25CaveatType]: { removeScope, removeAccount, diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index d7dcbd5d01..24dcd2eff8 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -30,7 +30,7 @@ describe('@metamask/multichain', () => { "createCaip25Caveat", "Caip25EndowmentPermissionName", "caip25EndowmentBuilder", - "Caip25CaveatMutatorFactories", + "Caip25CaveatMutators", "removeScope", ] `); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 0449def0fe..577b36adb6 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -47,6 +47,6 @@ export { createCaip25Caveat, Caip25EndowmentPermissionName, caip25EndowmentBuilder, - Caip25CaveatMutatorFactories, + Caip25CaveatMutators, removeScope, } from './caip25Permission'; From d40d63ddd20e535f1f9fb99f871a83e5d770f375 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 15:10:21 -0700 Subject: [PATCH 066/144] fix barrel export --- 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 24dcd2eff8..1f3ede7153 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -31,7 +31,6 @@ describe('@metamask/multichain', () => { "Caip25EndowmentPermissionName", "caip25EndowmentBuilder", "Caip25CaveatMutators", - "removeScope", ] `); }); diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 577b36adb6..160a4c1748 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -48,5 +48,4 @@ export { Caip25EndowmentPermissionName, caip25EndowmentBuilder, Caip25CaveatMutators, - removeScope, } from './caip25Permission'; From d75d5d69652177e2942c7542f8e7d10194782bca Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 28 Oct 2024 15:10:50 -0700 Subject: [PATCH 067/144] replace node assert with lodash.isEqual --- packages/multichain/src/caip25Permission.test.ts | 12 ++++++++++-- packages/multichain/src/caip25Permission.ts | 12 +++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 74bfaaf10e..8d0fc0cd03 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -726,7 +726,11 @@ describe('endowment:caip25', () => { invoker: 'test.com', parentCapability: Caip25EndowmentPermissionName, }); - }).toThrow(/Expected values to be strictly deep-equal/u); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received non-normalized value for caveat of type "${Caip25CaveatType}".`, + ), + ); }); it('throws if the input optionalScopes does not match the output of validateAndNormalizeScopes', () => { @@ -771,7 +775,11 @@ describe('endowment:caip25', () => { invoker: 'test.com', parentCapability: Caip25EndowmentPermissionName, }); - }).toThrow(/Expected values to be strictly deep-equal/u); + }).toThrow( + new Error( + `${Caip25EndowmentPermissionName} error: Received non-normalized value for caveat of type "${Caip25CaveatType}".`, + ), + ); }); it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and normalized', () => { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 019936533f..500e4c77a3 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -16,7 +16,6 @@ import { type Hex, type NonEmptyArray, } from '@metamask/utils'; -import { strict as assert } from 'assert'; import { cloneDeep, isEqual } from 'lodash'; import { getEthAccounts } from './adapters/caip-permission-adapter-eth-accounts'; @@ -141,8 +140,15 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - assert.deepEqual(requiredScopes, normalizedRequiredScopes); - assert.deepEqual(optionalScopes, normalizedOptionalScopes); + + if ( + !isEqual(requiredScopes, normalizedRequiredScopes) || + !isEqual(optionalScopes, normalizedOptionalScopes) + ) { + throw new Error( + `${Caip25EndowmentPermissionName} error: Received non-normalized value for caveat of type "${Caip25CaveatType}".`, + ); + } }, }; }; From 23d10ed4a5e1fd5c21c7e7a47bddb14ecfa8f951 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 11:36:53 -0700 Subject: [PATCH 068/144] reorganize describes in caip25Permission.test.ts --- .../multichain/src/caip25Permission.test.ts | 304 +++++++++--------- packages/multichain/src/caip25Permission.ts | 6 +- 2 files changed, 156 insertions(+), 154 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 8d0fc0cd03..acbf12afb1 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -28,7 +28,7 @@ const MockScopeAssert = jest.mocked(ScopeAssert); const { removeAccount, removeScope } = Caip25CaveatMutators[Caip25CaveatType]; -describe('endowment:caip25', () => { +describe('caip25EndowmentBuilder', () => { beforeEach(() => { MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: {}, @@ -36,65 +36,49 @@ describe('endowment:caip25', () => { }); }); - it('builds the expected permission specification', () => { - const specification = caip25EndowmentBuilder.specificationBuilder({ - methodHooks: { - findNetworkClientIdByChainId: jest.fn(), - listAccounts: jest.fn(), - }, - }); - expect(specification).toStrictEqual({ - permissionType: PermissionType.Endowment, - targetName: Caip25EndowmentPermissionName, - endowmentGetter: expect.any(Function), - allowedCaveats: [Caip25CaveatType], - validator: expect.any(Function), - }); + describe('specificationBuilder', () => { + it('builds the expected permission specification', () => { + const specification = caip25EndowmentBuilder.specificationBuilder({ + methodHooks: { + findNetworkClientIdByChainId: jest.fn(), + listAccounts: jest.fn(), + }, + }); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetName: Caip25EndowmentPermissionName, + endowmentGetter: expect.any(Function), + allowedCaveats: [Caip25CaveatType], + validator: expect.any(Function), + }); - expect(specification.endowmentGetter()).toBeNull(); + expect(specification.endowmentGetter()).toBeNull(); + }); }); - it('builds the caveat', () => { - expect( - createCaip25Caveat({ - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }), - ).toStrictEqual({ - type: Caip25CaveatType, - value: { - requiredScopes: {}, - optionalScopes: {}, - isMultichainOrigin: true, - }, + describe('createCaip25Caveat', () => { + it('builds the caveat', () => { + expect( + createCaip25Caveat({ + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }), + ).toStrictEqual({ + type: Caip25CaveatType, + value: { + requiredScopes: {}, + optionalScopes: {}, + isMultichainOrigin: true, + }, + }); }); }); - describe('caveat mutator removeScope', () => { - it('can remove a caveat', () => { - const ethereumGoerliCaveat = { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: ['chainChanged'], - accounts: [], - }, - }, - optionalScopes: { - 'eip155:5': { - methods: ['eth_call'], - notifications: ['accountsChanged'], - accounts: [], - }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(ethereumGoerliCaveat, 'eip155:5'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { + describe('Caip25CaveatMutators.authorizedScopes', () => { + describe('removeScope', () => { + it('can remove a caveat', () => { + const ethereumGoerliCaveat = { requiredScopes: { 'eip155:1': { methods: ['eth_call'], @@ -102,139 +86,161 @@ describe('endowment:caip25', () => { accounts: [], }, }, - optionalScopes: {}, - }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope(ethereumGoerliCaveat, 'eip155:5'); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.UpdateValue, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: [], + }, + }, + optionalScopes: {}, + }, + }); }); - }); - it('can noop when nothing is removed', () => { - const ethereumGoerliCaveat = { - requiredScopes: { - 'eip155:1': { - methods: ['eth_call'], - notifications: ['chainChanged'], - accounts: [], + it('can noop when nothing is removed', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: [], + }, }, - }, - optionalScopes: { - 'eip155:5': { - methods: ['eth_call'], - notifications: ['accountsChanged'], - accounts: [], + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, }, - }, - sessionProperties: {}, - isMultichainOrigin: true, - }; - const result = removeScope(ethereumGoerliCaveat, 'eip155:2'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope(ethereumGoerliCaveat, 'eip155:2'); + 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(ethereumGoerliCaveat, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { + describe('removeAccount', () => { + it('can remove an account', () => { + const ethereumGoerliCaveat: Caip25CaveatValue = { requiredScopes: { 'eip155:1': { methods: ['eth_call'], notifications: ['chainChanged'], - accounts: ['eip155:1:0x2'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], }, }, optionalScopes: {}, isMultichainOrigin: true, - }, + }; + const result = removeAccount(ethereumGoerliCaveat, '0x1'); + 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(ethereumGoerliCaveat, '0x1'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.UpdateValue, - value: { + 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:0x2'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], }, 'eip155:2': { methods: ['eth_call'], notifications: ['chainChanged'], - accounts: ['eip155:2:0x2'], + accounts: ['eip155:2:0x1', 'eip155:2:0x2'], }, }, optionalScopes: { 'eip155:3': { methods: ['eth_call'], notifications: ['chainChanged'], - accounts: ['eip155:3:0x2'], + accounts: ['eip155:3:0x1', 'eip155:3:0x2'], }, }, isMultichainOrigin: true, - }, + }; + const result = removeAccount(ethereumGoerliCaveat, '0x1'); + 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'], + 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'], - accounts: [], + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, }, - }, - isMultichainOrigin: true, - }; - const result = removeAccount(ethereumGoerliCaveat, '0x3'); - expect(result).toStrictEqual({ - operation: CaveatMutatorOperation.Noop, + isMultichainOrigin: true, + }; + const result = removeAccount(ethereumGoerliCaveat, '0x3'); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.Noop, + }); }); }); }); diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 500e4c77a3..5342fbeb11 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -140,7 +140,6 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - if ( !isEqual(requiredScopes, normalizedRequiredScopes) || !isEqual(optionalScopes, normalizedOptionalScopes) @@ -198,10 +197,7 @@ function removeAccountFilterFn(targetAddress: string) { * @param scopeObject - The scope object to remove the account from. * @param targetAddress - The address to remove from the scope object. */ -function removeAccountOnScope( - scopeObject: ScopeObject, - targetAddress: string, -) { +function removeAccountOnScope(scopeObject: ScopeObject, targetAddress: string) { if (scopeObject.accounts) { scopeObject.accounts = scopeObject.accounts.filter( removeAccountFilterFn(targetAddress), From 788c9eb1d3038fbe587cb23fbd0a07dd6987fa15 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 11:49:00 -0700 Subject: [PATCH 069/144] adjust caveat mutator scenarios --- .../multichain/src/caip25Permission.test.ts | 115 +++++++++++++++++- 1 file changed, 110 insertions(+), 5 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index acbf12afb1..3d497426ca 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -77,7 +77,42 @@ describe('caip25EndowmentBuilder', () => { describe('Caip25CaveatMutators.authorizedScopes', () => { describe('removeScope', () => { - it('can remove a caveat', () => { + it('returns a version of the caveat with the given scope removed from requiredScopes if it is present', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: [], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope(ethereumGoerliCaveat, 'eip155:1'); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.UpdateValue, + value: { + requiredScopes: {}, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, + }, + }, + }); + }); + + it('returns a version of the caveat with the given scope removed from optionalScopes if it is present', () => { const ethereumGoerliCaveat = { requiredScopes: { 'eip155:1': { @@ -112,7 +147,47 @@ describe('caip25EndowmentBuilder', () => { }); }); - it('can noop when nothing is removed', () => { + it('returns a version of the caveat with the given scope removed from requiredScopes and optionalScopes if it is present', () => { + const ethereumGoerliCaveat = { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: [], + }, + 'eip155:5': { + methods: [], + notifications: ['chainChanged'], + accounts: [], + }, + }, + optionalScopes: { + 'eip155:5': { + methods: ['eth_call'], + notifications: ['accountsChanged'], + accounts: [], + }, + }, + sessionProperties: {}, + isMultichainOrigin: true, + }; + const result = removeScope(ethereumGoerliCaveat, 'eip155:5'); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.UpdateValue, + value: { + requiredScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: [], + }, + }, + optionalScopes: {}, + }, + }); + }); + + it('returns the caveat unchanged when the given scope is not found in either requiredScopes or optionalScopes', () => { const ethereumGoerliCaveat = { requiredScopes: { 'eip155:1': { @@ -139,7 +214,7 @@ describe('caip25EndowmentBuilder', () => { }); describe('removeAccount', () => { - it('can remove an account', () => { + it('returns a version of the caveat with the given account removed from requiredScopes if it is present', () => { const ethereumGoerliCaveat: Caip25CaveatValue = { requiredScopes: { 'eip155:1': { @@ -168,7 +243,36 @@ describe('caip25EndowmentBuilder', () => { }); }); - it('can remove an account in multiple scopes in optional and required', () => { + it('returns a version of the caveat with the given account removed from optionalScopes if it is present', () => { + const ethereumGoerliCaveat: Caip25CaveatValue = { + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x1', 'eip155:1:0x2'], + } + }, + isMultichainOrigin: true, + }; + const result = removeAccount(ethereumGoerliCaveat, '0x1'); + expect(result).toStrictEqual({ + operation: CaveatMutatorOperation.UpdateValue, + value: { + requiredScopes: {}, + optionalScopes: { + 'eip155:1': { + methods: ['eth_call'], + notifications: ['chainChanged'], + accounts: ['eip155:1:0x2'], + }, + }, + isMultichainOrigin: true, + }, + }); + }); + + it('returns a version of the caveat with the given account removed from requiredScopes and optionalScopes if it is present', () => { const ethereumGoerliCaveat: Caip25CaveatValue = { requiredScopes: { 'eip155:1': { @@ -217,9 +321,10 @@ describe('caip25EndowmentBuilder', () => { isMultichainOrigin: true, }, }); + }); - it('can noop when nothing is removed', () => { + it('returns the caveat unchanged when the given account is not found in either requiredScopes or optionalScopes', () => { const ethereumGoerliCaveat: Caip25CaveatValue = { requiredScopes: { 'eip155:1': { From b859af5e1600a68defb58ab5678c251232e6273c Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 29 Oct 2024 11:50:56 -0700 Subject: [PATCH 070/144] Update packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.test.ts Co-authored-by: Elliot Winkler --- .../src/adapters/caip-permission-adapter-eth-accounts.test.ts | 2 +- 1 file changed, 1 insertion(+), 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 f779f1f576..6899e2ddda 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 @@ -174,7 +174,7 @@ describe('CAIP-25 eth_accounts adapters', () => { }); }); - it('returns a CAIP-25 caveat value with upserted "wallet:eip155" optional scope with CAIP-10 account addresses formed from the accounts param', () => { + it('returns a CAIP-25 caveat value with missing "wallet:eip155" optional scope filled in, forming CAIP-10 account addresses from the accounts param', () => { const input: Caip25CaveatValue = { requiredScopes: {}, optionalScopes: {}, From 5902e742af085f7582d00c5e9d21741b0d5f4206 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 29 Oct 2024 11:51:43 -0700 Subject: [PATCH 071/144] Update packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts Co-authored-by: Elliot Winkler --- .../adapters/caip-permission-adapter-permittedChains.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 016067199a..3df527794a 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -53,7 +53,7 @@ describe('CAIP-25 permittedChains adapters', () => { }); describe('addPermittedEthChainId', () => { - it('adds an optional scope for the chainId if it does not already exist in required or optional scopes', () => { + it('returns a version of the caveat value with a new optional scope for the chainId if it does not already exist in required or optional scopes', () => { const result = addPermittedEthChainId( { requiredScopes: { From 8c10fc0ccda57475738779282f351a4a6cc280c6 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 29 Oct 2024 11:51:55 -0700 Subject: [PATCH 072/144] Update packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts Co-authored-by: Elliot Winkler --- .../adapters/caip-permission-adapter-permittedChains.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3df527794a..50c24c4673 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -109,7 +109,7 @@ describe('CAIP-25 permittedChains adapters', () => { }); }); - it('adds an optional scope for "wallet:eip155" if it does not already exist in the optional scopes', () => { + it('returns a version of the caveat value with a new optional scope for "wallet:eip155" if it does not already exist in the optional scopes', () => { const result = addPermittedEthChainId( { requiredScopes: { From 7712d545bef38a55baf60d7754ebf2c30420c8b1 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 11:55:07 -0700 Subject: [PATCH 073/144] nest noramlizeScope 'scopeString is chain scoped' --- .../multichain/src/scope/transform.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index a98c2986dd..c205355784 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -19,17 +19,19 @@ const validScopeObject: ScopeObject = { describe('Scope Transform', () => { 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, + describe('scopeString is chain scoped', () => { + it('returns the scope with empty accounts array when accounts are not defined', () => { + expect(normalizeScope('eip155:1', externalScopeObject)).toStrictEqual({ + 'eip155:1': 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, + it('returns the scope unchanged when 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', () => { From d3fbaa95c1a7471fdf8118dd79e57e6083859ffe Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 11:57:07 -0700 Subject: [PATCH 074/144] make Scope Transform normalizeScope chain scoped test body more explicit --- packages/multichain/src/scope/transform.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index c205355784..3938d35f1d 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -22,13 +22,19 @@ describe('Scope Transform', () => { describe('scopeString is chain scoped', () => { it('returns the scope with empty accounts array when accounts are not defined', () => { expect(normalizeScope('eip155:1', externalScopeObject)).toStrictEqual({ - 'eip155:1': validScopeObject, + 'eip155:1': { + ...externalScopeObject, + accounts: [] + }, }); }); it('returns the scope unchanged when accounts are defined', () => { - expect(normalizeScope('eip155:1', validScopeObject)).toStrictEqual({ - 'eip155:1': validScopeObject, + expect(normalizeScope('eip155:1', {...externalScopeObject, accounts: []})).toStrictEqual({ + 'eip155:1': { + ...externalScopeObject, + accounts: [] + }, }); }); }) From 02f35f599bd8a1199fb6782aba30c50784541cf8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 12:22:46 -0700 Subject: [PATCH 075/144] replace type cast with @ts-expect-error --- packages/multichain/src/caip25Permission.test.ts | 8 +++----- packages/multichain/src/scope/transform.test.ts | 10 ++++++---- packages/multichain/src/scope/validation.test.ts | 12 ++++++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 3d497426ca..4c9e3ce2c0 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -1,5 +1,3 @@ -import type { NonEmptyArray } from '@metamask/controller-utils'; -import type { CaveatConstraint } from '@metamask/permission-controller'; import { CaveatMutatorOperation, PermissionType, @@ -251,7 +249,7 @@ describe('caip25EndowmentBuilder', () => { methods: ['eth_call'], notifications: ['chainChanged'], accounts: ['eip155:1:0x1', 'eip155:1:0x2'], - } + }, }, isMultichainOrigin: true, }; @@ -321,7 +319,6 @@ describe('caip25EndowmentBuilder', () => { isMultichainOrigin: true, }, }); - }); it('returns the caveat unchanged when the given account is not found in either requiredScopes or optionalScopes', () => { @@ -386,7 +383,8 @@ describe('caip25EndowmentBuilder', () => { expect(() => { validator({ - caveats: [] as unknown as NonEmptyArray, + // @ts-expect-error Intentionally invalid input + caveats: [], date: 1234, id: '1', invoker: 'test.com', diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index 3938d35f1d..13b69dde96 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -24,20 +24,22 @@ describe('Scope Transform', () => { expect(normalizeScope('eip155:1', externalScopeObject)).toStrictEqual({ 'eip155:1': { ...externalScopeObject, - accounts: [] + accounts: [], }, }); }); it('returns the scope unchanged when accounts are defined', () => { - expect(normalizeScope('eip155:1', {...externalScopeObject, accounts: []})).toStrictEqual({ + expect( + normalizeScope('eip155:1', { ...externalScopeObject, accounts: [] }), + ).toStrictEqual({ 'eip155:1': { ...externalScopeObject, - accounts: [] + accounts: [], }, }); }); - }) + }); describe('scopeString is namespace scoped', () => { it('returns the scope as is when `references` is not defined', () => { diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index 253e54a99b..bfbcf78f47 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -146,7 +146,8 @@ describe('Scope Validation', () => { it('does not throw an error if required scopes are defined but none are valid', () => { expect( validateScopes( - { 'eip155:1': {} as unknown as ExternalScopeObject }, + // @ts-expect-error Intentionally invalid input + { 'eip155:1': {} }, undefined, ), ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); @@ -155,7 +156,8 @@ describe('Scope Validation', () => { it('does not throw an error if optional scopes are defined but none are valid', () => { expect( validateScopes(undefined, { - 'eip155:1': {} as unknown as ExternalScopeObject, + // @ts-expect-error Intentionally invalid input + 'eip155:1': {}, }), ).toStrictEqual({ validRequiredScopes: {}, validOptionalScopes: {} }); }); @@ -165,10 +167,12 @@ describe('Scope Validation', () => { validateScopes( { 'eip155:1': validScopeObjectWithAccounts, - 'eip155:64': {} as unknown as ExternalScopeObject, + // @ts-expect-error Intentionally invalid input + 'eip155:64': {}, }, { - 'eip155:2': {} as unknown as ExternalScopeObject, + // @ts-expect-error Intentionally invalid input + 'eip155:2': {}, 'eip155:5': validScopeObjectWithAccounts, }, ), From 053b5ea80b276306a6d3cd0766cae1b9fb30d9e7 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 12:22:53 -0700 Subject: [PATCH 076/144] yarn --- packages/multichain/package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 027022c411..e4413e356d 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.4.0", + "@metamask/controller-utils": "^11.4.1", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", @@ -57,7 +57,7 @@ "devDependencies": { "@metamask/auto-changelog": "^3.4.4", "@metamask/network-controller": "^22.0.0", - "@metamask/permission-controller": "^11.0.2", + "@metamask/permission-controller": "^11.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", "jest": "^27.5.1", diff --git a/yarn.lock b/yarn.lock index b79cd8fb41..a32e68c310 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@npm:^11.4.1, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.1, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3050,10 +3050,10 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.4.0" + "@metamask/controller-utils": "npm:^11.4.1" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" "@metamask/network-controller": "npm:^22.0.0" - "@metamask/permission-controller": "npm:^11.0.2" + "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" "@types/jest": "npm:^27.4.1" @@ -3221,7 +3221,7 @@ __metadata: languageName: node linkType: hard -"@metamask/permission-controller@npm:^11.0.0, @metamask/permission-controller@npm:^11.0.2, @metamask/permission-controller@npm:^11.0.3, @metamask/permission-controller@workspace:packages/permission-controller": +"@metamask/permission-controller@npm:^11.0.0, @metamask/permission-controller@npm:^11.0.3, @metamask/permission-controller@workspace:packages/permission-controller": version: 0.0.0-use.local resolution: "@metamask/permission-controller@workspace:packages/permission-controller" dependencies: From ac9835cf2c401c1a9692f892417674389d96c972 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 13:06:58 -0700 Subject: [PATCH 077/144] Flatten isValidScope test --- .../multichain/src/scope/validation.test.ts | 183 ++++++++---------- 1 file changed, 79 insertions(+), 104 deletions(-) diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index bfbcf78f47..f2055defe2 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -9,132 +9,107 @@ const validScopeObject: ExternalScopeObject = { describe('Scope Validation', () => { 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', - { + it('returns false when the scopeString is neither a CAIP namespace or CAIP chainId', () => { + expect( + isValidScope('not a namespace or a caip chain id', validScopeObject), + ).toBe(false); + }); + + it('returns true when the scopeString is a valid CAIP namespace and the scopeObject is valid', () => { + expect(isValidScope('eip155', validScopeObject)).toBe(true); + }); + + it('returns true when the scopeString is a valid CAIP chainId and the scopeObject is valid', () => { + expect(isValidScope('eip155:1', validScopeObject)).toBe(true); + }); + + it('returns false when the scopeString is a CAIP chainId but references is nonempty', () => { + expect( + isValidScope('eip155:1', { ...validScopeObject, references: ['5'], - }, - ], - [ - false, - 'the scopeString is a valid CAIP namespace but references are invalid CAIP references', - 'eip155', - { + }), + ).toBe(false); + }); + + it('returns false when the scopeString is a valid CAIP namespace but references are invalid CAIP references', () => { + expect( + isValidScope('eip155', { ...validScopeObject, references: ['@'], - }, - ], - [ - false, - 'methods contains empty string', - validScopeString, - { + }), + ).toBe(false); + }); + + it('returns false when methods contains empty string', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, methods: [''], - }, - ], - [ - false, - 'methods contains non-string', - validScopeString, - { + }), + ).toBe(false); + }); + + it('returns false when methods contains non-string', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, + // @ts-expect-error Intentionally invalid input methods: [{ foo: 'bar' }], - }, - ], - [ - true, - 'methods contains only strings', - validScopeString, - { + }), + ).toBe(false); + }); + + it('returns true when methods contains only strings', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, methods: ['method1', 'method2'], - }, - ], - [ - false, - 'notifications contains empty string', - validScopeString, - { + }), + ).toBe(true); + }); + + it('returns false when notifications contains empty string', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, notifications: [''], - }, - ], - [ - false, - 'notifications contains non-string', - validScopeString, - { - ...validScopeObject, - notifications: [{ foo: 'bar' }], - }, - ], - [ - false, - 'notifications contains non-string', - 'eip155:1', - { + }), + ).toBe(false); + }); + + it('returns false when notifications contains non-string', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, + // @ts-expect-error Intentionally invalid input notifications: [{ foo: 'bar' }], - }, - ], - [ - false, - 'unexpected properties are defined', - validScopeString, - { + }), + ).toBe(false); + }); + + it('returns false when unexpected properties are defined', () => { + expect( + isValidScope(validScopeString, { ...validScopeObject, + // @ts-expect-error Intentionally invalid input unexpectedParam: 'foobar', - }, - ], - [ - true, - 'only expected properties are defined', - validScopeString, - { + }), + ).toBe(false); + }); + + it('returns true when only expected properties are defined', () => { + expect( + isValidScope(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); - }, - ); + }), + ).toBe(true); + }); }); describe('validateScopes', () => { From 72e228e39c2f299348f7fc121f5b3795e499b5ed Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 13:18:04 -0700 Subject: [PATCH 078/144] Fix validation.test.ts ts-expect-error --- packages/multichain/src/scope/validation.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index f2055defe2..5a96215901 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -146,7 +146,6 @@ describe('Scope Validation', () => { 'eip155:64': {}, }, { - // @ts-expect-error Intentionally invalid input 'eip155:2': {}, 'eip155:5': validScopeObjectWithAccounts, }, From bd104068451e99687e8683fdde5fb5a693bd2e74 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 29 Oct 2024 13:18:19 -0700 Subject: [PATCH 079/144] flatten isSupportedAccount it.each --- .../multichain/src/scope/supported.test.ts | 304 +++++++++--------- 1 file changed, 150 insertions(+), 154 deletions(-) diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index 76f41b9a44..9c45a27d95 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -1,5 +1,3 @@ -import type { CaipAccountId } from '@metamask/utils'; - import { KnownNotifications, KnownRpcMethods, @@ -103,158 +101,156 @@ 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', - }, - ], - ], + it('returns true if eoa account matching eip155 namespaced address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('eip155:1:0xdeadbeef', getInternalAccounts), + ).toBe(true); + }); - [ - 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); - }, - ); + it('returns true if eoa account matching eip155 namespaced address with different casing exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadBEEF', + }, + ]); + expect( + isSupportedAccount('eip155:1:0xDEADbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns true if erc4337 account matching eip155 namespaced address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('eip155:1:0xdeadbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns true if erc4337 account matching eip155 namespaced address with different casing exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:erc4337', + address: '0xdeadBEEF', + }, + ]); + expect( + isSupportedAccount('eip155:1:0xDEADbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns false if neither eoa or erc4337 account matching eip155 namespaced address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'other', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('eip155:1:0xdeadbeef', getInternalAccounts), + ).toBe(false); + }); + + it('returns true if eoa account matching wallet:eip155 address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('wallet:eip155:0xdeadbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns true if eoa account matching wallet:eip155 address with different casing exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadBEEF', + }, + ]); + expect( + isSupportedAccount('wallet:eip155:0xDEADbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns true if erc4337 account matching wallet:eip155 address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('wallet:eip155:0xdeadbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns true if erc4337 account matching wallet:eip155 address with different casing exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:erc4337', + address: '0xdeadBEEF', + }, + ]); + expect( + isSupportedAccount('wallet:eip155:0xDEADbeef', getInternalAccounts), + ).toBe(true); + }); + + it('returns false if neither eoa or erc4337 account matching wallet:eip155 address exists', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'other', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('wallet:eip155:0xdeadbeef', getInternalAccounts), + ).toBe(false); + }); + + it('returns false if wallet namespace with unknown reference', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('wallet:foobar:0xdeadbeef', getInternalAccounts), + ).toBe(false); + }); + + it('returns false if unknown namespace', () => { + const getInternalAccounts = jest.fn().mockReturnValue([ + { + type: 'eip155:eoa', + address: '0xdeadbeef', + }, + { + type: 'eip155:erc4337', + address: '0xdeadbeef', + }, + ]); + expect( + isSupportedAccount('foo:bar:0xdeadbeef', getInternalAccounts), + ).toBe(false); + }); }); }); From e5e39eb88ca7037597b1c67662be176ebdd93a34 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 11:30:50 -0600 Subject: [PATCH 080/144] cleanup removeAccount function docs/comments --- packages/multichain/src/caip25Permission.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 5342fbeb11..d2c3ac3267 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -209,14 +209,13 @@ function removeAccountOnScope(scopeObject: ScopeObject, targetAddress: string) { * Removes the target account from the scope object. * * @param existingScopes - The scope object to remove the account from. - * @param targetAddress - The address to remove from the scope object. + * @param targetAddress - The address to remove from the scope object. Not a CAIP-10 formatted address because it will be removed across each chain scope. * @returns The updated scope object. */ function removeAccount( existingScopes: Caip25CaveatValue, - targetAddress: string, // non caip-10 formatted address + targetAddress: string, ) { - // copy existing scopes const copyOfExistingScopes = cloneDeep(existingScopes); [ @@ -228,7 +227,6 @@ function removeAccount( }); }); - // deep equal check for changes const noChange = isEqual(copyOfExistingScopes, existingScopes); if (noChange) { From 27e08081657ef33411c90089ee56a884f4064d1d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 11:35:15 -0600 Subject: [PATCH 081/144] add JSDoc for caip25EndowmentBuilder --- packages/multichain/src/caip25Permission.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index d2c3ac3267..042eeec97d 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -152,6 +152,11 @@ const specificationBuilder: PermissionSpecificationBuilder< }; }; +/** + * The `caip25` endowment specification builder. Passed to the + * `PermissionController` for constructing and validating the + * `endowment:caip25` permission. + */ export const caip25EndowmentBuilder = Object.freeze({ targetName: Caip25EndowmentPermissionName, specificationBuilder, From 0c679a8fb66139d4f5a35cd2817e6bf96cc0503a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 11:51:39 -0600 Subject: [PATCH 082/144] repace reduceKeyHelper with Object.fromEntries --- packages/multichain/src/caip25Permission.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 042eeec97d..84458e4f88 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -173,16 +173,6 @@ export const Caip25CaveatMutators = { }, }; -const reduceKeysHelper = ( - acc: Record, - [key, value]: [Key, Value], -) => { - return { - ...acc, - [key]: value, - }; -}; - /** * Removes the account from the scope object. * @@ -279,8 +269,8 @@ function removeScope( return { operation: CaveatMutatorOperation.UpdateValue, value: { - requiredScopes: newRequiredScopes.reduce(reduceKeysHelper, {}), - optionalScopes: newOptionalScopes.reduce(reduceKeysHelper, {}), + requiredScopes: Object.fromEntries(newRequiredScopes), + optionalScopes: Object.fromEntries(newOptionalScopes), }, }; } From c5ed91c41c8e5e9b23cd1dbb2a59316f0e7a8e60 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 11:52:39 -0600 Subject: [PATCH 083/144] add bip122 entries to "known" method consts --- packages/multichain/src/scope/constants.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index 81fbd4db63..ba69f489dd 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -30,6 +30,7 @@ const Eip155Methods = MetaMaskOpenRPCDocument.methods // Methods for ecosystem that are chain specific export const KnownRpcMethods: Record = { eip155: Eip155Methods, + bip122: [], }; // Methods for ecosystems that aren't chain specific @@ -38,10 +39,12 @@ export const KnownWalletNamespaceRpcMethods: Record< string[] > = { eip155: WalletEip155Methods, + bip122: [], }; // Notifications export const KnownNotifications: Record = { eip155: ['eth_subscription'], + bip122: [], }; From 91e426cff6d19eb5d65389780d8eac5da6a2cb5f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 12:05:21 -0600 Subject: [PATCH 084/144] removeAccountOnScope -> removeAccountFromScopeObject --- packages/multichain/src/caip25Permission.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 84458e4f88..e383246334 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -192,7 +192,10 @@ function removeAccountFilterFn(targetAddress: string) { * @param scopeObject - The scope object to remove the account from. * @param targetAddress - The address to remove from the scope object. */ -function removeAccountOnScope(scopeObject: ScopeObject, targetAddress: string) { +function removeAccountFromScopeObject( + scopeObject: ScopeObject, + targetAddress: string, +) { if (scopeObject.accounts) { scopeObject.accounts = scopeObject.accounts.filter( removeAccountFilterFn(targetAddress), @@ -218,7 +221,7 @@ function removeAccount( copyOfExistingScopes.optionalScopes, ].forEach((scopes) => { Object.entries(scopes).forEach(([, scopeObject]) => { - removeAccountOnScope(scopeObject, targetAddress); + removeAccountFromScopeObject(scopeObject, targetAddress); }); }); From 619e34d2f3cd6bf44370b491af469ac5f2c5df02 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 12:06:19 -0600 Subject: [PATCH 085/144] rename param of 'removeAccount' caip25Permission helper function --- packages/multichain/src/caip25Permission.ts | 25 ++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index e383246334..35d7c29961 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -206,26 +206,25 @@ function removeAccountFromScopeObject( /** * Removes the target account from the scope object. * - * @param existingScopes - The scope object to remove the account from. + * @param caip25CaveatValue - The CAIP-25 permission caveat value from which to remove the account (across all chain scopes). * @param targetAddress - The address to remove from the scope object. Not a CAIP-10 formatted address because it will be removed across each chain scope. * @returns The updated scope object. */ function removeAccount( - existingScopes: Caip25CaveatValue, + caip25CaveatValue: Caip25CaveatValue, targetAddress: string, ) { - const copyOfExistingScopes = cloneDeep(existingScopes); + const copyOfCaveatValue = cloneDeep(caip25CaveatValue); - [ - copyOfExistingScopes.requiredScopes, - copyOfExistingScopes.optionalScopes, - ].forEach((scopes) => { - Object.entries(scopes).forEach(([, scopeObject]) => { - removeAccountFromScopeObject(scopeObject, targetAddress); - }); - }); + [copyOfCaveatValue.requiredScopes, copyOfCaveatValue.optionalScopes].forEach( + (scopes) => { + Object.entries(scopes).forEach(([, scopeObject]) => { + removeAccountFromScopeObject(scopeObject, targetAddress); + }); + }, + ); - const noChange = isEqual(copyOfExistingScopes, existingScopes); + const noChange = isEqual(copyOfCaveatValue, caip25CaveatValue); if (noChange) { return { @@ -235,7 +234,7 @@ function removeAccount( return { operation: CaveatMutatorOperation.UpdateValue, - value: copyOfExistingScopes, + value: copyOfCaveatValue, }; } From 35a81ce96bc3ad7a55863df672c58c5169c2e78e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 12:14:03 -0600 Subject: [PATCH 086/144] remove unnecessary type assertion --- packages/multichain/src/scope/supported.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 42f6ccd318..70a50d2946 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -1,5 +1,5 @@ import { toHex, isEqualCaseInsensitive } from '@metamask/controller-utils'; -import type { CaipAccountId, Hex, CaipChainId } from '@metamask/utils'; +import type { CaipAccountId, Hex } from '@metamask/utils'; import { KnownCaipNamespace, parseCaipAccountId } from '@metamask/utils'; import { @@ -15,7 +15,7 @@ export const isSupportedScopeString = ( scopeString: string, isChainIdSupported: (chainId: Hex) => boolean, ) => { - const { namespace, reference } = parseScopeString(scopeString as CaipChainId); + const { namespace, reference } = parseScopeString(scopeString); switch (namespace) { case KnownCaipNamespace.Wallet: From b04a941ceff57c070b4795ac88d8267231cabce4 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 14:30:36 -0600 Subject: [PATCH 087/144] fix bug in normalizeScope --- packages/multichain/src/scope/transform.test.ts | 8 ++++++++ packages/multichain/src/scope/transform.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index 13b69dde96..408e7ee38b 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -74,6 +74,14 @@ describe('Scope Transform', () => { normalizedScopes['eip155:5'].methods, ); }); + + it('returns the scope as is when `references` is an empty array', () => { + expect( + normalizeScope('eip155', { ...validScopeObject, references: [] }), + ).toStrictEqual({ + eip155: validScopeObject, + }); + }); }); }); diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 94f69d060d..7890f3fda0 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -42,7 +42,7 @@ export const normalizeScope = ( }; // Scope is already a CAIP-2 ID and has no references to flatten - if (!namespace || reference || !references) { + if (!namespace || reference || !references || references?.length === 0) { return { [scopeString]: normalizedScopeObject }; } From 41e0413ca25ddb29da7af1eeff666e88cef6369a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 14:37:50 -0600 Subject: [PATCH 088/144] refactor normalizeScope for clarity --- packages/multichain/src/scope/transform.ts | 27 ++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 7890f3fda0..872c64255f 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -36,23 +36,26 @@ export const normalizeScope = ( const { references, ...scopeObject } = externalScopeObject; const { namespace, reference } = parseScopeString(scopeString); - const normalizedScopeObject = { + const normalizedScopeObject: ScopeObject = { accounts: [], ...scopeObject, }; - // Scope is already a CAIP-2 ID and has no references to flatten - if (!namespace || reference || !references || references?.length === 0) { - return { [scopeString]: normalizedScopeObject }; - } - - const scopeMap: ScopesObject = {}; - references.forEach((nestedReference: CaipReference) => { - scopeMap[`${namespace}:${nestedReference}`] = cloneDeep( - normalizedScopeObject, + const shouldFlatten = + namespace && + !reference && + references !== undefined && + references.length > 0; + + if (shouldFlatten) { + return Object.fromEntries( + references.map((ref: CaipReference) => [ + `${namespace}:${ref}`, + cloneDeep(normalizedScopeObject), + ]), ); - }); - return scopeMap; + } + return { [scopeString]: normalizedScopeObject }; }; export const mergeScopeObject = ( From 3f56531543e1b1d07729e78bc4b70d40d8369df5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 14:48:10 -0600 Subject: [PATCH 089/144] add JSDocs for scope(s)Object types --- packages/multichain/src/scope/types.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index 2742bc5457..75f42ac7cf 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -12,13 +12,21 @@ import type { Json, } from '@metamask/utils'; -// These External prefixed types represent the CAIP-217 -// Scope and ScopeObject as defined in the spec. +/** + * Represents a `scopeString` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). + */ export type ExternalScopeString = CaipChainId | CaipNamespace; +/** + * Represents a `scopeObject` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). + */ export type ExternalScopeObject = Omit & { references?: CaipReference[]; accounts?: CaipAccountId[]; }; +/** + * Represents a `scope` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). + * TODO update the language in CAIP-217 to use "scope" instead of "scopeObject" for this full record type. + */ export type ExternalScopesObject = Record< ExternalScopeString, ExternalScopeObject From 9c40d696318f7c031278ca3233f1ca50b4d9a3e2 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 15:47:55 -0600 Subject: [PATCH 090/144] add JSDocs for Scope types --- packages/multichain/src/scope/types.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index 75f42ac7cf..12d3eae4b6 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -32,14 +32,18 @@ export type ExternalScopesObject = Record< 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. +/** + * Represents a `scopeString` as defined in + * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that + * CAIP namespaces (aside from "wallet") are disallowed for our internal representations of CAIP-25 session scopes + */ export type ScopeString = CaipChainId | KnownCaipNamespace.Wallet; +/** + * Represents a `scopeObject` as defined in + * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that + * the `references` property is disallowed for our internal representations of CAIP-25 session scopes. + * e.g. We flatten each reference into its own scopeObject before storing them in a `endowment:caip25` permission. + */ export type ScopeObject = { methods: string[]; notifications: string[]; @@ -47,6 +51,12 @@ export type ScopeObject = { rpcDocuments?: string[]; rpcEndpoints?: string[]; }; +/** + * Represents a keyed `scopeObject` as defined in + * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that + * `scopeObject`s do not contain `references` in our internal representations of CAIP-25 session scopes. + * e.g. We flatten each reference into its own scopeObject before storing them in a `endowment:caip25` permission. + */ export type ScopesObject = Record & { [KnownCaipNamespace.Wallet]?: ScopeObject; }; From 71f254a16d58ea81640e41f6d32acc9b83481f86 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 16:02:35 -0600 Subject: [PATCH 091/144] add 'Internal' prefix to CAIP-25 Scope types --- .../caip-permission-adapter-eth-accounts.ts | 14 ++++---- ...caip-permission-adapter-permittedChains.ts | 12 +++---- .../multichain/src/caip25Permission.test.ts | 2 +- packages/multichain/src/caip25Permission.ts | 10 +++--- packages/multichain/src/index.ts | 6 ++-- packages/multichain/src/scope/assert.test.ts | 4 +-- packages/multichain/src/scope/assert.ts | 6 ++-- .../multichain/src/scope/authorization.ts | 6 ++-- .../multichain/src/scope/transform.test.ts | 4 +-- packages/multichain/src/scope/transform.ts | 34 +++++++++---------- packages/multichain/src/scope/types.ts | 10 +++--- packages/multichain/src/scope/validation.ts | 1 + 12 files changed, 55 insertions(+), 54 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 87848250b5..d699c652de 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -7,10 +7,10 @@ import { import type { Caip25CaveatValue } from '../caip25Permission'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; -import type { ScopesObject, ScopeString } from '../scope/types'; +import type { InternalScopesObject, InternalScopeString } from '../scope/types'; import { KnownWalletScopeString, parseScopeString } from '../scope/types'; -const isEip155ScopeString = (scopeString: ScopeString) => { +const isEip155ScopeString = (scopeString: InternalScopeString) => { const { namespace } = parseScopeString(scopeString); return ( @@ -45,19 +45,19 @@ export const getEthAccounts = ( }; const setEthAccountsForScopesObject = ( - scopesObject: ScopesObject, + scopesObject: InternalScopesObject, accounts: Hex[], ) => { - const updatedScopesObject: ScopesObject = {}; + const updatedScopesObject: InternalScopesObject = {}; Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet; if ( - !isEip155ScopeString(scopeString as ScopeString) && + !isEip155ScopeString(scopeString as InternalScopeString) && !isWalletNamespace ) { - updatedScopesObject[scopeString as ScopeString] = scopeObject; + updatedScopesObject[scopeString as InternalScopeString] = scopeObject; return; } @@ -68,7 +68,7 @@ const setEthAccountsForScopesObject = ( : `${scopeString}:${account}`) as CaipAccountId, ); - updatedScopesObject[scopeString as ScopeString] = { + updatedScopesObject[scopeString as InternalScopeString] = { ...scopeObject, accounts: caipAccounts, }; diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index f994418946..8c3dc9d39f 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -5,7 +5,7 @@ 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 type { InternalScopesObject, InternalScopeString } from '../scope/types'; import { KnownWalletScopeString, parseScopeString } from '../scope/types'; export const getPermittedEthChainIds = ( @@ -61,24 +61,24 @@ export const addPermittedEthChainId = ( }; const filterEthScopesObjectByChainId = ( - scopesObject: ScopesObject, + scopesObject: InternalScopesObject, chainIds: Hex[], ) => { - const updatedScopesObject: ScopesObject = {}; + const updatedScopesObject: InternalScopesObject = {}; Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { const { namespace, reference } = parseScopeString(scopeString); if (!reference) { - updatedScopesObject[scopeString as ScopeString] = scopeObject; + updatedScopesObject[scopeString as InternalScopeString] = scopeObject; return; } if (namespace === KnownCaipNamespace.Eip155) { const chainId = toHex(reference); if (chainIds.includes(chainId)) { - updatedScopesObject[scopeString as ScopeString] = scopeObject; + updatedScopesObject[scopeString as InternalScopeString] = scopeObject; } } else { - updatedScopesObject[scopeString as ScopeString] = scopeObject; + updatedScopesObject[scopeString as InternalScopeString] = scopeObject; } }); diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index 4c9e3ce2c0..b641c395e8 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -891,7 +891,7 @@ describe('caip25EndowmentBuilder', () => { ); }); - it('does not throw if the input requiredScopes and optionalScopes ScopesObject are already validated and normalized', () => { + it('does not throw if the input requiredScopes and optionalScopes InternalScopesObject are already validated and normalized', () => { MockScopeAuthorization.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: { 'eip155:1': { diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 35d7c29961..10b46ad18b 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -23,13 +23,13 @@ import { assertScopesSupported } from './scope/assert'; import { validateAndNormalizeScopes } from './scope/authorization'; import type { ExternalScopeString, - ScopeObject, - ScopesObject, + InternalScopeObject, + InternalScopesObject, } from './scope/types'; export type Caip25CaveatValue = { - requiredScopes: ScopesObject; - optionalScopes: ScopesObject; + requiredScopes: InternalScopesObject; + optionalScopes: InternalScopesObject; sessionProperties?: Record; isMultichainOrigin: boolean; }; @@ -193,7 +193,7 @@ function removeAccountFilterFn(targetAddress: string) { * @param targetAddress - The address to remove from the scope object. */ function removeAccountFromScopeObject( - scopeObject: ScopeObject, + scopeObject: InternalScopeObject, targetAddress: string, ) { if (scopeObject.accounts) { diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 160a4c1748..d64d66ed4c 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -20,9 +20,9 @@ export type { ExternalScopeString, ExternalScopeObject, ExternalScopesObject, - ScopeString, - ScopeObject, - ScopesObject, + InternalScopeString, + InternalScopeObject, + InternalScopesObject, ScopedProperties, NonWalletKnownCaipNamespace, } from './scope/types'; diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index e333cde866..2537f1e63e 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -2,7 +2,7 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import { assertScopeSupported, assertScopesSupported } from './assert'; import * as Supported from './supported'; -import type { ScopeObject } from './types'; +import type { InternalScopeObject } from './types'; jest.mock('./supported', () => ({ isSupportedScopeString: jest.fn(), @@ -11,7 +11,7 @@ jest.mock('./supported', () => ({ })); const MockSupported = jest.mocked(Supported); -const validScopeObject: ScopeObject = { +const validScopeObject: InternalScopeObject = { methods: [], notifications: [], accounts: [], diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 77a9dd6205..22dd18b9cf 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -6,11 +6,11 @@ import { isSupportedNotification, isSupportedScopeString, } from './supported'; -import type { ScopeObject, ScopesObject } from './types'; +import type { InternalScopeObject, InternalScopesObject } from './types'; export const assertScopeSupported = ( scopeString: string, - scopeObject: ScopeObject, + scopeObject: InternalScopeObject, { isChainIdSupported, }: { @@ -56,7 +56,7 @@ export const assertScopeSupported = ( }; export const assertScopesSupported = ( - scopes: ScopesObject, + scopes: InternalScopesObject, { isChainIdSupported, }: { diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index 2b97a231e1..d7b8dbb18f 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -4,7 +4,7 @@ import { normalizeAndMergeScopes } from './transform'; import type { ExternalScopesObject, ExternalScopeString, - ScopesObject, + InternalScopesObject, } from './types'; import { validateScopes } from './validation'; @@ -26,8 +26,8 @@ export const validateAndNormalizeScopes = ( requiredScopes: ExternalScopesObject, optionalScopes: ExternalScopesObject, ): { - normalizedRequiredScopes: ScopesObject; - normalizedOptionalScopes: ScopesObject; + normalizedRequiredScopes: InternalScopesObject; + normalizedOptionalScopes: InternalScopesObject; } => { const { validRequiredScopes, validOptionalScopes } = validateScopes( requiredScopes, diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index 408e7ee38b..613f3ba20a 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -4,14 +4,14 @@ import { mergeScopeObject, normalizeAndMergeScopes, } from './transform'; -import type { ExternalScopeObject, ScopeObject } from './types'; +import type { ExternalScopeObject, InternalScopeObject } from './types'; const externalScopeObject: ExternalScopeObject = { methods: [], notifications: [], }; -const validScopeObject: ScopeObject = { +const validScopeObject: InternalScopeObject = { methods: [], notifications: [], accounts: [], diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 872c64255f..0813da4798 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -4,9 +4,9 @@ import { cloneDeep } from 'lodash'; import type { ExternalScopeObject, ExternalScopesObject, - ScopeString, - ScopeObject, - ScopesObject, + InternalScopeString, + InternalScopeObject, + InternalScopesObject, } from './types'; import { parseScopeString } from './types'; @@ -22,7 +22,7 @@ export const getUniqueArrayItems = (list: Value[]): Value[] => { /** * Normalizes a ScopeString and ExternalScopeObject into a separate - * ScopeString and ScopeObject for each reference in the `references` + * InternalScopeString and InternalScopeObject for each reference in the `references` * value if defined and adds an empty `accounts` array if not defined. * * @param scopeString - The string representing the scope @@ -32,11 +32,11 @@ export const getUniqueArrayItems = (list: Value[]): Value[] => { export const normalizeScope = ( scopeString: string, externalScopeObject: ExternalScopeObject, -): ScopesObject => { +): InternalScopesObject => { const { references, ...scopeObject } = externalScopeObject; const { namespace, reference } = parseScopeString(scopeString); - const normalizedScopeObject: ScopeObject = { + const normalizedScopeObject: InternalScopeObject = { accounts: [], ...scopeObject, }; @@ -59,10 +59,10 @@ export const normalizeScope = ( }; export const mergeScopeObject = ( - scopeObjectA: ScopeObject, - scopeObjectB: ScopeObject, + scopeObjectA: InternalScopeObject, + scopeObjectB: InternalScopeObject, ) => { - const mergedScopeObject: ScopeObject = { + const mergedScopeObject: InternalScopeObject = { methods: getUniqueArrayItems([ ...scopeObjectA.methods, ...scopeObjectB.methods, @@ -95,13 +95,13 @@ export const mergeScopeObject = ( }; export const mergeScopes = ( - scopeA: ScopesObject, - scopeB: ScopesObject, -): ScopesObject => { - const scope: ScopesObject = {}; + scopeA: InternalScopesObject, + scopeB: InternalScopesObject, +): InternalScopesObject => { + const scope: InternalScopesObject = {}; Object.entries(scopeA).forEach(([_scopeString, scopeObjectA]) => { - const scopeString = _scopeString as ScopeString; + const scopeString = _scopeString as InternalScopeString; const scopeObjectB = scopeB[scopeString]; scope[scopeString] = scopeObjectB @@ -110,7 +110,7 @@ export const mergeScopes = ( }); Object.entries(scopeB).forEach(([_scopeString, scopeObjectB]) => { - const scopeString = _scopeString as ScopeString; + const scopeString = _scopeString as InternalScopeString; const scopeObjectA = scopeA[scopeString]; if (!scopeObjectA) { @@ -123,8 +123,8 @@ export const mergeScopes = ( export const normalizeAndMergeScopes = ( scopes: ExternalScopesObject, -): ScopesObject => { - let mergedScopes: ScopesObject = {}; +): InternalScopesObject => { + let mergedScopes: InternalScopesObject = {}; Object.keys(scopes).forEach((scopeString) => { const normalizedScopes = normalizeScope(scopeString, scopes[scopeString]); mergedScopes = mergeScopes(mergedScopes, normalizedScopes); diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index 12d3eae4b6..a678a7d3c0 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -19,7 +19,7 @@ export type ExternalScopeString = CaipChainId | CaipNamespace; /** * Represents a `scopeObject` as defined in [CAIP-217](https://chainagnostic.org/CAIPs/caip-217). */ -export type ExternalScopeObject = Omit & { +export type ExternalScopeObject = Omit & { references?: CaipReference[]; accounts?: CaipAccountId[]; }; @@ -37,14 +37,14 @@ export type ExternalScopesObject = Record< * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that * CAIP namespaces (aside from "wallet") are disallowed for our internal representations of CAIP-25 session scopes */ -export type ScopeString = CaipChainId | KnownCaipNamespace.Wallet; +export type InternalScopeString = CaipChainId | KnownCaipNamespace.Wallet; /** * Represents a `scopeObject` as defined in * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that * the `references` property is disallowed for our internal representations of CAIP-25 session scopes. * e.g. We flatten each reference into its own scopeObject before storing them in a `endowment:caip25` permission. */ -export type ScopeObject = { +export type InternalScopeObject = { methods: string[]; notifications: string[]; accounts: CaipAccountId[]; @@ -57,8 +57,8 @@ export type ScopeObject = { * `scopeObject`s do not contain `references` in our internal representations of CAIP-25 session scopes. * e.g. We flatten each reference into its own scopeObject before storing them in a `endowment:caip25` permission. */ -export type ScopesObject = Record & { - [KnownCaipNamespace.Wallet]?: ScopeObject; +export type InternalScopesObject = Record & { + [KnownCaipNamespace.Wallet]?: InternalScopeObject; }; export type ScopedProperties = Record> & { diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index 3dbb7cfa25..0689d0f9d5 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -35,6 +35,7 @@ export const isValidScope = ( if (reference && references && references.length > 0) { return false; } + if (namespace && references) { const areReferencesValid = references.every((nestedReference) => { return isCaipReference(nestedReference); From 828080f20870fc225261eea6b88add2071023cf3 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 16:06:36 -0600 Subject: [PATCH 092/144] improve JSDoc --- packages/multichain/src/scope/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index a678a7d3c0..bce910efd2 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -35,7 +35,7 @@ export type ExternalScopesObject = Record< /** * Represents a `scopeString` as defined in * [CAIP-217](https://chainagnostic.org/CAIPs/caip-217), with the exception that - * CAIP namespaces (aside from "wallet") are disallowed for our internal representations of CAIP-25 session scopes + * CAIP namespaces without a reference (aside from "wallet") are disallowed for our internal representations of CAIP-25 session scopes */ export type InternalScopeString = CaipChainId | KnownCaipNamespace.Wallet; /** From de7ac07f0dc1e0db3a83a7702470dd9bce4fdabe Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 7 Nov 2024 16:07:09 -0600 Subject: [PATCH 093/144] Update packages/multichain/src/scope/authorization.ts Co-authored-by: Elliot Winkler --- packages/multichain/src/scope/authorization.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index d7b8dbb18f..9fa31619ea 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -8,6 +8,9 @@ import type { } from './types'; import { validateScopes } from './validation'; +/** + * Represents the parameters of a [CAIP-25](https://chainagnostic.org/CAIPs/caip-25) request. + */ export type Caip25Authorization = ( | { requiredScopes: ExternalScopesObject; From aa24e964a60d94cd7dc9830a90376607c5aafa93 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 16:19:01 -0600 Subject: [PATCH 094/144] cleanup consts/types and comments --- packages/multichain/src/scope/constants.ts | 16 +++++----------- packages/multichain/src/scope/types.ts | 5 ----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index ba69f489dd..be9223ee6e 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -1,18 +1,13 @@ import MetaMaskOpenRPCDocument from '@metamask/api-specs'; -import type { KnownCaipNamespace } from '@metamask/utils'; -// ScopeString for ecosystems that aren't chain specific +import type { NonWalletKnownCaipNamespace } from './types'; + +// ScopeStrings for offchain methods that are not specific to a chainId but are specific to a CAIP namespace export enum KnownWalletScopeString { Eip155 = 'wallet:eip155', } -// Known CAIP Namespaces excluding "wallet" -export type NonWalletKnownCaipNamespace = Exclude< - KnownCaipNamespace, - KnownCaipNamespace.Wallet ->; - -// Methods that do not belong to an ecosystem +// Methods that do not belong exclusively to any CAIP namespace export const KnownWalletRpcMethods: string[] = [ 'wallet_registerOnboarding', 'wallet_scanQRCode', @@ -33,7 +28,7 @@ export const KnownRpcMethods: Record = { bip122: [], }; -// Methods for ecosystems that aren't chain specific +// Methods for CAIP namespaces that aren't chain specific export const KnownWalletNamespaceRpcMethods: Record< NonWalletKnownCaipNamespace, string[] @@ -42,7 +37,6 @@ export const KnownWalletNamespaceRpcMethods: Record< bip122: [], }; -// Notifications export const KnownNotifications: Record = { eip155: ['eth_subscription'], diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index bce910efd2..e73e571bd6 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -83,11 +83,6 @@ export const parseScopeString = ( return {}; }; -// ScopeString for ecosystems that aren't chain specific -export enum KnownWalletScopeString { - Eip155 = 'wallet:eip155', -} - // Known CAIP Namespaces excluding "wallet" export type NonWalletKnownCaipNamespace = Exclude< KnownCaipNamespace, From da87d6508a582453b57b0498d7d952edac36d372 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 16:24:04 -0600 Subject: [PATCH 095/144] fix imports --- .../src/adapters/caip-permission-adapter-eth-accounts.ts | 3 ++- .../adapters/caip-permission-adapter-permittedChains.ts | 8 ++++++-- packages/multichain/src/index.test.ts | 2 +- packages/multichain/src/index.ts | 3 ++- 4 files changed, 11 insertions(+), 5 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 d699c652de..f9cb66e75b 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,10 @@ import { } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; +import { KnownWalletScopeString } from '../scope/constants'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { InternalScopesObject, InternalScopeString } from '../scope/types'; -import { KnownWalletScopeString, parseScopeString } from '../scope/types'; +import { parseScopeString } from '../scope/types'; const isEip155ScopeString = (scopeString: InternalScopeString) => { const { namespace } = parseScopeString(scopeString); diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 8c3dc9d39f..000c835419 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,10 +3,14 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import { KnownNotifications, KnownRpcMethods } from '../scope/constants'; +import { + KnownNotifications, + KnownRpcMethods, + KnownWalletScopeString, +} from '../scope/constants'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { InternalScopesObject, InternalScopeString } from '../scope/types'; -import { KnownWalletScopeString, parseScopeString } from '../scope/types'; +import { 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 1f3ede7153..a35eb21b0e 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -14,8 +14,8 @@ describe('@metamask/multichain', () => { "KnownRpcMethods", "KnownWalletNamespaceRpcMethods", "KnownNotifications", - "parseScopeString", "KnownWalletScopeString", + "parseScopeString", "isSupportedScopeString", "isSupportedAccount", "isSupportedMethod", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index d64d66ed4c..cff11a05f1 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -15,6 +15,7 @@ export { KnownRpcMethods, KnownWalletNamespaceRpcMethods, KnownNotifications, + KnownWalletScopeString, } from './scope/constants'; export type { ExternalScopeString, @@ -26,7 +27,7 @@ export type { ScopedProperties, NonWalletKnownCaipNamespace, } from './scope/types'; -export { parseScopeString, KnownWalletScopeString } from './scope/types'; +export { parseScopeString } from './scope/types'; export { isSupportedScopeString, isSupportedAccount, From 8194afdbca66c7baf06647d70df5d7add2b3d0dc Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 16:42:57 -0600 Subject: [PATCH 096/144] add more TSDocs --- .../caip-permission-adapter-eth-accounts.ts | 22 ++++++++++++++++++ ...caip-permission-adapter-permittedChains.ts | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) 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 f9cb66e75b..be84e25d10 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -11,6 +11,11 @@ import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { InternalScopesObject, InternalScopeString } from '../scope/types'; import { parseScopeString } from '../scope/types'; +/** + * Checks if a scope string is either an EIP155 or wallet namespaced scope string. + * @param scopeString - The scope string to check. + * @returns True if the scope string is an EIP155 or wallet namespaced scope string, false otherwise. + */ const isEip155ScopeString = (scopeString: InternalScopeString) => { const { namespace } = parseScopeString(scopeString); @@ -20,6 +25,11 @@ const isEip155ScopeString = (scopeString: InternalScopeString) => { ); }; +/** + * Gets the Ethereum (EIP155 namespaced) accounts from the required and optional scopes. + * @param caip25CaveatValue - The CAIP-25 caveat value to get the Ethereum accounts from. + * @returns An array of Ethereum accounts. + */ export const getEthAccounts = ( caip25CaveatValue: Pick< Caip25CaveatValue, @@ -45,6 +55,12 @@ export const getEthAccounts = ( return getUniqueArrayItems(ethAccounts); }; +/** + * Sets the Ethereum (EIP155 namespaced) accounts for the given scopes object. + * @param scopesObject - The scopes object to set the Ethereum accounts for. + * @param accounts - The Ethereum accounts to set. + * @returns The updated scopes object with the Ethereum accounts set. + */ const setEthAccountsForScopesObject = ( scopesObject: InternalScopesObject, accounts: Hex[], @@ -78,6 +94,12 @@ const setEthAccountsForScopesObject = ( return updatedScopesObject; }; +/** + * Sets the Ethereum (EIP155 namespaced) accounts for the given CAIP-25 caveat value. + * @param caip25CaveatValue - The CAIP-25 caveat value to set the Ethereum accounts for. + * @param accounts - The Ethereum accounts to set. + * @returns The updated CAIP-25 caveat value with the Ethereum accounts set. + */ export const setEthAccounts = ( caip25CaveatValue: Caip25CaveatValue, accounts: Hex[], diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 000c835419..270783cb32 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -12,6 +12,11 @@ import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { InternalScopesObject, InternalScopeString } from '../scope/types'; import { parseScopeString } from '../scope/types'; +/** + * Gets the Ethereum (EIP155 namespaced) chainIDs from the required and optional scopes. + * @param caip25CaveatValue - The CAIP-25 caveat value from which to get the Ethereum chainIDs. + * @returns An array of Ethereum chainIDs. + */ export const getPermittedEthChainIds = ( caip25CaveatValue: Pick< Caip25CaveatValue, @@ -34,6 +39,12 @@ export const getPermittedEthChainIds = ( return getUniqueArrayItems(ethChainIds); }; +/** + * Adds an Ethereum (EIP155 namespaced) chainID to the required and optional scopes. + * @param caip25CaveatValue - The CAIP-25 caveat value to add the Ethereum chainID to. + * @param chainId - The Ethereum chainID to add. + * @returns The updated CAIP-25 caveat value with the added Ethereum chainID. + */ export const addPermittedEthChainId = ( caip25CaveatValue: Caip25CaveatValue, chainId: Hex, @@ -64,6 +75,12 @@ export const addPermittedEthChainId = ( }; }; +/** + * Filters the scopes object to only include the scopes for the given chainIDs. + * @param scopesObject - The scopes object to filter. + * @param chainIds - The chainIDs to filter the scopes object by. + * @returns The filtered scopes object. + */ const filterEthScopesObjectByChainId = ( scopesObject: InternalScopesObject, chainIds: Hex[], @@ -89,6 +106,12 @@ const filterEthScopesObjectByChainId = ( return updatedScopesObject; }; +/** + * Sets the permitted Ethereum (EIP155 namespaced) chainIDs for the required and optional scopes. + * @param caip25CaveatValue - The CAIP-25 caveat value to set the permitted Ethereum chainIDs for. + * @param chainIds - The Ethereum chainIDs to set as permitted. + * @returns The updated CAIP-25 caveat value with the permitted Ethereum chainIDs. + */ export const setPermittedEthChainIds = ( caip25CaveatValue: Caip25CaveatValue, chainIds: Hex[], From 20311d3b6897510bbf716f0e9585f7adecb2a01d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 7 Nov 2024 17:05:09 -0600 Subject: [PATCH 097/144] update lock file --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1af7ecc813..0b85959de5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.1, @metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3090,7 +3090,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.1, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.0, @metamask/network-controller@npm:^22.0.1, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: From 6e345cf0c4a13329261720bf02ba6138538fa309 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 11:21:00 -0600 Subject: [PATCH 098/144] add more documentation + function annotations --- packages/multichain/src/scope/assert.ts | 13 ++++++++++ .../multichain/src/scope/authorization.ts | 6 +++++ packages/multichain/src/scope/constants.ts | 24 ++++++++++++++----- packages/multichain/src/scope/supported.ts | 24 +++++++++++++++++++ packages/multichain/src/scope/transform.ts | 11 +++++++++ packages/multichain/src/scope/types.ts | 9 ++++++- packages/multichain/src/scope/validation.ts | 12 ++++++++++ 7 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 22dd18b9cf..177563815a 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -8,6 +8,13 @@ import { } from './supported'; import type { InternalScopeObject, InternalScopesObject } from './types'; +/** + * Asserts that a scope string and its associated scope object are supported. + * @param scopeString - The scope string against which to assert support. + * @param scopeObject - The scope object against which to assert support. + * @param options - An object containing the following properties: + * @param options.isChainIdSupported - A predicate that determines if a chainID is supported. + */ export const assertScopeSupported = ( scopeString: string, scopeObject: InternalScopeObject, @@ -55,6 +62,12 @@ export const assertScopeSupported = ( } }; +/** + * Asserts that all scope strings and their associated scope objects are supported. + * @param scopes - The scopes object against which to assert support. + * @param options - An object containing the following properties: + * @param options.isChainIdSupported - A predicate that determines if a chainID is supported. + */ export const assertScopesSupported = ( scopes: InternalScopesObject, { diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index 9fa31619ea..6c0ac3d23a 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -25,6 +25,12 @@ export type Caip25Authorization = ( scopedProperties?: Record; }; +/** + * Validates and normalizes a set of scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. + * @param requiredScopes - The required scopes to validate and normalize. + * @param optionalScopes - The optional scopes to validate and normalize. + * @returns An object containing the normalized required scopes and normalized optional scopes. + */ export const validateAndNormalizeScopes = ( requiredScopes: ExternalScopesObject, optionalScopes: ExternalScopesObject, diff --git a/packages/multichain/src/scope/constants.ts b/packages/multichain/src/scope/constants.ts index be9223ee6e..7323720721 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -2,12 +2,16 @@ import MetaMaskOpenRPCDocument from '@metamask/api-specs'; import type { NonWalletKnownCaipNamespace } from './types'; -// ScopeStrings for offchain methods that are not specific to a chainId but are specific to a CAIP namespace +/** + * ScopeStrings for offchain methods that are not specific to a chainId but are specific to a CAIP namespace. + */ export enum KnownWalletScopeString { Eip155 = 'wallet:eip155', } -// Methods that do not belong exclusively to any CAIP namespace +/** + * Methods that do not belong exclusively to any CAIP namespace. + */ export const KnownWalletRpcMethods: string[] = [ 'wallet_registerOnboarding', 'wallet_scanQRCode', @@ -15,20 +19,25 @@ export const KnownWalletRpcMethods: string[] = [ const WalletEip155Methods = ['wallet_addEthereumChain']; -// All MetaMask methods, except for ones we have -// specified in the constants above +/** + * 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 +/** + * Methods for ecosystem that are chain specific. + */ export const KnownRpcMethods: Record = { eip155: Eip155Methods, bip122: [], }; -// Methods for CAIP namespaces that aren't chain specific +/** + * Methods for CAIP namespaces that aren't chain specific. + */ export const KnownWalletNamespaceRpcMethods: Record< NonWalletKnownCaipNamespace, string[] @@ -37,6 +46,9 @@ export const KnownWalletNamespaceRpcMethods: Record< bip122: [], }; +/** + * Notifications for known CAIP namespaces. + */ export const KnownNotifications: Record = { eip155: ['eth_subscription'], diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 70a50d2946..48ba07fa95 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -11,6 +11,12 @@ import { import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './types'; import { parseScopeString } from './types'; +/** + * Determines if a scope string is supported. + * @param scopeString - The scope string to check. + * @param isChainIdSupported - A predicate that determines if a chainID is supported. + * @returns A boolean indicating if the scope string is supported. + */ export const isSupportedScopeString = ( scopeString: string, isChainIdSupported: (chainId: Hex) => boolean, @@ -27,6 +33,12 @@ export const isSupportedScopeString = ( } }; +/** + * Determines if an account is supported by the wallet (i.e. on a keyring known to the wallet). + * @param account - The CAIP account ID to check. + * @param getInternalAccounts - A function that returns the internal accounts. + * @returns A boolean indicating if the account is supported by the wallet. + */ export const isSupportedAccount = ( account: CaipAccountId, getInternalAccounts: () => { type: string; address: string }[], @@ -55,6 +67,12 @@ export const isSupportedAccount = ( } }; +/** + * Determines if a method is supported by the wallet. + * @param scopeString - The scope string to check. + * @param method - The method to check. + * @returns A boolean indicating if the method is supported by the wallet. + */ export const isSupportedMethod = ( scopeString: ExternalScopeString, method: string, @@ -78,6 +96,12 @@ export const isSupportedMethod = ( ).includes(method); }; +/** + * Determines if a notification is supported by the wallet. + * @param scopeString - The scope string to check. + * @param notification - The notification to check. + * @returns A boolean indicating if the notification is supported by the wallet. + */ export const isSupportedNotification = ( scopeString: ExternalScopeString, notification: string, diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 0813da4798..3bde06fd08 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -94,6 +94,12 @@ export const mergeScopeObject = ( return mergedScopeObject; }; +/** + * Merges two InternalScopeObjects + * @param scopeA - The first scope object to merge. + * @param scopeB - The second scope object to merge. + * @returns The merged scope object. + */ export const mergeScopes = ( scopeA: InternalScopesObject, scopeB: InternalScopesObject, @@ -121,6 +127,11 @@ export const mergeScopes = ( return scope; }; +/** + * Normalizes and merges a set of ExternalScopesObjects into a InternalScopesObject (i.e. a set of InternalScopeObjects where references are flattened). + * @param scopes - The external scopes to normalize and merge. + * @returns The normalized and merged scopes. + */ export const normalizeAndMergeScopes = ( scopes: ExternalScopesObject, ): InternalScopesObject => { diff --git a/packages/multichain/src/scope/types.ts b/packages/multichain/src/scope/types.ts index e73e571bd6..77b1669fd5 100644 --- a/packages/multichain/src/scope/types.ts +++ b/packages/multichain/src/scope/types.ts @@ -65,6 +65,11 @@ export type ScopedProperties = Record> & { [KnownCaipNamespace.Wallet]?: Record; }; +/** + * Parses a scope string into a namespace and reference. + * @param scopeString - The scope string to parse. + * @returns An object containing the namespace and reference. + */ export const parseScopeString = ( scopeString: string, ): { @@ -83,7 +88,9 @@ export const parseScopeString = ( return {}; }; -// Known CAIP Namespaces excluding "wallet" +/** + * CAIP namespaces excluding "wallet" currently supported by/known to the wallet. + */ export type NonWalletKnownCaipNamespace = Exclude< KnownCaipNamespace, KnownCaipNamespace.Wallet diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index 0689d0f9d5..896d1eef98 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -7,6 +7,12 @@ import type { } from './types'; import { parseScopeString } from './types'; +/** + * Validates a scope object according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. + * @param scopeString - The scope string to validate. + * @param scopeObject - The scope object to validate. + * @returns A boolean indicating if the scope object is valid according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. + */ export const isValidScope = ( scopeString: ExternalScopeString, scopeObject: ExternalScopeObject, @@ -68,6 +74,12 @@ export const isValidScope = ( return true; }; +/** + * Validates a set of scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. + * @param requiredScopes - The required scopes to validate. + * @param optionalScopes - The optional scopes to validate. + * @returns An object containing the valid required scopes and optional scopes. + */ export const validateScopes = ( requiredScopes?: ExternalScopesObject, optionalScopes?: ExternalScopesObject, From ef5d50095b77a0d9974f583ab8ac43fa6ca1531f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 11:22:08 -0600 Subject: [PATCH 099/144] align package versions --- packages/multichain/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index e4413e356d..6c54d26c41 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.4.1", + "@metamask/controller-utils": "^11.4.2", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.0", + "@metamask/network-controller": "^22.0.1", "@metamask/permission-controller": "^11.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", From 71b9820834cf5ab6f79cbb1f0b27f918cf085740 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 11:23:49 -0600 Subject: [PATCH 100/144] update lockfile --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0b85959de5..e6eedb60f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.1, @metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3051,9 +3051,9 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.4.1" + "@metamask/controller-utils": "npm:^11.4.2" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/network-controller": "npm:^22.0.0" + "@metamask/network-controller": "npm:^22.0.1" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3090,7 +3090,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.0, @metamask/network-controller@npm:^22.0.1, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.1, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: From f37586d630634e6d4ab0aeff4f0c1037edf9342a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 12:17:24 -0600 Subject: [PATCH 101/144] add error constants --- packages/multichain/src/scope/assert.test.ts | 21 ++++----- packages/multichain/src/scope/assert.ts | 27 ++++------- packages/multichain/src/scope/errors.ts | 48 ++++++++++++++++++++ 3 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 packages/multichain/src/scope/errors.ts diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 2537f1e63e..f2abb9a159 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,6 +1,9 @@ -import { JsonRpcError } from '@metamask/rpc-errors'; - import { assertScopeSupported, assertScopesSupported } from './assert'; +import { + REQUESTED_CHAINS_NOT_SUPPORTED_ERROR, + REQUESTED_METHODS_NOT_SUPPORTED_ERROR, + REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR, +} from './errors'; import * as Supported from './supported'; import type { InternalScopeObject } from './types'; @@ -42,9 +45,7 @@ describe('Scope Assert', () => { assertScopeSupported('scopeString', validScopeObject, { isChainIdSupported, }); - }).toThrow( - new JsonRpcError(5100, 'Requested chains are not supported'), - ); + }).toThrow(REQUESTED_CHAINS_NOT_SUPPORTED_ERROR); }); }); @@ -88,9 +89,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow( - new JsonRpcError(5101, 'Requested methods are not supported'), - ); + }).toThrow(REQUESTED_METHODS_NOT_SUPPORTED_ERROR); }); it('checks if the notifications are supported', () => { @@ -130,9 +129,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow( - new JsonRpcError(5102, 'Requested notifications are not supported'), - ); + }).toThrow(REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR); }); it('does not throw if the scopeObject is valid', () => { @@ -182,7 +179,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow(new JsonRpcError(5100, 'Requested chains are not supported')); + }).toThrow(REQUESTED_CHAINS_NOT_SUPPORTED_ERROR); }); 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 177563815a..cd5bbda881 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,6 +1,10 @@ -import { JsonRpcError } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; +import { + REQUESTED_CHAINS_NOT_SUPPORTED_ERROR, + REQUESTED_METHODS_NOT_SUPPORTED_ERROR, + REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR, +} from './errors'; import { isSupportedMethod, isSupportedNotification, @@ -26,7 +30,7 @@ export const assertScopeSupported = ( ) => { const { methods, notifications } = scopeObject; if (!isSupportedScopeString(scopeString, isChainIdSupported)) { - throw new JsonRpcError(5100, 'Requested chains are not supported'); + throw REQUESTED_CHAINS_NOT_SUPPORTED_ERROR; } const allMethodsSupported = methods.every((method) => @@ -34,15 +38,7 @@ export const assertScopeSupported = ( ); 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'); + throw REQUESTED_METHODS_NOT_SUPPORTED_ERROR; } if ( @@ -51,14 +47,7 @@ export const assertScopeSupported = ( 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'); + throw REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR; } }; diff --git a/packages/multichain/src/scope/errors.ts b/packages/multichain/src/scope/errors.ts new file mode 100644 index 0000000000..6c84cc679e --- /dev/null +++ b/packages/multichain/src/scope/errors.ts @@ -0,0 +1,48 @@ +import { JsonRpcError } from '@metamask/rpc-errors'; + +/** + * Thrown when chains requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + */ +export const REQUESTED_CHAINS_NOT_SUPPORTED_ERROR = new JsonRpcError( + 5100, + 'Requested chains are not supported', +); + +/** + * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * TODO: consider throwing the more generic version of this error (UNKNOWN_METHODS_REQUESTED_ERROR) unless in a DevMode build of the wallet + */ +export const REQUESTED_METHODS_NOT_SUPPORTED_ERROR = new JsonRpcError( + 5101, + 'Requested methods are not supported', +); + +/** + * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * TODO: consider throwing the more generic version of this error (UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR) unless in a DevMode build of the wallet + */ +export const REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR = new JsonRpcError( + 5102, + 'Requested notifications are not supported', +); + +/** + * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes) + */ +export const UNKNOWN_METHODS_REQUESTED_ERROR = new JsonRpcError( + 5201, + 'Unknown method(s) requested', +); + +/** + * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + */ +export const UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR = new JsonRpcError( + 5202, + 'Unknown notification(s) requested', +); From 67bd1d2db957afed69229c6ed26dfdeaa2d258e6 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 8 Nov 2024 12:25:31 -0600 Subject: [PATCH 102/144] Update packages/multichain/src/scope/constants.ts Co-authored-by: Mark Stacey --- 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 7323720721..8610638b5d 100644 --- a/packages/multichain/src/scope/constants.ts +++ b/packages/multichain/src/scope/constants.ts @@ -28,7 +28,7 @@ const Eip155Methods = MetaMaskOpenRPCDocument.methods .filter((method: string) => !KnownWalletRpcMethods.includes(method)); /** - * Methods for ecosystem that are chain specific. + * Methods by ecosystem that are chain specific. */ export const KnownRpcMethods: Record = { eip155: Eip155Methods, From cc686bc69bd0abd38fe916ba3cd66b75d486cacb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 12:48:04 -0600 Subject: [PATCH 103/144] add snapshot test for KnownRpcMethods --- .../multichain/src/scope/constants.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 packages/multichain/src/scope/constants.test.ts diff --git a/packages/multichain/src/scope/constants.test.ts b/packages/multichain/src/scope/constants.test.ts new file mode 100644 index 0000000000..8369ec721a --- /dev/null +++ b/packages/multichain/src/scope/constants.test.ts @@ -0,0 +1,59 @@ +import { KnownRpcMethods } from './constants'; + +describe('KnownRpcMethods', () => { + it('should match the snapshot', () => { + expect(KnownRpcMethods).toMatchInlineSnapshot(` + 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", + "web3_clientVersion", + "eth_subscribe", + "eth_unsubscribe", + "eth_blockNumber", + "eth_call", + "eth_chainId", + "eth_coinbase", + "eth_estimateGas", + "eth_feeHistory", + "eth_gasPrice", + "eth_getBalance", + "eth_getBlockByHash", + "eth_getBlockByNumber", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getProof", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getTransactionByHash", + "eth_getTransactionCount", + "eth_getTransactionReceipt", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_sendRawTransaction", + "eth_syncing", + "eth_uninstallFilter", + ], + } + `); + }); +}); From 664069be66cb1e751a9c7f6383ccbee0c2743657 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 13:16:39 -0600 Subject: [PATCH 104/144] improve ScopeObject validation --- .../multichain/src/scope/validation.test.ts | 15 +++++++- packages/multichain/src/scope/validation.ts | 38 +++++++++++++------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index 5a96215901..54ea3ca2cc 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -15,8 +15,8 @@ describe('Scope Validation', () => { ).toBe(false); }); - it('returns true when the scopeString is a valid CAIP namespace and the scopeObject is valid', () => { - expect(isValidScope('eip155', validScopeObject)).toBe(true); + it('returns true when the scopeString is "wallet" and the scopeObject does not contain references', () => { + expect(isValidScope('wallet', validScopeObject)).toBe(true); }); it('returns true when the scopeString is a valid CAIP chainId and the scopeObject is valid', () => { @@ -41,6 +41,17 @@ describe('Scope Validation', () => { ).toBe(false); }); + it('returns false when the scopeString is a valid CAIP namespace (other than "wallet") but references is an empty array', () => { + expect( + isValidScope('eip155', { ...validScopeObject, references: [] }), + ).toBe(false); + }); + + it('returns false when the scopeString is a valid CAIP namespace (other than "wallet") but references is undefined', () => { + expect(isValidScope('eip155', validScopeObject)).toBe(false); + }); + + it('returns false when methods contains empty string', () => { expect( isValidScope(validScopeString, { diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index 896d1eef98..a8aeb8c851 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -19,7 +19,8 @@ export const isValidScope = ( ): boolean => { const { namespace, reference } = parseScopeString(scopeString); - if (!namespace && !reference) { + // Namespace is required + if (!namespace) { return false; } @@ -30,22 +31,32 @@ export const isValidScope = ( accounts, rpcDocuments, rpcEndpoints, - ...restScopeObject + ...extraProperties } = scopeObject; + // Methods and notifications are required if (!methods || !notifications) { return false; } - // These assume that the namespace has a notion of chainIds - if (reference && references && references.length > 0) { + // For namespaces other than 'wallet', either reference or non-empty references array must be present + if ( + namespace !== 'wallet' && + !reference && + (!references || references.length === 0) + ) { return false; } - if (namespace && references) { - const areReferencesValid = references.every((nestedReference) => { - return isCaipReference(nestedReference); - }); + // If references are present, reference must be absent and all references must be valid + if (references) { + if (reference && references.length > 0) { + return false; + } + + const areReferencesValid = references.every((nestedReference) => + isCaipReference(nestedReference), + ); if (!areReferencesValid) { return false; @@ -53,21 +64,24 @@ export const isValidScope = ( } const areMethodsValid = methods.every( - (method) => typeof method === 'string' && method !== '', + (method) => typeof method === 'string' && method.trim() !== '', ); + if (!areMethodsValid) { return false; } const areNotificationsValid = notifications.every( - (notification) => typeof notification === 'string' && notification !== '', + (notification) => + typeof notification === 'string' && notification.trim() !== '', ); + if (!areNotificationsValid) { return false; } - // unexpected properties found on scopeObject - if (Object.keys(restScopeObject).length !== 0) { + // Ensure no unexpected properties are present in the scope object + if (Object.keys(extraProperties).length > 0) { return false; } From 1abd454dcb02b26a9118c9560f09525ddf83e98c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 13:25:30 -0600 Subject: [PATCH 105/144] error builder methods --- packages/multichain/src/scope/assert.test.ts | 14 ++-- packages/multichain/src/scope/assert.ts | 12 +-- packages/multichain/src/scope/errors.ts | 80 ++++++++++---------- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index f2abb9a159..02b8ed8a8c 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,9 +1,5 @@ import { assertScopeSupported, assertScopesSupported } from './assert'; -import { - REQUESTED_CHAINS_NOT_SUPPORTED_ERROR, - REQUESTED_METHODS_NOT_SUPPORTED_ERROR, - REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR, -} from './errors'; +import { Caip25Errors } from './errors'; import * as Supported from './supported'; import type { InternalScopeObject } from './types'; @@ -45,7 +41,7 @@ describe('Scope Assert', () => { assertScopeSupported('scopeString', validScopeObject, { isChainIdSupported, }); - }).toThrow(REQUESTED_CHAINS_NOT_SUPPORTED_ERROR); + }).toThrow(Caip25Errors.requestedChainsNotSupportedError()); }); }); @@ -89,7 +85,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow(REQUESTED_METHODS_NOT_SUPPORTED_ERROR); + }).toThrow(Caip25Errors.requestedMethodsNotSupportedError()); }); it('checks if the notifications are supported', () => { @@ -129,7 +125,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow(REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR); + }).toThrow(Caip25Errors.requestedNotificationsNotSupportedError()); }); it('does not throw if the scopeObject is valid', () => { @@ -179,7 +175,7 @@ describe('Scope Assert', () => { isChainIdSupported, }, ); - }).toThrow(REQUESTED_CHAINS_NOT_SUPPORTED_ERROR); + }).toThrow(Caip25Errors.requestedChainsNotSupportedError()); }); 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 cd5bbda881..d2bee8ab30 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,10 +1,6 @@ import type { Hex } from '@metamask/utils'; -import { - REQUESTED_CHAINS_NOT_SUPPORTED_ERROR, - REQUESTED_METHODS_NOT_SUPPORTED_ERROR, - REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR, -} from './errors'; +import { Caip25Errors } from './errors'; import { isSupportedMethod, isSupportedNotification, @@ -30,7 +26,7 @@ export const assertScopeSupported = ( ) => { const { methods, notifications } = scopeObject; if (!isSupportedScopeString(scopeString, isChainIdSupported)) { - throw REQUESTED_CHAINS_NOT_SUPPORTED_ERROR; + throw Caip25Errors.requestedChainsNotSupportedError(); } const allMethodsSupported = methods.every((method) => @@ -38,7 +34,7 @@ export const assertScopeSupported = ( ); if (!allMethodsSupported) { - throw REQUESTED_METHODS_NOT_SUPPORTED_ERROR; + throw Caip25Errors.requestedMethodsNotSupportedError(); } if ( @@ -47,7 +43,7 @@ export const assertScopeSupported = ( isSupportedNotification(scopeString, notification), ) ) { - throw REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR; + throw Caip25Errors.requestedNotificationsNotSupportedError(); } }; diff --git a/packages/multichain/src/scope/errors.ts b/packages/multichain/src/scope/errors.ts index 6c84cc679e..97ff9c9872 100644 --- a/packages/multichain/src/scope/errors.ts +++ b/packages/multichain/src/scope/errors.ts @@ -1,48 +1,48 @@ import { JsonRpcError } from '@metamask/rpc-errors'; /** - * Thrown when chains requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * CAIP25 Errors. */ -export const REQUESTED_CHAINS_NOT_SUPPORTED_ERROR = new JsonRpcError( - 5100, - 'Requested chains are not supported', -); +export const Caip25Errors = { + /** + * Thrown when chains requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * @returns A new JsonRpcError instance. + */ + requestedChainsNotSupportedError: () => + new JsonRpcError(5100, 'Requested chains are not supported'), -/** - * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * TODO: consider throwing the more generic version of this error (UNKNOWN_METHODS_REQUESTED_ERROR) unless in a DevMode build of the wallet - */ -export const REQUESTED_METHODS_NOT_SUPPORTED_ERROR = new JsonRpcError( - 5101, - 'Requested methods are not supported', -); + /** + * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * TODO: consider throwing the more generic version of this error (UNKNOWN_METHODS_REQUESTED_ERROR) unless in a DevMode build of the wallet + * @returns A new JsonRpcError instance. + */ + requestedMethodsNotSupportedError: () => + new JsonRpcError(5101, 'Requested methods are not supported'), -/** - * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - * TODO: consider throwing the more generic version of this error (UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR) unless in a DevMode build of the wallet - */ -export const REQUESTED_NOTIFICATIONS_NOT_SUPPORTED_ERROR = new JsonRpcError( - 5102, - 'Requested notifications are not supported', -); + /** + * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * TODO: consider throwing the more generic version of this error (UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR) unless in a DevMode build of the wallet + * @returns A new JsonRpcError instance. + */ + requestedNotificationsNotSupportedError: () => + new JsonRpcError(5102, 'Requested notifications are not supported'), -/** - * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes) - */ -export const UNKNOWN_METHODS_REQUESTED_ERROR = new JsonRpcError( - 5201, - 'Unknown method(s) requested', -); + /** + * Thrown when methods requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * @returns A new JsonRpcError instance. + */ + unknownMethodsRequestedError: () => + new JsonRpcError(5201, 'Unknown method(s) requested'), -/** - * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. - * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). - */ -export const UNKNOWN_NOTIFICATIONS_REQUESTED_ERROR = new JsonRpcError( - 5202, - 'Unknown notification(s) requested', -); + /** + * Thrown when notifications requested in a CAIP-25 `wallet_createSession` call are not supported by the wallet. + * Defined in [CAIP-25 error codes section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md#trusted-failure-codes). + * @returns A new JsonRpcError instance. + */ + unknownNotificationsRequestedError: () => + new JsonRpcError(5202, 'Unknown notification(s) requested'), +}; From 0899f2676128e8e3f224cbf7adafc4c459f197af Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 13:27:11 -0600 Subject: [PATCH 106/144] lint --- packages/multichain/src/scope/validation.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index 54ea3ca2cc..f5ae880a3c 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -51,7 +51,6 @@ describe('Scope Validation', () => { expect(isValidScope('eip155', validScopeObject)).toBe(false); }); - it('returns false when methods contains empty string', () => { expect( isValidScope(validScopeString, { From b99b78f7bdeee533d6a29123ce861ad66c08512c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 14:27:19 -0600 Subject: [PATCH 107/144] improve type checks in supported helper methods --- packages/multichain/src/scope/assert.ts | 2 +- packages/multichain/src/scope/supported.ts | 40 +++++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index d2bee8ab30..87d59cc1d6 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -66,4 +66,4 @@ export const assertScopesSupported = ( isChainIdSupported, }); } -}; +}; \ No newline at end of file diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 48ba07fa95..db2faea3d6 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -79,21 +79,25 @@ export const isSupportedMethod = ( ): boolean => { const { namespace, reference } = parseScopeString(scopeString); + if (!namespace || !isKnownCaipNamespace(namespace)) { + return false; + } + if (namespace === KnownCaipNamespace.Wallet) { if (reference) { - return ( - KnownWalletNamespaceRpcMethods[ - reference as NonWalletKnownCaipNamespace - ] || [] - ).includes(method); + if ( + !isKnownCaipNamespace(reference) || + reference === KnownCaipNamespace.Wallet + ) { + return false; + } + return KnownWalletNamespaceRpcMethods[reference].includes(method); } return KnownWalletRpcMethods.includes(method); } - return ( - KnownRpcMethods[namespace as NonWalletKnownCaipNamespace] || [] - ).includes(method); + return KnownRpcMethods[namespace].includes(method); }; /** @@ -108,7 +112,27 @@ export const isSupportedNotification = ( ): boolean => { const { namespace } = parseScopeString(scopeString); + if (!namespace || !isKnownCaipNamespace(namespace)) { + return false; + } + return ( KnownNotifications[namespace as NonWalletKnownCaipNamespace] || [] ).includes(notification); }; + +/** + * Checks whether the given namespace is a known CAIP namespace. + * + * @param namespace - The namespace to check + * @returns Whether the given namespace is a known CAIP namespace. + */ +function isKnownCaipNamespace( + namespace: string, +): namespace is KnownCaipNamespace { + const nonWalletKnownCaipNamespaces = Object.keys(KnownCaipNamespace) + .filter((key) => key !== KnownCaipNamespace.Wallet) + .map((key) => key.toLowerCase()); + + return nonWalletKnownCaipNamespaces.includes(namespace); +} From 97c59e8a3adfa8a417ae9ff3f5c127df5071648d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 14:35:17 -0600 Subject: [PATCH 108/144] lint --- packages/multichain/src/scope/assert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index 87d59cc1d6..d2bee8ab30 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -66,4 +66,4 @@ export const assertScopesSupported = ( isChainIdSupported, }); } -}; \ No newline at end of file +}; From b700e4f2398d0067519e12a3d8f2bc3f7e4e98c0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 14:59:48 -0600 Subject: [PATCH 109/144] validateScopes -> getValidScopes --- packages/multichain/src/index.test.ts | 2 +- packages/multichain/src/index.ts | 2 +- packages/multichain/src/scope/authorization.test.ts | 8 ++++---- packages/multichain/src/scope/authorization.ts | 4 ++-- packages/multichain/src/scope/validation.test.ts | 10 +++++----- packages/multichain/src/scope/validation.ts | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index a35eb21b0e..9cac4a71aa 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -25,7 +25,7 @@ describe('@metamask/multichain', () => { "mergeScopes", "normalizeAndMergeScopes", "isValidScope", - "validateScopes", + "getValidScopes", "Caip25CaveatType", "createCaip25Caveat", "Caip25EndowmentPermissionName", diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index cff11a05f1..2dc8c6f910 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -40,7 +40,7 @@ export { mergeScopes, normalizeAndMergeScopes, } from './scope/transform'; -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/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index c4a9c3463d..dcc00e0cbc 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -4,7 +4,7 @@ import type { ExternalScopeObject } from './types'; import * as Validation from './validation'; jest.mock('./validation', () => ({ - validateScopes: jest.fn(), + getValidScopes: jest.fn(), })); const MockValidation = jest.mocked(Validation); @@ -33,7 +33,7 @@ describe('Scope Authorization', () => { } catch (err) { // noop } - expect(MockValidation.validateScopes).toHaveBeenCalledWith( + expect(MockValidation.getValidScopes).toHaveBeenCalledWith( { 'eip155:1': validScopeObject, }, @@ -44,7 +44,7 @@ describe('Scope Authorization', () => { }); it('normalized and merges the validated scopes', () => { - MockValidation.validateScopes.mockReturnValue({ + MockValidation.getValidScopes.mockReturnValue({ validRequiredScopes: { 'eip155:1': validScopeObject, }, @@ -63,7 +63,7 @@ describe('Scope Authorization', () => { }); it('returns the normalized and merged scopes', () => { - MockValidation.validateScopes.mockReturnValue({ + MockValidation.getValidScopes.mockReturnValue({ validRequiredScopes: { 'eip155:1': validScopeObject, }, diff --git a/packages/multichain/src/scope/authorization.ts b/packages/multichain/src/scope/authorization.ts index 6c0ac3d23a..6974d65295 100644 --- a/packages/multichain/src/scope/authorization.ts +++ b/packages/multichain/src/scope/authorization.ts @@ -6,7 +6,7 @@ import type { ExternalScopeString, InternalScopesObject, } from './types'; -import { validateScopes } from './validation'; +import { getValidScopes } from './validation'; /** * Represents the parameters of a [CAIP-25](https://chainagnostic.org/CAIPs/caip-25) request. @@ -38,7 +38,7 @@ export const validateAndNormalizeScopes = ( normalizedRequiredScopes: InternalScopesObject; normalizedOptionalScopes: InternalScopesObject; } => { - const { validRequiredScopes, validOptionalScopes } = validateScopes( + const { validRequiredScopes, validOptionalScopes } = getValidScopes( requiredScopes, optionalScopes, ); diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index f5ae880a3c..a76ab15b8e 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -1,5 +1,5 @@ import type { ExternalScopeObject } from './types'; -import { isValidScope, validateScopes } from './validation'; +import { isValidScope, getValidScopes } from './validation'; const validScopeString = 'eip155:1'; const validScopeObject: ExternalScopeObject = { @@ -122,7 +122,7 @@ describe('Scope Validation', () => { }); }); - describe('validateScopes', () => { + describe('getValidScopes', () => { const validScopeObjectWithAccounts = { ...validScopeObject, accounts: [], @@ -130,7 +130,7 @@ describe('Scope Validation', () => { it('does not throw an error if required scopes are defined but none are valid', () => { expect( - validateScopes( + getValidScopes( // @ts-expect-error Intentionally invalid input { 'eip155:1': {} }, undefined, @@ -140,7 +140,7 @@ describe('Scope Validation', () => { it('does not throw an error if optional scopes are defined but none are valid', () => { expect( - validateScopes(undefined, { + getValidScopes(undefined, { // @ts-expect-error Intentionally invalid input 'eip155:1': {}, }), @@ -149,7 +149,7 @@ describe('Scope Validation', () => { it('returns the valid required and optional scopes', () => { expect( - validateScopes( + getValidScopes( { 'eip155:1': validScopeObjectWithAccounts, // @ts-expect-error Intentionally invalid input diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index a8aeb8c851..cb76497dbe 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -89,12 +89,12 @@ export const isValidScope = ( }; /** - * Validates a set of scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. + * Filters out invalid scopes and returns valid sets of required and optional scopes according to the [CAIP-217](https://chainagnostic.org/CAIPs/caip-217) spec. * @param requiredScopes - The required scopes to validate. * @param optionalScopes - The optional scopes to validate. - * @returns An object containing the valid required scopes and optional scopes. + * @returns An object containing valid required scopes and optional scopes. */ -export const validateScopes = ( +export const getValidScopes = ( requiredScopes?: ExternalScopesObject, optionalScopes?: ExternalScopesObject, ) => { From e5762958f52c21a6d44fb46ae262601e0033951f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 15:17:22 -0600 Subject: [PATCH 110/144] narrow validation of references property --- .../multichain/src/scope/validation.test.ts | 20 ++++++++++++------- packages/multichain/src/scope/validation.ts | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/multichain/src/scope/validation.test.ts b/packages/multichain/src/scope/validation.test.ts index a76ab15b8e..6871b01069 100644 --- a/packages/multichain/src/scope/validation.test.ts +++ b/packages/multichain/src/scope/validation.test.ts @@ -23,20 +23,20 @@ describe('Scope Validation', () => { expect(isValidScope('eip155:1', validScopeObject)).toBe(true); }); - it('returns false when the scopeString is a CAIP chainId but references is nonempty', () => { + it('returns false when the scopeString is a valid CAIP namespace but references are invalid CAIP references', () => { expect( - isValidScope('eip155:1', { + isValidScope('eip155', { ...validScopeObject, - references: ['5'], + references: ['@'], }), ).toBe(false); }); - it('returns false when the scopeString is a valid CAIP namespace but references are invalid CAIP references', () => { + it('returns false when the scopeString is a CAIP chainId but references is defined', () => { expect( - isValidScope('eip155', { + isValidScope('eip155:1', { ...validScopeObject, - references: ['@'], + references: [], }), ).toBe(false); }); @@ -111,7 +111,6 @@ describe('Scope Validation', () => { it('returns true when only expected properties are defined', () => { expect( isValidScope(validScopeString, { - references: [], methods: [], notifications: [], accounts: [], @@ -119,6 +118,13 @@ describe('Scope Validation', () => { rpcEndpoints: [], }), ).toBe(true); + + expect( + isValidScope('eip155', { + ...validScopeObject, + references: ['1'], + }), + ).toBe(true); }); }); diff --git a/packages/multichain/src/scope/validation.ts b/packages/multichain/src/scope/validation.ts index cb76497dbe..26e96fdc65 100644 --- a/packages/multichain/src/scope/validation.ts +++ b/packages/multichain/src/scope/validation.ts @@ -50,7 +50,7 @@ export const isValidScope = ( // If references are present, reference must be absent and all references must be valid if (references) { - if (reference && references.length > 0) { + if (reference) { return false; } From ba9c728c83cfaeb824f847494b97d17c193dccec Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 15:31:01 -0600 Subject: [PATCH 111/144] fixup type check functions --- .../multichain/src/scope/supported.test.ts | 8 ++++++ packages/multichain/src/scope/supported.ts | 26 ++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/multichain/src/scope/supported.test.ts b/packages/multichain/src/scope/supported.test.ts index 9c45a27d95..fd488cfd71 100644 --- a/packages/multichain/src/scope/supported.test.ts +++ b/packages/multichain/src/scope/supported.test.ts @@ -26,6 +26,14 @@ describe('Scope Support', () => { expect(isSupportedNotification('eip155', 'anything else')).toBe(false); expect(isSupportedNotification('', '')).toBe(false); }); + + it('returns false for unknown namespaces', () => { + expect(isSupportedNotification('unknown', 'anything else')).toBe(false); + }); + + it('returns false for wallet namespace', () => { + expect(isSupportedNotification('wallet', 'anything else')).toBe(false); + }); }); describe('isSupportedMethod', () => { diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index db2faea3d6..032c3d5dfd 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -112,13 +112,11 @@ export const isSupportedNotification = ( ): boolean => { const { namespace } = parseScopeString(scopeString); - if (!namespace || !isKnownCaipNamespace(namespace)) { + if (!namespace || !isNonWalletKnownCaipNamespace(namespace)) { return false; } - return ( - KnownNotifications[namespace as NonWalletKnownCaipNamespace] || [] - ).includes(notification); + return (KnownNotifications[namespace] || []).includes(notification); }; /** @@ -130,9 +128,25 @@ export const isSupportedNotification = ( function isKnownCaipNamespace( namespace: string, ): namespace is KnownCaipNamespace { - const nonWalletKnownCaipNamespaces = Object.keys(KnownCaipNamespace) + const knownNamespaces = Object.keys(KnownCaipNamespace).map((key) => + key.toLowerCase(), + ); + + return knownNamespaces.includes(namespace); +} + +/** + * Checks whether the given namespace is a known non-wallet CAIP namespace. + * + * @param namespace - The namespace to check + * @returns Whether the given namespace is a known non-wallet CAIP namespace. + */ +function isNonWalletKnownCaipNamespace( + namespace: string, +): namespace is NonWalletKnownCaipNamespace { + const knownNamespaces = Object.keys(KnownCaipNamespace) .filter((key) => key !== KnownCaipNamespace.Wallet) .map((key) => key.toLowerCase()); - return nonWalletKnownCaipNamespaces.includes(namespace); + return knownNamespaces.includes(namespace); } From ee616de74412a80467341ebfb2b0365b4f05bc8a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 15:34:06 -0600 Subject: [PATCH 112/144] add tests for errors file --- packages/multichain/src/scope/errors.test.ts | 33 ++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/multichain/src/scope/errors.test.ts diff --git a/packages/multichain/src/scope/errors.test.ts b/packages/multichain/src/scope/errors.test.ts new file mode 100644 index 0000000000..b524aacbbc --- /dev/null +++ b/packages/multichain/src/scope/errors.test.ts @@ -0,0 +1,33 @@ +import { Caip25Errors } from './errors'; + +describe('Caip25Errors', () => { + it('requestedChainsNotSupportedError', () => { + expect(Caip25Errors.requestedChainsNotSupportedError().message).toBe( + 'Requested chains are not supported', + ); + }); + + it('requestedMethodsNotSupportedError', () => { + expect(Caip25Errors.requestedMethodsNotSupportedError().message).toBe( + 'Requested methods are not supported', + ); + }); + + it('requestedNotificationsNotSupportedError', () => { + expect(Caip25Errors.requestedNotificationsNotSupportedError().message).toBe( + 'Requested notifications are not supported', + ); + }); + + it('unknownMethodsRequestedError', () => { + expect(Caip25Errors.unknownMethodsRequestedError().message).toBe( + 'Unknown method(s) requested', + ); + }); + + it('unknownNotificationsRequestedError', () => { + expect(Caip25Errors.unknownNotificationsRequestedError().message).toBe( + 'Unknown notification(s) requested', + ); + }); +}); From 5633bddd0a517ab5549d144281a0e17d18ab4f3f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 15:55:26 -0600 Subject: [PATCH 113/144] add TSDoc --- packages/multichain/src/scope/transform.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 3bde06fd08..4c530f3cc7 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -58,6 +58,12 @@ export const normalizeScope = ( return { [scopeString]: normalizedScopeObject }; }; +/** + * Merges two InternalScopeObjects + * @param scopeObjectA - The first scope object to merge. + * @param scopeObjectB - The second scope object to merge. + * @returns The merged scope object. + */ export const mergeScopeObject = ( scopeObjectA: InternalScopeObject, scopeObjectB: InternalScopeObject, From 43cddcc966250d2b89ef3509ca3ef843d293440e Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 8 Nov 2024 16:07:00 -0600 Subject: [PATCH 114/144] Update packages/multichain/src/scope/transform.ts Co-authored-by: Mark Stacey --- packages/multichain/src/scope/transform.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 4c530f3cc7..3563ac55f2 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -113,7 +113,8 @@ export const mergeScopes = ( const scope: InternalScopesObject = {}; Object.entries(scopeA).forEach(([_scopeString, scopeObjectA]) => { - const scopeString = _scopeString as InternalScopeString; + // Cast needed because index type is returned as `string` by `Object.entries` + const scopeString = _scopeString as keyof typeof scopeA; const scopeObjectB = scopeB[scopeString]; scope[scopeString] = scopeObjectB @@ -122,7 +123,8 @@ export const mergeScopes = ( }); Object.entries(scopeB).forEach(([_scopeString, scopeObjectB]) => { - const scopeString = _scopeString as InternalScopeString; + // Cast needed because index type is returned as `string` by `Object.entries` + const scopeString = _scopeString as keyof typeof scopeB; const scopeObjectA = scopeA[scopeString]; if (!scopeObjectA) { From 693ee5c47c4de6ad936a12913c45a70ac140fa90 Mon Sep 17 00:00:00 2001 From: Mark Stacey Date: Fri, 8 Nov 2024 18:38:26 -0330 Subject: [PATCH 115/144] chore: Suggestion on simplifying type guard (#4912) --- packages/multichain/src/scope/supported.ts | 26 ++++++---------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/multichain/src/scope/supported.ts b/packages/multichain/src/scope/supported.ts index 032c3d5dfd..7bb21e3652 100644 --- a/packages/multichain/src/scope/supported.ts +++ b/packages/multichain/src/scope/supported.ts @@ -8,7 +8,7 @@ import { KnownWalletNamespaceRpcMethods, KnownWalletRpcMethods, } from './constants'; -import type { NonWalletKnownCaipNamespace, ExternalScopeString } from './types'; +import type { ExternalScopeString } from './types'; import { parseScopeString } from './types'; /** @@ -112,11 +112,15 @@ export const isSupportedNotification = ( ): boolean => { const { namespace } = parseScopeString(scopeString); - if (!namespace || !isNonWalletKnownCaipNamespace(namespace)) { + if ( + !namespace || + !isKnownCaipNamespace(namespace) || + namespace === KnownCaipNamespace.Wallet + ) { return false; } - return (KnownNotifications[namespace] || []).includes(notification); + return KnownNotifications[namespace].includes(notification); }; /** @@ -134,19 +138,3 @@ function isKnownCaipNamespace( return knownNamespaces.includes(namespace); } - -/** - * Checks whether the given namespace is a known non-wallet CAIP namespace. - * - * @param namespace - The namespace to check - * @returns Whether the given namespace is a known non-wallet CAIP namespace. - */ -function isNonWalletKnownCaipNamespace( - namespace: string, -): namespace is NonWalletKnownCaipNamespace { - const knownNamespaces = Object.keys(KnownCaipNamespace) - .filter((key) => key !== KnownCaipNamespace.Wallet) - .map((key) => key.toLowerCase()); - - return knownNamespaces.includes(namespace); -} From 4b1b1b966be0bec9d4f7faecd6c181ed35356d5a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 8 Nov 2024 16:16:59 -0600 Subject: [PATCH 116/144] lint --- packages/multichain/src/scope/transform.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/multichain/src/scope/transform.ts b/packages/multichain/src/scope/transform.ts index 3563ac55f2..561b64b002 100644 --- a/packages/multichain/src/scope/transform.ts +++ b/packages/multichain/src/scope/transform.ts @@ -4,7 +4,6 @@ import { cloneDeep } from 'lodash'; import type { ExternalScopeObject, ExternalScopesObject, - InternalScopeString, InternalScopeObject, InternalScopesObject, } from './types'; @@ -123,7 +122,7 @@ export const mergeScopes = ( }); Object.entries(scopeB).forEach(([_scopeString, scopeObjectB]) => { - // Cast needed because index type is returned as `string` by `Object.entries` + // Cast needed because index type is returned as `string` by `Object.entries` const scopeString = _scopeString as keyof typeof scopeB; const scopeObjectA = scopeA[scopeString]; From 7ceab946cc4829dca983d577a00ca37623b55001 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 11:27:20 -0600 Subject: [PATCH 117/144] add more tests for transform helpers --- .../multichain/src/scope/transform.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/multichain/src/scope/transform.test.ts b/packages/multichain/src/scope/transform.test.ts index 613f3ba20a..5eede12203 100644 --- a/packages/multichain/src/scope/transform.test.ts +++ b/packages/multichain/src/scope/transform.test.ts @@ -321,6 +321,18 @@ describe('Scope Transform', () => { }, }); }); + it('returns an empty object when no scopes are provided', () => { + expect(mergeScopes({}, {})).toStrictEqual({}); + }); + + it('returns an unchanged scope when two identical scopeObjects are provided', () => { + expect( + mergeScopes( + { 'eip155:1': validScopeObject }, + { 'eip155:1': validScopeObject }, + ), + ).toStrictEqual({ 'eip155:1': validScopeObject }); + }); }); describe('normalizeAndMergeScopes', () => { @@ -348,5 +360,21 @@ describe('Scope Transform', () => { }, }); }); + it('returns an empty object when no scopes are provided', () => { + expect(normalizeAndMergeScopes({})).toStrictEqual({}); + }); + it('return an unchanged scope when scopeObjects are already normalized (i.e. none contain references to flatten)', () => { + expect( + normalizeAndMergeScopes({ + 'eip155:1': validScopeObject, + 'eip155:2': validScopeObject, + 'eip155:3': validScopeObject, + }), + ).toStrictEqual({ + 'eip155:1': validScopeObject, + 'eip155:2': validScopeObject, + 'eip155:3': validScopeObject, + }); + }); }); }); From 86450dcb1d6127ffe05cf3678d6c73c10afd4b30 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 11:34:33 -0600 Subject: [PATCH 118/144] add caip25 error code snapshot tests --- packages/multichain/src/scope/errors.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/multichain/src/scope/errors.test.ts b/packages/multichain/src/scope/errors.test.ts index b524aacbbc..f176cd36d8 100644 --- a/packages/multichain/src/scope/errors.test.ts +++ b/packages/multichain/src/scope/errors.test.ts @@ -5,29 +5,36 @@ describe('Caip25Errors', () => { expect(Caip25Errors.requestedChainsNotSupportedError().message).toBe( 'Requested chains are not supported', ); + expect(Caip25Errors.requestedChainsNotSupportedError().code).toBe(5100); }); it('requestedMethodsNotSupportedError', () => { expect(Caip25Errors.requestedMethodsNotSupportedError().message).toBe( 'Requested methods are not supported', ); + expect(Caip25Errors.requestedMethodsNotSupportedError().code).toBe(5101); }); it('requestedNotificationsNotSupportedError', () => { expect(Caip25Errors.requestedNotificationsNotSupportedError().message).toBe( 'Requested notifications are not supported', ); + expect(Caip25Errors.requestedNotificationsNotSupportedError().code).toBe( + 5102, + ); }); it('unknownMethodsRequestedError', () => { expect(Caip25Errors.unknownMethodsRequestedError().message).toBe( 'Unknown method(s) requested', ); + expect(Caip25Errors.unknownMethodsRequestedError().code).toBe(5201); }); it('unknownNotificationsRequestedError', () => { expect(Caip25Errors.unknownNotificationsRequestedError().message).toBe( 'Unknown notification(s) requested', ); + expect(Caip25Errors.unknownNotificationsRequestedError().code).toBe(5202); }); }); From 858f76460d5b58de74b56cabb859eba204466054 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 12:06:46 -0600 Subject: [PATCH 119/144] add mocking to fix test --- .../src/scope/authorization.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index dcc00e0cbc..ed19e0e979 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -21,18 +21,18 @@ const validScopeObject: ExternalScopeObject = { describe('Scope Authorization', () => { describe('validateAndNormalizeScopes', () => { it('validates the scopes', () => { - try { - validateAndNormalizeScopes( - { - 'eip155:1': validScopeObject, - }, - { - 'eip155:5': validScopeObject, - }, - ); - } catch (err) { - // noop - } + MockValidation.getValidScopes.mockReturnValue({ + validRequiredScopes: {}, + validOptionalScopes: {}, + }); + validateAndNormalizeScopes( + { + 'eip155:1': validScopeObject, + }, + { + 'eip155:5': validScopeObject, + }, + ); expect(MockValidation.getValidScopes).toHaveBeenCalledWith( { 'eip155:1': validScopeObject, From b525790869c8f81d9ab4780f5ad453745ffc9308 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 12:07:30 -0600 Subject: [PATCH 120/144] fix typo --- packages/multichain/src/scope/authorization.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/scope/authorization.test.ts b/packages/multichain/src/scope/authorization.test.ts index ed19e0e979..4759b40edd 100644 --- a/packages/multichain/src/scope/authorization.test.ts +++ b/packages/multichain/src/scope/authorization.test.ts @@ -43,7 +43,7 @@ describe('Scope Authorization', () => { ); }); - it('normalized and merges the validated scopes', () => { + it('normalizes and merges the validated scopes', () => { MockValidation.getValidScopes.mockReturnValue({ validRequiredScopes: { 'eip155:1': validScopeObject, From 6c313bc109ff958aebc10c2de39494ddbdc9114b Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 11 Nov 2024 12:25:10 -0600 Subject: [PATCH 121/144] Update packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts Co-authored-by: Mark Stacey --- .../src/adapters/caip-permission-adapter-eth-accounts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 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 be84e25d10..00fc3c854e 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -78,11 +78,11 @@ const setEthAccountsForScopesObject = ( return; } - const caipAccounts = accounts.map( + const caipAccounts = accounts.map( (account) => (isWalletNamespace ? `${KnownWalletScopeString.Eip155}:${account}` - : `${scopeString}:${account}`) as CaipAccountId, + : `${scopeString}:${account}`), ); updatedScopesObject[scopeString as InternalScopeString] = { From 1a59b95673386b9673a1b0ef22082c61c79d91f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 13:07:35 -0600 Subject: [PATCH 122/144] add getEthAccounts tests --- ...ip-permission-adapter-eth-accounts.test.ts | 33 +++++++++++++++++++ .../caip-permission-adapter-eth-accounts.ts | 21 +++++++----- 2 files changed, 46 insertions(+), 8 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 6899e2ddda..d58a8ad4b2 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 @@ -6,6 +6,39 @@ import { describe('CAIP-25 eth_accounts adapters', () => { describe('getEthAccounts', () => { + it('returns an empty array if the required scopes are empty', () => { + const ethAccounts = getEthAccounts({ + requiredScopes: {}, + optionalScopes: {}, + }); + expect(ethAccounts).toStrictEqual([]); + }); + it('returns an empty array if the scope objects have no accounts', () => { + const ethAccounts = getEthAccounts({ + requiredScopes: { + 'eip155:1': { methods: [], notifications: [], accounts: [] }, + 'eip155:2': { methods: [], notifications: [], accounts: [] }, + }, + optionalScopes: {}, + }); + expect(ethAccounts).toStrictEqual([]); + }); + it('returns an empty array if the scope objects have no eth accounts', () => { + const ethAccounts = getEthAccounts({ + requiredScopes: { + 'bip122:000000000019d6689c085ae165831e93': { + methods: [], + notifications: [], + accounts: [ + 'bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6', + ], + }, + }, + optionalScopes: {}, + }); + expect(ethAccounts).toStrictEqual([]); + }); + it('returns the unique set of EIP155 accounts from the CAIP-25 caveat value', () => { const ethAccounts = getEthAccounts({ requiredScopes: { 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 00fc3c854e..e1195cd045 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -69,7 +69,7 @@ const setEthAccountsForScopesObject = ( Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet; - + const { namespace, reference } = parseScopeString(scopeString); if ( !isEip155ScopeString(scopeString as InternalScopeString) && !isWalletNamespace @@ -77,13 +77,18 @@ const setEthAccountsForScopesObject = ( updatedScopesObject[scopeString as InternalScopeString] = scopeObject; return; } - - const caipAccounts = accounts.map( - (account) => - (isWalletNamespace - ? `${KnownWalletScopeString.Eip155}:${account}` - : `${scopeString}:${account}`), - ); + let caipAccounts: CaipAccountId[] = []; + if (isWalletNamespace) { + caipAccounts = accounts.map( + (account) => `${KnownWalletScopeString.Eip155}:${account}`, + ); + } else if (namespace && reference) { + caipAccounts = accounts.map( + (account) => `${namespace}:${reference}:${account}`, + ); + } else { + throw new Error('Invalid scope string'); + } updatedScopesObject[scopeString as InternalScopeString] = { ...scopeObject, From d73f644dadcbf604d001c8bad18b5925e71a8a02 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 11 Nov 2024 13:10:30 -0600 Subject: [PATCH 123/144] cleanup type casting --- .../caip-permission-adapter-eth-accounts.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 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 e1195cd045..20507111e1 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -67,14 +67,13 @@ const setEthAccountsForScopesObject = ( ) => { const updatedScopesObject: InternalScopesObject = {}; - Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { + Object.entries(scopesObject).forEach(([key, scopeObject]) => { + // Cast needed because index type is returned as `string` by `Object.entries` + const scopeString = key as keyof typeof scopesObject; const isWalletNamespace = scopeString === KnownCaipNamespace.Wallet; const { namespace, reference } = parseScopeString(scopeString); - if ( - !isEip155ScopeString(scopeString as InternalScopeString) && - !isWalletNamespace - ) { - updatedScopesObject[scopeString as InternalScopeString] = scopeObject; + if (!isEip155ScopeString(scopeString) && !isWalletNamespace) { + updatedScopesObject[scopeString] = scopeObject; return; } let caipAccounts: CaipAccountId[] = []; @@ -90,7 +89,7 @@ const setEthAccountsForScopesObject = ( throw new Error('Invalid scope string'); } - updatedScopesObject[scopeString as InternalScopeString] = { + updatedScopesObject[scopeString] = { ...scopeObject, accounts: caipAccounts, }; From 5371ff0d09fa463e841d5cb2f0e8db6d83400290 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 12:42:02 -0600 Subject: [PATCH 124/144] Expand setEthAccounts TSDoc comment --- .../src/adapters/caip-permission-adapter-eth-accounts.ts | 2 ++ 1 file changed, 2 insertions(+) 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 20507111e1..eacf5ce519 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -100,6 +100,8 @@ const setEthAccountsForScopesObject = ( /** * Sets the Ethereum (EIP155 namespaced) accounts for the given CAIP-25 caveat value. + * We set the same accounts for all the scopes that are EIP155 or Wallet namespaced because + * we do not provide UI/UX flows for selecting different accounts across different chains. * @param caip25CaveatValue - The CAIP-25 caveat value to set the Ethereum accounts for. * @param accounts - The Ethereum accounts to set. * @returns The updated CAIP-25 caveat value with the Ethereum accounts set. From 8d490f7e84dfb2aec26b6ee45c1bb070fe78e270 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 12:49:35 -0600 Subject: [PATCH 125/144] fix type casting --- .../caip-permission-adapter-permittedChains.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 270783cb32..911890e0f4 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -87,19 +87,21 @@ const filterEthScopesObjectByChainId = ( ) => { const updatedScopesObject: InternalScopesObject = {}; - Object.entries(scopesObject).forEach(([scopeString, scopeObject]) => { + Object.entries(scopesObject).forEach(([key, scopeObject]) => { + // Cast needed because index type is returned as `string` by `Object.entries` + const scopeString = key as keyof typeof scopesObject; const { namespace, reference } = parseScopeString(scopeString); if (!reference) { - updatedScopesObject[scopeString as InternalScopeString] = scopeObject; + updatedScopesObject[scopeString] = scopeObject; return; } if (namespace === KnownCaipNamespace.Eip155) { const chainId = toHex(reference); if (chainIds.includes(chainId)) { - updatedScopesObject[scopeString as InternalScopeString] = scopeObject; + updatedScopesObject[scopeString] = scopeObject; } } else { - updatedScopesObject[scopeString as InternalScopeString] = scopeObject; + updatedScopesObject[scopeString] = scopeObject; } }); From aba885783234b4daead5da592e248a28d773b226 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 13:12:40 -0600 Subject: [PATCH 126/144] update TSDoc for addPermittedEthChainId --- .../src/adapters/caip-permission-adapter-permittedChains.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 911890e0f4..40aeeb51ff 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -40,7 +40,8 @@ export const getPermittedEthChainIds = ( }; /** - * Adds an Ethereum (EIP155 namespaced) chainID to the required and optional scopes. + * Adds an Ethereum (EIP155 namespaced) chainID to the optional scopes if it is not already present + * in either the pre-existing required or optional scopes. * @param caip25CaveatValue - The CAIP-25 caveat value to add the Ethereum chainID to. * @param chainId - The Ethereum chainID to add. * @returns The updated CAIP-25 caveat value with the added Ethereum chainID. From fc71fc6163b5e49d4a4f9bf9caf014505692ce77 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 15:24:37 -0600 Subject: [PATCH 127/144] update setPermittedEthChainIds TSDoc --- .../adapters/caip-permission-adapter-permittedChains.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 40aeeb51ff..a9b133a4cc 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -9,7 +9,7 @@ import { KnownWalletScopeString, } from '../scope/constants'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; -import type { InternalScopesObject, InternalScopeString } from '../scope/types'; +import type { InternalScopesObject } from '../scope/types'; import { parseScopeString } from '../scope/types'; /** @@ -77,9 +77,12 @@ export const addPermittedEthChainId = ( }; /** - * Filters the scopes object to only include the scopes for the given chainIDs. + * Filters the scopes object to only include: + * - Scopes without references (e.g. "wallet:") + * - EIP155 scopes for the given chainIDs + * - Non EIP155 scopes (e.g. "bip122:" or any other non ethereum namespaces) * @param scopesObject - The scopes object to filter. - * @param chainIds - The chainIDs to filter the scopes object by. + * @param chainIds - The chainIDs to filter EIP155 scopes by. * @returns The filtered scopes object. */ const filterEthScopesObjectByChainId = ( From f7444b201805f331180695e5ffa97bd78df7ff49 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 12 Nov 2024 15:29:32 -0600 Subject: [PATCH 128/144] Update packages/multichain/src/index.ts Co-authored-by: Mark Stacey --- packages/multichain/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index 2dc8c6f910..ec75d9b17d 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -40,7 +40,6 @@ export { mergeScopes, normalizeAndMergeScopes, } from './scope/transform'; -export { isValidScope, getValidScopes } from './scope/validation'; export type { Caip25CaveatValue } from './caip25Permission'; export { From da672b99bbd3b553d18149ae1d2d632b20020872 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 12 Nov 2024 15:33:00 -0600 Subject: [PATCH 129/144] Update packages/multichain/src/caip25Permission.ts Co-authored-by: Mark Stacey --- packages/multichain/src/caip25Permission.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 10b46ad18b..63a4182216 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -241,7 +241,7 @@ function removeAccount( /** * 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,. + * the existing scopes. * * @param caip25CaveatValue - The CAIP-25 permission caveat value to remove the scope from. * @param targetScopeString - The scope that is being removed. From 76a0afd8e48afba2196a87b0261d49f24b1f035f Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 12 Nov 2024 15:34:56 -0600 Subject: [PATCH 130/144] Update packages/multichain/src/caip25Permission.ts Co-authored-by: Mark Stacey --- packages/multichain/src/caip25Permission.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 63a4182216..3e273bf494 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -212,7 +212,7 @@ function removeAccountFromScopeObject( */ function removeAccount( caip25CaveatValue: Caip25CaveatValue, - targetAddress: string, + targetAddress: Hex, ) { const copyOfCaveatValue = cloneDeep(caip25CaveatValue); From d10f7d04fca83cd8d79a58919c838597285c190b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 16:47:52 -0600 Subject: [PATCH 131/144] improve caip25permission caveat type validation --- packages/multichain/src/caip25Permission.ts | 21 ++-- packages/multichain/src/scope/assert.ts | 125 +++++++++++++++++++- 2 files changed, 137 insertions(+), 9 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 3e273bf494..0875cf981b 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -12,6 +12,7 @@ import { } from '@metamask/permission-controller'; import type { CaipAccountId, Json } from '@metamask/utils'; import { + hasProperty, parseCaipAccountId, type Hex, type NonEmptyArray, @@ -19,7 +20,10 @@ import { import { cloneDeep, isEqual } from 'lodash'; import { getEthAccounts } from './adapters/caip-permission-adapter-eth-accounts'; -import { assertScopesSupported } from './scope/assert'; +import { + assertScopesSupported, + assertIsExternalScopesObject, +} from './scope/assert'; import { validateAndNormalizeScopes } from './scope/authorization'; import type { ExternalScopeString, @@ -89,18 +93,21 @@ const specificationBuilder: PermissionSpecificationBuilder< ); } - const { requiredScopes, optionalScopes, isMultichainOrigin } = - caip25Caveat.value as Caip25CaveatValue; - if ( - !requiredScopes || - !optionalScopes || - typeof isMultichainOrigin !== 'boolean' + !caip25Caveat.value || + !hasProperty(caip25Caveat.value, 'requiredScopes') || + !hasProperty(caip25Caveat.value, 'optionalScopes') || + !hasProperty(caip25Caveat.value, 'isMultichainOrigin') || + typeof caip25Caveat.value.isMultichainOrigin !== 'boolean' ) { throw new Error( `${Caip25EndowmentPermissionName} error: Received invalid value for caveat of type "${Caip25CaveatType}".`, ); } + const { requiredScopes, optionalScopes } = caip25Caveat.value; + + assertIsExternalScopesObject(requiredScopes); + assertIsExternalScopesObject(optionalScopes); const { normalizedRequiredScopes, normalizedOptionalScopes } = validateAndNormalizeScopes(requiredScopes, optionalScopes); diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index d2bee8ab30..b134aee234 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -1,4 +1,11 @@ -import type { Hex } from '@metamask/utils'; +import { + hasProperty, + isCaipAccountId, + isCaipChainId, + isCaipNamespace, + isCaipReference, + type Hex, +} from '@metamask/utils'; import { Caip25Errors } from './errors'; import { @@ -6,7 +13,13 @@ import { isSupportedNotification, isSupportedScopeString, } from './supported'; -import type { InternalScopeObject, InternalScopesObject } from './types'; +import type { + ExternalScopeObject, + ExternalScopesObject, + ExternalScopeString, + InternalScopeObject, + InternalScopesObject, +} from './types'; /** * Asserts that a scope string and its associated scope object are supported. @@ -67,3 +80,111 @@ export const assertScopesSupported = ( }); } }; +/** + * Asserts that an object is a valid ExternalScopeObject. + * @param obj - The object to assert. + */ +function assertIsExternalScopeObject( + obj: unknown, +): asserts obj is ExternalScopeObject { + if (typeof obj !== 'object' || obj === null) { + throw new Error('ExternalScopeObject must be an object'); + } + + if (hasProperty(obj, 'references')) { + if ( + !Array.isArray(obj.references) || + !obj.references.every(isCaipReference) + ) { + throw new Error( + 'ExternalScopeObject.references must be an array of CaipReference', + ); + } + } + + if (hasProperty(obj, 'accounts')) { + if (!Array.isArray(obj.accounts) || !obj.accounts.every(isCaipAccountId)) { + throw new Error( + 'ExternalScopeObject.accounts must be an array of CaipAccountId', + ); + } + } + + if (hasProperty(obj, 'methods')) { + if ( + !Array.isArray(obj.methods) || + !obj.methods.every((method) => typeof method === 'string') + ) { + throw new Error( + 'ExternalScopeObject.methods must be an array of strings', + ); + } + } + + if (hasProperty(obj, 'notifications')) { + if ( + !Array.isArray(obj.notifications) || + !obj.notifications.every( + (notification) => typeof notification === 'string', + ) + ) { + throw new Error( + 'ExternalScopeObject.notifications must be an array of strings', + ); + } + } + + if (hasProperty(obj, 'rpcDocuments')) { + if ( + !Array.isArray(obj.rpcDocuments) || + !obj.rpcDocuments.every((doc) => typeof doc === 'string') + ) { + throw new Error( + 'ExternalScopeObject.rpcDocuments must be an array of strings', + ); + } + } + + if (hasProperty(obj, 'rpcEndpoints')) { + if ( + !Array.isArray(obj.rpcEndpoints) || + !obj.rpcEndpoints.every((endpoint) => typeof endpoint === 'string') + ) { + throw new Error( + 'ExternalScopeObject.rpcEndpoints must be an array of strings', + ); + } + } +} + +/** + * Asserts that a scope string is a valid ExternalScopeString. + * @param scopeString - The scope string to assert. + */ +function assertIsExternalScopeString( + scopeString: unknown, +): asserts scopeString is ExternalScopeString { + if ( + typeof scopeString !== 'string' || + (!isCaipNamespace(scopeString) && !isCaipChainId(scopeString)) + ) { + throw new Error('scopeString is not a valid ExternalScopeString'); + } +} + +/** + * Asserts that an object is a valid ExternalScopesObject. + * @param obj - The object to assert. + */ +export function assertIsExternalScopesObject( + obj: unknown, +): asserts obj is ExternalScopesObject { + if (typeof obj !== 'object' || obj === null) { + throw new Error('Object is not an ExternalScopesObject'); + } + + for (const [scopeString, scopeObject] of Object.entries(obj)) { + assertIsExternalScopeString(scopeString); + assertIsExternalScopeObject(scopeObject); + } +} From e5096d7ebc29b4ad1ab14f703bb3c1b80bf64163 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 12 Nov 2024 16:53:20 -0600 Subject: [PATCH 132/144] Update packages/multichain/src/caip25Permission.ts Co-authored-by: Mark Stacey --- packages/multichain/src/caip25Permission.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index 0875cf981b..fe51e50a55 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -246,8 +246,8 @@ function removeAccount( } /** - * Removes the target account from the value arrays of all - * `endowment:caip25` caveats. No-ops if the target scopeString is not in + * Removes the target account from the value arrays of the given + * `endowment:caip25` caveat. No-ops if the target scopeString is not in * the existing scopes. * * @param caip25CaveatValue - The CAIP-25 permission caveat value to remove the scope from. From f93eb36867269602f4fc5f274a34eb00c1c401f9 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 16:55:30 -0600 Subject: [PATCH 133/144] update lock file --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2f73ad4af5..bc14a6119c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.3, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@npm:^11.4.3, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3090,7 +3090,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.2, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.1, @metamask/network-controller@npm:^22.0.2, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: From 5b08f70ecf9d9c5280fcd9f3f153c4dea043fb43 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 16:55:58 -0600 Subject: [PATCH 134/144] fix constraints --- packages/multichain/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/package.json b/packages/multichain/package.json index 6c54d26c41..a65ad36a54 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.4.2", + "@metamask/controller-utils": "^11.4.3", "@metamask/eth-json-rpc-filters": "^7.0.0", "@metamask/rpc-errors": "^7.0.1", "@metamask/utils": "^10.0.0", @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^3.4.4", - "@metamask/network-controller": "^22.0.1", + "@metamask/network-controller": "^22.0.2", "@metamask/permission-controller": "^11.0.3", "@types/jest": "^27.4.1", "deepmerge": "^4.2.2", From 6557b4606a73c0a8a18f54fda1fa2069024781de Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2024 17:02:18 -0600 Subject: [PATCH 135/144] update lockfile again --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index bc14a6119c..326e782975 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2334,7 +2334,7 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^11.4.2, @metamask/controller-utils@npm:^11.4.3, @metamask/controller-utils@workspace:packages/controller-utils": +"@metamask/controller-utils@npm:^11.4.3, @metamask/controller-utils@workspace:packages/controller-utils": version: 0.0.0-use.local resolution: "@metamask/controller-utils@workspace:packages/controller-utils" dependencies: @@ -3051,9 +3051,9 @@ __metadata: dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/controller-utils": "npm:^11.4.2" + "@metamask/controller-utils": "npm:^11.4.3" "@metamask/eth-json-rpc-filters": "npm:^7.0.0" - "@metamask/network-controller": "npm:^22.0.1" + "@metamask/network-controller": "npm:^22.0.2" "@metamask/permission-controller": "npm:^11.0.3" "@metamask/rpc-errors": "npm:^7.0.1" "@metamask/utils": "npm:^10.0.0" @@ -3090,7 +3090,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@npm:^22.0.1, @metamask/network-controller@npm:^22.0.2, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@npm:^22.0.2, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: From 161dad94288c985da8ea1ed71488a89a436a9b6d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 09:54:37 -0600 Subject: [PATCH 136/144] fix tests --- packages/multichain/src/caip25Permission.test.ts | 2 ++ packages/multichain/src/index.test.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/caip25Permission.test.ts b/packages/multichain/src/caip25Permission.test.ts index b641c395e8..b124f34f9b 100644 --- a/packages/multichain/src/caip25Permission.test.ts +++ b/packages/multichain/src/caip25Permission.test.ts @@ -20,8 +20,10 @@ jest.mock('./scope/authorization', () => ({ const MockScopeAuthorization = jest.mocked(ScopeAuthorization); jest.mock('./scope/assert', () => ({ + ...jest.requireActual('./scope/assert'), assertScopesSupported: jest.fn(), })); + const MockScopeAssert = jest.mocked(ScopeAssert); const { removeAccount, removeScope } = Caip25CaveatMutators[Caip25CaveatType]; diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 9cac4a71aa..1939f52b50 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -24,8 +24,6 @@ describe('@metamask/multichain', () => { "mergeScopeObject", "mergeScopes", "normalizeAndMergeScopes", - "isValidScope", - "getValidScopes", "Caip25CaveatType", "createCaip25Caveat", "Caip25EndowmentPermissionName", From 7b3e1495e340f56b94c540b1acb8c9dbf8af2a14 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 10:47:09 -0600 Subject: [PATCH 137/144] add tests for caip25permission caveat type validation --- packages/multichain/src/scope/assert.test.ts | 288 ++++++++++++++++++- packages/multichain/src/scope/assert.ts | 2 +- 2 files changed, 288 insertions(+), 2 deletions(-) diff --git a/packages/multichain/src/scope/assert.test.ts b/packages/multichain/src/scope/assert.test.ts index 02b8ed8a8c..362078cd22 100644 --- a/packages/multichain/src/scope/assert.test.ts +++ b/packages/multichain/src/scope/assert.test.ts @@ -1,4 +1,10 @@ -import { assertScopeSupported, assertScopesSupported } from './assert'; +import * as Utils from '@metamask/utils'; + +import { + assertScopeSupported, + assertScopesSupported, + assertIsExternalScopesObject, +} from './assert'; import { Caip25Errors } from './errors'; import * as Supported from './supported'; import type { InternalScopeObject } from './types'; @@ -8,7 +14,15 @@ jest.mock('./supported', () => ({ isSupportedNotification: jest.fn(), isSupportedMethod: jest.fn(), })); + +jest.mock('@metamask/utils', () => ({ + ...jest.requireActual('@metamask/utils'), + isCaipReference: jest.fn(), + isCaipAccountId: jest.fn(), +})); + const MockSupported = jest.mocked(Supported); +const MockUtils = jest.mocked(Utils); const validScopeObject: InternalScopeObject = { methods: [], @@ -17,6 +31,15 @@ const validScopeObject: InternalScopeObject = { }; describe('Scope Assert', () => { + beforeEach(() => { + MockUtils.isCaipReference.mockImplementation(() => true); + MockUtils.isCaipAccountId.mockImplementation(() => true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('assertScopeSupported', () => { const isChainIdSupported = jest.fn(); @@ -194,4 +217,267 @@ describe('Scope Assert', () => { ).toBeUndefined(); }); }); + + describe('assertIsExternalScopesObject', () => { + it('does not throw if passed obj is a valid ExternalScopesObject with all valid properties', () => { + const obj = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => assertIsExternalScopesObject(obj)).not.toThrow(); + }); + + it('does not throw if passed obj is a valid ExternalScopesObject with some optional properties missing', () => { + const obj = { + accounts: ['eip155:1:0x1234'], + methods: ['method1'], + }; + expect(() => assertIsExternalScopesObject(obj)).not.toThrow(); + }); + + it('throws an error if passed obj is not an object', () => { + expect(() => assertIsExternalScopesObject(null)).toThrow( + 'ExternalScopesObject must be an object', + ); + expect(() => assertIsExternalScopesObject(123)).toThrow( + 'ExternalScopesObject must be an object', + ); + expect(() => assertIsExternalScopesObject('string')).toThrow( + 'ExternalScopesObject must be an object', + ); + }); + + it('throws and error if passed an object with an ExternalScopeObject value that is not an object', () => { + expect(() => assertIsExternalScopesObject({ 'eip155:1': 123 })).toThrow( + 'ExternalScopeObject must be an object', + ); + }); + + it('throws an error if passed an object with a key that is not a valid ExternalScopeString', () => { + jest.spyOn(Utils, 'isCaipReference').mockImplementation(() => false); + + expect(() => + assertIsExternalScopesObject({ 'invalid-scope-string': {} }), + ).toThrow('scopeString is not a valid ExternalScopeString'); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a references property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: 'not-an-array', + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.references must be an array of CaipReference', + ); + }); + + it('throws an error if references contains invalid CaipReference', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['invalidRef'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + jest + .spyOn(Utils, 'isCaipReference') + .mockImplementation((ref) => ref !== 'invalidRef'); + + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.references must be an array of CaipReference', + ); + jest.restoreAllMocks(); + }); + + it('throws an error if passed an object with an ExternalScopeObject with an accounts property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: 'not-an-array', + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.accounts must be an array of CaipAccountId', + ); + }); + + it('throws an error if accounts contains invalid CaipAccountId', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234', 'invalidAccount'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + MockUtils.isCaipAccountId.mockImplementation( + (id) => id !== 'invalidAccount', + ); + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.accounts must be an array of CaipAccountId', + ); + jest.restoreAllMocks(); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a methods property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: 'not-an-array', + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.methods must be an array of strings'); + }); + + it('throws an error if methods contains non-string elements', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 123], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.methods must be an array of strings'); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a notifications property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: 'not-an-array', + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.notifications must be an array of strings', + ); + }); + + it('throws an error if notifications contains non-string elements', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1', false], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow( + 'ExternalScopeObject.notifications must be an array of strings', + ); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a rpcDocuments property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: 'not-an-array', + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.rpcDocuments must be an array of strings'); + }); + + it('throws an error if rpcDocuments contains non-string elements', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1', 456], + rpcEndpoints: ['endpoint1'], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.rpcDocuments must be an array of strings'); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a rpcEndpoints property that is not an array', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: 'not-an-array', + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.rpcEndpoints must be an array of strings'); + }); + + it('throws an error if passed an object with an ExternalScopeObject with a rpcEndpoints property that contains non-string elements', () => { + const invalidExternalScopeObject = { + 'eip155:1': { + references: ['reference1', 'reference2'], + accounts: ['eip155:1:0x1234'], + methods: ['method1', 'method2'], + notifications: ['notification1'], + rpcDocuments: ['doc1'], + rpcEndpoints: ['endpoint1', null], + }, + }; + expect(() => + assertIsExternalScopesObject(invalidExternalScopeObject), + ).toThrow('ExternalScopeObject.rpcEndpoints must be an array of strings'); + }); + }); }); diff --git a/packages/multichain/src/scope/assert.ts b/packages/multichain/src/scope/assert.ts index b134aee234..522ed2f1d8 100644 --- a/packages/multichain/src/scope/assert.ts +++ b/packages/multichain/src/scope/assert.ts @@ -180,7 +180,7 @@ export function assertIsExternalScopesObject( obj: unknown, ): asserts obj is ExternalScopesObject { if (typeof obj !== 'object' || obj === null) { - throw new Error('Object is not an ExternalScopesObject'); + throw new Error('ExternalScopesObject must be an object'); } for (const [scopeString, scopeObject] of Object.entries(obj)) { From a53240985b8abddf7a75cc183bd918b8e4d9f1ec Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 11:16:01 -0600 Subject: [PATCH 138/144] fix Hex type casting --- .../adapters/caip-permission-adapter-eth-accounts.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 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 eacf5ce519..cf4627530d 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -1,4 +1,5 @@ import { + assertIsStrictHexString, type CaipAccountId, type Hex, KnownCaipNamespace, @@ -35,8 +36,8 @@ export const getEthAccounts = ( Caip25CaveatValue, 'requiredScopes' | 'optionalScopes' >, -) => { - const ethAccounts: string[] = []; +): Hex[] => { + const ethAccounts: Hex[] = []; const sessionScopes = mergeScopes( caip25CaveatValue.requiredScopes, caip25CaveatValue.optionalScopes, @@ -47,6 +48,9 @@ export const getEthAccounts = ( const { address, chainId } = parseCaipAccountId(account); if (isEip155ScopeString(chainId)) { + // This address should always be a valid Hex string because + // it's an EIP155/Ethereum account + assertIsStrictHexString(address); ethAccounts.push(address); } }); @@ -66,7 +70,6 @@ const setEthAccountsForScopesObject = ( accounts: Hex[], ) => { const updatedScopesObject: InternalScopesObject = {}; - Object.entries(scopesObject).forEach(([key, scopeObject]) => { // Cast needed because index type is returned as `string` by `Object.entries` const scopeString = key as keyof typeof scopesObject; @@ -76,6 +79,7 @@ const setEthAccountsForScopesObject = ( updatedScopesObject[scopeString] = scopeObject; return; } + let caipAccounts: CaipAccountId[] = []; if (isWalletNamespace) { caipAccounts = accounts.map( @@ -85,8 +89,6 @@ const setEthAccountsForScopesObject = ( caipAccounts = accounts.map( (account) => `${namespace}:${reference}:${account}`, ); - } else { - throw new Error('Invalid scope string'); } updatedScopesObject[scopeString] = { From 6c38448482c76b96781c0b0a1fa65ac016fb8eed Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 11:39:24 -0600 Subject: [PATCH 139/144] remove redundant addition of wallet:eip155 optional scope --- ...permission-adapter-permittedChains.test.ts | 56 ------------------- ...caip-permission-adapter-permittedChains.ts | 5 -- 2 files changed, 61 deletions(-) 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 50c24c4673..c1504bd96f 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.test.ts @@ -109,57 +109,6 @@ describe('CAIP-25 permittedChains adapters', () => { }); }); - it('returns a version of the caveat value with a new 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: [], - accounts: [], - }, - }, - isMultichainOrigin: false, - }); - }); - it('does not modify the input CAIP-25 caveat value object', () => { const input: Caip25CaveatValue = { requiredScopes: { @@ -354,11 +303,6 @@ describe('CAIP-25 permittedChains adapters', () => { notifications: KnownNotifications.eip155, accounts: [], }, - '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 a9b133a4cc..12cabdd5a4 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -61,11 +61,6 @@ export const addPermittedEthChainId = ( return { ...caip25CaveatValue, optionalScopes: { - [KnownWalletScopeString.Eip155]: { - methods: [], - notifications: [], - accounts: [], - }, ...caip25CaveatValue.optionalScopes, [scopeString]: { methods: KnownRpcMethods.eip155, From bce936014cf00c4ec3019898a42a581c4f93247b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 11:43:49 -0600 Subject: [PATCH 140/144] added to setEthAccounts TSDoc to explain why we inject a wallet:eip155 scope --- .../src/adapters/caip-permission-adapter-eth-accounts.ts | 5 +++++ 1 file changed, 5 insertions(+) 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 cf4627530d..6e96aac786 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-eth-accounts.ts @@ -104,6 +104,11 @@ const setEthAccountsForScopesObject = ( * Sets the Ethereum (EIP155 namespaced) accounts for the given CAIP-25 caveat value. * We set the same accounts for all the scopes that are EIP155 or Wallet namespaced because * we do not provide UI/UX flows for selecting different accounts across different chains. + * + * Additionally, this function adds a `wallet:eip155` scope with empty methods, notifications, and accounts + * to ensure that the `wallet:eip155` scope is always present in the caveat value. + * This is required for Snaps currently can have account permissions without chain permissions. + * This added `wallet:eip155` scope should be removed once Snaps are able to have/use chain permissions. * @param caip25CaveatValue - The CAIP-25 caveat value to set the Ethereum accounts for. * @param accounts - The Ethereum accounts to set. * @returns The updated CAIP-25 caveat value with the Ethereum accounts set. From f905350f5431351e46841002bdc05b39c9e6cc9c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 12:30:48 -0600 Subject: [PATCH 141/144] add type/const docs --- packages/multichain/src/caip25Permission.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/multichain/src/caip25Permission.ts b/packages/multichain/src/caip25Permission.ts index fe51e50a55..b7c1679491 100644 --- a/packages/multichain/src/caip25Permission.ts +++ b/packages/multichain/src/caip25Permission.ts @@ -31,6 +31,11 @@ import type { InternalScopesObject, } from './scope/types'; +/** + * The CAIP-25 permission caveat value. + * This permission contains the required and optional scopes and session properties from the [CAIP-25](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) request that initiated the permission session. + * It also contains a boolean (isMultichainOrigin) indicating if the permission session is multichain, which may be needed to determine implicit permissioning. + */ export type Caip25CaveatValue = { requiredScopes: InternalScopesObject; optionalScopes: InternalScopesObject; @@ -38,8 +43,16 @@ export type Caip25CaveatValue = { isMultichainOrigin: boolean; }; +/** + * The name of the CAIP-25 permission caveat. + */ export const Caip25CaveatType = 'authorizedScopes'; +/** + * Creates a CAIP-25 permission caveat. + * @param value - The CAIP-25 permission caveat value. + * @returns The CAIP-25 permission caveat (now including the type). + */ export const createCaip25Caveat = (value: Caip25CaveatValue) => { return { type: Caip25CaveatType, @@ -47,6 +60,9 @@ export const createCaip25Caveat = (value: Caip25CaveatValue) => { }; }; +/** + * The target name of the CAIP-25 endowment permission. + */ export const Caip25EndowmentPermissionName = 'endowment:caip25'; type Caip25EndowmentSpecification = ValidPermissionSpecification<{ From d0865d41173b81587f6af6432c4ddc3bdd012542 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 12:35:00 -0600 Subject: [PATCH 142/144] lint --- .../src/adapters/caip-permission-adapter-permittedChains.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts index 12cabdd5a4..dbf1975840 100644 --- a/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts +++ b/packages/multichain/src/adapters/caip-permission-adapter-permittedChains.ts @@ -3,11 +3,7 @@ import type { Hex } from '@metamask/utils'; import { KnownCaipNamespace } from '@metamask/utils'; import type { Caip25CaveatValue } from '../caip25Permission'; -import { - KnownNotifications, - KnownRpcMethods, - KnownWalletScopeString, -} from '../scope/constants'; +import { KnownNotifications, KnownRpcMethods } from '../scope/constants'; import { getUniqueArrayItems, mergeScopes } from '../scope/transform'; import type { InternalScopesObject } from '../scope/types'; import { parseScopeString } from '../scope/types'; From 555383cbb78a7960be615331ec00f28a25279656 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 13 Nov 2024 15:24:47 -0600 Subject: [PATCH 143/144] Update packages/multichain/src/index.ts Co-authored-by: Mark Stacey --- packages/multichain/src/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/multichain/src/index.ts b/packages/multichain/src/index.ts index ec75d9b17d..51864f461c 100644 --- a/packages/multichain/src/index.ts +++ b/packages/multichain/src/index.ts @@ -28,12 +28,6 @@ export type { NonWalletKnownCaipNamespace, } from './scope/types'; export { parseScopeString } from './scope/types'; -export { - isSupportedScopeString, - isSupportedAccount, - isSupportedMethod, - isSupportedNotification, -} from './scope/supported'; export { normalizeScope, mergeScopeObject, From bc21a363200e5d9e95cfdfce3f548874ddd8901c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2024 15:30:39 -0600 Subject: [PATCH 144/144] update snapshot --- packages/multichain/src/index.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/multichain/src/index.test.ts b/packages/multichain/src/index.test.ts index 1939f52b50..61f0fdcc42 100644 --- a/packages/multichain/src/index.test.ts +++ b/packages/multichain/src/index.test.ts @@ -16,10 +16,6 @@ describe('@metamask/multichain', () => { "KnownNotifications", "KnownWalletScopeString", "parseScopeString", - "isSupportedScopeString", - "isSupportedAccount", - "isSupportedMethod", - "isSupportedNotification", "normalizeScope", "mergeScopeObject", "mergeScopes",