Skip to content

Commit 33f666e

Browse files
feat: persist required assets in transaction meta (#7820)
## Explanation Adds support for persisting `requiredAssets` on transaction metadata. Use if available when generating required tokens. ## References ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [x] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new persisted `requiredAssets` field to transaction/batch metadata and changes required-token computation to prefer this data when present, which can affect downstream transaction-pay behavior. Changes are optional/guarded but touch shared types and request plumbing used by multiple consumers. > > **Overview** > **Adds support for persisting `requiredAssets` on transactions and batches.** `addTransaction` now accepts `requiredAssets` and stores it on `TransactionMeta`, and the batch (EIP-7702) flow forwards `requiredAssets` from the batch request into the created transaction. > > **Updates transaction-pay required-token generation to prefer explicit assets.** `parseRequiredTokens` now builds required tokens from `transactionMeta.requiredAssets` when provided, falling back to parsing token transfers/gas fees only when absent/empty; tests and changelogs were updated accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit be34464. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 281cb40 commit 33f666e

File tree

10 files changed

+262
-0
lines changed

10 files changed

+262
-0
lines changed

packages/transaction-controller/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add optional `requiredAssets` to `TransactionMeta` ([#7820](https://github.com/MetaMask/core/pull/7820))
13+
- Provided by new options in `addTransaction` and `addTransactionBatch`.
14+
1015
## [62.14.0]
1116

1217
### Changed

packages/transaction-controller/src/TransactionController.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,35 @@ describe('TransactionController', () => {
16401640
expect(updateFirstTimeInteractionMock).toHaveBeenCalledTimes(1);
16411641
});
16421642

1643+
it('persists requiredAssets on transaction meta', async () => {
1644+
const { controller } = setupController();
1645+
1646+
const requiredAssets = [
1647+
{
1648+
address: '0x1234567890123456789012345678901234567890' as Hex,
1649+
amount: '0x1' as Hex,
1650+
standard: 'erc20',
1651+
},
1652+
];
1653+
1654+
await controller.addTransaction(
1655+
{
1656+
from: ACCOUNT_MOCK,
1657+
to: ACCOUNT_MOCK,
1658+
},
1659+
{
1660+
networkClientId: NETWORK_CLIENT_ID_MOCK,
1661+
requiredAssets,
1662+
},
1663+
);
1664+
1665+
await flushPromises();
1666+
1667+
const transactionMeta = controller.state.transactions[0];
1668+
1669+
expect(transactionMeta.requiredAssets).toStrictEqual(requiredAssets);
1670+
});
1671+
16431672
it.each([
16441673
[TransactionEnvelopeType.legacy],
16451674
[

packages/transaction-controller/src/TransactionController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,7 @@ export class TransactionController extends BaseController<
12851285
origin,
12861286
publishHook,
12871287
requestId,
1288+
requiredAssets,
12881289
requireApproval,
12891290
securityAlertResponse,
12901291
skipInitialGasEstimate,
@@ -1383,6 +1384,7 @@ export class TransactionController extends BaseController<
13831384
networkClientId,
13841385
origin,
13851386
requestId,
1387+
requiredAssets,
13861388
securityAlertResponse,
13871389
selectedGasFeeToken: gasFeeToken,
13881390
status: TransactionStatus.unapproved as const,

packages/transaction-controller/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export type {
7575
PublishBatchHookTransaction,
7676
PublishHook,
7777
PublishHookResult,
78+
RequiredAsset,
7879
SavedGasFees,
7980
SecurityAlertResponse,
8081
SecurityProviderRequest,
@@ -87,6 +88,7 @@ export type {
8788
TransactionBatchMeta,
8889
TransactionBatchRequest,
8990
TransactionBatchResult,
91+
TransactionBatchSingleRequest,
9092
TransactionError,
9193
TransactionHistory,
9294
TransactionHistoryEntry,

packages/transaction-controller/src/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,11 @@ export type TransactionMeta = {
394394
*/
395395
requestId?: string;
396396

397+
/**
398+
* Assets required by the transaction.
399+
*/
400+
requiredAssets?: RequiredAsset[];
401+
397402
/**
398403
* IDs of any transactions that must be confirmed before this one is submitted.
399404
* Unlike a transaction batch, these transactions can be on alternate chains.
@@ -1804,6 +1809,9 @@ export type TransactionBatchRequest = {
18041809
/** Whether an approval request should be created to require confirmation from the user. */
18051810
requireApproval?: boolean;
18061811

1812+
/** Assets required by the batch transaction. */
1813+
requiredAssets?: RequiredAsset[];
1814+
18071815
/** Security alert ID to persist on the transaction. */
18081816
securityAlertId?: string;
18091817

@@ -2167,6 +2175,9 @@ export type AddTransactionOptions = {
21672175
/** ID of JSON-RPC request from DAPP. */
21682176
requestId?: string;
21692177

2178+
/** Assets required by the transaction. */
2179+
requiredAssets?: RequiredAsset[];
2180+
21702181
/** Whether the transaction requires approval by the user, defaults to true unless explicitly disabled. */
21712182
requireApproval?: boolean | undefined;
21722183

@@ -2218,3 +2229,17 @@ export type GetGasFeeTokensRequest = {
22182229
/** Value of the transaction. */
22192230
value?: Hex;
22202231
};
2232+
2233+
/**
2234+
* An asset required by a transaction.
2235+
*/
2236+
export type RequiredAsset = {
2237+
/** Contract address of the asset. */
2238+
address: Hex;
2239+
2240+
/** Amount of the asset required. */
2241+
amount: Hex;
2242+
2243+
/** Token standard of the asset (e.g., 'erc20'). */
2244+
standard: string;
2245+
};

packages/transaction-controller/src/utils/batch.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { SequentialPublishBatchHook } from '../hooks/SequentialPublishBatchHook'
4040
import type {
4141
GasFeeFlow,
4242
PublishBatchHook,
43+
RequiredAsset,
4344
TransactionBatchSingleRequest,
4445
} from '../types';
4546

@@ -399,6 +400,46 @@ describe('Batch Utils', () => {
399400
expect(result.batchId).toMatch(/^0x[0-9a-f]{32}$/u);
400401
});
401402

403+
it('passes requiredAssets from batch request to addTransaction for 7702 flow', async () => {
404+
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
405+
delegationAddress: undefined,
406+
isSupported: true,
407+
});
408+
409+
const requiredAssets: RequiredAsset[] = [
410+
{
411+
address: '0x1234567890123456789012345678901234567890',
412+
amount: '0x1',
413+
standard: 'erc20',
414+
},
415+
];
416+
417+
addTransactionMock.mockResolvedValueOnce({
418+
transactionMeta: TRANSACTION_META_MOCK,
419+
result: Promise.resolve(''),
420+
});
421+
422+
await addTransactionBatch({
423+
...request,
424+
request: {
425+
...request.request,
426+
requiredAssets,
427+
transactions: [
428+
{
429+
params: TRANSACTION_BATCH_PARAMS_MOCK,
430+
},
431+
],
432+
},
433+
});
434+
435+
expect(addTransactionMock).toHaveBeenCalledWith(
436+
expect.any(Object),
437+
expect.objectContaining({
438+
requiredAssets,
439+
}),
440+
);
441+
});
442+
402443
it('preserves nested transaction types when disable7702 is true', async () => {
403444
const publishBatchHook: jest.MockedFn<PublishBatchHook> = jest.fn();
404445
mockRequestApproval(MESSENGER_MOCK, {
@@ -1219,6 +1260,7 @@ describe('Batch Utils', () => {
12191260
networkClientId: NETWORK_CLIENT_ID_MOCK,
12201261
origin: ORIGIN_MOCK,
12211262
publishHook: expect.any(Function),
1263+
requiredAssets: undefined,
12221264
requireApproval: false,
12231265
type: undefined,
12241266
},

packages/transaction-controller/src/utils/batch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ async function addTransactionBatchWith7702(
303303
origin,
304304
overwriteUpgrade,
305305
requestId,
306+
requiredAssets,
306307
requireApproval,
307308
securityAlertId,
308309
skipInitialGasEstimate,
@@ -436,6 +437,7 @@ async function addTransactionBatchWith7702(
436437
origin,
437438
requestId,
438439
requireApproval,
440+
requiredAssets,
439441
securityAlertResponse,
440442
skipInitialGasEstimate,
441443
type: TransactionType.batch,

packages/transaction-pay-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Generate required tokens using `requiredAssets` from transaction metadata ([#7820](https://github.com/MetaMask/core/pull/7820))
13+
1014
## [12.1.0]
1115

1216
### Changed

packages/transaction-pay-controller/src/utils/required-tokens.test.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,142 @@ describe('Required Tokens Utils', () => {
3636
});
3737

3838
describe('parseRequiredTokens', () => {
39+
describe('with requiredAssets', () => {
40+
it('returns tokens from requiredAssets instead of parsing transaction', () => {
41+
const tokenAddress = '0xabc123' as Hex;
42+
43+
getTokenInfoMock.mockReturnValue({ decimals: 6, symbol: 'USDC' });
44+
getTokenBalanceMock.mockReturnValue('5000000');
45+
getTokenFiatRateMock.mockReturnValue({
46+
usdRate: '1',
47+
fiatRate: '1',
48+
});
49+
50+
const transactionMeta: TransactionMeta = {
51+
...TRANSACTION_META_MOCK,
52+
requiredAssets: [
53+
{
54+
address: tokenAddress,
55+
amount: '0x2DC6C0' as Hex,
56+
standard: 'erc20',
57+
},
58+
],
59+
};
60+
61+
const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);
62+
63+
expect(result).toStrictEqual([
64+
{
65+
address: tokenAddress,
66+
allowUnderMinimum: false,
67+
amountFiat: '3',
68+
amountHuman: '3',
69+
amountRaw: '3000000',
70+
amountUsd: '3',
71+
balanceFiat: '5',
72+
balanceHuman: '5',
73+
balanceRaw: '5000000',
74+
balanceUsd: '5',
75+
chainId: TRANSACTION_META_MOCK.chainId,
76+
decimals: 6,
77+
skipIfBalance: false,
78+
symbol: 'USDC',
79+
},
80+
]);
81+
});
82+
83+
it('returns multiple tokens from requiredAssets', () => {
84+
const tokenAddress1 = '0xabc123' as Hex;
85+
const tokenAddress2 = '0xdef456' as Hex;
86+
87+
getTokenInfoMock.mockReturnValue({ decimals: 6, symbol: 'USDC' });
88+
getTokenBalanceMock.mockReturnValue('5000000');
89+
getTokenFiatRateMock.mockReturnValue({
90+
usdRate: '1',
91+
fiatRate: '1',
92+
});
93+
94+
const transactionMeta: TransactionMeta = {
95+
...TRANSACTION_META_MOCK,
96+
requiredAssets: [
97+
{
98+
address: tokenAddress1,
99+
amount: '0x2DC6C0' as Hex,
100+
standard: 'erc20',
101+
},
102+
{
103+
address: tokenAddress2,
104+
amount: '0x1E8480' as Hex,
105+
standard: 'erc20',
106+
},
107+
],
108+
};
109+
110+
const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);
111+
112+
expect(result).toHaveLength(2);
113+
expect(result[0].address).toBe(tokenAddress1);
114+
expect(result[1].address).toBe(tokenAddress2);
115+
});
116+
117+
it('filters out tokens that cannot be built', () => {
118+
const tokenAddress = '0xabc123' as Hex;
119+
120+
getTokenInfoMock.mockReturnValue(undefined);
121+
122+
const transactionMeta: TransactionMeta = {
123+
...TRANSACTION_META_MOCK,
124+
requiredAssets: [
125+
{
126+
address: tokenAddress,
127+
amount: '0x2DC6C0' as Hex,
128+
standard: 'erc20',
129+
},
130+
],
131+
};
132+
133+
const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);
134+
135+
expect(result).toStrictEqual([]);
136+
});
137+
138+
it('falls back to parsing transaction when requiredAssets is empty', () => {
139+
getTokenInfoMock.mockReturnValue({ decimals: 3, symbol: 'TST' });
140+
getTokenBalanceMock.mockReturnValue('789000');
141+
getTokenFiatRateMock.mockReturnValue({
142+
usdRate: '1.5',
143+
fiatRate: '2',
144+
});
145+
146+
const transactionMeta: TransactionMeta = {
147+
...TRANSACTION_META_MOCK,
148+
requiredAssets: [],
149+
};
150+
151+
const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);
152+
153+
expect(result).toStrictEqual([
154+
{
155+
address: TRANSACTION_META_MOCK.txParams.to,
156+
allowUnderMinimum: false,
157+
amountFiat: '246.912',
158+
amountHuman: '123.456',
159+
amountRaw: '123456',
160+
amountUsd: '185.184',
161+
balanceFiat: '1578',
162+
balanceHuman: '789',
163+
balanceRaw: '789000',
164+
balanceUsd: '1183.5',
165+
chainId: TRANSACTION_META_MOCK.chainId,
166+
decimals: 3,
167+
skipIfBalance: false,
168+
symbol: 'TST',
169+
},
170+
expect.anything(),
171+
]);
172+
});
173+
});
174+
39175
it('returns token transfer required token', () => {
40176
getTokenInfoMock.mockReturnValue({ decimals: 3, symbol: 'TST' });
41177
getTokenBalanceMock.mockReturnValue('789000');

packages/transaction-pay-controller/src/utils/required-tokens.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const FOUR_BYTE_TOKEN_TRANSFER = '0xa9059cbb';
2323
/**
2424
* Parse required tokens from a transaction.
2525
*
26+
* If the transaction has `requiredAssets`, those are used to determine required tokens.
27+
* Otherwise, falls back to parsing the transaction data for token transfers.
28+
*
2629
* @param transaction - Transaction metadata.
2730
* @param messenger - Controller messenger.
2831
* @returns An array of required tokens.
@@ -31,6 +34,18 @@ export function parseRequiredTokens(
3134
transaction: TransactionMeta,
3235
messenger: TransactionPayControllerMessenger,
3336
): TransactionPayRequiredToken[] {
37+
const { requiredAssets } = transaction;
38+
39+
if (requiredAssets?.length) {
40+
const assetTokens = requiredAssets
41+
.map((asset) =>
42+
buildRequiredToken(transaction, asset.address, asset.amount, messenger),
43+
)
44+
.filter(Boolean) as TransactionPayRequiredToken[];
45+
46+
return assetTokens;
47+
}
48+
3449
return [
3550
getTokenTransferToken(transaction, messenger),
3651
getGasFeeToken(transaction, messenger),

0 commit comments

Comments
 (0)