Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
256129e
feat: tempo transactions poc utils approach
maxime-oe Mar 6, 2026
d85f948
Merge branch 'main' into feat/poc-tempo-transactions-utils
maxime-oe Apr 2, 2026
df3c761
chore: tempo transactions and fee token refactoring
maxime-oe Apr 3, 2026
8e9488b
feat: tempo trim insufficient bal alert logic
maxime-oe Apr 3, 2026
001cf69
fix: getAddTransactionSendCallExtraOptions wrong input
maxime-oe Apr 3, 2026
279da5a
Merge branch 'main' into feat/poc-tempo-transactions-utils
maxime-oe Apr 7, 2026
2458ca1
chore: remove unused imports useInsufficientBalanceAlert.ts
maxime-oe Apr 7, 2026
c5b8b84
fix: useNativeCurrencySymbol to use direct chainId input by default
maxime-oe Apr 7, 2026
2512f8a
fix: Tempo remove redundant checkIsValidTempoTransaction
maxime-oe Apr 7, 2026
b58e67d
[skip ci] Bump version number to 4364
metamaskbot Apr 7, 2026
39e7df1
chore: rename wrong test name in useNativeCurrencySymbol
maxime-oe Apr 7, 2026
700827a
fix: tempo autoselect available gasfeetoken when selected not avail
maxime-oe Apr 7, 2026
a80facd
Merge branch 'main' into feat/poc-tempo-transactions-utils
arafetbenmakhlouf Apr 8, 2026
3067458
feat: tempo transactions optimizations (dedup checks)
maxime-oe Apr 8, 2026
61271f9
feat: add Tempo explorer name
maxime-oe Apr 8, 2026
b7dc473
fix: useAutomaticGasFeeTokenSelect ensure checked is false when selec…
maxime-oe Apr 8, 2026
c8c4b28
Revert "[skip ci] Bump version number to 4364"
maxime-oe Apr 8, 2026
ce049d3
Merge branch 'main' into feat/poc-tempo-transactions-utils
joaoloureirop Apr 8, 2026
4ca642a
[skip ci] Bump version number to 4395
metamaskbot Apr 9, 2026
16e149c
[skip ci] Bump version number to 4403
metamaskbot Apr 9, 2026
f338bb8
Merge branch 'main' into
maxime-oe Apr 9, 2026
00a09e1
Revert "[skip ci] Bump version number to 4403"
maxime-oe Apr 9, 2026
89667cf
Revert "[skip ci] Bump version number to 4395"
maxime-oe Apr 9, 2026
f9be910
Merge branch 'main' into feat/poc-tempo-transactions-utils
maxime-oe Apr 9, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,18 @@ describe('GasFeeTokenModal', () => {
transactionId = 'test-transaction-id',
gasFeeTokens = [],
selectedGasFeeToken = undefined,
excludeNativeTokenForFee,
}: {
transactionId?: string;
gasFeeTokens?: GasFeeToken[];
selectedGasFeeToken?: string;
excludeNativeTokenForFee?: boolean;
} = {}) => {
mockUseTransactionMetadataRequest.mockReturnValue({
id: transactionId,
gasFeeTokens,
selectedGasFeeToken,
excludeNativeTokenForFee,
} as ReturnType<typeof useTransactionMetadataRequest>);

const selectedToken = selectedGasFeeToken
Expand Down Expand Up @@ -216,6 +219,19 @@ describe('GasFeeTokenModal', () => {
await waitFor(() => expect(mockOnClose).toHaveBeenCalled());
});

it('never renders native list item if `excludeNativeTokenForFee` is set to `true`', () => {
const { queryByTestId } = setupTest({
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
selectedGasFeeToken: GAS_FEE_TOKEN_MOCK.tokenAddress,
excludeNativeTokenForFee: true,
});
expect(
queryByTestId(
`gas-fee-token-list-item-${MOCK_NATIVE_USE_GAS_FEE_TOKEN.symbol}`,
),
).toBeNull();
});

it('shows native token as selected when no gas fee token is selected', () => {
const { getByTestId } = setupTest({
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ export function GasFeeTokenModal({ onClose }: { onClose?: () => void }) {
id: transactionId = '',
gasFeeTokens,
selectedGasFeeToken,
excludeNativeTokenForFee,
} = transactionMeta || {};

const gasFeeTokenAddresses = [
NATIVE_TOKEN_ADDRESS as Hex,
...(excludeNativeTokenForFee ? [] : [NATIVE_TOKEN_ADDRESS as Hex]),
Comment thread
cursor[bot] marked this conversation as resolved.
...(gasFeeTokens
// Temporarily disable future ETH flow
?.filter((token) => token.tokenAddress !== NATIVE_TOKEN_ADDRESS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Alert } from '../../../types/alerts';
import { GasFeeToken } from '@metamask/transaction-controller';
import { useTransactionBatchesMetadata } from '../../../hooks/transactions/useTransactionBatchesMetadata';
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
import { Hex } from '@metamask/utils';

jest.mock('../../../hooks/alerts/useInsufficientBalanceAlert');
jest.mock('../../../hooks/gas/useEstimationFailed');
Expand Down Expand Up @@ -47,6 +48,8 @@ describe('SelectedGasFeeToken', () => {
gasFeeTokens = [],
transactionMetadata,
estimationFailed = false,
excludeNativeTokenForFee,
chainId = '0x1',
}: {
insufficientBalance?: Alert[];
selectedGasFeeToken?: ReturnType<typeof useSelectedGasFeeToken>;
Expand All @@ -58,6 +61,8 @@ describe('SelectedGasFeeToken', () => {
> | null;
expectModal?: boolean;
estimationFailed?: boolean;
excludeNativeTokenForFee?: boolean;
chainId?: Hex;
} = {}) => {
mockUseInsufficientBalanceAlert.mockReturnValue(insufficientBalance);
mockUseEstimationFailed.mockReturnValue(estimationFailed);
Expand All @@ -81,14 +86,16 @@ describe('SelectedGasFeeToken', () => {
mockUseTransactionMetadataRequest.mockReturnValue(transactionMetadata);
} else if (gasFeeTokens.length > 0) {
mockUseTransactionMetadataRequest.mockReturnValue({
chainId: '0x1',
chainId,
gasFeeTokens,
excludeNativeTokenForFee,
} as Partial<
ReturnType<typeof useTransactionMetadataRequest>
> as ReturnType<typeof useTransactionMetadataRequest>);
} else {
mockUseTransactionMetadataRequest.mockReturnValue({
chainId: '0x1',
chainId,
excludeNativeTokenForFee,
} as Partial<
ReturnType<typeof useTransactionMetadataRequest>
> as ReturnType<typeof useTransactionMetadataRequest>);
Expand Down Expand Up @@ -119,15 +126,19 @@ describe('SelectedGasFeeToken', () => {
pressTokenButton: () =>
fireEvent.press(renderResult.getByTestId('selected-gas-fee-token')),
expectModalToOpen: () => {
expect(renderResult.queryByTestId('gas-fee-token-modal')).toBeNull();
expect(
renderResult.queryByTestId('gas-fee-token-modal'),
).not.toBeOnTheScreen();
fireEvent.press(renderResult.getByTestId('selected-gas-fee-token'));
expect(
renderResult.getByTestId('gas-fee-token-modal'),
).toBeOnTheScreen();
},
expectModalNotToOpen: () => {
fireEvent.press(renderResult.getByTestId('selected-gas-fee-token'));
expect(renderResult.queryByTestId('gas-fee-token-modal')).toBeNull();
expect(
renderResult.queryByTestId('gas-fee-token-modal'),
).not.toBeOnTheScreen();
},
};
};
Expand All @@ -145,6 +156,18 @@ describe('SelectedGasFeeToken', () => {
expect(getByText('ETH')).toBeOnTheScreen();
});

it('renders local native symbol if `gasFeeTokens` is empty and `excludeNativeTokenForFee` is set (Tempo)', () => {
const { getByTestId, getByText } = setupTest({
chainId: '0x1079',
selectedGasFeeToken: undefined,
gaslessSupported: true,
gasFeeTokens: [] as unknown as GasFeeToken[],
excludeNativeTokenForFee: true,
});
expect(getByTestId('selected-gas-fee-token')).toBeOnTheScreen();
expect(getByText('pathUSD')).toBeOnTheScreen();
});

it('renders the arrow icon when gas fee tokens are available', () => {
const { getByTestId, getByText } = setupTest({
selectedGasFeeToken: {
Expand All @@ -165,7 +188,40 @@ describe('SelectedGasFeeToken', () => {

it('does not render the arrow icon when no gas fee tokens are available', () => {
const { queryByTestId } = setupTest();
expect(queryByTestId('selected-gas-fee-token-arrow')).toBeNull();
expect(queryByTestId('selected-gas-fee-token-arrow')).not.toBeOnTheScreen();
});

it('does not render arrow icon if only one gas fee token and `excludeNativeTokenForFee` is set', () => {
const { queryByTestId } = setupTest({
selectedGasFeeToken: {
tokenAddress: '0xTokenAddress',
symbol: 'DAI',
} as unknown as ReturnType<typeof useSelectedGasFeeToken>,
gaslessSupported: true,
gasFeeTokens: [
{ tokenAddress: '0xTokenAddress', symbol: 'DAI' },
] as unknown as GasFeeToken[],
excludeNativeTokenForFee: true,
});
expect(queryByTestId('selected-gas-fee-token-arrow')).not.toBeOnTheScreen();
});

it('still renders the arrow icon if two non-native gas fee tokens and `excludeNativeTokenForFee` is set', () => {
const { getByTestId, getByText } = setupTest({
selectedGasFeeToken: {
tokenAddress: '0xTokenAddress',
symbol: 'DAI',
} as unknown as ReturnType<typeof useSelectedGasFeeToken>,
gaslessSupported: true,
gasFeeTokens: [
{ tokenAddress: '0xTokenAddress', symbol: 'DAI' },
{ tokenAddress: '0xOtherTokenAddress', symbol: 'USDS' },
] as unknown as GasFeeToken[],
excludeNativeTokenForFee: true,
});
expect(getByTestId('selected-gas-fee-token')).toBeOnTheScreen();
expect(getByText('DAI')).toBeOnTheScreen();
expect(getByTestId('selected-gas-fee-token-arrow')).toBeOnTheScreen();
});

describe('Modal', () => {
Expand Down Expand Up @@ -213,7 +269,7 @@ describe('SelectedGasFeeToken', () => {

// Close modal
fireEvent.press(getByTestId('close-button'));
expect(queryByTestId('gas-fee-token-modal')).toBeNull();
expect(queryByTestId('gas-fee-token-modal')).not.toBeOnTheScreen();
});

describe('Future native token', () => {
Expand Down Expand Up @@ -304,7 +360,9 @@ describe('SelectedGasFeeToken', () => {
estimationFailed: true,
});

expect(queryByTestId('selected-gas-fee-token-arrow')).toBeNull();
expect(
queryByTestId('selected-gas-fee-token-arrow'),
).not.toBeOnTheScreen();
});

it('opens modal when estimation has not failed and gas fee tokens are supported', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { TouchableOpacity } from 'react-native';
import Icon, {
IconName,
Expand All @@ -13,16 +13,20 @@
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
import { useIsInsufficientBalance } from '../../../hooks/useIsInsufficientBalance';
import { useTransactionBatchesMetadata } from '../../../hooks/transactions/useTransactionBatchesMetadata';
import useNetworkInfo from '../../../hooks/useNetworkInfo';
import { GasFeeTokenIcon, GasFeeTokenIconSize } from '../gas-fee-token-icon';
import { GasFeeTokenModal } from '../gas-fee-token-modal';
import styleSheet from './selected-gas-fee-token.styles';
import { useNativeCurrencySymbol } from '../../../hooks/useNativeCurrencySymbol';

export function SelectedGasFeeToken() {
const [isModalOpen, setIsModalOpen] = useState(false);
const transactionMetadata = useTransactionMetadataRequest();
const transactionBatchesMetadata = useTransactionBatchesMetadata();
const { chainId: chainIdSingle, gasFeeTokens } = transactionMetadata || {};
const {
chainId: chainIdSingle,
gasFeeTokens,
excludeNativeTokenForFee,
} = transactionMetadata || {};
const { chainId: chainIdBatch } = transactionBatchesMetadata || {};
const chainId = chainIdSingle ?? chainIdBatch;
const hasGasFeeTokens = Boolean(gasFeeTokens?.length);
Expand Down Expand Up @@ -50,19 +54,34 @@
hasGasFeeTokens &&
(!hasOnlyFutureNativeToken || supportsFutureNative);

const { networkNativeCurrency: nativeCurrency } = useNetworkInfo(chainId);
const nonNativeGasFeeTokensLength = useMemo(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since in all other EVM blockchains, the native token is implicitly always an option, we calculate an alternative that doesn't count the native token for chains without any.
This allows to show the arrow and make clickable the gas fee token only when there is more than one available. Without this change the arrow would wrongfully appear on Tempo.

Image

() =>
(
gasFeeTokens?.filter(
(token) =>
token.tokenAddress && token.tokenAddress !== NATIVE_TOKEN_ADDRESS,
) ?? []
).length,
[gasFeeTokens],
);
// Disable fee token choice selection if only 1 token available.
// Taking in account networks that don't have a native token.
const hasMoreThanOneGasFeeTokenToChooseFrom = excludeNativeTokenForFee
? supportsGasFeeTokens && nonNativeGasFeeTokensLength > 1
: supportsGasFeeTokens;

const { nativeCurrencySymbol } = useNativeCurrencySymbol(chainId);

const handlePress = useCallback(() => {
if (!supportsGasFeeTokens) {
if (!hasMoreThanOneGasFeeTokenToChooseFrom) {
return;
}

setIsModalOpen(true);
}, [supportsGasFeeTokens]);
}, [hasMoreThanOneGasFeeTokenToChooseFrom]);

const nativeTicker = nativeCurrency;
const gasFeeToken = useSelectedGasFeeToken();
const symbol = gasFeeToken?.symbol ?? nativeTicker;
const gasTokenSymbol = gasFeeToken?.symbol ?? nativeCurrencySymbol;

return (
<>
Expand All @@ -73,14 +92,14 @@
onPress={handlePress}
style={styles.gasFeeTokenButton}
testID="selected-gas-fee-token"
disabled={!supportsGasFeeTokens}
disabled={!hasMoreThanOneGasFeeTokenToChooseFrom}
>
<GasFeeTokenIcon
tokenAddress={gasFeeToken?.tokenAddress ?? NATIVE_TOKEN_ADDRESS}
size={GasFeeTokenIconSize.Sm}
/>
<Text testID="selected-gas-fee-token-symbol">{symbol}</Text>
{supportsGasFeeTokens && (
<Text testID="selected-gas-fee-token-symbol">{gasTokenSymbol}</Text>

Check warning on line 101 in app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ1JzMU1WefdrwLukORn&open=AZ1JzMU1WefdrwLukORn&pullRequest=27142

Check warning on line 101 in app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'Text' is deprecated.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ1JzMU1WefdrwLukORm&open=AZ1JzMU1WefdrwLukORm&pullRequest=27142
{hasMoreThanOneGasFeeTokenToChooseFrom && (
<Icon
testID="selected-gas-fee-token-arrow"
name={IconName.ArrowDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ export const useInsufficientBalanceAlert = ({
return [];
}

const { selectedGasFeeToken, isGasFeeSponsored, gasFeeTokens } =
transactionMetadata;
const {
selectedGasFeeToken,
isGasFeeSponsored,
gasFeeTokens,
excludeNativeTokenForFee,
} = transactionMetadata;

const isGasFeeTokensEmpty = gasFeeTokens?.length === 0;

Expand All @@ -58,7 +62,14 @@ export const useInsufficientBalanceAlert = ({
const isSimulationComplete = !isSimulationEnabled || Boolean(gasFeeTokens);

// Check if user has selected a gas fee token (or we're ignoring that check)
const hasNoGasFeeTokenSelected = ignoreGasFeeToken || !selectedGasFeeToken;
// Note: In the case of chains with no native token (ex: Tempo), `selectedGasFeeToken`
// may be populated despite no gas token being available.
// For those chains, `excludeNativeTokenForFee` will always be `true`, hence we can
// rely on the combination of `excludeNativeTokenForFee` and `isGasFeeTokensEmpty`.
const hasNoGasFeeTokenSelected =
ignoreGasFeeToken ||
!selectedGasFeeToken ||
(excludeNativeTokenForFee && isGasFeeTokensEmpty);

// Gasless check is complete AND one of:
// - Gasless is NOT supported (native currency needed for gas)
Expand Down Expand Up @@ -113,8 +124,8 @@ export const useInsufficientBalanceAlert = ({
ignoreGasFeeToken,
isUsingPay,
hasInsufficientBalance,
nativeCurrency,
isQuotesLoading,
nativeCurrency,
goToBuy,
onReject,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ const GAS_FEE_TOKEN_MOCK: GasFeeToken = {
tokenAddress: '0x1234567890123456789012345678901234567890',
};

function getState({ gasFeeTokens }: { gasFeeTokens?: GasFeeToken[] } = {}) {
function getState({
gasFeeTokens,
excludeNativeTokenForFee,
}: { gasFeeTokens?: GasFeeToken[]; excludeNativeTokenForFee?: boolean } = {}) {
const state = merge({}, transferTransactionStateMock, {
engine: {
backgroundState: {
Expand All @@ -42,6 +45,7 @@ function getState({ gasFeeTokens }: { gasFeeTokens?: GasFeeToken[] } = {}) {
address: FROM_MOCK,
gasFeeTokens: gasFeeTokens ?? [GAS_FEE_TOKEN_MOCK],
selectedGasFeeToken: GAS_FEE_TOKEN_MOCK.tokenAddress,
excludeNativeTokenForFee,
},
],
},
Expand All @@ -64,11 +68,13 @@ function getState({ gasFeeTokens }: { gasFeeTokens?: GasFeeToken[] } = {}) {
function runHook({
gasFeeTokens,
tokenAddress,
excludeNativeTokenForFee,
}: {
gasFeeTokens?: GasFeeToken[];
tokenAddress?: Hex;
excludeNativeTokenForFee?: boolean;
}) {
const state = getState({ gasFeeTokens });
const state = getState({ gasFeeTokens, excludeNativeTokenForFee });
const { result } = renderHookWithProvider(
() => useGasFeeToken({ tokenAddress }),
state,
Expand Down Expand Up @@ -153,6 +159,23 @@ describe('useGasFeeToken', () => {
expect(result.tokenAddress).toStrictEqual(NATIVE_TOKEN_ADDRESS);
});

it('returns native gas fee token if `tokenAddress` doesnt match any `gasFeeTokens`', () => {
const result = runHook({
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
tokenAddress: '0x00000000000d6ffc74a8feb35af5827bf57f6786', // non-existing
});
expect(result.tokenAddress).toStrictEqual(NATIVE_TOKEN_ADDRESS);
});

it('returns first of gasFeeTokens if `tokenAddress` doesnt match any `gasFeeTokens` but `excludeNativeTokenForFee` is set', () => {
const result = runHook({
gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
tokenAddress: '0x00000000000d6ffc74a8feb35af5827bf57f6786', // non-existing
excludeNativeTokenForFee: true,
});
expect(result.tokenAddress).toStrictEqual(GAS_FEE_TOKEN_MOCK.tokenAddress);
});

it('returns token transfer transaction when tokenAddress is not the native token address', () => {
const result = runHook({ tokenAddress: GAS_FEE_TOKEN_MOCK.tokenAddress });
expect(result.transferTransaction).toEqual(
Expand Down
Loading
Loading