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 `gasFeeToken` property to `addTransaction` and `addTransactionBatch` methods ([#7123](https://github.com/MetaMask/core/pull/7123))
- Also add optional `gasFeeToken` and `isGasFeeTokenIgnoredIfBalance` properties to `TransactionMeta`.

## [61.2.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, {
coverageThreshold: {
global: {
branches: 91.76,
functions: 93.24,
functions: 92.76,
lines: 96.83,
statements: 96.82,
},
Expand Down
1 change: 1 addition & 0 deletions packages/transaction-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@metamask/rpc-errors": "^7.0.2",
"@metamask/utils": "^11.8.1",
"async-mutex": "^0.5.0",
"bignumber.js": "^9.1.2",
"bn.js": "^5.2.1",
"eth-method-registry": "^4.0.0",
"fast-json-patch": "^3.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1693,10 +1693,12 @@ describe('TransactionController', () => {
isFirstTimeInteraction: undefined,
isGasFeeIncluded: undefined,
isGasFeeSponsored: undefined,
isGasFeeTokenIgnoredIfBalance: false,
nestedTransactions: undefined,
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: undefined,
securityAlertResponse: undefined,
selectedGasFeeToken: undefined,
sendFlowHistory: expect.any(Array),
status: TransactionStatus.unapproved as const,
time: expect.any(Number),
Expand Down
48 changes: 36 additions & 12 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ import {
import { validateConfirmedExternalTransaction } from './utils/external-transactions';
import { updateFirstTimeInteraction } from './utils/first-time-interaction';
import { addGasBuffer, estimateGas, updateGas } from './utils/gas';
import { getGasFeeTokens } from './utils/gas-fee-tokens';
import {
checkGasFeeTokenBeforePublish,
getGasFeeTokens,
} from './utils/gas-fee-tokens';
import { updateGasFees } from './utils/gas-fees';
import { getGasFeeFlow } from './utils/gas-flow';
import {
Expand Down Expand Up @@ -1215,6 +1218,7 @@ export class TransactionController extends BaseController<
batchId,
deviceConfirmedOn,
disableGasBuffer,
gasFeeToken,
isGasFeeIncluded,
isGasFeeSponsored,
method,
Expand Down Expand Up @@ -1315,13 +1319,15 @@ export class TransactionController extends BaseController<
deviceConfirmedOn,
disableGasBuffer,
id: random(),
isGasFeeTokenIgnoredIfBalance: Boolean(gasFeeToken),
isGasFeeIncluded,
isGasFeeSponsored,
isFirstTimeInteraction: undefined,
nestedTransactions,
networkClientId,
origin,
securityAlertResponse,
selectedGasFeeToken: gasFeeToken,
status: TransactionStatus.unapproved as const,
time: Date.now(),
txParams,
Expand Down Expand Up @@ -3114,6 +3120,20 @@ export class TransactionController extends BaseController<
clearApprovingTransactionId = () =>
this.#approvingTransactionIds.delete(transactionId);

const { networkClientId } = transactionMeta;
const ethQuery = this.#getEthQuery({ networkClientId });

await checkGasFeeTokenBeforePublish({
ethQuery,
fetchGasFeeTokens: async (tx) =>
(await this.#getGasFeeTokens(tx)).gasFeeTokens,
transaction: transactionMeta,
updateTransaction: (txId, fn) =>
this.#updateTransactionInternal({ transactionId: txId }, fn),
});

transactionMeta = this.#getTransactionOrThrow(transactionId);

const [nonce, releaseNonce] = await getNextNonce(
transactionMeta,
(address: string) =>
Expand Down Expand Up @@ -3165,9 +3185,6 @@ export class TransactionController extends BaseController<
return ApprovalState.NotApproved;
}

const { networkClientId } = transactionMeta;
const ethQuery = this.#getEthQuery({ networkClientId });

let preTxBalance: string | undefined;
const shouldUpdatePreTxBalance =
transactionMeta.type === TransactionType.swap;
Expand Down Expand Up @@ -4255,14 +4272,8 @@ export class TransactionController extends BaseController<
};
}

const gasFeeTokensResponse = await getGasFeeTokens({
chainId,
getSimulationConfig: this.#getSimulationConfig,
isEIP7702GasFeeTokensEnabled: this.#isEIP7702GasFeeTokensEnabled,
messenger: this.messenger,
publicKeyEIP7702: this.#publicKeyEIP7702,
transactionMeta,
});
const gasFeeTokensResponse = await this.#getGasFeeTokens(transactionMeta);

gasFeeTokens = gasFeeTokensResponse?.gasFeeTokens ?? [];
isGasFeeSponsored = gasFeeTokensResponse?.isGasFeeSponsored ?? false;
}
Expand Down Expand Up @@ -4637,4 +4648,17 @@ export class TransactionController extends BaseController<

return { transactionHash };
}

async #getGasFeeTokens(transaction: TransactionMeta) {
const { chainId } = transaction;

return await getGasFeeTokens({
chainId,
getSimulationConfig: this.#getSimulationConfig,
isEIP7702GasFeeTokensEnabled: this.#isEIP7702GasFeeTokensEnabled,
messenger: this.messenger,
publicKeyEIP7702: this.#publicKeyEIP7702,
transactionMeta: transaction,
});
}
}
9 changes: 9 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ export type TransactionMeta = {
/** Whether MetaMask will be compensated for the gas fee by the transaction. */
isGasFeeIncluded?: boolean;

/** Whether the `selectedGasFeeToken` is only used if the user has insufficient native balance. */
isGasFeeTokenIgnoredIfBalance?: boolean;

/** Whether the intent of the transaction was achieved via an alternate route or chain. */
isIntentComplete?: boolean;

Expand Down Expand Up @@ -1723,6 +1726,9 @@ export type TransactionBatchRequest = {
/** Address of the account to submit the transaction batch. */
from: Hex;

/** Address of an ERC-20 token to pay for the gas fee, if the user has insufficient native balance. */
gasFeeToken?: Hex;

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

Expand Down Expand Up @@ -2061,6 +2067,9 @@ export type AddTransactionOptions = {
/** Whether to disable the gas estimation buffer. */
disableGasBuffer?: boolean;

/** Address of an ERC-20 token to pay for the gas fee, if the user has insufficient native balance. */
gasFeeToken?: Hex;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function mockParseLog({
}
}

describe('Simulation Utils', () => {
describe('Balance Change Utils', () => {
const simulateTransactionsMock = jest.mocked(simulateTransactions);
const queryMock = jest.mocked(query);

Expand Down
10 changes: 4 additions & 6 deletions packages/transaction-controller/src/utils/balance-changes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { Fragment, LogDescription, Result } from '@ethersproject/abi';
import { Interface } from '@ethersproject/abi';
import { hexToBN, query, toHex } from '@metamask/controller-utils';
import { hexToBN, toHex } from '@metamask/controller-utils';
import type EthQuery from '@metamask/eth-query';
import { abiERC20, abiERC721, abiERC1155 } from '@metamask/metamask-eth-abis';
import { createModuleLogger, type Hex } from '@metamask/utils';
import BN from 'bn.js';

import { getNativeBalance } from './balance';
import { simulateTransactions } from '../api/simulation-api';
import type {
SimulationResponseLog,
Expand Down Expand Up @@ -726,11 +727,8 @@ async function baseRequest({

log('Required balance', requiredBalanceHex);

const currentBalanceHex = (await query(ethQuery, 'getBalance', [
from,
'latest',
])) as Hex;

const { balanceRaw } = await getNativeBalance(from, ethQuery);
const currentBalanceHex = toHex(balanceRaw);
const currentBalanceBN = hexToBN(currentBalanceHex);

log('Current balance', currentBalanceHex);
Expand Down
68 changes: 68 additions & 0 deletions packages/transaction-controller/src/utils/balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { query, toHex } from '@metamask/controller-utils';
import type EthQuery from '@metamask/eth-query';

import { getNativeBalance, isNativeBalanceSufficientForGas } from './balance';
import type { TransactionMeta } from '..';

jest.mock('@metamask/controller-utils', () => ({
...jest.requireActual('@metamask/controller-utils'),
query: jest.fn(),
}));

const ETH_QUERY_MOCK = {} as EthQuery;
const BALANCE_MOCK = '21000000000000';

const TRANSACTION_META_MOCK = {
txParams: {
from: '0x1234',
gas: toHex(21000),
maxFeePerGas: toHex(1000000000), // 1 Gwei
},
} as TransactionMeta;

describe('Balance Utils', () => {
const queryMock = jest.mocked(query);

beforeEach(() => {
jest.resetAllMocks();

queryMock.mockResolvedValue(toHex(BALANCE_MOCK));
});

describe('getNativeBalance', () => {
it('returns native balance', async () => {
const result = await getNativeBalance('0x1234', ETH_QUERY_MOCK);

expect(result).toStrictEqual({
balanceRaw: BALANCE_MOCK,
balanceHuman: '0.000021',
});
});
});

describe('isNativeBalanceSufficientForGas', () => {
it('returns true if balance is sufficient for gas', async () => {
const result = await isNativeBalanceSufficientForGas(
TRANSACTION_META_MOCK,
ETH_QUERY_MOCK,
);

expect(result).toBe(true);
});

it('returns false if balance is insufficient for gas', async () => {
const result = await isNativeBalanceSufficientForGas(
{
...TRANSACTION_META_MOCK,
txParams: {
...TRANSACTION_META_MOCK.txParams,
gas: toHex(21001),
},
},
ETH_QUERY_MOCK,
);

expect(result).toBe(false);
});
});
});
52 changes: 52 additions & 0 deletions packages/transaction-controller/src/utils/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { query } from '@metamask/controller-utils';
import type EthQuery from '@metamask/eth-query';
import type { Hex } from '@metamask/utils';
import { BigNumber } from 'bignumber.js';

import type { TransactionMeta } from '..';

/**
* Get the native balance for an address.
*
* @param address - Address to get the balance for.
* @param ethQuery - EthQuery instance to use.
* @returns Balance in both human-readable and raw format.
*/
export async function getNativeBalance(address: Hex, ethQuery: EthQuery) {
const balanceRawHex = (await query(ethQuery, 'getBalance', [
address,
'latest',
])) as Hex;

const balanceRaw = new BigNumber(balanceRawHex).toString(10);
const balanceHuman = new BigNumber(balanceRaw).shiftedBy(-18).toString(10);

return {
balanceHuman,
balanceRaw,
};
}

/**
* Determine if the native balance is sufficient to cover max gas cost.
*
* @param transaction - Transaction metadata.
* @param ethQuery - EthQuery instance.
* @returns True if the native balance is sufficient, false otherwise.
*/
export async function isNativeBalanceSufficientForGas(
transaction: TransactionMeta,
ethQuery: EthQuery,
): Promise<boolean> {
const from = transaction.txParams.from as Hex;

const gasCostRawValue = new BigNumber(
transaction.txParams.gas ?? '0x0',
).multipliedBy(
transaction.txParams.maxFeePerGas ?? transaction.txParams.gasPrice ?? '0x0',
);

const { balanceRaw } = await getNativeBalance(from, ethQuery);

return gasCostRawValue.isLessThanOrEqualTo(balanceRaw);
}
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 @@ -289,6 +289,7 @@ async function addTransactionBatchWith7702(
const {
batchId: batchIdOverride,
from,
gasFeeToken,
networkClientId,
origin,
requireApproval,
Expand Down Expand Up @@ -400,6 +401,7 @@ async function addTransactionBatchWith7702(

const { result } = await addTransaction(txParams, {
batchId,
gasFeeToken,
isGasFeeIncluded: userRequest.isGasFeeIncluded,
isGasFeeSponsored: userRequest.isGasFeeSponsored,
nestedTransactions,
Expand Down
Loading
Loading