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
1 change: 1 addition & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@metamask/accounts-controller` from `^38.1.1` to `^38.1.2` ([#8912](https://github.com/MetaMask/core/pull/8912))
- Bump `@metamask/core-backend` from `^6.3.0` to `^6.3.1` ([#8912](https://github.com/MetaMask/core/pull/8912))
- Bump `@metamask/remote-feature-flag-controller` from `^4.2.1` to `^4.2.2` ([#8986](https://github.com/MetaMask/core/pull/8986))
- **BREAKING:** Expand saved gas fee support to allow transaction-scoped lookup, saved gas fee estimate levels, and legacy gas price values. Consumers that provide `getSavedGasFees` must now accept `TransactionMeta` instead of a chain ID. ([#8993](https://github.com/MetaMask/core/pull/8993))

### Fixed

Expand Down
11 changes: 8 additions & 3 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,9 @@ export type TransactionControllerOptions = {
getPermittedAccounts?: (origin?: string) => Promise<string[]>;

/** Gets the saved gas fee config. */
getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined;
getSavedGasFees?: (
transactionMeta: TransactionMeta,
) => SavedGasFees | undefined;

/**
* Gets the transaction simulation configuration.
Expand Down Expand Up @@ -826,7 +828,9 @@ export class TransactionController extends BaseController<

readonly #getPermittedAccounts?: (origin?: string) => Promise<string[]>;

readonly #getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined;
readonly #getSavedGasFees: (
transactionMeta: TransactionMeta,
) => SavedGasFees | undefined;

readonly #getSimulationConfig: GetSimulationConfig;

Expand Down Expand Up @@ -963,7 +967,8 @@ export class TransactionController extends BaseController<
this.#getNetworkState = getNetworkState;
this.#getPermittedAccounts = getPermittedAccounts;
this.#getSavedGasFees =
getSavedGasFees ?? ((_chainId): SavedGasFees | undefined => undefined);
getSavedGasFees ??
((_transactionMeta): SavedGasFees | undefined => undefined);
this.#getSimulationConfig =
getSimulationConfig ??
((): ReturnType<GetSimulationConfig> => Promise.resolve({}));
Expand Down
8 changes: 5 additions & 3 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,13 +1258,15 @@ export type DappSuggestedGasFees = {
};

/**
* Gas values saved by the user for a specific chain.
* Gas values saved by the user for a specific chain and account.
*/
// Convert to a `type` in a future major version.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface SavedGasFees {
maxBaseFee: string;
priorityFee: string;
level?: UserFeeLevel | GasFeeEstimateLevel;
maxBaseFee?: string;
priorityFee?: string;
gasPrice?: string;
}

/**
Expand Down
93 changes: 92 additions & 1 deletion packages/transaction-controller/src/utils/gas-fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { NetworkClientId } from '@metamask/network-controller';

import type { TransactionControllerMessenger } from '../TransactionController';
import type { GasFeeFlow, GasFeeFlowResponse } from '../types';
import { GasFeeEstimateType, TransactionType, UserFeeLevel } from '../types';
import {
GasFeeEstimateLevel,
GasFeeEstimateType,
TransactionType,
UserFeeLevel,
} from '../types';
import type { UpdateGasFeesRequest } from './gas-fees';
import { gweiDecimalToWeiDecimal, updateGasFees } from './gas-fees';
import { rpcRequest } from './provider';
Expand All @@ -15,8 +20,12 @@ jest.mock('./provider', () => ({
console.error = jest.fn();

const GAS_MOCK = 123;
const GAS_LOW_MOCK = 111;
const GAS_HIGH_MOCK = 789;
const GAS_HEX_MOCK = toHex(GAS_MOCK);
const GAS_HEX_WEI_MOCK = toHex(GAS_MOCK * 1e9);
const GAS_LOW_HEX_WEI_MOCK = toHex(GAS_LOW_MOCK * 1e9);
const GAS_HIGH_HEX_WEI_MOCK = toHex(GAS_HIGH_MOCK * 1e9);
const ORIGIN_MOCK = 'test.com';
const MESSENGER_MOCK = {} as unknown as TransactionControllerMessenger;
const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId' as NetworkClientId;
Expand All @@ -35,17 +44,27 @@ const UPDATE_GAS_FEES_REQUEST_MOCK = {
const FLOW_RESPONSE_FEE_MARKET_MOCK = {
estimates: {
type: GasFeeEstimateType.FeeMarket,
low: {
maxFeePerGas: GAS_LOW_HEX_WEI_MOCK,
maxPriorityFeePerGas: GAS_LOW_HEX_WEI_MOCK,
},
medium: {
maxFeePerGas: GAS_HEX_WEI_MOCK,
maxPriorityFeePerGas: GAS_HEX_WEI_MOCK,
},
high: {
maxFeePerGas: GAS_HIGH_HEX_WEI_MOCK,
maxPriorityFeePerGas: GAS_HIGH_HEX_WEI_MOCK,
},
},
} as GasFeeFlowResponse;

const FLOW_RESPONSE_LEGACY_MOCK = {
estimates: {
type: GasFeeEstimateType.Legacy,
low: GAS_LOW_HEX_WEI_MOCK,
medium: GAS_HEX_WEI_MOCK,
high: GAS_HIGH_HEX_WEI_MOCK,
},
} as GasFeeFlowResponse;

Expand Down Expand Up @@ -183,6 +202,78 @@ describe('gas-fees', () => {
expect(updateGasFeeRequest.getGasFeeEstimates).not.toHaveBeenCalled();
});

it('calls getSavedGasFees with the transaction metadata', async () => {
updateGasFeeRequest.txMeta.type = TransactionType.simpleSend;

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.getSavedGasFees).toHaveBeenCalledWith(
updateGasFeeRequest.txMeta,
);
});

it('does not call getSavedGasFees if initial gas fee params are provided', async () => {
updateGasFeeRequest.txMeta.type = TransactionType.simpleSend;
updateGasFeeRequest.txMeta.txParams.maxFeePerGas = GAS_HEX_MOCK;
updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas = GAS_HEX_MOCK;

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.getSavedGasFees).not.toHaveBeenCalled();
});

it('uses saved fee market estimate level if saved gas fees include a level', async () => {
updateGasFeeRequest.txMeta.type = TransactionType.simpleSend;
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
level: GasFeeEstimateLevel.High,
});
mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK);

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe(
GAS_HIGH_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas).toBe(
GAS_HIGH_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
GasFeeEstimateLevel.High,
);
});

it('uses saved legacy estimate level if saved gas fees include a level', async () => {
updateGasFeeRequest.eip1559 = false;
updateGasFeeRequest.txMeta.type = TransactionType.simpleSend;
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
level: GasFeeEstimateLevel.Low,
});
mockGasFeeFlowMockResponse(FLOW_RESPONSE_LEGACY_MOCK);

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.gasPrice).toBe(
GAS_LOW_HEX_WEI_MOCK,
);
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(
GasFeeEstimateLevel.Low,
);
});

it('uses saved gasPrice if saved gas fees include a legacy custom value', async () => {
updateGasFeeRequest.eip1559 = false;
updateGasFeeRequest.txMeta.type = TransactionType.simpleSend;
updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({
level: UserFeeLevel.CUSTOM,
gasPrice: '10',
});

await updateGasFees(updateGasFeeRequest);

expect(updateGasFeeRequest.txMeta.txParams.gasPrice).toBe('0x2540be400');
expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe(UserFeeLevel.CUSTOM);
});

describe('sets maxFeePerGas', () => {
it('to undefined if not eip1559', async () => {
updateGasFeeRequest.eip1559 = false;
Expand Down
82 changes: 66 additions & 16 deletions packages/transaction-controller/src/utils/gas-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import type {
TransactionType,
GasFeeFlow,
} from '../types';
import { GasFeeEstimateType, UserFeeLevel } from '../types';
import {
GasFeeEstimateLevel,
GasFeeEstimateType,
UserFeeLevel,
} from '../types';
import { getGasFeeFlow } from './gas-flow';
import { rpcRequest } from './provider';
import { SWAP_TRANSACTION_TYPES } from './swaps';
Expand All @@ -26,7 +30,9 @@ export type UpdateGasFeesRequest = {
getGasFeeEstimates: (
options: FetchGasFeeEstimateOptions,
) => Promise<GasFeeState>;
getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined;
getSavedGasFees: (
transactionMeta: TransactionMeta,
) => SavedGasFees | undefined;
messenger: TransactionControllerMessenger;
txMeta: TransactionMeta;
};
Expand Down Expand Up @@ -59,11 +65,15 @@ export async function updateGasFees(
const isSwap = SWAP_TRANSACTION_TYPES.includes(
txMeta.type as TransactionType,
);
const savedGasFees = isSwap
? undefined
: request.getSavedGasFees(txMeta.chainId);
const savedGasFees =
isSwap || hasInitialGasFeeParams(initialParams)
? undefined
: request.getSavedGasFees(txMeta);

const suggestedGasFees = await getSuggestedGasFees(request);
const suggestedGasFees = await getSuggestedGasFees({
...request,
savedGasFees,
});

log('Suggested gas fees', suggestedGasFees);

Expand Down Expand Up @@ -140,7 +150,7 @@ function getMaxFeePerGas(request: GetGasFeeRequest): string | undefined {
return undefined;
}

if (savedGasFees) {
if (savedGasFees?.maxBaseFee) {
const maxFeePerGas = gweiDecimalToWeiHex(savedGasFees.maxBaseFee);
log('Using maxFeePerGas from savedGasFees', maxFeePerGas);
return maxFeePerGas;
Expand Down Expand Up @@ -192,7 +202,7 @@ function getMaxPriorityFeePerGas(
return undefined;
}

if (savedGasFees) {
if (savedGasFees?.priorityFee) {
const maxPriorityFeePerGas = gweiDecimalToWeiHex(savedGasFees.priorityFee);
log(
'Using maxPriorityFeePerGas from savedGasFees.priorityFee',
Expand Down Expand Up @@ -244,12 +254,18 @@ function getMaxPriorityFeePerGas(
* @returns The gasPrice value.
*/
function getGasPrice(request: GetGasFeeRequest): string | undefined {
const { eip1559, initialParams, suggestedGasFees } = request;
const { eip1559, initialParams, savedGasFees, suggestedGasFees } = request;

if (eip1559) {
return undefined;
}

if (savedGasFees?.gasPrice) {
const gasPrice = gweiDecimalToWeiHex(savedGasFees.gasPrice);
log('Using gasPrice from savedGasFees.gasPrice', gasPrice);
return gasPrice;
}

if (initialParams.gasPrice) {
log('Using gasPrice from request', initialParams.gasPrice);
return initialParams.gasPrice;
Expand All @@ -275,11 +291,11 @@ function getGasPrice(request: GetGasFeeRequest): string | undefined {
* @param request - The request object.
* @returns The user fee level.
*/
function getUserFeeLevel(request: GetGasFeeRequest): UserFeeLevel | undefined {
function getUserFeeLevel(request: GetGasFeeRequest): string | undefined {
const { initialParams, savedGasFees, suggestedGasFees, txMeta } = request;

if (savedGasFees) {
return UserFeeLevel.CUSTOM;
return savedGasFees.level ?? UserFeeLevel.CUSTOM;
}

if (
Expand Down Expand Up @@ -339,10 +355,16 @@ function updateDefaultGasEstimates(txMeta: TransactionMeta): void {
* @returns The suggested gas fees.
*/
async function getSuggestedGasFees(
request: UpdateGasFeesRequest,
request: UpdateGasFeesRequest & { savedGasFees?: SavedGasFees },
): Promise<SuggestedGasFees> {
const { eip1559, gasFeeFlows, getGasFeeEstimates, messenger, txMeta } =
request;
const {
eip1559,
gasFeeFlows,
getGasFeeEstimates,
messenger,
savedGasFees,
txMeta,
} = request;
const { networkClientId } = txMeta;

if (
Expand Down Expand Up @@ -370,13 +392,19 @@ async function getSuggestedGasFees(
});

const gasFeeEstimateType = response.estimates?.type;
const savedGasFeeEstimateLevel = getSavedGasFeeEstimateLevel(savedGasFees);

switch (gasFeeEstimateType) {
case GasFeeEstimateType.FeeMarket:
return response.estimates.medium;
return (
response.estimates[savedGasFeeEstimateLevel] ??
response.estimates.medium
);
case GasFeeEstimateType.Legacy:
return {
gasPrice: response.estimates.medium,
gasPrice:
response.estimates[savedGasFeeEstimateLevel] ??
response.estimates.medium,
};
case GasFeeEstimateType.GasPrice:
return { gasPrice: response.estimates.gasPrice };
Expand All @@ -401,3 +429,25 @@ async function getSuggestedGasFees(

return { gasPrice };
}

function hasInitialGasFeeParams(initialParams: TransactionParams): boolean {
return [
initialParams.maxFeePerGas,
initialParams.maxPriorityFeePerGas,
initialParams.gasPrice,
].some(Boolean);
}

function getSavedGasFeeEstimateLevel(
savedGasFees: SavedGasFees | undefined,
): GasFeeEstimateLevel {
return isGasFeeEstimateLevel(savedGasFees?.level)
? savedGasFees.level
: GasFeeEstimateLevel.Medium;
}

function isGasFeeEstimateLevel(level: unknown): level is GasFeeEstimateLevel {
return Object.values(GasFeeEstimateLevel).includes(
level as GasFeeEstimateLevel,
);
}
Loading