diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 2f924eab481..41739135d28 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for `wallet_getSupportedExecutionPermissions` and `wallet_getGrantedExecutionPermissions` RPC methods ([#7603](https://github.com/MetaMask/core/pull/7603)) + ### Changed - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511)) diff --git a/packages/eth-json-rpc-middleware/src/index.test.ts b/packages/eth-json-rpc-middleware/src/index.test.ts index fdb1730d485..8dac22d37d4 100644 --- a/packages/eth-json-rpc-middleware/src/index.test.ts +++ b/packages/eth-json-rpc-middleware/src/index.test.ts @@ -4,6 +4,288 @@ describe('index module', () => { it('has expected JavaScript exports', () => { expect(indexModule).toMatchInlineSnapshot(` Object { + "GetGrantedExecutionPermissionsResultStruct": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "chainId": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "context": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "delegationManager": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "dependencies": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "factory": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "factoryData": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "type": "array", + "validator": [Function], + }, + "from": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "permission": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "data": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "record", + "validator": [Function], + }, + "isAdjustmentAllowed": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "boolean", + "validator": [Function], + }, + "type": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "to": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "type": "array", + "validator": [Function], + }, + "GetSupportedExecutionPermissionsResultStruct": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "record", + "validator": [Function], + }, + "GrantedExecutionPermissionStruct": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "chainId": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "context": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "delegationManager": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "dependencies": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "factory": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "factoryData": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "type": "array", + "validator": [Function], + }, + "from": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "permission": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "data": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "record", + "validator": [Function], + }, + "isAdjustmentAllowed": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "boolean", + "validator": [Function], + }, + "type": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "to": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, + "SupportedExecutionPermissionConfigStruct": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Object { + "chainIds": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "type": "array", + "validator": [Function], + }, + "ruleTypes": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": Struct { + "coercer": [Function], + "entries": [Function], + "refiner": [Function], + "schema": null, + "type": "string", + "validator": [Function], + }, + "type": "array", + "validator": [Function], + }, + }, + "type": "object", + "validator": [Function], + }, "createBlockCacheMiddleware": [Function], "createBlockRefMiddleware": [Function], "createBlockRefRewriteMiddleware": [Function], diff --git a/packages/eth-json-rpc-middleware/src/index.ts b/packages/eth-json-rpc-middleware/src/index.ts index d729ba2fd1c..8efff9e55d6 100644 --- a/packages/eth-json-rpc-middleware/src/index.ts +++ b/packages/eth-json-rpc-middleware/src/index.ts @@ -5,6 +5,7 @@ export * from './block-tracker-inspector'; export { createFetchMiddleware } from './fetch'; export * from './inflight-cache'; export type { + PermissionDependency, RequestExecutionPermissionsRequestParams, RequestExecutionPermissionsResult, ProcessRequestExecutionPermissionsHook, @@ -14,6 +15,24 @@ export type { RevokeExecutionPermissionRequestParams, RevokeExecutionPermissionResult, } from './methods/wallet-revoke-execution-permission'; +export type { + GrantedExecutionPermission, + GetGrantedExecutionPermissionsResult, + ProcessGetGrantedExecutionPermissionsHook, +} from './methods/wallet-get-granted-execution-permissions'; +export { + GrantedExecutionPermissionStruct, + GetGrantedExecutionPermissionsResultStruct, +} from './methods/wallet-get-granted-execution-permissions'; +export type { + SupportedExecutionPermissionConfig, + GetSupportedExecutionPermissionsResult, + ProcessGetSupportedExecutionPermissionsHook, +} from './methods/wallet-get-supported-execution-permissions'; +export { + SupportedExecutionPermissionConfigStruct, + GetSupportedExecutionPermissionsResultStruct, +} from './methods/wallet-get-supported-execution-permissions'; export * from './providerAsMiddleware'; export * from './retryOnEmpty'; export * from './wallet'; diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.test.ts new file mode 100644 index 00000000000..6444b5f7bbd --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.test.ts @@ -0,0 +1,108 @@ +import type { Hex, Json, JsonRpcRequest } from '@metamask/utils'; + +import type { + GetGrantedExecutionPermissionsResult, + ProcessGetGrantedExecutionPermissionsHook, +} from './wallet-get-granted-execution-permissions'; +import { createWalletGetGrantedExecutionPermissionsHandler } from './wallet-get-granted-execution-permissions'; +import type { WalletMiddlewareParams } from '../wallet'; + +const RESULT_MOCK: GetGrantedExecutionPermissionsResult = [ + { + chainId: '0x01' as Hex, + from: '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4' as Hex, + to: '0x016562aA41A8697720ce0943F003141f5dEAe006' as Hex, + permission: { + type: 'native-token-allowance', + isAdjustmentAllowed: true, + data: { + allowance: '0x1DCD65000000', + }, + }, + context: + '0x016562aA41A8697720ce0943F003141f5dEAe0060000771577157715' as Hex, + dependencies: [ + { + factory: '0x1234567890123456789012345678901234567890' as Hex, + factoryData: '0xabcdef' as Hex, + }, + ], + delegationManager: '0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2' as Hex, + }, +]; + +const REQUEST_MOCK = { + params: [], +} as unknown as JsonRpcRequest; + +describe('wallet_getGrantedExecutionPermissions', () => { + let request: JsonRpcRequest; + let processGetGrantedExecutionPermissionsMock: jest.MockedFunction; + let context: WalletMiddlewareParams['context']; + + const callMethod = async (): Promise | undefined> => { + const handler = createWalletGetGrantedExecutionPermissionsHandler({ + processGetGrantedExecutionPermissions: + processGetGrantedExecutionPermissionsMock, + }); + return handler({ request, context } as WalletMiddlewareParams); + }; + + beforeEach(() => { + jest.resetAllMocks(); + + request = { ...REQUEST_MOCK }; + + context = new Map([ + ['origin', 'test-origin'], + ]) as WalletMiddlewareParams['context']; + + processGetGrantedExecutionPermissionsMock = jest.fn(); + processGetGrantedExecutionPermissionsMock.mockResolvedValue(RESULT_MOCK); + }); + + it('calls hook', async () => { + await callMethod(); + expect(processGetGrantedExecutionPermissionsMock).toHaveBeenCalledWith( + request, + context, + ); + }); + + it('returns result from hook', async () => { + const result = await callMethod(); + expect(result).toStrictEqual(RESULT_MOCK); + }); + + it('throws if no hook', async () => { + await expect( + createWalletGetGrantedExecutionPermissionsHandler({})({ + request, + } as WalletMiddlewareParams), + ).rejects.toThrow( + 'wallet_getGrantedExecutionPermissions - no middleware configured', + ); + }); + + describe('params validation', () => { + it.each([ + ['undefined', undefined], + ['empty array', []], + ['empty object', {}], + ])('accepts params as %s', async (_description, params) => { + request = { ...REQUEST_MOCK, params } as unknown as JsonRpcRequest; + expect(await callMethod()).toStrictEqual(RESULT_MOCK); + }); + + it.each([ + ['non-empty array', [1]], + ['non-empty object', { foo: 'bar' }], + ['string', 'invalid'], + ['number', 123], + ['null', null], + ])('rejects invalid params: %s', async (_description, params) => { + request = { ...REQUEST_MOCK, params } as unknown as JsonRpcRequest; + await expect(callMethod()).rejects.toThrow(/Invalid params/u); + }); + }); +}); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.ts new file mode 100644 index 00000000000..d58259d8de9 --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-granted-execution-permissions.ts @@ -0,0 +1,107 @@ +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { Infer } from '@metamask/superstruct'; +import { + array, + boolean, + object, + record, + string, + unknown, +} from '@metamask/superstruct'; +import { HexChecksumAddressStruct, StrictHexStruct } from '@metamask/utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; + +import { NoParamsStruct } from '../utils/structs'; +import { validateParams } from '../utils/validation'; +import type { WalletMiddlewareContext } from '../wallet'; + +/** + * Superstruct schema for the `wallet_getGrantedExecutionPermissions` request params. + * + * This method expects no parameters. Different JSON-RPC clients may send "no params" + * in different ways (omitted, empty array, or empty object), so we accept all three. + */ +export const GetGrantedExecutionPermissionsParamsStruct = NoParamsStruct; + +const DependencyStruct = object({ + factory: StrictHexStruct, + factoryData: StrictHexStruct, +}); + +const PermissionStruct = object({ + type: string(), + isAdjustmentAllowed: boolean(), + data: record(string(), unknown()), +}); + +/** + * Superstruct schema for a single granted execution permission. + */ +export const GrantedExecutionPermissionStruct = object({ + chainId: StrictHexStruct, + from: HexChecksumAddressStruct, + to: HexChecksumAddressStruct, + permission: PermissionStruct, + context: StrictHexStruct, + dependencies: array(DependencyStruct), + delegationManager: HexChecksumAddressStruct, +}); + +/** + * Represents a single granted execution permission. + */ +export type GrantedExecutionPermission = Infer< + typeof GrantedExecutionPermissionStruct +>; + +/** + * Superstruct schema for the `wallet_getGrantedExecutionPermissions` result. + */ +export const GetGrantedExecutionPermissionsResultStruct = array( + GrantedExecutionPermissionStruct, +); + +/** + * Result type for the `wallet_getGrantedExecutionPermissions` JSON-RPC method. + * Returns an array of all granted permissions that are not yet revoked. + */ +export type GetGrantedExecutionPermissionsResult = Json & + Infer; + +/** + * Hook type for processing the `wallet_getGrantedExecutionPermissions` request. + */ +export type ProcessGetGrantedExecutionPermissionsHook = ( + req: JsonRpcRequest, + context: WalletMiddlewareContext, +) => Promise; + +/** + * Creates a handler for the `wallet_getGrantedExecutionPermissions` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processGetGrantedExecutionPermissions - The function to process the + * get granted execution permissions request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_getGrantedExecutionPermissions` JSON-RPC method. + */ +export function createWalletGetGrantedExecutionPermissionsHandler({ + processGetGrantedExecutionPermissions, +}: { + processGetGrantedExecutionPermissions?: ProcessGetGrantedExecutionPermissionsHook; +}): JsonRpcMiddleware { + return async ({ request, context }) => { + if (!processGetGrantedExecutionPermissions) { + throw rpcErrors.methodNotSupported( + 'wallet_getGrantedExecutionPermissions - no middleware configured', + ); + } + + const { params } = request; + + validateParams(params, GetGrantedExecutionPermissionsParamsStruct); + + return await processGetGrantedExecutionPermissions(request, context); + }; +} diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts new file mode 100644 index 00000000000..6f74c162ebc --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.test.ts @@ -0,0 +1,99 @@ +import type { Hex, Json, JsonRpcRequest } from '@metamask/utils'; + +import type { + GetSupportedExecutionPermissionsResult, + ProcessGetSupportedExecutionPermissionsHook, +} from './wallet-get-supported-execution-permissions'; +import { createWalletGetSupportedExecutionPermissionsHandler } from './wallet-get-supported-execution-permissions'; +import type { WalletMiddlewareParams } from '../wallet'; + +const RESULT_MOCK: GetSupportedExecutionPermissionsResult = { + 'native-token-allowance': { + chainIds: ['0x123', '0x345'] as Hex[], + ruleTypes: ['expiry'], + }, + 'erc20-token-allowance': { + chainIds: ['0x123'] as Hex[], + ruleTypes: [], + }, + 'erc721-token-allowance': { + chainIds: ['0x123'] as Hex[], + ruleTypes: ['expiry'], + }, +}; + +const REQUEST_MOCK = { + params: [], +} as unknown as JsonRpcRequest; + +describe('wallet_getSupportedExecutionPermissions', () => { + let request: JsonRpcRequest; + let processGetSupportedExecutionPermissionsMock: jest.MockedFunction; + let context: WalletMiddlewareParams['context']; + + const callMethod = async (): Promise | undefined> => { + const handler = createWalletGetSupportedExecutionPermissionsHandler({ + processGetSupportedExecutionPermissions: + processGetSupportedExecutionPermissionsMock, + }); + return handler({ request, context } as WalletMiddlewareParams); + }; + + beforeEach(() => { + jest.resetAllMocks(); + + request = { ...REQUEST_MOCK }; + + context = new Map([ + ['origin', 'test-origin'], + ]) as WalletMiddlewareParams['context']; + + processGetSupportedExecutionPermissionsMock = jest.fn(); + processGetSupportedExecutionPermissionsMock.mockResolvedValue(RESULT_MOCK); + }); + + it('calls hook', async () => { + await callMethod(); + expect(processGetSupportedExecutionPermissionsMock).toHaveBeenCalledWith( + request, + context, + ); + }); + + it('returns result from hook', async () => { + const result = await callMethod(); + expect(result).toStrictEqual(RESULT_MOCK); + }); + + it('throws if no hook', async () => { + await expect( + createWalletGetSupportedExecutionPermissionsHandler({})({ + request, + } as WalletMiddlewareParams), + ).rejects.toThrow( + 'wallet_getSupportedExecutionPermissions - no middleware configured', + ); + }); + + describe('params validation', () => { + it.each([ + ['undefined', undefined], + ['empty array', []], + ['empty object', {}], + ])('accepts params as %s', async (_description, params) => { + request = { ...REQUEST_MOCK, params } as unknown as JsonRpcRequest; + expect(await callMethod()).toStrictEqual(RESULT_MOCK); + }); + + it.each([ + ['non-empty array', [1]], + ['non-empty object', { foo: 'bar' }], + ['string', 'invalid'], + ['number', 123], + ['null', null], + ])('rejects invalid params: %s', async (_description, params) => { + request = { ...REQUEST_MOCK, params } as unknown as JsonRpcRequest; + await expect(callMethod()).rejects.toThrow(/Invalid params/u); + }); + }); +}); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.ts new file mode 100644 index 00000000000..120f947aae1 --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-get-supported-execution-permissions.ts @@ -0,0 +1,85 @@ +import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { Infer } from '@metamask/superstruct'; +import { array, object, record, string } from '@metamask/superstruct'; +import { StrictHexStruct } from '@metamask/utils'; +import type { Json, JsonRpcRequest } from '@metamask/utils'; + +import { NoParamsStruct } from '../utils/structs'; +import { validateParams } from '../utils/validation'; +import type { WalletMiddlewareContext } from '../wallet'; + +/** + * Superstruct schema for the `wallet_getSupportedExecutionPermissions` request params. + * + * This method expects no parameters. Different JSON-RPC clients may send "no params" + * in different ways (omitted, empty array, or empty object), so we accept all three. + */ +export const GetSupportedExecutionPermissionsParamsStruct = NoParamsStruct; + +/** + * Superstruct schema for a supported permission type configuration. + */ +export const SupportedExecutionPermissionConfigStruct = object({ + chainIds: array(StrictHexStruct), + ruleTypes: array(string()), +}); + +/** + * Represents the supported configuration for a permission type. + */ +export type SupportedExecutionPermissionConfig = Infer< + typeof SupportedExecutionPermissionConfigStruct +>; + +/** + * Superstruct schema for the `wallet_getSupportedExecutionPermissions` result. + */ +export const GetSupportedExecutionPermissionsResultStruct = record( + string(), + SupportedExecutionPermissionConfigStruct, +); + +/** + * Result type for the `wallet_getSupportedExecutionPermissions` JSON-RPC method. + * Returns an object keyed on supported permission types with their configurations. + */ +export type GetSupportedExecutionPermissionsResult = Json & + Infer; + +/** + * Hook type for processing the `wallet_getSupportedExecutionPermissions` request. + */ +export type ProcessGetSupportedExecutionPermissionsHook = ( + req: JsonRpcRequest, + context: WalletMiddlewareContext, +) => Promise; + +/** + * Creates a handler for the `wallet_getSupportedExecutionPermissions` JSON-RPC method. + * + * @param options - The options for the handler. + * @param options.processGetSupportedExecutionPermissions - The function to process the + * get supported execution permissions request. + * @returns A JSON-RPC middleware function that handles the + * `wallet_getSupportedExecutionPermissions` JSON-RPC method. + */ +export function createWalletGetSupportedExecutionPermissionsHandler({ + processGetSupportedExecutionPermissions, +}: { + processGetSupportedExecutionPermissions?: ProcessGetSupportedExecutionPermissionsHook; +}): JsonRpcMiddleware { + return async ({ request, context }) => { + if (!processGetSupportedExecutionPermissions) { + throw rpcErrors.methodNotSupported( + 'wallet_getSupportedExecutionPermissions - no middleware configured', + ); + } + + const { params } = request; + + validateParams(params, GetSupportedExecutionPermissionsParamsStruct); + + return await processGetSupportedExecutionPermissions(request, context); + }; +} diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts index 430cbff7136..a2127612276 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.test.ts @@ -9,21 +9,20 @@ import type { import { createWalletRequestExecutionPermissionsHandler } from './wallet-request-execution-permissions'; import type { WalletMiddlewareParams } from '../wallet'; -const ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123a'; +const FROM_ADDRESS_MOCK = '0x123abc123abc123abc123abc123abc123abc123A'; +const TO_ADDRESS_MOCK = '0x016562aA41A8697720ce0943F003141f5dEAe006'; const CHAIN_ID_MOCK = '0x1'; const CONTEXT_MOCK = '0x123abc'; +const DELEGATION_MANAGER_MOCK = '0xabc123abc123abc123abc123abc123abc123abc1'; +const FACTORY_MOCK = '0xdef456def456def456def456def456def456def4'; +const FACTORY_DATA_MOCK = '0x1234'; const REQUEST_MOCK = { params: [ { chainId: CHAIN_ID_MOCK, - address: ADDRESS_MOCK, - signer: { - type: 'account', - data: { - address: ADDRESS_MOCK, - }, - }, + from: FROM_ADDRESS_MOCK, + to: TO_ADDRESS_MOCK, permission: { type: 'test-permission', isAdjustmentAllowed: true, @@ -32,7 +31,6 @@ const REQUEST_MOCK = { rules: [ { type: 'test-rule', - isAdjustmentAllowed: false, data: { ruleKey: 'ruleValue' }, }, ], @@ -43,11 +41,8 @@ const REQUEST_MOCK = { const RESULT_MOCK: RequestExecutionPermissionsResult = [ { chainId: CHAIN_ID_MOCK, - address: ADDRESS_MOCK, - signer: { - type: 'account', - data: { address: ADDRESS_MOCK }, - }, + from: FROM_ADDRESS_MOCK, + to: TO_ADDRESS_MOCK, permission: { type: 'test-permission', isAdjustmentAllowed: true, @@ -56,11 +51,17 @@ const RESULT_MOCK: RequestExecutionPermissionsResult = [ rules: [ { type: 'test-rule', - isAdjustmentAllowed: false, data: { ruleKey: 'ruleValue' }, }, ], context: CONTEXT_MOCK, + dependencies: [ + { + factory: FACTORY_MOCK, + factoryData: FACTORY_DATA_MOCK, + }, + ], + delegationManager: DELEGATION_MANAGER_MOCK, }, ]; @@ -106,6 +107,18 @@ describe('wallet_requestExecutionPermissions', () => { expect(result).toStrictEqual(RESULT_MOCK); }); + it('supports undefined rules', async () => { + params[0].rules = undefined; + + await callMethod(); + + expect(processRequestExecutionPermissionsMock).toHaveBeenCalledWith( + params, + request, + context, + ); + }); + it('supports null rules', async () => { params[0].rules = null as never; @@ -118,8 +131,8 @@ describe('wallet_requestExecutionPermissions', () => { ); }); - it('supports optional address', async () => { - params[0].address = undefined as never; + it('supports optional from', async () => { + params[0].from = undefined; await callMethod(); @@ -148,7 +161,7 @@ describe('wallet_requestExecutionPermissions', () => { it('throws if missing properties', async () => { params[0].chainId = undefined as never; - params[0].signer = undefined as never; + params[0].to = undefined as never; params[0].permission = undefined as never; await expect(callMethod()).rejects.toThrow('Invalid params'); @@ -156,13 +169,9 @@ describe('wallet_requestExecutionPermissions', () => { it('throws if wrong types', async () => { params[0].chainId = 123 as never; - params[0].address = 123 as never; + params[0].from = 123 as never; + params[0].to = 123 as never; params[0].permission = '123' as never; - params[0].signer = { - // Make signer an object but invalid to ensure object-type error messages are stable - type: 123 as never, - data: '123' as never, - } as never; params[0].rules = [{} as never]; await expect(callMethod()).rejects.toThrow('Invalid params'); @@ -170,8 +179,8 @@ describe('wallet_requestExecutionPermissions', () => { it('throws if not hex', async () => { params[0].chainId = '123' as never; - params[0].address = '123' as never; - params[0].signer.data.address = '123' as never; + params[0].from = '123' as never; + params[0].to = '123' as never; await expect(callMethod()).rejects.toThrow('Invalid params'); }); diff --git a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts index cb1c0f17f53..dc2ec4027a8 100644 --- a/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts +++ b/packages/eth-json-rpc-middleware/src/methods/wallet-request-execution-permissions.ts @@ -26,21 +26,13 @@ const PermissionStruct = object({ const RuleStruct = object({ type: string(), - isAdjustmentAllowed: boolean(), data: record(string(), unknown()), }); -const AccountSignerStruct = object({ - type: literal('account'), - data: object({ - address: HexChecksumAddressStruct, - }), -}); - const PermissionRequestStruct = object({ chainId: StrictHexStruct, - address: optional(HexChecksumAddressStruct), - signer: AccountSignerStruct, + from: optional(HexChecksumAddressStruct), + to: HexChecksumAddressStruct, permission: PermissionStruct, rules: optional(union([array(RuleStruct), literal(null)])), }); @@ -52,9 +44,16 @@ export type RequestExecutionPermissionsRequestParams = Infer< typeof RequestExecutionPermissionsStruct >; +export type PermissionDependency = { + factory: Hex; + factoryData: Hex; +}; + export type RequestExecutionPermissionsResult = Json & (Infer & { context: Hex; + dependencies: PermissionDependency[]; + delegationManager: Hex; })[]; export type ProcessRequestExecutionPermissionsHook = ( diff --git a/packages/eth-json-rpc-middleware/src/utils/structs.ts b/packages/eth-json-rpc-middleware/src/utils/structs.ts new file mode 100644 index 00000000000..dccf4565573 --- /dev/null +++ b/packages/eth-json-rpc-middleware/src/utils/structs.ts @@ -0,0 +1,21 @@ +import { define, object, optional, union } from '@metamask/superstruct'; + +/** + * Superstruct schema for an empty array []. + * Validates that the value is an array with zero elements. + */ +export const EmptyArrayStruct = define<[]>('EmptyArray', (value) => + Array.isArray(value) && value.length === 0 ? true : 'Expected an empty array', +); + +/** + * Superstruct schema for JSON-RPC methods that expect no parameters. + * + * Different JSON-RPC clients may send "no params" in different ways: + * - Omitted entirely (undefined) + * - Empty array [] + * - Empty object {} + * + * This struct accepts all three forms for maximum compatibility. + */ +export const NoParamsStruct = optional(union([object({}), EmptyArrayStruct])); diff --git a/packages/eth-json-rpc-middleware/src/wallet.ts b/packages/eth-json-rpc-middleware/src/wallet.ts index 9f1797f367d..f5893c22c44 100644 --- a/packages/eth-json-rpc-middleware/src/wallet.ts +++ b/packages/eth-json-rpc-middleware/src/wallet.ts @@ -10,6 +10,10 @@ import { rpcErrors } from '@metamask/rpc-errors'; import { isValidHexAddress } from '@metamask/utils'; import type { JsonRpcRequest, Json, Hex } from '@metamask/utils'; +import { createWalletGetGrantedExecutionPermissionsHandler } from './methods/wallet-get-granted-execution-permissions'; +import type { ProcessGetGrantedExecutionPermissionsHook } from './methods/wallet-get-granted-execution-permissions'; +import { createWalletGetSupportedExecutionPermissionsHandler } from './methods/wallet-get-supported-execution-permissions'; +import type { ProcessGetSupportedExecutionPermissionsHook } from './methods/wallet-get-supported-execution-permissions'; import { createWalletRequestExecutionPermissionsHandler } from './methods/wallet-request-execution-permissions'; import type { ProcessRequestExecutionPermissionsHook } from './methods/wallet-request-execution-permissions'; import { createWalletRevokeExecutionPermissionHandler } from './methods/wallet-revoke-execution-permission'; @@ -83,6 +87,8 @@ export type WalletMiddlewareOptions = { ) => Promise; processRequestExecutionPermissions?: ProcessRequestExecutionPermissionsHook; processRevokeExecutionPermission?: ProcessRevokeExecutionPermissionHook; + processGetGrantedExecutionPermissions?: ProcessGetGrantedExecutionPermissionsHook; + processGetSupportedExecutionPermissions?: ProcessGetSupportedExecutionPermissionsHook; }; export type WalletMiddlewareKeyValues = { @@ -117,6 +123,8 @@ export type WalletMiddlewareParams = MiddlewareParams< * @param options.processTypedMessageV4 - The function to process the typed message v4 request. * @param options.processRequestExecutionPermissions - The function to process the request execution permissions request. * @param options.processRevokeExecutionPermission - The function to process the revoke execution permission request. + * @param options.processGetGrantedExecutionPermissions - The function to process the get granted execution permissions request. + * @param options.processGetSupportedExecutionPermissions - The function to process the get supported execution permissions request. * @returns A JSON-RPC middleware that handles wallet-related JSON-RPC methods. */ export function createWalletMiddleware({ @@ -131,6 +139,8 @@ export function createWalletMiddleware({ processTypedMessageV4, processRequestExecutionPermissions, processRevokeExecutionPermission, + processGetGrantedExecutionPermissions, + processGetSupportedExecutionPermissions, }: WalletMiddlewareOptions): JsonRpcMiddleware< JsonRpcRequest, Json, @@ -167,6 +177,14 @@ export function createWalletMiddleware({ createWalletRevokeExecutionPermissionHandler({ processRevokeExecutionPermission, }), + wallet_getGrantedExecutionPermissions: + createWalletGetGrantedExecutionPermissionsHandler({ + processGetGrantedExecutionPermissions, + }), + wallet_getSupportedExecutionPermissions: + createWalletGetSupportedExecutionPermissionsHandler({ + processGetSupportedExecutionPermissions, + }), }); //