diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 50cecaa5100..ec19a6982d4 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] +### Changed + +- Zero out source network fees in Relay strategy when quote indicates execute flow ([#8181](https://github.com/MetaMask/core/pull/8181)) + ## [16.5.0] ### Added 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 f54a242207d..3af5f22b493 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 @@ -2163,6 +2163,81 @@ describe('Relay Quotes Utils', () => { }); }); + describe('zeroes source network fees for execute flow', () => { + const ZERO_AMOUNT = { fiat: '0', human: '0', raw: '0', usd: '0' }; + + it('sets source network fees to zero when quote has isExecute', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); + quoteMock.metamask.isExecute = true; + + successfulFetchMock.mockResolvedValue({ + json: async () => quoteMock, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.sourceNetwork).toStrictEqual({ + estimate: ZERO_AMOUNT, + max: ZERO_AMOUNT, + }); + }); + + it('preserves isExecute from quote response on normalized quote', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); + quoteMock.metamask.isExecute = true; + + successfulFetchMock.mockResolvedValue({ + json: async () => quoteMock, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].original.metamask.isExecute).toBe(true); + }); + + it('does not zero source network fees when quote does not have isExecute', async () => { + successfulFetchMock.mockResolvedValue({ + json: async () => QUOTE_MOCK, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].fees.sourceNetwork).not.toStrictEqual({ + estimate: ZERO_AMOUNT, + max: ZERO_AMOUNT, + }); + }); + + it('returns empty gas limits when quote has isExecute', async () => { + const quoteMock = cloneDeep(QUOTE_MOCK); + quoteMock.metamask.isExecute = true; + + successfulFetchMock.mockResolvedValue({ + json: async () => quoteMock, + } as never); + + const result = await getRelayQuotes({ + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: TRANSACTION_META_MOCK, + }); + + expect(result[0].original.metamask.gasLimits).toStrictEqual([]); + }); + }); + it('includes target network fee in quote', 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 c53c8bf030d..5c68becd685 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -473,6 +473,7 @@ async function normalizeQuote( const targetAmount = getFiatValueFromUsd(targetAmountUsd, usdToFiatRate); const metamask = { + ...quote.metamask, gasLimits, }; @@ -566,6 +567,9 @@ function getFiatRates( * transaction's params so that gas estimation and gas-fee-token logic handle * both transactions together. * + * When the execute flow is active (indicated by `quote.metamask.isExecute`), + * network fees are zeroed because the relayer covers them. + * * @param quote - Relay quote. * @param messenger - Controller messenger. * @param request - Quote request. @@ -585,6 +589,14 @@ async function calculateSourceNetworkCost( > { const { from, sourceChainId, sourceTokenAddress } = request; + if (quote.metamask?.isExecute) { + log('Zeroing network fees for execute flow'); + + const zeroAmount = { fiat: '0', human: '0', raw: '0', usd: '0' }; + + return { estimate: zeroAmount, max: zeroAmount, gasLimits: [] }; + } + const relayParams = quote.steps .flatMap((step) => step.items) .map((item) => item.data); 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 d44807a8668..51b474c7d00 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 @@ -14,11 +14,7 @@ import type { TransactionPayQuote, } from '../../types'; import type { FeatureFlags } from '../../utils/feature-flags'; -import { - isEIP7702Chain, - isRelayExecuteEnabled, - getFeatureFlags, -} from '../../utils/feature-flags'; +import { getFeatureFlags } from '../../utils/feature-flags'; import { getLiveTokenBalance, normalizeTokenAddress } from '../../utils/token'; import { collectTransactionIds, @@ -135,9 +131,6 @@ describe('Relay Submit Utils', () => { const getLiveTokenBalanceMock = jest.mocked(getLiveTokenBalance); const normalizeTokenAddressMock = jest.mocked(normalizeTokenAddress); - const isEIP7702ChainMock = jest.mocked(isEIP7702Chain); - const isRelayExecuteEnabledMock = jest.mocked(isRelayExecuteEnabled); - const { addTransactionMock, addTransactionBatchMock, @@ -155,9 +148,6 @@ describe('Relay Submit Utils', () => { beforeEach(() => { jest.resetAllMocks(); - isEIP7702ChainMock.mockReturnValue(false); - isRelayExecuteEnabledMock.mockReturnValue(false); - getLiveTokenBalanceMock.mockResolvedValue('9999999999'); normalizeTokenAddressMock.mockImplementation( (tokenAddress) => tokenAddress, @@ -1022,8 +1012,7 @@ describe('Relay Submit Utils', () => { } as FeatureFlags; beforeEach(() => { - isEIP7702ChainMock.mockReturnValue(true); - isRelayExecuteEnabledMock.mockReturnValue(true); + request.quotes[0].original.metamask.isExecute = true; getDelegationTransactionMock.mockResolvedValue(DELEGATION_RESULT_MOCK); getFeatureFlagsMock.mockReturnValue(FEATURE_FLAGS_MOCK); @@ -1113,6 +1102,10 @@ describe('Relay Submit Utils', () => { ...request.quotes[0], original: { ...ORIGINAL_QUOTE_MOCK, + metamask: { + ...ORIGINAL_QUOTE_MOCK.metamask, + isExecute: true, + }, steps: [ { ...ORIGINAL_QUOTE_MOCK.steps[0], @@ -1261,17 +1254,8 @@ describe('Relay Submit Utils', () => { }); }); - it('uses TransactionController path when chain is not EIP-7702', async () => { - isEIP7702ChainMock.mockReturnValue(false); - - await submitRelayQuotes(request); - - expect(getDelegationTransactionMock).not.toHaveBeenCalled(); - expect(addTransactionMock).toHaveBeenCalledTimes(1); - }); - - it('uses TransactionController path when executeEnabled is false', async () => { - isRelayExecuteEnabledMock.mockReturnValue(false); + it('uses TransactionController path when isExecute is not set', async () => { + request.quotes[0].original.metamask.isExecute = undefined; await submitRelayQuotes(request); 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 3886db2155e..2cf1571863c 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-submit.ts @@ -22,11 +22,7 @@ import type { TransactionPayControllerMessenger, TransactionPayQuote, } from '../../types'; -import { - getFeatureFlags, - isEIP7702Chain, - isRelayExecuteEnabled, -} from '../../utils/feature-flags'; +import { getFeatureFlags } from '../../utils/feature-flags'; import { getLiveTokenBalance, normalizeTokenAddress, @@ -320,12 +316,7 @@ async function submitTransactions( ] : normalizedParams; - const { sourceChainId } = quote.request; - - if ( - isRelayExecuteEnabled(messenger) && - isEIP7702Chain(messenger, sourceChainId) - ) { + if (quote.original.metamask.isExecute) { return await submitViaRelayExecute( quote, transaction, diff --git a/packages/transaction-pay-controller/src/strategy/relay/types.ts b/packages/transaction-pay-controller/src/strategy/relay/types.ts index 0b5a412da32..c8a0ed5cade 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/types.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/types.ts @@ -74,6 +74,7 @@ export type RelayQuote = { }; metamask: { gasLimits: number[]; + isExecute?: boolean; isMaxGasStation?: boolean; }; request: RelayQuoteRequest;