Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `requiredAssets` to `TransactionMeta` ([#7820](https://github.com/MetaMask/core/pull/7820))
- Provided by new options in `addTransaction` and `addTransactionBatch`.

## [62.14.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1640,6 +1640,35 @@ describe('TransactionController', () => {
expect(updateFirstTimeInteractionMock).toHaveBeenCalledTimes(1);
});

it('persists requiredAssets on transaction meta', async () => {
const { controller } = setupController();

const requiredAssets = [
{
address: '0x1234567890123456789012345678901234567890' as Hex,
amount: '0x1' as Hex,
standard: 'erc20',
},
];

await controller.addTransaction(
{
from: ACCOUNT_MOCK,
to: ACCOUNT_MOCK,
},
{
networkClientId: NETWORK_CLIENT_ID_MOCK,
requiredAssets,
},
);

await flushPromises();

const transactionMeta = controller.state.transactions[0];

expect(transactionMeta.requiredAssets).toStrictEqual(requiredAssets);
});

it.each([
[TransactionEnvelopeType.legacy],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,7 @@ export class TransactionController extends BaseController<
origin,
publishHook,
requestId,
requiredAssets,
requireApproval,
securityAlertResponse,
skipInitialGasEstimate,
Expand Down Expand Up @@ -1383,6 +1384,7 @@ export class TransactionController extends BaseController<
networkClientId,
origin,
requestId,
requiredAssets,
securityAlertResponse,
selectedGasFeeToken: gasFeeToken,
status: TransactionStatus.unapproved as const,
Expand Down
2 changes: 2 additions & 0 deletions packages/transaction-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type {
PublishBatchHookTransaction,
PublishHook,
PublishHookResult,
RequiredAsset,
SavedGasFees,
SecurityAlertResponse,
SecurityProviderRequest,
Expand All @@ -87,6 +88,7 @@ export type {
TransactionBatchMeta,
TransactionBatchRequest,
TransactionBatchResult,
TransactionBatchSingleRequest,
TransactionError,
TransactionHistory,
TransactionHistoryEntry,
Expand Down
25 changes: 25 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ export type TransactionMeta = {
*/
requestId?: string;

/**
* Assets required by the transaction.
*/
requiredAssets?: RequiredAsset[];

/**
* IDs of any transactions that must be confirmed before this one is submitted.
* Unlike a transaction batch, these transactions can be on alternate chains.
Expand Down Expand Up @@ -1804,6 +1809,9 @@ export type TransactionBatchRequest = {
/** Whether an approval request should be created to require confirmation from the user. */
requireApproval?: boolean;

/** Assets required by the batch transaction. */
requiredAssets?: RequiredAsset[];

/** Security alert ID to persist on the transaction. */
securityAlertId?: string;

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

/** Assets required by the transaction. */
requiredAssets?: RequiredAsset[];

/** Whether the transaction requires approval by the user, defaults to true unless explicitly disabled. */
requireApproval?: boolean | undefined;

Expand Down Expand Up @@ -2218,3 +2229,17 @@ export type GetGasFeeTokensRequest = {
/** Value of the transaction. */
value?: Hex;
};

/**
* An asset required by a transaction.
*/
export type RequiredAsset = {
/** Contract address of the asset. */
address: Hex;

/** Amount of the asset required. */
amount: Hex;

/** Token standard of the asset (e.g., 'erc20'). */
standard: string;
};
42 changes: 42 additions & 0 deletions packages/transaction-controller/src/utils/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { SequentialPublishBatchHook } from '../hooks/SequentialPublishBatchHook'
import type {
GasFeeFlow,
PublishBatchHook,
RequiredAsset,
TransactionBatchSingleRequest,
} from '../types';

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

it('passes requiredAssets from batch request to addTransaction for 7702 flow', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
isSupported: true,
});

const requiredAssets: RequiredAsset[] = [
{
address: '0x1234567890123456789012345678901234567890',
amount: '0x1',
standard: 'erc20',
},
];

addTransactionMock.mockResolvedValueOnce({
transactionMeta: TRANSACTION_META_MOCK,
result: Promise.resolve(''),
});

await addTransactionBatch({
...request,
request: {
...request.request,
requiredAssets,
transactions: [
{
params: TRANSACTION_BATCH_PARAMS_MOCK,
},
],
},
});

expect(addTransactionMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
requiredAssets,
}),
);
});

