diff --git a/packages/eip-5792-middleware/CHANGELOG.md b/packages/eip-5792-middleware/CHANGELOG.md index dc398cb43b4..014cdc0f8f4 100644 --- a/packages/eip-5792-middleware/CHANGELOG.md +++ b/packages/eip-5792-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Pass `requiredAssets` from `wallet_sendCalls` to `addTransaction` and `addTransactionBatch` ([#7819](https://github.com/MetaMask/core/pull/7819)) + ### Changed - Bump `@metamask/transaction-controller` from `^62.7.0` to `^62.14.0` ([#7596](https://github.com/MetaMask/core/pull/7596), [#7602](https://github.com/MetaMask/core/pull/7602), [#7604](https://github.com/MetaMask/core/pull/7604), [#7642](https://github.com/MetaMask/core/pull/7642), [#7737](https://github.com/MetaMask/core/pull/7737), [#7760](https://github.com/MetaMask/core/pull/7760), [#7775](https://github.com/MetaMask/core/pull/7775), [#7802](https://github.com/MetaMask/core/pull/7802), [#7832](https://github.com/MetaMask/core/pull/7832)) diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts index df81221d9b4..6ed39a40539 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts @@ -570,6 +570,38 @@ describe('EIP-5792', () => { ); }); + it('validates call-level auxiliary funds with unsupported token standard', async () => { + await expect( + processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + calls: [ + { + to: '0x123', + capabilities: { + auxiliaryFunds: { + optional: false, + requiredAssets: [ + { + address: '0x456', + amount: '0x1', + standard: 'erc777', + }, + ], + }, + }, + }, + ], + }, + REQUEST_MOCK, + ), + ).rejects.toThrow( + /The requested asset 0x456 is not available through the wallet.*s auxiliary fund system: unsupported token standard erc777/u, + ); + }); + it('validates auxiliary funds with valid ERC-20 asset', async () => { const result = await processSendCalls( sendCallsHooks, @@ -661,14 +693,204 @@ describe('EIP-5792', () => { ); expect(result).toBeDefined(); - const requiredAssets = - payload.capabilities?.auxiliaryFunds?.requiredAssets; - expect(requiredAssets).toHaveLength(1); - expect(requiredAssets?.[0]).toMatchObject({ - amount: '0x5', - address: '0x123', - standard: 'erc20', - }); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + requiredAssets: [ + expect.objectContaining({ + amount: '0x5', + address: '0x123', + standard: 'erc20', + }), + ], + }), + ); + }); + + it('passes requiredAssets to addTransactionBatch', async () => { + const requiredAssets = [ + { + address: '0x123' as Hex, + amount: '0x1' as Hex, + standard: 'erc20', + }, + ]; + + await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets, + }, + }, + }, + REQUEST_MOCK, + ); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + requiredAssets, + }), + ); + }); + + it('passes requiredAssets to addTransaction for single call', async () => { + const requiredAssets = [ + { + address: '0x456' as Hex, + amount: '0x2' as Hex, + standard: 'erc20', + }, + ]; + + await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + calls: [{ to: '0x123' }], + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets, + }, + }, + }, + REQUEST_MOCK, + ); + + expect(addTransactionMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + requiredAssets, + }), + ); + }); + + it('passes undefined requiredAssets when no auxiliaryFunds capability', async () => { + await processSendCalls( + sendCallsHooks, + messenger, + SEND_CALLS_MOCK, + REQUEST_MOCK, + ); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + requiredAssets: undefined, + }), + ); + }); + + it('collects and deduplicates requiredAssets from individual call capabilities', async () => { + await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + calls: [ + { + to: '0x123', + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0xAAA' as Hex, + amount: '0x1' as Hex, + standard: 'erc20', + }, + ], + }, + }, + }, + { + to: '0x456', + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0xAAA' as Hex, + amount: '0x2' as Hex, + standard: 'erc20', + }, + ], + }, + }, + }, + ], + }, + REQUEST_MOCK, + ); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + requiredAssets: [ + expect.objectContaining({ + address: '0xAAA', + amount: '0x3', + standard: 'erc20', + }), + ], + }), + ); + }); + + it('combines requiredAssets from top-level and call capabilities', async () => { + await processSendCalls( + sendCallsHooks, + messenger, + { + ...SEND_CALLS_MOCK, + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0xBBB' as Hex, + amount: '0x5' as Hex, + standard: 'erc20', + }, + ], + }, + }, + calls: [ + { + to: '0x123', + capabilities: { + auxiliaryFunds: { + optional: true, + requiredAssets: [ + { + address: '0xBBB' as Hex, + amount: '0x3' as Hex, + standard: 'erc20', + }, + ], + }, + }, + }, + { to: '0x456' }, + ], + }, + REQUEST_MOCK, + ); + + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + requiredAssets: [ + expect.objectContaining({ + address: '0xBBB', + amount: '0x8', + standard: 'erc20', + }), + ], + }), + ); }); }); }); diff --git a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts index e2474bf45d1..9f97779612a 100644 --- a/packages/eip-5792-middleware/src/hooks/processSendCalls.ts +++ b/packages/eip-5792-middleware/src/hooks/processSendCalls.ts @@ -3,6 +3,7 @@ import { JsonRpcError, providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { BatchTransactionParams, IsAtomicBatchSupportedResultEntry, + RequiredAsset, SecurityAlertResponse, TransactionController, ValidateSecurityRequest, @@ -217,16 +218,17 @@ async function processSingleTransaction({ }; validateSecurity(securityRequest, chainId); - dedupeAuxiliaryFundsRequiredAssets(sendCalls); + const requiredAssets = dedupeAuxiliaryFundsRequiredAssets(sendCalls); const batchId = generateBatchId(); await addTransaction(txParams, { - requestId, + batchId, networkClientId, origin, + requestId, + requiredAssets, securityAlertResponse: { securityAlertId } as SecurityAlertResponse, - batchId, }); return batchId; } @@ -306,13 +308,14 @@ async function processMultipleTransaction({ isAuxiliaryFundsSupported, ); - dedupeAuxiliaryFundsRequiredAssets(sendCalls); + const requiredAssets = dedupeAuxiliaryFundsRequiredAssets(sendCalls); const result = await addTransactionBatch({ from, networkClientId, origin, requestId, + requiredAssets, securityAlertId, transactions, validateSecurity, @@ -489,6 +492,17 @@ function validateCapabilities( isAuxiliaryFundsSupported, }); } + + for (const call of calls) { + if (call.capabilities?.auxiliaryFunds) { + validateAuxFundsSupportAndRequiredAssets({ + auxiliaryFunds: call.capabilities.auxiliaryFunds, + chainId, + keyringType, + isAuxiliaryFundsSupported, + }); + } + } } /** @@ -588,36 +602,47 @@ function validateUpgrade( } /** - * Function to possibly deduplicate `auxiliaryFunds` capability `requiredAssets`. - * Does nothing if no `requiredAssets` exists in `auxiliaryFunds` capability. + * Collects and deduplicates `auxiliaryFunds` capability `requiredAssets` from + * both top-level capabilities and individual call capabilities. * * @param sendCalls - The original sendCalls request. + * @returns The deduplicated required assets array, or undefined if none exist. */ -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]; - } +function dedupeAuxiliaryFundsRequiredAssets( + sendCalls: SendCallsPayload, +): RequiredAsset[] | undefined { + const rootRequiredAssets = + sendCalls.capabilities?.auxiliaryFunds?.requiredAssets ?? []; - const totalAmount = group.reduce((sum, asset) => { - return sum + BigInt(asset.amount); - }, 0n); + const callRequiredAssets = sendCalls.calls.flatMap( + (call) => call.capabilities?.auxiliaryFunds?.requiredAssets ?? [], + ); - return { - ...group[0], - amount: add0x(totalAmount.toString(16)), - }; - }); + const allRequiredAssets = [...rootRequiredAssets, ...callRequiredAssets]; - sendCalls.capabilities.auxiliaryFunds.requiredAssets = deduplicatedAssets; + if (allRequiredAssets.length === 0) { + return undefined; } + + const grouped = groupBy( + allRequiredAssets, + (asset) => `${asset.address.toLowerCase()}-${asset.standard}`, + ); + + 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)), + }; + }); + + return deduplicatedAssets; }