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

### Added

- Add `estimateGasBatch` function and messenger action to estimate gas for batch transactions ([#7405](https://github.com/MetaMask/core/pull/7405))
- Add optional `gasLimit7702` property to `TransactionBatchRequest`.
- Automatically fail pending transactions if no receipt and hash not recognised by network after multiple attempts ([#7329](https://github.com/MetaMask/core/pull/7329))
- Add optional `isTimeoutEnabled` callback to disable for specific transactions.
- Ignores transactions with future nonce.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ import { getBalanceChanges } from './utils/balance-changes';
import { addTransactionBatch } from './utils/batch';
import { getDelegationAddress } from './utils/eip7702';
import { updateFirstTimeInteraction } from './utils/first-time-interaction';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import {
addGasBuffer,
estimateGas,
estimateGasBatch,
updateGas,
} from './utils/gas';
import { getGasFeeTokens } from './utils/gas-fee-tokens';
import { updateGasFees } from './utils/gas-fees';
import { getGasFeeFlow } from './utils/gas-flow';
Expand Down Expand Up @@ -538,6 +543,7 @@ describe('TransactionController', () => {
const updateGasMock = jest.mocked(updateGas);
const updateGasFeesMock = jest.mocked(updateGasFees);
const estimateGasMock = jest.mocked(estimateGas);
const estimateGasBatchMock = jest.mocked(estimateGasBatch);
const addGasBufferMock = jest.mocked(addGasBuffer);
const updateSwapsTransactionMock = jest.mocked(updateSwapsTransaction);
const updatePostTransactionBalanceMock = jest.mocked(
Expand Down Expand Up @@ -8663,5 +8669,66 @@ describe('TransactionController', () => {
expect(approvedEventListener).not.toHaveBeenCalled();
});
});

describe('TransactionController:estimateGasBatch', () => {
it('calls estimateGasBatch method via messenger and returns gas estimates', async () => {
const { messenger } = setupController();

const totalGasLimitMock = 100000;
const gasLimitsMock = [50000, 50000];

estimateGasBatchMock.mockResolvedValueOnce({
totalGasLimit: totalGasLimitMock,
gasLimits: gasLimitsMock,
});

const result = await messenger.call(
'TransactionController:estimateGasBatch',
{
chainId: CHAIN_ID_MOCK,
from: ACCOUNT_MOCK,
transactions: [
{
to: ACCOUNT_2_MOCK,
value: VALUE_MOCK,
data: DATA_MOCK,
},
{
to: ACCOUNT_2_MOCK,
value: VALUE_MOCK,
data: DATA_MOCK,
},
],
},
);

expect(result).toStrictEqual({
totalGasLimit: totalGasLimitMock,
gasLimits: gasLimitsMock,
});

expect(estimateGasBatchMock).toHaveBeenCalledTimes(1);
expect(estimateGasBatchMock).toHaveBeenCalledWith({
chainId: CHAIN_ID_MOCK,
ethQuery: expect.anything(),
from: ACCOUNT_MOCK,
getSimulationConfig: expect.any(Function),
isAtomicBatchSupported: expect.any(Function),
messenger: expect.anything(),
transactions: [
{
to: ACCOUNT_2_MOCK,
value: VALUE_MOCK,
data: DATA_MOCK,
},
{
to: ACCOUNT_2_MOCK,
value: VALUE_MOCK,
data: DATA_MOCK,
},
],
});
});
});
});
});
51 changes: 50 additions & 1 deletion packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ import {
} from './utils/eip7702';
import { validateConfirmedExternalTransaction } from './utils/external-transactions';
import { updateFirstTimeInteraction } from './utils/first-time-interaction';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import {
addGasBuffer,
estimateGas,
estimateGasBatch,
updateGas,
} from './utils/gas';
import {
checkGasFeeTokenBeforePublish,
getGasFeeTokens,
Expand Down Expand Up @@ -310,6 +315,11 @@ export type TransactionControllerEstimateGasAction = {
handler: TransactionController['estimateGas'];
};

export type TransactionControllerEstimateGasBatchAction = {
type: `${typeof controllerName}:estimateGasBatch`;
handler: TransactionController['estimateGasBatch'];
};

/**
* Adds external provided transaction to state as confirmed transaction.
*
Expand Down Expand Up @@ -400,6 +410,7 @@ export type TransactionControllerActions =
| TransactionControllerAddTransactionBatchAction
| TransactionControllerConfirmExternalTransactionAction
| TransactionControllerEstimateGasAction
| TransactionControllerEstimateGasBatchAction
| TransactionControllerGetGasFeeTokensAction
| TransactionControllerGetNonceLockAction
| TransactionControllerGetStateAction
Expand Down Expand Up @@ -1783,6 +1794,39 @@ export class TransactionController extends BaseController<
return { gas: estimatedGas, simulationFails };
}

/**
* Estimates required gas for a batch of transactions.
*
* @param request - Request object.
* @param request.chainId - Chain ID of the transactions.
* @param request.from - Address of the sender.
* @param request.transactions - Array of transactions within a batch request.
* @returns Object containing the gas limit.
*/
async estimateGasBatch({
chainId,
from,
transactions,
}: {
chainId: Hex;
from: Hex;
transactions: BatchTransactionParams[];
}): Promise<{ totalGasLimit: number; gasLimits: number[] }> {
const ethQuery = this.#getEthQuery({
chainId,
});

return estimateGasBatch({
chainId,
ethQuery,
from,
getSimulationConfig: this.#getSimulationConfig,
isAtomicBatchSupported: this.isAtomicBatchSupported.bind(this),
messenger: this.messenger,
transactions,
});
}

