From 1f0792449f101c7ef6bef3974835015ece651388 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 11:56:32 +0200 Subject: [PATCH 01/18] feat: add isAuxiliaryFundsSupported hook to getCapabilities --- packages/eip-5792-middleware/src/hooks/getCapabilities.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts index 947c5f7becc..f24b77772d7 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts @@ -25,6 +25,8 @@ export type GetCapabilitiesHooks = { getSendBundleSupportedChains: ( chainIds: Hex[], ) => Promise>; + /** TODO [ffmcgee] */ + isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }; /** @@ -48,6 +50,7 @@ export async function getCapabilities( isAtomicBatchSupported, isRelaySupported, getSendBundleSupportedChains, + isAuxiliaryFundsSupported, } = hooks; let chainIdsNormalized = chainIds?.map( @@ -115,6 +118,10 @@ export async function getCapabilities( status, }; + acc[chainId as Hex].auxiliaryFunds = { + supported: isAuxiliaryFundsSupported(chainId), + }; + return acc; }, alternateGasFeesAcc); } From fcd8531a4655a09292a632b8ffe8bdd31c4d7bfa Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 12:42:06 +0200 Subject: [PATCH 02/18] feat: filter out unsupported hardware wallet accounts --- .../eip-5792-middleware/src/hooks/getCapabilities.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts index f24b77772d7..b1188ab4a11 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts @@ -118,9 +118,12 @@ export async function getCapabilities( status, }; - acc[chainId as Hex].auxiliaryFunds = { - supported: isAuxiliaryFundsSupported(chainId), - }; + // TODO: [ffmcgee] based of ongoing thread, either filter by wallet type as well or not (just use `isSupportedAccount`) + if (isAuxiliaryFundsSupported(chainId) && isSupportedAccount) { + acc[chainId as Hex].auxiliaryFunds = { + supported: true, + }; + } return acc; }, alternateGasFeesAcc); From e85a68545430f86fa5ea47b98199ac8a4ba168ed Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 12:46:04 +0200 Subject: [PATCH 03/18] debug: log --- packages/eip-5792-middleware/src/hooks/getCapabilities.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts index b1188ab4a11..cae765f80f5 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts @@ -119,6 +119,11 @@ export async function getCapabilities( }; // TODO: [ffmcgee] based of ongoing thread, either filter by wallet type as well or not (just use `isSupportedAccount`) + console.log('iterating: ', { + chainId, + isSupportedAccount, + isAuxFundSupported: isAuxiliaryFundsSupported(chainId), + }); if (isAuxiliaryFundsSupported(chainId) && isSupportedAccount) { acc[chainId as Hex].auxiliaryFunds = { supported: true, From 0351f46672aa52ee136eeca6d21ffc2ec47ad9f2 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 16:52:02 +0200 Subject: [PATCH 04/18] feat: add guards for eip-7682 requiredAssets optional param --- packages/eip-5792-middleware/src/constants.ts | 7 + .../src/hooks/getCapabilities.test.ts | 3 + .../src/hooks/getCapabilities.ts | 15 +-- .../src/hooks/processSendCalls.test.ts | 3 + .../src/hooks/processSendCalls.ts | 127 +++++++++++++++++- 5 files changed, 139 insertions(+), 16 deletions(-) diff --git a/packages/eip-5792-middleware/src/constants.ts b/packages/eip-5792-middleware/src/constants.ts index 3ba7e6eb46c..0ad202bbc41 100644 --- a/packages/eip-5792-middleware/src/constants.ts +++ b/packages/eip-5792-middleware/src/constants.ts @@ -19,6 +19,13 @@ export enum EIP5792ErrorCode { RejectedUpgrade = 5750, } +// To be moved to @metamask/rpc-errors in future. +export enum EIP7682ErrorCode { + UnsupportedAsset = 5771, + UnsupportedChain = 5772, + MalformedRequiredAssets = 5773, +} + // wallet_getCallStatus export enum GetCallsStatusCode { PENDING = 100, diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts index 209986829d6..9b0d827562f 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts @@ -44,6 +44,8 @@ describe('EIP-5792', () => { PreferencesControllerGetStateAction['handler'] > = jest.fn(); + const isAuxiliaryFundsSupportedMock: jest.Mock = jest.fn(); + let messenger: EIP5792Messenger; const getCapabilitiesHooks = { @@ -53,6 +55,7 @@ describe('EIP-5792', () => { getIsSmartTransaction: getIsSmartTransactionMock, isRelaySupported: isRelaySupportedMock, getSendBundleSupportedChains: getSendBundleSupportedChainsMock, + isAuxiliaryFundsSupported: isAuxiliaryFundsSupportedMock, }; beforeEach(() => { diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts index cae765f80f5..82fb7776e3b 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts @@ -109,23 +109,18 @@ export async function getCapabilities( } const status = isSupported ? 'supported' : 'ready'; + const hexChainId = chainId as Hex; - if (acc[chainId as Hex] === undefined) { - acc[chainId as Hex] = {}; + if (acc[hexChainId] === undefined) { + acc[hexChainId] = {}; } - acc[chainId as Hex].atomic = { + acc[hexChainId].atomic = { status, }; - // TODO: [ffmcgee] based of ongoing thread, either filter by wallet type as well or not (just use `isSupportedAccount`) - console.log('iterating: ', { - chainId, - isSupportedAccount, - isAuxFundSupported: isAuxiliaryFundsSupported(chainId), - }); if (isAuxiliaryFundsSupported(chainId) && isSupportedAccount) { - acc[chainId as Hex].auxiliaryFunds = { + acc[hexChainId].auxiliaryFunds = { supported: true, }; } diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index 2a8a9147899..c5202dc463a 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -81,6 +81,8 @@ describe('EIP-5792', () => { AccountsControllerGetStateAction['handler'] > = jest.fn(); + const isAuxiliaryFundsSupportedMock: jest.Mock = jest.fn(); + let messenger: EIP5792Messenger; const sendCallsHooks = { @@ -90,6 +92,7 @@ describe('EIP-5792', () => { getDismissSmartAccountSuggestionEnabledMock, isAtomicBatchSupported: isAtomicBatchSupportedMock, validateSecurity: validateSecurityMock, + isAuxiliaryFundsSupported: isAuxiliaryFundsSupportedMock, }; beforeEach(() => { diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index 6dec738d126..a2404d7e780 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -14,6 +14,7 @@ import { parse, v4 as uuid } from 'uuid'; import { EIP5792ErrorCode, + EIP7682ErrorCode, KEYRING_TYPES_SUPPORTING_7702, MessageType, VERSION, @@ -43,6 +44,8 @@ export type ProcessSendCallsHooks = { request: ValidateSecurityRequest, chainId: Hex, ) => Promise; + /** TODO [ffmcgee] */ + isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }; /** @@ -76,6 +79,7 @@ export async function processSendCalls( getDismissSmartAccountSuggestionEnabled, isAtomicBatchSupported, validateSecurity: validateSecurityHook, + isAuxiliaryFundsSupported, } = hooks; const { calls, from: paramFrom } = params; @@ -100,12 +104,14 @@ export async function processSendCalls( addTransaction, chainId, from, + messenger, networkClientId, origin, securityAlertId, sendCalls: params, transactions, validateSecurity, + isAuxiliaryFundsSupported, }); } else { batchId = await processMultipleTransaction({ @@ -121,6 +127,7 @@ export async function processSendCalls( securityAlertId, transactions, validateSecurity, + isAuxiliaryFundsSupported, }); } @@ -134,28 +141,33 @@ export async function processSendCalls( * @param params.addTransaction - Function to add a single transaction. * @param params.chainId - The chain ID for the transaction. * @param params.from - The sender address. + * @param params.messenger - Messenger instance for controller communication. * @param params.networkClientId - The network client ID. * @param params.origin - The origin of the request (optional). * @param params.securityAlertId - The security alert ID for this transaction. * @param params.sendCalls - The original sendCalls request. * @param params.transactions - Array containing the single transaction. * @param params.validateSecurity - Function to validate security for the transaction. + * @param params.isAuxiliaryFundsSupported - TODO [ffmcgee] * @returns Promise resolving to the generated batch ID for the transaction. */ async function processSingleTransaction({ addTransaction, chainId, from, + messenger, networkClientId, origin, securityAlertId, sendCalls, transactions, validateSecurity, + isAuxiliaryFundsSupported, }: { addTransaction: TransactionController['addTransaction']; chainId: Hex; from: Hex; + messenger: EIP5792Messenger; networkClientId: string; origin?: string; securityAlertId: string; @@ -165,8 +177,16 @@ async function processSingleTransaction({ securityRequest: ValidateSecurityRequest, chainId: Hex, ) => void; + isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }) { - validateSingleSendCall(sendCalls, chainId); + const keyringType = getAccountKeyringType(from, messenger); + + validateSingleSendCall( + sendCalls, + chainId, + keyringType, + isAuxiliaryFundsSupported, + ); const txParams = { from, @@ -208,6 +228,7 @@ async function processSingleTransaction({ * @param params.securityAlertId - The security alert ID for this batch. * @param params.transactions - Array of transactions to process. * @param params.validateSecurity - Function to validate security for the transactions. + * @param params.isAuxiliaryFundsSupported - TODO [ffmcgee] * @returns Promise resolving to the generated batch ID for the transaction batch. */ async function processMultipleTransaction({ @@ -223,6 +244,7 @@ async function processMultipleTransaction({ securityAlertId, transactions, validateSecurity, + isAuxiliaryFundsSupported, }: { addTransactionBatch: TransactionController['addTransactionBatch']; isAtomicBatchSupported: TransactionController['isAtomicBatchSupported']; @@ -239,6 +261,7 @@ async function processMultipleTransaction({ securityRequest: ValidateSecurityRequest, chainId: Hex, ) => Promise; + isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }) { const batchSupport = await isAtomicBatchSupported({ address: from, @@ -258,6 +281,7 @@ async function processMultipleTransaction({ dismissSmartAccountSuggestionEnabled, chainBatchSupport, keyringType, + isAuxiliaryFundsSupported, ); const result = await addTransactionBatch({ @@ -287,10 +311,17 @@ function generateBatchId(): Hex { * * @param sendCalls - The sendCalls request to validate. * @param dappChainId - The chain ID that the dApp is connected to. + * @param keyringType - The type of keyring associated with the account. + * @param isAuxiliaryFundsSupported - TODO [ffmcgee] */ -function validateSingleSendCall(sendCalls: SendCallsPayload, dappChainId: Hex) { +function validateSingleSendCall( + sendCalls: SendCallsPayload, + dappChainId: Hex, + keyringType: KeyringTypes, + isAuxiliaryFundsSupported: (chainId: Hex) => boolean, +) { validateSendCallsVersion(sendCalls); - validateCapabilities(sendCalls); + validateCapabilities(sendCalls, keyringType, isAuxiliaryFundsSupported); validateDappChainId(sendCalls, dappChainId); } @@ -302,6 +333,7 @@ function validateSingleSendCall(sendCalls: SendCallsPayload, dappChainId: Hex) { * @param dismissSmartAccountSuggestionEnabled - Whether smart account suggestions are disabled. * @param chainBatchSupport - Information about atomic batch support for the chain. * @param keyringType - The type of keyring associated with the account. + * @param isAuxiliaryFundsSupported - TODO [ffmcgee] */ function validateSendCalls( sendCalls: SendCallsPayload, @@ -309,10 +341,11 @@ function validateSendCalls( dismissSmartAccountSuggestionEnabled: boolean, chainBatchSupport: IsAtomicBatchSupportedResultEntry | undefined, keyringType: KeyringTypes, + isAuxiliaryFundsSupported: (chainId: Hex) => boolean, ) { validateSendCallsVersion(sendCalls); validateSendCallsChainId(sendCalls, dappChainId, chainBatchSupport); - validateCapabilities(sendCalls); + validateCapabilities(sendCalls, keyringType, isAuxiliaryFundsSupported); validateUpgrade( dismissSmartAccountSuggestionEnabled, chainBatchSupport, @@ -382,10 +415,17 @@ function validateSendCallsChainId( * Validates that all required capabilities in the sendCalls request are supported. * * @param sendCalls - The sendCalls request to validate. + * @param keyringType - The type of keyring associated with the account. + * @param isAuxiliaryFundsSupported - TODO [ffmcgee] + * * @throws JsonRpcError if unsupported non-optional capabilities are requested. */ -function validateCapabilities(sendCalls: SendCallsPayload) { - const { calls, capabilities } = sendCalls; +function validateCapabilities( + sendCalls: SendCallsPayload, + keyringType: KeyringTypes, + isAuxiliaryFundsSupported: (chainId: Hex) => boolean, +) { + const { calls, capabilities, chainId } = sendCalls; const requiredTopLevelCapabilities = Object.keys(capabilities ?? {}).filter( (name) => capabilities?.[name].optional !== true, @@ -410,6 +450,81 @@ function validateCapabilities(sendCalls: SendCallsPayload) { )}`, ); } + + if (capabilities?.auxiliaryFunds) { + validateRequiredAssets( + capabilities.auxiliaryFunds, + chainId, + keyringType, + isAuxiliaryFundsSupported, + ); + } +} + +/** + * Validates EIP-7682 optional `requiredAssets` parameter. + * + * docs: {@link https://eips.ethereum.org/EIPS/eip-7682#extended-usage-requiredassets-parameter} + * + * @param auxiliaryFunds - The auxiliaryFunds param to validate. + * @param auxiliaryFunds.optional a + * @param auxiliaryFunds.requiredAssets a + * @param chainId - The chain ID of the incoming request. + * @param keyringType - The type of keyring associated with the account. + * @param isAuxiliaryFundsSupported - TODO [ffmcgee] + * @throws JsonRpcError if the version is not supported. + */ +function validateRequiredAssets( + // TODO: [ffmcgee] object param instead of multiple params + auxiliaryFunds: { + optional?: boolean; + requiredAssets?: { + address: Hex; + amount: Hex; // Amount required, as a hex string representing the integer value in the asset's smallest unit + standard: string; // Token standard + tokenId?: Hex; // Token ID as a hex string (required for ERC-721 and ERC-1155) + }[]; + }, + chainId: Hex, + keyringType: KeyringTypes, + isAuxiliaryFundsSupported: (chainId: Hex) => boolean, +) { + const isSupportedAccount = + KEYRING_TYPES_SUPPORTING_7702.includes(keyringType); + + if (!isSupportedAccount) { + throw new JsonRpcError( + EIP7682ErrorCode.UnsupportedChain, // TODO [ffmcgee] update error code + 'Unsupported account type', + ); + } + + if (!isAuxiliaryFundsSupported(chainId)) { + throw new JsonRpcError( + EIP7682ErrorCode.UnsupportedChain, + `The wallet no longer supports auxiliary funds on the requested chain: ${chainId}`, + ); + } + + if (!auxiliaryFunds?.requiredAssets) { + return; + } + + for (const asset of auxiliaryFunds.requiredAssets) { + if (!['erc20', 'erc721', 'erc1155'].includes(asset.standard)) { + throw new JsonRpcError( + EIP7682ErrorCode.UnsupportedAsset, + `The requested asset ${asset.address} is not available through the wallet’s auxiliary fund system: unsupported token standard ${asset.standard}`, + ); + } + + if (['erc721', 'erc1155'].includes(asset.standard) && !asset.tokenId) { + throw new JsonRpcError( + EIP7682ErrorCode.MalformedRequiredAssets, + `The structure of the requiredAssets object is malformed: token standard ${asset.standard} requires a "tokenId" to be specified`, + ); + } + } } /** From 0b55763a3e1c5a662e2d810eb325030b7afef764 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 17:24:36 +0200 Subject: [PATCH 05/18] test: bump test coverage --- .../src/hooks/getCapabilities.test.ts | 114 +++++++++ .../src/hooks/processSendCalls.test.ts | 233 ++++++++++++++++++ packages/eip-5792-middleware/src/types.ts | 8 + 3 files changed, 355 insertions(+) diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts index 9b0d827562f..7e40b805d64 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.test.ts @@ -428,5 +428,119 @@ describe('EIP-5792', () => { }, }); }); + + it('fetches all network configurations when chainIds is undefined', async () => { + const networkConfigurationsMock = { + '0x1': { chainId: '0x1' }, + '0x89': { chainId: '0x89' }, + }; + + messenger.registerActionHandler( + 'NetworkController:getState', + jest.fn().mockReturnValue({ + networkConfigurationsByChainId: networkConfigurationsMock, + }), + ); + + isAtomicBatchSupportedMock.mockResolvedValueOnce([ + { + chainId: '0x1', + delegationAddress: DELEGATION_ADDRESS_MOCK, + isSupported: true, + }, + { + chainId: '0x89', + delegationAddress: undefined, + isSupported: false, + upgradeContractAddress: DELEGATION_ADDRESS_MOCK, + }, + ]); + + const capabilities = await getCapabilities( + getCapabilitiesHooks, + messenger, + FROM_MOCK, + undefined, + ); + + expect(capabilities).toStrictEqual({ + '0x1': { + atomic: { + status: 'supported', + }, + alternateGasFees: { + supported: true, + }, + }, + '0x89': { + atomic: { + status: 'ready', + }, + }, + }); + }); + + it('includes auxiliary funds capability when supported', async () => { + isAtomicBatchSupportedMock.mockResolvedValueOnce([ + { + chainId: CHAIN_ID_MOCK, + delegationAddress: DELEGATION_ADDRESS_MOCK, + isSupported: true, + }, + ]); + + isAuxiliaryFundsSupportedMock.mockReturnValue(true); + + const capabilities = await getCapabilities( + getCapabilitiesHooks, + messenger, + FROM_MOCK, + [CHAIN_ID_MOCK], + ); + + expect(capabilities).toStrictEqual({ + [CHAIN_ID_MOCK]: { + atomic: { + status: 'supported', + }, + alternateGasFees: { + supported: true, + }, + auxiliaryFunds: { + supported: true, + }, + }, + }); + }); + + it('does not include auxiliary funds capability when not supported', async () => { + isAtomicBatchSupportedMock.mockResolvedValueOnce([ + { + chainId: CHAIN_ID_MOCK, + delegationAddress: DELEGATION_ADDRESS_MOCK, + isSupported: true, + }, + ]); + + isAuxiliaryFundsSupportedMock.mockReturnValue(false); + + const capabilities = await getCapabilities( + getCapabilitiesHooks, + messenger, + FROM_MOCK, + [CHAIN_ID_MOCK], + ); + + expect(capabilities).toStrictEqual({ + [CHAIN_ID_MOCK]: { + atomic: { + status: 'supported', + }, + alternateGasFees: { + supported: true, + }, + }, + }); + }); }); }); diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index c5202dc463a..071bf9e3306 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -127,6 +127,8 @@ describe('EIP-5792', () => { getDismissSmartAccountSuggestionEnabledMock.mockReturnValue(false); + isAuxiliaryFundsSupportedMock.mockReturnValue(true); + isAtomicBatchSupportedMock.mockResolvedValue([ { chainId: CHAIN_ID_MOCK, @@ -435,5 +437,236 @@ describe('EIP-5792', () => { `EIP-7702 upgrade not supported as account type is unknown`, ); }); + + it('validates auxiliary funds with unsupported account type', async () => { + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + from: FROM_MOCK_HARDWARE, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc20', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ), + ).rejects.toThrow('Unsupported account type'); + }); + + it('validates auxiliary funds with unsupported chain', async () => { + isAuxiliaryFundsSupportedMock.mockReturnValue(false); + + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc20', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ), + ).rejects.toThrow( + `The wallet no longer supports auxiliary funds on the requested chain: ${CHAIN_ID_MOCK}`, + ); + }); + + it('validates auxiliary funds with unsupported token standard', async () => { + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc777', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ), + ).rejects.toThrow( + /The requested asset 0x123 is not available through the wallet.*s auxiliary fund system: unsupported token standard erc777/u, + ); + }); + + it('validates auxiliary funds with missing tokenId for ERC-721', async () => { + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc721', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ), + ).rejects.toThrow( + 'The structure of the requiredAssets object is malformed: token standard erc721 requires a "tokenId" to be specified', + ); + }); + + it('validates auxiliary funds with missing tokenId for ERC-1155', async () => { + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc1155', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ), + ).rejects.toThrow( + 'The structure of the requiredAssets object is malformed: token standard erc1155 requires a "tokenId" to be specified', + ); + }); + + it('validates auxiliary funds with valid ERC-20 asset', async () => { + const result = await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc20', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + }); + + it('validates auxiliary funds with valid ERC-721 asset', async () => { + const result = await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc721', + tokenId: '0x1', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + }); + + it('validates auxiliary funds with valid ERC-1155 asset', async () => { + const result = await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123', + amount: '0x1', + standard: 'erc1155', + tokenId: '0x1', + }, + ], + }, + }, + }, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + }); + + it('validates auxiliary funds with no requiredAssets', async () => { + const result = await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + }, + }, + }, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + }); }); }); diff --git a/packages/eip-5792-middleware/src/types.ts b/packages/eip-5792-middleware/src/types.ts index ae73f7d9824..9d9ee7e4845 100644 --- a/packages/eip-5792-middleware/src/types.ts +++ b/packages/eip-5792-middleware/src/types.ts @@ -97,10 +97,18 @@ export const GetCapabilitiesStruct = tuple([ optional(array(StrictHexStruct)), ]); +const RequiredAssetStruct = type({ + address: nonempty(HexChecksumAddressStruct), + amount: nonempty(StrictHexStruct), + standard: nonempty(string()), + tokenId: optional(StrictHexStruct), +}); + export const CapabilitiesStruct = record( string(), type({ optional: optional(boolean()), + requiredAssets: optional(array(RequiredAssetStruct)), }), ); From b06a70835dc67e5ad8991b5734d9637973b7c860 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 16 Sep 2025 17:29:28 +0200 Subject: [PATCH 06/18] docs: update CHANGELOG.md --- packages/eip-5792-middleware/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index cab79bc25de..96214fc58e9 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Add `auxiliaryFunds` + `requiredAssets` support defined under [ERC-7682](https://eips.ethereum.org/EIPS/eip-7682) ([#6623](https://github.com/MetaMask/core/pull/6623)) - Bump `@metamask/transaction-controller` from `^60.2.0` to `^60.3.0` ([#6561](https://github.com/MetaMask/core/pull/6561)) - Bump `@metamask/utils` from `^11.4.2` to `^11.8.0` ([#6588](https://github.com/MetaMask/core/pull/6588)) From 6301d16b7ade1c62e4406aaca280c261d5d4ec13 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 11:36:13 +0200 Subject: [PATCH 07/18] refactor: minor adjustments and documentation update --- .../src/hooks/getCapabilities.ts | 4 +- .../src/hooks/processSendCalls.ts | 77 +++++++++---------- packages/eip-5792-middleware/src/types.ts | 2 + 3 files changed, 42 insertions(+), 41 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts index 82fb7776e3b..4059f6d953a 100644 --- a/packages/eip-5792-middleware/src/hooks/getCapabilities.ts +++ b/packages/eip-5792-middleware/src/hooks/getCapabilities.ts @@ -25,7 +25,7 @@ export type GetCapabilitiesHooks = { getSendBundleSupportedChains: ( chainIds: Hex[], ) => Promise>; - /** TODO [ffmcgee] */ + /** Function to validate if auxiliary funds capability is supported. */ isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }; @@ -119,7 +119,7 @@ export async function getCapabilities( status, }; - if (isAuxiliaryFundsSupported(chainId) && isSupportedAccount) { + if (isSupportedAccount && isAuxiliaryFundsSupported(chainId)) { acc[hexChainId].auxiliaryFunds = { supported: true, }; diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index a2404d7e780..55d86119b92 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -22,6 +22,7 @@ import { import type { EIP5792Messenger, SendCallsPayload, + SendCallsRequiredAssetsParam, SendCallsResult, } from '../types'; import { getAccountKeyringType } from '../utils'; @@ -44,7 +45,7 @@ export type ProcessSendCallsHooks = { request: ValidateSecurityRequest, chainId: Hex, ) => Promise; - /** TODO [ffmcgee] */ + /** Function to validate if auxiliary funds capability is supported. */ isAuxiliaryFundsSupported: (chainId: Hex) => boolean; }; @@ -148,7 +149,7 @@ export async function processSendCalls( * @param params.sendCalls - The original sendCalls request. * @param params.transactions - Array containing the single transaction. * @param params.validateSecurity - Function to validate security for the transaction. - * @param params.isAuxiliaryFundsSupported - TODO [ffmcgee] + * @param params.isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. * @returns Promise resolving to the generated batch ID for the transaction. */ async function processSingleTransaction({ @@ -228,7 +229,7 @@ async function processSingleTransaction({ * @param params.securityAlertId - The security alert ID for this batch. * @param params.transactions - Array of transactions to process. * @param params.validateSecurity - Function to validate security for the transactions. - * @param params.isAuxiliaryFundsSupported - TODO [ffmcgee] + * @param params.isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. * @returns Promise resolving to the generated batch ID for the transaction batch. */ async function processMultipleTransaction({ @@ -312,7 +313,7 @@ function generateBatchId(): Hex { * @param sendCalls - The sendCalls request to validate. * @param dappChainId - The chain ID that the dApp is connected to. * @param keyringType - The type of keyring associated with the account. - * @param isAuxiliaryFundsSupported - TODO [ffmcgee] + * @param isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. */ function validateSingleSendCall( sendCalls: SendCallsPayload, @@ -333,7 +334,7 @@ function validateSingleSendCall( * @param dismissSmartAccountSuggestionEnabled - Whether smart account suggestions are disabled. * @param chainBatchSupport - Information about atomic batch support for the chain. * @param keyringType - The type of keyring associated with the account. - * @param isAuxiliaryFundsSupported - TODO [ffmcgee] + * @param isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. */ function validateSendCalls( sendCalls: SendCallsPayload, @@ -416,7 +417,7 @@ function validateSendCallsChainId( * * @param sendCalls - The sendCalls request to validate. * @param keyringType - The type of keyring associated with the account. - * @param isAuxiliaryFundsSupported - TODO [ffmcgee] + * @param isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. * * @throws JsonRpcError if unsupported non-optional capabilities are requested. */ @@ -452,12 +453,12 @@ function validateCapabilities( } if (capabilities?.auxiliaryFunds) { - validateRequiredAssets( - capabilities.auxiliaryFunds, + validateRequiredAssets({ + auxiliaryFunds: capabilities.auxiliaryFunds, chainId, keyringType, isAuxiliaryFundsSupported, - ); + }); } } @@ -466,36 +467,41 @@ function validateCapabilities( * * docs: {@link https://eips.ethereum.org/EIPS/eip-7682#extended-usage-requiredassets-parameter} * - * @param auxiliaryFunds - The auxiliaryFunds param to validate. - * @param auxiliaryFunds.optional a - * @param auxiliaryFunds.requiredAssets a - * @param chainId - The chain ID of the incoming request. - * @param keyringType - The type of keyring associated with the account. - * @param isAuxiliaryFundsSupported - TODO [ffmcgee] - * @throws JsonRpcError if the version is not supported. + * @param param - The parameter object. + * @param param.auxiliaryFunds - The auxiliaryFunds param to validate. + * @param param.auxiliaryFunds.optional - Metadata to signal for wallets that support this optional capability, while maintaining compatibility with wallets that do not. + * @param param.auxiliaryFunds.requiredAssets -Metadata that enables a wallets support for `auxiliaryFunds` capability. + * @param param.chainId - The chain ID of the incoming request. + * @param param.keyringType - The type of keyring associated with the account. + * @param param.isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. + * @throws JsonRpcError if auxiliary funds capability is not supported. */ -function validateRequiredAssets( - // TODO: [ffmcgee] object param instead of multiple params +function validateRequiredAssets({ + auxiliaryFunds, + chainId, + keyringType, + isAuxiliaryFundsSupported, +}: { auxiliaryFunds: { optional?: boolean; - requiredAssets?: { - address: Hex; - amount: Hex; // Amount required, as a hex string representing the integer value in the asset's smallest unit - standard: string; // Token standard - tokenId?: Hex; // Token ID as a hex string (required for ERC-721 and ERC-1155) - }[]; - }, - chainId: Hex, - keyringType: KeyringTypes, - isAuxiliaryFundsSupported: (chainId: Hex) => boolean, -) { + requiredAssets?: SendCallsRequiredAssetsParam[]; + }; + chainId: Hex; + keyringType: KeyringTypes; + isAuxiliaryFundsSupported: (chainId: Hex) => boolean; +}) { + // If we can make use of that capability then we should, but otherwise we can process the request and ignore the capability + // so if the capability is signaled as optional, no validation is required, so we don't block the transaction from happening. + if (auxiliaryFunds.optional) { + return; + } const isSupportedAccount = KEYRING_TYPES_SUPPORTING_7702.includes(keyringType); if (!isSupportedAccount) { throw new JsonRpcError( - EIP7682ErrorCode.UnsupportedChain, // TODO [ffmcgee] update error code - 'Unsupported account type', + EIP5792ErrorCode.UnsupportedNonOptionalCapability, + 'Unsupported non-optional capabilities: auxiliaryFunds', ); } @@ -511,19 +517,12 @@ function validateRequiredAssets( } for (const asset of auxiliaryFunds.requiredAssets) { - if (!['erc20', 'erc721', 'erc1155'].includes(asset.standard)) { + if (asset.standard !== 'erc20') { throw new JsonRpcError( EIP7682ErrorCode.UnsupportedAsset, `The requested asset ${asset.address} is not available through the wallet’s auxiliary fund system: unsupported token standard ${asset.standard}`, ); } - - if (['erc721', 'erc1155'].includes(asset.standard) && !asset.tokenId) { - throw new JsonRpcError( - EIP7682ErrorCode.MalformedRequiredAssets, - `The structure of the requiredAssets object is malformed: token standard ${asset.standard} requires a "tokenId" to be specified`, - ); - } } } diff --git a/packages/eip-5792-middleware/src/types.ts b/packages/eip-5792-middleware/src/types.ts index 9d9ee7e4845..621a7f03756 100644 --- a/packages/eip-5792-middleware/src/types.ts +++ b/packages/eip-5792-middleware/src/types.ts @@ -77,6 +77,8 @@ export type GetCapabilitiesHook = ( export type SendCallsParams = Infer; export type SendCallsPayload = SendCallsParams[0]; +export type SendCallsRequiredAssetsParam = Infer; + export type SendCallsResult = { id: Hex; capabilities?: Record; From 887747d0fdd1b8a61e815bb6cadfa76cd3e8fdfd Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 11:40:34 +0200 Subject: [PATCH 08/18] test: update processSendCalls.test.ts --- .../src/hooks/processSendCalls.test.ts | 106 ------------------ 1 file changed, 106 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index 071bf9e3306..ad20cc42380 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -520,60 +520,6 @@ describe('EIP-5792', () => { ); }); - it('validates auxiliary funds with missing tokenId for ERC-721', async () => { - await expect( - processSendCalls( - sendCallsHooks, - messenger, - { - ...SEND_CALLS_MOCK, - capabilities: { - auxiliaryFunds: { - optional: true, - requiredAssets: [ - { - address: '0x123', - amount: '0x1', - standard: 'erc721', - }, - ], - }, - }, - }, - REQUEST_MOCK, - ), - ).rejects.toThrow( - 'The structure of the requiredAssets object is malformed: token standard erc721 requires a "tokenId" to be specified', - ); - }); - - it('validates auxiliary funds with missing tokenId for ERC-1155', async () => { - await expect( - processSendCalls( - sendCallsHooks, - messenger, - { - ...SEND_CALLS_MOCK, - capabilities: { - auxiliaryFunds: { - optional: true, - requiredAssets: [ - { - address: '0x123', - amount: '0x1', - standard: 'erc1155', - }, - ], - }, - }, - }, - REQUEST_MOCK, - ), - ).rejects.toThrow( - 'The structure of the requiredAssets object is malformed: token standard erc1155 requires a "tokenId" to be specified', - ); - }); - it('validates auxiliary funds with valid ERC-20 asset', async () => { const result = await processSendCalls( sendCallsHooks, @@ -599,58 +545,6 @@ describe('EIP-5792', () => { expect(result).toBeDefined(); }); - it('validates auxiliary funds with valid ERC-721 asset', async () => { - const result = await processSendCalls( - sendCallsHooks, - messenger, - { - ...SEND_CALLS_MOCK, - capabilities: { - auxiliaryFunds: { - optional: true, - requiredAssets: [ - { - address: '0x123', - amount: '0x1', - standard: 'erc721', - tokenId: '0x1', - }, - ], - }, - }, - }, - REQUEST_MOCK, - ); - - expect(result).toBeDefined(); - }); - - it('validates auxiliary funds with valid ERC-1155 asset', async () => { - const result = await processSendCalls( - sendCallsHooks, - messenger, - { - ...SEND_CALLS_MOCK, - capabilities: { - auxiliaryFunds: { - optional: true, - requiredAssets: [ - { - address: '0x123', - amount: '0x1', - standard: 'erc1155', - tokenId: '0x1', - }, - ], - }, - }, - }, - REQUEST_MOCK, - ); - - expect(result).toBeDefined(); - }); - it('validates auxiliary funds with no requiredAssets', async () => { const result = await processSendCalls( sendCallsHooks, From 7e8f0b794048572aa2da37cebfc4d40f5fd1ca62 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 12:41:23 +0200 Subject: [PATCH 09/18] fix: deduplicate requiredAssets & adjust auxFunds validation --- packages/eip-5792-middleware/package.json | 1 + .../src/hooks/processSendCalls.test.ts | 23 +++++--- .../src/hooks/processSendCalls.ts | 59 +++++++++++++++++-- packages/eip-5792-middleware/src/types.ts | 1 - 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/eip-5792-middleware/package.json b/packages/eip-5792-middleware/package.json index 30c55fb1cb8..0fc9208f7d4 100644 --- a/packages/eip-5792-middleware/package.json +++ b/packages/eip-5792-middleware/package.json @@ -51,6 +51,7 @@ "@metamask/superstruct": "^3.1.0", "@metamask/transaction-controller": "^60.3.0", "@metamask/utils": "^11.8.0", + "lodash": "^4.17.21", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index ad20cc42380..f55d162691a 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -12,7 +12,7 @@ import type { NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; import type { TransactionController } from '@metamask/transaction-controller'; -import type { JsonRpcRequest } from '@metamask/utils'; +import type { Hex, JsonRpcRequest } from '@metamask/utils'; import { processSendCalls } from './processSendCalls'; import type { @@ -448,11 +448,16 @@ describe('EIP-5792', () => { from: FROM_MOCK_HARDWARE, capabilities: { auxiliaryFunds: { - optional: true, + optional: false, requiredAssets: [ { address: '0x123', - amount: '0x1', + amount: '0x2', + standard: 'erc20', + }, + { + address: '0x123', + amount: '0x2', standard: 'erc20', }, ], @@ -461,7 +466,9 @@ describe('EIP-5792', () => { }, REQUEST_MOCK, ), - ).rejects.toThrow('Unsupported account type'); + ).rejects.toThrow( + 'Unsupported non-optional capabilities: auxiliaryFunds', + ); }); it('validates auxiliary funds with unsupported chain', async () => { @@ -475,11 +482,11 @@ describe('EIP-5792', () => { ...SEND_CALLS_MOCK, capabilities: { auxiliaryFunds: { - optional: true, + optional: false, requiredAssets: [ { - address: '0x123', - amount: '0x1', + address: '0x123' as Hex, + amount: '0x1' as Hex, standard: 'erc20', }, ], @@ -502,7 +509,7 @@ describe('EIP-5792', () => { ...SEND_CALLS_MOCK, capabilities: { auxiliaryFunds: { - optional: true, + optional: false, requiredAssets: [ { address: '0x123', diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index 55d86119b92..10639f377d4 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -9,7 +9,8 @@ import type { } from '@metamask/transaction-controller'; import { TransactionEnvelopeType } from '@metamask/transaction-controller'; import type { Hex, JsonRpcRequest } from '@metamask/utils'; -import { bytesToHex } from '@metamask/utils'; +import { add0x, bytesToHex } from '@metamask/utils'; +import { groupBy } from 'lodash'; import { parse, v4 as uuid } from 'uuid'; import { @@ -202,6 +203,8 @@ async function processSingleTransaction({ }; validateSecurity(securityRequest, chainId); + dedupeAuxiliaryFundsRequiredAssets(sendCalls); + const batchId = generateBatchId(); await addTransaction(txParams, { @@ -285,6 +288,16 @@ async function processMultipleTransaction({ isAuxiliaryFundsSupported, ); + console.log( + { assets: sendCalls.capabilities?.auxiliaryFunds?.requiredAssets }, + 'before, multiple tx', + ); + dedupeAuxiliaryFundsRequiredAssets(sendCalls); + console.log( + { assets: sendCalls.capabilities?.auxiliaryFunds?.requiredAssets }, + 'after, multiple tx', + ); + const result = await addTransactionBatch({ from, networkClientId, @@ -429,12 +442,15 @@ function validateCapabilities( const { calls, capabilities, chainId } = sendCalls; const requiredTopLevelCapabilities = Object.keys(capabilities ?? {}).filter( - (name) => capabilities?.[name].optional !== true, + (name) => + name !== 'auxiliaryFunds' && capabilities?.[name].optional !== true, ); const requiredCallCapabilities = calls.flatMap((call) => Object.keys(call.capabilities ?? {}).filter( - (name) => call.capabilities?.[name].optional !== true, + (name) => + name !== 'auxiliaryFunds' && + call.capabilities?.[name].optional !== true, ), ); @@ -470,7 +486,7 @@ function validateCapabilities( * @param param - The parameter object. * @param param.auxiliaryFunds - The auxiliaryFunds param to validate. * @param param.auxiliaryFunds.optional - Metadata to signal for wallets that support this optional capability, while maintaining compatibility with wallets that do not. - * @param param.auxiliaryFunds.requiredAssets -Metadata that enables a wallets support for `auxiliaryFunds` capability. + * @param param.auxiliaryFunds.requiredAssets - Metadata that enables a wallets support for `auxiliaryFunds` capability. * @param param.chainId - The chain ID of the incoming request. * @param param.keyringType - The type of keyring associated with the account. * @param param.isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. @@ -557,3 +573,38 @@ function validateUpgrade( ); } } + +/** + * Function to possibly deduplicate `auxiliaryFunds` capability `requiredAssets`. + * Does nothing if no `requiredAssets` exists in `auxiliaryFunds` capability. + * + * @param sendCalls - The original sendCalls request. + */ +function dedupeAuxiliaryFundsRequiredAssets(sendCalls: SendCallsPayload): void { + if (sendCalls.capabilities?.auxiliaryFunds?.requiredAssets) { + const { requiredAssets } = sendCalls.capabilities.auxiliaryFunds; + // Group assets by their address (lowercased) and standard + const grouped = groupBy( + requiredAssets, + (asset) => `${asset.address.toLowerCase()}-${asset.standard}`, + ); + + // For each group, sum the amounts and return a single asset + const deduplicatedAssets = Object.values(grouped).map((group) => { + if (group.length === 1) { + return group[0]; + } + + const totalAmount = group.reduce((sum, asset) => { + return sum + BigInt(asset.amount); + }, 0n); + + return { + ...group[0], + amount: add0x(totalAmount.toString(16)) as Hex, + }; + }); + + sendCalls.capabilities.auxiliaryFunds.requiredAssets = deduplicatedAssets; + } +} diff --git a/packages/eip-5792-middleware/src/types.ts b/packages/eip-5792-middleware/src/types.ts index 621a7f03756..bf9049a94fd 100644 --- a/packages/eip-5792-middleware/src/types.ts +++ b/packages/eip-5792-middleware/src/types.ts @@ -103,7 +103,6 @@ const RequiredAssetStruct = type({ address: nonempty(HexChecksumAddressStruct), amount: nonempty(StrictHexStruct), standard: nonempty(string()), - tokenId: optional(StrictHexStruct), }); export const CapabilitiesStruct = record( From 3fc5fabea7640a1cce53a9334f9c1fe27e8f6dfd Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 12:44:28 +0200 Subject: [PATCH 10/18] yarn --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 07b15095ff5..88288b1d80e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3070,6 +3070,7 @@ __metadata: deepmerge: "npm:^4.2.2" jest: "npm:^27.5.1" klona: "npm:^2.0.6" + lodash: "npm:^4.17.21" ts-jest: "npm:^27.1.4" typedoc: "npm:^0.24.8" typedoc-plugin-missing-exports: "npm:^2.0.0" From 0c2c11495838e9e45a6bb730e991d068575de4c4 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 12:47:39 +0200 Subject: [PATCH 11/18] jest --- packages/eip-5792-middleware/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eip-5792-middleware/jest.config.js b/packages/eip-5792-middleware/jest.config.js index 70d67779fbe..d534fe809f8 100644 --- a/packages/eip-5792-middleware/jest.config.js +++ b/packages/eip-5792-middleware/jest.config.js @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 85, - functions: 100, + functions: 90, lines: 90, statements: 90, }, From 2b1cfc683df7b365e5605a700171e28275eb5a46 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 12:58:30 +0200 Subject: [PATCH 12/18] test: bump up stmts test cov for processSendCAlls.test.ts --- packages/eip-5792-middleware/jest.config.js | 2 +- .../src/hooks/processSendCalls.test.ts | 62 +++++++++++++++++++ .../src/hooks/processSendCalls.ts | 8 --- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/packages/eip-5792-middleware/jest.config.js b/packages/eip-5792-middleware/jest.config.js index d534fe809f8..70d67779fbe 100644 --- a/packages/eip-5792-middleware/jest.config.js +++ b/packages/eip-5792-middleware/jest.config.js @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 85, - functions: 90, + functions: 100, lines: 90, statements: 90, }, diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index f55d162691a..02927f89424 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -569,5 +569,67 @@ describe('EIP-5792', () => { expect(result).toBeDefined(); }); + + it('validates auxiliary funds with optional false and no requiredAssets', async () => { + const result = await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: false, + }, + }, + }, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + }); + + it('deduplicates auxiliary funds requiredAssets by address and standard, summing amounts', async () => { + const payload: SendCallsPayload = { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0x123' as Hex, + amount: '0x2' as Hex, + standard: 'erc20', + }, + { + address: '0x123' as Hex, + amount: '0x3' as Hex, + standard: 'erc20', + }, + ], + }, + }, + }; + + const result = await processSendCalls( + sendCallsHooks, + messenger, + payload, + REQUEST_MOCK, + ); + + expect(result).toBeDefined(); + expect(payload.capabilities?.auxiliaryFunds?.requiredAssets?.length).toBe( + 1, + ); + expect( + payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].amount, + ).toBe('0x5'); + expect( + payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].address, + ).toBe('0x123'); + expect( + payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].standard, + ).toBe('erc20'); + }); }); }); diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index 10639f377d4..a6545d0d9f3 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -288,15 +288,7 @@ async function processMultipleTransaction({ isAuxiliaryFundsSupported, ); - console.log( - { assets: sendCalls.capabilities?.auxiliaryFunds?.requiredAssets }, - 'before, multiple tx', - ); dedupeAuxiliaryFundsRequiredAssets(sendCalls); - console.log( - { assets: sendCalls.capabilities?.auxiliaryFunds?.requiredAssets }, - 'after, multiple tx', - ); const result = await addTransactionBatch({ from, From 54432911947db68c2d137a1425fffd1ae9a3e47b Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 15:09:06 +0200 Subject: [PATCH 13/18] refactor: extract auxiliaryFunds string into enum --- packages/eip-5792-middleware/src/constants.ts | 4 ++++ .../eip-5792-middleware/src/hooks/processSendCalls.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/eip-5792-middleware/src/constants.ts b/packages/eip-5792-middleware/src/constants.ts index 0ad202bbc41..b6b62f031a2 100644 --- a/packages/eip-5792-middleware/src/constants.ts +++ b/packages/eip-5792-middleware/src/constants.ts @@ -11,6 +11,10 @@ export enum MessageType { SendTransaction = 'eth_sendTransaction', } +export enum SupportedCapabilities { + AuxiliaryFunds = 'auxiliaryFunds', +} + // To be moved to @metamask/rpc-errors in future. export enum EIP5792ErrorCode { UnsupportedNonOptionalCapability = 5700, diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index a6545d0d9f3..8b7572bcd14 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -18,6 +18,7 @@ import { EIP7682ErrorCode, KEYRING_TYPES_SUPPORTING_7702, MessageType, + SupportedCapabilities, VERSION, } from '../constants'; import type { @@ -435,13 +436,15 @@ function validateCapabilities( const requiredTopLevelCapabilities = Object.keys(capabilities ?? {}).filter( (name) => - name !== 'auxiliaryFunds' && capabilities?.[name].optional !== true, + // Non optional capabilities other than `auxiliaryFunds` are not supported by the wallet + name !== SupportedCapabilities.AuxiliaryFunds.toString() && + capabilities?.[name].optional !== true, ); const requiredCallCapabilities = calls.flatMap((call) => Object.keys(call.capabilities ?? {}).filter( (name) => - name !== 'auxiliaryFunds' && + name !== SupportedCapabilities.AuxiliaryFunds.toString() && call.capabilities?.[name].optional !== true, ), ); @@ -509,7 +512,7 @@ function validateRequiredAssets({ if (!isSupportedAccount) { throw new JsonRpcError( EIP5792ErrorCode.UnsupportedNonOptionalCapability, - 'Unsupported non-optional capabilities: auxiliaryFunds', + `Unsupported non-optional capabilities: ${SupportedCapabilities.AuxiliaryFunds}`, ); } From c8231998bdbc1e2dbd5dfbbf31b4c13c0bcf2e97 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 17 Sep 2025 17:42:51 +0200 Subject: [PATCH 14/18] refactor: rename function --- packages/eip-5792-middleware/src/hooks/processSendCalls.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index 8b7572bcd14..8efcc6eb513 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -464,7 +464,7 @@ function validateCapabilities( } if (capabilities?.auxiliaryFunds) { - validateRequiredAssets({ + validateAuxFundsSupportAndRequiredAssets({ auxiliaryFunds: capabilities.auxiliaryFunds, chainId, keyringType, @@ -474,7 +474,7 @@ function validateCapabilities( } /** - * Validates EIP-7682 optional `requiredAssets` parameter. + * Validates EIP-7682 optional `requiredAssets` to see if the account and chain are supported, and that param is well-formed. * * docs: {@link https://eips.ethereum.org/EIPS/eip-7682#extended-usage-requiredassets-parameter} * @@ -487,7 +487,7 @@ function validateCapabilities( * @param param.isAuxiliaryFundsSupported - Function to validate if auxiliary funds capability is supported. * @throws JsonRpcError if auxiliary funds capability is not supported. */ -function validateRequiredAssets({ +function validateAuxFundsSupportAndRequiredAssets({ auxiliaryFunds, chainId, keyringType, From 80eba8ba8bcec9a4115e207e5f2fc8107ebe84b5 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 18 Sep 2025 10:17:33 +0200 Subject: [PATCH 15/18] refactor: address code review issues --- packages/eip-5792-middleware/src/hooks/processSendCalls.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index 8efcc6eb513..6f87cb86720 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -512,7 +512,7 @@ function validateAuxFundsSupportAndRequiredAssets({ if (!isSupportedAccount) { throw new JsonRpcError( EIP5792ErrorCode.UnsupportedNonOptionalCapability, - `Unsupported non-optional capabilities: ${SupportedCapabilities.AuxiliaryFunds}`, + `Unsupported non-optional capability: ${SupportedCapabilities.AuxiliaryFunds}`, ); } @@ -596,7 +596,7 @@ function dedupeAuxiliaryFundsRequiredAssets(sendCalls: SendCallsPayload): void { return { ...group[0], - amount: add0x(totalAmount.toString(16)) as Hex, + amount: add0x(totalAmount.toString(16)), }; }); From f2955b4d294d8d7059aa99b030cf5a77cdacad85 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 18 Sep 2025 10:21:38 +0200 Subject: [PATCH 16/18] test: update --- .../eip-5792-middleware/src/hooks/processSendCalls.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index 02927f89424..e10d4a47dd7 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -15,6 +15,7 @@ import type { TransactionController } from '@metamask/transaction-controller'; import type { Hex, JsonRpcRequest } from '@metamask/utils'; import { processSendCalls } from './processSendCalls'; +import { SupportedCapabilities } from '../constants'; import type { SendCallsPayload, SendCallsParams, @@ -467,7 +468,7 @@ describe('EIP-5792', () => { REQUEST_MOCK, ), ).rejects.toThrow( - 'Unsupported non-optional capabilities: auxiliaryFunds', + `Unsupported non-optional capability: ${SupportedCapabilities.AuxiliaryFunds}`, ); }); From 6b2341f8c305c6c898821b027b56d0f72102584d Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 18 Sep 2025 10:30:27 +0200 Subject: [PATCH 17/18] chore: update CHANGELOG.md --- packages/eip-5792-middleware/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index 96214fc58e9..9d06c35de78 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Add `auxiliaryFunds` + `requiredAssets` support defined under [ERC-7682](https://eips.ethereum.org/EIPS/eip-7682) ([#6623](https://github.com/MetaMask/core/pull/6623)) -- Bump `@metamask/transaction-controller` from `^60.2.0` to `^60.3.0` ([#6561](https://github.com/MetaMask/core/pull/6561)) +- Bump `@metamask/transaction-controller` from `^60.2.0` to `^60.4.0` ([#6561](https://github.com/MetaMask/core/pull/6561), [#6641](https://github.com/MetaMask/core/pull/6641)) - Bump `@metamask/utils` from `^11.4.2` to `^11.8.0` ([#6588](https://github.com/MetaMask/core/pull/6588)) ## [1.1.0] From 67be6a9053e51d2dde34660d312e3cb303d6de85 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 18 Sep 2025 18:46:45 +0200 Subject: [PATCH 18/18] test: address nit --- .../src/hooks/processSendCalls.test.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index e10d4a47dd7..86598dfb507 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -619,18 +619,14 @@ describe('EIP-5792', () => { ); expect(result).toBeDefined(); - expect(payload.capabilities?.auxiliaryFunds?.requiredAssets?.length).toBe( - 1, - ); - expect( - payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].amount, - ).toBe('0x5'); - expect( - payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].address, - ).toBe('0x123'); - expect( - payload.capabilities?.auxiliaryFunds?.requiredAssets?.[0].standard, - ).toBe('erc20'); + const requiredAssets = + payload.capabilities?.auxiliaryFunds?.requiredAssets; + expect(requiredAssets).toHaveLength(1); + expect(requiredAssets?.[0]).toMatchObject({ + amount: '0x5', + address: '0x123', + standard: 'erc20', + }); }); }); });