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
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ async function normalizeQuote(
const targetAmount = getFiatValueFromUsd(targetAmountUsd, usdToFiatRate);

const metamask = {
...quote.metamask,
gasLimits,
};

Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -155,9 +148,6 @@ describe('Relay Submit Utils', () => {
beforeEach(() => {
jest.resetAllMocks();

isEIP7702ChainMock.mockReturnValue(false);
isRelayExecuteEnabledMock.mockReturnValue(false);

getLiveTokenBalanceMock.mockResolvedValue('9999999999');
normalizeTokenAddressMock.mockImplementation(
(tokenAddress) => tokenAddress,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export type RelayQuote = {
};
metamask: {
gasLimits: number[];
isExecute?: boolean;
isMaxGasStation?: boolean;
};
request: RelayQuoteRequest;
Expand Down
Loading