Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/eip-5792-middleware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
238 changes: 230 additions & 8 deletions packages/eip-5792-middleware/src/hooks/processSendCalls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
}),
],
}),
);
});
});
});
83 changes: 54 additions & 29 deletions packages/eip-5792-middleware/src/hooks/processSendCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JsonRpcError, providerErrors, rpcErrors } from '@metamask/rpc-errors';
import type {
BatchTransactionParams,
IsAtomicBatchSupportedResultEntry,
RequiredAsset,
SecurityAlertResponse,
TransactionController,
ValidateSecurityRequest,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -489,6 +492,17 @@ function validateCapabilities(
isAuxiliaryFundsSupported,
});
}

for (const call of calls) {
if (call.capabilities?.auxiliaryFunds) {
validateAuxFundsSupportAndRequiredAssets({
auxiliaryFunds: call.capabilities.auxiliaryFunds,
chainId,
keyringType,
isAuxiliaryFundsSupported,
});
}
}
}

/**
Expand Down Expand Up @@ -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;
}
Loading