it('preserves nested transaction types when disable7702 is true', async () => {
const publishBatchHook: jest.MockedFn<PublishBatchHook> = jest.fn();
mockRequestApproval(MESSENGER_MOCK, {
Expand Down Expand Up @@ -1219,6 +1260,7 @@ describe('Batch Utils', () => {
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: ORIGIN_MOCK,
publishHook: expect.any(Function),
requiredAssets: undefined,
requireApproval: false,
type: undefined,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ async function addTransactionBatchWith7702(
origin,
overwriteUpgrade,
requestId,
requiredAssets,
requireApproval,
securityAlertId,
skipInitialGasEstimate,
Expand Down Expand Up @@ -436,6 +437,7 @@ async function addTransactionBatchWith7702(
origin,
requestId,
requireApproval,
requiredAssets,
securityAlertResponse,
skipInitialGasEstimate,
type: TransactionType.batch,
Expand Down
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/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

- Generate required tokens using `requiredAssets` from transaction metadata ([#7820](https://github.com/MetaMask/core/pull/7820))

## [12.1.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,142 @@ describe('Required Tokens Utils', () => {
});

describe('parseRequiredTokens', () => {
describe('with requiredAssets', () => {
it('returns tokens from requiredAssets instead of parsing transaction', () => {
const tokenAddress = '0xabc123' as Hex;

getTokenInfoMock.mockReturnValue({ decimals: 6, symbol: 'USDC' });
getTokenBalanceMock.mockReturnValue('5000000');
getTokenFiatRateMock.mockReturnValue({
usdRate: '1',
fiatRate: '1',
});

const transactionMeta: TransactionMeta = {
...TRANSACTION_META_MOCK,
requiredAssets: [
{
address: tokenAddress,
amount: '0x2DC6C0' as Hex,
standard: 'erc20',
},
],
};

const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);

expect(result).toStrictEqual([
{
address: tokenAddress,
allowUnderMinimum: false,
amountFiat: '3',
amountHuman: '3',
amountRaw: '3000000',
amountUsd: '3',
balanceFiat: '5',
balanceHuman: '5',
balanceRaw: '5000000',
balanceUsd: '5',
chainId: TRANSACTION_META_MOCK.chainId,
decimals: 6,
skipIfBalance: false,
symbol: 'USDC',
},
]);
});

it('returns multiple tokens from requiredAssets', () => {
const tokenAddress1 = '0xabc123' as Hex;
const tokenAddress2 = '0xdef456' as Hex;

getTokenInfoMock.mockReturnValue({ decimals: 6, symbol: 'USDC' });
getTokenBalanceMock.mockReturnValue('5000000');
getTokenFiatRateMock.mockReturnValue({
usdRate: '1',
fiatRate: '1',
});

const transactionMeta: TransactionMeta = {
...TRANSACTION_META_MOCK,
requiredAssets: [
{
address: tokenAddress1,
amount: '0x2DC6C0' as Hex,
standard: 'erc20',
},
{
address: tokenAddress2,
amount: '0x1E8480' as Hex,
standard: 'erc20',
},
],
};

const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);

expect(result).toHaveLength(2);
expect(result[0].address).toBe(tokenAddress1);
expect(result[1].address).toBe(tokenAddress2);
});

it('filters out tokens that cannot be built', () => {
const tokenAddress = '0xabc123' as Hex;

getTokenInfoMock.mockReturnValue(undefined);

const transactionMeta: TransactionMeta = {
...TRANSACTION_META_MOCK,
requiredAssets: [
{
address: tokenAddress,
amount: '0x2DC6C0' as Hex,
standard: 'erc20',
},
],
};

const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);

expect(result).toStrictEqual([]);
});

it('falls back to parsing transaction when requiredAssets is empty', () => {
getTokenInfoMock.mockReturnValue({ decimals: 3, symbol: 'TST' });
getTokenBalanceMock.mockReturnValue('789000');
getTokenFiatRateMock.mockReturnValue({
usdRate: '1.5',
fiatRate: '2',
});

const transactionMeta: TransactionMeta = {
...TRANSACTION_META_MOCK,
requiredAssets: [],
};

const result = parseRequiredTokens(transactionMeta, MESSENGER_MOCK);

expect(result).toStrictEqual([
{
address: TRANSACTION_META_MOCK.txParams.to,
allowUnderMinimum: false,
amountFiat: '246.912',
amountHuman: '123.456',
amountRaw: '123456',
amountUsd: '185.184',
balanceFiat: '1578',
balanceHuman: '789',
balanceRaw: '789000',
balanceUsd: '1183.5',
chainId: TRANSACTION_META_MOCK.chainId,
decimals: 3,
skipIfBalance: false,
symbol: 'TST',
},
expect.anything(),
]);
});
});

it('returns token transfer required token', () => {
getTokenInfoMock.mockReturnValue({ decimals: 3, symbol: 'TST' });
getTokenBalanceMock.mockReturnValue('789000');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const FOUR_BYTE_TOKEN_TRANSFER = '0xa9059cbb';
/**
* Parse required tokens from a transaction.
*
* If the transaction has `requiredAssets`, those are used to determine required tokens.
* Otherwise, falls back to parsing the transaction data for token transfers.
*
* @param transaction - Transaction metadata.
* @param messenger - Controller messenger.
* @returns An array of required tokens.
Expand All @@ -31,6 +34,18 @@ export function parseRequiredTokens(
transaction: TransactionMeta,
messenger: TransactionPayControllerMessenger,
): TransactionPayRequiredToken[] {
const { requiredAssets } = transaction;

if (requiredAssets?.length) {
const assetTokens = requiredAssets
.map((asset) =>
buildRequiredToken(transaction, asset.address, asset.amount, messenger),
)
.filter(Boolean) as TransactionPayRequiredToken[];

return assetTokens;
}

return [
getTokenTransferToken(transaction, messenger),
getGasFeeToken(transaction, messenger),
Expand Down
Loading