From fac8dec4f77a14c2e5df10c06d825c3e1272c858 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Wed, 19 Nov 2025 23:51:05 +0000 Subject: [PATCH 1/6] Use gas fee token on relay source transactions --- .../src/strategy/relay/relay-quotes.ts | 89 ++++++++++++++++-- .../src/strategy/relay/relay-submit.ts | 43 +++++---- .../transaction-pay-controller/src/types.ts | 19 ++-- .../src/utils/gas.ts | 90 ++++++++++++------- .../src/utils/totals.ts | 5 ++ 5 files changed, 177 insertions(+), 69 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 8d3bc7bb88d..d4806dc613c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -1,5 +1,5 @@ import { Interface } from '@ethersproject/abi'; -import { successfulFetch } from '@metamask/controller-utils'; +import { successfulFetch, toHex } from '@metamask/controller-utils'; import type { Json } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -27,8 +27,12 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../../types'; -import { calculateGasCost } from '../../utils/gas'; -import { getNativeToken, getTokenFiatRate } from '../../utils/token'; +import { calculateGasCost, calculateGasFeeTokenCost } from '../../utils/gas'; +import { + getNativeToken, + getTokenBalance, + getTokenFiatRate, +} from '../../utils/token'; const log = createModuleLogger(projectLogger, 'relay-strategy'); @@ -225,11 +229,11 @@ function normalizeRequest(request: QuoteRequest) { * @param fullRequest - Full quotes request. * @returns Normalized quote. */ -function normalizeQuote( +async function normalizeQuote( quote: RelayQuote, request: QuoteRequest, fullRequest: PayStrategyGetQuotesRequest, -): TransactionPayQuote { +): Promise> { const { messenger } = fullRequest; const { details } = quote; const { currencyIn } = details; @@ -246,7 +250,8 @@ function normalizeQuote( usdToFiatRate, ); - const sourceNetwork = calculateSourceNetworkCost(quote, messenger); + const { isGasFeeToken: isSourceGasFeeToken, ...sourceNetwork } = + await calculateSourceNetworkCost(quote, messenger, request); const targetNetwork = { usd: '0', @@ -263,6 +268,7 @@ function normalizeQuote( dust, estimatedDuration: details.timeEstimate, fees: { + isSourceGasFeeToken, provider, sourceNetwork, targetNetwork, @@ -376,14 +382,21 @@ function getFeatureFlags(messenger: TransactionPayControllerMessenger) { * * @param quote - Relay quote. * @param messenger - Controller messenger. + * @param request - Quote request. * @returns Total source network cost in USD and fiat. */ -function calculateSourceNetworkCost( +async function calculateSourceNetworkCost( quote: RelayQuote, messenger: TransactionPayControllerMessenger, -): TransactionPayQuote['fees']['sourceNetwork'] { + request: QuoteRequest, +): Promise< + TransactionPayQuote['fees']['sourceNetwork'] & { + isGasFeeToken?: boolean; + } +> { + const { from, sourceChainId, sourceTokenAddress } = request; const allParams = quote.steps.flatMap((s) => s.items).map((i) => i.data); - const { chainId } = allParams[0]; + const { chainId, data, to, value } = allParams[0]; const totalGasLimit = calculateSourceNetworkGasLimit(allParams); const estimate = calculateGasCost({ @@ -399,6 +412,64 @@ function calculateSourceNetworkCost( isMax: true, }); + const nativeBalance = getTokenBalance( + messenger, + from, + sourceChainId, + getNativeToken(sourceChainId), + ); + + if (nativeBalance && new BigNumber(nativeBalance).isLessThan(max.raw)) { + log('Checking gas fee tokens as insufficient native balance', { + nativeBalance, + max: max.raw, + }); + + const gasFeeTokens = await messenger.call( + 'TransactionController:getGasFeeTokens', + { + chainId: sourceChainId, + data, + from, + to, + value: toHex(value ?? '0'), + }, + ); + + log('Source gas fee tokens', { gasFeeTokens }); + + const gasFeeToken = gasFeeTokens.find( + (t) => t.tokenAddress.toLowerCase() === sourceTokenAddress.toLowerCase(), + ); + + if (!gasFeeToken) { + log('No matching gas fee token found', { + sourceTokenAddress, + gasFeeTokens, + }); + + return { estimate, max }; + } + + const gasFeeTokenCost = calculateGasFeeTokenCost({ + chainId: sourceChainId, + gasFeeToken, + messenger, + }); + + if (gasFeeTokenCost) { + log('Using gas fee token for source network', { + gasFeeTokenCost, + }); + + return { + isGasFeeToken: true, + estimate: gasFeeTokenCost, + max: gasFeeTokenCost, + }; + } + } + return { estimate, max }; } diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 45030386d97..818df7870d8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -21,6 +21,7 @@ import { projectLogger } from '../../logger'; import type { PayStrategyExecuteRequest, TransactionPayControllerMessenger, + TransactionPayQuote, } from '../../types'; import { collectTransactionIds, @@ -50,7 +51,7 @@ export async function submitRelayQuotes( for (const quote of quotes) { ({ transactionHash } = await executeSingleQuote( - quote.original, + quote, messenger, transaction, )); @@ -68,22 +69,12 @@ export async function submitRelayQuotes( * @returns An object containing the transaction hash if available. */ async function executeSingleQuote( - quote: RelayQuote, + quote: TransactionPayQuote, messenger: TransactionPayControllerMessenger, transaction: TransactionMeta, ) { log('Executing single quote', quote); - const { kind } = quote.steps[0]; - - if (kind !== 'transaction') { - throw new Error(`Unsupported step kind: ${kind as string}`); - } - - const transactionParams = quote.steps[0].items[0].data; - const chainId = toHex(transactionParams.chainId); - const from = transactionParams.from as Hex; - updateTransaction( { transactionId: transaction.id, @@ -95,9 +86,9 @@ async function executeSingleQuote( }, ); - await submitTransactions(quote, chainId, from, transaction.id, messenger); + await submitTransactions(quote, transaction.id, messenger); - const targetHash = await waitForRelayCompletion(quote); + const targetHash = await waitForRelayCompletion(quote.original); log('Relay request completed', targetHash); @@ -179,37 +170,37 @@ function normalizeParams( * Submit transactions for a relay quote. * * @param quote - Relay quote. - * @param chainId - ID of the chain. - * @param from - Address of the sender. * @param parentTransactionId - ID of the parent transaction. * @param messenger - Controller messenger. * @returns Hash of the last submitted transaction. */ async function submitTransactions( - quote: RelayQuote, - chainId: Hex, - from: Hex, + quote: TransactionPayQuote, parentTransactionId: string, messenger: TransactionPayControllerMessenger, ): Promise { - const params = quote.steps.flatMap((s) => s.items).map((i) => i.data); + const params = quote.original.steps + .flatMap((s) => s.items) + .map((i) => i.data); + const normalizedParams = params.map(normalizeParams); const transactionIds: string[] = []; + const { from, sourceChainId, sourceTokenAddress } = quote.request; const networkClientId = messenger.call( 'NetworkController:findNetworkClientIdByChainId', - chainId, + sourceChainId, ); log('Adding transactions', { normalizedParams, - chainId, + sourceChainId, from, networkClientId, }); const { end } = collectTransactionIds( - chainId, + sourceChainId, from, messenger, (transactionId) => { @@ -234,11 +225,16 @@ async function submitTransactions( let result: { result: Promise } | undefined; + const gasFeeToken = quote.fees.isSourceGasFeeToken + ? sourceTokenAddress + : undefined; + if (params.length === 1) { result = await messenger.call( 'TransactionController:addTransaction', normalizedParams[0], { + gasFeeToken, networkClientId, origin: ORIGIN_METAMASK, requireApproval: false, @@ -247,6 +243,7 @@ async function submitTransactions( } else { await messenger.call('TransactionController:addTransactionBatch', { from, + gasFeeToken, networkClientId, origin: ORIGIN_METAMASK, requireApproval: false, diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 05c3fd1e1c0..fa13bada97b 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -21,12 +21,15 @@ import type { TransactionControllerAddTransactionBatchAction, TransactionControllerUnapprovedTransactionAddedEvent, } from '@metamask/transaction-controller'; -import type { TransactionControllerGetStateAction } from '@metamask/transaction-controller'; -import type { TransactionControllerStateChangeEvent } from '@metamask/transaction-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller'; -import type { TransactionControllerAddTransactionAction } from '@metamask/transaction-controller'; -import type { TransactionControllerUpdateTransactionAction } from '@metamask/transaction-controller'; -import type { BatchTransaction } from '@metamask/transaction-controller'; +import type { + BatchTransaction, + TransactionControllerAddTransactionAction, + TransactionControllerGetGasFeeTokensAction, + TransactionControllerGetStateAction, + TransactionControllerStateChangeEvent, + TransactionControllerUpdateTransactionAction, + TransactionMeta, +} from '@metamask/transaction-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Draft } from 'immer'; @@ -47,6 +50,7 @@ export type AllowedActions = | TokensControllerGetStateAction | TransactionControllerAddTransactionAction | TransactionControllerAddTransactionBatchAction + | TransactionControllerGetGasFeeTokensAction | TransactionControllerGetStateAction | TransactionControllerUpdateTransactionAction; @@ -273,6 +277,9 @@ export type QuoteRequest = { /** Fees associated with a transaction pay quote. */ export type TransactionPayFees = { + /** Whether a gas fee token is used to pay source network fees. */ + isSourceGasFeeToken?: boolean; + /** Whether a gas fee token is used to pay target network fees. */ isTargetGasFeeToken?: boolean; diff --git a/packages/transaction-pay-controller/src/utils/gas.ts b/packages/transaction-pay-controller/src/utils/gas.ts index 04a350de421..d1b456e2d6a 100644 --- a/packages/transaction-pay-controller/src/utils/gas.ts +++ b/packages/transaction-pay-controller/src/utils/gas.ts @@ -1,6 +1,9 @@ import { toHex } from '@metamask/controller-utils'; import type { GasFeeEstimates } from '@metamask/gas-fee-controller'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import type { + GasFeeToken, + TransactionMeta, +} from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; @@ -67,7 +70,7 @@ export function calculateTransactionGasCost( const hasBalance = new BigNumber(nativeBalance).gte(max.raw); - const gasFeeTokenCost = calculateGasFeeTokenCost({ + const gasFeeTokenCost = calculateTransactionGasFeeTokenCost({ hasBalance, messenger, transaction, @@ -155,6 +158,55 @@ export function calculateGasCost(request: { }; } +/** + * Calculate the cost of a gas fee token on a transaction. + * + * @param request - Request parameters. + * @param request.chainId - Chain ID. + * @param request.gasFeeToken - Gas fee token to calculate cost for. + * @param request.messenger - Controller messenger. + * @returns Cost of the gas fee token. + */ +export function calculateGasFeeTokenCost({ + chainId, + gasFeeToken, + messenger, +}: { + chainId: Hex; + gasFeeToken: GasFeeToken; + messenger: TransactionPayControllerMessenger; +}): (Amount & { isGasFeeToken?: boolean }) | undefined { + const tokenInfo = getTokenInfo(messenger, gasFeeToken.tokenAddress, chainId); + + const tokenFiatRate = getTokenFiatRate( + messenger, + gasFeeToken.tokenAddress, + chainId, + ); + + if (!tokenFiatRate || !tokenInfo) { + log('Cannot get gas fee token info'); + return undefined; + } + + const rawValue = new BigNumber(gasFeeToken.amount); + const raw = rawValue.toString(10); + + const humanValue = rawValue.shiftedBy(-tokenInfo.decimals); + const human = humanValue.toString(10); + + const fiat = humanValue.multipliedBy(tokenFiatRate.fiatRate).toString(10); + const usd = humanValue.multipliedBy(tokenFiatRate.usdRate).toString(10); + + return { + isGasFeeToken: true, + fiat, + human, + raw, + usd, + }; +} + /** * Get gas fee estimates for a given chain. * @@ -197,7 +249,7 @@ function getGasFee(chainId: Hex, messenger: TransactionPayControllerMessenger) { * @param request.transaction - Transaction to calculate gas fee token cost for. * @returns Cost of the gas fee token. */ -function calculateGasFeeTokenCost({ +function calculateTransactionGasFeeTokenCost({ hasBalance, messenger, transaction, @@ -236,33 +288,9 @@ function calculateGasFeeTokenCost({ return undefined; } - const tokenInfo = getTokenInfo(messenger, selectedGasFeeToken, chainId); - - const tokenFiatRate = getTokenFiatRate( - messenger, - selectedGasFeeToken, + return calculateGasFeeTokenCost({ chainId, - ); - - if (!tokenFiatRate || !tokenInfo) { - log('Cannot get gas fee token info'); - return undefined; - } - - const rawValue = new BigNumber(gasFeeToken.amount); - const raw = rawValue.toString(10); - - const humanValue = rawValue.shiftedBy(-tokenInfo.decimals); - const human = humanValue.toString(10); - - const fiat = humanValue.multipliedBy(tokenFiatRate.fiatRate).toString(10); - const usd = humanValue.multipliedBy(tokenFiatRate.usdRate).toString(10); - - return { - isGasFeeToken: true, - fiat, - human, - raw, - usd, - }; + gasFeeToken, + messenger, + }); } diff --git a/packages/transaction-pay-controller/src/utils/totals.ts b/packages/transaction-pay-controller/src/utils/totals.ts index 021f67113f1..5fee22c2fc4 100644 --- a/packages/transaction-pay-controller/src/utils/totals.ts +++ b/packages/transaction-pay-controller/src/utils/totals.ts @@ -75,6 +75,10 @@ export function calculateTotals({ sumProperty(quotes, (quote) => quote.estimatedDuration), ); + const isSourceGasFeeToken = quotes.some( + (quote) => quote.fees.isSourceGasFeeToken, + ); + const isTargetGasFeeToken = targetNetworkFee.isGasFeeToken || quotes.some((quote) => quote.fees.isTargetGasFeeToken); @@ -82,6 +86,7 @@ export function calculateTotals({ return { estimatedDuration, fees: { + isSourceGasFeeToken, isTargetGasFeeToken, provider: providerFee, sourceNetwork: { From 6fabc7cd797abd7c9583e9c35044788bab8e9dbb Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 00:35:21 +0000 Subject: [PATCH 2/6] Fix unit tests --- .../src/strategy/relay/relay-submit.test.ts | 10 ++++++++++ .../src/strategy/relay/relay-submit.ts | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index a97d5792621..03b96b35cab 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -34,6 +34,8 @@ const TRANSACTION_HASH_MOCK = '0x1234'; const ENDPOINT_MOCK = '/test123'; const ORIGINAL_TRANSACTION_ID_MOCK = '456-789'; const FROM_MOCK = '0xabcde' as Hex; +const CHAIN_ID_MOCK = '0x1' as Hex; +const TOKEN_ADDRESS_MOCK = '0x123' as Hex; const TRANSACTION_META_MOCK = { id: '123-456', @@ -87,7 +89,15 @@ const STATUS_RESPONSE_MOCK = { const REQUEST_MOCK: PayStrategyExecuteRequest = { quotes: [ { + fees: { + sourceNetwork: {}, + }, original: ORIGINAL_QUOTE_MOCK, + request: { + from: FROM_MOCK, + sourceChainId: CHAIN_ID_MOCK, + sourceTokenAddress: TOKEN_ADDRESS_MOCK, + }, } as TransactionPayQuote, ], messenger: {} as TransactionPayControllerMessenger, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts index 818df7870d8..e24558d29a8 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -179,11 +179,16 @@ async function submitTransactions( parentTransactionId: string, messenger: TransactionPayControllerMessenger, ): Promise { - const params = quote.original.steps - .flatMap((s) => s.items) - .map((i) => i.data); + const { steps } = quote.original; + const params = steps.flatMap((s) => s.items).map((i) => i.data); + const invalidKind = steps.find((s) => s.kind !== 'transaction')?.kind; + + if (invalidKind) { + throw new Error(`Unsupported step kind: ${invalidKind}`); + } const normalizedParams = params.map(normalizeParams); + const transactionIds: string[] = []; const { from, sourceChainId, sourceTokenAddress } = quote.request; From 051fd47e746f0c8ae6e9bfa0665f12e6b0561352 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 10:33:07 +0000 Subject: [PATCH 3/6] Add unit tests --- .../src/strategy/relay/relay-quotes.test.ts | 318 ++++++++++++++---- .../src/strategy/relay/relay-quotes.ts | 88 ++--- .../src/strategy/relay/relay-submit.test.ts | 33 +- .../src/tests/messenger-mock.ts | 11 + .../src/utils/gas.test.ts | 48 ++- .../src/utils/totals.test.ts | 2 + 6 files changed, 386 insertions(+), 114 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 1730061e912..76da8cb196f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -1,5 +1,8 @@ -import { successfulFetch } from '@metamask/controller-utils'; -import type { TransactionMeta } from '@metamask/transaction-controller'; +import { successfulFetch, toHex } from '@metamask/controller-utils'; +import type { + GasFeeToken, + TransactionMeta, +} from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { cloneDeep } from 'lodash'; @@ -17,8 +20,16 @@ import type { GetDelegationTransactionCallback, QuoteRequest, } from '../../types'; -import { calculateGasCost, calculateTransactionGasCost } from '../../utils/gas'; -import { getNativeToken, getTokenFiatRate } from '../../utils/token'; +import { + calculateGasCost, + calculateGasFeeTokenCost, + calculateTransactionGasCost, +} from '../../utils/gas'; +import { + getNativeToken, + getTokenBalance, + getTokenFiatRate, +} from '../../utils/token'; jest.mock('../../utils/token'); jest.mock('../../utils/gas'); @@ -100,11 +111,18 @@ const DELEGATION_RESULT_MOCK = { value: '0x333' as Hex, } as Awaited>; +const GAS_FEE_TOKEN_MOCK = { + amount: toHex(1230000), + tokenAddress: '0xabc' as Hex, +} as GasFeeToken; + describe('Relay Quotes Utils', () => { const successfulFetchMock = jest.mocked(successfulFetch); const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); const calculateGasCostMock = jest.mocked(calculateGasCost); + const calculateGasFeeTokenCostMock = jest.mocked(calculateGasFeeTokenCost); const getNativeTokenMock = jest.mocked(getNativeToken); + const getTokenBalanceMock = jest.mocked(getTokenBalance); const calculateTransactionGasCostMock = jest.mocked( calculateTransactionGasCost, @@ -113,6 +131,7 @@ describe('Relay Quotes Utils', () => { const { messenger, getDelegationTransactionMock, + getGasFeeTokensMock, getRemoteFeatureFlagControllerStateMock, } = getMessengerMock(); @@ -138,12 +157,20 @@ describe('Relay Quotes Utils', () => { usd: '3.45', }); + calculateGasFeeTokenCostMock.mockReturnValue({ + fiat: '5.56', + human: '2.725', + raw: '2725000000000000', + usd: '4.45', + }); + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ cacheTimestamp: 0, remoteFeatureFlags: {}, }); getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); + getGasFeeTokensMock.mockResolvedValue([]); }); describe('getRelayQuotes', () => { @@ -349,89 +376,242 @@ describe('Relay Quotes Utils', () => { }); }); - it('includes source network fee in quote', async () => { - successfulFetchMock.mockResolvedValue({ - json: async () => QUOTE_MOCK, - } as never); + describe('includes source network fee', () => { + it('in quote', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); - const result = await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + max: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + }); }); - expect(result[0].fees.sourceNetwork).toStrictEqual({ - estimate: { - fiat: '4.56', - human: '1.725', - raw: '1725000000000000', - usd: '3.45', - }, - max: { - fiat: '4.56', - human: '1.725', - raw: '1725000000000000', - usd: '3.45', - }, + it('using fallback if gas missing', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); + delete quoteMock.steps[0].items[0].data.gas; + + successfulFetchMock.mockResolvedValue({ + json: async () => quoteMock, + } as never); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(calculateGasCostMock).toHaveBeenCalledWith( + expect.objectContaining({ gas: 900000 }), + ); }); - }); - it('includes source network fee in quote using fallback if gas missing', async () => { - const quoteMock = cloneDeep(QUOTE_MOCK); - delete quoteMock.steps[0].items[0].data.gas; + it('using gas total from multiple transactions', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); - successfulFetchMock.mockResolvedValue({ - json: async () => quoteMock, - } as never); + quoteMock.steps[0].items.push({ + data: { + gas: '480000', + }, + } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, + quoteMock.steps.push({ + items: [ + { + data: { + gas: '1000', + }, + }, + { + data: { + gas: '2000', + }, + }, + ], + } as never); + + successfulFetchMock.mockResolvedValue({ + json: async () => quoteMock, + } as never); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(calculateGasCostMock).toHaveBeenCalledWith( + expect.objectContaining({ gas: 504000 }), + ); }); - expect(calculateGasCostMock).toHaveBeenCalledWith( - expect.objectContaining({ gas: 900000 }), - ); - }); + it('using gas fee token cost if insufficient native balance', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); - it('includes source network fee using gas total from multiple transactions', async () => { - const quoteMock = cloneDeep(QUOTE_MOCK); + getTokenBalanceMock.mockReturnValue('1724999999999999'); + getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); - quoteMock.steps[0].items.push({ - data: { - gas: '480000', - }, - } as never); + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.isSourceGasFeeToken).toBe(true); + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: { + fiat: '5.56', + human: '2.725', + raw: '2725000000000000', + usd: '4.45', + }, + max: { + fiat: '5.56', + human: '2.725', + raw: '2725000000000000', + usd: '4.45', + }, + }); + }); - quoteMock.steps.push({ - items: [ - { - data: { - gas: '1000', - }, + it('not using gas fee token if sufficient native balance', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + getTokenBalanceMock.mockReturnValue('1725000000000000'); + getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', }, - { - data: { - gas: '2000', - }, + max: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', }, - ], - } as never); + }); + }); - successfulFetchMock.mockResolvedValue({ - json: async () => quoteMock, - } as never); + it('not using gas fee token if source token not found', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); - await getRelayQuotes({ - messenger, - requests: [QUOTE_REQUEST_MOCK], - transaction: TRANSACTION_META_MOCK, + getTokenBalanceMock.mockReturnValue('1724999999999999'); + getGasFeeTokensMock.mockResolvedValue([ + { ...GAS_FEE_TOKEN_MOCK, tokenAddress: '0xdef' as Hex }, + ]); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + max: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + }); }); - expect(calculateGasCostMock).toHaveBeenCalledWith( - expect.objectContaining({ gas: 504000 }), - ); + it('not using gas fee token if calculation fails', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + getTokenBalanceMock.mockReturnValue('1724999999999999'); + getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); + calculateGasFeeTokenCostMock.mockReturnValue(undefined); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.isSourceGasFeeToken).toBeUndefined(); + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + max: { + fiat: '4.56', + human: '1.725', + raw: '1725000000000000', + usd: '3.45', + }, + }); + }); + + it('using gas fee token cost with normalized value', async () => { + const quote = cloneDeep(QUOTE_MOCK); + quote.steps[0].items[0].data.value = undefined as never; + + successfulFetchMock.mockResolvedValue({ + json: async () => quote, + } as never); + + getTokenBalanceMock.mockReturnValue('1724999999999999'); + getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(getGasFeeTokensMock).toHaveBeenCalledWith( + expect.objectContaining({ + value: '0x0', + }), + ); + }); }); it('includes target network fee in quote', async () => { diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index d4806dc613c..90befc0d33a 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -419,58 +419,60 @@ async function calculateSourceNetworkCost( getNativeToken(sourceChainId), ); - if (nativeBalance && new BigNumber(nativeBalance).isLessThan(max.raw)) { - log('Checking gas fee tokens as insufficient native balance', { - nativeBalance, - max: max.raw, - }); - - const gasFeeTokens = await messenger.call( - 'TransactionController:getGasFeeTokens', - { - chainId: sourceChainId, - data, - from, - to, - value: toHex(value ?? '0'), - }, - ); + if (new BigNumber(nativeBalance).isGreaterThanOrEqualTo(max.raw)) { + return { estimate, max }; + } - log('Source gas fee tokens', { gasFeeTokens }); + log('Checking gas fee tokens as insufficient native balance', { + nativeBalance, + max: max.raw, + }); - const gasFeeToken = gasFeeTokens.find( - (t) => t.tokenAddress.toLowerCase() === sourceTokenAddress.toLowerCase(), - ); + const gasFeeTokens = await messenger.call( + 'TransactionController:getGasFeeTokens', + { + chainId: sourceChainId, + data, + from, + to, + value: toHex(value ?? '0'), + }, + ); - if (!gasFeeToken) { - log('No matching gas fee token found', { - sourceTokenAddress, - gasFeeTokens, - }); + log('Source gas fee tokens', { gasFeeTokens }); - return { estimate, max }; - } + const gasFeeToken = gasFeeTokens.find( + (t) => t.tokenAddress.toLowerCase() === sourceTokenAddress.toLowerCase(), + ); - const gasFeeTokenCost = calculateGasFeeTokenCost({ - chainId: sourceChainId, - gasFeeToken, - messenger, + if (!gasFeeToken) { + log('No matching gas fee token found', { + sourceTokenAddress, + gasFeeTokens, }); - if (gasFeeTokenCost) { - log('Using gas fee token for source network', { - gasFeeTokenCost, - }); - - return { - isGasFeeToken: true, - estimate: gasFeeTokenCost, - max: gasFeeTokenCost, - }; - } + return { estimate, max }; + } + + const gasFeeTokenCost = calculateGasFeeTokenCost({ + chainId: sourceChainId, + gasFeeToken, + messenger, + }); + + if (!gasFeeTokenCost) { + return { estimate, max }; } - return { estimate, max }; + log('Using gas fee token for source network', { + gasFeeTokenCost, + }); + + return { + isGasFeeToken: true, + estimate: gasFeeTokenCost, + max: gasFeeTokenCost, + }; } /** diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts index 03b96b35cab..fd168f4e0a2 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.test.ts @@ -177,7 +177,21 @@ describe('Relay Submit Utils', () => { ); }); - it('adds batch transaction if multiple params', async () => { + it('adds transaction with gas fee token if isSourceGasFeeToken', async () => { + request.quotes[0].fees.isSourceGasFeeToken = true; + + await submitRelayQuotes(request); + + expect(addTransactionMock).toHaveBeenCalledTimes(1); + expect(addTransactionMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + gasFeeToken: TOKEN_ADDRESS_MOCK, + }), + ); + }); + + it('adds transaction batch if multiple params', async () => { request.quotes[0].original.steps[0].items.push({ ...request.quotes[0].original.steps[0].items[0], }); @@ -212,6 +226,23 @@ describe('Relay Submit Utils', () => { }); }); + it('adds transaction batch with gas fee token if isSourceGasFeeToken', async () => { + request.quotes[0].original.steps[0].items.push({ + ...request.quotes[0].original.steps[0].items[0], + }); + + request.quotes[0].fees.isSourceGasFeeToken = true; + + await submitRelayQuotes(request); + + expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); + expect(addTransactionBatchMock).toHaveBeenCalledWith( + expect.objectContaining({ + gasFeeToken: TOKEN_ADDRESS_MOCK, + }), + ); + }); + it('adds transaction if params missing', async () => { request.quotes[0].original.steps[0].items[0].data.value = undefined as never; diff --git a/packages/transaction-pay-controller/src/tests/messenger-mock.ts b/packages/transaction-pay-controller/src/tests/messenger-mock.ts index c3ecd2b7a17..120ace8cec9 100644 --- a/packages/transaction-pay-controller/src/tests/messenger-mock.ts +++ b/packages/transaction-pay-controller/src/tests/messenger-mock.ts @@ -15,6 +15,7 @@ import type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote import type { TransactionControllerAddTransactionAction, TransactionControllerAddTransactionBatchAction, + TransactionControllerGetGasFeeTokensAction, TransactionControllerGetStateAction, } from '@metamask/transaction-controller'; import type { TransactionControllerUpdateTransactionAction } from '@metamask/transaction-controller'; @@ -111,6 +112,10 @@ export function getMessengerMock({ TransactionPayControllerGetDelegationTransactionAction['handler'] > = jest.fn(); + const getGasFeeTokensMock: jest.MockedFn< + TransactionControllerGetGasFeeTokensAction['handler'] + > = jest.fn(); + const messenger: RootMessenger = new Messenger({ namespace: MOCK_ANY_NAMESPACE, }); @@ -210,6 +215,11 @@ export function getMessengerMock({ 'TransactionPayController:getDelegationTransaction', getDelegationTransactionMock, ); + + messenger.registerActionHandler( + 'TransactionController:getGasFeeTokens', + getGasFeeTokensMock, + ); } const publish = messenger.publish.bind(messenger); @@ -225,6 +235,7 @@ export function getMessengerMock({ getCurrencyRateControllerStateMock, getDelegationTransactionMock, getGasFeeControllerStateMock, + getGasFeeTokensMock, getNetworkClientByIdMock, getRemoteFeatureFlagControllerStateMock, getStrategyMock, diff --git a/packages/transaction-pay-controller/src/utils/gas.test.ts b/packages/transaction-pay-controller/src/utils/gas.test.ts index afcba93be99..3719e728126 100644 --- a/packages/transaction-pay-controller/src/utils/gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/gas.test.ts @@ -2,7 +2,11 @@ import { toHex } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; import { clone, cloneDeep } from 'lodash'; -import { calculateGasCost, calculateTransactionGasCost } from './gas'; +import { + calculateGasCost, + calculateGasFeeTokenCost, + calculateTransactionGasCost, +} from './gas'; import { getTokenBalance, getTokenFiatRate, getTokenInfo } from './token'; import type { GasFeeEstimates } from '../../../gas-fee-controller/src'; import type { @@ -384,4 +388,46 @@ describe('Gas Utils', () => { }); }); }); + + describe('calculateGasFeeTokenCost', () => { + it('returns gas fee token cost', () => { + const result = calculateGasFeeTokenCost({ + chainId: CHAIN_ID_MOCK, + gasFeeToken: GAS_FEE_TOKEN_MOCK, + messenger, + }); + + expect(result).toStrictEqual({ + isGasFeeToken: true, + fiat: '2460', + human: '1.23', + raw: '1230000', + usd: '4920', + }); + }); + + it('returns undefined if token info is unavailable', () => { + getTokenInfoMock.mockReturnValue(undefined); + + const result = calculateGasFeeTokenCost({ + chainId: CHAIN_ID_MOCK, + gasFeeToken: GAS_FEE_TOKEN_MOCK, + messenger, + }); + + expect(result).toBeUndefined(); + }); + + it('returns undefined if fiat rate is unavailable', () => { + getTokenFiatRateMock.mockReturnValue(undefined); + + const result = calculateGasFeeTokenCost({ + chainId: CHAIN_ID_MOCK, + gasFeeToken: GAS_FEE_TOKEN_MOCK, + messenger, + }); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/packages/transaction-pay-controller/src/utils/totals.test.ts b/packages/transaction-pay-controller/src/utils/totals.test.ts index 93111ab94b8..8b7fc898c70 100644 --- a/packages/transaction-pay-controller/src/utils/totals.test.ts +++ b/packages/transaction-pay-controller/src/utils/totals.test.ts @@ -74,6 +74,7 @@ const QUOTE_2_MOCK: TransactionPayQuote = { }, estimatedDuration: 234, fees: { + isSourceGasFeeToken: true, provider: { fiat: '7.77', usd: '8.88', @@ -232,6 +233,7 @@ describe('Totals Utils', () => { expect(result.sourceAmount.fiat).toBe('20.9'); expect(result.sourceAmount.usd).toBe('23.02'); + expect(result.fees.isSourceGasFeeToken).toBe(true); }); }); }); From 5593db3dc106211fed67927024bf90cfa5cd3a8b Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 11:52:43 +0000 Subject: [PATCH 4/6] Support multiple params --- .../src/strategy/relay/relay-quotes.test.ts | 33 ++++++++++++++++ .../src/strategy/relay/relay-quotes.ts | 23 ++++++++++- .../src/utils/gas.test.ts | 39 +------------------ .../src/utils/gas.ts | 21 +++------- 4 files changed, 63 insertions(+), 53 deletions(-) diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 76da8cb196f..4ac4e610870 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -113,6 +113,7 @@ const DELEGATION_RESULT_MOCK = { const GAS_FEE_TOKEN_MOCK = { amount: toHex(1230000), + gas: toHex(21000), tokenAddress: '0xabc' as Hex, } as GasFeeToken; @@ -493,6 +494,38 @@ describe('Relay Quotes Utils', () => { }); }); + it('using estimated gas fee token cost if insufficient native balance and batch', async () => { + const quote = cloneDeep(QUOTE_MOCK); + + quote.steps[0].items.push({ + data: { + gas: '21000', + }, + } as never); + + successfulFetchMock.mockResolvedValue({ + json: async () => quote, + } as never); + + getTokenBalanceMock.mockReturnValue('1724999999999999'); + getGasFeeTokensMock.mockResolvedValue([GAS_FEE_TOKEN_MOCK]); + + await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(calculateGasFeeTokenCostMock).toHaveBeenCalledWith( + expect.objectContaining({ + gasFeeToken: { + ...GAS_FEE_TOKEN_MOCK, + amount: toHex(1230000 * 2), + }, + }), + ); + }); + it('not using gas fee token if sufficient native balance', async () => { successfulFetchMock.mockResolvedValue({ json: async () => QUOTE_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 90befc0d33a..7f6c60cdb91 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -454,9 +454,30 @@ async function calculateSourceNetworkCost( return { estimate, max }; } + let finalAmount = gasFeeToken.amount; + + if (allParams.length > 1) { + const gasRate = new BigNumber(gasFeeToken.amount, 16).dividedBy( + gasFeeToken.gas, + 16, + ); + + const finalAmountValue = gasRate.multipliedBy(totalGasLimit); + + finalAmount = toHex(finalAmountValue.toFixed(0)); + + log('Estimated gas fee token amount for batch', { + finalAmount: finalAmountValue.toString(10), + gasRate: gasRate.toString(10), + totalGasLimit, + }); + } + + const finalGasFeeToken = { ...gasFeeToken, amount: finalAmount }; + const gasFeeTokenCost = calculateGasFeeTokenCost({ chainId: sourceChainId, - gasFeeToken, + gasFeeToken: finalGasFeeToken, messenger, }); diff --git a/packages/transaction-pay-controller/src/utils/gas.test.ts b/packages/transaction-pay-controller/src/utils/gas.test.ts index 3719e728126..96318f56569 100644 --- a/packages/transaction-pay-controller/src/utils/gas.test.ts +++ b/packages/transaction-pay-controller/src/utils/gas.test.ts @@ -7,7 +7,7 @@ import { calculateGasFeeTokenCost, calculateTransactionGasCost, } from './gas'; -import { getTokenBalance, getTokenFiatRate, getTokenInfo } from './token'; +import { getTokenBalance, getTokenFiatRate } from './token'; import type { GasFeeEstimates } from '../../../gas-fee-controller/src'; import type { GasFeeToken, @@ -27,6 +27,7 @@ const TOKEN_ADDRESS_MOCK = '0x789' as Hex; const GAS_FEE_TOKEN_MOCK = { amount: toHex(1230000), + decimals: 6, tokenAddress: TOKEN_ADDRESS_MOCK, } as GasFeeToken; @@ -55,7 +56,6 @@ const GAS_FEE_CONTROLLER_STATE_MOCK = { describe('Gas Utils', () => { const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); - const getTokenInfoMock = jest.mocked(getTokenInfo); const getTokenBalanceMock = jest.mocked(getTokenBalance); const { messenger, getGasFeeControllerStateMock } = getMessengerMock(); @@ -69,11 +69,6 @@ describe('Gas Utils', () => { usdRate: '4000', fiatRate: '2000', }); - - getTokenInfoMock.mockReturnValue({ - decimals: 6, - symbol: 'TST', - }); }); describe('calculateGasCost', () => { @@ -326,24 +321,6 @@ describe('Gas Utils', () => { }); }); - it('does not return gas fee token if token info is unavailable', () => { - const transactionMeta = clone(TRANSACTION_META_MOCK); - - transactionMeta.gasFeeTokens = [GAS_FEE_TOKEN_MOCK]; - transactionMeta.selectedGasFeeToken = TOKEN_ADDRESS_MOCK; - - getTokenInfoMock.mockReturnValue(undefined); - - const result = calculateTransactionGasCost(transactionMeta, messenger); - - expect(result).toStrictEqual({ - fiat: '0.273', - human: '0.0001365', - raw: '136500000000000', - usd: '0.546', - }); - }); - it('does not return gas fee token if fiat rate is unavailable', () => { const transactionMeta = clone(TRANSACTION_META_MOCK); @@ -406,18 +383,6 @@ describe('Gas Utils', () => { }); }); - it('returns undefined if token info is unavailable', () => { - getTokenInfoMock.mockReturnValue(undefined); - - const result = calculateGasFeeTokenCost({ - chainId: CHAIN_ID_MOCK, - gasFeeToken: GAS_FEE_TOKEN_MOCK, - messenger, - }); - - expect(result).toBeUndefined(); - }); - it('returns undefined if fiat rate is unavailable', () => { getTokenFiatRateMock.mockReturnValue(undefined); diff --git a/packages/transaction-pay-controller/src/utils/gas.ts b/packages/transaction-pay-controller/src/utils/gas.ts index d1b456e2d6a..8d166de8fd9 100644 --- a/packages/transaction-pay-controller/src/utils/gas.ts +++ b/packages/transaction-pay-controller/src/utils/gas.ts @@ -7,12 +7,7 @@ import type { import type { Hex } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; -import { - getNativeToken, - getTokenBalance, - getTokenFiatRate, - getTokenInfo, -} from './token'; +import { getNativeToken, getTokenBalance, getTokenFiatRate } from './token'; import type { TransactionPayControllerMessenger } from '..'; import { createModuleLogger, projectLogger } from '../logger'; import type { Amount } from '../types'; @@ -176,23 +171,19 @@ export function calculateGasFeeTokenCost({ gasFeeToken: GasFeeToken; messenger: TransactionPayControllerMessenger; }): (Amount & { isGasFeeToken?: boolean }) | undefined { - const tokenInfo = getTokenInfo(messenger, gasFeeToken.tokenAddress, chainId); + const { amount, decimals, tokenAddress } = gasFeeToken; - const tokenFiatRate = getTokenFiatRate( - messenger, - gasFeeToken.tokenAddress, - chainId, - ); + const tokenFiatRate = getTokenFiatRate(messenger, tokenAddress, chainId); - if (!tokenFiatRate || !tokenInfo) { + if (!tokenFiatRate) { log('Cannot get gas fee token info'); return undefined; } - const rawValue = new BigNumber(gasFeeToken.amount); + const rawValue = new BigNumber(amount); const raw = rawValue.toString(10); - const humanValue = rawValue.shiftedBy(-tokenInfo.decimals); + const humanValue = rawValue.shiftedBy(-decimals); const human = humanValue.toString(10); const fiat = humanValue.multipliedBy(tokenFiatRate.fiatRate).toString(10); From 1f667f075d78cb307367540ce0bc337c7e3ff1b6 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 11:55:20 +0000 Subject: [PATCH 5/6] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 6d9d8aa246c..97cb6cb30d9 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Use gas fee token for Relay deposit transactions if insufficient native balance ([#7193](https://github.com/MetaMask/core/pull/7193)) + ## [9.0.0] ### Changed From 6407120d01f386d2817413e9cc117ae5bd6b34e4 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Thu, 20 Nov 2025 11:59:26 +0000 Subject: [PATCH 6/6] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 97cb6cb30d9..1ecd4e04e63 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Use gas fee token for Relay deposit transactions if insufficient native balance ([#7193](https://github.com/MetaMask/core/pull/7193)) + - Add optional `fees.isSourceGasFeeToken` property to `TransactionPayQuote` and `TransactionPayTotals` type. ## [9.0.0]