Skip to content
Draft
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: 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: 93.09,
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
69 changes: 60 additions & 9 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import type {
GetSimulationConfig,
AddTransactionOptions,
PublishHookResult,
GetGasFeeTokensRequest,
} from './types';
import {
GasFeeEstimateLevel,
Expand All @@ -137,7 +138,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 {
getGasFeeTokens,
processGasFeeTokensNoApproval,
} from './utils/gas-fee-tokens';
import { updateGasFees } from './utils/gas-fees';
import { getGasFeeFlow } from './utils/gas-flow';
import {
Expand Down Expand Up @@ -378,6 +382,11 @@ export type TransactionControllerEmulateTransactionUpdate = {
handler: TransactionController['emulateTransactionUpdate'];
};

export type TransactionControllerGetGasFeeTokensAction = {
type: `${typeof controllerName}:getGasFeeTokens`;
handler: (request: GetGasFeeTokensRequest) => Promise<GasFeeToken[]>;
};

/**
* The internal actions available to the TransactionController.
*/
Expand All @@ -386,6 +395,7 @@ export type TransactionControllerActions =
| TransactionControllerAddTransactionBatchAction
| TransactionControllerConfirmExternalTransactionAction
| TransactionControllerEstimateGasAction
| TransactionControllerGetGasFeeTokensAction
| TransactionControllerGetNonceLockAction
| TransactionControllerGetStateAction
| TransactionControllerGetTransactionsAction
Expand Down Expand Up @@ -1215,6 +1225,7 @@ export class TransactionController extends BaseController<
batchId,
deviceConfirmedOn,
disableGasBuffer,
gasFeeToken,
isGasFeeIncluded,
isGasFeeSponsored,
method,
Expand Down Expand Up @@ -1315,13 +1326,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 @@ -1406,6 +1419,15 @@ export class TransactionController extends BaseController<
log('Error while updating first interaction properties', error);
});
} else {
await processGasFeeTokensNoApproval({
ethQuery,
fetchGasFeeTokens: async (tx) =>
(await this.#getGasFeeTokens(tx)).gasFeeTokens,
transaction: addedTransactionMeta,
updateTransaction: (transactionId, fn) =>
this.#updateTransactionInternal({ transactionId }, fn),
});

log(
'Skipping simulation & first interaction update as approval not required',
);
Expand Down Expand Up @@ -4255,14 +4277,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 @@ -4477,6 +4493,11 @@ export class TransactionController extends BaseController<
`${controllerName}:emulateTransactionUpdate`,
this.emulateTransactionUpdate.bind(this),
);

this.messenger.registerActionHandler(
`${controllerName}:getGasFeeTokens`,
this.#getGasFeeTokensAction.bind(this),
);
}

#deleteTransaction(transactionId: string) {
Expand Down Expand Up @@ -4637,4 +4658,34 @@ 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,
});
}

async #getGasFeeTokensAction(request: GetGasFeeTokensRequest) {
const { chainId, data, from, to, value } = request;

const ethQuery = this.#getEthQuery({ chainId });
const delegationAddress = await getDelegationAddress(from, ethQuery);

const transactionMeta = {
chainId,
delegationAddress,
txParams: { data, from, to, value },
} as TransactionMeta;

const result = await this.#getGasFeeTokens(transactionMeta);

return result.gasFeeTokens;
}
}
17 changes: 17 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 Expand Up @@ -2106,3 +2115,11 @@ export type AddTransactionOptions = {
/** Type of transaction to add, such as 'cancel' or 'swap'. */
type?: TransactionType;
};

export type GetGasFeeTokensRequest = {
chainId: Hex;
data?: Hex;
from: Hex;
to: Hex;
value?: Hex;
};
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
33 changes: 33 additions & 0 deletions packages/transaction-controller/src/utils/balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { query, toHex } from '@metamask/controller-utils';
import type EthQuery from '@metamask/eth-query';

import { getNativeBalance } from './balance';

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

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

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: '123456789.123456789123456789',
});
});
});
});
26 changes: 26 additions & 0 deletions packages/transaction-controller/src/utils/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { query } from '@metamask/controller-utils';
import type EthQuery from '@metamask/eth-query';
import type { Hex } from '@metamask/utils';
import { BigNumber } from 'bignumber.js';

/**
* 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,
};
}
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