diff --git a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.js b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.ts similarity index 85% rename from app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.js rename to app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.ts index 1b3951644666..3bf8f115db40 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.test.ts @@ -2,35 +2,43 @@ import { JsonRpcError } from '@metamask/rpc-errors'; import { Caip25CaveatType, Caip25EndowmentPermissionName, - validateAndNormalizeScopes, - bucketScopes, KnownRpcMethods, KnownNotifications, + Caip25Authorization, + ScopesObject, } from '@metamask/multichain'; +import * as Multichain from '@metamask/multichain'; +import { Json, JsonRpcRequest, JsonRpcSuccess } from '@metamask/utils'; import { CaveatTypes } from '../../../../../../shared/constants/permissions'; -import { shouldEmitDappViewedEvent } from '../../../util'; +import * as Util from '../../../util'; import { PermissionNames } from '../../../../controllers/permissions'; -import { processScopedProperties, validateAndAddEip3085 } from './helpers'; -import { walletCreateSessionHandler } from './handler'; +import * as Helpers from './helpers'; +import { walletCreateSession } from './handler'; jest.mock('../../../util', () => ({ ...jest.requireActual('../../../util'), shouldEmitDappViewedEvent: jest.fn(), })); +const MockUtil = jest.mocked(Util); jest.mock('@metamask/multichain', () => ({ ...jest.requireActual('@metamask/multichain'), validateAndNormalizeScopes: jest.fn(), bucketScopes: jest.fn(), })); +const MockMultichain = jest.mocked(Multichain); jest.mock('./helpers', () => ({ ...jest.requireActual('./helpers'), validateAndAddEip3085: jest.fn(), processScopedProperties: jest.fn(), })); +const MockHelpers = jest.mocked(Helpers); const baseRequest = { + jsonrpc: '2.0' as const, + id: 0, + method: 'wallet_createSession', origin: 'http://test.com', params: { requiredScopes: { @@ -62,7 +70,7 @@ const createMockedHandler = () => { }); const grantPermissions = jest.fn().mockResolvedValue(undefined); const findNetworkClientIdByChainId = jest.fn().mockReturnValue('mainnet'); - const addNetwork = jest.fn().mockResolvedValue(); + const addNetwork = jest.fn().mockResolvedValue(undefined); const removeNetwork = jest.fn(); const sendMetrics = jest.fn(); const metamaskState = { @@ -75,9 +83,17 @@ const createMockedHandler = () => { }, }; const listAccounts = jest.fn().mockReturnValue([]); - const response = {}; - const handler = (request) => - walletCreateSessionHandler(request, response, next, end, { + const response = { + jsonrpc: '2.0' as const, + id: 0, + } as unknown as JsonRpcSuccess<{ + sessionScopes: ScopesObject; + sessionProperties?: Record; + }>; + const handler = ( + request: JsonRpcRequest & { origin: string }, + ) => + walletCreateSession.implementation(request, response, next, end, { findNetworkClientIdByChainId, requestPermissionApprovalForOrigin, grantPermissions, @@ -106,11 +122,11 @@ const createMockedHandler = () => { describe('wallet_createSession', () => { beforeEach(() => { - validateAndNormalizeScopes.mockReturnValue({ + MockMultichain.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: {}, normalizedOptionalScopes: {}, }); - bucketScopes.mockReturnValue({ + MockMultichain.bucketScopes.mockReturnValue({ supportedScopes: {}, supportableScopes: {}, unsupportableScopes: {}, @@ -142,20 +158,28 @@ describe('wallet_createSession', () => { params: { ...baseRequest.params, optionalScopes: { - foo: 'bar', + foo: { + methods: [], + notifications: [], + }, }, }, }); - expect(validateAndNormalizeScopes).toHaveBeenCalledWith( + expect(MockMultichain.validateAndNormalizeScopes).toHaveBeenCalledWith( baseRequest.params.requiredScopes, - { foo: 'bar' }, + { + foo: { + methods: [], + notifications: [], + }, + }, ); }); it('throws an error when processing scopes fails', async () => { const { handler, end } = createMockedHandler(); - validateAndNormalizeScopes.mockImplementation(() => { + MockMultichain.validateAndNormalizeScopes.mockImplementation(() => { throw new Error('failed to process scopes'); }); await handler(baseRequest); @@ -164,7 +188,7 @@ describe('wallet_createSession', () => { it('processes the scopedProperties', async () => { const { handler } = createMockedHandler(); - validateAndNormalizeScopes.mockReturnValue({ + MockMultichain.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], @@ -190,7 +214,7 @@ describe('wallet_createSession', () => { }, }); - expect(processScopedProperties).toHaveBeenCalledWith( + expect(MockHelpers.processScopedProperties).toHaveBeenCalledWith( { 'eip155:1': { methods: ['eth_chainId'], @@ -211,7 +235,7 @@ describe('wallet_createSession', () => { it('throws an error when processing scopedProperties fails', async () => { const { handler, end } = createMockedHandler(); - processScopedProperties.mockImplementation(() => { + MockHelpers.processScopedProperties.mockImplementation(() => { throw new Error('failed to process scoped properties'); }); await handler(baseRequest); @@ -222,7 +246,7 @@ describe('wallet_createSession', () => { it('buckets the required scopes', async () => { const { handler } = createMockedHandler(); - validateAndNormalizeScopes.mockReturnValue({ + MockMultichain.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: { 'eip155:1': { methods: ['eth_chainId'], @@ -234,7 +258,7 @@ describe('wallet_createSession', () => { }); await handler(baseRequest); - expect(bucketScopes).toHaveBeenNthCalledWith( + expect(MockMultichain.bucketScopes).toHaveBeenNthCalledWith( 1, { 'eip155:1': { @@ -250,16 +274,16 @@ describe('wallet_createSession', () => { ); const isChainIdSupportedBody = - bucketScopes.mock.calls[0][1].isChainIdSupported.toString(); + MockMultichain.bucketScopes.mock.calls[0][1].isChainIdSupported.toString(); expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); const isChainIdSupportableBody = - bucketScopes.mock.calls[0][1].isChainIdSupportable.toString(); + MockMultichain.bucketScopes.mock.calls[0][1].isChainIdSupportable.toString(); expect(isChainIdSupportableBody).toContain('validScopedProperties'); }); it('buckets the optional scopes', async () => { const { handler } = createMockedHandler(); - validateAndNormalizeScopes.mockReturnValue({ + MockMultichain.validateAndNormalizeScopes.mockReturnValue({ normalizedRequiredScopes: {}, normalizedOptionalScopes: { 'eip155:100': { @@ -271,7 +295,7 @@ describe('wallet_createSession', () => { }); await handler(baseRequest); - expect(bucketScopes).toHaveBeenNthCalledWith( + expect(MockMultichain.bucketScopes).toHaveBeenNthCalledWith( 2, { 'eip155:100': { @@ -287,10 +311,10 @@ describe('wallet_createSession', () => { ); const isChainIdSupportedBody = - bucketScopes.mock.calls[1][1].isChainIdSupported.toString(); + MockMultichain.bucketScopes.mock.calls[1][1].isChainIdSupported.toString(); expect(isChainIdSupportedBody).toContain('findNetworkClientIdByChainId'); const isChainIdSupportableBody = - bucketScopes.mock.calls[1][1].isChainIdSupportable.toString(); + MockMultichain.bucketScopes.mock.calls[1][1].isChainIdSupportable.toString(); expect(isChainIdSupportableBody).toContain('validScopedProperties'); }); @@ -309,7 +333,7 @@ describe('wallet_createSession', () => { { address: '0x3' }, { address: '0x4' }, ]); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:1337': { @@ -369,7 +393,7 @@ describe('wallet_createSession', () => { it('validates and upserts EIP 3085 scoped properties when matching sessionScope is defined', async () => { const { handler, findNetworkClientIdByChainId, addNetwork } = createMockedHandler(); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:1': { @@ -386,6 +410,13 @@ describe('wallet_createSession', () => { supportableScopes: {}, unsupportableScopes: {}, }); + MockHelpers.processScopedProperties.mockReturnValue({ + 'eip155:1': { + eip3085: { + foo: 'bar', + }, + }, + }); await handler({ ...baseRequest, params: { @@ -400,7 +431,7 @@ describe('wallet_createSession', () => { }, }); - expect(validateAndAddEip3085).toHaveBeenCalledWith({ + expect(MockHelpers.validateAndAddEip3085).toHaveBeenCalledWith({ eip3085Params: { foo: 'bar' }, addNetwork, findNetworkClientIdByChainId, @@ -409,7 +440,7 @@ describe('wallet_createSession', () => { it('does not validate and upsert EIP 3085 scoped properties when there is no matching sessionScope', async () => { const { handler } = createMockedHandler(); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:1': { @@ -440,13 +471,13 @@ describe('wallet_createSession', () => { }, }); - expect(validateAndAddEip3085).not.toHaveBeenCalled(); + expect(MockHelpers.validateAndAddEip3085).not.toHaveBeenCalled(); }); it('grants the CAIP-25 permission for the supported scopes and accounts that were approved', async () => { const { handler, grantPermissions, requestPermissionApprovalForOrigin } = createMockedHandler(); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:5': { @@ -528,9 +559,9 @@ describe('wallet_createSession', () => { }); it('emits the dapp viewed metrics event', async () => { - shouldEmitDappViewedEvent.mockReturnValue(true); + MockUtil.shouldEmitDappViewedEvent.mockReturnValue(true); const { handler, sendMetrics } = createMockedHandler(); - bucketScopes.mockReturnValue({ + MockMultichain.bucketScopes.mockReturnValue({ supportedScopes: {}, supportableScopes: {}, unsupportableScopes: {}, @@ -554,7 +585,7 @@ describe('wallet_createSession', () => { it('returns the session ID, properties, and merged scopes', async () => { const { handler, requestPermissionApprovalForOrigin, response } = createMockedHandler(); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:5': { @@ -615,12 +646,13 @@ describe('wallet_createSession', () => { it('reverts any upserted network clients if the request fails', async () => { const { handler, removeNetwork, grantPermissions } = createMockedHandler(); - bucketScopes + MockMultichain.bucketScopes .mockReturnValueOnce({ supportedScopes: { 'eip155:1': { methods: [], notifications: [], + accounts: [], }, }, supportableScopes: {}, @@ -631,14 +663,14 @@ describe('wallet_createSession', () => { supportableScopes: {}, unsupportableScopes: {}, }); - processScopedProperties.mockReturnValue({ + MockHelpers.processScopedProperties.mockReturnValue({ 'eip155:1': { eip3085: { foo: 'bar', }, }, }); - validateAndAddEip3085.mockReturnValue('0xdeadbeef'); + MockHelpers.validateAndAddEip3085.mockResolvedValue('0xdeadbeef'); grantPermissions.mockImplementation(() => { throw new Error('failed to grant permission'); }); diff --git a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.js b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.ts similarity index 61% rename from app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.js rename to app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.ts index e080be148f92..8a36fd775034 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/handler.ts @@ -9,45 +9,128 @@ import { mergeScopes, bucketScopes, validateAndNormalizeScopes, + ScopesObject, + Caip25Authorization, + ScopeString, + ScopedProperties, } from '@metamask/multichain'; +import { + Caveat, + CaveatSpecificationConstraint, + invalidParams, + PermissionController, + PermissionSpecificationConstraint, + RequestedPermissions, + ValidPermission, +} from '@metamask/permission-controller'; +import { + CaipChainId, + Hex, + isPlainObject, + Json, + JsonRpcRequest, + JsonRpcSuccess, +} from '@metamask/utils'; +import { NetworkController } from '@metamask/network-controller'; +import { + JsonRpcEngineEndCallback, + JsonRpcEngineNextCallback, +} from '@metamask/json-rpc-engine'; import { PermissionNames } from '../../../../controllers/permissions'; import { MetaMetricsEventCategory, MetaMetricsEventName, + MetaMetricsEventOptions, + MetaMetricsEventPayload, } from '../../../../../../shared/constants/metametrics'; import { shouldEmitDappViewedEvent } from '../../../util'; import { CaveatTypes } from '../../../../../../shared/constants/permissions'; import { MESSAGE_TYPE } from '../../../../../../shared/constants/app'; import { processScopedProperties, validateAndAddEip3085 } from './helpers'; -export async function walletCreateSessionHandler(req, res, _next, end, hooks) { +type AbstractPermissionController = PermissionController< + PermissionSpecificationConstraint, + CaveatSpecificationConstraint +>; + +/** + * Handler for the `wallet_createSession` RPC method. + * + * @param req - The request object. + * @param res - The response object. + * @param _next - The next middleware function. + * @param end - The end function. + * @param hooks - The hooks object. + * @param hooks.listAccounts + * @param hooks.removeNetwork + * @param hooks.addNetwork + * @param hooks.findNetworkClientIdByChainId + * @param hooks.requestPermissionApprovalForOrigin + * @param hooks.sendMetrics + * @param hooks.metamaskState + * @param hooks.metamaskState.metaMetricsId + * @param hooks.metamaskState.permissionHistory + * @param hooks.metamaskState.accounts + * @param hooks.grantPermissions + */ +async function walletCreateSessionHandler( + req: JsonRpcRequest & { origin: string }, + res: JsonRpcSuccess<{ + sessionScopes: ScopesObject; + sessionProperties?: Record; + }>, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + hooks: { + listAccounts: () => { address: string }[]; + removeNetwork: NetworkController['removeNetwork']; + addNetwork: NetworkController['addNetwork']; + findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + requestPermissionApprovalForOrigin: ( + requestedPermissions: RequestedPermissions, + ) => Promise<{ approvedAccounts: Hex[]; approvedChainIds: Hex[] }>; + sendMetrics: ( + payload: MetaMetricsEventPayload, + options?: MetaMetricsEventOptions, + ) => void; + metamaskState: { + metaMetricsId: string; + permissionHistory: Record; + accounts: Record; + }; + grantPermissions: ( + ...args: Parameters + ) => Record>>; + }, +) { + const { origin } = req; + if (!isPlainObject(req.params)) { + return end(invalidParams({ data: { request: req } })); + } const { - origin, - params: { - requiredScopes, - optionalScopes, - sessionProperties, - scopedProperties, - }, - } = req; + requiredScopes, + optionalScopes, + sessionProperties, + scopedProperties, + } = req.params; if (sessionProperties && Object.keys(sessionProperties).length === 0) { return end(new JsonRpcError(5302, 'Invalid sessionProperties requested')); } - const chainIdsForNetworksAdded = []; + const chainIdsForNetworksAdded: Hex[] = []; try { const { normalizedRequiredScopes, normalizedOptionalScopes } = - validateAndNormalizeScopes(requiredScopes, optionalScopes); + validateAndNormalizeScopes(requiredScopes || {}, optionalScopes || {}); const validScopedProperties = processScopedProperties( normalizedRequiredScopes, normalizedOptionalScopes, - scopedProperties, + scopedProperties as ScopedProperties, ); - const existsNetworkClientForChainId = (chainId) => { + const existsNetworkClientForChainId = (chainId: Hex) => { try { hooks.findNetworkClientIdByChainId(chainId); return true; @@ -56,9 +139,9 @@ export async function walletCreateSessionHandler(req, res, _next, end, hooks) { } }; - const existsEip3085ForChainId = (chainId) => { - const scopeString = `eip155:${parseInt(chainId, 16)}`; - return validScopedProperties?.[scopeString]?.eip3085; + const existsEip3085ForChainId = (chainId: Hex) => { + const scopeString: CaipChainId = `eip155:${parseInt(chainId, 16)}`; + return Boolean(validScopedProperties?.[scopeString]?.eip3085); }; const { @@ -146,22 +229,24 @@ export async function walletCreateSessionHandler(req, res, _next, end, hooks) { ); await Promise.all( - Object.keys(scopedProperties || {}).map(async (scopeString) => { - const scope = sessionScopes[scopeString]; - if (!scope) { - return; - } - - const chainId = await validateAndAddEip3085({ - eip3085Params: scopedProperties[scopeString].eip3085, - addNetwork: hooks.addNetwork, - findNetworkClientIdByChainId: hooks.findNetworkClientIdByChainId, - }); - - if (chainId) { - chainIdsForNetworksAdded.push(chainId); - } - }), + Object.entries(validScopedProperties).map( + async ([scopeString, scopedProperty]) => { + const scope = sessionScopes[scopeString as ScopeString]; + if (!scope) { + return; + } + + const chainId = await validateAndAddEip3085({ + eip3085Params: scopedProperty.eip3085, + addNetwork: hooks.addNetwork, + findNetworkClientIdByChainId: hooks.findNetworkClientIdByChainId, + }); + + if (chainId) { + chainIdsForNetworksAdded.push(chainId); + } + }, + ), ); hooks.grantPermissions({ diff --git a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/helpers.ts b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/helpers.ts index fa88aeb1c286..10d780c4b110 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/helpers.ts +++ b/app/scripts/lib/rpc-method-middleware/handlers/wallet-createSession/helpers.ts @@ -105,7 +105,7 @@ export const processScopedProperties = ( if (!scope) { continue; } - validScopedProperties[scopeString] = {}; + validScopedProperties[scopeString as CaipChainId] = {}; if (scopedProperty.eip3085) { try { @@ -113,7 +113,8 @@ export const processScopedProperties = ( scopeString, scopedProperty.eip3085, ); - validScopedProperties[scopeString].eip3085 = scopedProperty.eip3085; + validScopedProperties[scopeString as CaipChainId].eip3085 = + scopedProperty.eip3085; } catch (err) { // noop } diff --git a/package.json b/package.json index fd094b24ebcc..1baac817e7f4 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "attributions:generate": "./development/generate-attributions.sh" }, "resolutions": { - "@metamask/controller-utils": "npm:@metamask-previews/controller-utils@11.3.0-preview-257618a8", + "@metamask/controller-utils": "npm:@metamask-previews/controller-utils@11.3.0-preview-e34032be", "chokidar": "^3.6.0", "gridplus-sdk": "~2.6.0", "gridplus-sdk/secp256k1": "^5.0.1", @@ -345,7 +345,7 @@ "@metamask/message-manager": "^10.1.0", "@metamask/message-signing-snap": "^0.4.0", "@metamask/metamask-eth-abis": "^3.1.1", - "@metamask/multichain": "npm:@metamask-previews/multichain@0.0.0-preview-257618a8", + "@metamask/multichain": "npm:@metamask-previews/multichain@0.0.0-preview-e34032be", "@metamask/name-controller": "^8.0.0", "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch", "@metamask/notification-controller": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index e0a7b0cf95be..772e0a2ab29c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4987,9 +4987,9 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:@metamask-previews/controller-utils@11.3.0-preview-257618a8": - version: 11.3.0-preview-257618a8 - resolution: "@metamask-previews/controller-utils@npm:11.3.0-preview-257618a8" +"@metamask/controller-utils@npm:@metamask-previews/controller-utils@11.3.0-preview-e34032be": + version: 11.3.0-preview-e34032be + resolution: "@metamask-previews/controller-utils@npm:11.3.0-preview-e34032be" dependencies: "@ethereumjs/util": "npm:^8.1.0" "@metamask/eth-query": "npm:^4.0.0" @@ -5000,7 +5000,7 @@ __metadata: bn.js: "npm:^5.2.1" eth-ens-namehash: "npm:^2.0.8" fast-deep-equal: "npm:^3.1.3" - checksum: 10/e3218a2def8cccec501bce3f307130f7dd459082980e44d3d8a400a4de20a0d68bb93fb10a58571a72440b763077b37e31de6466c9b2efeb1dd78f3f6f005ab3 + checksum: 10/dda56ae95951e049b848a2378253bae9d936b7a9c665abeb2ef45a17060364f04b3ef1e1820d7934597472a406ec61aaf82169dce1a7932efb3e9510fac76053 languageName: node linkType: hard @@ -5718,9 +5718,9 @@ __metadata: languageName: node linkType: hard -"@metamask/multichain@npm:@metamask-previews/multichain@0.0.0-preview-257618a8": - version: 0.0.0-preview-257618a8 - resolution: "@metamask-previews/multichain@npm:0.0.0-preview-257618a8" +"@metamask/multichain@npm:@metamask-previews/multichain@0.0.0-preview-e34032be": + version: 0.0.0-preview-e34032be + resolution: "@metamask-previews/multichain@npm:0.0.0-preview-e34032be" dependencies: "@metamask/api-specs": "npm:^0.10.12" "@metamask/controller-utils": "npm:^11.3.0" @@ -5734,7 +5734,7 @@ __metadata: peerDependencies: "@metamask/network-controller": ^21.0.0 "@metamask/permission-controller": ^11.0.0 - checksum: 10/98fa07065e92d5b4a689fd2c0ff93adfdb380232bfba46e74a388e512e3bb6aad787fc9fec65d48a8faf50537cc9d11236f8d62bae5b262d98cea7f5b494c440 + checksum: 10/29cd358bb9c5bd822962b5c1980ac696d720e0f7520506c6580e36f75a8a9582baacb0fc571e6850c57c38d45c0bd75c5ceec12c15fbc6e30f9a4c61aa53784a languageName: node linkType: hard @@ -26030,7 +26030,7 @@ __metadata: "@metamask/message-manager": "npm:^10.1.0" "@metamask/message-signing-snap": "npm:^0.4.0" "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/multichain": "npm:@metamask-previews/multichain@0.0.0-preview-257618a8" + "@metamask/multichain": "npm:@metamask-previews/multichain@0.0.0-preview-e34032be" "@metamask/name-controller": "npm:^8.0.0" "@metamask/network-controller": "patch:@metamask/network-controller@npm%3A21.0.0#~/.yarn/patches/@metamask-network-controller-npm-21.0.0-559aa8e395.patch" "@metamask/notification-controller": "npm:^6.0.0"