/**
* Estimates required gas for a given transaction and add additional gas buffer with the given multiplier.
*
Expand Down Expand Up @@ -4612,6 +4656,11 @@ export class TransactionController extends BaseController<
this.estimateGas.bind(this),
);

this.messenger.registerActionHandler(
`${controllerName}:estimateGasBatch`,
this.estimateGasBatch.bind(this),
);

this.messenger.registerActionHandler(
`${controllerName}:getGasFeeTokens`,
this.#getGasFeeTokensAction.bind(this),
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type {
TransactionControllerEmulateTransactionUpdate,
TransactionControllerEvents,
TransactionControllerEstimateGasAction,
TransactionControllerEstimateGasBatchAction,
TransactionControllerGetGasFeeTokensAction,
TransactionControllerGetNonceLockAction,
TransactionControllerGetStateAction,
Expand Down
8 changes: 8 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,11 @@ export enum TransactionType {
*/
predictWithdraw = 'predictWithdraw',

/**
* Deposit funds for Relay quote.
*/
relayDeposit = 'relayDeposit',

/**
* When a transaction is failed it can be retried by
* resubmitting the same transaction with a higher gas fee. This type is also used
Expand Down Expand Up @@ -1739,6 +1744,9 @@ export type TransactionBatchRequest = {
/** Address of an ERC-20 token to pay for the gas fee, if the user has insufficient native balance. */
gasFeeToken?: Hex;

/** Gas limit for the transaction batch if submitted via EIP-7702. */
gasLimit7702?: Hex;

/** Whether MetaMask will be compensated for the gas fee by the transaction. */
isGasFeeIncluded?: boolean;

Expand Down
41 changes: 37 additions & 4 deletions packages/transaction-controller/src/utils/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const UPGRADE_CONTRACT_ADDRESS_MOCK =
const NONCE_PREVIOUS_MOCK = '0x110';
const NONCE_MOCK = '0x111';
const NONCE_MOCK_2 = '0x112';
const GAS_LIMIT_7702_MOCK = '0x1234';

const TRANSACTION_META_MOCK = {
id: BATCH_ID_CUSTOM_MOCK,
Expand Down Expand Up @@ -345,7 +346,8 @@ describe('Batch Utils', () => {
getChainIdMock.mockReturnValue(CHAIN_ID_MOCK);

simulateGasBatchMock.mockResolvedValue({
gasLimit: GAS_TOTAL_MOCK,
totalGasLimit: GAS_TOTAL_MOCK,
gasLimits: [GAS_TOTAL_MOCK],
});

doesChainSupportEIP7702Mock.mockReturnValue(true);
Expand Down Expand Up @@ -795,6 +797,34 @@ describe('Batch Utils', () => {
);
});

it('includes gasLimit7702 in EIP-7702 transaction params if provided', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
isSupported: true,
});

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

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

request.request.gasLimit7702 = GAS_LIMIT_7702_MOCK;

await addTransactionBatch(request);

expect(addTransactionMock).toHaveBeenCalledTimes(1);
expect(addTransactionMock).toHaveBeenCalledWith(
expect.objectContaining({
gas: GAS_LIMIT_7702_MOCK,
}),
expect.anything(),
);
});

it('throws if chain not supported', async () => {
doesChainSupportEIP7702Mock.mockReturnValue(false);

Expand Down Expand Up @@ -1309,7 +1339,8 @@ describe('Batch Utils', () => {
} as TransactionBatchSingleRequest['existingTransaction'];

simulateGasBatchMock.mockResolvedValueOnce({
gasLimit: GAS_TOTAL_MOCK,
totalGasLimit: GAS_TOTAL_MOCK,
gasLimits: [GAS_TOTAL_MOCK],
});

addTransactionMock.mockResolvedValueOnce({
Expand Down Expand Up @@ -1402,7 +1433,8 @@ describe('Batch Utils', () => {
const existingTransactionMock = {};

simulateGasBatchMock.mockResolvedValueOnce({
gasLimit: GAS_TOTAL_MOCK,
totalGasLimit: GAS_TOTAL_MOCK,
gasLimits: [GAS_TOTAL_MOCK],
});

addTransactionMock
Expand Down Expand Up @@ -1496,7 +1528,8 @@ describe('Batch Utils', () => {
} as TransactionBatchSingleRequest['existingTransaction'];

simulateGasBatchMock.mockResolvedValueOnce({
gasLimit: GAS_TOTAL_MOCK,
totalGasLimit: GAS_TOTAL_MOCK,
gasLimits: [GAS_TOTAL_MOCK],
});

addTransactionMock.mockResolvedValue({
Expand Down
4 changes: 3 additions & 1 deletion packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ async function addTransactionBatchWith7702(
disableUpgrade,
from,
gasFeeToken,
gasLimit7702,
networkClientId,
origin,
overwriteUpgrade,
Expand Down Expand Up @@ -358,6 +359,7 @@ async function addTransactionBatchWith7702(
const txParams: TransactionParams = {
...batchParams,
from,
gas: gasLimit7702,
maxFeePerGas: nestedTransactions[0]?.maxFeePerGas,
maxPriorityFeePerGas: nestedTransactions[0]?.maxPriorityFeePerGas,
};
Expand Down Expand Up @@ -866,7 +868,7 @@ async function prepareApprovalData({
log('Preparing approval data for batch');
const chainId = getChainId(networkClientId);

const { gasLimit } = await simulateGasBatch({
const { totalGasLimit: gasLimit } = await simulateGasBatch({
chainId,
from,
getSimulationConfig,
Expand Down
Loading
Loading