diff --git a/.github/workflows/run-e2e-workflow.yml b/.github/workflows/run-e2e-workflow.yml
index 715ca7b0c00..47ac303e090 100644
--- a/.github/workflows/run-e2e-workflow.yml
+++ b/.github/workflows/run-e2e-workflow.yml
@@ -34,7 +34,6 @@ jobs:
test-e2e-mobile:
name: ${{ inputs.test-suite-name }}
runs-on: ${{ inputs.platform == 'ios' && 'macos-latest-xlarge' || 'gha-mmsdk-scale-set-ubuntu-22.04-amd64-xl' }}
- continue-on-error: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.platform }}-${{ inputs.test-suite-name }}-${{ inputs.split_number }}
cancel-in-progress: ${{ !(contains(github.ref, 'refs/heads/main') || contains(github.ref, 'refs/heads/stable')) }}
diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx
index 29deb07e6ec..70d042ce473 100644
--- a/app/components/Nav/App/App.tsx
+++ b/app/components/Nav/App/App.tsx
@@ -158,7 +158,8 @@ import SolanaNewFeatureContent from '../../UI/SolanaNewFeatureContent';
import { DeepLinkModal } from '../../UI/DeepLinkModal';
import { checkForDeeplink } from '../../../actions/user';
import { WalletDetails } from '../../Views/MultichainAccounts/WalletDetails/WalletDetails';
-import { AddressList as AccountAddressList } from '../../Views/MultichainAccounts/AddressList';
+import { AddressList as MultichainAccountAddressList } from '../../Views/MultichainAccounts/AddressList';
+import { PrivateKeyList as MultichainAccountPrivateKeyList } from '../../Views/MultichainAccounts/PrivateKeyList';
import MultichainAccountActions from '../../Views/MultichainAccounts/sheets/MultichainAccountActions/MultichainAccountActions';
import useInterval from '../../hooks/useInterval';
import { Duration } from '@metamask/utils';
@@ -745,7 +746,21 @@ const MultichainAddressList = () => {
>
+
+ );
+};
+
+const MultichainPrivateKeyList = () => {
+ const route = useRoute();
+
+ return (
+
+
@@ -897,6 +912,10 @@ const AppFlow = () => {
name={Routes.MULTICHAIN_ACCOUNTS.ADDRESS_LIST}
component={MultichainAddressList}
/>
+
{
label={strings('login.cancel')}
width={ButtonWidthTypes.Full}
testID={ForgotPasswordModalSelectorsIDs.CANCEL_BUTTON}
+ isDisabled={isDeletingWallet}
/>
diff --git a/app/components/Views/AccountBackupStep1/index.js b/app/components/Views/AccountBackupStep1/index.js
index 41d281e3c99..d93db27c04c 100644
--- a/app/components/Views/AccountBackupStep1/index.js
+++ b/app/components/Views/AccountBackupStep1/index.js
@@ -25,7 +25,7 @@ import { ManualBackUpStepsSelectorsIDs } from '../../../../e2e/selectors/Onboard
import trackOnboarding from '../../../util/metrics/TrackOnboarding/trackOnboarding';
import Routes from '../../../../app/constants/navigation/Routes';
import { MetricsEventBuilder } from '../../../core/Analytics/MetricsEventBuilder';
-import SRPDesignLight from '../../../images/secure_wallet.png';
+import SRPDesignLight from '../../../images/secure_wallet_light.png';
import SRPDesignDark from '../../../images/secure_wallet_dark.png';
import Button, {
ButtonVariants,
diff --git a/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap b/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap
index 844dc4a0d5f..8ddcb286a35 100644
--- a/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/AccountStatus/__snapshots__/index.test.tsx.snap
@@ -44,13 +44,15 @@ exports[`AccountStatus Snapshots android renders correctly with accountName in r
Wallet already exists
@@ -215,13 +217,15 @@ exports[`AccountStatus Snapshots android renders correctly with type="found and
Wallet already exists
@@ -386,13 +390,15 @@ exports[`AccountStatus Snapshots android renders correctly with type="not_exist"
Wallet not found
@@ -557,13 +563,15 @@ exports[`AccountStatus Snapshots iOS renders correctly with accountName in route
Wallet already exists
@@ -728,13 +736,15 @@ exports[`AccountStatus Snapshots iOS renders correctly with type="found" 1`] = `
Wallet already exists
@@ -899,13 +909,15 @@ exports[`AccountStatus Snapshots iOS renders correctly with type="not_exist" 1`]
Wallet not found
diff --git a/app/components/Views/AccountStatus/index.styles.ts b/app/components/Views/AccountStatus/index.styles.ts
index cfec14245cb..50af705d907 100644
--- a/app/components/Views/AccountStatus/index.styles.ts
+++ b/app/components/Views/AccountStatus/index.styles.ts
@@ -1,4 +1,10 @@
-import { StyleSheet, Platform, StatusBar } from 'react-native';
+import { StyleSheet, Platform, StatusBar, Dimensions } from 'react-native';
+
+const IMAGE_MAX_WIDTH = 343;
+const IMAGE_ASPECT_RATIO = 343 / 302;
+const HORIZONTAL_PADDING = 16;
+const CONTAINER_WIDTH = Dimensions.get('window').width - HORIZONTAL_PADDING * 2;
+const WALLET_IMAGE_WIDTH = Math.min(CONTAINER_WIDTH, IMAGE_MAX_WIDTH);
const styles = StyleSheet.create({
scrollView: {
@@ -19,6 +25,8 @@ const styles = StyleSheet.create({
marginHorizontal: 'auto',
alignSelf: 'center',
marginVertical: 16,
+ width: WALLET_IMAGE_WIDTH,
+ height: Math.round(WALLET_IMAGE_WIDTH / IMAGE_ASPECT_RATIO),
},
description: {
fontSize: 14,
diff --git a/app/components/Views/AccountStatus/index.tsx b/app/components/Views/AccountStatus/index.tsx
index d363f646553..96bca6f15f2 100644
--- a/app/components/Views/AccountStatus/index.tsx
+++ b/app/components/Views/AccountStatus/index.tsx
@@ -143,7 +143,7 @@ const AccountStatus = ({
diff --git a/app/components/Views/ChoosePassword/index.js b/app/components/Views/ChoosePassword/index.js
index f2a31857b97..5d1856ea403 100644
--- a/app/components/Views/ChoosePassword/index.js
+++ b/app/components/Views/ChoosePassword/index.js
@@ -476,7 +476,6 @@ class ChoosePassword extends PureComponent {
this.props.passwordSet();
this.props.setLockTime(AppConstants.DEFAULT_LOCK_TIMEOUT);
- this.setState({ loading: false });
if (authType.oauth2Login) {
endTrace({ name: TraceName.OnboardingNewSocialCreateWallet });
diff --git a/app/components/Views/Login/__snapshots__/index.test.tsx.snap b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
index 2e2513c3612..bc91516f98a 100644
--- a/app/components/Views/Login/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Login/__snapshots__/index.test.tsx.snap
@@ -286,24 +286,28 @@ exports[`Login renders matching snapshot 1`] = `
Unlock
-
Forgot password?
-
+
@@ -628,24 +632,28 @@ exports[`Login renders matching snapshot when password input is focused 1`] = `
Unlock
-
Forgot password?
-
+
diff --git a/app/components/Views/Login/index.tsx b/app/components/Views/Login/index.tsx
index 798a1c1c848..7d643b8359c 100644
--- a/app/components/Views/Login/index.tsx
+++ b/app/components/Views/Login/index.tsx
@@ -771,6 +771,8 @@ const Login: React.FC = ({ saveOnboardingEvent }) => {
onPress={toggleWarningModal}
testID={LoginViewSelectors.RESET_WALLET}
label={strings('login.forgot_password')}
+ isDisabled={loading}
+ size={ButtonSize.Lg}
/>
)}
@@ -783,6 +785,9 @@ const Login: React.FC = ({ saveOnboardingEvent }) => {
onPress={handleUseOtherMethod}
testID={LoginViewSelectors.OTHER_METHODS_BUTTON}
label={strings('login.other_methods')}
+ loading={loading}
+ isDisabled={loading}
+ size={ButtonSize.Lg}
/>
)}
diff --git a/app/components/Views/Login/styles.ts b/app/components/Views/Login/styles.ts
index d694c5fdf5d..184955f0c1e 100644
--- a/app/components/Views/Login/styles.ts
+++ b/app/components/Views/Login/styles.ts
@@ -69,6 +69,7 @@ const styleSheet = (params: { theme: Theme }) => {
},
goBack: {
marginVertical: 14,
+ alignSelf: 'center',
},
biometrics: {
flexDirection: 'row',
diff --git a/app/components/Views/MultichainAccounts/AccountGroupDetails/AccountGroupDetails.tsx b/app/components/Views/MultichainAccounts/AccountGroupDetails/AccountGroupDetails.tsx
index d29214c54bb..98677a5da4f 100644
--- a/app/components/Views/MultichainAccounts/AccountGroupDetails/AccountGroupDetails.tsx
+++ b/app/components/Views/MultichainAccounts/AccountGroupDetails/AccountGroupDetails.tsx
@@ -34,6 +34,7 @@ import { AccountGroupType } from '@metamask/account-api';
import { isHDOrFirstPartySnapAccount } from '../../../../util/address';
import { selectInternalAccountsById } from '../../../../selectors/accountsController';
import { SecretRecoveryPhrase, Wallet, RemoveAccount } from './components';
+import { createPrivateKeyListNavigationDetails } from '../PrivateKeyList/PrivateKeyList';
interface AccountGroupDetailsProps {
route: {
@@ -142,6 +143,16 @@ export const AccountGroupDetails = (props: AccountGroupDetailsProps) => {
{
+ navigation.navigate(
+ ...createPrivateKeyListNavigationDetails({
+ groupId: id,
+ title: strings(
+ 'multichain_accounts.account_details.private_keys',
+ ),
+ }),
+ );
+ }}
>
{strings('multichain_accounts.account_details.private_keys')}
diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx
new file mode 100644
index 00000000000..955cd5b56ac
--- /dev/null
+++ b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.test.tsx
@@ -0,0 +1,221 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react-native';
+import { AccountGroupId, AccountWalletId } from '@metamask/account-api';
+import { SolAccountType, EthScope, SolScope } from '@metamask/keyring-api';
+
+import { createMockInternalAccount } from '../../../../util/test/accountsControllerTestUtils';
+import { renderScreen } from '../../../../util/test/renderWithProvider';
+import { PrivateKeyListIds } from '../../../../../e2e/selectors/MultichainAccounts/PrivateKeyList.selectors';
+import { strings } from '../../../../../locales/i18n';
+
+import { PrivateKeyList } from './PrivateKeyList';
+
+const ACCOUNT_WALLET_ID = 'entropy:wallet-id-1' as AccountWalletId;
+const ACCOUNT_GROUP_ID = 'entropy:wallet-id-1/1' as AccountGroupId;
+
+const TITLE = 'Private Key List';
+const shortenedEthAddress = '0x4FeC2...fdcB5';
+
+const mockNavigate = jest.fn();
+const mockGoBack = jest.fn();
+jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
+ useNavigation: () => ({
+ navigate: mockNavigate,
+ goBack: mockGoBack,
+ }),
+}));
+
+jest.mock('../../../../util/navigation/navUtils', () => ({
+ useParams: jest.fn().mockReturnValue({
+ title: TITLE,
+ groupId: ACCOUNT_GROUP_ID,
+ }),
+ useRoute: jest.fn(),
+ createNavigationDetails: jest.fn(),
+}));
+
+jest.mock('../../../../core/Engine', () => ({
+ context: {
+ KeyringController: {
+ verifyPassword: (password: string) => {
+ if (password === 'correct-password') {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('Wrong password'));
+ },
+ exportAccount: jest.fn().mockImplementation((password, address) => {
+ if (password === 'correct-password') {
+ return Promise.resolve(`mock-private-key-for-${address}`);
+ }
+ return Promise.reject(new Error('Wrong password'));
+ }),
+ state: {
+ keyrings: [],
+ },
+ },
+ },
+}));
+
+const mockEthEoaAccount = {
+ ...createMockInternalAccount(
+ '0x4fec2622fb662e892dd0e5060b91fa49ddcfdcb5',
+ 'Eth Account 1',
+ ),
+ id: 'mock-eth-account-1',
+ scopes: [EthScope.Eoa],
+};
+
+const mockSolAccount = {
+ ...createMockInternalAccount(
+ 'FcdCd3moFy29rZDxjt9jhT5HpFB8VssD6c79g4UGPZgj',
+ 'Sol Account 1',
+ ),
+ id: 'mock-eth-account-2',
+ scopes: [SolScope.Mainnet, SolScope.Testnet, SolScope.Devnet],
+ type: SolAccountType.DataAccount,
+};
+
+const renderWithPrivateKeyList = () => {
+ const mockAccountsControllerState = {
+ internalAccounts: {
+ accounts: {
+ [mockEthEoaAccount.id]: mockEthEoaAccount,
+ [mockSolAccount.id]: mockSolAccount,
+ },
+ },
+ };
+
+ const mockAccountTreeControllerState = {
+ accountTree: {
+ wallets: {
+ [ACCOUNT_WALLET_ID]: {
+ id: ACCOUNT_WALLET_ID,
+ metadata: { name: 'Mock Wallet' },
+ groups: {
+ [ACCOUNT_GROUP_ID]: {
+ accounts: [mockEthEoaAccount.id, mockSolAccount.id],
+ id: ACCOUNT_GROUP_ID,
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const mockNetworkControllerState = {
+ networkConfigurationsByChainId: {
+ 0x1: {
+ chainId: '0x1',
+ name: 'Ethereum',
+ },
+ 0xaa36a7: {
+ chainId: '0xaa36a7',
+ name: 'Sepolia Test Network',
+ },
+ 0x2105: {
+ chainId: '0x2105',
+ name: 'Base',
+ },
+ 0xa4b1: {
+ chainId: '0xa4b1',
+ name: 'Arbitrum One',
+ },
+ },
+ };
+
+ const mockMultichainNetworkController = {
+ multichainNetworkConfigurationsByChainId: {
+ [SolScope.Mainnet]: {
+ name: 'Solana Mainnet',
+ chainId: SolScope.Mainnet,
+ isTestnet: false,
+ },
+ [SolScope.Testnet]: {
+ name: 'Solana Testnet',
+ chainId: SolScope.Testnet,
+ isTestnet: true,
+ },
+ [SolScope.Devnet]: {
+ name: 'Solana Devnet',
+ chainId: SolScope.Devnet,
+ isTestnet: true,
+ },
+ },
+ };
+
+ const mockState = {
+ engine: {
+ backgroundState: {
+ AccountsController: mockAccountsControllerState,
+ AccountTreeController: mockAccountTreeControllerState,
+ NetworkController: mockNetworkControllerState,
+ MultichainNetworkController: mockMultichainNetworkController,
+ },
+ },
+ };
+
+ return renderScreen(
+ () => ,
+ {
+ name: 'PrivateKeyList',
+ },
+ {
+ state: mockState,
+ },
+ );
+};
+
+describe('PrivateKeyList', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders password input box correctly', () => {
+ const { getByTestId } = renderWithPrivateKeyList();
+
+ expect(getByTestId(PrivateKeyListIds.PASSWORD_TITLE)).toBeDefined();
+ expect(getByTestId(PrivateKeyListIds.BANNER)).toBeDefined();
+ expect(getByTestId(PrivateKeyListIds.PASSWORD_INPUT)).toBeDefined();
+ expect(getByTestId(PrivateKeyListIds.CONTINUE_BUTTON)).toBeDefined();
+ expect(getByTestId(PrivateKeyListIds.CANCEL_BUTTON)).toBeDefined();
+ });
+
+ it('shows an error message for an incorrect password', async () => {
+ const { getByTestId, findByTestId, queryByTestId } =
+ renderWithPrivateKeyList();
+
+ fireEvent.changeText(
+ getByTestId(PrivateKeyListIds.PASSWORD_INPUT),
+ 'wrong-password',
+ );
+ fireEvent.press(getByTestId(PrivateKeyListIds.CONTINUE_BUTTON));
+
+ const errorMessage = await findByTestId(PrivateKeyListIds.PASSWORD_ERROR);
+ expect(errorMessage).toBeOnTheScreen();
+ expect(errorMessage).toHaveTextContent(
+ strings('multichain_accounts.private_key_list.wrong_password'),
+ );
+ expect(queryByTestId(PrivateKeyListIds.LIST)).toBeNull();
+ });
+
+ it('reveals private key list for correct password and filters accounts', async () => {
+ const { getByTestId, getByText, findByTestId, getAllByText } =
+ renderWithPrivateKeyList();
+
+ fireEvent.changeText(
+ getByTestId(PrivateKeyListIds.PASSWORD_INPUT),
+ 'correct-password',
+ );
+ fireEvent.press(getByTestId(PrivateKeyListIds.CONTINUE_BUTTON));
+
+ await findByTestId(PrivateKeyListIds.LIST);
+
+ expect(getByText(TITLE)).toBeDefined();
+
+ expect(getAllByText(shortenedEthAddress).length).toBe(3);
+ expect(getByText('Ethereum')).toBeDefined();
+ expect(getByText('Base')).toBeDefined();
+ expect(getByText('Arbitrum One')).toBeDefined();
+ });
+});
diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx
new file mode 100644
index 00000000000..32e8f3aeff4
--- /dev/null
+++ b/app/components/Views/MultichainAccounts/PrivateKeyList/PrivateKeyList.tsx
@@ -0,0 +1,277 @@
+import React, {
+ Fragment,
+ useState,
+ useEffect,
+ useCallback,
+ useRef,
+} from 'react';
+import { View, TextInput } from 'react-native';
+import { useSelector } from 'react-redux';
+import { FlashList } from '@shopify/flash-list';
+import { KeyringTypes } from '@metamask/keyring-controller';
+import { InternalAccount } from '@metamask/keyring-internal-api';
+// Core
+import Engine from '../../../../core/Engine';
+import ClipboardManager from '../../../../core/ClipboardManager';
+// Hooks
+import { useStyles } from '../../../hooks/useStyles';
+// Selectors
+import {
+ selectInternalAccountListSpreadByScopesByGroupId,
+ selectInternalAccountsByGroupId,
+} from '../../../../selectors/multichainAccounts/accounts';
+// Components
+import MultichainAddressRow from '../../../../component-library/components-temp/MultichainAccounts/MultichainAddressRow';
+import SheetHeader from '../../../../component-library/components/Sheet/SheetHeader';
+import BottomSheet, {
+ BottomSheetRef,
+} from '../../../../component-library/components/BottomSheets/BottomSheet';
+import Banner, {
+ BannerVariant,
+ BannerAlertSeverity,
+} from '../../../../component-library/components/Banners/Banner';
+import { strings } from '../../../../../locales/i18n';
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../../component-library/components/Texts/Text';
+import Button, {
+ ButtonSize,
+ ButtonVariants,
+} from '../../../../component-library/components/Buttons/Button';
+import {
+ useParams,
+ createNavigationDetails,
+} from '../../../../util/navigation/navUtils';
+import Routes from '../../../../constants/navigation/Routes';
+import { PrivateKeyListIds } from '../../../../../e2e/selectors/MultichainAccounts/PrivateKeyList.selectors';
+
+import styleSheet from './styles';
+import type { Params as PrivateKeyListParams, AddressItem } from './types';
+
+export const createPrivateKeyListNavigationDetails =
+ createNavigationDetails(
+ Routes.MULTICHAIN_ACCOUNTS.PRIVATE_KEY_LIST,
+ );
+
+/**
+ * Check if the account has the private key available according to its keyring type.
+ * TODO: Add support for KeyringTypes.snap
+ *
+ * @param account - The internal account to check.
+ * @returns True if the private key is available, false otherwise.
+ */
+const hasPrivateKeyAvailable = (account: InternalAccount) =>
+ account.metadata.keyring.type === KeyringTypes.hd ||
+ account.metadata.keyring.type === KeyringTypes.simple;
+
+/**
+ * AddressList component displays a list of addresses spread by scopes.
+ *
+ * @param props - Component properties.
+ * @returns {JSX.Element} The rendered component.
+ */
+export const PrivateKeyList = () => {
+ const { groupId, title } = useParams();
+
+ const { styles } = useStyles(styleSheet, {});
+ const sheetRef = useRef(null);
+ const [password, setPassword] = useState('');
+ const [wrongPassword, setWrongPassword] = useState(false);
+ const [reveal, setReveal] = useState(false);
+ const [privateKeys, setPrivateKeys] = useState>({});
+
+ const getInternalAccountsByGroupId = useSelector(
+ selectInternalAccountsByGroupId,
+ );
+ const accounts = getInternalAccountsByGroupId(groupId);
+
+ const selectInternalAccountsSpreadByScopes = useSelector(
+ selectInternalAccountListSpreadByScopesByGroupId,
+ );
+ const internalAccountsSpreadByScopes =
+ selectInternalAccountsSpreadByScopes(groupId);
+
+ useEffect(
+ () => () => {
+ // Clean state variables on unmount
+ setPassword('');
+ setWrongPassword(false);
+ setReveal(false);
+ setPrivateKeys({});
+ },
+ [],
+ );
+
+ const onPasswordChange = useCallback((pswd: string) => {
+ setPassword(pswd);
+ }, []);
+
+ const unlockPrivateKeys = useCallback(async () => {
+ const { KeyringController } = Engine.context;
+ const pkAccounts = accounts.filter((account: InternalAccount) =>
+ hasPrivateKeyAvailable(account),
+ );
+
+ const privateKeyMap: Record = {};
+ await Promise.all(
+ pkAccounts.map(async (account: InternalAccount) => {
+ const pk = await KeyringController.exportAccount(
+ password,
+ account.address,
+ );
+ privateKeyMap[account.address] = pk;
+ }),
+ );
+
+ setPrivateKeys(privateKeyMap);
+ }, [accounts, password]);
+
+ const verifyPasswordAndUnlockKeys = useCallback(async () => {
+ const { KeyringController } = Engine.context;
+ try {
+ await KeyringController.verifyPassword(password);
+ } catch (error) {
+ setWrongPassword(true);
+ setReveal(false);
+ setPrivateKeys({});
+ return;
+ }
+
+ await unlockPrivateKeys();
+ setWrongPassword(false);
+ setReveal(true);
+ }, [password, unlockPrivateKeys]);
+
+ const onCancel = useCallback(() => {
+ if (sheetRef.current) {
+ sheetRef.current.onCloseBottomSheet();
+ }
+ }, []);
+
+ const filteredAccounts = useCallback(
+ () =>
+ internalAccountsSpreadByScopes.filter((item: AddressItem) =>
+ hasPrivateKeyAvailable(item.account),
+ ),
+ [internalAccountsSpreadByScopes],
+ );
+
+ const renderAddressItem = useCallback(
+ ({ item }: { item: AddressItem }) => (
+ {
+ await ClipboardManager.setStringExpire(
+ privateKeys[item.account.address],
+ );
+ },
+ }}
+ />
+ ),
+ [privateKeys],
+ );
+
+ const renderPassword = useCallback(
+ () => (
+ <>
+
+
+ {strings('multichain_accounts.private_key_list.enter_password')}
+
+
+
+
+ {wrongPassword && (
+
+ {strings('multichain_accounts.private_key_list.wrong_password')}
+
+ )}
+
+
+
+
+ >
+ ),
+ [
+ styles.password,
+ styles.input,
+ styles.buttons,
+ styles.button,
+ onPasswordChange,
+ wrongPassword,
+ onCancel,
+ verifyPasswordAndUnlockKeys,
+ ],
+ );
+
+ const renderPrivateKeyList = useCallback(
+ () => (
+
+ item.scope}
+ renderItem={renderAddressItem}
+ testID={PrivateKeyListIds.LIST}
+ />
+
+ ),
+ [filteredAccounts, renderAddressItem, styles.container],
+ );
+
+ return (
+
+
+
+
+
+ {reveal ? renderPrivateKeyList() : renderPassword()}
+
+
+ );
+};
diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/index.ts b/app/components/Views/MultichainAccounts/PrivateKeyList/index.ts
new file mode 100644
index 00000000000..ab587078c02
--- /dev/null
+++ b/app/components/Views/MultichainAccounts/PrivateKeyList/index.ts
@@ -0,0 +1,2 @@
+export { PrivateKeyList } from './PrivateKeyList';
+export type { Params as PrivateKeyParams, AddressItem } from './types';
diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/styles.ts b/app/components/Views/MultichainAccounts/PrivateKeyList/styles.ts
new file mode 100644
index 00000000000..41a53dde431
--- /dev/null
+++ b/app/components/Views/MultichainAccounts/PrivateKeyList/styles.ts
@@ -0,0 +1,77 @@
+import { StyleSheet } from 'react-native';
+import { fontStyles } from '../../../../styles/common';
+import { Theme } from '../../../../util/theme/models';
+
+const styleSheet = (params: { theme: Theme }) => {
+ const { theme } = params;
+ const { colors } = theme;
+
+ return StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+
+ container: {
+ padding: 16,
+ flexGrow: 1,
+ flexDirection: 'row',
+ marginBottom: 50,
+ maxHeight: '75%',
+ },
+
+ header: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ margin: 16,
+ },
+
+ password: {
+ marginHorizontal: 16,
+ marginTop: 24,
+ },
+
+ buttons: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-evenly',
+ marginTop: 16,
+ marginBottom: 16,
+ },
+
+ button: {
+ flex: 1,
+ marginHorizontal: 8,
+ },
+
+ banner: {
+ marginHorizontal: 10,
+ },
+
+ input: {
+ backgroundColor: colors.background.default,
+ fontSize: 20,
+ color: colors.text.default,
+ borderColor: colors.border.default,
+ borderWidth: 1,
+ borderRadius: 8,
+ marginVertical: 8,
+ paddingVertical: 16,
+ paddingHorizontal: 16,
+ ...fontStyles.normal,
+ },
+
+ sheet: {
+ marginVertical: 16,
+ marginHorizontal: 16,
+ },
+
+ bottomSheetContent: {
+ backgroundColor: colors.background.default,
+ display: 'flex',
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/Views/MultichainAccounts/PrivateKeyList/types.ts b/app/components/Views/MultichainAccounts/PrivateKeyList/types.ts
new file mode 100644
index 00000000000..cf3982106a1
--- /dev/null
+++ b/app/components/Views/MultichainAccounts/PrivateKeyList/types.ts
@@ -0,0 +1,14 @@
+import { AccountGroupId } from '@metamask/account-api';
+import { type CaipChainId } from '@metamask/utils';
+import { type InternalAccount } from '@metamask/keyring-internal-api';
+
+export interface Params {
+ title: string;
+ groupId: AccountGroupId;
+}
+
+export interface AddressItem {
+ scope: CaipChainId;
+ networkName: string;
+ account: InternalAccount;
+}
diff --git a/app/components/Views/Onboarding/__snapshots__/index.test.tsx.snap b/app/components/Views/Onboarding/__snapshots__/index.test.tsx.snap
index f9fb64b6f62..b62221b3a24 100644
--- a/app/components/Views/Onboarding/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Onboarding/__snapshots__/index.test.tsx.snap
@@ -142,7 +142,7 @@ exports[`Onboarding should render correctly 1`] = `
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#121314",
+ "backgroundColor": "#1C1E21",
"borderRadius": 12,
"flexDirection": "row",
"height": 48,
@@ -157,7 +157,7 @@ exports[`Onboarding should render correctly 1`] = `
accessibilityRole="text"
style={
{
- "color": "#ffffff",
+ "color": "#FFFFFF",
"fontFamily": "Geist Medium",
"fontSize": 16,
"letterSpacing": 0,
@@ -179,7 +179,7 @@ exports[`Onboarding should render correctly 1`] = `
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#3c4d9d0f",
+ "backgroundColor": "rgba(60, 77, 157, 0.1)",
"borderColor": "transparent",
"borderRadius": 12,
"borderWidth": 1,
@@ -372,7 +372,7 @@ exports[`Onboarding should render correctly with android 1`] = `
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#121314",
+ "backgroundColor": "#1C1E21",
"borderRadius": 12,
"flexDirection": "row",
"height": 40,
@@ -387,7 +387,7 @@ exports[`Onboarding should render correctly with android 1`] = `
accessibilityRole="text"
style={
{
- "color": "#ffffff",
+ "color": "#FFFFFF",
"fontFamily": "Geist Medium",
"fontSize": 16,
"letterSpacing": 0,
@@ -409,7 +409,7 @@ exports[`Onboarding should render correctly with android 1`] = `
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#3c4d9d0f",
+ "backgroundColor": "rgba(60, 77, 157, 0.1)",
"borderColor": "transparent",
"borderRadius": 12,
"borderWidth": 1,
@@ -602,7 +602,7 @@ exports[`Onboarding should render correctly with large device and iphoneX 1`] =
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#121314",
+ "backgroundColor": "#1C1E21",
"borderRadius": 12,
"flexDirection": "row",
"height": 48,
@@ -617,7 +617,7 @@ exports[`Onboarding should render correctly with large device and iphoneX 1`] =
accessibilityRole="text"
style={
{
- "color": "#ffffff",
+ "color": "#FFFFFF",
"fontFamily": "Geist Medium",
"fontSize": 16,
"letterSpacing": 0,
@@ -639,7 +639,7 @@ exports[`Onboarding should render correctly with large device and iphoneX 1`] =
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#3c4d9d0f",
+ "backgroundColor": "rgba(60, 77, 157, 0.1)",
"borderColor": "transparent",
"borderRadius": 12,
"borderWidth": 1,
@@ -832,7 +832,7 @@ exports[`Onboarding should render correctly with medium device and android 1`] =
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#121314",
+ "backgroundColor": "#1C1E21",
"borderRadius": 12,
"flexDirection": "row",
"height": 40,
@@ -847,7 +847,7 @@ exports[`Onboarding should render correctly with medium device and android 1`] =
accessibilityRole="text"
style={
{
- "color": "#ffffff",
+ "color": "#FFFFFF",
"fontFamily": "Geist Medium",
"fontSize": 16,
"letterSpacing": 0,
@@ -869,7 +869,7 @@ exports[`Onboarding should render correctly with medium device and android 1`] =
{
"alignItems": "center",
"alignSelf": "stretch",
- "backgroundColor": "#3c4d9d0f",
+ "backgroundColor": "rgba(60, 77, 157, 0.1)",
"borderColor": "transparent",
"borderRadius": 12,
"borderWidth": 1,
diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js
index 46681f3aad1..6508abf1263 100644
--- a/app/components/Views/Onboarding/index.js
+++ b/app/components/Views/Onboarding/index.js
@@ -206,6 +206,15 @@ const createStyles = (colors) =>
borderWidth: 1,
color: colors.text.default,
},
+ blackButton: {
+ backgroundColor: importedColors.btnBlack,
+ },
+ blackButtonText: {
+ color: importedColors.btnBlackText,
+ },
+ inverseBlackButton: {
+ backgroundColor: importedColors.btnBlackInverse,
+ },
});
/**
@@ -664,7 +673,9 @@ class Onboarding extends PureComponent {
- {this.props.loadingMsg}
+
+ {this.props.loadingMsg}
+
);
@@ -701,9 +712,17 @@ class Onboarding extends PureComponent {
variant={ButtonVariants.Primary}
onPress={() => this.handleCtaActions('create')}
testID={OnboardingSelectorIDs.NEW_WALLET_BUTTON}
- label={strings('onboarding.start_exploring_now')}
+ label={
+
+ {strings('onboarding.start_exploring_now')}
+
+ }
width={ButtonWidthTypes.Full}
size={Device.isMediumDevice() ? ButtonSize.Md : ButtonSize.Lg}
+ style={styles.blackButton}
/>
+ }
onPress={onPressGetStarted}
width={ButtonWidthTypes.Full}
size={Device.isMediumDevice() ? ButtonSize.Md : ButtonSize.Lg}
testID={OnboardingCarouselSelectorIDs.GET_STARTED_BUTTON_ID}
+ style={styles.blackButton}
/>
diff --git a/app/components/Views/OnboardingSheet/__snapshots__/index.test.tsx.snap b/app/components/Views/OnboardingSheet/__snapshots__/index.test.tsx.snap
index c1206ea2da1..0f4d1f6b821 100644
--- a/app/components/Views/OnboardingSheet/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/OnboardingSheet/__snapshots__/index.test.tsx.snap
@@ -722,7 +722,7 @@ exports[`OnboardingSheet Snapshots renders correctly with createWallet=true (cre
}
}
>
- Continue with Secret Recovery Phrase
+ Use Secret Recovery Phrase
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.styles.ts b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.styles.ts
index f295fff1d2b..f3dbdf08884 100644
--- a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.styles.ts
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.styles.ts
@@ -1,13 +1,10 @@
import { StyleSheet } from 'react-native';
-import { Theme } from '../../../../../../util/theme/models';
-const styleSheet = (params: { theme: Theme }) => {
- const { theme } = params;
- return StyleSheet.create({
- nativeTokenIcon: {
- backgroundColor: theme.colors.background.default,
+const styleSheet = () =>
+ StyleSheet.create({
+ badgeWrapper: {
+ marginRight: 4,
},
});
-};
export default styleSheet;
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.test.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.test.tsx
index 5bb7ccb78cd..f25952c9a78 100644
--- a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.test.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.test.tsx
@@ -1,35 +1,31 @@
import React from 'react';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
-import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
import useNetworkInfo from '../../../hooks/useNetworkInfo';
import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
import { GasFeeTokenIcon } from './gas-fee-token-icon';
+import { transferTransactionStateMock } from '../../../__mocks__/transfer-transaction-mock';
jest.mock('../../../hooks/transactions/useTransactionMetadataRequest');
jest.mock('../../../hooks/useNetworkInfo');
describe('GasFeeTokenIcon', () => {
- const mockUseTransactionMetadataRequest = jest.mocked(
- useTransactionMetadataRequest,
- );
const mockUseNetworkInfo = jest.mocked(useNetworkInfo);
beforeEach(() => {
+ mockUseNetworkInfo.mockReturnValue({
+ networkImage: 10,
+ networkNativeCurrency: 'ETH',
+ networkName: 'Ethereum',
+ });
jest.clearAllMocks();
});
it('renders the token icon when tokenAddress is not the native token address', () => {
const tokenAddress = '0xTokenAddress';
- mockUseTransactionMetadataRequest.mockReturnValue({
- chainId: '0x1',
- } as unknown as ReturnType);
- mockUseNetworkInfo.mockReturnValue({
- networkImage: 'https://example.com/network-image.png',
- networkNativeCurrency: 'ETH',
- } as unknown as ReturnType);
const { getByTestId } = renderWithProvider(
,
+ { state: transferTransactionStateMock },
);
expect(getByTestId('token-icon')).toBeOnTheScreen();
@@ -37,16 +33,10 @@ describe('GasFeeTokenIcon', () => {
it('renders the native token icon when tokenAddress is the native token address', () => {
const tokenAddress = NATIVE_TOKEN_ADDRESS;
- mockUseTransactionMetadataRequest.mockReturnValue({
- chainId: '0x1',
- } as unknown as ReturnType);
- mockUseNetworkInfo.mockReturnValue({
- networkImage: 'https://example.com/network-image.png',
- networkNativeCurrency: 'ETH',
- } as unknown as ReturnType);
const { getByTestId } = renderWithProvider(
,
+ { state: transferTransactionStateMock },
);
expect(getByTestId('native-icon')).toBeOnTheScreen();
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.tsx
index 796d56eb645..ac97e3154b1 100644
--- a/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.tsx
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-icon/gas-fee-token-icon.tsx
@@ -5,12 +5,19 @@ import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTr
import styleSheet from './gas-fee-token-icon.styles';
import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
import { View } from 'react-native';
-import Identicon from '../../../../../UI/Identicon';
-import {
- AvatarToken,
- AvatarTokenSize,
-} from '@metamask/design-system-react-native';
import useNetworkInfo from '../../../hooks/useNetworkInfo';
+import { AvatarSize } from '../../../../../../component-library/components/Avatars/Avatar';
+import AvatarToken from '../../../../../../component-library/components/Avatars/Avatar/variants/AvatarToken';
+import BadgeWrapper, {
+ BadgePosition,
+} from '../../../../../../component-library/components/Badges/BadgeWrapper';
+import Badge, {
+ BadgeVariant,
+} from '../../../../../../component-library/components/Badges/Badge';
+import { RootState } from '../../../../../../reducers';
+import { useSelector } from 'react-redux';
+import { selectTokensByChainIdAndAddress } from '../../../../../../selectors/tokensController';
+import { TokenI } from '../../../../../UI/Tokens/types';
export enum GasFeeTokenIconSize {
Sm = 'sm',
@@ -26,16 +33,27 @@ export function GasFeeTokenIcon({
}) {
const transactionMeta = useTransactionMetadataRequest();
const { chainId } = transactionMeta || {};
- const { networkImage, networkNativeCurrency: nativeCurrency } =
- useNetworkInfo(chainId);
- const { styles } = useStyles(styleSheet, {});
+ const {
+ networkImage,
+ networkNativeCurrency: nativeCurrency,
+ networkName,
+ } = useNetworkInfo(chainId);
+ const tokensResult = useSelector((state: RootState) =>
+ selectTokensByChainIdAndAddress(state, chainId as Hex),
+ );
+ const token = Object.values(tokensResult || {}).find(
+ (t) => t.address.toLowerCase() === tokenAddress.toLowerCase(),
+ ) as TokenI | undefined;
if (tokenAddress !== NATIVE_TOKEN_ADDRESS) {
return (
-
);
@@ -44,15 +62,47 @@ export function GasFeeTokenIcon({
return (
);
}
+
+function TokenIconWithNetworkBadge({
+ size,
+ token,
+ networkName,
+ networkImage,
+ nativeCurrency,
+}: {
+ size: GasFeeTokenIconSize;
+ token?: TokenI;
+ networkName?: string;
+ networkImage?: object;
+ nativeCurrency?: string;
+}) {
+ const { styles } = useStyles(styleSheet, {});
+ return (
+
+
+ }
+ style={styles.badgeWrapper}
+ >
+
+
+
+ );
+}
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.styles.ts b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.styles.ts
new file mode 100644
index 00000000000..0d3327f3b93
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.styles.ts
@@ -0,0 +1,62 @@
+import { StyleSheet } from 'react-native';
+import { Theme } from '../../../../../../util/theme/models';
+
+const styleSheet = (params: {
+ theme: Theme;
+ vars: { isSelected?: boolean };
+}) => {
+ const { theme, vars } = params;
+ const { isSelected } = vars;
+ return StyleSheet.create({
+ gasFeeTokenListItem: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ backgroundColor: isSelected ? theme.colors.background.muted : undefined,
+ padding: 8,
+ },
+ gasFeeTokenListItemContent: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingLeft: 8,
+ },
+ gasFeeTokenListItemTextContainer: {
+ paddingLeft: 8,
+ },
+ gasFeeTokenListItemSymbol: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 2,
+ },
+ gasFeeTokenListItemSymbolText: {
+ color: theme.colors.text.default,
+ padding: 0,
+ },
+ gasFeeTokenListItemAmountContainer: {
+ alignItems: 'flex-end',
+ paddingRight: 8,
+ },
+ warningIndicator: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ borderRadius: 16,
+ borderColor: theme.colors.border.default,
+ padding: 4,
+ gap: 1,
+ },
+ gasFeeTokenListItemSelectedIndicator: {
+ borderRadius: 8,
+ backgroundColor: theme.colors.info.default,
+ width: 4,
+ position: 'absolute',
+ alignSelf: 'center',
+ left: 4,
+ height: 50,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.test.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.test.tsx
new file mode 100644
index 00000000000..1887d95cfbe
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.test.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import { GasFeeTokenListItem } from './gas-fee-token-list-item';
+import { useGasFeeToken } from '../../../hooks/gas/useGasFeeToken';
+import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
+import { Hex } from '@metamask/utils';
+import { GasFeeToken } from '@metamask/transaction-controller';
+import { transferTransactionStateMock } from '../../../__mocks__/transfer-transaction-mock';
+
+jest.mock('../../../hooks/gas/useGasFeeToken');
+
+const mockUseGasFeeToken = useGasFeeToken as jest.MockedFunction<
+ typeof useGasFeeToken
+>;
+
+const MOCK_TOKEN = {
+ symbol: 'TEST',
+ amountFiat: '$1,000.00',
+ amountFormatted: '1',
+ balanceFiat: '2,345.00',
+ tokenAddress: '0xTOKEN123' as Hex,
+} as ReturnType;
+
+const NATIVE_TOKEN = {
+ symbol: 'ETH',
+ amountFiat: '$0.04',
+ amountFormatted: '0.000066',
+ balanceFiat: '537,761.36',
+ tokenAddress: NATIVE_TOKEN_ADDRESS as Hex,
+} as ReturnType;
+
+const runSetup = ({
+ tokenAddress,
+ isSelected = false,
+ onClick,
+ warning,
+ mockGasFeeTokenResponse = undefined,
+}: {
+ tokenAddress?: Hex;
+ isSelected?: boolean;
+ onClick?: (token: GasFeeToken) => void;
+ warning?: string;
+ mockGasFeeTokenResponse?: ReturnType;
+}) => {
+ mockUseGasFeeToken.mockReturnValue(
+ mockGasFeeTokenResponse as ReturnType,
+ );
+
+ return renderWithProvider(
+ ,
+ { state: transferTransactionStateMock },
+ );
+};
+
+describe('GasFeeTokenListItem', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('does not render if gasFeeToken is undefined', () => {
+ const { toJSON } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ });
+ expect(toJSON()).toBeNull();
+ });
+
+ it('renders fiat amount for a token', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-amount-fiat').props.children,
+ ).toContain('$1,000.00');
+ });
+
+ it('renders fiat balance for a token', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-balance').props.children,
+ ).toContain('Bal: 2,345.00 USD');
+ });
+
+ it('renders token amount', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-amount-token').props.children,
+ ).toBe('1 TEST');
+ });
+
+ it('renders warning indicator if warning is passed', () => {
+ const { getByText, getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ warning: 'Test Warning',
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ expect(getByText('Test Warning')).toBeTruthy();
+ expect(
+ getByTestId('gas-fee-token-list-item-symbol').props.children,
+ ).toContain('TEST');
+ });
+
+ describe('with no token address (native token)', () => {
+ it('renders fiat amount', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: undefined,
+ mockGasFeeTokenResponse: NATIVE_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-amount-fiat').props.children,
+ ).toContain('$0.04');
+ });
+
+ it('renders fiat balance', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: undefined,
+ mockGasFeeTokenResponse: NATIVE_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-balance').props.children,
+ ).toContain('Bal: 537,761.36 USD');
+ });
+
+ it('renders token amount', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: undefined,
+ mockGasFeeTokenResponse: NATIVE_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-amount-token').props.children,
+ ).toBe('0.000066 ETH');
+ });
+ });
+
+ it('calls onClick with gasFeeToken when pressed', () => {
+ const onClick = jest.fn();
+ const { getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ onClick,
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ getByTestId(`gas-fee-token-list-item-${MOCK_TOKEN.symbol}`).props.onPress();
+ expect(onClick).toHaveBeenCalledWith(MOCK_TOKEN);
+ });
+
+ it('shows selected indicator if isSelected', () => {
+ const { getByTestId } = runSetup({
+ tokenAddress: MOCK_TOKEN.tokenAddress,
+ isSelected: true,
+ mockGasFeeTokenResponse: MOCK_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-selected-indicator'),
+ ).toBeTruthy();
+ });
+});
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.tsx
new file mode 100644
index 00000000000..ecf46798f28
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/gas-fee-token-list-item.tsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import { GasFeeToken } from '@metamask/transaction-controller';
+import { Hex } from '@metamask/utils';
+import { useSelector } from 'react-redux';
+
+import { GasFeeTokenIcon, GasFeeTokenIconSize } from '../gas-fee-token-icon';
+import { useGasFeeToken } from '../../../hooks/gas/useGasFeeToken';
+import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
+import { selectCurrentCurrency } from '../../../../../../selectors/currencyRateController';
+import { strings } from '../../../../../../../locales/i18n';
+import { TouchableOpacity, View } from 'react-native';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../../../../component-library/components/Texts/Text';
+import Icon, {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../../../../component-library/components/Icons/Icon';
+import styleSheet from './gas-fee-token-list-item.styles';
+import { useStyles } from '../../../../../hooks/useStyles';
+
+export interface GasFeeTokenListItemProps {
+ isSelected?: boolean;
+ onClick?: (token: GasFeeToken) => void;
+ tokenAddress?: Hex;
+ warning?: string;
+}
+
+export function GasFeeTokenListItem({
+ isSelected,
+ onClick,
+ tokenAddress,
+ warning,
+}: GasFeeTokenListItemProps) {
+ const gasFeeToken = useGasFeeToken({ tokenAddress });
+ const currentCurrency = useSelector(selectCurrentCurrency);
+
+ if (!gasFeeToken) {
+ return null;
+ }
+
+ const { amountFiat, amountFormatted, balanceFiat, symbol } = gasFeeToken;
+
+ return (
+
+ }
+ isSelected={isSelected}
+ leftPrimary={symbol}
+ leftSecondary={`${strings(
+ 'gas_fee_token_modal.list_balance',
+ )} ${balanceFiat} ${currentCurrency.toUpperCase()}`}
+ rightPrimary={amountFiat}
+ rightSecondary={`${amountFormatted} ${symbol}`}
+ warning={warning && }
+ onClick={() => onClick?.(gasFeeToken)}
+ />
+ );
+}
+
+function ListItem({
+ image,
+ leftPrimary,
+ leftSecondary,
+ rightPrimary,
+ rightSecondary,
+ isSelected,
+ warning,
+ onClick,
+}: {
+ image: React.ReactNode;
+ leftPrimary: string;
+ leftSecondary: string;
+ rightPrimary?: string;
+ rightSecondary: string;
+ isSelected?: boolean;
+ warning?: React.ReactNode;
+ onClick?: () => void;
+}) {
+ const { styles } = useStyles(styleSheet, { isSelected });
+
+ return (
+ onClick?.()}
+ style={styles.gasFeeTokenListItem}
+ >
+ {isSelected && }
+
+ {image}
+
+
+
+ {leftPrimary}
+
+ {warning}
+
+
+ {leftSecondary}
+
+
+
+
+
+ {rightPrimary}
+
+
+ {rightSecondary}
+
+
+
+ );
+}
+
+function WarningIndicator({ text }: { text: string }) {
+ const { styles } = useStyles(styleSheet, {});
+ return (
+
+
+
+ {text}
+
+
+ );
+}
+
+function SelectedIndicator() {
+ const { styles } = useStyles(styleSheet, {});
+ return (
+
+ );
+}
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/index.ts b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/index.ts
new file mode 100644
index 00000000000..e9933cd3fe2
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-list-item/index.ts
@@ -0,0 +1 @@
+export * from './gas-fee-token-list-item';
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.styles.ts b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.styles.ts
new file mode 100644
index 00000000000..d49503e1743
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.styles.ts
@@ -0,0 +1,96 @@
+import { StyleSheet } from 'react-native';
+import { Theme } from '../../../../../../util/theme/models';
+
+const styleSheet = (params: {
+ theme: Theme;
+ vars: { noMargin?: boolean; isSelected?: boolean };
+}) => {
+ const { theme, vars } = params;
+ const { noMargin, isSelected } = vars;
+ return StyleSheet.create({
+ modalContainer: {
+ backgroundColor: theme.colors.background.default,
+ borderTopRightRadius: 16,
+ borderTopLeftRadius: 16,
+ paddingLeft: 16,
+ paddingRight: 16,
+ paddingBottom: 16,
+ },
+ titleText: {
+ marginLeft: noMargin ? 0 : 4,
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ nativeToggleIcon: {
+ margin: 2,
+ },
+ nativeToggleIconImg: {
+ margin: 8,
+ },
+ container: {
+ position: 'relative',
+ flexDirection: 'row',
+ paddingTop: 8,
+ },
+ backButton: {
+ position: 'absolute',
+ left: 0,
+ top: 24,
+ zIndex: 1,
+ },
+ title: {
+ color: theme.colors.text.default,
+ textAlign: 'center',
+ flex: 1,
+ padding: 16,
+ },
+ titlePayETH: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginInline: 4,
+ flexDirection: 'row',
+ },
+ contentContainer: {
+ display: 'flex',
+ flexDirection: 'column',
+ paddingLeft: 0,
+ paddingRight: 0,
+ },
+ nativeToggleContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ borderStyle: 'solid',
+ borderColor: theme.colors.border.muted,
+ borderRadius: 6,
+ },
+ gasFeeTokenListItem: {
+ position: 'relative',
+ width: '100%',
+ backgroundColor: theme.colors.background.default,
+ },
+ gasFeeTokenListItemSelected: {
+ position: 'relative',
+ width: '100%',
+ },
+ gasFeeTokenListItemSelectedIndicator: {
+ width: 4,
+ position: 'absolute',
+ top: 4,
+ left: 4,
+ backgroundColor: theme.colors.primary.default,
+ },
+ nativeToggleOption: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 8,
+ borderRadius: 8,
+ backgroundColor: isSelected
+ ? theme.colors.primary.muted
+ : theme.colors.background.alternative,
+ marginRight: 8,
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.test.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.test.tsx
new file mode 100644
index 00000000000..db062fe13b3
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.test.tsx
@@ -0,0 +1,248 @@
+import React from 'react';
+import { fireEvent } from '@testing-library/react-native';
+import renderWithProvider from '../../../../../../util/test/renderWithProvider';
+import { GasFeeTokenModal } from './gas-fee-token-modal';
+import { GasFeeToken } from '@metamask/transaction-controller';
+import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
+import { updateSelectedGasFeeToken } from '../../../../../../util/transaction-controller';
+import { transferTransactionStateMock } from '../../../__mocks__/transfer-transaction-mock';
+import { merge } from 'lodash';
+import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
+import { toHex } from '@metamask/controller-utils';
+import {
+ useGasFeeToken,
+ useSelectedGasFeeToken,
+} from '../../../hooks/gas/useGasFeeToken';
+import useNetworkInfo from '../../../hooks/useNetworkInfo';
+import { Hex } from '@metamask/utils';
+
+jest.mock('../../../hooks/transactions/useTransactionMetadataRequest');
+jest.mock('../../../../../../util/transaction-controller');
+jest.mock('../../../hooks/useNetworkInfo');
+jest.mock('../../../hooks/gas/useGasFeeToken');
+
+const WETH_TOKEN_ADDRESS = '0x1234567890123456789012345678901234567894';
+
+const GAS_FEE_TOKEN_MOCK: GasFeeToken = {
+ amount: toHex(10000),
+ balance: toHex(12345),
+ decimals: 18,
+ gas: '0x1',
+ gasTransfer: '0x2a',
+ maxFeePerGas: '0x3',
+ maxPriorityFeePerGas: '0x4',
+ rateWei: toHex('2000000000000000000'),
+ recipient: '0x1234567890123456789012345678901234567892',
+ symbol: 'USDC',
+ tokenAddress: '0x1234567890123456789012345678901234567893',
+};
+const GAS_FEE_TOKEN_2_MOCK: GasFeeToken = {
+ amount: toHex(20000),
+ balance: toHex(43210),
+ decimals: 4,
+ gas: '0x3',
+ gasTransfer: '0x3a',
+ maxFeePerGas: '0x4',
+ maxPriorityFeePerGas: '0x5',
+ rateWei: toHex('1798170000000000000'),
+ recipient: '0x1234567890123456789012345678901234567893',
+ symbol: 'WETH',
+ tokenAddress: WETH_TOKEN_ADDRESS,
+};
+
+const MOCK_WETH_USE_GAS_FEE_TOKEN = {
+ symbol: 'WETH',
+ amountFiat: '$1,000.00',
+ amountFormatted: '1',
+ balanceFiat: '2,345.00',
+ tokenAddress: WETH_TOKEN_ADDRESS as Hex,
+} as ReturnType;
+
+const MOCK_USDC_USE_GAS_FEE_TOKEN = {
+ ...GAS_FEE_TOKEN_MOCK,
+ symbol: 'USDC',
+ tokenAddress: GAS_FEE_TOKEN_MOCK.tokenAddress as Hex,
+};
+
+const MOCK_NATIVE_USE_GAS_FEE_TOKEN = {
+ ...MOCK_WETH_USE_GAS_FEE_TOKEN,
+ symbol: 'ETH',
+ tokenAddress: NATIVE_TOKEN_ADDRESS as Hex,
+};
+
+describe('GasFeeTokenModal', () => {
+ const mockUseTransactionMetadataRequest = jest.mocked(
+ useTransactionMetadataRequest,
+ );
+ const mockUpdateSelectedGasFeeToken = jest.mocked(updateSelectedGasFeeToken);
+ const mockUseSelectedGasFeeToken = jest.mocked(useSelectedGasFeeToken);
+ const mockUseGasFeeToken = jest.mocked(useGasFeeToken);
+
+ const mockOnClose = jest.fn();
+
+ const setupTest = ({
+ transactionId = 'test-transaction-id',
+ gasFeeTokens = [],
+ selectedGasFeeToken = undefined,
+ mockGasFeeTokenResponse = undefined,
+ }: {
+ transactionId?: string;
+ gasFeeTokens?: GasFeeToken[];
+ selectedGasFeeToken?: string;
+ mockGasFeeTokenResponse?: ReturnType;
+ } = {}) => {
+ mockUseTransactionMetadataRequest.mockReturnValue({
+ id: transactionId,
+ gasFeeTokens,
+ selectedGasFeeToken,
+ } as ReturnType);
+
+ const selectedToken = selectedGasFeeToken
+ ? gasFeeTokens.find((token) => token.tokenAddress === selectedGasFeeToken)
+ : undefined;
+
+ mockUseSelectedGasFeeToken.mockReturnValue(
+ selectedToken as ReturnType,
+ );
+ mockUseGasFeeToken.mockReturnValueOnce(
+ mockGasFeeTokenResponse ?? MOCK_WETH_USE_GAS_FEE_TOKEN,
+ );
+
+ (useNetworkInfo as jest.Mock).mockReturnValue({
+ networkNativeCurrency: 'ETH',
+ });
+
+ const state = merge({}, transferTransactionStateMock, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ { id: transactionId, gasFeeTokens, selectedGasFeeToken },
+ ],
+ },
+ },
+ },
+ });
+ return renderWithProvider(, {
+ state,
+ });
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders modal, header, back button', () => {
+ const { getByTestId, getByText } = setupTest();
+ expect(getByTestId('gas-fee-token-modal')).toBeTruthy();
+ expect(getByTestId('back-button')).toBeTruthy();
+ expect(getByText('Select a token')).toBeTruthy();
+ });
+
+ it('calls onClose when back button is pressed', () => {
+ const { getByTestId } = setupTest();
+ fireEvent.press(getByTestId('back-button'));
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('renders multiple gas fee tokens', () => {
+ mockUseGasFeeToken.mockReturnValueOnce(
+ MOCK_USDC_USE_GAS_FEE_TOKEN as ReturnType,
+ );
+ const { getByTestId } = setupTest({
+ gasFeeTokens: [GAS_FEE_TOKEN_MOCK, GAS_FEE_TOKEN_2_MOCK],
+ selectedGasFeeToken: GAS_FEE_TOKEN_MOCK.tokenAddress,
+ });
+ expect(
+ getByTestId(
+ `gas-fee-token-list-item-${MOCK_WETH_USE_GAS_FEE_TOKEN.symbol}`,
+ ),
+ ).toBeTruthy();
+ expect(
+ getByTestId(
+ `gas-fee-token-list-item-${MOCK_USDC_USE_GAS_FEE_TOKEN.symbol}`,
+ ),
+ ).toBeTruthy();
+ });
+
+ it('does not render other tokens section when no gas fee tokens available', () => {
+ const { queryByText } = setupTest({ gasFeeTokens: [] });
+ expect(queryByText('Pay with other tokens')).toBeNull();
+ });
+
+ it('handles token selection and calls updateSelectedGasFeeToken', () => {
+ const transactionId = 'test-tx-id';
+ const { getByTestId } = setupTest({
+ transactionId,
+ gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
+ selectedGasFeeToken: GAS_FEE_TOKEN_MOCK.tokenAddress,
+ });
+ fireEvent.press(
+ getByTestId(
+ `gas-fee-token-list-item-${MOCK_WETH_USE_GAS_FEE_TOKEN.symbol}`,
+ ),
+ );
+ expect(mockUpdateSelectedGasFeeToken).toHaveBeenCalledWith(
+ transactionId,
+ MOCK_WETH_USE_GAS_FEE_TOKEN.tokenAddress,
+ );
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('handles native token selection', () => {
+ const transactionId = 'test-tx-id';
+ const { getByTestId } = setupTest({
+ transactionId,
+ mockGasFeeTokenResponse: MOCK_NATIVE_USE_GAS_FEE_TOKEN,
+ });
+ fireEvent.press(
+ getByTestId(
+ `gas-fee-token-list-item-${MOCK_NATIVE_USE_GAS_FEE_TOKEN.symbol}`,
+ ),
+ );
+ expect(mockUpdateSelectedGasFeeToken).toHaveBeenCalledWith(
+ transactionId,
+ undefined,
+ );
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+
+ it('shows native token as selected when no gas fee token is selected', () => {
+ const { getByTestId } = setupTest({
+ gasFeeTokens: [GAS_FEE_TOKEN_MOCK],
+ selectedGasFeeToken: undefined,
+ mockGasFeeTokenResponse: MOCK_NATIVE_USE_GAS_FEE_TOKEN,
+ });
+ expect(
+ getByTestId('gas-fee-token-list-item-selected-indicator'),
+ ).toBeTruthy();
+ });
+
+ it('gracefully handles missing transaction metadata', () => {
+ mockUseTransactionMetadataRequest.mockReturnValue(
+ undefined as unknown as ReturnType,
+ );
+ const { getByTestId } = renderWithProvider(
+ ,
+ { state: transferTransactionStateMock },
+ );
+ expect(getByTestId('gas-fee-token-modal')).toBeTruthy();
+ });
+
+ it('works if onClose prop missing', () => {
+ setupTest();
+ expect(() => {
+ renderWithProvider(, {
+ state: transferTransactionStateMock,
+ });
+ }).not.toThrow();
+ });
+
+ it('handles empty gas fee tokens array', () => {
+ const { getByTestId } = setupTest({
+ gasFeeTokens: [],
+ mockGasFeeTokenResponse: MOCK_NATIVE_USE_GAS_FEE_TOKEN,
+ });
+ expect(getByTestId('native-icon')).toBeTruthy();
+ });
+});
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.tsx b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.tsx
new file mode 100644
index 00000000000..5b0d44fdd15
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/gas-fee-token-modal.tsx
@@ -0,0 +1,95 @@
+import React, { useCallback } from 'react';
+import { GasFeeToken } from '@metamask/transaction-controller';
+import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
+import { strings } from '../../../../../../../locales/i18n';
+import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
+import { updateSelectedGasFeeToken } from '../../../../../../util/transaction-controller';
+import BottomModal from '../../UI/bottom-modal';
+import { View } from 'react-native';
+import { useStyles } from '../../../../../../component-library/hooks';
+import Text, {
+ TextVariant,
+} from '../../../../../../component-library/components/Texts/Text';
+import { IconName } from '../../../../../../component-library/components/Icons/Icon';
+import ButtonIcon, {
+ ButtonIconSizes,
+} from '../../../../../../component-library/components/Buttons/ButtonIcon';
+import styleSheet from './gas-fee-token-modal.styles';
+import { GasFeeTokenListItem } from '../gas-fee-token-list-item';
+import { Hex } from '@metamask/utils';
+
+export function GasFeeTokenModal({ onClose }: { onClose?: () => void }) {
+ const transactionMeta = useTransactionMetadataRequest();
+
+ const { styles } = useStyles(styleSheet, {});
+
+ const {
+ id: transactionId = '',
+ gasFeeTokens,
+ selectedGasFeeToken,
+ } = transactionMeta || {};
+
+ const gasFeeTokenAddresses = [
+ NATIVE_TOKEN_ADDRESS as Hex,
+ ...(gasFeeTokens
+ // Temporarily disable future ETH flow
+ ?.filter((token) => token.tokenAddress !== NATIVE_TOKEN_ADDRESS)
+ .map((token) => token.tokenAddress) ?? []),
+ ];
+
+ const handleTokenClick = useCallback(
+ async (token: GasFeeToken) => {
+ const selectedAddress =
+ token.tokenAddress === NATIVE_TOKEN_ADDRESS
+ ? undefined
+ : token.tokenAddress;
+
+ updateSelectedGasFeeToken(transactionId, selectedAddress);
+
+ onClose?.();
+ },
+ [onClose, transactionId],
+ );
+
+ return (
+ {
+ // Intentionally empty
+ })
+ }
+ >
+
+
+
+
+
+
+ {strings('gas_fee_token_modal.title')}
+
+
+
+ {gasFeeTokenAddresses.map((tokenAddress: Hex) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/components/Views/confirmations/components/gas/gas-fee-token-modal/index.ts b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/index.ts
new file mode 100644
index 00000000000..c7bb16a3674
--- /dev/null
+++ b/app/components/Views/confirmations/components/gas/gas-fee-token-modal/index.ts
@@ -0,0 +1 @@
+export * from './gas-fee-token-modal';
diff --git a/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.test.tsx b/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.test.tsx
index 96a5f6fa091..ef42dba11a6 100644
--- a/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.test.tsx
+++ b/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.test.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import { SelectedGasFeeToken } from './selected-gas-fee-token';
import { useInsufficientBalanceAlert } from '../../../hooks/alerts/useInsufficientBalanceAlert';
@@ -7,6 +8,9 @@ import { useIsGaslessSupported } from '../../../hooks/gas/useIsGaslessSupported'
import useNetworkInfo from '../../../hooks/useNetworkInfo';
import { transferTransactionStateMock } from '../../../__mocks__/transfer-transaction-mock';
import { merge } from 'lodash';
+import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
+import { Alert } from '../../../types/alerts';
+import { GasFeeToken } from '@metamask/transaction-controller';
jest.mock('../../../hooks/alerts/useInsufficientBalanceAlert');
jest.mock('../../../hooks/gas/useGasFeeToken');
@@ -21,73 +25,90 @@ describe('SelectedGasFeeToken', () => {
const mockUseIsGaslessSupported = jest.mocked(useIsGaslessSupported);
const mockUseNetworkInfo = jest.mocked(useNetworkInfo);
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('renders the gas fee token button with the native token symbol', () => {
- mockUseInsufficientBalanceAlert.mockReturnValue([]);
- mockUseSelectedGasFeeToken.mockReturnValue(undefined);
+ const setupTest = ({
+ insufficientBalance = [],
+ selectedGasFeeToken = undefined,
+ gaslessSupported = false,
+ isSmartTransaction = false,
+ gasFeeTokens = [],
+ }: {
+ insufficientBalance?: Alert[];
+ selectedGasFeeToken?: ReturnType;
+ gaslessSupported?: boolean;
+ isSmartTransaction?: boolean;
+ gasFeeTokens?: GasFeeToken[];
+ expectModal?: boolean;
+ } = {}) => {
+ mockUseInsufficientBalanceAlert.mockReturnValue(insufficientBalance);
+ mockUseSelectedGasFeeToken.mockReturnValue(selectedGasFeeToken);
mockUseIsGaslessSupported.mockReturnValue({
- isSupported: false,
- isSmartTransaction: false,
+ isSupported: gaslessSupported,
+ isSmartTransaction,
});
mockUseNetworkInfo.mockReturnValue({
networkNativeCurrency: 'ETH',
} as ReturnType);
- const { getByTestId, getByText } = renderWithProvider(
- ,
- {
- state: transferTransactionStateMock,
+ const state =
+ gasFeeTokens.length > 0
+ ? merge({}, transferTransactionStateMock, {
+ engine: {
+ backgroundState: {
+ TransactionController: {
+ transactions: [
+ {
+ id: '699ca2f0-e459-11ef-b6f6-d182277cf5e1',
+ gasFeeTokens,
+ },
+ ],
+ },
+ },
+ },
+ })
+ : transferTransactionStateMock;
+
+ const renderResult = renderWithProvider(, { state });
+
+ return {
+ ...renderResult,
+ pressTokenButton: () =>
+ fireEvent.press(renderResult.getByTestId('selected-gas-fee-token')),
+ expectModalToOpen: () => {
+ expect(renderResult.queryByTestId('gas-fee-token-modal')).toBeNull();
+ 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();
},
- );
+ };
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ it('renders the gas fee token button with the native token symbol', () => {
+ const { getByTestId, getByText } = setupTest();
expect(getByTestId('selected-gas-fee-token')).toBeOnTheScreen();
expect(getByText('ETH')).toBeOnTheScreen();
});
it('renders the arrow icon when gas fee tokens are available', () => {
- mockUseInsufficientBalanceAlert.mockReturnValue([]);
- mockUseSelectedGasFeeToken.mockReturnValue({
- tokenAddress: '0xTokenAddress',
- symbol: 'DAI',
- } as unknown as ReturnType);
- mockUseIsGaslessSupported.mockReturnValue({
- isSupported: true,
+ const { getByTestId, getByText } = setupTest({
+ selectedGasFeeToken: {
+ tokenAddress: '0xTokenAddress',
+ symbol: 'DAI',
+ } as unknown as ReturnType,
+ gaslessSupported: true,
isSmartTransaction: true,
+ gasFeeTokens: [
+ { tokenAddress: '0xTokenAddress', symbol: 'DAI' },
+ ] as unknown as GasFeeToken[],
});
- mockUseNetworkInfo.mockReturnValue({
- networkNativeCurrency: 'ETH',
- } as ReturnType);
-
- const stateWithSelectedGasFeeToken = merge(
- {},
- transferTransactionStateMock,
- {
- engine: {
- backgroundState: {
- TransactionController: {
- transactions: [
- {
- id: '699ca2f0-e459-11ef-b6f6-d182277cf5e1',
- gasFeeTokens: [
- { tokenAddress: '0xTokenAddress', symbol: 'DAI' },
- ],
- },
- ],
- },
- },
- },
- },
- );
-
- const { getByTestId, getByText } = renderWithProvider(
- ,
- {
- state: stateWithSelectedGasFeeToken,
- },
- );
expect(getByTestId('selected-gas-fee-token')).toBeOnTheScreen();
expect(getByText('DAI')).toBeOnTheScreen();
@@ -95,20 +116,98 @@ describe('SelectedGasFeeToken', () => {
});
it('does not render the arrow icon when no gas fee tokens are available', () => {
- mockUseInsufficientBalanceAlert.mockReturnValue([]);
- mockUseSelectedGasFeeToken.mockReturnValue(undefined);
- mockUseIsGaslessSupported.mockReturnValue({
- isSupported: false,
- isSmartTransaction: false,
+ const { queryByTestId } = setupTest();
+ expect(queryByTestId('selected-gas-fee-token-arrow')).toBeNull();
+ });
+
+ describe('Modal', () => {
+ it('opens modal when button is pressed and gas fee tokens are supported', () => {
+ const { expectModalToOpen } = setupTest({
+ selectedGasFeeToken: {
+ tokenAddress: '0xTokenAddress',
+ symbol: 'DAI',
+ } as unknown as ReturnType,
+ gaslessSupported: true,
+ isSmartTransaction: true,
+ gasFeeTokens: [
+ { tokenAddress: '0xTokenAddress', symbol: 'DAI' },
+ ] as unknown as GasFeeToken[],
+ });
+
+ expectModalToOpen();
});
- mockUseNetworkInfo.mockReturnValue({
- networkNativeCurrency: 'ETH',
- } as ReturnType);
- const { queryByTestId } = renderWithProvider(, {
- state: transferTransactionStateMock,
+ it('does not open modal when button is pressed but gas fee tokens are not supported', () => {
+ const { expectModalNotToOpen } = setupTest({
+ gaslessSupported: false,
+ isSmartTransaction: false,
+ });
+
+ expectModalNotToOpen();
});
- expect(queryByTestId('selected-gas-fee-token-arrow')).toBeNull();
+ it('closes modal when onClose is called', () => {
+ const { getByTestId, queryByTestId } = setupTest({
+ selectedGasFeeToken: {
+ tokenAddress: '0xTokenAddress',
+ symbol: 'DAI',
+ } as unknown as ReturnType,
+ gaslessSupported: true,
+ isSmartTransaction: true,
+ gasFeeTokens: [
+ { tokenAddress: '0xTokenAddress', symbol: 'DAI' },
+ ] as unknown as GasFeeToken[],
+ });
+
+ // Open modal
+ fireEvent.press(getByTestId('selected-gas-fee-token'));
+ expect(getByTestId('gas-fee-token-modal')).toBeOnTheScreen();
+
+ // Close modal
+ fireEvent.press(getByTestId('back-button'));
+ expect(queryByTestId('gas-fee-token-modal')).toBeNull();
+ });
+
+ describe('Future native token', () => {
+ it('supports future native tokens when insufficient native balance and smart transaction', () => {
+ const { expectModalToOpen } = setupTest({
+ insufficientBalance: [{ reason: 'insufficient' } as unknown as Alert],
+ gaslessSupported: true,
+ isSmartTransaction: true,
+ gasFeeTokens: [
+ { tokenAddress: NATIVE_TOKEN_ADDRESS, symbol: 'ETH' },
+ ] as unknown as GasFeeToken[],
+ });
+
+ expectModalToOpen();
+ });
+
+ it('does not support gas fee tokens when only future native token and insufficient balance but not smart transaction', () => {
+ const { expectModalNotToOpen } = setupTest({
+ insufficientBalance: [
+ { reason: 'insufficient' },
+ ] as unknown as Alert[],
+ gaslessSupported: true,
+ isSmartTransaction: false,
+ gasFeeTokens: [
+ { tokenAddress: NATIVE_TOKEN_ADDRESS, symbol: 'ETH' },
+ ] as unknown as GasFeeToken[],
+ });
+
+ expectModalNotToOpen();
+ });
+ });
+
+ it('does not support gas fee tokens when gasless is not supported', () => {
+ const { expectModalNotToOpen } = setupTest({
+ gaslessSupported: false,
+ isSmartTransaction: true,
+ gasFeeTokens: [
+ { tokenAddress: '0xTokenAddress', symbol: 'DAI' },
+ ] as unknown as GasFeeToken[],
+ });
+
+ expectModalNotToOpen();
+ });
});
});
diff --git a/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx b/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx
index 084fecb9813..9287341b4ba 100644
--- a/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx
+++ b/app/components/Views/confirmations/components/gas/selected-gas-fee-token/selected-gas-fee-token.tsx
@@ -7,20 +7,24 @@ import Icon, {
import styleSheet from './selected-gas-fee-token.styles';
import { useStyles } from '../../../../../hooks/useStyles';
import { NATIVE_TOKEN_ADDRESS } from '../../../constants/tokens';
-import React, { useCallback } from 'react';
+import React, { useCallback, useState } from 'react';
import { TouchableOpacity } from 'react-native';
import { GasFeeTokenIcon, GasFeeTokenIconSize } from '../gas-fee-token-icon';
import useNetworkInfo from '../../../hooks/useNetworkInfo';
import { useInsufficientBalanceAlert } from '../../../hooks/alerts/useInsufficientBalanceAlert';
import { useSelectedGasFeeToken } from '../../../hooks/gas/useGasFeeToken';
import { useIsGaslessSupported } from '../../../hooks/gas/useIsGaslessSupported';
+import { GasFeeTokenModal } from '../gas-fee-token-modal';
export function SelectedGasFeeToken() {
+ const [isModalOpen, setIsModalOpen] = useState(false);
const transactionMetadata = useTransactionMetadataRequest();
const { chainId, gasFeeTokens } = transactionMetadata || {};
const hasGasFeeTokens = Boolean(gasFeeTokens?.length);
- const { styles } = useStyles(styleSheet, {});
+ const { styles } = useStyles(styleSheet, {
+ hasGasFeeTokens,
+ });
const { isSupported: isGaslessSupported, isSmartTransaction } =
useIsGaslessSupported();
@@ -47,7 +51,7 @@ export function SelectedGasFeeToken() {
return;
}
- // Implement the logic to open the gas fee token selection modal
+ setIsModalOpen(true);
}, [supportsGasFeeTokens]);
const nativeTicker = nativeCurrency;
@@ -55,23 +59,28 @@ export function SelectedGasFeeToken() {
const symbol = gasFeeToken?.symbol ?? nativeTicker;
return (
-
-
- {symbol}
- {hasGasFeeTokens && (
-
+ <>
+ {isModalOpen && (
+ setIsModalOpen(false)} />
)}
-
+
+
+ {symbol}
+ {hasGasFeeTokens && (
+
+ )}
+
+ >
);
}
diff --git a/app/components/Views/confirmations/hooks/useEthFiatAmount.tsx b/app/components/Views/confirmations/hooks/useEthFiatAmount.tsx
index 322b9c0c95d..0132998ee56 100644
--- a/app/components/Views/confirmations/hooks/useEthFiatAmount.tsx
+++ b/app/components/Views/confirmations/hooks/useEthFiatAmount.tsx
@@ -12,8 +12,6 @@ interface UseEthFiatAmountOverrides {
showFiat?: boolean;
}
-type UseEthFiatAmountReturn = string | undefined;
-
/**
* Get an Eth amount converted to fiat and formatted for display
*
@@ -21,13 +19,13 @@ type UseEthFiatAmountReturn = string | undefined;
* @param {UseEthFiatAmountOverrides} [overrides] - A configuration object that allows the caller to explicitly
* ensure fiat is shown even if the property is not set in state.
* @param {boolean} [hideCurrencySymbol] - Indicates whether the returned formatted amount should include the trailing currency symbol
- * @returns {UseEthFiatAmountReturn} The formatted token amount in the user's chosen fiat currency
+ * @returns {string | undefined} The formatted token amount in the user's chosen fiat currency
*/
export function useEthFiatAmount(
ethAmount?: string | BigNumber,
overrides: UseEthFiatAmountOverrides = {},
hideCurrencySymbol?: boolean,
-): UseEthFiatAmountReturn {
+): string | undefined {
const currentRates = useSelector(selectCurrencyRates);
const currentCurrency = useSelector(selectCurrentCurrency);
diff --git a/app/components/Views/confirmations/hooks/useIsInsufficientBalance.test.ts b/app/components/Views/confirmations/hooks/useIsInsufficientBalance.test.ts
new file mode 100644
index 00000000000..b6fe2351936
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useIsInsufficientBalance.test.ts
@@ -0,0 +1,39 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useIsInsufficientBalance } from './useIsInsufficientBalance';
+// eslint-disable-next-line import/no-namespace
+import * as useInsufficientBalanceAlertModule from './alerts/useInsufficientBalanceAlert';
+import { Severity } from '../types/alerts';
+
+const ALERT_MOCK = {
+ field: 'MOCK_FIELD',
+ key: 'MOCK_KEY',
+ message: `Insufficient balance`,
+ title: 'Insufficient Balance',
+ severity: Severity.Danger,
+};
+
+describe('useIsInsufficientBalance', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should return true when useInsufficientBalanceAlert returns a non-empty array', () => {
+ jest
+ .spyOn(useInsufficientBalanceAlertModule, 'useInsufficientBalanceAlert')
+ .mockReturnValue([ALERT_MOCK]);
+
+ const { result } = renderHook(() => useIsInsufficientBalance());
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should return false when useInsufficientBalanceAlert returns an empty array', () => {
+ jest
+ .spyOn(useInsufficientBalanceAlertModule, 'useInsufficientBalanceAlert')
+ .mockReturnValue([]);
+
+ const { result } = renderHook(() => useIsInsufficientBalance());
+
+ expect(result.current).toBe(false);
+ });
+});
diff --git a/app/components/Views/confirmations/hooks/useIsInsufficientBalance.ts b/app/components/Views/confirmations/hooks/useIsInsufficientBalance.ts
new file mode 100644
index 00000000000..b8315dcd6cb
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useIsInsufficientBalance.ts
@@ -0,0 +1,7 @@
+import { useInsufficientBalanceAlert } from './alerts/useInsufficientBalanceAlert';
+
+export function useIsInsufficientBalance() {
+ return Boolean(
+ useInsufficientBalanceAlert({ ignoreGasFeeToken: true }).length,
+ );
+}
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 6e3e1440707..2b7601d9cd0 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -277,6 +277,7 @@ const Routes = {
ACCOUNT_GROUP_DETAILS: 'MultichainAccountGroupDetails',
WALLET_DETAILS: 'MultichainWalletDetails',
ADDRESS_LIST: 'MultichainAddressList',
+ PRIVATE_KEY_LIST: 'MultichainPrivateKeyList',
ACCOUNT_CELL_ACTIONS: 'MultichainAccountActions',
},
SOLANA_NEW_FEATURE_CONTENT: 'SolanaNewFeatureContentView',
diff --git a/app/core/Engine/controllers/transaction-controller/utils.test.ts b/app/core/Engine/controllers/transaction-controller/utils.test.ts
index 89a07348507..91dad8566f4 100644
--- a/app/core/Engine/controllers/transaction-controller/utils.test.ts
+++ b/app/core/Engine/controllers/transaction-controller/utils.test.ts
@@ -41,6 +41,11 @@ jest.mock('../../../Analytics/MetricsEventBuilder', () => ({
},
}));
+jest.mock('../../../../util/address', () => ({
+ getAddressAccountType: jest.fn().mockReturnValue('MetaMask'),
+ isValidHexAddress: jest.fn().mockReturnValue(true),
+}));
+
jest.mock('../../../../util/rpc-domain-utils', () => ({
getNetworkRpcUrl: jest.fn(),
extractRpcDomain: jest.fn(),
@@ -178,6 +183,7 @@ describe('generateRPCProperties', () => {
});
describe('generateDefaultTransactionMetrics', () => {
+ const FROM_ADDRESS_MOCK = '0x6D404AfE1a6A07Aa3CbcBf9Fd027671Df628ebFc';
const mockMetametricsEvent = {
name: 'test_event',
category: 'test_category',
@@ -195,7 +201,7 @@ describe('generateDefaultTransactionMetrics', () => {
userFeeLevel: 'medium',
txParams: {
authorizationList: [],
- from: '0x1',
+ from: FROM_ADDRESS_MOCK,
},
};
@@ -244,8 +250,10 @@ describe('generateDefaultTransactionMetrics', () => {
metametricsEvent: mockMetametricsEvent,
properties: {
account_eip7702_upgraded: undefined,
+ account_type: 'MetaMask',
additional_property: 'test_value',
chain_id: '0x1',
+ dapp_host_name: 'N/A',
eip7702_upgrade_transaction: false,
gas_estimation_failed: false,
gas_fee_presented: expect.any(Array),
@@ -258,7 +266,7 @@ describe('generateDefaultTransactionMetrics', () => {
transaction_type: 'simple_send',
},
sensitiveProperties: {
- from_address: '0x1',
+ from_address: FROM_ADDRESS_MOCK,
sensitive_data: 'sensitive_value',
to_address: undefined,
value: undefined,
@@ -283,7 +291,9 @@ describe('generateDefaultTransactionMetrics', () => {
metametricsEvent: mockMetametricsEvent,
properties: {
account_eip7702_upgraded: undefined,
+ account_type: 'MetaMask',
chain_id: '0x1',
+ dapp_host_name: 'N/A',
eip7702_upgrade_transaction: false,
gas_estimation_failed: false,
gas_fee_presented: expect.any(Array),
@@ -296,7 +306,7 @@ describe('generateDefaultTransactionMetrics', () => {
transaction_type: 'simple_send',
},
sensitiveProperties: {
- from_address: '0x1',
+ from_address: FROM_ADDRESS_MOCK,
to_address: undefined,
value: undefined,
},
@@ -522,10 +532,12 @@ describe('generateDefaultTransactionMetrics', () => {
);
expect(metrics.properties).toStrictEqual({
account_eip7702_upgraded: undefined,
+ account_type: 'MetaMask',
api_method: 'wallet_sendCalls',
batch_transaction_count: 2,
batch_transaction_method: 'eip7702',
chain_id: '0xaa36a7',
+ dapp_host_name: 'metamask.github.io',
eip7702_upgrade_transaction: true,
gas_estimation_failed: true,
gas_fee_presented: ['custom'],
@@ -555,7 +567,9 @@ describe('generateDefaultTransactionMetrics', () => {
);
expect(metrics.properties).toStrictEqual({
account_eip7702_upgraded: undefined,
+ account_type: 'MetaMask',
chain_id: '0xaa36a7',
+ dapp_host_name: 'metamask',
eip7702_upgrade_transaction: true,
gas_estimation_failed: true,
gas_fee_presented: ['custom'],
@@ -590,7 +604,9 @@ describe('generateDefaultTransactionMetrics', () => {
);
expect(metrics.properties).toStrictEqual({
account_eip7702_upgraded: undefined,
+ account_type: 'MetaMask',
chain_id: '0xaa36a7',
+ dapp_host_name: 'metamask',
eip7702_upgrade_rejection: true,
eip7702_upgrade_transaction: true,
gas_estimation_failed: true,
@@ -620,10 +636,12 @@ describe('generateDefaultTransactionMetrics', () => {
);
expect(metrics.properties).toStrictEqual({
account_eip7702_upgraded: '0x63c0c19a282a1b52b07dd5a65b58948a07dae32b',
+ account_type: 'MetaMask',
api_method: 'wallet_sendCalls',
batch_transaction_count: 2,
batch_transaction_method: 'eip7702',
chain_id: '0x1',
+ dapp_host_name: 'jumper123.exchange',
eip7702_upgrade_transaction: false,
gas_estimation_failed: false,
gas_fee_presented: ['custom', 'low', 'medium', 'high'],
diff --git a/app/core/Engine/controllers/transaction-controller/utils.ts b/app/core/Engine/controllers/transaction-controller/utils.ts
index d6ae0a1b8d6..2279cf65116 100644
--- a/app/core/Engine/controllers/transaction-controller/utils.ts
+++ b/app/core/Engine/controllers/transaction-controller/utils.ts
@@ -25,6 +25,10 @@ import type {
TransactionEventHandlerRequest,
TransactionMetrics,
} from './types';
+import {
+ getAddressAccountType,
+ isValidHexAddress,
+} from '../../../../util/address';
const BATCHED_MESSAGE_TYPE = {
WALLET_SEND_CALLS: 'wallet_sendCalls',
@@ -164,7 +168,12 @@ export async function generateDefaultTransactionMetrics(
transactionMeta: TransactionMeta,
transactionEventHandlerRequest: TransactionEventHandlerRequest,
) {
- const { chainId, status, type, id } = transactionMeta;
+ const { chainId, status, type, id, origin, txParams } = transactionMeta || {};
+ const { from } = txParams || {};
+
+ const accountType = isValidHexAddress(from)
+ ? getAddressAccountType(from)
+ : 'unknown';
const batchProperties = await getBatchProperties(transactionMeta);
const gasFeeProperties = getGasMetricProperties(transactionMeta);
@@ -184,6 +193,8 @@ export async function generateDefaultTransactionMetrics(
transaction_envelope_type: transactionMeta.txParams.type,
transaction_internal_id: id,
transaction_type: getTransactionTypeValue(type),
+ account_type: accountType,
+ dapp_host_name: origin ?? 'N/A',
},
sensitiveProperties: {
from_address: transactionMeta.txParams.from,
diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts
index 3e8fa2c74d0..dd7fb0df284 100644
--- a/app/core/RPCMethods/RPCMethodMiddleware.ts
+++ b/app/core/RPCMethods/RPCMethodMiddleware.ts
@@ -664,7 +664,10 @@ export const getRpcMethodMiddleware = ({
parity_defaultAccount: getEthAccounts,
eth_sendTransaction: async () => {
checkTabActive();
-
+ const transactionAnalytics = {
+ dapp_url: url.current,
+ request_source: getSource(),
+ };
return RPCMethods.eth_sendTransaction({
hostname,
req,
@@ -686,6 +689,7 @@ export const getRpcMethodMiddleware = ({
isWalletConnect,
});
},
+ analytics: transactionAnalytics,
});
},
diff --git a/app/core/RPCMethods/eth_sendTransaction.test.ts b/app/core/RPCMethods/eth_sendTransaction.test.ts
index a00c2c17d49..ba521f895b3 100644
--- a/app/core/RPCMethods/eth_sendTransaction.test.ts
+++ b/app/core/RPCMethods/eth_sendTransaction.test.ts
@@ -13,6 +13,8 @@ import type {
} from '@metamask/transaction-controller';
import eth_sendTransaction from './eth_sendTransaction';
import PPOMUtil from '../../lib/ppom/ppom-util';
+import { updateConfirmationMetric } from '../redux/slices/confirmationMetrics';
+import { store } from '../../store';
jest.mock('../../core/Engine', () => ({
context: {
@@ -153,6 +155,8 @@ function getMockAddTransaction({
}
describe('eth_sendTransaction', () => {
+ const analytics = { dapp_url: 'example.metamask.io', request_source: 'test' };
+
it('sends the transaction and returns the resulting hash', async () => {
const mockAddress = '0x0000000000000000000000000000000000000001';
const mockTransactionParameters = { from: mockAddress };
@@ -172,6 +176,7 @@ describe('eth_sendTransaction', () => {
returnValue: expectedResult,
}),
validateAccountAndChainId: jest.fn(),
+ analytics,
});
expect(pendingResult.result).toBe(expectedResult);
@@ -193,6 +198,7 @@ describe('eth_sendTransaction', () => {
returnValue: 'fake-hash',
}),
validateAccountAndChainId: jest.fn(),
+ analytics,
}),
).rejects.toThrow('Invalid parameters: expected an array');
});
@@ -216,6 +222,7 @@ describe('eth_sendTransaction', () => {
returnValue: 'fake-hash',
}),
validateAccountAndChainId: jest.fn(),
+ analytics,
}),
).rejects.toThrow(
'Invalid parameters: expected the first parameter to be an object',
@@ -239,6 +246,7 @@ describe('eth_sendTransaction', () => {
validateAccountAndChainId: jest.fn().mockImplementation(async () => {
throw new Error('test validation error');
}),
+ analytics,
}),
).rejects.toThrow('test validation error');
});
@@ -262,6 +270,7 @@ describe('eth_sendTransaction', () => {
addTransactionError: new Error('Failed to add transaction'),
}),
validateAccountAndChainId: jest.fn(),
+ analytics,
}),
).rejects.toThrow('Failed to add transaction');
});
@@ -285,6 +294,7 @@ describe('eth_sendTransaction', () => {
processTransactionError: new Error('User rejected the transaction'),
}),
validateAccountAndChainId: jest.fn(),
+ analytics,
}),
).rejects.toThrow('User rejected the transaction');
});
@@ -309,8 +319,50 @@ describe('eth_sendTransaction', () => {
returnValue: expectedResult,
}),
validateAccountAndChainId: jest.fn(),
+ analytics: {
+ dapp_url: 'example.metamask.io',
+ request_source: 'test',
+ },
});
expect(spy).toBeCalledTimes(1);
});
+
+ it('dispatches updateConfirmationMetric with analytics payload', async () => {
+ const mockAddress = '0x0000000000000000000000000000000000000001';
+ const mockTransactionParameters = { from: mockAddress };
+ const expectedResult = 'fake-hash';
+ const pendingResult = constructPendingJsonRpcResponse();
+
+ const dispatchSpy = jest.spyOn(store, 'dispatch');
+
+ await eth_sendTransaction({
+ hostname: 'example.metamask.io',
+ req: constructSendTransactionRequest([mockTransactionParameters]),
+ res: pendingResult,
+ sendTransaction: getMockAddTransaction({
+ expectedTransaction: mockTransactionParameters,
+ expectedOrigin: {
+ origin: 'example.metamask.io',
+ networkClientId: NETWORK_CLIENT_ID_MOCK,
+ },
+ returnValue: expectedResult,
+ }),
+ validateAccountAndChainId: jest.fn(),
+ analytics: {
+ dapp_url: 'example.metamask.io',
+ request_source: 'test',
+ },
+ });
+
+ const id = '123';
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ updateConfirmationMetric({
+ id,
+ params: {
+ properties: { ...analytics },
+ },
+ }),
+ );
+ });
});
diff --git a/app/core/RPCMethods/eth_sendTransaction.ts b/app/core/RPCMethods/eth_sendTransaction.ts
index 9748092c600..598d466c082 100644
--- a/app/core/RPCMethods/eth_sendTransaction.ts
+++ b/app/core/RPCMethods/eth_sendTransaction.ts
@@ -11,6 +11,8 @@ import {
} from '@metamask/transaction-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import ppomUtil, { PPOMRequest } from '../../lib/ppom/ppom-util';
+import { updateConfirmationMetric } from '../redux/slices/confirmationMetrics';
+import { store } from '../../store';
/**
* A JavaScript object that is not `null`, a function, or an array.
@@ -75,6 +77,7 @@ async function eth_sendTransaction({
res,
sendTransaction,
validateAccountAndChainId,
+ analytics,
}: {
hostname: string;
req: JsonRpcRequest<[TransactionParams & JsonRpcParams]> & {
@@ -84,6 +87,7 @@ async function eth_sendTransaction({
res: PendingJsonRpcResponse;
sendTransaction: TransactionController['addTransaction'];
validateAccountAndChainId: (args: SendArgs) => Promise;
+ analytics: { dapp_url?: string; request_source?: string };
}) {
if (
!Array.isArray(req.params) &&
@@ -119,6 +123,16 @@ async function eth_sendTransaction({
transactionMeta,
});
+ const { id } = transactionMeta;
+ store.dispatch(
+ updateConfirmationMetric({
+ id,
+ params: {
+ properties: { ...analytics },
+ },
+ }),
+ );
+
res.result = await result;
}
diff --git a/app/images/account_status.png b/app/images/account_status.png
index d850f2c4969..c52c060c5e1 100644
Binary files a/app/images/account_status.png and b/app/images/account_status.png differ
diff --git a/app/images/secure_wallet.png b/app/images/secure_wallet.png
deleted file mode 100644
index a449907d123..00000000000
Binary files a/app/images/secure_wallet.png and /dev/null differ
diff --git a/app/images/secure_wallet_dark.png b/app/images/secure_wallet_dark.png
index 4551eb9fe69..8bd58544b02 100644
Binary files a/app/images/secure_wallet_dark.png and b/app/images/secure_wallet_dark.png differ
diff --git a/app/images/secure_wallet_light.png b/app/images/secure_wallet_light.png
new file mode 100644
index 00000000000..69e469e07e6
Binary files /dev/null and b/app/images/secure_wallet_light.png differ
diff --git a/app/styles/common.ts b/app/styles/common.ts
index 6013192cd64..57e5b31ac4d 100644
--- a/app/styles/common.ts
+++ b/app/styles/common.ts
@@ -20,6 +20,8 @@ export const colors = {
applePayBlack: '#000000',
applePayWhite: '#FFFFFF',
btnBlack: '#1C1E21',
+ btnBlackText: '#FFFFFF',
+ btnBlackInverse: 'rgba(60, 77, 157, 0.1)',
modalScrollButton: '#ECEEFF',
gettingStartedPageBackgroundColor: '#EAC2FF',
gettingStartedTextColor: '#3D065F',
diff --git a/app/util/transaction-controller/index.test.ts b/app/util/transaction-controller/index.test.ts
index 96d58e39cea..fbfd2cd63fa 100644
--- a/app/util/transaction-controller/index.test.ts
+++ b/app/util/transaction-controller/index.test.ts
@@ -97,6 +97,7 @@ jest.mock('../../core/Engine', () => ({
updateTransactionGasFees: jest.fn(),
updateAtomicBatchData: jest.fn(),
addTransactionBatch: jest.fn(),
+ updateSelectedGasFeeToken: jest.fn(),
},
},
}));
@@ -574,4 +575,18 @@ describe('Transaction Controller Util', () => {
updaterFunctionParams: [ID_MOCK, EIP_1559_TRANSACTION_PARAMS_MOCK],
});
});
+
+ describe('updateSelectedGasFeeToken', () => {
+ it('calls updateSelectedGasFeeToken with transactionId and selectedGasFeeToken', () => {
+ const transactionId = '0xabcdef1234567890abcdef1234567890abcdef';
+ const selectedGasFeeToken = '0x1234567890abcdef1234567890abcdef12345678';
+ TransactionControllerUtils.updateSelectedGasFeeToken(
+ transactionId,
+ selectedGasFeeToken,
+ );
+ expect(
+ Engine.context.TransactionController.updateSelectedGasFeeToken,
+ ).toHaveBeenCalledWith(transactionId, selectedGasFeeToken);
+ });
+ });
});
diff --git a/app/util/transaction-controller/index.ts b/app/util/transaction-controller/index.ts
index baf7233b58a..01e80453e6a 100644
--- a/app/util/transaction-controller/index.ts
+++ b/app/util/transaction-controller/index.ts
@@ -170,6 +170,18 @@ export const getNetworkNonce = async (
return nextNonce;
};
+export function updateSelectedGasFeeToken(
+ transactionId: string,
+ selectedGasFeeToken?: Hex,
+) {
+ const { TransactionController } = Engine.context;
+
+ return TransactionController.updateSelectedGasFeeToken(
+ transactionId,
+ selectedGasFeeToken,
+ );
+}
+
function sanitizeTransactionParamsGasValues(
transactionId: string,
requestedTransactionParamsToUpdate: Partial,
diff --git a/docs/testing/e2e/segment-events.md b/docs/testing/e2e/segment-events.md
index 9e967c7e489..f697c99a8a2 100644
--- a/docs/testing/e2e/segment-events.md
+++ b/docs/testing/e2e/segment-events.md
@@ -1,126 +1,348 @@
# E2E Testing for Segment Events
-This guide explains how to set up E2E tests for tracking Segment events in MetaMask Mobile.
+This guide explains how to test Segment analytics events in MetaMask Mobile E2E tests. **Segment is mocked by default** in all E2E tests, and you should **only use `withFixtures`** for test setup.
## Prerequisites
-1. Ensure you have the necessary imports:
-```javascript
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import { getEventsPayloads } from './helpers';
-import { EVENT_NAME } from '../../../app/core/Analytics/MetaMetrics.events';
-```
-
-## Two Approaches to Mocking Segment Events
-
-### 1. Using withFixtures (Recommended for Most Cases)
-
-This approach is simpler and integrates well with the existing test fixtures.
-
-```javascript
-const testSpecificMock = {
- POST: [mockEvents.POST.segmentTrack]
-};
-
-await withFixtures({
- // The withOnboardingFixture will also make explicit for tests that events
- // will be checked
- fixture: new FixtureBuilder().withOnboardingFixture().build(),
- restartDevice: true,
- testSpecificMock,
-}, async ({ mockServer }) => {
- // Your test code here
-
- // Get and verify events
- const events = await getEventsPayloads(mockServer, [
- EVENT_NAME.WALLET_IMPORTED,
- EVENT_NAME.WALLET_SETUP_COMPLETED
- ]);
+Ensure you have the necessary imports:
+
+```typescript
+import { withFixtures } from '../../framework/fixtures/FixtureHelper';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
+import { EventPayload, getEventsPayloads } from '../analytics/helpers';
+import { testSpecificMock } from './helpers/your-test-mocks'; // Your test-specific mocks
+```
+
+## Setting Up Segment Event Testing
+
+### Basic Setup with withFixtures
+
+Segment mocking is **automatically included** in all E2E tests. You don't need to set up any additional mocking - just use `withFixtures`:
+
+```typescript
+describe('Your Feature Analytics', () => {
+ let capturedEvents: EventPayload[] = [];
+
+ const EVENT_NAMES = {
+ FEATURE_STARTED: 'Feature Started',
+ FEATURE_COMPLETED: 'Feature Completed',
+ };
+
+ beforeEach(async () => {
+ jest.setTimeout(120000);
+ });
+
+ it('should track analytics events during feature usage', async () => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder()
+ .withGanacheNetwork()
+ .withMetaMetricsOptIn() // Required for event tracking
+ .build(),
+ testSpecificMock, // Your API mocks (not Segment mocks)
+ restartDevice: true,
+ endTestfn: async ({ mockServer }) => {
+ try {
+ // Capture events at the end of the test
+ capturedEvents = await getEventsPayloads(
+ mockServer,
+ [EVENT_NAMES.FEATURE_STARTED, EVENT_NAMES.FEATURE_COMPLETED],
+ 30000, // timeout in ms
+ );
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ console.error(`Error capturing events: ${errorMessage}`);
+ }
+ },
+ },
+ async () => {
+ // Your test implementation here
+ await loginToApp();
+ // Trigger actions that generate events
+ // ...
+ },
+ );
+ });
});
```
-### 2. Using startMockServer Directly
+## Event Verification
+
+### Basic Event Verification
+
+After capturing events, verify them in your test:
+
+```typescript
+it('should validate captured events', async () => {
+ // Filter events by type
+ const featureStartedEvents = capturedEvents.filter(
+ (e) => e.event === EVENT_NAMES.FEATURE_STARTED,
+ );
+
+ const featureCompletedEvents = capturedEvents.filter(
+ (e) => e.event === EVENT_NAMES.FEATURE_COMPLETED,
+ );
+
+ // Check event counts
+ await Assertions.checkIfArrayHasLength(featureStartedEvents, 1);
+ await Assertions.checkIfArrayHasLength(featureCompletedEvents, 1);
+
+ // Verify event properties
+ await Assertions.checkIfObjectContains(featureStartedEvents[0].properties, {
+ source: 'MainView',
+ chain_id: '1',
+ user_type: 'imported',
+ });
+});
+```
-This approach gives you more control over the mock server setup and is useful when you need to handle complex mocking scenarios.
+### Advanced Event Verification with SoftAssert
-```javascript
-const TEST_SPECIFIC_MOCK_SERVER_PORT = 8001;
-const segmentMock = {
- POST: [mockEvents.POST.segmentTrack]
-};
+For comprehensive event testing with better error reporting:
-mockServer = await startMockServer(segmentMock, TEST_SPECIFIC_MOCK_SERVER_PORT);
+```typescript
+import SoftAssert from '../../utils/SoftAssert';
-await TestHelpers.launchApp({
- newInstance: true,
- delete: true,
- launchArgs: {
- mockServerPort: String(TEST_SPECIFIC_MOCK_SERVER_PORT),
+it('should validate all event properties', async () => {
+ const softAssert = new SoftAssert();
+
+ // Check event counts
+ const checkEventCount = softAssert.checkAndCollect(
+ () => Assertions.checkIfArrayHasLength(capturedEvents, 4),
+ 'Should have 4 total events',
+ );
+
+ const checkStartedCount = softAssert.checkAndCollect(
+ () => Assertions.checkIfArrayHasLength(featureStartedEvents, 2),
+ 'Should have 2 feature started events',
+ );
+
+ // Verify properties for each event
+ const propertyAssertions = [];
+ for (let i = 0; i < featureStartedEvents.length; i++) {
+ propertyAssertions.push(
+ softAssert.checkAndCollect(async () => {
+ Assertions.checkIfObjectContains(featureStartedEvents[i].properties, {
+ action: 'Feature',
+ source: 'MainView',
+ chain_id: '1',
+ });
+ }, `Feature Started [${i}]: Check basic properties`),
+ softAssert.checkAndCollect(
+ () =>
+ Assertions.checkIfValueIsDefined(
+ featureStartedEvents[i].properties.timestamp,
+ ),
+ `Feature Started [${i}]: Check timestamp is defined`,
+ ),
+ );
}
+
+ // Execute all assertions
+ await Promise.all([
+ checkEventCount,
+ checkStartedCount,
+ ...propertyAssertions,
+ ]);
+
+ softAssert.throwIfErrors();
});
```
-## Verifying Events
+## Event Capture Patterns
-After setting up the mocks and running your test, you can verify the events using `getEventsPayloads`:
+### Pattern 1: Capture All Events (Recommended for Development)
-```javascript
-const events = await getEventsPayloads(mockServer, [
- EVENT_NAME.WALLET_IMPORTED,
- EVENT_NAME.WALLET_SETUP_COMPLETED
-]);
+```typescript
+endTestfn: async ({ mockServer }) => {
+ try {
+ // Capture all events without filtering for debugging
+ capturedEvents = await getEventsPayloads(mockServer, [], 30000);
+ console.log('All captured events:', capturedEvents);
+ } catch (error) {
+ console.error(`Error capturing events: ${error}`);
+ }
+},
+```
-// Check number of events
-await Assertions.checkIfArrayHasLength(events, 2);
+### Pattern 2: Capture Specific Events (Recommended for Production)
-// Find specific events
-const walletImportedEvent = events.find(
- (event) => event.event === EVENT_NAME.WALLET_IMPORTED
-);
+```typescript
+endTestfn: async ({ mockServer }) => {
+ try {
+ // Only capture the events you're testing
+ capturedEvents = await getEventsPayloads(
+ mockServer,
+ [EVENT_NAMES.SWAP_STARTED, EVENT_NAMES.SWAP_COMPLETED],
+ 30000
+ );
+ } catch (error) {
+ console.error(`Error capturing events: ${error}`);
+ }
+},
+```
-// Verify event properties
-await Assertions.checkIfObjectsMatch(
- walletImportedEvent.properties,
- { biometrics_enabled: false }
-);
+## MetaMetrics Opt-in Requirements
+
+### Always Use `.withMetaMetricsOptIn()`
+
+For events to be tracked, users must have opted into MetaMetrics. Always include this in your fixture:
+
+```typescript
+fixture: new FixtureBuilder()
+ .withGanacheNetwork()
+ .withMetaMetricsOptIn() // Required for event tracking
+ .build(),
+```
+
+### Exception: Onboarding Tests
+
+When testing the onboarding flow, opt-in happens during the flow when the accept button is tapped:
+
+```typescript
+fixture: new FixtureBuilder()
+ .withOnboardingFixture() // Opt-in happens during onboarding
+ .build(),
+```
+
+## Common Event Properties to Verify
+
+### Standard Properties
+
+Most events should include these properties:
+
+```typescript
+{
+ chain_id: '1', // Current network chain ID
+ account_type: 'Imported', // Account type
+ source: 'MainView', // Where the action was initiated
+ action: 'Feature', // Type of action
+ name: 'FeatureName', // Feature name
+}
+```
+
+### Dynamic Properties
+
+These properties are calculated at runtime and should be verified as defined:
+
+```typescript
+// Check that dynamic values exist
+await Assertions.checkIfValueIsDefined(event.properties.timestamp);
+await Assertions.checkIfValueIsDefined(event.properties.response_time);
+await Assertions.checkIfValueIsDefined(event.properties.network_fees_USD);
```
## Best Practices
-1. Use `getEventsPayloads` to retrieve and verify events
-2. Clean up mock servers after tests using `stopMockServer`
-3. Use appropriate assertions to verify event properties
-4. Consider testing both positive and negative cases (e.g., with and without metrics opt-in)
-## Important: MetaMetrics Opt-in State
+### 1. Use Descriptive Event Names
-When testing Segment events, it's crucial to ensure the MetaMetrics opt-in state is properly set. There are two scenarios to consider:
+```typescript
+const EVENT_NAMES = {
+ SWAP_STARTED: 'Swap Started',
+ SWAP_COMPLETED: 'Swap Completed',
+ QUOTES_RECEIVED: 'Quotes Received',
+} as const;
+```
-### 1. Using Onboarding Fixture
-When using `withOnboardingFixture()`, the opt-in state is automatically set during the onboarding flow **WHEN THE ACCEPT BUTTON IS TAPPED**. No additional configuration is needed.
+### 2. Capture Events at Test End
-```javascript
-await withFixtures({
- fixture: new FixtureBuilder().withOnboardingFixture().build(),
- // ... other config
-});
+Always capture events in the `endTestfn` to ensure all events are collected:
+
+```typescript
+endTestfn: async ({ mockServer }) => {
+ capturedEvents = await getEventsPayloads(mockServer, expectedEvents, 30000);
+},
```
-### 2. Using Injected State (Without Onboarding)
-When using injected state without the onboarding flow, you **must** explicitly set the MetaMetrics opt-in state using `withMetaMetricsOptIn()`:
+### 3. Test Both Success and Error Cases
-```javascript
-await withFixtures({
- fixture: new FixtureBuilder()
- .withFoo()
- .withMetaMetricsOptIn() // Required when not using onboarding
- .build(),
- // ... other config
+```typescript
+it('should track error events when transaction fails', async () => {
+ // Test error scenario and verify error events
});
```
+### 4. Use Timeouts for Event Capture
+
+Set reasonable timeouts for event capture (typically 30 seconds):
+
+```typescript
+capturedEvents = await getEventsPayloads(mockServer, events, 30000);
+```
+
## Troubleshooting
-If events are not being captured:
-1. Check mock server setup
-2. Ensure correct event names are being used
-3. If using injected state without onboarding, verify `withMetaMetricsOptIn()` is called
\ No newline at end of file
+### Events Not Being Captured
+
+1. **Check MetaMetrics opt-in**: Ensure `.withMetaMetricsOptIn()` is called
+2. **Check event names**: Verify event names match exactly (case-sensitive)
+3. **Check timing**: Events are captured at the end of the test in `endTestfn`
+4. **Check mock server**: Ensure `mockServer` is passed correctly to `getEventsPayloads`
+
+### Debugging Events
+
+Use the capture-all pattern to see what events are being generated:
+
+```typescript
+// Temporary debugging - capture all events
+capturedEvents = await getEventsPayloads(mockServer, [], 30000);
+console.log(
+ 'All events:',
+ capturedEvents.map((e) => e.event),
+);
+```
+
+## Example: Complete Test Structure
+
+```typescript
+describe('Feature Analytics', () => {
+ let capturedEvents: EventPayload[] = [];
+
+ const EVENT_NAMES = {
+ FEATURE_OPENED: 'Feature Opened',
+ FEATURE_COMPLETED: 'Feature Completed',
+ } as const;
+
+ beforeEach(async () => {
+ jest.setTimeout(120000);
+ });
+
+ it('should track feature analytics events', async () => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder()
+ .withGanacheNetwork()
+ .withMetaMetricsOptIn()
+ .build(),
+ testSpecificMock,
+ restartDevice: true,
+ endTestfn: async ({ mockServer }) => {
+ capturedEvents = await getEventsPayloads(
+ mockServer,
+ Object.values(EVENT_NAMES),
+ 30000,
+ );
+ },
+ },
+ async () => {
+ await loginToApp();
+ // Your test actions here
+ },
+ );
+
+ // Verify events
+ const openedEvents = capturedEvents.filter(
+ (e) => e.event === EVENT_NAMES.FEATURE_OPENED,
+ );
+
+ await Assertions.checkIfArrayHasLength(openedEvents, 1);
+ await Assertions.checkIfObjectContains(openedEvents[0].properties, {
+ source: 'MainView',
+ chain_id: '1',
+ });
+ });
+});
+```
+
+Remember: **Segment is mocked by default** - no additional setup required. Just use `withFixtures` and focus on your test logic!
diff --git a/e2e/MOCKING.md b/e2e/MOCKING.md
new file mode 100644
index 00000000000..8cc99e5f1fb
--- /dev/null
+++ b/e2e/MOCKING.md
@@ -0,0 +1,314 @@
+# E2E API Mocking Guide
+
+This guide explains how to mock APIs in MetaMask Mobile E2E tests. The mocking system allows tests to run predictably by intercepting HTTP requests and returning controlled responses.
+
+## Architecture Overview
+
+The E2E mocking system consists of three main components:
+
+1. **Default Mocks** (`e2e/api-mocking/mock-responses/defaults/`) - Shared mocks used across all tests
+2. **Test-Specific Mocks** - Custom mocks defined within individual test files
+
+### How Mocking Works
+
+- All network requests go through a proxy server that can intercept and mock responses
+- Default mocks are automatically loaded for all tests via `FixtureHelper.createMockAPIServer() -> startMockSerer()`
+- Test-specific mocks take precedence over default mocks
+- The mock server runs on a dedicated port and is automatically started/stopped by the test framework
+
+## Default Mocks
+
+Default mocks are organized by API category in `e2e/api-mocking/mock-responses/defaults/`:
+
+```
+defaults/
+├── index.ts # Aggregates all default mocks
+├── accounts.ts # Account-related API mocks
+├── defi-adapter.ts # DeFi protocol mocks
+├── dapp-scanning.ts # Dapp security scanning mocks
+├── metametrics-test.ts # Analytics mocks for testing
+├── onramp-apis.ts # Onramp service mocks
+├── price-apis.ts # Price feed mocks
+├── staking.ts # Staking API mocks
+├── swap-apis.ts # Swap/exchange API mocks
+├── token-apis.ts # Token metadata API mocks
+├── user-storage.ts # User storage service mocks
+├── walletconnect.ts # WalletConnect mocks
+└── web-3-auth.ts # Web3 authentication mocks
+```
+
+### Adding New Default Mocks
+
+To add default mocks that all tests can benefit from:
+
+1. **Create or edit a category file** in `defaults/` folder:
+
+```typescript
+// defaults/my-new-service.ts
+import { MockApiEndpoint } from '../../framework/types';
+
+export const MY_SERVICE_MOCKS = {
+ GET: [
+ {
+ urlEndpoint: 'https://api.myservice.com/data',
+ responseCode: 200,
+ response: { success: true, data: [] },
+ },
+ ] as MockApiEndpoint[],
+ POST: [
+ {
+ urlEndpoint: 'https://api.myservice.com/submit',
+ responseCode: 201,
+ response: { id: '123', status: 'created' },
+ },
+ ] as MockApiEndpoint[],
+};
+```
+
+2. **Add to the main index file**:
+
+```typescript
+// defaults/index.ts
+import { MY_SERVICE_MOCKS } from './my-new-service';
+
+export const DEFAULT_MOCKS = {
+ GET: [
+ // ... existing mocks
+ ...(MY_SERVICE_MOCKS.GET || []),
+ ],
+ POST: [
+ // ... existing mocks
+ ...(MY_SERVICE_MOCKS.POST || []),
+ ],
+ // ... other methods
+};
+```
+
+Default mocks are automatically included in all tests through `FixtureHelper.createMockAPIServer()`.
+
+## Test-Specific Mocks
+
+Test-specific mocks are defined within individual test files and take precedence over default mocks.
+
+### Method 1: Using testSpecificMock Parameter
+
+Pass a `testSpecificMock` function to `withFixtures`:
+
+```typescript
+import { withFixtures } from '../framework/fixtures/FixtureHelper';
+import { setupMockRequest } from '../api-mocking/mockHelpers';
+
+describe('My Test Suite', () => {
+ it('should handle custom API response', async () => {
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: 'https://api.example.com/custom',
+ response: { customData: 'test' },
+ responseCode: 200,
+ });
+ };
+
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder().build(),
+ testSpecificMock,
+ },
+ async ({ mockServer }) => {
+ // Your test code here
+ },
+ );
+ });
+});
+```
+
+### Method 2: Using Mock Server Reference
+
+Access the mock server directly within the test:
+
+```typescript
+await withFixtures(
+ {
+ fixture: new FixtureBuilder().build(),
+ },
+ async ({ mockServer }) => {
+ // Set up additional mocks within the test
+ await setupMockRequest(mockServer, {
+ requestMethod: 'POST',
+ url: 'https://api.example.com/submit',
+ response: { result: 'success' },
+ responseCode: 201,
+ });
+
+ // Your test code here
+ },
+);
+```
+
+## Mock Helper Functions
+
+The `e2e/api-mocking/mockHelpers.ts` file provides several utilities for mocking:
+
+### setupMockRequest
+
+For simple GET/POST/PUT/DELETE requests:
+
+```typescript
+await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: 'https://api.example.com/data',
+ response: { data: [] },
+ responseCode: 200,
+});
+```
+
+### setupMockPostRequest
+
+For POST requests with body validation:
+
+```typescript
+await setupMockPostRequest(
+ mockServer,
+ 'https://api.example.com/validate',
+ { field: 'expectedValue' }, // Expected request body
+ { result: 'validated' }, // Response
+ {
+ statusCode: 200,
+ ignoreFields: ['timestamp'], // Fields to ignore in body validation
+ },
+);
+```
+
+## FixtureHelper Integration
+
+### Automatic Mock Server Setup
+
+`FixtureHelper.createMockAPIServer()` automatically:
+
+1. Starts a mock server on the configured port
+2. Loads all default mocks from `defaults/index.ts`
+3. Applies test-specific mocks (if provided)
+4. Calls `mockNotificationServices()` for notification-related mocks
+
+### Notification Services Mocking
+
+The `mockNotificationServices()` function in `FixtureHelper.ts` automatically sets up mocks for:
+
+- Push notification APIs
+- Notification list/read/update endpoints
+- Feature announcement APIs
+- Authentication services for notifications
+
+This is applied to all tests automatically, so no additional setup is needed for basic notification functionality.
+
+## Best Practices
+
+### 1. Use Default Mocks for Common APIs
+
+If multiple tests need the same API mocked, add it to the appropriate default mock file rather than duplicating it in each test.
+
+### 2. Be Specific with URL Matching
+
+Use specific URL patterns to avoid unintended matches:
+
+```typescript
+// Good - specific endpoint
+urlEndpoint: 'https://api.metamask.io/prices/eth';
+
+// Better - use regex for dynamic parts
+urlEndpoint: /^https:\/\/api\.metamask\.io\/prices\/[a-z]+$/;
+
+// Avoid - too broad
+urlEndpoint: 'metamask.io';
+```
+
+### 3. Handle POST Request Bodies Properly
+
+For POST requests, validate the request body when needed:
+
+```typescript
+await setupMockPostRequest(
+ mockServer,
+ 'https://api.example.com/submit',
+ {
+ method: 'transfer',
+ amount: '1000000000000000000', // Expected request body
+ },
+ { success: true },
+ {
+ ignoreFields: ['timestamp', 'nonce'], // Ignore dynamic fields
+ },
+);
+```
+
+### 4. Use Descriptive Response Codes
+
+Always specify appropriate HTTP response codes:
+
+```typescript
+// Success cases
+responseCode: 200, // OK
+responseCode: 201, // Created
+responseCode: 204, // No Content
+
+// Error cases
+responseCode: 400, // Bad Request
+responseCode: 404, // Not Found
+responseCode: 500, // Internal Server Error
+```
+
+### 5. Organize Test-Specific Mocks
+
+For complex tests with many mocks, organize them in a setup function:
+
+```typescript
+const setupTestMocks = async (mockServer: Mockttp) => {
+ // Price API mocks
+ await setupMockRequest(mockServer, {
+ /* ... */
+ });
+
+ // Token API mocks
+ await setupMockRequest(mockServer, {
+ /* ... */
+ });
+
+ // Swap API mocks
+ await setupMockRequest(mockServer, {
+ /* ... */
+ });
+};
+
+// Use in test
+await withFixtures(
+ {
+ fixture: new FixtureBuilder().build(),
+ testSpecificMock: setupTestMocks,
+ },
+ async ({ mockServer }) => {
+ // Test code
+ },
+);
+```
+
+## Debugging Mocks
+
+### Live Request Validation
+
+The mock server tracks requests that weren't mocked and logs them at the end of tests. Check the test output for warnings about unmocked requests. (This will soon be enforced to track new untracked request)
+
+### Enable Debug Logging
+
+Add debug logging to see which mocks are being hit:
+
+```typescript
+// The mock helpers automatically log when mocks are triggered
+// Check test output for lines like:
+// "Mocking GET request to: https://api.example.com/data"
+```
+
+### Common Issues
+
+1. **Mock not triggering**: Check URL pattern matching
+2. **Wrong response**: Verify mock takes precedence (test-specific > default)
+3. **POST body validation failing**: Check `ignoreFields` and expected request body format
diff --git a/e2e/api-mocking/mock-e2e-allowlist.js b/e2e/api-mocking/mock-e2e-allowlist.js
index 56421dd9722..787c0a3fa0a 100644
--- a/e2e/api-mocking/mock-e2e-allowlist.js
+++ b/e2e/api-mocking/mock-e2e-allowlist.js
@@ -12,7 +12,7 @@ export const ALLOWLISTED_HOSTS = [
'*.infura.io',
'carrot.megaeth.com',
'testnet-rpc.monad.xyz',
- 'on-ramp-cache.uat-api.cx.metamask.io',
+ 'virtual.linea.rpc.tenderly.co',
];
export const ALLOWLISTED_URLS = [
@@ -27,4 +27,5 @@ export const ALLOWLISTED_URLS = [
'https://token.api.cx.metamask.io/assets/nativeCurrencyLogos/ethereum.svg',
'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/stETH.svg',
'https://raw.githubusercontent.com/MetaMask/contract-metadata/master/images/rETH.svg',
+ 'https://cdn.contentful.com:443/spaces/jdkgyfmyd9sw/environments/dev/entries?content_type=promotionalBanner&fields.showInMobile=true',
];
diff --git a/e2e/api-mocking/mock-responses/cardholder-mocks.ts b/e2e/api-mocking/mock-responses/cardholder-mocks.ts
index d55b7a51882..ba8d7f6a7f5 100644
--- a/e2e/api-mocking/mock-responses/cardholder-mocks.ts
+++ b/e2e/api-mocking/mock-responses/cardholder-mocks.ts
@@ -1,161 +1,134 @@
-import { CaipAccountId } from '@metamask/utils';
-import { MockApiEndpoint } from '../../framework/types';
+import { TestSpecificMock } from '../../framework/types';
import { DEFAULT_FIXTURE_ACCOUNT } from '../../framework/fixtures/FixtureBuilder';
-import { mockEvents } from '../mock-config/mock-events';
+import { setupMockRequest } from '../mockHelpers';
+import { createGeolocationResponse } from './ramps/ramps-geolocation';
+import { RAMPS_NETWORKS_RESPONSE } from './ramps/ramps-mocks';
+import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
/**
* Mock responses for cardholder API calls
* Used in E2E tests to avoid dependency on external APIs
*/
-/**
- * Get cardholder API mocks with realistic responses
- * @returns {CardholderApiMocks} Object containing GET mocks for cardholder APIs
- */
-const getCardholderApiMocks = (
- caipAccountAddresses: CaipAccountId[],
- cardholderAddresses?: CaipAccountId[],
-): MockApiEndpoint => {
- const url = new URL('v1/metadata', 'https://accounts.api.cx.metamask.io');
- url.searchParams.set(
- 'accountIds',
- caipAccountAddresses.join(',').toLowerCase(),
- );
- url.searchParams.set('label', 'card_user');
-
- return {
- urlEndpoint: url.toString(),
- response: {
- is: cardholderAddresses || caipAccountAddresses,
- },
- responseCode: 200,
- };
-};
-
-const cardApiMocks = getCardholderApiMocks([
- `eip155:0:${DEFAULT_FIXTURE_ACCOUNT.toLowerCase()}`,
-]);
-
-export const testSpecificMock = {
- GET: [
- cardApiMocks,
+const clientConfig = {
+ urlEndpoint:
+ 'https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=dev',
+ response: [
{
- urlEndpoint: 'https://on-ramp.dev-api.cx.metamask.io/geolocation',
- response: 'PT',
- responseCode: 200,
- },
- {
- urlEndpoint:
- 'https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=dev',
- response: [
- {
- depositConfig: {
- active: true,
- entrypoints: {
- walletActions: true,
- },
- minimumVersion: '1.0.0',
- providerApiKey: 'DUMMY_VALUE',
- providerFrontendAuth: 'DUMMY_VALUE',
- },
- cardFeature: {
- constants: {
- accountsApiUrl: 'https://accounts.api.cx.metamask.io',
- onRampApiUrl: 'https://on-ramp.uat-api.cx.metamask.io',
- },
- chains: {
- 'eip155:59144': {
- tokens: [
- {
- symbol: 'USDC',
- address: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff',
- decimals: 6,
- enabled: true,
- name: 'USD Coin',
- },
- {
- enabled: true,
- name: 'Tether USD',
- symbol: 'USDT',
- address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93',
- decimals: 6,
- },
- {
- address: '0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f',
- decimals: 18,
- enabled: true,
- name: 'Wrapped Ether',
- symbol: 'WETH',
- },
- {
- decimals: 18,
- enabled: true,
- name: 'EURe',
- symbol: 'EURe',
- address: '0x3ff47c5Bf409C86533FE1f4907524d304062428D',
- },
- {
- name: 'GBPe',
- symbol: 'GBPe',
- address: '0x3Bce82cf1A2bc357F956dd494713Fe11DC54780f',
- decimals: 18,
- enabled: true,
- },
- {
- decimals: 6,
- enabled: true,
- name: 'Aave USDC',
- symbol: 'aUSDC',
- address: '0x374D7860c4f2f604De0191298dD393703Cce84f3',
- },
- ],
- balanceScannerAddress:
- '0xed9f04f2da1b42ae558d5e688fe2ef7080931c9a',
+ depositConfig: {
+ active: true,
+ entrypoints: {
+ walletActions: true,
+ },
+ minimumVersion: '1.0.0',
+ providerApiKey: 'DUMMY_VALUE',
+ providerFrontendAuth: 'DUMMY_VALUE',
+ },
+ cardFeature: {
+ constants: {
+ accountsApiUrl: 'https://accounts.api.cx.metamask.io',
+ onRampApiUrl: 'https://on-ramp.uat-api.cx.metamask.io',
+ },
+ chains: {
+ 'eip155:59144': {
+ tokens: [
+ {
+ symbol: 'USDC',
+ address: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff',
+ decimals: 6,
+ enabled: true,
+ name: 'USD Coin',
+ },
+ {
+ enabled: true,
+ name: 'Tether USD',
+ symbol: 'USDT',
+ address: '0xA219439258ca9da29E9Cc4cE5596924745e12B93',
+ decimals: 6,
+ },
+ {
+ address: '0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f',
+ decimals: 18,
enabled: true,
- foxConnectAddresses: {
- us: '0xA90b298d05C2667dDC64e2A4e17111357c215dD2',
- global: '0x9dd23A4a0845f10d65D293776B792af1131c7B30',
- },
+ name: 'Wrapped Ether',
+ symbol: 'WETH',
},
+ {
+ decimals: 18,
+ enabled: true,
+ name: 'EURe',
+ symbol: 'EURe',
+ address: '0x3ff47c5Bf409C86533FE1f4907524d304062428D',
+ },
+ {
+ name: 'GBPe',
+ symbol: 'GBPe',
+ address: '0x3Bce82cf1A2bc357F956dd494713Fe11DC54780f',
+ decimals: 18,
+ enabled: true,
+ },
+ {
+ decimals: 6,
+ enabled: true,
+ name: 'Aave USDC',
+ symbol: 'aUSDC',
+ address: '0x374D7860c4f2f604De0191298dD393703Cce84f3',
+ },
+ ],
+ balanceScannerAddress: '0xed9f04f2da1b42ae558d5e688fe2ef7080931c9a',
+ enabled: true,
+ foxConnectAddresses: {
+ us: '0xA90b298d05C2667dDC64e2A4e17111357c215dD2',
+ global: '0x9dd23A4a0845f10d65D293776B792af1131c7B30',
},
},
},
- ],
- responseCode: 200,
- },
- ],
- POST: [
- mockEvents.POST.segmentTrack,
- {
- urlEndpoint: 'https://linea-mainnet.infura.io/v3/',
- requestBody: {
- method: 'eth_chainId',
- params: [],
},
- ignoreFields: ['id', 'jsonrpc'],
- response: { jsonrpc: '2.0', id: 42, result: '0xe708' },
- responseCode: 200,
- },
- {
- urlEndpoint: 'https://linea-mainnet.infura.io/v3/',
- requestBody: {
- method: 'eth_call',
- params: [
- {
- to: '0xed9f04f2da1b42ae558d5e688fe2ef7080931c9a',
- data: '0xda89f7dd00000000000000000000000076cf1cdd1fcc252442b50d6e97207228aa4aefc3000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000006000000000000000000000000176211869ca2b568f2a7d4ee941e073a821ee1ff000000000000000000000000a219439258ca9da29e9cc4ce5596924745e12b93000000000000000000000000e5d7c2a44ffddf6b295a15c148167daaaf5cf34f0000000000000000000000003ff47c5bf409c86533fe1f4907524d304062428d0000000000000000000000003bce82cf1a2bc357f956dd494713fe11dc54780f000000000000000000000000374d7860c4f2f604de0191298dd393703cce84f3000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd200000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd200000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd200000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd200000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd200000000000000000000000000000000000000000000000000000000000000020000000000000000000000009dd23a4a0845f10d65d293776b792af1131c7b30000000000000000000000000a90b298d05c2667ddc64e2a4e17111357c215dd2',
- },
- 'latest',
- ],
- },
- ignoreFields: ['id', 'jsonrpc'],
- response: {
- jsonrpc: '2.0',
- id: 44,
- result:
- '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000004e0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000',
- },
- responseCode: 200,
},
],
+ responseCode: 200,
+};
+
+export const testSpecificMock: TestSpecificMock = async (mockServer) => {
+ // Geolocation mocks - set to Spain (all envs)
+ for (const mock of createGeolocationResponse(
+ RampsRegions[RampsRegionsEnum.SPAIN],
+ )) {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: mock.urlEndpoint,
+ response: mock.response,
+ responseCode: mock.responseCode,
+ });
+ }
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/regions\/networks\?.*$/,
+ response: RAMPS_NETWORKS_RESPONSE,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/regions\/networks\?.*$/,
+ response: RAMPS_NETWORKS_RESPONSE,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: clientConfig.urlEndpoint,
+ response: clientConfig.response,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: /^https:\/\/accounts\.api\.cx\.metamask\.io\/v1\/metadata\?.*$/,
+ response: {
+ is: [`eip155:0:${DEFAULT_FIXTURE_ACCOUNT.toLowerCase()}`],
+ },
+ responseCode: 200,
+ });
};
diff --git a/e2e/api-mocking/mock-responses/defaults/accounts.ts b/e2e/api-mocking/mock-responses/defaults/accounts.ts
index 93de5e8cf66..339e92baa46 100644
--- a/e2e/api-mocking/mock-responses/defaults/accounts.ts
+++ b/e2e/api-mocking/mock-responses/defaults/accounts.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-useless-escape */
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
export const ACCOUNTS_API_TRANSACTIONS_RESPONSE = {
data: [
{
@@ -1214,7 +1214,7 @@ export const ACCOUNTS_API_ACTIVE_NETWORKS_RESPONSE = {
* Returns empty/basic responses to prevent API failures.
* For specific account tests, add detailed mocks in the test files.
*/
-export const DEFAULT_ACCOUNTS_MOCK: TestSpecificMock = {
+export const DEFAULT_ACCOUNTS_MOCK: MockEventsObject = {
GET: [
{
urlEndpoint:
@@ -1246,7 +1246,45 @@ export const DEFAULT_ACCOUNTS_MOCK: TestSpecificMock = {
urlEndpoint:
/^https:\/\/accounts\.api\.cx\.metamask\.io\/v2\/accounts\/[^\/]+\/balances\?.*$/,
responseCode: 200,
- response: { count: 0, balances: [], unprocessedNetworks: [] },
+ response: {
+ count: 3,
+ balances: [
+ {
+ object: 'token',
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ type: 'native',
+ timestamp: '2015-07-30T15:26:13.000Z',
+ decimals: 18,
+ chainId: 1,
+ balance: '0.5',
+ },
+ {
+ object: 'token',
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ symbol: 'USDC',
+ name: 'USD Coin',
+ type: 'erc20',
+ timestamp: '2018-05-28T00:00:00.000Z',
+ decimals: 6,
+ chainId: 1,
+ balance: '1000.0',
+ },
+ {
+ object: 'token',
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'DAI',
+ name: 'Dai Stablecoin',
+ type: 'erc20',
+ timestamp: '2017-12-18T00:00:00.000Z',
+ decimals: 18,
+ chainId: 1,
+ balance: '500.0',
+ },
+ ],
+ unprocessedNetworks: [],
+ },
},
{
urlEndpoint:
diff --git a/e2e/api-mocking/mock-responses/defaults/dapp-scanning.ts b/e2e/api-mocking/mock-responses/defaults/dapp-scanning.ts
index 9783d7848ae..5caf8b8bba7 100644
--- a/e2e/api-mocking/mock-responses/defaults/dapp-scanning.ts
+++ b/e2e/api-mocking/mock-responses/defaults/dapp-scanning.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
/**
* Minimal mock data for MetaMask dapp scanning API endpoints used in E2E testing.
* Returns basic feature flags structure to prevent API failures.
* For specific swap tests, add detailed mocks in the test files.
*/
-export const DAPP_SCANNING_MOCKS: TestSpecificMock = {
+export const DAPP_SCANNING_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint:
diff --git a/e2e/api-mocking/mock-responses/defaults/defi-adapter.ts b/e2e/api-mocking/mock-responses/defaults/defi-adapter.ts
index f2670b0e0e5..a07bcf532c0 100644
--- a/e2e/api-mocking/mock-responses/defaults/defi-adapter.ts
+++ b/e2e/api-mocking/mock-responses/defaults/defi-adapter.ts
@@ -1,4 +1,4 @@
-import { MockApiEndpoint, TestSpecificMock } from '../../../framework';
+import { MockApiEndpoint, MockEventsObject } from '../../../framework';
const defiAdaptersRegex =
/^https:\/\/defiadapters\.api\.cx\.metamask\.io\/positions\/.*$/;
@@ -11,6 +11,6 @@ export const noDefiPositionsMock = {
},
} as unknown as MockApiEndpoint;
-export const DEFI_ADAPTERS_MOCKS: TestSpecificMock = {
+export const DEFI_ADAPTERS_MOCKS: MockEventsObject = {
GET: [noDefiPositionsMock],
};
diff --git a/e2e/api-mocking/mock-responses/defaults/index.ts b/e2e/api-mocking/mock-responses/defaults/index.ts
index ae6c38e2e46..9f49cf9c812 100644
--- a/e2e/api-mocking/mock-responses/defaults/index.ts
+++ b/e2e/api-mocking/mock-responses/defaults/index.ts
@@ -7,7 +7,6 @@ import { DAPP_SCANNING_MOCKS } from './dapp-scanning';
import { PRICE_API_MOCKS } from './price-apis';
import { WEB_3_AUTH_MOCKS } from './web-3-auth';
import { DEFI_ADAPTERS_MOCKS } from './defi-adapter';
-import { ONRAMP_API_MOCKS } from './onramp-apis';
import { TOKEN_API_MOCKS } from './token-apis';
import { SWAP_API_MOCKS } from './swap-apis';
import { STAKING_MOCKS } from './staking';
@@ -16,6 +15,7 @@ import { METAMETRICS_API_MOCKS } from './metametrics-test';
import { DEFAULT_ACCOUNTS_MOCK } from './accounts';
import { getAuthMocks } from '../auth-mocks';
import { USER_STORAGE_MOCK } from './user-storage';
+import { DEFAULT_RAMPS_API_MOCKS } from './onramp-apis';
// Get auth mocks
const authMocks = getAuthMocks();
@@ -28,17 +28,19 @@ export const DEFAULT_MOCKS = {
...(WEB_3_AUTH_MOCKS.GET || []),
...(SWAP_API_MOCKS.GET || []),
...(STAKING_MOCKS.GET || []),
- ...(ONRAMP_API_MOCKS.GET || []),
...(TOKEN_API_MOCKS.GET || []),
...(DEFI_ADAPTERS_MOCKS.GET || []),
...(DEFAULT_ACCOUNTS_MOCK.GET || []),
...(USER_STORAGE_MOCK.GET || []),
+ ...(DEFAULT_RAMPS_API_MOCKS.GET || []),
+ // IPFS Mock
{
urlEndpoint:
/^https:\/\/dweb\.link\/ipfs\/[a-zA-Z0-9]+#x-ipfs-companion-no-redirect$/,
responseCode: 200,
response: 'Hello from IPFS Gateway Checker',
},
+ // Security Alerts Mock - Always responds with benign unless overridden by testSpecificMock
{
urlEndpoint:
/^https:\/\/security-alerts\.api\.cx\.metamask\.io\/validate\/0x[a-fA-F0-9]+$/,
@@ -63,18 +65,6 @@ export const DEFAULT_MOCKS = {
status: 1,
},
},
- {
- urlEndpoint: 'https://token-api.metaswap.codefi.network/tokens',
- responseCode: 200,
- response: [],
- },
- {
- urlEndpoint: 'https://security-alerts.api.cx.metamask.io/validate',
- responseCode: 200,
- response: {
- flagAsDangerous: 0,
- },
- },
],
PUT: [...(USER_STORAGE_MOCK.PUT || [])],
DELETE: [],
diff --git a/e2e/api-mocking/mock-responses/defaults/metametrics-test.ts b/e2e/api-mocking/mock-responses/defaults/metametrics-test.ts
index b2dc136e797..af880d41eb7 100644
--- a/e2e/api-mocking/mock-responses/defaults/metametrics-test.ts
+++ b/e2e/api-mocking/mock-responses/defaults/metametrics-test.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
/**
* Minimal mock data for MetaMask swap API endpoints used in E2E testing.
* Returns basic feature flags structure to prevent API failures.
* For specific swap tests, add detailed mocks in the test files.
*/
-export const METAMETRICS_API_MOCKS: TestSpecificMock = {
+export const METAMETRICS_API_MOCKS: MockEventsObject = {
POST: [
{
urlEndpoint: 'https://metametrics.test/track',
diff --git a/e2e/api-mocking/mock-responses/defaults/onramp-apis.ts b/e2e/api-mocking/mock-responses/defaults/onramp-apis.ts
index 2b7c5493c36..5bf74d25bd7 100644
--- a/e2e/api-mocking/mock-responses/defaults/onramp-apis.ts
+++ b/e2e/api-mocking/mock-responses/defaults/onramp-apis.ts
@@ -1,21 +1,28 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
+import { RampsRegions, RampsRegionsEnum } from '../../../framework/Constants';
+import { RAMPS_NETWORKS_RESPONSE } from '../ramps/ramps-mocks';
+import { createGeolocationResponse } from '../ramps/ramps-geolocation';
/**
* Mock data for on-ramp API endpoints used in E2E testing.
- * Covers geolocation and other on-ramp services.
+ * Covers geolocation and network information.
+ * Can be overriden by testSpecificMock
*/
-export const ONRAMP_API_MOCKS: TestSpecificMock = {
+export const DEFAULT_RAMPS_API_MOCKS: MockEventsObject = {
GET: [
+ ...createGeolocationResponse(RampsRegions[RampsRegionsEnum.UNITED_STATES]),
{
- urlEndpoint: 'https://on-ramp.dev-api.cx.metamask.io/geolocation',
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/regions\/networks\?.*$/,
responseCode: 200,
- response: 'US',
+ response: RAMPS_NETWORKS_RESPONSE,
},
{
- urlEndpoint: 'https://on-ramp.api.cx.metamask.io/geolocation',
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/regions\/networks\?.*$/,
responseCode: 200,
- response: 'US',
+ response: RAMPS_NETWORKS_RESPONSE,
},
],
};
diff --git a/e2e/api-mocking/mock-responses/defaults/price-apis.ts b/e2e/api-mocking/mock-responses/defaults/price-apis.ts
index 429695960e3..0d23913911f 100644
--- a/e2e/api-mocking/mock-responses/defaults/price-apis.ts
+++ b/e2e/api-mocking/mock-responses/defaults/price-apis.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
/**
* Mock data for cryptocurrency price API endpoints used in E2E testing.
* Returns stable price data to ensure consistent balance displays.
* Uses round numbers to make test assertions predictable.
*/
-export const PRICE_API_MOCKS: TestSpecificMock = {
+export const PRICE_API_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint:
diff --git a/e2e/api-mocking/mock-responses/defaults/staking.ts b/e2e/api-mocking/mock-responses/defaults/staking.ts
index 950d03a362b..e20a9498a8b 100644
--- a/e2e/api-mocking/mock-responses/defaults/staking.ts
+++ b/e2e/api-mocking/mock-responses/defaults/staking.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
/**
* Minimal mock data for staking API endpoints used in E2E testing.
* Returns empty/basic responses to prevent API failures.
* For specific staking tests, add detailed mocks in the test files.
*/
-export const STAKING_MOCKS: TestSpecificMock = {
+export const STAKING_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint:
diff --git a/e2e/api-mocking/mock-responses/defaults/swap-apis.ts b/e2e/api-mocking/mock-responses/defaults/swap-apis.ts
index 7325e1f1cce..b2fe0663cfd 100644
--- a/e2e/api-mocking/mock-responses/defaults/swap-apis.ts
+++ b/e2e/api-mocking/mock-responses/defaults/swap-apis.ts
@@ -1,4 +1,4 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
export const SWAPS_FEATURE_FLAG_RESPONSE = {
ethereum: {
@@ -170,7 +170,7 @@ export const SWAPS_FEATURE_FLAG_RESPONSE = {
* Returns basic feature flags structure to prevent API failures.
* For specific swap tests, add detailed mocks in the test files.
*/
-export const SWAP_API_MOCKS: TestSpecificMock = {
+export const SWAP_API_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint: 'https://swap.dev-api.cx.metamask.io/featureFlags',
diff --git a/e2e/api-mocking/mock-responses/defaults/token-apis.ts b/e2e/api-mocking/mock-responses/defaults/token-apis.ts
index 9213fa39f31..9fa95fda2c0 100644
--- a/e2e/api-mocking/mock-responses/defaults/token-apis.ts
+++ b/e2e/api-mocking/mock-responses/defaults/token-apis.ts
@@ -1,4 +1,4 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
import { TOKEN_API_TOKENS_RESPONSE } from '../token-api-responses';
/**
@@ -9,7 +9,7 @@ import { TOKEN_API_TOKENS_RESPONSE } from '../token-api-responses';
const tokenListRegex =
/^https:\/\/token\.api\.cx\.metamask\.io\/tokens\/\d+\?.*$/;
-export const TOKEN_API_MOCKS: TestSpecificMock = {
+export const TOKEN_API_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint: tokenListRegex,
diff --git a/e2e/api-mocking/mock-responses/defaults/user-storage.ts b/e2e/api-mocking/mock-responses/defaults/user-storage.ts
index 57c5a31176f..a549fca366d 100644
--- a/e2e/api-mocking/mock-responses/defaults/user-storage.ts
+++ b/e2e/api-mocking/mock-responses/defaults/user-storage.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
const accountsStorageUrl =
'https://user-storage.api.cx.metamask.io/api/v1/userstorage/accounts_v2';
const contactStorageUrl =
'https://user-storage.api.cx.metamask.io/api/v1/userstorage/addressBook';
-export const USER_STORAGE_MOCK: TestSpecificMock = {
+export const USER_STORAGE_MOCK: MockEventsObject = {
GET: [
{
urlEndpoint: contactStorageUrl,
diff --git a/e2e/api-mocking/mock-responses/defaults/walletconnect.ts b/e2e/api-mocking/mock-responses/defaults/walletconnect.ts
index 36f1fdb3ab4..8be591bd991 100644
--- a/e2e/api-mocking/mock-responses/defaults/walletconnect.ts
+++ b/e2e/api-mocking/mock-responses/defaults/walletconnect.ts
@@ -1,11 +1,11 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
/**
* Minimal mock data for WalletConnect API endpoints used in E2E testing.
* Returns basic responses to prevent API failures.
* For specific WalletConnect tests, add detailed mocks in the test files.
*/
-export const WALLETCONNECT_MOCKS: TestSpecificMock = {
+export const WALLETCONNECT_MOCKS: MockEventsObject = {
POST: [
{
urlEndpoint: /^https:\/\/pulse\.walletconnect\.org\/batch\?.*$/,
diff --git a/e2e/api-mocking/mock-responses/defaults/web-3-auth.ts b/e2e/api-mocking/mock-responses/defaults/web-3-auth.ts
index 19bbcb6a35b..b7e606b7f07 100644
--- a/e2e/api-mocking/mock-responses/defaults/web-3-auth.ts
+++ b/e2e/api-mocking/mock-responses/defaults/web-3-auth.ts
@@ -1,4 +1,4 @@
-import { TestSpecificMock } from '../../../framework';
+import { MockEventsObject } from '../../../framework';
const response = {
nodeDetails: {
@@ -63,7 +63,7 @@ const response = {
* Returns basic node details to prevent API failures.
* For specific Web3Auth tests, add detailed mocks in the test files.
*/
-export const WEB_3_AUTH_MOCKS: TestSpecificMock = {
+export const WEB_3_AUTH_MOCKS: MockEventsObject = {
GET: [
{
urlEndpoint:
diff --git a/e2e/api-mocking/mock-responses/ramps-mocks.ts b/e2e/api-mocking/mock-responses/ramps-mocks.ts
deleted file mode 100644
index 9dce8b638cc..00000000000
--- a/e2e/api-mocking/mock-responses/ramps-mocks.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import { TestSpecificMock } from '../../framework/types';
-
-/**
- * Mock responses for ramps API calls
- * Used in E2E tests to avoid dependency on external APIs
- */
-
-/**
- * Get ramps API mocks with realistic responses for buy and sell flows
- * @returns {RampsApiMocks} Object containing GET and POST mocks for ramps APIs
- */
-export const getRampsApiMocks = (): TestSpecificMock => ({
- GET: [
- // Mock getQuotes for buy - more flexible URL matching
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/crypto-currencies/1/eth/fiat-currencies/eur/quotes',
- response: {
- quotes: [
- {
- provider: {
- id: '/providers/moonpay-staging',
- name: 'MoonPay (Staging)',
- logos: {
- light:
- 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/moonpay_light.png',
- dark: 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/moonpay_dark.png',
- height: 24,
- width: 88,
- },
- features: {
- buy: {
- enabled: true,
- browser: 'APP_BROWSER',
- supportedByBackend: true,
- redirection: 'JSON_REDIRECTION',
- },
- quotes: {
- enabled: true,
- supportedByBackend: false,
- },
- },
- },
- crypto: {
- id: '/currencies/crypto/1/eth',
- symbol: 'ETH',
- decimals: 18,
- network: {
- chainId: '1',
- chainName: 'Ethereum Mainnet',
- },
- },
- fiat: {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- decimals: 2,
- },
- amountIn: 100,
- amountOut: 0.035,
- networkFee: 2.5,
- providerFee: 1.5,
- error: false,
- tags: { isBestRate: true, isMostReliable: true },
- exchangeRate: 2857.14,
- amountOutInFiat: 97.5,
- },
- {
- provider: {
- id: '/providers/banxa-staging',
- name: 'Banxa (Staging)',
- logos: {
- light:
- 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/banxa_light.png',
- dark: 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/banxa_dark.png',
- height: 24,
- width: 65,
- },
- features: {
- buy: {
- enabled: true,
- browser: 'APP_BROWSER',
- supportedByBackend: true,
- redirection: 'JSON_REDIRECTION',
- },
- quotes: {
- enabled: true,
- supportedByBackend: false,
- },
- },
- },
- crypto: {
- id: '/currencies/crypto/1/eth',
- symbol: 'ETH',
- decimals: 18,
- network: {
- chainId: '1',
- chainName: 'Ethereum Mainnet',
- },
- },
- fiat: {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- decimals: 2,
- },
- amountIn: 100,
- amountOut: 0.034,
- networkFee: 3.0,
- providerFee: 2.0,
- error: false,
- tags: { isBestRate: false, isMostReliable: false },
- exchangeRate: 2941.18,
- amountOutInFiat: 95.0,
- },
- ],
- sorted: [],
- customActions: [],
- },
- responseCode: 200,
- },
- // Mock getSellQuotes for sell - more flexible URL matching
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/crypto-currencies/1/eth/fiat-currencies/eur/sell-quotes',
- response: {
- quotes: [
- {
- provider: {
- id: '/providers/moonpay-staging',
- name: 'MoonPay (Staging)',
- logos: {
- light:
- 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/moonpay_light.png',
- dark: 'https://on-ramp.dev-api.cx.metamask.io/assets/providers/moonpay_dark.png',
- height: 24,
- width: 88,
- },
- features: {
- sell: {
- enabled: true,
- },
- sellQuotes: {
- enabled: true,
- },
- },
- },
- crypto: {
- id: '/currencies/crypto/1/eth',
- symbol: 'ETH',
- decimals: 18,
- network: {
- chainId: '1',
- chainName: 'Ethereum Mainnet',
- },
- },
- fiat: {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- decimals: 2,
- },
- amountIn: 0.1,
- amountOut: 280,
- networkFee: 2.5,
- providerFee: 1.5,
- error: false,
- tags: { isBestRate: true, isMostReliable: true },
- exchangeRate: 2800,
- amountOutInFiat: 280,
- },
- ],
- sorted: [],
- customActions: [],
- },
- responseCode: 200,
- },
- // Mock getLimits for buy
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/crypto-currencies/1/eth/fiat-currencies/eur/limits',
- response: {
- minAmount: 20,
- maxAmount: 10000,
- },
- responseCode: 200,
- },
- // Mock getSellLimits for sell
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/crypto-currencies/1/eth/fiat-currencies/eur/sell-limits',
- response: {
- minAmount: 0.01,
- maxAmount: 10,
- },
- responseCode: 200,
- },
- // Mock getPaymentMethods for buy
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/crypto-currencies/1/eth/fiat-currencies/eur/payment-methods',
- response: [
- {
- id: '/payments/credit-debit-card',
- name: 'Credit/Debit Card',
- description: 'Pay with your credit or debit card',
- },
- {
- id: '/payments/bank-transfer',
- name: 'Bank Transfer',
- description: 'Pay via bank transfer',
- },
- ],
- responseCode: 200,
- },
- // Mock getSellPaymentMethods for sell
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/crypto-currencies/1/eth/fiat-currencies/eur/sell-payment-methods',
- response: [
- {
- id: '/payments/bank-transfer',
- name: 'Bank Transfer',
- description: 'Receive funds via bank transfer',
- },
- ],
- responseCode: 200,
- },
- // Mock getFiatCurrencies for buy
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/fiat-currencies',
- response: [
- {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- name: 'Euro',
- decimals: 2,
- },
- {
- id: '/currencies/fiat/usd',
- symbol: 'USD',
- name: 'US Dollar',
- decimals: 2,
- },
- ],
- responseCode: 200,
- },
- // Mock getSellFiatCurrencies for sell
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/bank-transfer/fiat-currencies',
- response: [
- {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- name: 'Euro',
- decimals: 2,
- },
- {
- id: '/currencies/fiat/usd',
- symbol: 'USD',
- name: 'US Dollar',
- decimals: 2,
- },
- ],
- responseCode: 200,
- },
- // Mock getDefaultFiatCurrency for buy
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/default-fiat-currency',
- response: {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- name: 'Euro',
- decimals: 2,
- },
- responseCode: 200,
- },
- // Mock getDefaultSellFiatCurrency for sell
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/default-sell-fiat-currency',
- response: {
- id: '/currencies/fiat/eur',
- symbol: 'EUR',
- name: 'Euro',
- decimals: 2,
- },
- responseCode: 200,
- },
- // Mock getCryptoCurrencies for buy
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/credit-debit-card/fiat-currencies/eur/crypto-currencies',
- response: [
- {
- id: '/currencies/crypto/1/eth',
- symbol: 'ETH',
- name: 'Ethereum',
- decimals: 18,
- network: {
- chainId: '1',
- chainName: 'Ethereum Mainnet',
- },
- },
- ],
- responseCode: 200,
- },
- // Mock getSellCryptoCurrencies for sell
- {
- urlEndpoint:
- 'https://on-ramp.dev-api.cx.metamask.io/regions/fr/payment-methods/bank-transfer/fiat-currencies/eur/crypto-currencies',
- response: [
- {
- id: '/currencies/crypto/1/eth',
- symbol: 'ETH',
- name: 'Ethereum',
- decimals: 18,
- network: {
- chainId: '1',
- chainName: 'Ethereum Mainnet',
- },
- },
- ],
- responseCode: 200,
- },
- // Mock regions endpoint
- {
- urlEndpoint: 'https://on-ramp.dev-api.cx.metamask.io/regions',
- response: [
- {
- id: '/regions/fr',
- name: 'France',
- emoji: '🇫🇷',
- currencies: ['/currencies/fiat/eur'],
- support: { buy: true, sell: true, recurringBuy: true },
- unsupported: false,
- recommended: false,
- detected: false,
- },
- ],
- responseCode: 200,
- },
- ],
- POST: [
- // Mock analytics tracking
- {
- urlEndpoint: 'https://api.segment.io/v1/track',
- response: { success: true },
- responseCode: 200,
- },
- // Mock any other POST endpoints that might be called
- {
- urlEndpoint: 'https://on-ramp.dev-api.cx.metamask.io/orders',
- response: {
- id: 'mock-order-id',
- status: 'PENDING',
- provider: '/providers/moonpay-staging',
- },
- responseCode: 200,
- },
- ],
-});
diff --git a/e2e/api-mocking/mock-responses/ramps/ramps-geolocation.ts b/e2e/api-mocking/mock-responses/ramps/ramps-geolocation.ts
new file mode 100644
index 00000000000..637379f4e81
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/ramps/ramps-geolocation.ts
@@ -0,0 +1,29 @@
+import { MockApiEndpoint, RampsRegion } from '../../../framework';
+
+/**
+ * Creates a region-specific geolocation response based on the selected region
+ */
+export const createGeolocationResponse = (
+ region: RampsRegion,
+): MockApiEndpoint[] => [
+ {
+ urlEndpoint: /^https:\/\/on-ramp\.api\.cx\.metamask\.io\/geolocation$/,
+ responseCode: 200,
+ response: {
+ id: region.id,
+ name: region.name,
+ emoji: region.emoji,
+ detected: true,
+ },
+ },
+ {
+ urlEndpoint: /^https:\/\/on-ramp\.uat-api\.cx\.metamask\.io\/geolocation$/,
+ responseCode: 200,
+ response: {
+ id: region.id,
+ name: region.name,
+ emoji: region.emoji,
+ detected: true,
+ },
+ },
+];
diff --git a/e2e/api-mocking/mock-responses/ramps/ramps-mocks.ts b/e2e/api-mocking/mock-responses/ramps/ramps-mocks.ts
new file mode 100644
index 00000000000..9696008b3d4
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/ramps/ramps-mocks.ts
@@ -0,0 +1,282 @@
+/**
+ * Comprehensive mock responses for all on-ramp API endpoints
+ * Covers both UAT and production environments
+ */
+
+// Geolocation response (France - create other mocks for other regions)
+export const RAMPS_GEOLOCATION_RESPONSE = {
+ id: '/regions/fr',
+ name: 'France',
+ emoji: '🇫🇷',
+ detected: true,
+};
+
+// Networks response
+export const RAMPS_NETWORKS_RESPONSE = {
+ networks: [
+ {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ nativeTokenSupported: true,
+ },
+ {
+ active: true,
+ chainId: '59144',
+ chainName: 'Linea',
+ shortName: 'Linea',
+ isEvm: true,
+ nativeTokenSupported: true,
+ },
+ {
+ active: true,
+ chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
+ chainName: 'Solana',
+ shortName: 'Solana',
+ nativeTokenSupported: true,
+ isEvm: false,
+ },
+ ],
+};
+
+// Countries response
+export const RAMPS_COUNTRIES_RESPONSE = [
+ {
+ currencies: ['/currencies/fiat/eur'],
+ emoji: '🇵🇹',
+ id: '/regions/pt',
+ name: 'Portugal',
+ support: {
+ buy: true,
+ sell: true,
+ },
+ unsupported: false,
+ detected: false,
+ },
+ {
+ currencies: ['/currencies/fiat/eur'],
+ emoji: '🇫🇷',
+ id: '/regions/fr',
+ name: 'France',
+ support: {
+ buy: true,
+ sell: true,
+ },
+ unsupported: false,
+ detected: false,
+ },
+ {
+ currencies: ['/currencies/fiat/usd'],
+ emoji: '🇺🇸',
+ id: '/regions/us',
+ name: 'United States of America',
+ states: [
+ {
+ emoji: '🇺🇸',
+ id: '/regions/us-ca',
+ name: 'California',
+ stateId: 'ca',
+ support: {
+ buy: true,
+ sell: true,
+ },
+ unsupported: false,
+ detected: false,
+ },
+ ],
+ enableSell: true,
+ support: {
+ buy: false,
+ sell: false,
+ },
+ unsupported: true,
+ detected: true,
+ },
+ {
+ currencies: ['/currencies/fiat/eur'],
+ emoji: '🇪🇸',
+ id: '/regions/es',
+ name: 'Spain',
+ support: {
+ buy: true,
+ sell: true,
+ },
+ unsupported: false,
+ detected: false,
+ },
+];
+
+// Light endpoint response (for all variations)
+export const RAMPS_LIGHT_RESPONSE = {
+ parameters: {
+ appleMerchantId: '',
+ disableLimits: false,
+ },
+ payments: [
+ {
+ id: '/payments/apple-pay',
+ paymentType: 'apple-pay',
+ name: 'Apple Pay',
+ score: 285,
+ icons: [
+ {
+ type: 'fontAwesome',
+ name: 'apple',
+ },
+ ],
+ logo: {
+ light: [
+ 'assets/Visa-regular@3x.png',
+ 'assets/Mastercard-regular@3x.png',
+ ],
+ dark: ['assets/Visa@3x.png', 'assets/Mastercard@3x.png'],
+ },
+ disclaimer: 'Apple Cash is not supported.',
+ delay: [0, 0],
+ pendingOrderDescription:
+ 'Card purchases may take a few minutes to complete.',
+ amountTier: [1, 3],
+ isApplePay: true,
+ },
+ {
+ id: '/payments/debit-credit-card',
+ paymentType: 'debit-credit-card',
+ name: 'Debit or Credit',
+ score: 268,
+ icons: [
+ {
+ type: 'materialIcons',
+ name: 'card',
+ },
+ ],
+ logo: {
+ light: [
+ 'assets/Visa-regular@3x.png',
+ 'assets/Mastercard-regular@3x.png',
+ ],
+ dark: ['assets/Visa@3x.png', 'assets/Mastercard@3x.png'],
+ },
+ disclaimer:
+ "Credit card purchases may incur your bank's cash advance fees, subject to your bank's policies.",
+ delay: [5, 10],
+ pendingOrderDescription:
+ 'Card purchases may take a few minutes to complete.',
+ amountTier: [1, 3],
+ sellEnabled: true,
+ },
+ {
+ id: '/payments/sepa-bank-transfer',
+ paymentType: 'bank-transfer',
+ name: 'SEPA Bank Transfer',
+ score: 250,
+ icons: [
+ {
+ type: 'materialCommunityIcons',
+ name: 'bank',
+ },
+ ],
+ logo: {
+ light: ['assets/SEPABankTransfer-regular@3x.png'],
+ dark: ['assets/SEPABankTransfer@3x.png'],
+ },
+ delay: [1440, 2880],
+ pendingOrderDescription:
+ 'Bank transfers may take 1-2 business days to complete.',
+ amountTier: [2, 3],
+ sellEnabled: true,
+ supportedCurrency: ['/currencies/fiat/eur'],
+ supportedRegions: ['/regions/fr', '/regions/pt', '/regions/es'],
+ },
+ ],
+ cryptoCurrencies: [
+ {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ {
+ id: '/currencies/crypto/1/0x6b175474e89094c44da98b954eedeac495271d0f',
+ idv2: '/currencies/crypto/1/0x6b175474e89094c44da98b954eedeac495271d0f',
+ legacyId: '/currencies/crypto/1/dai',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v2/tokenIcons/assets/eip155/1/erc20/0x6b175474e89094c44da98b954eedeac495271d0f.png',
+ decimals: 18,
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ symbol: 'DAI',
+ name: 'Dai Stablecoin',
+ },
+ ],
+ fiatCurrencies: [
+ {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ {
+ id: '/currencies/fiat/usd',
+ symbol: 'USD',
+ name: 'US Dollar',
+ decimals: 2,
+ denomSymbol: '$',
+ },
+ ],
+ limits: {
+ minAmount: 2,
+ maxAmount: 50000,
+ feeDynamicRate: 0,
+ feeFixedRate: 0,
+ },
+};
+
+export const RAMPS_AMOUNT_RESPONSE = {
+ cryptoAmount: '1.0',
+ fiatAmount: '3924.50',
+};
+
+// Gas fees response for offramp transactions
+export const GAS_FEES_RESPONSE = {
+ low: {
+ suggestedMaxPriorityFeePerGas: '1.5',
+ suggestedMaxFeePerGas: '25',
+ minWaitTimeEstimate: 60000,
+ maxWaitTimeEstimate: 180000,
+ },
+ medium: {
+ suggestedMaxPriorityFeePerGas: '2',
+ suggestedMaxFeePerGas: '30',
+ minWaitTimeEstimate: 15000,
+ maxWaitTimeEstimate: 60000,
+ },
+ high: {
+ suggestedMaxPriorityFeePerGas: '3',
+ suggestedMaxFeePerGas: '40',
+ minWaitTimeEstimate: 5000,
+ maxWaitTimeEstimate: 15000,
+ },
+ estimatedBaseFee: '22',
+ networkCongestion: 0.3,
+ latestPriorityFeeRange: ['1', '3'],
+ historicalPriorityFeeRange: ['1', '5'],
+ historicalBaseFeeRange: ['20', '30'],
+ priorityFeeTrend: 'down',
+ baseFeeTrend: 'stable',
+};
diff --git a/e2e/api-mocking/mock-responses/ramps/ramps-quotes-response.ts b/e2e/api-mocking/mock-responses/ramps/ramps-quotes-response.ts
new file mode 100644
index 00000000000..12bce5d8fea
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/ramps/ramps-quotes-response.ts
@@ -0,0 +1,2238 @@
+/* eslint-disable @metamask/design-tokens/color-no-hex */
+export const RAMPS_QUOTE_RESPONSE = {
+ success: [
+ {
+ provider: '/providers/mercuryo',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.024581104642021276,
+ exchangeRate: 4014.8724574114517,
+ networkFee: 0.03,
+ providerFee: 1.28,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH_1',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/mercuryo',
+ name: 'Mercuryo',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Mercuryo: “Mercuryo offers easy onboarding for MetaMask users, with a speedy purchase process of under 15 seconds Light KYC up to 700$. With support for 20+ tokens, customers can pay using preferred methods, such as Apple Pay and bank cards.”',
+ hqAddress: 'London, United Kingdom, 77 Gracechurch, EC3V0AG',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://mercuryo.io/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://mercuryo.io/legal/privacy/',
+ },
+ {
+ name: 'Support',
+ url: 'https://help.mercuryo.io/en/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/mercuryo_light.png',
+ dark: '/assets/providers/mercuryo_dark.png',
+ height: 24,
+ width: 88,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: true,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH_1',
+ },
+ metadata: {
+ reliability: 0.807440599,
+ tags: {
+ isMostReliable: true,
+ },
+ },
+ },
+ {
+ provider: '/providers/mercuryo-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.0231989044130259,
+ exchangeRate: 4008.8100000007603,
+ networkFee: 0.01,
+ providerFee: 6.99,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH_1',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/mercuryo-staging',
+ name: 'Mercuryo (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Mercuryo: “Mercuryo offers easy onboarding for MetaMask users, with a speedy purchase process of under 15 seconds Light KYC up to 700$. With support for 20+ tokens, customers can pay using preferred methods, such as Apple Pay and bank cards.”',
+ hqAddress: 'London, United Kingdom, 77 Gracechurch, EC3V0AG',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://mercuryo.io/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://mercuryo.io/legal/privacy/',
+ },
+ {
+ name: 'Support',
+ url: 'https://help.mercuryo.io/en/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/mercuryo_light.png',
+ dark: '/assets/providers/mercuryo_dark.png',
+ height: 24,
+ width: 88,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: true,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH_1',
+ },
+ metadata: {
+ reliability: 0.807440599,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/moonpay-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.0248,
+ exchangeRate: 3851.209677419355,
+ networkFee: 0.9,
+ providerFee: 3.59,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'eth',
+ },
+ nativeApplePay: {
+ supported: false,
+ merchantId: '',
+ },
+ providerInfo: {
+ id: '/providers/moonpay-staging',
+ name: 'Moonpay (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per MoonPay: “MoonPay provides a smooth experience for converting between fiat currencies and cryptocurrencies. Easily top-up with ETH, BNB and more directly in your MetaMask wallet via MoonPay using all major payment methods including debit and credit card, local bank transfers, Apple Pay, Google Pay, and Samsung Pay. MoonPay is active in more than 160 countries and is trusted by 250+ leading wallets, websites, and applications.”',
+ hqAddress: '8 The Green, Dover, DE, 19901, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.moonpay.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://www.moonpay.com/legal/privacy_policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.moonpay.com/hc/en-gb/categories/360001595097-Customer-Support-Help-Center',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/moonpay_light.png',
+ dark: '/assets/providers/moonpay_dark.png',
+ height: 24,
+ width: 88,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: false,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'eth',
+ },
+ metadata: {
+ reliability: 0.6823529412,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/moonpay',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.0248,
+ exchangeRate: 3851.209677419355,
+ networkFee: 0.9,
+ providerFee: 3.59,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'eth',
+ },
+ nativeApplePay: {
+ supported: false,
+ merchantId: '',
+ },
+ providerInfo: {
+ id: '/providers/moonpay',
+ name: 'Moonpay',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per MoonPay: “MoonPay provides a smooth experience for converting between fiat currencies and cryptocurrencies. Easily top-up with ETH, BNB and more directly in your MetaMask wallet via MoonPay using all major payment methods including debit and credit card, local bank transfers, Apple Pay, Google Pay, and Samsung Pay. MoonPay is active in more than 160 countries and is trusted by 250+ leading wallets, websites, and applications.”',
+ hqAddress: '8 The Green, Dover, DE, 19901, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.moonpay.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://www.moonpay.com/legal/privacy_policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.moonpay.com/hc/en-gb/categories/360001595097-Customer-Support-Help-Center',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/moonpay_light.png',
+ dark: '/assets/providers/moonpay_dark.png',
+ height: 24,
+ width: 88,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: false,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'eth',
+ },
+ metadata: {
+ reliability: 0.6823529412,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/transak',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02381807,
+ exchangeRate: 4072.1183538380733,
+ networkFee: 0.02,
+ providerFee: 2.99,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETHethereum',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/transak',
+ name: 'Transak',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Transak: "The fastest and securest way to buy 100+ cryptocurrencies on 75+ blockchains. Pay via Apple Pay, UPI, bank transfer or use your debit or credit card. Trusted by 2+ million global users. Transak empowers wallets, gaming, DeFi, NFTs, Exchanges, and DAOs across 125+ countries."',
+ hqAddress:
+ '35 Shearing Street, Bury St. Edmunds, IP32 6FE, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.transak.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://transak.com/privacy-policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.transak.com/hc/en-us',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/transak_light.png',
+ dark: '/assets/providers/transak_dark.png',
+ height: 24,
+ width: 90,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: null,
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETHethereum',
+ },
+ metadata: {
+ reliability: 0.6578456951,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/transak-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02369152,
+ exchangeRate: 4072.34318439678,
+ networkFee: 0.02,
+ providerFee: 3.5,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETHethereum',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/transak-staging',
+ name: 'Transak (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Transak: "The fastest and securest way to buy 100+ cryptocurrencies on 75+ blockchains. Pay via Apple Pay, UPI, bank transfer or use your debit or credit card. Trusted by 2+ million global users. Transak empowers wallets, gaming, DeFi, NFTs, Exchanges, and DAOs across 125+ countries."',
+ hqAddress:
+ '35 Shearing Street, Bury St. Edmunds, IP32 6FE, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.transak.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://transak.com/privacy-policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.transak.com/hc/en-us',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/transak_light.png',
+ dark: '/assets/providers/transak_dark.png',
+ height: 24,
+ width: 90,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: null,
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETHethereum',
+ },
+ metadata: {
+ reliability: 0.6578456951,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/banxa-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.035741,
+ exchangeRate: 2797.9071654402505,
+ networkFee: 0,
+ providerFee: 0,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'eth-eth',
+ },
+ nativeApplePay: {
+ supported: false,
+ merchantId: 'merchant.io.metamask.banxa.test',
+ },
+ providerInfo: {
+ id: '/providers/banxa-staging',
+ name: 'Banxa (Staging)',
+ environmentType: 'STAGING',
+ description:
+ "Per Banxa: “Established from 2014, Banxa is the world's first publicly listed Financial technology platform, powering a world-leading fiat to crypto gateway solution for customers to buy, sell and trade digital assets. Banxa's payment infrastructure offers online payment services across multiple currencies, crypto, and payment types from card to local bank transfers. Banxa now supports over 130+ countries and more than 80 currencies.”",
+ hqAddress: '2/6 Gwynne St, Cremorne VIC 3121, Australia',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://banxa.com/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://banxa.com/wp-content/uploads/2022/10/Customer-Terms-and-Conditions-1-July-2022.pdf',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.banxa.com/en/support/tickets/new',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/banxa_light.png',
+ dark: '/assets/providers/banxa_dark.png',
+ height: 24,
+ width: 65,
+ },
+ features: {
+ buy: {
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'eth-eth',
+ },
+ metadata: {
+ reliability: 0.6118746564,
+ tags: {
+ isBestRate: true,
+ },
+ },
+ },
+ {
+ provider: '/providers/banxa',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.024672,
+ exchangeRate: 3972.924773022049,
+ networkFee: 0.03,
+ providerFee: 1.95,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'eth-eth',
+ },
+ nativeApplePay: {
+ supported: false,
+ merchantId: 'merchant.io.metamask.banxa',
+ },
+ providerInfo: {
+ id: '/providers/banxa',
+ name: 'Banxa',
+ environmentType: 'PRODUCTION',
+ description:
+ "Per Banxa: “Established from 2014, Banxa is the world's first publicly listed Financial technology platform, powering a world-leading fiat to crypto gateway solution for customers to buy, sell and trade digital assets. Banxa's payment infrastructure offers online payment services across multiple currencies, crypto, and payment types from card to local bank transfers. Banxa now supports over 130+ countries and more than 80 currencies.”",
+ hqAddress: '2/6 Gwynne St, Cremorne VIC 3121, Australia',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://banxa.com/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://banxa.com/wp-content/uploads/2022/10/Customer-Terms-and-Conditions-1-July-2022.pdf',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.banxa.com/en/support/tickets/new',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/banxa_light.png',
+ dark: '/assets/providers/banxa_dark.png',
+ height: 24,
+ width: 65,
+ },
+ features: {
+ buy: {
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'eth-eth',
+ },
+ metadata: {
+ reliability: 0.6118746564,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/revolut-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02427084,
+ exchangeRate: 4015.930227383972,
+ networkFee: 0.09,
+ providerFee: 2.44,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/revolut-staging',
+ name: 'Revolut (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Revolut: "Revolut is a global super-app used by over 30M+ people to manage all things money. Existing Revolut customers can complete a transaction in a few taps (no KYC required), while having the option to pay directly from their Revolut account with lower fees. If you don\'t have a Revolut account, enjoy a £20 cashback welcome offer when you set up a Revolut account after you make a successful Metamask transaction via Revolut."',
+ hqAddress: '7 Westferry Circus, London E14 4HD, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.revolut.com',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://www.revolut.com/legal/revramp',
+ },
+ {
+ name: 'Support',
+ url: 'https://ramp.revolut.com/en/support',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/revolut_light.png',
+ dark: '/assets/providers/revolut_dark.png',
+ height: 24,
+ width: 58,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: true,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: [],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH',
+ },
+ metadata: {
+ reliability: 0.5063418563,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/revolut',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02416794,
+ exchangeRate: 4035.5115082212224,
+ networkFee: 0.03,
+ providerFee: 2.44,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/revolut',
+ name: 'Revolut',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Revolut: "Revolut is a global super-app used by over 30M+ people to manage all things money. Existing Revolut customers can complete a transaction in a few taps (no KYC required), while having the option to pay directly from their Revolut account with lower fees. If you don\'t have a Revolut account, enjoy a £20 cashback welcome offer when you set up a Revolut account after you make a successful Metamask transaction via Revolut."',
+ hqAddress: '7 Westferry Circus, London E14 4HD, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.revolut.com',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://www.revolut.com/legal/revramp',
+ },
+ {
+ name: 'Support',
+ url: 'https://ramp.revolut.com/en/support',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/revolut_light.png',
+ dark: '/assets/providers/revolut_dark.png',
+ height: 24,
+ width: 58,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: true,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: [],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH',
+ },
+ metadata: {
+ reliability: 0.5063418563,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/ramp-network',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.023741401691137465,
+ exchangeRate: 4027.984583391222,
+ networkFee: 0.07,
+ providerFee: 4.3,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH_ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/ramp-network',
+ name: 'Ramp Network',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Ramp is building the infrastructure to seamlessly connect Web3 with today’s global financial system. Through its core on- and off-ramp products, Ramp provides businesses and individuals across 150+ countries with a streamlined and smooth experience in converting between crypto and fiat currencies. Ramp is fully integrated with the world’s major payment methods, including debit and credit cards, bank transfers, Apple Pay, Google Pay, and more.',
+ hqAddress: '8 The Green, STE B, Dover, County of Kent, DE 19901, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://ramp.network/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://ramp.network/terms-of-service',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.ramp.network/en/collections/6690-customer-support-help-center',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/ramp-logo-light.png',
+ dark: '/assets/providers/ramp-logo-dark.png',
+ height: 24,
+ width: 79,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: true,
+ orderCustomId: 'DIGITS_10',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: [],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH_ETH',
+ },
+ metadata: {
+ reliability: 0.5046052632,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/ramp-network-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.023486763146531114,
+ exchangeRate: 4028.2264273599344,
+ networkFee: 1.7,
+ providerFee: 3.69,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'SEPOLIA_ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/ramp-network-staging',
+ name: 'Ramp Network (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Ramp is building the infrastructure to seamlessly connect Web3 with today’s global financial system. Through its core on- and off-ramp products, Ramp provides businesses and individuals across 150+ countries with a streamlined and smooth experience in converting between crypto and fiat currencies. Ramp is fully integrated with the world’s major payment methods, including debit and credit cards, bank transfers, Apple Pay, Google Pay, and more.',
+ hqAddress: '8 The Green, STE B, Dover, County of Kent, DE 19901, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://ramp.network/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://ramp.network/terms-of-service',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.ramp.network/en/collections/6690-customer-support-help-center',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/ramp-logo-light.png',
+ dark: '/assets/providers/ramp-logo-dark.png',
+ height: 24,
+ width: 79,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: true,
+ orderCustomId: 'DIGITS_10',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: [],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'SEPOLIA_ETH',
+ },
+ metadata: {
+ reliability: 0.5046052632,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/sardine-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.025497793837794054,
+ exchangeRate: 3920.9044196526966,
+ networkFee: 0.02558745,
+ providerFee: 0,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH:ethereum',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/sardine-staging',
+ name: 'Sardine (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Sardine: “Sardine provides the safest, most convenient way to purchase cryptocurrencies and convert between fiat currencies. We offer lower transaction fees, higher transaction limits, instant account funding, and support for all major payment methods across 180+ countries."',
+ hqAddress: '82 NE 191st St, #58243 Miami, Florida 33179-3899 US',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.sardine.ai/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://crypto.sardine.ai/privacy',
+ },
+ {
+ name: 'Support',
+ url: 'https://crypto.sardine.ai/support',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/sardine_light.png',
+ dark: '/assets/providers/sardine_dark.png',
+ height: 24,
+ width: 79,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: false,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH:ethereum',
+ },
+ metadata: {
+ reliability: 0,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/sardine',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.024538150261037586,
+ exchangeRate: 4074.2440439262605,
+ networkFee: 0.02558745,
+ providerFee: 0,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH:ethereum',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/sardine',
+ name: 'Sardine',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Sardine: “Sardine provides the safest, most convenient way to purchase cryptocurrencies and convert between fiat currencies. We offer lower transaction fees, higher transaction limits, instant account funding, and support for all major payment methods across 180+ countries."',
+ hqAddress: '82 NE 191st St, #58243 Miami, Florida 33179-3899 US',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.sardine.ai/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://crypto.sardine.ai/privacy',
+ },
+ {
+ name: 'Support',
+ url: 'https://crypto.sardine.ai/support',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/sardine_light.png',
+ dark: '/assets/providers/sardine_dark.png',
+ height: 24,
+ width: 79,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {
+ enabled: false,
+ },
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH:ethereum',
+ },
+ metadata: {
+ reliability: 0,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/binanceconnect-staging',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02372126,
+ exchangeRate: 4079.0668576714306,
+ networkFee: 1.239394511793,
+ providerFee: 2,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/binanceconnect-staging',
+ name: 'Binance Connect (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Binance Connect offers an easy way to buy and sell crypto through your DeFi wallet. You can conveniently purchase crypto using various payment methods, including credit cards, bank transfers, Binance wallet balance, and alternative methods through Binance Peer-to-Peer (P2P) services.',
+ hqAddress: 'House of Francis, Room 303, IIe Du Port, Mahe, Seychelles',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.binance.com/en/binance-connect/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://www.binance.com/en/terms/',
+ },
+ {
+ name: 'Support',
+ url: 'https://www.binance.com/en/support/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/binance-connect-light.png',
+ dark: '/assets/providers/binance-connect-dark.png',
+ height: 24,
+ width: 92,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'IN_APP_OS_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {},
+ sellQuotes: {},
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH',
+ },
+ metadata: {
+ reliability: 0,
+ tags: {},
+ },
+ },
+ {
+ provider: '/providers/binanceconnect',
+ url: null,
+ method: null,
+ headers: null,
+ quote: {
+ crypto: {
+ id: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ idv2: '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ legacyId: '/currencies/crypto/1/eth',
+ network: {
+ active: true,
+ chainId: '1',
+ chainName: 'Ethereum Mainnet',
+ shortName: 'Ethereum',
+ },
+ logo: 'https://uat-static.cx.metamask.io/api/v1/tokenIcons/1/0x0000000000000000000000000000000000000000.png',
+ decimals: 18,
+ address: '0x0000000000000000000000000000000000000000',
+ symbol: 'ETH',
+ name: 'Ethereum',
+ },
+ cryptoId:
+ '/currencies/crypto/1/0x0000000000000000000000000000000000000000',
+ fiat: {
+ id: '/currencies/fiat/eur',
+ symbol: 'EUR',
+ name: 'Euro',
+ decimals: 2,
+ denomSymbol: '€',
+ },
+ fiatId: '/currencies/fiat/eur',
+ amountIn: 100,
+ amountOut: 0.02372126,
+ exchangeRate: 4079.0668576714306,
+ networkFee: 1.239394511793,
+ providerFee: 2,
+ extraFee: 0,
+ receiver: '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ paymentMethod: '/payments/apple-pay',
+ cryptoTranslation: 'ETH',
+ },
+ nativeApplePay: {},
+ providerInfo: {
+ id: '/providers/binanceconnect',
+ name: 'Binance Connect',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Binance Connect offers an easy way to buy and sell crypto through your DeFi wallet. You can conveniently purchase crypto using various payment methods, including credit cards, bank transfers, Binance wallet balance, and alternative methods through Binance Peer-to-Peer (P2P) services.',
+ hqAddress: 'House of Francis, Room 303, IIe Du Port, Mahe, Seychelles',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.binance.com/en/binance-connect/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://www.binance.com/en/terms/',
+ },
+ {
+ name: 'Support',
+ url: 'https://www.binance.com/en/support/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/binance-connect-light.png',
+ dark: '/assets/providers/binance-connect-dark.png',
+ height: 24,
+ width: 92,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'IN_APP_OS_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {},
+ sellQuotes: {},
+ recurringBuy: {},
+ },
+ },
+ providerQuote: {
+ paymentMethod: ['apple-pay'],
+ fiatCurrency: 'EUR',
+ cryptoCurrency: 'ETH',
+ },
+ metadata: {
+ reliability: 0,
+ tags: {},
+ },
+ },
+ ],
+ sorted: [
+ {
+ sortBy: '2',
+ ids: [
+ '/providers/banxa-staging',
+ '/providers/sardine-staging',
+ '/providers/moonpay-staging',
+ '/providers/moonpay',
+ '/providers/banxa',
+ '/providers/mercuryo',
+ '/providers/sardine',
+ '/providers/revolut-staging',
+ '/providers/revolut',
+ '/providers/transak',
+ '/providers/ramp-network',
+ '/providers/binanceconnect-staging',
+ '/providers/binanceconnect',
+ '/providers/transak-staging',
+ '/providers/ramp-network-staging',
+ '/providers/mercuryo-staging',
+ ],
+ },
+ {
+ sortBy: '1',
+ ids: [
+ '/providers/mercuryo',
+ '/providers/mercuryo-staging',
+ '/providers/moonpay-staging',
+ '/providers/moonpay',
+ '/providers/transak',
+ '/providers/transak-staging',
+ '/providers/banxa-staging',
+ '/providers/banxa',
+ '/providers/revolut-staging',
+ '/providers/revolut',
+ '/providers/ramp-network',
+ '/providers/ramp-network-staging',
+ '/providers/sardine-staging',
+ '/providers/sardine',
+ '/providers/binanceconnect-staging',
+ '/providers/binanceconnect',
+ ],
+ },
+ ],
+ error: [
+ {
+ providerInfo: {
+ id: '/providers/coinbase-staging',
+ name: 'Coinbase (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Coinbase: "Coinbase Pay enables purchases and transfers directly from your Coinbase account to your Metamask wallet. It integrates with Dapps and self-custody wallets to use your saved payment methods or crypto balances."',
+ hqAddress: '248 3rd St #434, Oakland CA, 94607',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.coinbase.com/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://www.coinbase.com/legal/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/coinbase-light.png',
+ dark: '/assets/providers/coinbase-dark.png',
+ height: 24,
+ width: 110,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: 'GUID',
+ browser: 'IN_APP_OS_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: '-',
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: false,
+ },
+ sellQuotes: {
+ enabled: false,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/coinbase-staging',
+ },
+ {
+ providerInfo: {
+ id: '/providers/unlimitmeld-staging',
+ name: 'Unlimit (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Unlimit: "Unlimit’s fiat onramp allows users to use a variety of local payment methods to purchase crypto with the lowest fees and highest convenience."',
+ hqAddress: 'Vilnius, Eišiškių Sodų 18-oji g. 11',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.crypto.unlimit.com',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://cdn.gatefi.com/regform/gatefi-user-terms.pdf',
+ },
+ {
+ name: 'Support',
+ url: 'https://www.crypto.unlimit.com/support/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/unlimit-light.png',
+ dark: '/assets/providers/unlimit-dark.png',
+ height: 24,
+ width: 58,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/unlimitmeld-staging',
+ },
+ {
+ providerInfo: {
+ id: '/providers/unlimitmeld',
+ name: 'Unlimit',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Unlimit: "Unlimit’s fiat onramp allows users to use a variety of local payment methods to purchase crypto with the lowest fees and highest convenience."',
+ hqAddress: 'Vilnius, Eišiškių Sodų 18-oji g. 11',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.crypto.unlimit.com',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://cdn.gatefi.com/regform/gatefi-user-terms.pdf',
+ },
+ {
+ name: 'Support',
+ url: 'https://www.crypto.unlimit.com/support/',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/unlimit-light.png',
+ dark: '/assets/providers/unlimit-dark.png',
+ height: 24,
+ width: 58,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/unlimitmeld',
+ },
+ {
+ providerInfo: {
+ id: '/providers/stripe-staging',
+ name: 'Stripe (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Stripe: "Millions of companies—from the world’s largest enterprises to the most ambitious startups—use Stripe to accept payments and grow their revenue. The Stripe crypto onramp gives web3 developers an easy and fast way for their consumers to purchase crypto with a credit card, debit card, or ACH. Top web3 companies trust Stripe\'s crypto onramp to give users a seamless experience that is optimized for conversion and instant settlement of crypto."',
+ hqAddress: '354 Oyster Point Blvd, South San Francisco, CA 94080, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://crypto.link.com',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.link.com/topics/crypto',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://link.com/terms/crypto-onramp',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://link.com/privacy',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/stripe_light.png',
+ dark: '/assets/providers/stripe_dark.png',
+ height: 24,
+ width: 100,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: '-',
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {},
+ sellQuotes: {},
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/stripe-staging',
+ },
+ {
+ providerInfo: {
+ id: '/providers/stripe',
+ name: 'Stripe',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Stripe: "Millions of companies—from the world’s largest enterprises to the most ambitious startups—use Stripe to accept payments and grow their revenue. The Stripe crypto onramp gives web3 developers an easy and fast way for their consumers to purchase crypto with a credit card, debit card, or ACH. Top web3 companies trust Stripe\'s crypto onramp to give users a seamless experience that is optimized for conversion and instant settlement of crypto."',
+ hqAddress: '354 Oyster Point Blvd, South San Francisco, CA 94080, USA',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://crypto.link.com',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.link.com/topics/crypto',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://link.com/terms/crypto-onramp',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://link.com/privacy',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/stripe_light.png',
+ dark: '/assets/providers/stripe_dark.png',
+ height: 24,
+ width: 100,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: 'GUID',
+ browser: 'APP_BROWSER',
+ orderCustomIdRequired: true,
+ orderCustomIdExpiration: 60,
+ orderCustomIdSeparator: '-',
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {},
+ sellQuotes: {},
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/stripe',
+ },
+ {
+ providerInfo: {
+ id: '/providers/transak-native-staging',
+ name: 'Transak Native (Staging)',
+ environmentType: 'STAGING',
+ description:
+ 'Per Transak: "The fastest and securest way to buy 100+ cryptocurrencies on 75+ blockchains. Pay via Apple Pay, UPI, bank transfer or use your debit or credit card. Trusted by 2+ million global users. Transak empowers wallets, gaming, DeFi, NFTs, Exchanges, and DAOs across 125+ countries."',
+ hqAddress:
+ '35 Shearing Street, Bury St. Edmunds, IP32 6FE, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.transak.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://transak.com/privacy-policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.transak.com/hc/en-us',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/transak_light.png',
+ dark: '/assets/providers/transak_dark.png',
+ height: 24,
+ width: 90,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: null,
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/transak-native-staging',
+ },
+ {
+ providerInfo: {
+ id: '/providers/transak-native',
+ name: 'Transak Native',
+ environmentType: 'PRODUCTION',
+ description:
+ 'Per Transak: "The fastest and securest way to buy 100+ cryptocurrencies on 75+ blockchains. Pay via Apple Pay, UPI, bank transfer or use your debit or credit card. Trusted by 2+ million global users. Transak empowers wallets, gaming, DeFi, NFTs, Exchanges, and DAOs across 125+ countries."',
+ hqAddress:
+ '35 Shearing Street, Bury St. Edmunds, IP32 6FE, United Kingdom',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://www.transak.com/',
+ },
+ {
+ name: 'Privacy Policy',
+ url: 'https://transak.com/privacy-policy',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.transak.com/hc/en-us',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/transak_light.png',
+ dark: '/assets/providers/transak_dark.png',
+ height: 24,
+ width: 90,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: null,
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: true,
+ },
+ sellQuotes: {
+ enabled: true,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/transak-native',
+ },
+ {
+ providerInfo: {
+ id: '/providers/mockprovider-staging',
+ name: 'MockProvider (Staging)',
+ environmentType: 'STAGING',
+ description:
+ "Per Banxa: “Established from 2014, Banxa is the world's first publicly listed Financial technology platform, powering a world-leading fiat to crypto gateway solution for customers to buy, sell and trade digital assets. Banxa's payment infrastructure offers online payment services across multiple currencies, crypto, and payment types from card to local bank transfers. Banxa now supports over 130+ countries and more than 80 currencies.”",
+ hqAddress: '2/6 Gwynne St, Cremorne VIC 3121, Australia',
+ links: [
+ {
+ name: 'Homepage',
+ url: 'https://banxa.com/',
+ },
+ {
+ name: 'Terms of Service',
+ url: 'https://banxa.com/wp-content/uploads/2022/10/Customer-Terms-and-Conditions-1-July-2022.pdf',
+ },
+ {
+ name: 'Support',
+ url: 'https://support.banxa.com/en/support/tickets/new',
+ },
+ ],
+ logos: {
+ light: '/assets/providers/banxa_light.png',
+ dark: '/assets/providers/banxa_dark.png',
+ height: 24,
+ width: 65,
+ },
+ features: {
+ buy: {
+ enabled: true,
+ userAgent: null,
+ padCustomOrderId: false,
+ orderCustomId: '',
+ browser: null,
+ orderCustomIdRequired: false,
+ orderCustomIdExpiration: null,
+ orderCustomIdSeparator: null,
+ orderCustomIdPrefixes: ['c-', ''],
+ supportedByBackend: true,
+ redirection: 'JSON_REDIRECTION',
+ },
+ quotes: {
+ enabled: false,
+ supportedByBackend: false,
+ },
+ sell: {
+ enabled: false,
+ },
+ sellQuotes: {
+ enabled: false,
+ },
+ recurringBuy: {},
+ },
+ },
+ provider: '/providers/mockprovider-staging',
+ },
+ ],
+ customActions: [],
+};
diff --git a/e2e/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts b/e2e/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts
new file mode 100644
index 00000000000..a18edc43944
--- /dev/null
+++ b/e2e/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts
@@ -0,0 +1,120 @@
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../mockHelpers';
+import { MockApiEndpoint, RampsRegion } from '../../../framework/types';
+import {
+ RAMPS_NETWORKS_RESPONSE,
+ RAMPS_COUNTRIES_RESPONSE,
+ RAMPS_LIGHT_RESPONSE,
+ RAMPS_AMOUNT_RESPONSE,
+ GAS_FEES_RESPONSE,
+} from './ramps-mocks';
+import { createGeolocationResponse } from './ramps-geolocation';
+import { RAMPS_QUOTE_RESPONSE } from './ramps-quotes-response';
+
+/**
+ * Sets up comprehensive on-ramp API mocks that are aware of the selected region
+ * This ensures the geolocation response matches the region set in the test fixture
+ *
+ * @param mockServer - The mock server instance
+ * @param selectedRegion - The region selected in the test fixture
+ */
+export const setupRegionAwareOnRampMocks = async (
+ mockServer: Mockttp,
+ selectedRegion: RampsRegion,
+) => {
+ const geolocationResponse = createGeolocationResponse(selectedRegion);
+
+ const mockEndpoints: MockApiEndpoint[] = [
+ // 1. Geolocation endpoints (both UAT and prod) - region-specific
+ ...geolocationResponse,
+
+ // 2. Networks endpoints (both UAT and prod)
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/regions\/networks\?.*$/,
+ responseCode: 200,
+ response: RAMPS_NETWORKS_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/regions\/networks\?.*$/,
+ responseCode: 200,
+ response: RAMPS_NETWORKS_RESPONSE,
+ },
+
+ // 3. Countries endpoints (both UAT and prod) - Add more countries as needed - RAMPS_COUNTRIES_RESPONSE in on-ramp-mocks.ts
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/regions\/countries\?.*$/,
+ responseCode: 200,
+ response: RAMPS_COUNTRIES_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/regions\/countries\?.*$/,
+ responseCode: 200,
+ response: RAMPS_COUNTRIES_RESPONSE,
+ },
+
+ // 4. Light endpoints - all parameter variations (both UAT and prod)
+ // This controls things like: payment methods, available cryptocurrencies, fiat currencies and limits
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/regions\/[^/]+\/light\?.*$/,
+ responseCode: 200,
+ response: RAMPS_LIGHT_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/regions\/[^/]+\/light\?.*$/,
+ responseCode: 200,
+ response: RAMPS_LIGHT_RESPONSE,
+ },
+
+ // 5. Amount conversion endpoints (both UAT and prod)
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.api\.cx\.metamask\.io\/currencies\/crypto\/.*\/amount\?.*$/,
+ responseCode: 200,
+ response: RAMPS_AMOUNT_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp-cache\.uat-api\.cx\.metamask\.io\/currencies\/crypto\/.*\/amount\?.*$/,
+ responseCode: 200,
+ response: RAMPS_AMOUNT_RESPONSE,
+ },
+
+ // 6. Quote endpoints (both UAT and prod)
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp\.api\.cx\.metamask\.io\/providers\/all\/quote\?.*$/,
+ responseCode: 200,
+ response: RAMPS_QUOTE_RESPONSE,
+ },
+ {
+ urlEndpoint:
+ /^https:\/\/on-ramp\.uat-api\.cx\.metamask\.io\/providers\/all\/quote\?.*$/,
+ responseCode: 200,
+ response: RAMPS_QUOTE_RESPONSE,
+ },
+
+ // 7. Gas fees endpoint for offramp transactions (both UAT and prod)
+ {
+ urlEndpoint:
+ /^https:\/\/gas\.api\.cx\.metamask\.io\/networks\/\d+\/suggestedGasFees$/,
+ responseCode: 200,
+ response: GAS_FEES_RESPONSE,
+ },
+ ];
+
+ // Set up all mocks
+ for (const mock of mockEndpoints) {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: mock.urlEndpoint,
+ response: mock.response,
+ responseCode: mock.responseCode,
+ });
+ }
+};
diff --git a/e2e/api-mocking/mock-responses/simulations.js b/e2e/api-mocking/mock-responses/simulations.js
index 4a58e27b398..68020804ea1 100644
--- a/e2e/api-mocking/mock-responses/simulations.js
+++ b/e2e/api-mocking/mock-responses/simulations.js
@@ -21,12 +21,15 @@ export const SEND_ETH_SIMULATION_MOCK = {
{
transactions: [SEND_ETH_TRANSACTION_MOCK],
suggestFees: { withFeeTransfer: true, withTransfer: true },
- withCallTrace: true,
- withLogs: true,
},
],
},
- ignoreFields: ['params.0.blockOverrides', 'id'],
+ ignoreFields: [
+ 'params.0.blockOverrides',
+ 'id',
+ 'params.0.transactions',
+ 'params.0.suggestFees',
+ ],
urlEndpoint: LOCALHOST_SENTINEL_URL,
response: {
jsonrpc: '2.0',
diff --git a/e2e/api-mocking/mock-responses/token-api-responses.ts b/e2e/api-mocking/mock-responses/token-api-responses.ts
index 7c31a81fc52..313f5e0174c 100644
--- a/e2e/api-mocking/mock-responses/token-api-responses.ts
+++ b/e2e/api-mocking/mock-responses/token-api-responses.ts
@@ -290,4 +290,54 @@ export const TOKEN_API_TOKENS_RESPONSE = [
],
occurrences: 19,
},
+ {
+ address: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
+ symbol: 'SNX',
+ decimals: 18,
+ name: 'Synthetix Network Token',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f.png',
+ aggregators: [
+ 'uniswapLabs',
+ 'metamask',
+ 'aave',
+ 'cmc',
+ 'coinGecko',
+ 'coinMarketCap',
+ 'openSwap',
+ 'zerion',
+ 'oneInch',
+ 'liFi',
+ 'xSwap',
+ 'socket',
+ 'rubic',
+ 'squid',
+ 'rango',
+ 'sonarwatch',
+ 'sushiSwap',
+ 'pmm',
+ 'bancor',
+ ],
+ occurrences: 19,
+ },
+ {
+ address: '0x8704304af98a15ba0fb36e58fb69c7cb6b00e1d1',
+ symbol: 'CNG',
+ decimals: 18,
+ name: 'Change Token',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x8704304af98a15ba0fb36e58fb69c7cb6b00e1d1.png',
+ aggregators: ['metamask', 'coinGecko', 'openSwap'],
+ occurrences: 3,
+ },
+ {
+ address: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
+ symbol: 'SAI',
+ decimals: 18,
+ name: 'SAI',
+ iconUrl:
+ 'https://static.cx.metamask.io/api/v1/tokenIcons/1337/0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359.png',
+ aggregators: ['metamask', 'coinGecko'],
+ occurrences: 2,
+ },
];
diff --git a/e2e/api-mocking/mock-server.js b/e2e/api-mocking/mock-server.ts
similarity index 62%
rename from e2e/api-mocking/mock-server.js
rename to e2e/api-mocking/mock-server.ts
index 19d27690e8d..545d26573a7 100644
--- a/e2e/api-mocking/mock-server.js
+++ b/e2e/api-mocking/mock-server.ts
@@ -1,29 +1,48 @@
-/* eslint-disable no-console */
-/* eslint-disable import/no-nodejs-modules */
-import { getLocal } from 'mockttp';
-import portfinder from 'portfinder';
-import _ from 'lodash';
-import { device } from 'detox';
+import { getLocal, Headers, Mockttp } from 'mockttp';
import { ALLOWLISTED_HOSTS, ALLOWLISTED_URLS } from './mock-e2e-allowlist.js';
import { createLogger } from '../framework/logger';
+import { findMatchingPostEvent, processPostRequestBody } from './mockHelpers';
+import {
+ MockApiEndpoint,
+ MockEventsObject,
+ TestSpecificMock,
+} from '../framework/index';
const logger = createLogger({
name: 'MockServer',
});
+interface LiveRequest {
+ url: string;
+ method: string;
+ timestamp: string;
+}
+
+interface MockServer extends Mockttp {
+ _liveRequests?: LiveRequest[];
+}
+
/**
* Utility function to handle direct fetch requests
- * @param {string} url - The URL to fetch from
- * @param {string} method - The HTTP method
- * @param {Headers} headers - Request headers
- * @param {Object} requestBody - The request body object
- * @returns {Promise<{statusCode: number, body: string}>} Response object
*/
-const handleDirectFetch = async (url, method, headers, requestBody) => {
+const handleDirectFetch = async (
+ url: string,
+ method: string,
+ headers: Headers,
+ requestBody?: string,
+): Promise<{ statusCode: number; body: string }> => {
try {
+ // Convert mockttp headers to satisfy fetch API requirements
+ const fetchHeaders: HeadersInit = {};
+ for (const [key, value] of Object.entries(headers)) {
+ if (value) {
+ fetchHeaders[key] = Array.isArray(value) ? value[0] : value;
+ }
+ }
+
const response = await global.fetch(url, {
method,
- headers,
+ headers: fetchHeaders,
body: ['POST', 'PUT', 'PATCH'].includes(method) ? requestBody : undefined,
});
@@ -34,7 +53,7 @@ const handleDirectFetch = async (url, method, headers, requestBody) => {
body: responseBody,
};
} catch (error) {
- logger.error('Error forwarding request:', url);
+ logger.error('Error forwarding request:', url, error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Failed to forward request' }),
@@ -44,10 +63,8 @@ const handleDirectFetch = async (url, method, headers, requestBody) => {
/**
* Utility function to check if a URL is allowed
- * @param {string} url - The URL to check
- * @returns {boolean} True if the URL is allowed, false otherwise
*/
-const isUrlAllowed = (url) => {
+const isUrlAllowed = (url: string): boolean => {
try {
// First check if the exact URL is in the allowed URLs list
if (ALLOWLISTED_URLS.includes(url)) {
@@ -76,17 +93,16 @@ const isUrlAllowed = (url) => {
/**
* Starts the mock server and sets up mock events.
- *
- * @param {Object} events - The events to mock, organised by method.
- * @param {number} [port] - Optional port number. If not provided, a free port will be used.
- * @returns {Promise} Resolves to the running mock server.
*/
-export const startMockServer = async (events, port) => {
- const mockServer = getLocal();
- port = port || (await portfinder.getPortPromise());
+export const startMockServer = async (
+ events: MockEventsObject,
+ port: number,
+ testSpecificMock?: TestSpecificMock,
+): Promise => {
+ const mockServer = getLocal() as MockServer;
// Track live requests
- const liveRequests = [];
+ const liveRequests: LiveRequest[] = [];
mockServer._liveRequests = liveRequests;
try {
@@ -96,6 +112,7 @@ export const startMockServer = async (events, port) => {
logger.error(`Failed to start mock server on port ${port}: ${error}`);
throw new Error(`Failed to start mock server on port ${port}: ${error}`);
}
+
logger.info(`Mockttp server running at http://localhost:${port}`);
await mockServer
@@ -108,23 +125,37 @@ export const startMockServer = async (events, port) => {
)
.thenReply(200, 'favicon.ico');
- // Handle all /proxy requests
+ // Apply test-specific mocks first (takes precedence)
+ if (testSpecificMock) {
+ logger.info('Applying testSpecificMock function (takes precedence)');
+ await testSpecificMock(mockServer);
+ }
+
+ // Set up the main proxy handler (fallback logic)
await mockServer
.forAnyRequest()
.matching((request) => request.path.startsWith('/proxy'))
.thenCallback(async (request) => {
const urlEndpoint = new URL(request.url).searchParams.get('url');
+ if (!urlEndpoint) {
+ return {
+ statusCode: 400,
+ body: JSON.stringify({ error: 'Missing url parameter' }),
+ };
+ }
const method = request.method;
// Read the body ONCE for POST requests to avoid stream exhaustion
- let requestBodyText;
- let requestBodyJson;
+ let requestBodyText: string | undefined;
+ let requestBodyJson: unknown;
if (method === 'POST') {
try {
requestBodyText = await request.body.getText();
- try {
- requestBodyJson = JSON.parse(requestBodyText);
- } catch (e) {
- requestBodyJson = undefined;
+ if (requestBodyText) {
+ try {
+ requestBodyJson = JSON.parse(requestBodyText);
+ } catch (e) {
+ requestBodyJson = undefined;
+ }
}
} catch (e) {
requestBodyText = undefined;
@@ -133,38 +164,27 @@ export const startMockServer = async (events, port) => {
// Find matching mock event
const methodEvents = events[method] || [];
- const candidateEvents = methodEvents.filter((event) => {
+ const candidateEvents = methodEvents.filter((event: MockApiEndpoint) => {
const eventUrl = event.urlEndpoint;
- if (!eventUrl || !urlEndpoint) return false;
+ if (!eventUrl) return false;
if (event.urlEndpoint instanceof RegExp) {
return event.urlEndpoint.test(urlEndpoint);
}
// Support exact match and prefix (partial) match to avoid leaking keys in tests
-
- return urlEndpoint === eventUrl || urlEndpoint.startsWith(eventUrl);
+ const eventUrlStr = String(eventUrl);
+ return (
+ urlEndpoint === eventUrlStr || urlEndpoint.startsWith(eventUrlStr)
+ );
});
- let matchingEvent;
+ let matchingEvent: MockApiEndpoint | undefined;
if (candidateEvents.length > 0) {
if (method === 'POST') {
- // Prefer events whose requestBody matches (respecting ignoreFields)
- matchingEvent = candidateEvents.find((event) => {
- if (!event.requestBody || !requestBodyJson) return false;
- const requestToCheck = _.cloneDeep(requestBodyJson);
- const expectedRequest = _.cloneDeep(event.requestBody);
- const ignoreFields = event.ignoreFields || [];
- ignoreFields.forEach((field) => {
- _.unset(requestToCheck, field);
- _.unset(expectedRequest, field);
- });
- return _.isMatch(requestToCheck, expectedRequest);
- });
-
- // Fallback to an event without a requestBody matcher
- if (!matchingEvent) {
- matchingEvent = candidateEvents.find((event) => !event.requestBody);
- }
+ // Use the extracted logic for POST request matching
+ matchingEvent =
+ findMatchingPostEvent(candidateEvents, requestBodyJson) ||
+ undefined;
} else {
// Non-POST requests: first candidate by URL
matchingEvent = candidateEvents[0];
@@ -177,57 +197,19 @@ export const startMockServer = async (events, port) => {
logger.debug('Response:', matchingEvent.response);
// For POST requests, verify the request body if specified
if (method === 'POST' && matchingEvent.requestBody) {
- const parsedRequestBodyJson = requestBodyJson;
-
- // Ensure both objects exist before comparison
- if (!parsedRequestBodyJson || !matchingEvent.requestBody) {
- console.log('Request body validation failed: Missing request body');
- return {
- statusCode: 400,
- body: JSON.stringify({ error: 'Missing request body' }),
- };
- }
-
- // Clone objects to avoid mutations
- const requestToCheck = _.cloneDeep(parsedRequestBodyJson);
- const expectedRequest = _.cloneDeep(matchingEvent.requestBody);
-
- const ignoreFields = matchingEvent.ignoreFields || [];
-
- // Remove ignored fields from both objects for comparison
- ignoreFields.forEach((field) => {
- _.unset(requestToCheck, field);
- _.unset(expectedRequest, field);
- });
-
- const matches = _.isMatch(requestToCheck, expectedRequest);
-
- if (!matches) {
- logger.warn('Request body validation failed:');
- logger.info(
- 'Expected:',
- JSON.stringify(matchingEvent.requestBody, null, 2),
- );
- logger.info('Received:', JSON.stringify(requestBodyJson, null, 2));
- logger.info(
- 'Differences:',
- JSON.stringify(
- _.differenceWith(
- [parsedRequestBodyJson],
- [matchingEvent.requestBody],
- _.isEqual,
- ),
- null,
- 2,
- ),
- );
+ const result = processPostRequestBody(
+ requestBodyText,
+ matchingEvent.requestBody,
+ { ignoreFields: matchingEvent.ignoreFields || [] },
+ );
+ if (!result.matches) {
return {
- statusCode: 404,
+ statusCode: result.error === 'Missing request body' ? 400 : 404,
body: JSON.stringify({
- error: 'Request body validation failed',
+ error: result.error,
expected: matchingEvent.requestBody,
- received: parsedRequestBodyJson,
+ received: result.requestBodyJson,
}),
};
}
@@ -302,10 +284,8 @@ export const startMockServer = async (events, port) => {
/**
* Validates that no unexpected live requests were made
- * @param {import('mockttp').Mockttp} mockServer
- * @returns {void}
*/
-export const validateLiveRequests = (mockServer) => {
+export const validateLiveRequests = (mockServer: MockServer): void => {
if (mockServer._liveRequests && mockServer._liveRequests.length > 0) {
// Get unique requests by method + URL combination
const uniqueRequests = Array.from(
@@ -336,9 +316,8 @@ export const validateLiveRequests = (mockServer) => {
/**
* Stops the mock server.
- * @param {import('mockttp').Mockttp} mockServer
*/
-export const stopMockServer = async (mockServer) => {
+export const stopMockServer = async (mockServer: Mockttp): Promise => {
console.log('Mock server shutting down');
try {
await mockServer.stop();
diff --git a/e2e/api-mocking/mockHelpers.ts b/e2e/api-mocking/mockHelpers.ts
new file mode 100644
index 00000000000..dd22a6a7203
--- /dev/null
+++ b/e2e/api-mocking/mockHelpers.ts
@@ -0,0 +1,327 @@
+import { Mockttp } from 'mockttp';
+import _ from 'lodash';
+import { createLogger, MockApiEndpoint } from '../framework';
+import { getDecodedProxiedURL } from '../specs/notifications/utils/helpers';
+
+const logger = createLogger({
+ name: 'TestSpecificMockHelpers',
+});
+
+interface ResponseParam {
+ requestMethod: 'GET' | 'POST' | 'PUT' | 'DELETE';
+ url: string | RegExp;
+ response: unknown;
+ responseCode: number;
+}
+
+export interface PostRequestMatchingOptions {
+ ignoreFields?: string[];
+ allowPartialMatch?: boolean;
+}
+
+export interface PostRequestMatchResult {
+ matches: boolean;
+ error?: string;
+ requestBodyJson?: MockApiEndpoint['requestBody'];
+}
+
+/**
+ * Processes and validates POST request body against expected request body
+ * This is the unified logic extracted from mock-server.js
+ *
+ * @param requestBodyText - Raw request body text
+ * @param expectedRequestBody - Expected request body object to match against
+ * @param options - Options for matching behavior
+ * @returns Match result with validation status
+ */
+export const processPostRequestBody = (
+ requestBodyText: string | undefined,
+ expectedRequestBody: MockApiEndpoint['requestBody'],
+ options: PostRequestMatchingOptions = {},
+): PostRequestMatchResult => {
+ const { ignoreFields = [], allowPartialMatch = true } = options;
+
+ // Handle missing request body
+ if (!requestBodyText) {
+ return {
+ matches: false,
+ error: 'Missing request body',
+ };
+ }
+
+ let requestBodyJson: MockApiEndpoint['requestBody'] | undefined;
+ try {
+ requestBodyJson = JSON.parse(requestBodyText);
+ } catch (e) {
+ return {
+ matches: false,
+ error: 'Invalid request body JSON',
+ };
+ }
+
+ // If no expected body specified, consider it a match
+ if (!expectedRequestBody) {
+ return {
+ matches: true,
+ requestBodyJson,
+ };
+ }
+
+ // Clone objects to avoid mutations (using lodash for consistency with mock-server.js)
+ const requestToCheck = _.cloneDeep(requestBodyJson);
+ const expectedRequest = _.cloneDeep(expectedRequestBody);
+
+ // Remove ignored fields from both objects for comparison
+ ignoreFields.forEach((field) => {
+ _.unset(requestToCheck, field);
+ _.unset(expectedRequest, field);
+ });
+
+ const matches = allowPartialMatch
+ ? typeof requestToCheck === 'object' &&
+ requestToCheck !== null &&
+ typeof expectedRequest === 'object' &&
+ expectedRequest !== null
+ ? _.isMatch(requestToCheck, expectedRequest)
+ : false
+ : _.isEqual(requestToCheck, expectedRequest);
+
+ if (!matches) {
+ logger.warn('Request body validation failed:');
+ logger.info('Expected:', JSON.stringify(expectedRequestBody, null, 2));
+ logger.info('Received:', JSON.stringify(requestBodyJson, null, 2));
+ logger.info(
+ 'Differences:',
+ JSON.stringify(
+ _.differenceWith([requestBodyJson], [expectedRequestBody], _.isEqual),
+ null,
+ 2,
+ ),
+ );
+
+ return {
+ matches: false,
+ error: 'Request body validation failed',
+ requestBodyJson,
+ };
+ }
+
+ return {
+ matches: true,
+ requestBodyJson,
+ };
+};
+
+/**
+ * Finds a matching event from candidate events based on POST request body
+ * This implements the same logic as mock-server.js for finding the best match
+ *
+ * @param candidateEvents - Array of potential matching events
+ * @param requestBodyJson - Parsed request body JSON
+ * @returns The best matching event or undefined
+ */
+export const findMatchingPostEvent = (
+ candidateEvents: MockApiEndpoint[],
+ requestBodyJson: MockApiEndpoint['requestBody'],
+): MockApiEndpoint | undefined => {
+ if (!candidateEvents.length) {
+ return undefined;
+ }
+
+ // Prefer events whose requestBody matches (respecting ignoreFields)
+ const matchingEvent = candidateEvents.find((event) => {
+ if (!event.requestBody || !requestBodyJson) return false;
+
+ const result = processPostRequestBody(
+ JSON.stringify(requestBodyJson),
+ event.requestBody,
+ { ignoreFields: event.ignoreFields || [] },
+ );
+
+ return result.matches;
+ });
+
+ // Fallback to an event without a requestBody matcher
+ if (!matchingEvent) {
+ return candidateEvents.find((event) => !event.requestBody);
+ }
+
+ return matchingEvent;
+};
+
+export async function setupMockRequest(
+ server: Mockttp,
+ response: ResponseParam,
+ priority?: number,
+) {
+ let requestRuleBuilder;
+
+ if (response.requestMethod === 'GET') {
+ requestRuleBuilder = server.forGet('/proxy');
+ }
+
+ if (response.requestMethod === 'POST') {
+ requestRuleBuilder = server.forPost('/proxy');
+ }
+
+ if (response.requestMethod === 'PUT') {
+ requestRuleBuilder = server.forPut('/proxy');
+ }
+
+ if (response.requestMethod === 'DELETE') {
+ requestRuleBuilder = server.forDelete('/proxy');
+ }
+
+ await requestRuleBuilder
+ ?.matching((request) => {
+ const url = getDecodedProxiedURL(request.url);
+
+ if (response.url instanceof RegExp) {
+ const matches = response.url.test(url);
+ return matches;
+ }
+ const matches = url.includes(String(response.url));
+ return matches;
+ })
+ .asPriority(priority ?? 999) // Adding priority to this mock request helper as we want TestSpecificMocks to always take precedence
+ .thenCallback((request) => {
+ logger.info(
+ `Mocking ${request.method} request to: ${getDecodedProxiedURL(
+ request.url,
+ )}`,
+ );
+ logger.debug(`Returning response:`, response.response);
+ return {
+ statusCode: 200,
+ json: response.response,
+ };
+ });
+}
+
+/**
+ * Helper to mock a POST request with complex body matching through the mobile proxy pattern
+ *
+ * @param mockServer - The mock server instance
+ * @param url - The URL to match - supports string or RegExp
+ * @param requestBody - Expected request body object to match against
+ * @param response - The response to return
+ * @param options - Additional options for matching and response
+ * @param options.statusCode - HTTP status code to return (default: 200)
+ * @param options.ignoreFields - Array of field paths to ignore during request body comparison
+ * @param options.priority - Set the rule priority. Any matching rule with a higher priority will always take precedence over a matching lower-priority rule, unless the higher rule has an explicit completion check (like .once()) that has already been completed. The RulePriority enum defines the standard values useful for most cases, but any positive number may be used for advanced configurations. (default: 999)
+ */
+export const setupMockPostRequest = async (
+ mockServer: Mockttp,
+ url: string | RegExp,
+ requestBody: unknown,
+ response: unknown,
+ options: {
+ statusCode?: number;
+ ignoreFields?: string[];
+ priority?: number;
+ } = {},
+) => {
+ const { statusCode = 200, ignoreFields = [], priority = 999 } = options;
+
+ await mockServer
+ .forPost('/proxy')
+ .matching((request) => {
+ const decodedUrl = getDecodedProxiedURL(request.url);
+
+ if (url instanceof RegExp) {
+ const matches = url.test(decodedUrl);
+ return matches;
+ }
+ const matches = decodedUrl.includes(String(url));
+ return matches;
+ })
+ .asPriority(priority) // Adding priority to this mock request helper as we want TestSpecificMocks to always take precedence
+ .thenCallback(async (request) => {
+ const decodedUrl = getDecodedProxiedURL(request.url);
+
+ try {
+ const requestBodyText = await request.body.getText();
+
+ const result = processPostRequestBody(requestBodyText, requestBody, {
+ ignoreFields,
+ });
+
+ if (!result.matches) {
+ logger.warn('❌ Request body validation failed for', decodedUrl);
+ logger.debug('Expected:', requestBody);
+ logger.debug('Received:', result.requestBodyJson);
+ logger.debug('Ignored fields:', ignoreFields);
+ logger.debug('Error:', result.error);
+ return {
+ statusCode:
+ result.error === 'Missing request body' ||
+ result.error === 'Invalid request body JSON'
+ ? 400
+ : statusCode,
+ json: response,
+ };
+ }
+
+ logger.info(`Mocking POST request to: ${decodedUrl}`);
+ logger.debug(`Returning response:`, response);
+
+ return {
+ statusCode,
+ json: response,
+ };
+ } catch (error) {
+ logger.error('Error processing request:', error);
+ return {
+ statusCode: 400,
+ body: JSON.stringify({ error: 'Error processing request' }),
+ };
+ }
+ });
+};
+
+/**
+ * Helper to intercept and transform URLs through the mobile proxy pattern
+ * @param mockServer - The mock server instance
+ * @param urlMatcher - Function to match URLs that need transformation
+ * @param urlTransformer - Function to transform the URL
+ */
+export const interceptProxyUrl = async (
+ mockServer: Mockttp,
+ urlMatcher: (url: string) => boolean,
+ urlTransformer: (url: string) => string,
+) => {
+ await mockServer
+ .forAnyRequest()
+ .matching((req) => {
+ if (!req.path.startsWith('/proxy')) return false;
+ const urlParam = new URL(req.url).searchParams.get('url');
+ return urlParam ? urlMatcher(urlParam) : false;
+ })
+ .thenCallback(async (request) => {
+ const urlParam = new URL(request.url).searchParams.get('url');
+ if (!urlParam) {
+ return { statusCode: 400, body: 'Missing url parameter' };
+ }
+
+ const transformedUrl = urlTransformer(urlParam);
+
+ const headers: Record = {};
+ for (const [key, value] of Object.entries(request.headers)) {
+ if (typeof value === 'string') {
+ headers[key] = value;
+ }
+ }
+
+ const response = await fetch(transformedUrl, {
+ method: request.method,
+ headers,
+ body:
+ request.method === 'POST' ? await request.body.getText() : undefined,
+ });
+
+ return {
+ statusCode: response.status,
+ body: await response.text(),
+ };
+ });
+};
diff --git a/e2e/api-specs/json-rpc-coverage.js b/e2e/api-specs/json-rpc-coverage.js
index dbe8d32c3db..1745a18a330 100644
--- a/e2e/api-specs/json-rpc-coverage.js
+++ b/e2e/api-specs/json-rpc-coverage.js
@@ -21,6 +21,7 @@ import { BrowserViewSelectorsIDs } from '../selectors/Browser/BrowserView.select
import { getGanachePort } from '../framework/fixtures/FixtureUtils';
import { mockEvents } from '../api-mocking/mock-config/mock-events';
import { DappVariants } from '../framework/Constants';
+import { setupMockRequest } from '../api-mocking/mockHelpers';
const port = getGanachePort(8545, process.pid);
const chainId = 1337;
@@ -155,8 +156,15 @@ const main = async () => {
const server = mockServer(port, openrpcDocument);
server.start();
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/framework/.eslintrc.js b/e2e/framework/.eslintrc.js
index 617ee3f7c1f..bd3e1a2be65 100644
--- a/e2e/framework/.eslintrc.js
+++ b/e2e/framework/.eslintrc.js
@@ -19,7 +19,32 @@ module.exports = {
},
{
files: ['**/specs/**/*.{js,ts}'],
+ excludedFiles: ['**/specs/**/*.failing.{js,ts}'],
rules: {
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: [
+ {
+ name: '../api-mocking/mock-server',
+ message:
+ 'Do not import startMockServer directly in test specs. Use withFixtures() with testSpecificMock parameter instead.',
+ },
+ {
+ name: '../../api-mocking/mock-server',
+ message:
+ 'Do not import startMockServer directly in test specs. Use withFixtures() with testSpecificMock parameter instead.',
+ },
+ ],
+ patterns: [
+ {
+ group: ['**/api-mocking/mock-server*'],
+ message:
+ 'Do not import startMockServer directly in test specs. Use withFixtures() with testSpecificMock parameter instead.',
+ },
+ ],
+ },
+ ],
'no-restricted-syntax': [
'warn',
{
@@ -50,6 +75,11 @@ module.exports = {
message:
'Avoid direct waitFor() chains in test specs. Use Assertions utility methods (from e2e/framework/Assertions.ts) for better error handling.',
},
+ {
+ selector: "CallExpression[callee.name='startMockServer']",
+ message:
+ 'Do not call startMockServer directly in test specs. Use withFixtures() with testSpecificMock parameter instead.',
+ },
],
},
},
diff --git a/e2e/framework/Constants.ts b/e2e/framework/Constants.ts
index 901a649c2ce..db63580a020 100644
--- a/e2e/framework/Constants.ts
+++ b/e2e/framework/Constants.ts
@@ -76,6 +76,7 @@ export enum RampsRegionsEnum {
SAINT_LUCIA = 'saint-lucia',
FRANCE = 'france',
UNITED_STATES = 'united-states',
+ SPAIN = 'spain',
}
export const RampsRegions = {
@@ -109,4 +110,14 @@ export const RampsRegions = {
recommended: false,
detected: false,
},
+ [RampsRegionsEnum.SPAIN]: {
+ currencies: ['/currencies/fiat/eur'],
+ emoji: '🇪🇸',
+ id: '/regions/es',
+ name: 'Spain',
+ support: { buy: true, sell: true, recurringBuy: true },
+ unsupported: false,
+ recommended: false,
+ detected: false,
+ },
};
diff --git a/e2e/framework/fixtures/FixtureHelper.ts b/e2e/framework/fixtures/FixtureHelper.ts
index 06e5bf0bd5e..3cff60b97c9 100644
--- a/e2e/framework/fixtures/FixtureHelper.ts
+++ b/e2e/framework/fixtures/FixtureHelper.ts
@@ -32,12 +32,7 @@ import {
GanacheNodeOptions,
TestSpecificMock,
} from '../types';
-import {
- TestDapps,
- DappVariants,
- defaultGanacheOptions,
- DEFAULT_MOCKSERVER_PORT,
-} from '../Constants';
+import { TestDapps, DappVariants, defaultGanacheOptions } from '../Constants';
import ContractAddressRegistry from '../../../app/util/test/contract-address-registry';
import FixtureBuilder from './FixtureBuilder';
import { createLogger } from '../logger';
@@ -325,99 +320,25 @@ export const stopFixtureServer = async (fixtureServer: FixtureServer) => {
logger.debug('The fixture server is stopped');
};
-/**
- * Merges test-specific mocks with default mocks, prioritizing test-specific mocks
- * @param testSpecificMocks - Test-specific mock events organized by method
- * @returns Merged mock events with test-specific mocks taking priority
- */
-const mergeWithDefaultMocks = (
- testSpecificMocks: TestSpecificMock | undefined,
-) => {
- if (!testSpecificMocks) {
- return DEFAULT_MOCKS;
- }
-
- const mergedMocks: TestSpecificMock = {};
-
- // Get all HTTP methods from both test-specific and default mocks
- const allMethods = new Set([
- ...Object.keys(testSpecificMocks),
- ...Object.keys(DEFAULT_MOCKS),
- ]);
-
- allMethods.forEach((method) => {
- const testMocks = testSpecificMocks[method as keyof TestSpecificMock] || [];
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const defaultMocks = (DEFAULT_MOCKS as any)[method] || [];
-
- // Create a set of URLs that already exist in test-specific mocks
- const testMockUrls = new Set(testMocks.map((mock) => mock.urlEndpoint));
-
- // Filter out default mocks that have the same URL as test-specific mocks
- const filteredDefaultMocks = defaultMocks.filter(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (defaultMock: any) => !testMockUrls.has(defaultMock.urlEndpoint),
- );
-
- // Merge test-specific mocks first, then append non-duplicate default mocks
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (mergedMocks as any)[method] = [...testMocks, ...filteredDefaultMocks];
- });
-
- return mergedMocks;
-};
-
export const createMockAPIServer = async (
- mockServerInstance?: Mockttp,
testSpecificMock?: TestSpecificMock,
): Promise<{
mockServer: Mockttp;
mockServerPort: number;
}> => {
- // Handle mock server
- let mockServer: Mockttp | undefined;
- let mockServerPort: number = DEFAULT_MOCKSERVER_PORT;
-
- // Both
- if (mockServerInstance && testSpecificMock) {
- throw new Error(
- 'Cannot use both mockServerInstance and testSpecificMock at the same time. Please use only one.',
- );
- }
-
- // mockServerInstance only
- if (mockServerInstance && !testSpecificMock) {
- mockServer = mockServerInstance;
- mockServerPort = mockServer.port;
- logger.debug(
- `Mock server started from mockServerInstance on port ${mockServerPort}`,
- );
- }
-
- // testSpecificMock only
- if (!mockServerInstance && testSpecificMock) {
- mockServerPort = getMockServerPort();
- const mergedMocks = mergeWithDefaultMocks(testSpecificMock);
- mockServer = await startMockServer(mergedMocks, mockServerPort);
-
- logger.debug(
- `Mock server started from testSpecificMock on port ${mockServerPort}`,
- );
- }
-
- // neither
- if (!mockServerInstance && !testSpecificMock) {
- mockServerPort = getMockServerPort();
- const mergedMocks = mergeWithDefaultMocks(testSpecificMock);
- mockServer = await startMockServer(mergedMocks, mockServerPort);
+ const mockServerPort = getMockServerPort();
+ const mockServer = await startMockServer(
+ DEFAULT_MOCKS,
+ mockServerPort,
+ testSpecificMock, // Applied First, so any test-specific mocks take precedence
+ );
+ if (testSpecificMock) {
logger.debug(
- `Mock server started from testSpecificMock on port ${mockServerPort}`,
+ `Mock server started with testSpecificMock (priority) + defaults fallback on port ${mockServerPort}`,
);
- }
-
- if (!mockServer) {
- throw new Error('Test setup failure, no mock server setup');
+ } else {
+ logger.debug(`Mock server started with defaults on port ${mockServerPort}`);
}
// Additional Global Mocks
@@ -460,7 +381,6 @@ export async function withFixtures(
},
],
testSpecificMock,
- mockServerInstance,
launchArgs,
languageAndLocale,
permissions = {},
@@ -471,7 +391,6 @@ export async function withFixtures(
await TestHelpers.reverseServerPort();
const { mockServer, mockServerPort } = await createMockAPIServer(
- mockServerInstance,
testSpecificMock,
);
diff --git a/e2e/framework/fixtures/README.md b/e2e/framework/fixtures/README.md
index 9c028f06f7b..183011ac0f2 100644
--- a/e2e/framework/fixtures/README.md
+++ b/e2e/framework/fixtures/README.md
@@ -29,20 +29,19 @@ describe('My Test Suite', () => {
## WithFixturesOptions Reference
-| Option | Type | Required | Default | Description |
-| -------------------- | ----------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
-| `fixture` | `FixtureBuilder` | `true` | - | The fixture object created via FixtureBuilder |
-| `restartDevice` | `boolean` | `false` | `false` | Whether to restart the device before the test |
-| `smartContracts` | `string[]` | `false` | - | The list of contract strings to be deployed via the first seeder |
-| `disableLocalNodes` | `boolean` | `false` | `false` | Disables all local nodes for the test |
-| `dapps` | `DappOptions[]` | `false` | - | Lists the dapps that should be launched before the tests |
-| `localNodeOptions` | `LocalNodeOptionsInput` | `false` | Anvil | Allows overriding the use of Anvil in favor of any other node |
-| `testSpecificMock` | `TestSpecificMock` | `false` | - | Allows to set mocks that are specific to the test |
-| `launcArgs` | `LaunchArgs` | `false` | `-` | Allows sending arbitrary launchArgs such as the fixtureServerPort |
-| `languageAndLocale` | `LanguageAndLocale` | `false` | - | Set the device Language and Locale of the device |
-| `permissions` | `object` | `false` | - | Allows setting specific device permissions |
-| `mockServerInstance` | `Mockttp` | `false` | - | Allows providing a mock server instance instead of having one created automatically. Should not be used together with `testSpecificMock` |
-| `endTestfn` | `fn()` | `false` | - | Allows providing a function that is executed at the end of the test before the cleanup |
+| Option | Type | Required | Default | Description |
+| ------------------- | ----------------------- | -------- | ------- | -------------------------------------------------------------------------------------- |
+| `fixture` | `FixtureBuilder` | `true` | - | The fixture object created via FixtureBuilder |
+| `restartDevice` | `boolean` | `false` | `false` | Whether to restart the device before the test |
+| `smartContracts` | `string[]` | `false` | - | The list of contract strings to be deployed via the first seeder |
+| `disableLocalNodes` | `boolean` | `false` | `false` | Disables all local nodes for the test |
+| `dapps` | `DappOptions[]` | `false` | - | Lists the dapps that should be launched before the tests |
+| `localNodeOptions` | `LocalNodeOptionsInput` | `false` | Anvil | Allows overriding the use of Anvil in favor of any other node |
+| `testSpecificMock` | `TestSpecificMock` | `false` | - | Allows to set mocks that are specific to the test |
+| `launcArgs` | `LaunchArgs` | `false` | `-` | Allows sending arbitrary launchArgs such as the fixtureServerPort |
+| `languageAndLocale` | `LanguageAndLocale` | `false` | - | Set the device Language and Locale of the device |
+| `permissions` | `object` | `false` | - | Allows setting specific device permissions |
+| `endTestfn` | `fn()` | `false` | - | Allows providing a function that is executed at the end of the test before the cleanup |
## Migration from Legacy Options
diff --git a/e2e/framework/types.ts b/e2e/framework/types.ts
index f58c50d3451..b8626487d0d 100644
--- a/e2e/framework/types.ts
+++ b/e2e/framework/types.ts
@@ -153,11 +153,36 @@ export type LocalNode = AnvilManager | Ganache;
export interface TestSuiteParams {
contractRegistry?: ContractAddressRegistry;
- mockServer?: Mockttp;
+ mockServer: Mockttp;
localNodes?: LocalNode[];
}
-export interface TestSpecificMock {
+/**
+ * ONLY TO BE USED BY DEFAULT MOCKS
+ *
+ * If you are using individual mocks for specific tests
+ * Use the `testSpecificMock` function instead for improved mock management and type safety.
+ *
+ * Interface representing a collection of mock API endpoints grouped by HTTP methods.
+ * Each property corresponds to an HTTP method (GET, POST, PUT, etc.) and contains
+ * an array of mock endpoints for that method.
+ *
+ * @example
+ * ```typescript
+ * // Deprecated usage - avoid this pattern
+ * const mocks: MockObject = {
+ * GET: [{ url: '/api/users', response: { users: [] } }],
+ * POST: [{ url: '/api/users', response: { id: 1 } }]
+ * };
+ *
+ * // Preferred approach - use testSpecificMock instead
+ * testSpecificMock(mockServer) {
+ * mockServer.forGet('/api/users').thenReply(200, JSON.stringify({ users: [] }));
+ * mockServer.forPost('/api/users').thenReply(200, JSON.stringify({ id: 1 }));
+ * };
+ * ```
+ */
+export interface MockEventsObject {
GET?: MockApiEndpoint[];
POST?: MockApiEndpoint[];
PUT?: MockApiEndpoint[];
@@ -172,6 +197,8 @@ export interface MockApiEndpoint {
responseCode: number;
}
+export type TestSpecificMock = (mockServer: Mockttp) => Promise;
+
/**
* The options for the withFixtures function.
* @param {FixtureBuilder} fixture - The state of the fixture to load.
@@ -180,11 +207,10 @@ export interface MockApiEndpoint {
* @param {LocalNodeOptionsInput} [localNodeOptions] - The local node options to use for the test.
* @param {boolean} [disableLocalNodes=false] - If true, disables the local nodes.
* @param {DappOptions[]} [dapps] - The dapps to load for test. The base static port is defined and all dapps from dapp[1] will have the port incremented by 1.
- * @param {Record} [testSpecificMock] - The test specific mock to load for test. This needs to be properly typed once we convert api-mocking.js to ts
+ * @param {Record} [testSpecificMock] - The test specific mock function to use for the test.
* @param {Partial} [launchArgs] - The launch arguments to use for the test.
* @param {LanguageAndLocale} [languageAndLocale] - The language and locale to use for the test.
* @param {Record} [permissions] - The permissions to set for the device.
- * @param {Mockttp} [mockServerInstance] - The mock server instance to use for the test. Useful when a custom setup of the mock server is needed.
* @param {() => Promise} [endTestfn] - The function to execute after the test is finished.
*/
export interface WithFixturesOptions {
@@ -198,7 +224,6 @@ export interface WithFixturesOptions {
launchArgs?: Partial;
languageAndLocale?: LanguageAndLocale;
permissions?: Record;
- mockServerInstance?: Mockttp;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
endTestfn?: (...args: any[]) => Promise;
}
diff --git a/e2e/pages/importSrp/ImportSrpView.ts b/e2e/pages/importSrp/ImportSrpView.ts
index 29dbf408b98..1af6f8360e2 100644
--- a/e2e/pages/importSrp/ImportSrpView.ts
+++ b/e2e/pages/importSrp/ImportSrpView.ts
@@ -28,12 +28,21 @@ class ImportSrpView {
elemDescription: 'Import button',
});
}
-
- async enterSrpWord(srpIndex: number, word: string) {
- await Gestures.typeText(this.inputOfIndex(srpIndex), word, {
- elemDescription: `SRP word input at index ${srpIndex}`,
- hideKeyboard: true,
- });
+ async enterSrpWord(srpIndex: number, word: string): Promise {
+ const inputElement = this.inputOfIndex(srpIndex);
+ const elemDescription = `SRP word input at index ${srpIndex}`;
+
+ if (device.getPlatform() === 'ios') {
+ await Gestures.typeText(inputElement, word, {
+ elemDescription,
+ hideKeyboard: true,
+ });
+ } else {
+ // For Android, we use replaceText to avoid autocomplete issue
+ await Gestures.replaceText(inputElement, word, {
+ elemDescription,
+ });
+ }
}
async selectNWordSrp(numberOfWords: number) {
diff --git a/e2e/pages/swaps/SwapView.ts b/e2e/pages/swaps/SwapView.ts
index ea09ab4b86b..1acfec52418 100644
--- a/e2e/pages/swaps/SwapView.ts
+++ b/e2e/pages/swaps/SwapView.ts
@@ -6,7 +6,7 @@ import {
import Matchers from '../../framework/Matchers';
import Gestures from '../../framework/Gestures';
import { waitFor } from 'detox';
-import { logger } from '../../framework/logger.js';
+import { logger } from '../../framework/logger';
class SwapView {
get quoteSummary(): DetoxElement {
diff --git a/e2e/resources/mock-configs.js b/e2e/resources/mock-configs.js
deleted file mode 100644
index d44293a00ae..00000000000
--- a/e2e/resources/mock-configs.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { defaultGanacheOptions } from '../framework/Constants.ts';
-import { CustomNetworks } from './networks.e2e';
-import { mockEvents } from '../api-mocking/mock-config/mock-events';
-
-const MONAD_TESTNET = CustomNetworks.MonadTestnet.providerConfig;
-const MEGAETH_TESTNET = CustomNetworks.MegaTestnet.providerConfig;
-const SEI_TESTNET = CustomNetworks.SeiTestNet.providerConfig;
-
-/**
- * Shared mock configuration for all network tests
- */
-export const testSpecificMock = {
- GET: [mockEvents.GET.suggestedGasFeesApiGanache],
-};
-
-/**
- * Mega ETH local Ganache configuration
- */
-export const megaEthLocalConfig = {
- ...defaultGanacheOptions,
- chainId: parseInt(MEGAETH_TESTNET.chainId, 16),
- networkId: parseInt(MEGAETH_TESTNET.chainId, 16),
- gasPrice: '0x3b9aca00',
- gasLimit: '0x1c9c380',
-};
-
-/**
- * Monad local Ganache configuration
- */
-export const monadLocalConfig = {
- ...defaultGanacheOptions,
- chainId: parseInt(MONAD_TESTNET.chainId, 16),
- networkId: parseInt(MONAD_TESTNET.chainId, 16),
- gasPrice: '0x1',
- gasLimit: '0x5f5e100',
-};
-
-export const seiLocalConfig = {
- ...defaultGanacheOptions,
- chainId: parseInt(SEI_TESTNET.chainId, 16),
- networkId: parseInt(SEI_TESTNET.chainId, 16),
- gasPrice: '0x1',
- gasLimit: '0x5f5e100',
-};
-
-/**
- * Mega ETH provider configuration
- */
-export const megaEthProviderConfig = {
- chainId: '0x539',
- rpcUrl: 'http://localhost:8545', // Local Ganache
- ticker: MEGAETH_TESTNET.ticker, // "MegaETH" ticker (for display)
- nickname: `${MEGAETH_TESTNET.nickname}`, // "Mega Testnet (Local)"
- type: 'custom',
-};
-
-/**
- * Monad provider configuration
- */
-export const monadProviderConfig = {
- chainId: '0x539',
- rpcUrl: 'http://localhost:8545', // Local Ganache
- ticker: MONAD_TESTNET.ticker, // "MON" ticker (for display)
- nickname: `${MONAD_TESTNET.nickname}`, // "Monad Testnet (Local)"
- type: 'custom',
-};
-
-export const seiProviderConfig = {
- chainId: '0x539',
- rpcUrl: 'http://localhost:8545', // Local Ganache
- ticker: SEI_TESTNET.ticker, // "SEI" ticker (for display)
- nickname: `${SEI_TESTNET.nickname}`, // "Sei Testnet (Local)"
- type: 'custom',
-};
-
-/**
- * Permission configurations for different networks
- */
-export const permissionConfigs = {
- /**
- * Ganache permissions (for local testing)
- */
- ganache: ['0x539'],
-
- /**
- * Real network permissions (if needed for future authentic testing)
- */
- megaEth: [MEGAETH_TESTNET.chainId],
- monad: [MONAD_TESTNET.chainId],
- sei: [SEI_TESTNET.chainId],
-};
-
-/**
- * Complete test configurations that combine all the above
- * Ready-to-use configurations for withFixtures
- */
-export const testConfigurations = {
- /**
- * Mega ETH test configuration
- */
- megaEth: {
- ganacheOptions: megaEthLocalConfig,
- providerConfig: megaEthProviderConfig,
- permissions: permissionConfigs.megaEth,
- testSpecificMock,
- },
-
- /**
- * Monad test configuration
- */
- monad: {
- ganacheOptions: monadLocalConfig,
- providerConfig: monadProviderConfig,
- permissions: permissionConfigs.monad,
- testSpecificMock,
- },
- /**
- * Sei test configuration
- */
- sei: {
- ganacheOptions: seiLocalConfig,
- providerConfig: seiProviderConfig,
- permissions: permissionConfigs.sei,
- testSpecificMock,
- },
-};
-
-/**
- * Test configurations for easy network testing
- * Add new networks here to automatically include them in all tests
- */
-export const NETWORK_TEST_CONFIGS = [
- {
- name: 'MegaETH',
- networkConfig: MEGAETH_TESTNET,
- ganacheOptions: megaEthLocalConfig,
- providerConfig: megaEthProviderConfig,
- permissions: [MEGAETH_TESTNET.chainId],
- testSpecificMock,
- },
- {
- name: 'Monad',
- networkConfig: MONAD_TESTNET,
- ganacheOptions: monadLocalConfig,
- providerConfig: monadProviderConfig,
- permissions: [MONAD_TESTNET.chainId],
- testSpecificMock,
- },
- {
- name: 'Sei',
- networkConfig: SEI_TESTNET,
- ganacheOptions: seiLocalConfig,
- providerConfig: seiProviderConfig,
- permissions: [SEI_TESTNET.chainId],
- testSpecificMock,
- },
-];
diff --git a/e2e/resources/mock-configs.ts b/e2e/resources/mock-configs.ts
new file mode 100644
index 00000000000..820084ffbd3
--- /dev/null
+++ b/e2e/resources/mock-configs.ts
@@ -0,0 +1,306 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { defaultGanacheOptions } from '../framework/Constants';
+import { CustomNetworks } from './networks.e2e';
+import { mockEvents } from '../api-mocking/mock-config/mock-events';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../api-mocking/mockHelpers';
+
+const MONAD_TESTNET = CustomNetworks.MonadTestnet.providerConfig;
+const MEGAETH_TESTNET = CustomNetworks.MegaTestnet.providerConfig;
+const SEI_TESTNET = CustomNetworks.SeiTestNet.providerConfig;
+
+/**
+ * Provider configuration interface
+ */
+export interface ProviderConfig {
+ chainId: string;
+ rpcUrl: string;
+ ticker: string;
+ nickname: string;
+ type: string;
+}
+
+/**
+ * Ganache configuration interface
+ */
+export interface GanacheConfig {
+ chainId: number;
+ networkId: number;
+ gasPrice: string;
+ gasLimit: string;
+ [key: string]: any;
+}
+
+/**
+ * Test configuration interface
+ */
+export interface TestConfiguration {
+ ganacheOptions: GanacheConfig;
+ providerConfig: ProviderConfig;
+ permissions: string[];
+ testSpecificMock: (mockServer: Mockttp) => Promise;
+}
+
+/**
+ * Network test configuration interface
+ */
+export interface NetworkTestConfig {
+ name: string;
+ networkConfig: any;
+ ganacheOptions: GanacheConfig;
+ providerConfig: ProviderConfig;
+ permissions: string[];
+ testSpecificMock: (mockServer: Mockttp) => Promise;
+}
+
+/**
+ * Shared mock configuration for all network tests using redesigned patterns
+ */
+export const testSpecificMock = async (mockServer: Mockttp): Promise => {
+ const { urlEndpoint, response } = mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+};
+
+/**
+ * Redesigned confirmations mock configuration
+ */
+export const redesignedTestSpecificMock = async (
+ mockServer: Mockttp,
+): Promise => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ const gasFeesConfig = mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasFeesConfig.urlEndpoint,
+ response: gasFeesConfig.response,
+ responseCode: 200,
+ });
+};
+
+/**
+ * Mega ETH local Ganache configuration
+ */
+export const megaEthLocalConfig: GanacheConfig = {
+ ...defaultGanacheOptions,
+ chainId: parseInt(MEGAETH_TESTNET.chainId, 16),
+ networkId: parseInt(MEGAETH_TESTNET.chainId, 16),
+ gasPrice: '0x3b9aca00',
+ gasLimit: '0x1c9c380',
+};
+
+/**
+ * Monad local Ganache configuration
+ */
+export const monadLocalConfig: GanacheConfig = {
+ ...defaultGanacheOptions,
+ chainId: parseInt(MONAD_TESTNET.chainId, 16),
+ networkId: parseInt(MONAD_TESTNET.chainId, 16),
+ gasPrice: '0x1',
+ gasLimit: '0x5f5e100',
+};
+
+/**
+ * Sei local Ganache configuration
+ */
+export const seiLocalConfig: GanacheConfig = {
+ ...defaultGanacheOptions,
+ chainId: parseInt(SEI_TESTNET.chainId, 16),
+ networkId: parseInt(SEI_TESTNET.chainId, 16),
+ gasPrice: '0x1',
+ gasLimit: '0x5f5e100',
+};
+
+/**
+ * Mega ETH provider configuration
+ */
+export const megaEthProviderConfig: ProviderConfig = {
+ chainId: '0x539',
+ rpcUrl: 'http://localhost:8545', // Local Ganache
+ ticker: MEGAETH_TESTNET.ticker, // "MegaETH" ticker (for display)
+ nickname: `${MEGAETH_TESTNET.nickname}`, // "Mega Testnet (Local)"
+ type: 'custom',
+};
+
+/**
+ * Monad provider configuration
+ */
+export const monadProviderConfig: ProviderConfig = {
+ chainId: '0x539',
+ rpcUrl: 'http://localhost:8545', // Local Ganache
+ ticker: MONAD_TESTNET.ticker, // "MON" ticker (for display)
+ nickname: `${MONAD_TESTNET.nickname}`, // "Monad Testnet (Local)"
+ type: 'custom',
+};
+
+/**
+ * Sei provider configuration
+ */
+export const seiProviderConfig: ProviderConfig = {
+ chainId: '0x539',
+ rpcUrl: 'http://localhost:8545', // Local Ganache
+ ticker: SEI_TESTNET.ticker, // "SEI" ticker (for display)
+ nickname: `${SEI_TESTNET.nickname}`, // "Sei Testnet (Local)"
+ type: 'custom',
+};
+
+/**
+ * Permission configurations for different networks
+ */
+export const permissionConfigs = {
+ /**
+ * Ganache permissions (for local testing)
+ */
+ ganache: ['0x539'],
+
+ /**
+ * Real network permissions (if needed for future authentic testing)
+ */
+ megaEth: [MEGAETH_TESTNET.chainId],
+ monad: [MONAD_TESTNET.chainId],
+ sei: [SEI_TESTNET.chainId],
+} as const;
+
+/**
+ * Complete test configurations that combine all the above
+ * Ready-to-use configurations for withFixtures
+ */
+export const testConfigurations: Record = {
+ /**
+ * Mega ETH test configuration
+ */
+ megaEth: {
+ ganacheOptions: megaEthLocalConfig,
+ providerConfig: megaEthProviderConfig,
+ permissions: [...permissionConfigs.megaEth],
+ testSpecificMock,
+ },
+
+ /**
+ * Monad test configuration
+ */
+ monad: {
+ ganacheOptions: monadLocalConfig,
+ providerConfig: monadProviderConfig,
+ permissions: [...permissionConfigs.monad],
+ testSpecificMock,
+ },
+
+ /**
+ * Sei test configuration
+ */
+ sei: {
+ ganacheOptions: seiLocalConfig,
+ providerConfig: seiProviderConfig,
+ permissions: [...permissionConfigs.sei],
+ testSpecificMock,
+ },
+};
+
+/**
+ * Redesigned test configurations using new mock patterns
+ */
+export const redesignedTestConfigurations: Record = {
+ /**
+ * Mega ETH test configuration with redesigned mocks
+ */
+ megaEth: {
+ ganacheOptions: megaEthLocalConfig,
+ providerConfig: megaEthProviderConfig,
+ permissions: [...permissionConfigs.megaEth],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+
+ /**
+ * Monad test configuration with redesigned mocks
+ */
+ monad: {
+ ganacheOptions: monadLocalConfig,
+ providerConfig: monadProviderConfig,
+ permissions: [...permissionConfigs.monad],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+
+ /**
+ * Sei test configuration with redesigned mocks
+ */
+ sei: {
+ ganacheOptions: seiLocalConfig,
+ providerConfig: seiProviderConfig,
+ permissions: [...permissionConfigs.sei],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+};
+
+/**
+ * Test configurations for easy network testing
+ * Add new networks here to automatically include them in all tests
+ */
+export const NETWORK_TEST_CONFIGS: NetworkTestConfig[] = [
+ {
+ name: 'MegaETH',
+ networkConfig: MEGAETH_TESTNET,
+ ganacheOptions: megaEthLocalConfig,
+ providerConfig: megaEthProviderConfig,
+ permissions: [MEGAETH_TESTNET.chainId],
+ testSpecificMock,
+ },
+ {
+ name: 'Monad',
+ networkConfig: MONAD_TESTNET,
+ ganacheOptions: monadLocalConfig,
+ providerConfig: monadProviderConfig,
+ permissions: [MONAD_TESTNET.chainId],
+ testSpecificMock,
+ },
+ {
+ name: 'Sei',
+ networkConfig: SEI_TESTNET,
+ ganacheOptions: seiLocalConfig,
+ providerConfig: seiProviderConfig,
+ permissions: [SEI_TESTNET.chainId],
+ testSpecificMock,
+ },
+];
+
+/**
+ * Redesigned network test configurations using new mock patterns
+ */
+export const REDESIGNED_NETWORK_TEST_CONFIGS: NetworkTestConfig[] = [
+ {
+ name: 'MegaETH',
+ networkConfig: MEGAETH_TESTNET,
+ ganacheOptions: megaEthLocalConfig,
+ providerConfig: megaEthProviderConfig,
+ permissions: [MEGAETH_TESTNET.chainId],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+ {
+ name: 'Monad',
+ networkConfig: MONAD_TESTNET,
+ ganacheOptions: monadLocalConfig,
+ providerConfig: monadProviderConfig,
+ permissions: [MONAD_TESTNET.chainId],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+ {
+ name: 'Sei',
+ networkConfig: SEI_TESTNET,
+ ganacheOptions: seiLocalConfig,
+ providerConfig: seiProviderConfig,
+ permissions: [SEI_TESTNET.chainId],
+ testSpecificMock: redesignedTestSpecificMock,
+ },
+];
diff --git a/e2e/selectors/MultichainAccounts/PrivateKeyList.selectors.ts b/e2e/selectors/MultichainAccounts/PrivateKeyList.selectors.ts
new file mode 100644
index 00000000000..7ffc81d260a
--- /dev/null
+++ b/e2e/selectors/MultichainAccounts/PrivateKeyList.selectors.ts
@@ -0,0 +1,9 @@
+export const PrivateKeyListIds = {
+ PASSWORD_TITLE: 'password-title',
+ BANNER: 'banner',
+ PASSWORD_INPUT: 'password-input',
+ PASSWORD_ERROR: 'password-error',
+ CONTINUE_BUTTON: 'continue-button',
+ CANCEL_BUTTON: 'cancel-button',
+ LIST: 'list',
+};
diff --git a/e2e/specs/accounts/change-account-name.spec.ts b/e2e/specs/accounts/change-account-name.spec.ts
index 756ba66dce7..ff4e58da5f2 100644
--- a/e2e/specs/accounts/change-account-name.spec.ts
+++ b/e2e/specs/accounts/change-account-name.spec.ts
@@ -11,12 +11,25 @@ import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import { loginToApp } from '../../viewHelper';
import { mockEvents } from '../../api-mocking/mock-config/mock-events.js';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
const NEW_ACCOUNT_NAME = 'Edited Name';
const NEW_IMPORTED_ACCOUNT_NAME = 'New Imported Account';
const MAIN_ACCOUNT_INDEX = 0;
const IMPORTED_ACCOUNT_INDEX = 1;
+const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+};
+
// TODO: With this migration we also removed the need for ganache options and everything is simplified.
describe(Regression('Change Account Name'), () => {
it('renames an account and verifies the new name persists after locking and unlocking the wallet', async () => {
@@ -26,11 +39,7 @@ describe(Regression('Change Account Name'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
@@ -77,11 +86,7 @@ describe(Regression('Change Account Name'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/accounts/imported-account-remove-and-import.spec.ts b/e2e/specs/accounts/imported-account-remove-and-import.spec.ts
index fc5c7bc01b7..5fe924a07c3 100644
--- a/e2e/specs/accounts/imported-account-remove-and-import.spec.ts
+++ b/e2e/specs/accounts/imported-account-remove-and-import.spec.ts
@@ -12,6 +12,8 @@ import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet';
import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView';
import { mockEvents } from '../../api-mocking/mock-config/mock-events.js';
import { AccountListBottomSheetSelectorsText } from '../../selectors/wallet/AccountListBottomSheet.selectors';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
// This key is for testing private key import only
// It should NEVER hold any eth or token
@@ -19,6 +21,17 @@ const TEST_PRIVATE_KEY =
'cbfd798afcfd1fd8ecc48cbecb6dc7e876543395640b758a90e11d986e758ad1';
const ACCOUNT_INDEX = 1;
+const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+};
+
describe(
Regression('removes and reimports an account using a private key'),
() => {
@@ -29,13 +42,7 @@ describe(
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(
- false,
- ),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/accounts/rename-account-flows.spec.ts b/e2e/specs/accounts/rename-account-flows.spec.ts
index c392135a457..0603322840f 100644
--- a/e2e/specs/accounts/rename-account-flows.spec.ts
+++ b/e2e/specs/accounts/rename-account-flows.spec.ts
@@ -8,6 +8,8 @@ import { SmokeAccounts } from '../../tags';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(SmokeAccounts('Account Rename UI Flows'), () => {
const ORIGINAL_ACCOUNT_NAME = 'Account 1';
@@ -26,10 +28,15 @@ describe(SmokeAccounts('Account Rename UI Flows'), () => {
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
@@ -83,10 +90,15 @@ describe(SmokeAccounts('Account Rename UI Flows'), () => {
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(true),
- ],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(true);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
diff --git a/e2e/specs/accounts/reveal-private-key.spec.ts b/e2e/specs/accounts/reveal-private-key.spec.ts
index bfffc58d6b8..2bf20bf421c 100644
--- a/e2e/specs/accounts/reveal-private-key.spec.ts
+++ b/e2e/specs/accounts/reveal-private-key.spec.ts
@@ -12,6 +12,8 @@ import WalletView from '../../pages/wallet/WalletView';
import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet';
import AccountActionsBottomSheet from '../../pages/wallet/AccountActionsBottomSheet';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers.js';
// These keys are from the fixture and are used to test the reveal private key functionality
const HD_ACCOUNT_1_PRIVATE_KEY =
@@ -25,6 +27,17 @@ describe(Regression('reveal private key'), () => {
const PASSWORD = '123123123';
const INCORRECT_PASSWORD = 'wrongpassword';
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ };
+
it('reveals the correct private key for selected hd account from settings', async () => {
await withFixtures(
{
@@ -32,11 +45,7 @@ describe(Regression('reveal private key'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
@@ -86,11 +95,7 @@ describe(Regression('reveal private key'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
@@ -132,11 +137,7 @@ describe(Regression('reveal private key'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
@@ -178,11 +179,7 @@ describe(Regression('reveal private key'), () => {
.withImportedAccountKeyringController()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails(false),
- ],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/analytics/import-wallet.spec.ts b/e2e/specs/analytics/import-wallet.spec.ts
index 2441a7c679a..728ff7c353e 100644
--- a/e2e/specs/analytics/import-wallet.spec.ts
+++ b/e2e/specs/analytics/import-wallet.spec.ts
@@ -9,11 +9,6 @@ import {
getEventsPayloads,
onboardingEvents,
} from './helpers';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import {
- getBalanceMocks,
- INFURA_MOCK_BALANCE_1_ETH,
-} from '../../api-mocking/mock-responses/balance-mocks';
import {
IDENTITY_TEAM_PASSWORD,
IDENTITY_TEAM_SEED_PHRASE,
@@ -21,18 +16,6 @@ import {
import SoftAssert from '../../utils/SoftAssert';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import { TestSpecificMock } from '../../framework';
-
-const balanceMock = getBalanceMocks([
- {
- address: '0xAa4179E7f103701e904D27DF223a39Aa9c27405a',
- balance: INFURA_MOCK_BALANCE_1_ETH,
- },
-]);
-
-const testSpecificMock = {
- POST: [...balanceMock, mockEvents.POST.segmentTrack],
-} as TestSpecificMock;
describe(SmokeWalletPlatform('Analytics during import wallet flow'), () => {
beforeAll(async () => {
@@ -44,7 +27,6 @@ describe(SmokeWalletPlatform('Analytics during import wallet flow'), () => {
{
fixture: new FixtureBuilder().withOnboardingFixture().build(),
restartDevice: true,
- testSpecificMock,
},
async ({ mockServer }) => {
if (!mockServer) {
@@ -212,7 +194,6 @@ describe(SmokeWalletPlatform('Analytics during import wallet flow'), () => {
{
fixture: new FixtureBuilder().withOnboardingFixture().build(),
restartDevice: true,
- testSpecificMock,
},
async ({ mockServer }) => {
if (!mockServer) {
diff --git a/e2e/specs/analytics/new-wallet.spec.ts b/e2e/specs/analytics/new-wallet.spec.ts
index e8fab5aff6f..f97a4db26b8 100644
--- a/e2e/specs/analytics/new-wallet.spec.ts
+++ b/e2e/specs/analytics/new-wallet.spec.ts
@@ -5,27 +5,10 @@ import { CreateNewWallet } from '../../viewHelper';
import TestHelpers from '../../helpers';
import Assertions from '../../framework/Assertions';
import { getEventsPayloads, onboardingEvents } from './helpers';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import {
- getBalanceMocks,
- INFURA_MOCK_BALANCE_1_ETH,
-} from '../../api-mocking/mock-responses/balance-mocks';
import SoftAssert from '../../utils/SoftAssert';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
-import { TestSpecificMock } from '../../framework';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-const balanceMock = getBalanceMocks([
- {
- address: '0xAa4179E7f103701e904D27DF223a39Aa9c27405a',
- balance: INFURA_MOCK_BALANCE_1_ETH,
- },
-]);
-
-const testSpecificMock = {
- POST: [...balanceMock, mockEvents.POST.segmentTrack],
-} as TestSpecificMock;
-
const eventNames = [
onboardingEvents.ANALYTICS_PREFERENCE_SELECTED,
onboardingEvents.WELCOME_MESSAGE_VIEWED,
@@ -50,7 +33,6 @@ describe(SmokeWalletPlatform('Analytics during import wallet flow'), () => {
{
fixture: new FixtureBuilder().withOnboardingFixture().build(),
restartDevice: true,
- testSpecificMock,
},
async ({ mockServer }) => {
await CreateNewWallet();
@@ -212,7 +194,6 @@ describe(SmokeWalletPlatform('Analytics during import wallet flow'), () => {
{
fixture: new FixtureBuilder().withOnboardingFixture().build(),
restartDevice: true,
- testSpecificMock,
},
async ({ mockServer }) => {
await CreateNewWallet({
diff --git a/e2e/specs/analytics/opt-out.ts b/e2e/specs/analytics/opt-out.ts
new file mode 100644
index 00000000000..ab7e5917a80
--- /dev/null
+++ b/e2e/specs/analytics/opt-out.ts
@@ -0,0 +1,78 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { withFixtures } from '../../framework/fixtures/FixtureHelper';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
+import Assertions from '../../framework/Assertions';
+import { Regression } from '../../tags';
+import SettingsView from '../../pages/Settings/SettingsView';
+import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView';
+import { loginToApp } from '../../viewHelper';
+import TabBarComponent from '../../pages/wallet/TabBarComponent';
+import CommonView from '../../pages/CommonView';
+import {
+ EventPayload,
+ filterEvents,
+ getEventsPayloads,
+ onboardingEvents,
+} from './helpers';
+import SoftAssert from '../../utils/SoftAssert';
+
+describe(
+ Regression('Regression - metametrics opt out from settings WITH ANALYTICS'),
+ (): void => {
+ beforeEach(async (): Promise => {
+ jest.setTimeout(150000);
+ });
+
+ it('should disable metametrics from settings and track the preference change event', async (): Promise => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder().withMetaMetricsOptIn().build(),
+ restartDevice: true,
+ },
+ async ({ mockServer }) => {
+ await loginToApp();
+
+ // Navigate to metametrics settings and disable it
+ await TabBarComponent.tapSettings();
+ await SettingsView.tapSecurityAndPrivacy();
+ await SecurityAndPrivacy.scrollToMetaMetrics();
+
+ await Assertions.expectToggleToBeOn(
+ SecurityAndPrivacy.metaMetricsToggle as Promise,
+ );
+
+ await SecurityAndPrivacy.tapMetaMetricsToggle();
+ await CommonView.tapOkAlert();
+
+ await Assertions.expectToggleToBeOff(
+ SecurityAndPrivacy.metaMetricsToggle as Promise,
+ );
+
+ // Verify the analytics preference change event was tracked
+ const events = await getEventsPayloads(mockServer!);
+ const softAssert = new SoftAssert();
+
+ await softAssert.checkAndCollect(async () => {
+ const analyticsEvents = filterEvents(
+ events,
+ onboardingEvents.ANALYTICS_PREFERENCE_SELECTED,
+ ) as EventPayload[];
+ await Assertions.checkIfValueIsDefined(analyticsEvents);
+ await Assertions.checkIfArrayHasLength(analyticsEvents, 1);
+ await Assertions.checkIfObjectContains(
+ analyticsEvents[0].properties,
+ {
+ has_marketing_consent: false,
+ is_metrics_opted_in: true,
+ location: 'onboarding_metametrics',
+ updated_after_onboarding: false,
+ },
+ );
+ }, 'Analytics Preference Selected event should be tracked when disabling metametrics');
+
+ softAssert.throwIfErrors();
+ },
+ );
+ });
+ },
+);
diff --git a/e2e/specs/assets/defi/view-defi-details.spec.ts b/e2e/specs/assets/defi/view-defi-details.spec.ts
index af53adc21bf..651b0957110 100644
--- a/e2e/specs/assets/defi/view-defi-details.spec.ts
+++ b/e2e/specs/assets/defi/view-defi-details.spec.ts
@@ -5,6 +5,8 @@ import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder';
import { loginToApp } from '../../../viewHelper';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeNetworkAbstractions('View DeFi details'), () => {
it('open the Aave V3 position details', async () => {
@@ -12,8 +14,15 @@ describe(SmokeNetworkAbstractions('View DeFi details'), () => {
{
fixture: new FixtureBuilder().withPopularNetworks().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.defiPositionsWithData],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.defiPositionsWithData;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
languageAndLocale: {
language: 'en',
diff --git a/e2e/specs/assets/defi/view-defi-tab.spec.ts b/e2e/specs/assets/defi/view-defi-tab.spec.ts
index 098b3e485b8..2e20a28998b 100644
--- a/e2e/specs/assets/defi/view-defi-tab.spec.ts
+++ b/e2e/specs/assets/defi/view-defi-tab.spec.ts
@@ -6,6 +6,8 @@ import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
import { WalletViewSelectorsText } from '../../../selectors/wallet/WalletView.selectors';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { loginToApp } from '../../../viewHelper';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(SmokeNetworkAbstractions('View DeFi tab'), () => {
it('open the DeFi tab with an address that has no positions', async () => {
@@ -13,8 +15,15 @@ describe(SmokeNetworkAbstractions('View DeFi tab'), () => {
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.defiPositionsWithNoData],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.defiPositionsWithNoData;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
@@ -42,8 +51,14 @@ describe(SmokeNetworkAbstractions('View DeFi tab'), () => {
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.defiPositionsError],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } = mockEvents.GET.defiPositionsError;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
@@ -75,8 +90,15 @@ describe(SmokeNetworkAbstractions('View DeFi tab'), () => {
{
fixture: new FixtureBuilder().withPopularNetworks().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.defiPositionsWithData],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.defiPositionsWithData;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
languageAndLocale: {
language: 'en',
diff --git a/e2e/specs/assets/import-tokens.spec.ts b/e2e/specs/assets/import-tokens.spec.ts
index 16df48ac79e..6839fd29bdf 100644
--- a/e2e/specs/assets/import-tokens.spec.ts
+++ b/e2e/specs/assets/import-tokens.spec.ts
@@ -86,8 +86,6 @@ describe(Regression('Import Tokens'), () => {
await Assertions.expectElementToBeVisible(
WalletView.tokenInWallet('0 LINK'),
);
- await Assertions.expectElementToBeVisible(
- WalletView.tokenInWallet('0 CNG'),
- );
+ await Assertions.expectTextDisplayed('Change Token');
});
});
diff --git a/e2e/specs/assets/transaction.spec.ts b/e2e/specs/assets/transaction.spec.ts
index d7b9f0d8826..6338a31da58 100644
--- a/e2e/specs/assets/transaction.spec.ts
+++ b/e2e/specs/assets/transaction.spec.ts
@@ -14,6 +14,8 @@ import WalletView from '../../pages/wallet/WalletView';
import TokenOverview from '../../pages/wallet/TokenOverview';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import ToastModal from '../../pages/wallet/ToastModal';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(Regression('Transaction'), () => {
beforeAll(async () => {
@@ -30,8 +32,15 @@ describe(Regression('Transaction'), () => {
{
fixture: new FixtureBuilder().withPopularNetworks().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
diff --git a/e2e/specs/card/card-button.spec.ts b/e2e/specs/card/card-button.spec.ts
index 0787a32f225..a54c4091014 100644
--- a/e2e/specs/card/card-button.spec.ts
+++ b/e2e/specs/card/card-button.spec.ts
@@ -22,8 +22,8 @@ describe(SmokeCard('Card NavBar Button'), () => {
.build(),
restartDevice: true,
testSpecificMock,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
diff --git a/e2e/specs/card/card-home-add-funds.spec.ts b/e2e/specs/card/card-home-add-funds.spec.ts
index d5b1faec07e..29aafd01b5a 100644
--- a/e2e/specs/card/card-home-add-funds.spec.ts
+++ b/e2e/specs/card/card-home-add-funds.spec.ts
@@ -22,8 +22,8 @@ describe(SmokeCard('CardHome - Add Funds'), () => {
.build(),
restartDevice: true,
testSpecificMock,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
diff --git a/e2e/specs/card/card-home-manage-card.spec.ts b/e2e/specs/card/card-home-manage-card.spec.ts
index d411f36bc0e..07e3abe9c4f 100644
--- a/e2e/specs/card/card-home-manage-card.spec.ts
+++ b/e2e/specs/card/card-home-manage-card.spec.ts
@@ -22,8 +22,8 @@ describe(SmokeCard('CardHome - Manage Card'), () => {
.build(),
restartDevice: true,
testSpecificMock,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
diff --git a/e2e/specs/confirmations-redesigned/signatures/alert-system.spec.ts b/e2e/specs/confirmations-redesigned/signatures/alert-system.spec.ts
index 74215a400ca..93a70bdc463 100644
--- a/e2e/specs/confirmations-redesigned/signatures/alert-system.spec.ts
+++ b/e2e/specs/confirmations-redesigned/signatures/alert-system.spec.ts
@@ -12,7 +12,11 @@ import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
import FooterActions from '../../../pages/Browser/Confirmations/FooterActions';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
-import { MockApiEndpoint } from '../../../framework/types';
+import { Mockttp } from 'mockttp';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../../api-mocking/mockHelpers';
const typedSignRequestBody = {
method: 'eth_signTypedData',
@@ -28,10 +32,7 @@ const typedSignRequestBody = {
describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
const runTest = async (
- testSpecificMock: {
- GET?: MockApiEndpoint[];
- POST?: MockApiEndpoint[];
- },
+ testSpecificMock: (mockServer: Mockttp) => Promise,
alertAssertion: () => Promise,
) => {
await withFixtures(
@@ -48,13 +49,7 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
)
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ...(testSpecificMock.GET ?? []),
- ],
- POST: [...(testSpecificMock.POST ?? [])],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
@@ -71,13 +66,32 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
describe('Security Alert API', () => {
it('should sign typed message', async () => {
- const testSpecificMock = {
- POST: [
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ mockEvents.POST.securityAlertApiValidate.urlEndpoint,
+ typedSignRequestBody,
+ mockEvents.POST.securityAlertApiValidate.response,
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
+ statusCode: 201,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
@@ -88,18 +102,28 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
});
it('should show security alert for malicious request, acknowledge and confirm the signature', async () => {
- const testSpecificMock = {
- POST: [
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
+ typedSignRequestBody,
+ {
+ block: 20733277,
+ result_type: 'Malicious',
+ reason: 'malicious_domain',
+ description: `You're interacting with a malicious domain. If you approve this request, you might lose your assets.`,
+ features: [],
+ },
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
- response: {
- block: 20733277,
- result_type: 'Malicious',
- reason: 'malicious_domain',
- description: `You're interacting with a malicious domain. If you approve this request, you might lose your assets.`,
- features: [],
- },
ignoreFields: [
'id',
'jsonrpc',
@@ -108,7 +132,7 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
'traceContext',
],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
@@ -133,28 +157,44 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
});
it('should show security alert for error when validating request fails', async () => {
- const testSpecificMock = {
- GET: [
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
+ response: {
+ message: 'Internal Server Error',
+ },
+ responseCode: 500,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
+ typedSignRequestBody,
{
- urlEndpoint:
- 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
- responseCode: 500,
- response: {
- message: 'Internal Server Error',
- },
+ error: 'Internal Server Error',
+ message: 'An unexpected error occurred on the server.',
},
- ],
- POST: [
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
- response: {
- error: 'Internal Server Error',
- message: 'An unexpected error occurred on the server.',
- },
- responseCode: 500,
+ statusCode: 500,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
@@ -170,6 +210,17 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
describe('Inline Alert', () => {
it('should show mismatch field alert, click the alert, acknowledge and confirm the signature', async () => {
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ };
+
await withFixtures(
{
dapps: [
@@ -182,11 +233,12 @@ describe(SmokeConfirmationsRedesigned('Alert System - Signature'), () => {
.withPermissionControllerConnectedToTestDapp(
buildPermissions(['0xaa36a7']),
)
+ .withPreferencesController({
+ securityAlertsEnabled: true,
+ })
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations],
- },
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/confirmations-redesigned/signatures/signatures.spec.ts b/e2e/specs/confirmations-redesigned/signatures/signatures.spec.ts
index 377e3d5a488..95b65bacca5 100644
--- a/e2e/specs/confirmations-redesigned/signatures/signatures.spec.ts
+++ b/e2e/specs/confirmations-redesigned/signatures/signatures.spec.ts
@@ -12,6 +12,8 @@ import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import RowComponents from '../../../pages/Browser/Confirmations/RowComponents';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
const SIGNATURE_LIST = [
{
@@ -47,8 +49,15 @@ const SIGNATURE_LIST = [
];
describe(SmokeConfirmationsRedesigned('Signature Requests'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/contract-deployment.spec.ts b/e2e/specs/confirmations-redesigned/transactions/contract-deployment.spec.ts
index 86884069dcd..b01656d11e4 100644
--- a/e2e/specs/confirmations-redesigned/transactions/contract-deployment.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/contract-deployment.spec.ts
@@ -13,14 +13,25 @@ import RowComponents from '../../../pages/Browser/Confirmations/RowComponents';
import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations';
import TestDApp from '../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmationsRedesigned('Contract Deployment'), () => {
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/contract-interaction.spec.ts b/e2e/specs/confirmations-redesigned/transactions/contract-interaction.spec.ts
index 08beffede16..af4db4e5b2e 100644
--- a/e2e/specs/confirmations-redesigned/transactions/contract-interaction.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/contract-interaction.spec.ts
@@ -13,18 +13,28 @@ import RowComponents from '../../../pages/Browser/Confirmations/RowComponents';
import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../api-mocking/mock-responses/simulations';
import TestDApp from '../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmationsRedesigned('Contract Interaction'), () => {
const NFT_CONTRACT = SMART_CONTRACTS.NFTS;
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
-
beforeAll(async () => {
jest.setTimeout(2500000);
});
diff --git a/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts b/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts
index 6b86c8d02fb..20dc99f67b0 100644
--- a/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/dapp-initiated-transfer.spec.ts
@@ -18,6 +18,12 @@ import TestDApp from '../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../framework/Constants';
import { EventPayload, getEventsPayloads } from '../../analytics/helpers';
import SoftAssert from '../../../utils/SoftAssert';
+import { Mockttp } from 'mockttp';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../../api-mocking/mockHelpers';
+import Gestures from '../../../framework/Gestures';
const expectedEvents = {
TRANSACTION_ADDED: 'Transaction Added',
@@ -36,14 +42,60 @@ const expectedEventNames = [
];
describe(SmokeConfirmationsRedesigned('DApp Initiated Transfer'), () => {
- const testSpecificMock = {
- POST: [SEND_ETH_SIMULATION_MOCK, mockEvents.POST.segmentTrack],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
- };
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ // Mock gas fees API for Ganache network
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: mockEvents.GET.suggestedGasFeesApiGanache.urlEndpoint,
+ response: mockEvents.GET.suggestedGasFeesApiGanache.response,
+ responseCode: mockEvents.GET.suggestedGasFeesApiGanache.responseCode,
+ });
+
+ // Mock security alerts API for Ganache chain (0x539)
+ await setupMockPostRequest(
+ mockServer,
+ 'https://security-alerts.api.cx.metamask.io/validate/0x539',
+ mockEvents.POST.securityAlertApiValidate.requestBody,
+ mockEvents.POST.securityAlertApiValidate.response,
+ {
+ statusCode: mockEvents.POST.securityAlertApiValidate.responseCode,
+ },
+ );
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+
+ const {
+ urlEndpoint: simulationEndpoint,
+ requestBody,
+ response: simulationResponse,
+ ignoreFields,
+ } = SEND_ETH_SIMULATION_MOCK;
+ await setupMockPostRequest(
+ mockServer,
+ simulationEndpoint,
+ requestBody,
+ simulationResponse,
+ {
+ statusCode: 200,
+ ignoreFields,
+ },
+ );
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ };
let eventsToCheck: EventPayload[];
beforeAll(async () => {
@@ -92,6 +144,12 @@ describe(SmokeConfirmationsRedesigned('DApp Initiated Transfer'), () => {
RowComponents.SimulationDetails,
);
await Assertions.expectElementToBeVisible(RowComponents.GasFeesDetails);
+
+ // Scroll to Advanced Details section on Android
+ if (device.getPlatform() === 'android') {
+ await Gestures.swipe(RowComponents.GasFeesDetails, 'up');
+ }
+
await Assertions.expectElementToBeVisible(
RowComponents.AdvancedDetails,
);
diff --git a/e2e/specs/confirmations-redesigned/transactions/send-max-transfer.spec.ts b/e2e/specs/confirmations-redesigned/transactions/send-max-transfer.spec.ts
index fa244fb9dcc..5d269d46749 100644
--- a/e2e/specs/confirmations-redesigned/transactions/send-max-transfer.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/send-max-transfer.spec.ts
@@ -14,16 +14,50 @@ import TabBarComponent from '../../../pages/wallet/TabBarComponent';
import FooterActions from '../../../pages/Browser/Confirmations/FooterActions';
import SendView from '../../../pages/Send/SendView';
import AmountView from '../../../pages/Send/AmountView';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
const RECIPIENT = '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb';
describe(SmokeConfirmationsRedesigned('Send Max Transfer'), () => {
- const testSpecificMock = {
- POST: [SEND_ETH_SIMULATION_MOCK],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ const {
+ urlEndpoint: simulationEndpoint,
+ requestBody,
+ response: simulationResponse,
+ ignoreFields,
+ } = SEND_ETH_SIMULATION_MOCK;
+
+ await setupMockPostRequest(
+ mockServer,
+ simulationEndpoint,
+ requestBody,
+ simulationResponse,
+ {
+ statusCode: 200,
+ ignoreFields: ignoreFields || [],
+ },
+ );
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/token-approve/approve.spec.ts b/e2e/specs/confirmations-redesigned/transactions/token-approve/approve.spec.ts
index 8cb08b8f5f7..cd1d008e7ed 100644
--- a/e2e/specs/confirmations-redesigned/transactions/token-approve/approve.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/token-approve/approve.spec.ts
@@ -14,17 +14,28 @@ import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApprov
import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations';
import TestDApp from '../../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../../framework/Constants';
+import { setupMockRequest } from '../../../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(SmokeConfirmationsRedesigned('Token Approve - approve method'), () => {
const ERC_20_CONTRACT = SMART_CONTRACTS.HST;
const ERC_721_CONTRACT = SMART_CONTRACTS.NFTS;
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
it('creates an approve transaction confirmation for given ERC 20, changes the spending cap and submits it', async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/token-approve/increase-allowance.spec.ts b/e2e/specs/confirmations-redesigned/transactions/token-approve/increase-allowance.spec.ts
index e344447e1c4..9ee0b9141de 100644
--- a/e2e/specs/confirmations-redesigned/transactions/token-approve/increase-allowance.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/token-approve/increase-allowance.spec.ts
@@ -14,18 +14,29 @@ import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApprov
import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations';
import TestDApp from '../../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../../api-mocking/mockHelpers';
describe(
SmokeConfirmationsRedesigned('Token Approve - increaseAllowance method'),
() => {
const ERC_20_CONTRACT = SMART_CONTRACTS.HST;
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
it('creates an approve transaction confirmation for given ERC 20, changes the spending cap and submits it', async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/token-approve/set-approval-for-all.spec.ts b/e2e/specs/confirmations-redesigned/transactions/token-approve/set-approval-for-all.spec.ts
index 70acb5d4c4c..c35fe9da094 100644
--- a/e2e/specs/confirmations-redesigned/transactions/token-approve/set-approval-for-all.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/token-approve/set-approval-for-all.spec.ts
@@ -14,6 +14,8 @@ import TokenApproveConfirmation from '../../../../pages/Confirmation/TokenApprov
import { SIMULATION_ENABLED_NETWORKS_MOCK } from '../../../../api-mocking/mock-responses/simulations';
import TestDApp from '../../../../pages/Browser/TestDApp';
import { DappVariants } from '../../../../framework/Constants';
+import { setupMockRequest } from '../../../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(
SmokeConfirmationsRedesigned('Token Approve - setApprovalForAll method'),
@@ -21,12 +23,21 @@ describe(
const ERC_721_CONTRACT = SMART_CONTRACTS.NFTS;
const ERC_1155_CONTRACT = SMART_CONTRACTS.ERC1155;
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
it('creates an approve transaction confirmation for given ERC721 and submits it', async () => {
diff --git a/e2e/specs/confirmations-redesigned/transactions/wallet-initiated-transfer.spec.ts b/e2e/specs/confirmations-redesigned/transactions/wallet-initiated-transfer.spec.ts
index 2657e33886e..3fe5c48bea4 100644
--- a/e2e/specs/confirmations-redesigned/transactions/wallet-initiated-transfer.spec.ts
+++ b/e2e/specs/confirmations-redesigned/transactions/wallet-initiated-transfer.spec.ts
@@ -16,17 +16,48 @@ import FooterActions from '../../../pages/Browser/Confirmations/FooterActions';
import SendView from '../../../pages/Send/SendView';
import AmountView from '../../../pages/Send/AmountView';
import RowComponents from '../../../pages/Browser/Confirmations/RowComponents';
+import { Mockttp } from 'mockttp';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../../api-mocking/mockHelpers';
const RECIPIENT = '0x0c54fccd2e384b4bb6f2e405bf5cbc15a017aafb';
const AMOUNT = '1';
describe(SmokeConfirmationsRedesigned('Wallet Initiated Transfer'), () => {
- const testSpecificMock = {
- POST: [SEND_ETH_SIMULATION_MOCK],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ const {
+ urlEndpoint: simulationEndpoint,
+ requestBody,
+ response: simulationResponse,
+ ignoreFields,
+ } = SEND_ETH_SIMULATION_MOCK;
+
+ await setupMockPostRequest(
+ mockServer,
+ simulationEndpoint,
+ requestBody,
+ simulationResponse,
+ {
+ statusCode: 200,
+ ignoreFields,
+ },
+ );
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations/advanced-gas-fees.mock.spec.ts b/e2e/specs/confirmations/advanced-gas-fees.mock.spec.ts
index 229a9010de9..aef70d176e5 100644
--- a/e2e/specs/confirmations/advanced-gas-fees.mock.spec.ts
+++ b/e2e/specs/confirmations/advanced-gas-fees.mock.spec.ts
@@ -10,13 +10,27 @@ import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const VALID_ADDRESS = '0xebe6CcB6B55e1d094d9c58980Bc10Fed69932cAb';
-const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
describe(SmokeConfirmations('Advanced Gas Fees and Priority Tests'), () => {
diff --git a/e2e/specs/confirmations/approve-custom-erc20.spec.ts b/e2e/specs/confirmations/approve-custom-erc20.spec.ts
index 26194a0a3fd..44a0cb181be 100644
--- a/e2e/specs/confirmations/approve-custom-erc20.spec.ts
+++ b/e2e/specs/confirmations/approve-custom-erc20.spec.ts
@@ -12,16 +12,30 @@ import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/Activi
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const HST_CONTRACT = SMART_CONTRACTS.HST;
describe(SmokeConfirmations('ERC20 tokens'), () => {
it('approve custom ERC20 token amount from a dapp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/approve-default-erc20.spec.ts b/e2e/specs/confirmations/approve-default-erc20.spec.ts
index 7295044e90f..d8117175f79 100644
--- a/e2e/specs/confirmations/approve-default-erc20.spec.ts
+++ b/e2e/specs/confirmations/approve-default-erc20.spec.ts
@@ -14,17 +14,31 @@ import TestDApp from '../../pages/Browser/TestDApp';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const HST_CONTRACT = SMART_CONTRACTS.HST;
const EXPECTED_TOKEN_AMOUNT = '7';
describe(SmokeConfirmations('ERC20 tokens'), () => {
it('approve default ERC20 token amount from a dapp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/approve-erc721.spec.ts b/e2e/specs/confirmations/approve-erc721.spec.ts
index f44a8ce730d..9b24150bc53 100644
--- a/e2e/specs/confirmations/approve-erc721.spec.ts
+++ b/e2e/specs/confirmations/approve-erc721.spec.ts
@@ -10,16 +10,30 @@ import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/Activi
import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
describe(SmokeConfirmations('ERC721 tokens'), () => {
const NFT_CONTRACT = SMART_CONTRACTS.NFTS;
it('approve an ERC721 token from a dapp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/batch-transfer-erc1155.spec.ts b/e2e/specs/confirmations/batch-transfer-erc1155.spec.ts
index 15428905ad6..dc5ab3a7aa0 100644
--- a/e2e/specs/confirmations/batch-transfer-erc1155.spec.ts
+++ b/e2e/specs/confirmations/batch-transfer-erc1155.spec.ts
@@ -12,16 +12,30 @@ import ContractApprovalBottomSheet from '../../pages/Browser/ContractApprovalBot
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { DappVariants } from '../../framework/Constants';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
describe(SmokeConfirmations('ERC1155 token'), () => {
const ERC1155_CONTRACT = SMART_CONTRACTS.ERC1155;
it('batch transfer ERC1155 tokens', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/increase-allowance-erc20.spec.ts b/e2e/specs/confirmations/increase-allowance-erc20.spec.ts
index b9227e69338..db1b0cba3f1 100644
--- a/e2e/specs/confirmations/increase-allowance-erc20.spec.ts
+++ b/e2e/specs/confirmations/increase-allowance-erc20.spec.ts
@@ -11,16 +11,30 @@ import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/Activi
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const HST_CONTRACT = SMART_CONTRACTS.HST;
describe(SmokeConfirmations('ERC20 - Increase Allowance'), () => {
it('from a dApp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/security-alert-send-eth.spec.ts b/e2e/specs/confirmations/security-alert-send-eth.spec.ts
deleted file mode 100644
index bc78e19f2bc..00000000000
--- a/e2e/specs/confirmations/security-alert-send-eth.spec.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import AmountView from '../../pages/Send/AmountView';
-import SendView from '../../pages/Send/SendView';
-import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView';
-import { loginToApp } from '../../viewHelper';
-import WalletView from '../../pages/wallet/WalletView';
-import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import { withFixtures } from '../../framework/fixtures/FixtureHelper';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import Assertions from '../../framework/Assertions';
-import { SmokeConfirmations } from '../../tags';
-import { MockApiEndpoint } from '../../framework/types';
-
-describe(SmokeConfirmations('Security Alert API - Send flow'), () => {
- const BENIGN_ADDRESS_MOCK = '0x50587E46C5B96a3F6f9792922EC647F13E6EFAE4';
-
- const defaultFixture = new FixtureBuilder().withGanacheNetwork().build();
-
- const navigateToSendConfirmation = async () => {
- await loginToApp();
- await WalletView.tapWalletSendButton();
- await SendView.inputAddress(BENIGN_ADDRESS_MOCK);
- await SendView.tapNextButton();
- await AmountView.typeInTransactionAmount('0');
- await AmountView.tapNextButton();
- };
-
- const runTest = async (
- testSpecificMock: {
- GET?: MockApiEndpoint[];
- POST?: MockApiEndpoint[];
- },
- alertAssertion: () => Promise,
- ) => {
- await withFixtures(
- {
- fixture: defaultFixture,
- restartDevice: true,
- testSpecificMock,
- },
- async () => {
- await navigateToSendConfirmation();
- await alertAssertion();
- },
- );
- };
-
- it('should not show security alerts for benign requests', async () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
- POST: [
- {
- ...mockEvents.POST.securityAlertApiValidate,
- urlEndpoint:
- 'https://security-alerts.api.cx.metamask.io/validate/0x539',
- },
- ],
- };
-
- await runTest(testSpecificMock, async () => {
- try {
- await Assertions.expectElementToNotBeVisible(
- TransactionConfirmationView.securityAlertBanner,
- );
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log('The banner alert is not visible');
- }
- });
- });
-
- it('should show security alerts for malicious request', async () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
- POST: [
- {
- ...mockEvents.POST.securityAlertApiValidate,
- urlEndpoint:
- 'https://security-alerts.api.cx.metamask.io/validate/0x539',
- response: {
- block: 20733277,
- result_type: 'Malicious',
- reason: 'transfer_farming',
- description: '',
- features: ['Interaction with a known malicious address'],
- },
- },
- ],
- };
-
- await runTest(testSpecificMock, async () => {
- await Assertions.expectElementToBeVisible(
- TransactionConfirmationView.securityAlertBanner,
- );
- });
- });
-
- it('should show security alerts for error when validating request fails', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- {
- urlEndpoint:
- 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
- responseCode: 500,
- response: {
- message: 'Internal Server Error',
- },
- },
- ],
- POST: [
- {
- ...mockEvents.POST.securityAlertApiValidate,
- urlEndpoint:
- 'https://security-alerts.api.cx.metamask.io/validate/0x539',
- response: {
- error: 'Internal Server Error',
- message: 'An unexpected error occurred on the server.',
- },
- responseCode: 500,
- },
- ],
- };
-
- await runTest(testSpecificMock, async () => {
- await Assertions.expectElementToBeVisible(
- TransactionConfirmationView.securityAlertResponseFailedBanner,
- );
- });
- });
-});
diff --git a/e2e/specs/confirmations/send-erc20-with-dapp.spec.ts b/e2e/specs/confirmations/send-erc20-with-dapp.spec.ts
index 22f234b29ff..ddd4e17611e 100644
--- a/e2e/specs/confirmations/send-erc20-with-dapp.spec.ts
+++ b/e2e/specs/confirmations/send-erc20-with-dapp.spec.ts
@@ -14,16 +14,30 @@ import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const HST_CONTRACT = SMART_CONTRACTS.HST;
describe(SmokeConfirmations('ERC20 tokens'), () => {
it('send an ERC20 token from a dapp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/send-erc721.spec.ts b/e2e/specs/confirmations/send-erc721.spec.ts
index c40dbfbc1a9..720d563682a 100644
--- a/e2e/specs/confirmations/send-erc721.spec.ts
+++ b/e2e/specs/confirmations/send-erc721.spec.ts
@@ -10,16 +10,30 @@ import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
describe(SmokeConfirmations('ERC721 tokens'), () => {
const NFT_CONTRACT = SMART_CONTRACTS.NFTS;
it('send an ERC721 token from a dapp', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/send-failing-contract.spec.ts b/e2e/specs/confirmations/send-failing-contract.spec.ts
index 8df26ced7ef..249126a3716 100644
--- a/e2e/specs/confirmations/send-failing-contract.spec.ts
+++ b/e2e/specs/confirmations/send-failing-contract.spec.ts
@@ -10,17 +10,32 @@ import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Failing contracts'), () => {
const FAILING_CONTRACT = SMART_CONTRACTS.FAILING;
it('sends a failing contract transaction', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
+
await withFixtures(
{
dapps: [
diff --git a/e2e/specs/confirmations/send-to-contract-address.spec.ts b/e2e/specs/confirmations/send-to-contract-address.spec.ts
index 3b025cd325d..7d50a12ac10 100644
--- a/e2e/specs/confirmations/send-to-contract-address.spec.ts
+++ b/e2e/specs/confirmations/send-to-contract-address.spec.ts
@@ -12,6 +12,8 @@ import TabBarComponent from '../../pages/wallet/TabBarComponent';
import Assertions from '../../framework/Assertions';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const HST_CONTRACT = SMART_CONTRACTS.HST;
@@ -19,8 +21,15 @@ describe(SmokeConfirmations('Send to contract address'), () => {
it('should send ETH to a contract from inside the wallet', async () => {
const AMOUNT = '12';
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.ts b/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.ts
index b4d6d782265..b584450779c 100644
--- a/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.ts
+++ b/e2e/specs/confirmations/set-approval-for-all-erc1155.spec.ts
@@ -12,16 +12,30 @@ import ContractApprovalBottomSheet from '../../pages/Browser/ContractApprovalBot
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
describe(SmokeConfirmations('ERC1155 token'), () => {
const ERC1155_CONTRACT = SMART_CONTRACTS.ERC1155;
it('approve all ERC1155 tokens', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/set-approve-for-all-erc721.spec.ts b/e2e/specs/confirmations/set-approve-for-all-erc721.spec.ts
index f14e9497959..aff104d348f 100644
--- a/e2e/specs/confirmations/set-approve-for-all-erc721.spec.ts
+++ b/e2e/specs/confirmations/set-approve-for-all-erc721.spec.ts
@@ -12,16 +12,30 @@ import ContractApprovalBottomSheet from '../../pages/Browser/ContractApprovalBot
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../framework/Constants';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
describe(SmokeConfirmations('ERC721 token'), () => {
const NFT_CONTRACT = SMART_CONTRACTS.NFTS;
it('approve all ERC721 tokens', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.suggestedGasFeesApiGanache,
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ const { urlEndpoint: gasUrlEndpoint, response: gasResponse } =
+ mockEvents.GET.suggestedGasFeesApiGanache;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: gasUrlEndpoint,
+ response: gasResponse,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/signatures/ethereum-sign.spec.ts b/e2e/specs/confirmations/signatures/ethereum-sign.spec.ts
index e062e3ff0a5..607d4e07c36 100644
--- a/e2e/specs/confirmations/signatures/ethereum-sign.spec.ts
+++ b/e2e/specs/confirmations/signatures/ethereum-sign.spec.ts
@@ -10,11 +10,20 @@ import Assertions from '../../../framework/Assertions';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Ethereum Sign'), () => {
it('Sign in with Ethereum', async () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
await withFixtures(
diff --git a/e2e/specs/confirmations/signatures/personal-sign.spec.ts b/e2e/specs/confirmations/signatures/personal-sign.spec.ts
index 2c97e1662da..8e00ceacc44 100644
--- a/e2e/specs/confirmations/signatures/personal-sign.spec.ts
+++ b/e2e/specs/confirmations/signatures/personal-sign.spec.ts
@@ -10,10 +10,19 @@ import Assertions from '../../../framework/Assertions';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Personal Sign'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.ts b/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.ts
index 3a9d59fef4c..f3bbeb255d4 100644
--- a/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.ts
+++ b/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.ts
@@ -1,41 +1,37 @@
+import Assertions from '../../../framework/Assertions';
import Browser from '../../../pages/Browser/BrowserView';
+import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder';
+import RequestTypes from '../../../pages/Browser/Confirmations/RequestTypes';
+import AlertSystem from '../../../pages/Browser/Confirmations/AlertSystem';
import TabBarComponent from '../../../pages/wallet/TabBarComponent';
-import { loginToApp } from '../../../viewHelper';
-import SigningBottomSheet from '../../../pages/Browser/SigningBottomSheet';
import TestDApp from '../../../pages/Browser/TestDApp';
-import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder';
-import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
-import Assertions from '../../../framework/Assertions';
+import { loginToApp } from '../../../viewHelper';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
-import ConfirmationView from '../../../pages/Confirmation/ConfirmationView';
-import { SmokeConfirmations } from '../../../tags';
+import { SmokeConfirmationsRedesigned } from '../../../tags';
+import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
-import { MockApiEndpoint } from '../../../framework/types';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../../api-mocking/mockHelpers';
-describe(SmokeConfirmations('Security Alert API - Signature'), () => {
- beforeAll(async () => {
- jest.setTimeout(2500000);
- });
-
- const defaultFixture = new FixtureBuilder()
- .withSepoliaNetwork()
- .withPermissionControllerConnectedToTestDapp(buildPermissions(['0xaa36a7']))
- .build();
-
- const navigateToTestDAppAndTapTypedSignButton = async () => {
- await loginToApp();
- await TabBarComponent.tapBrowser();
- await Browser.navigateToTestDApp();
- await TestDApp.tapTypedSignButton();
- await Assertions.expectElementToBeVisible(SigningBottomSheet.typedRequest);
- };
+const typedSignRequestBody = {
+ method: 'eth_signTypedData',
+ params: [
+ [
+ { type: 'string', name: 'Message', value: 'Hi, Alice!' },
+ { type: 'uint32', name: 'A number', value: '1337' },
+ ],
+ '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
+ ],
+ origin: 'localhost',
+};
+describe(SmokeConfirmationsRedesigned('Security Alert API - Signature'), () => {
const runTest = async (
- testSpecificMock: {
- GET?: MockApiEndpoint[];
- POST?: MockApiEndpoint[];
- },
+ testSpecificMock: (mockServer: Mockttp) => Promise,
alertAssertion: () => Promise,
) => {
await withFixtures(
@@ -45,61 +41,87 @@ describe(SmokeConfirmations('Security Alert API - Signature'), () => {
dappVariant: DappVariants.TEST_DAPP,
},
],
- fixture: defaultFixture,
+ fixture: new FixtureBuilder()
+ .withSepoliaNetwork()
+ .withPermissionControllerConnectedToTestDapp(
+ buildPermissions(['0xaa36a7']),
+ )
+ .build(),
restartDevice: true,
testSpecificMock,
},
async () => {
- await navigateToTestDAppAndTapTypedSignButton();
+ await loginToApp();
+ await TabBarComponent.tapBrowser();
+ await Browser.navigateToTestDApp();
+ await TestDApp.tapTypedSignButton();
+ await Assertions.expectElementToBeVisible(
+ RequestTypes.TypedSignRequest,
+ );
await alertAssertion();
},
);
};
- const typedSignRequestBody = {
- method: 'eth_signTypedData',
- params: [
- [
- { type: 'string', name: 'Message', value: 'Hi, Alice!' },
- { type: 'uint32', name: 'A number', value: '1337' },
- ],
- '0x76cf1CdD1fcC252442b50D6e97207228aA4aefC3',
- ],
- origin: 'localhost',
- };
-
it('should sign typed message', async () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
- POST: [
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ mockEvents.POST.securityAlertApiValidate.urlEndpoint,
+ typedSignRequestBody,
+ mockEvents.POST.securityAlertApiValidate.response,
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
+ statusCode: 201,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
await Assertions.expectElementToNotBeVisible(
- ConfirmationView.securityAlertBanner,
+ AlertSystem.securityAlertBanner,
);
});
});
it('should show security alert for malicious request', async () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
- POST: [
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
+ typedSignRequestBody,
+ {
+ block: 20733277,
+ result_type: 'Malicious',
+ reason: 'malicious_domain',
+ description: `You're interacting with a malicious domain. If you approve this request, you might lose your assets.`,
+ features: [],
+ },
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
- response: {
- block: 20733277,
- result_type: 'Malicious',
- reason: 'malicious_domain',
- description: `You're interacting with a malicious domain. If you approve this request, you might lose your assets.`,
- features: [],
- },
ignoreFields: [
'id',
'jsonrpc',
@@ -108,45 +130,66 @@ describe(SmokeConfirmations('Security Alert API - Signature'), () => {
'traceContext',
],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
await Assertions.expectElementToBeVisible(
- ConfirmationView.securityAlertBanner,
+ AlertSystem.securityAlertBanner,
+ );
+ await Assertions.expectElementToBeVisible(
+ AlertSystem.securityAlertResponseMaliciousBanner,
);
});
});
it('should show security alert for error when validating request fails', async () => {
- const testSpecificMock = {
- GET: [
- mockEvents.GET.remoteFeatureFlagsOldConfirmations,
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
+ response: {
+ message: 'Internal Server Error',
+ },
+ responseCode: 500,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
+ typedSignRequestBody,
{
- urlEndpoint:
- 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
- responseCode: 500,
- response: {
- message: 'Internal Server Error',
- },
+ error: 'Internal Server Error',
+ message: 'An unexpected error occurred on the server.',
},
- ],
- POST: [
{
- ...mockEvents.POST.securityAlertApiValidate,
- requestBody: typedSignRequestBody,
- response: {
- error: 'Internal Server Error',
- message: 'An unexpected error occurred on the server.',
- },
- responseCode: 500,
+ statusCode: 500,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
},
- ],
+ );
};
await runTest(testSpecificMock, async () => {
await Assertions.expectElementToBeVisible(
- ConfirmationView.securityAlertResponseFailedBanner,
+ AlertSystem.securityAlertBanner,
+ );
+ await Assertions.expectElementToBeVisible(
+ AlertSystem.securityAlertResponseFailedBanner,
);
});
});
diff --git a/e2e/specs/confirmations/signatures/typed-sign-v3.spec.ts b/e2e/specs/confirmations/signatures/typed-sign-v3.spec.ts
index 58993156471..b599ee8ecad 100644
--- a/e2e/specs/confirmations/signatures/typed-sign-v3.spec.ts
+++ b/e2e/specs/confirmations/signatures/typed-sign-v3.spec.ts
@@ -10,10 +10,19 @@ import Assertions from '../../../framework/Assertions';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Typed Sign V3'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations/signatures/typed-sign-v4.spec.ts b/e2e/specs/confirmations/signatures/typed-sign-v4.spec.ts
index b0385b55309..1d273877815 100644
--- a/e2e/specs/confirmations/signatures/typed-sign-v4.spec.ts
+++ b/e2e/specs/confirmations/signatures/typed-sign-v4.spec.ts
@@ -10,10 +10,19 @@ import Assertions from '../../../framework/Assertions';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Typed Sign V4'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/confirmations/signatures/typed-sign.spec.ts b/e2e/specs/confirmations/signatures/typed-sign.spec.ts
index 5bbc9744aa8..fffcde80f94 100644
--- a/e2e/specs/confirmations/signatures/typed-sign.spec.ts
+++ b/e2e/specs/confirmations/signatures/typed-sign.spec.ts
@@ -10,10 +10,19 @@ import Assertions from '../../../framework/Assertions';
import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
import { DappVariants } from '../../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
describe(SmokeConfirmations('Typed Sign'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
beforeAll(async () => {
diff --git a/e2e/specs/identity/account-syncing/multi-srp.spec.ts b/e2e/specs/identity/account-syncing/multi-srp.spec.ts
index 9d38d3928ae..a07f26859d7 100644
--- a/e2e/specs/identity/account-syncing/multi-srp.spec.ts
+++ b/e2e/specs/identity/account-syncing/multi-srp.spec.ts
@@ -90,6 +90,10 @@ describe(SmokeIdentity('Account syncing - Mutiple SRPs'), () => {
await waitUntilSyncedAccountsNumberEquals(3);
await Assertions.expectElementToBeVisible(WalletView.container);
+ const secretPhraseImportedText = 'Secret Recovery Phrase 2 imported';
+ // Waiting for toast notification to appear and disappear
+ await Assertions.expectTextDisplayed(secretPhraseImportedText);
+ await Assertions.expectTextNotDisplayed(secretPhraseImportedText);
// Create second account for SRP 2
await WalletView.tapIdenticon();
@@ -112,6 +116,7 @@ describe(SmokeIdentity('Account syncing - Mutiple SRPs'), () => {
},
);
await device.enableSynchronization();
+ await waitUntilEventsEmittedNumberEquals(6);
},
);
diff --git a/e2e/specs/identity/utils/helpers.ts b/e2e/specs/identity/utils/helpers.ts
index 7afc553e12b..47798a48040 100644
--- a/e2e/specs/identity/utils/helpers.ts
+++ b/e2e/specs/identity/utils/helpers.ts
@@ -84,7 +84,7 @@ export const arrangeTestUtils = (
clearInterval(ids.interval);
reject(
new Error(
- `Timeout waiting for event ${event} to be emitted ${expectedNumber} times`,
+ `Timeout waiting for event ${event} to be emitted ${expectedNumber} times\n Actual: ${counter}`,
),
);
}, BASE_TIMEOUT);
diff --git a/e2e/specs/identity/utils/withIdentityFixtures.ts b/e2e/specs/identity/utils/withIdentityFixtures.ts
index 6142fc67747..1c9ced19c2e 100644
--- a/e2e/specs/identity/utils/withIdentityFixtures.ts
+++ b/e2e/specs/identity/utils/withIdentityFixtures.ts
@@ -11,12 +11,10 @@ import {
UserStorageMockttpControllerOverrides,
} from './user-storage/userStorageMockttpController';
import { Mockttp } from 'mockttp';
-import { TestSpecificMock } from '../../../framework/types';
export interface IdentityFixtureOptions {
fixture?: object;
restartDevice?: boolean;
- testSpecificMock?: TestSpecificMock;
userStorageFeatures?: (keyof typeof pathRegexps)[];
userStorageOverrides?: Partial<
Record
@@ -37,7 +35,6 @@ export async function withIdentityFixtures(
const {
fixture = new FixtureBuilder().withBackupAndSyncSettings().build(),
restartDevice = true,
- testSpecificMock,
mockBalancesAccounts = [],
userStorageFeatures = [
USER_STORAGE_FEATURE_NAMES.accounts,
@@ -47,6 +44,25 @@ export async function withIdentityFixtures(
sharedUserStorageController,
} = options;
+ let userStorageController: UserStorageMockttpController;
+
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ if (mockBalancesAccounts.length > 0) {
+ await setupAccountMockedBalances(mockServer, mockBalancesAccounts);
+ }
+
+ if (sharedUserStorageController) {
+ userStorageController = sharedUserStorageController;
+ } else {
+ userStorageController = createUserStorageController();
+ }
+
+ for (const feature of userStorageFeatures) {
+ const overrides = userStorageOverrides?.[feature] || {};
+ await userStorageController.setupPath(feature, mockServer, overrides);
+ }
+ };
+
await withFixtures(
{
fixture,
@@ -57,22 +73,6 @@ export async function withIdentityFixtures(
if (!mockServer) {
throw new Error('Mock server is not defined');
}
- if (mockBalancesAccounts.length > 0) {
- await setupAccountMockedBalances(mockServer, mockBalancesAccounts);
- }
-
- let userStorageController: UserStorageMockttpController;
-
- if (sharedUserStorageController) {
- userStorageController = sharedUserStorageController;
- } else {
- userStorageController = createUserStorageController();
- }
-
- for (const feature of userStorageFeatures) {
- const overrides = userStorageOverrides?.[feature] || {};
- await userStorageController.setupPath(feature, mockServer, overrides);
- }
await testFn({
mockServer,
diff --git a/e2e/specs/multichain-accounts/common.ts b/e2e/specs/multichain-accounts/common.ts
index d6a98987aad..089f26aad05 100644
--- a/e2e/specs/multichain-accounts/common.ts
+++ b/e2e/specs/multichain-accounts/common.ts
@@ -1,3 +1,4 @@
+import { Mockttp } from 'mockttp';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import FixtureBuilder, {
DEFAULT_FIXTURE_ACCOUNT_CHECKSUM,
@@ -6,6 +7,7 @@ import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet';
import WalletView from '../../pages/wallet/WalletView';
import { loginToApp } from '../../viewHelper';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
export interface Account {
name: string;
@@ -32,8 +34,15 @@ export const goToAccountDetails = async (account: Account) => {
export const withMultichainAccountDetailsEnabled = async (
testFn: () => Promise,
) => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails()],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureMultichainAccountsAccountDetails();
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
return await withFixtures(
{
diff --git a/e2e/specs/notifications/enable-notifications-after-onboarding.spec.ts b/e2e/specs/notifications/enable-notifications-after-onboarding.spec.ts
index 6f4278eb15e..07f2e26b032 100644
--- a/e2e/specs/notifications/enable-notifications-after-onboarding.spec.ts
+++ b/e2e/specs/notifications/enable-notifications-after-onboarding.spec.ts
@@ -7,23 +7,13 @@ import { loginToApp } from '../../viewHelper';
import {
getMockFeatureAnnouncementItemId,
getMockWalletNotificationItemIds,
- mockNotificationServices,
} from './utils/mocks';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
-import { startMockServer } from '../../api-mocking/mock-server';
-import { Mockttp } from 'mockttp';
-import { DEFAULT_MOCKS } from '../../api-mocking/mock-responses/defaults';
describe(SmokeNetworkAbstractions('Notification Onboarding'), () => {
- let mockServer: Mockttp;
-
beforeAll(async () => {
jest.setTimeout(170000);
- const mockServerPort = getMockServerPort();
- mockServer = await startMockServer(DEFAULT_MOCKS, mockServerPort);
- await mockNotificationServices(mockServer);
});
it('should enable notifications and view feature announcements and wallet notifications', async () => {
@@ -33,7 +23,6 @@ describe(SmokeNetworkAbstractions('Notification Onboarding'), () => {
{
fixture: new FixtureBuilder().withBackupAndSyncSettings().build(),
restartDevice: true,
- mockServerInstance: mockServer,
permissions: {
notifications: 'YES',
},
diff --git a/e2e/specs/notifications/notification-settings-flow.spec.ts b/e2e/specs/notifications/notification-settings-flow.spec.ts
index ed756631aa0..7b53402ae77 100644
--- a/e2e/specs/notifications/notification-settings-flow.spec.ts
+++ b/e2e/specs/notifications/notification-settings-flow.spec.ts
@@ -1,7 +1,5 @@
-import type { Mockttp } from 'mockttp';
import { SmokeNetworkAbstractions } from '../../tags';
import Assertions from '../../framework/Assertions';
-import { mockNotificationServices } from './utils/mocks';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import FixtureBuilder, {
DEFAULT_FIXTURE_ACCOUNT_CHECKSUM,
@@ -10,18 +8,10 @@ import { loginToApp } from '../../viewHelper';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import SettingsView from '../../pages/Settings/SettingsView';
import NotificationSettingsView from '../../pages/Notifications/NotificationSettingsView';
-import { startMockServer } from '../../api-mocking/mock-server';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
-import { DEFAULT_MOCKS } from '../../api-mocking/mock-responses/defaults';
describe(SmokeNetworkAbstractions('Notification Onboarding'), () => {
- let mockServer: Mockttp;
-
beforeAll(async () => {
jest.setTimeout(170000);
- const mockServerPort = getMockServerPort();
- mockServer = await startMockServer(DEFAULT_MOCKS, mockServerPort);
- await mockNotificationServices(mockServer);
});
it('should enable notifications and toggle feature announcements and account notifications', async () => {
@@ -29,7 +19,6 @@ describe(SmokeNetworkAbstractions('Notification Onboarding'), () => {
{
fixture: new FixtureBuilder().withBackupAndSyncSettings().build(),
restartDevice: true,
- mockServerInstance: mockServer,
permissions: {
notifications: 'YES',
},
diff --git a/e2e/specs/notifications/utils/mocks.ts b/e2e/specs/notifications/utils/mocks.ts
index 40a8982561d..c5b0c37b2d1 100644
--- a/e2e/specs/notifications/utils/mocks.ts
+++ b/e2e/specs/notifications/utils/mocks.ts
@@ -27,6 +27,7 @@ import {
import { getDecodedProxiedURL } from './helpers';
import { MockttpNotificationTriggerServer } from './mock-notification-trigger-server';
import { mockAuthServices } from '../../identity/utils/mocks';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
export const mockListNotificationsResponse = getMockListNotificationsResponse();
mockListNotificationsResponse.response = [
@@ -82,8 +83,17 @@ export async function mockNotificationServices(server: Mockttp) {
// Trigger Config
await new MockttpNotificationTriggerServer().setupServer(server);
+ const contentfulUrlRegex =
+ /^https:\/\/cdn\.contentful\.com:443\/spaces\/[a-zA-Z0-9]+\/environments\/[a-zA-Z0-9]+\/entries\?.*$/;
+
// Notifications
await mockAPICall(server, mockFeatureAnnouncementResponse);
+ await setupMockRequest(server, {
+ url: contentfulUrlRegex,
+ requestMethod: 'GET',
+ response: mockFeatureAnnouncementResponse.response,
+ responseCode: 200,
+ });
await mockAPICall(server, mockListNotificationsResponse);
await mockAPICall(server, getMockMarkNotificationsAsReadResponse());
@@ -99,7 +109,7 @@ interface ResponseParam {
response: unknown;
}
-async function mockAPICall(server: Mockttp, response: ResponseParam) {
+export async function mockAPICall(server: Mockttp, response: ResponseParam) {
let requestRuleBuilder;
if (response.requestMethod === 'GET') {
diff --git a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.ts b/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.ts
deleted file mode 100644
index 0dfbe171520..00000000000
--- a/e2e/specs/onboarding/onboarding-wizard-opt-in.spec.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import TestHelpers from '../../helpers';
-import { Regression } from '../../tags';
-import WalletView from '../../pages/wallet/WalletView';
-import SettingsView from '../../pages/Settings/SettingsView';
-import SecurityAndPrivacy from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView';
-import LoginView from '../../pages/wallet/LoginView';
-import { CreateNewWallet } from '../../viewHelper';
-import TabBarComponent from '../../pages/wallet/TabBarComponent';
-import CommonView from '../../pages/CommonView';
-import Assertions from '../../framework/Assertions';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import {
- getEventsPayloads,
- onboardingEvents,
- filterEvents,
- EventPayload,
-} from '../analytics/helpers';
-import SoftAssert from '../../utils/SoftAssert';
-import { MockttpServer } from 'mockttp';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
-import { startMockServer } from '../../api-mocking/mock-server';
-import Utilities from '../../utils/Utilities';
-
-const PASSWORD = '12345678';
-
-const testSpecificMock = {
- POST: [mockEvents.POST.segmentTrack],
-};
-
-describe(
- Regression('Regression - metametrics opt out from settings WITH ANALYTICS'),
- () => {
- let mockServer: MockttpServer;
- let eventsBeforeDisablingAnalytics: EventPayload[];
-
- beforeAll(async () => {
- jest.setTimeout(150000);
- await TestHelpers.reverseServerPort();
-
- const mockServerPort = getMockServerPort();
- mockServer = await startMockServer(testSpecificMock, mockServerPort);
-
- await TestHelpers.launchApp({
- permissions: { notifications: 'YES' },
- launchArgs: {
- mockServerPort: `${mockServerPort}`,
- },
- });
- });
-
- afterAll(async () => {
- if (mockServer) {
- await mockServer.stop();
- }
- });
-
- it('should create a new wallet with analytics opt-in', async () => {
- await CreateNewWallet({ optInToMetrics: true });
- });
-
- it('should check that metametrics is enabled in settings', async () => {
- await TestHelpers.delay(3000); // Wait for UI to stabilize
- await TabBarComponent.tapSettings();
- await TestHelpers.delay(1500); // Wait for settings to load
- await SettingsView.tapSecurityAndPrivacy();
- await SecurityAndPrivacy.scrollToMetaMetrics();
- await TestHelpers.delay(1500);
- await Assertions.expectToggleToBeOn(
- SecurityAndPrivacy.metaMetricsToggle as Promise,
- );
- });
-
- it('should disable metametrics and track preference change', async () => {
- await SecurityAndPrivacy.tapMetaMetricsToggle();
- await TestHelpers.delay(1000); // Wait for toggle action
- await CommonView.tapOkAlert();
- await Assertions.expectToggleToBeOff(
- SecurityAndPrivacy.metaMetricsToggle as Promise,
- );
-
- const events = await getEventsPayloads(mockServer);
-
- const softAssert = new SoftAssert();
- await softAssert.checkAndCollect(async () => {
- const e = filterEvents(
- events,
- onboardingEvents.ANALYTICS_PREFERENCE_SELECTED,
- ) as EventPayload[];
- await Assertions.checkIfValueIsDefined(e);
- await Assertions.checkIfArrayHasLength(e, 1);
- await Assertions.checkIfObjectContains(e[0].properties, {
- has_marketing_consent: false,
- is_metrics_opted_in: true,
- location: 'onboarding_metametrics',
- updated_after_onboarding: false,
- });
- }, 'Analytics Preference Selected (opt-in) was tracked during onboarding and is not tracked after disabling analytics');
-
- softAssert.throwIfErrors();
-
- // Store events before terminating app to verify no new events are sent after relaunch
- eventsBeforeDisablingAnalytics = events;
-
- // Terminating the app for the next test
- await device.terminateApp();
- await TestHelpers.delay(1500); // Wait for app termination
- });
-
- it('should relaunch and log in, verifying no new MetaMetrics events were sent', async () => {
- // Launch the app for this test
- await device.launchApp({
- launchArgs: {
- mockServerPort: `${getMockServerPort()}`,
- detoxURLBlacklistRegex: Utilities.BlacklistURLs,
- },
- });
- await TestHelpers.delay(2000); // Wait for app launch
-
- await LoginView.enterPassword(PASSWORD);
- await Assertions.expectElementToBeVisible(WalletView.container);
- // Removed delay - we already wait for wallet view to be visible
-
- const eventsAfterRelaunch = await getEventsPayloads(mockServer);
- await Assertions.checkIfArrayHasLength(
- eventsAfterRelaunch,
- eventsBeforeDisablingAnalytics.length,
- );
- });
-
- it('should verify metametrics remains turned off after app restart', async () => {
- await device.disableSynchronization();
- await TestHelpers.delay(500); // Wait for UI to stabilize
- await TabBarComponent.tapSettings();
- await TestHelpers.delay(500); // Wait for settings to load
- await SettingsView.tapSecurityAndPrivacy(); // ANIMATION HERE, we need to be careful with this
-
- await TestHelpers.delay(500); // Wait for animation
-
- await SecurityAndPrivacy.scrollToMetaMetrics();
- await Assertions.expectToggleToBeOff(
- SecurityAndPrivacy.metaMetricsToggle as Promise,
- );
-
- await device.enableSynchronization();
- });
- },
-);
diff --git a/e2e/specs/quarantine/asset-sort.failing.ts b/e2e/specs/quarantine/asset-sort.failing.ts
index c07d07f45a9..9fa44b67cef 100644
--- a/e2e/specs/quarantine/asset-sort.failing.ts
+++ b/e2e/specs/quarantine/asset-sort.failing.ts
@@ -14,6 +14,8 @@ import ImportTokensView from '../../pages/wallet/ImportTokenFlow/ImportTokensVie
import TestHelpers from '../../helpers';
import { getFixturesServerPort } from '../../framework/fixtures/FixtureUtils';
import { MockApiEndpoint } from '../../framework';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const AAVE_TENDERLY_MAINNET_DETAILS = {
address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9',
@@ -82,8 +84,13 @@ describe(SmokeNetworkAbstractions('Import Tokens'), () => {
.withNetworkController(CustomNetworks.Tenderly.Mainnet)
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [TOKEN_RESPONSE],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: TOKEN_RESPONSE.urlEndpoint,
+ response: TOKEN_RESPONSE.response,
+ responseCode: 200,
+ });
},
},
async () => {
@@ -113,8 +120,13 @@ describe(SmokeNetworkAbstractions('Import Tokens'), () => {
.withMetaMetricsOptIn()
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [TOKEN_RESPONSE],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: TOKEN_RESPONSE.urlEndpoint,
+ response: TOKEN_RESPONSE.response,
+ responseCode: 200,
+ });
},
},
async () => {
@@ -151,8 +163,13 @@ describe(SmokeNetworkAbstractions('Import Tokens'), () => {
.withTokens([AAVE_TENDERLY_MAINNET_DETAILS])
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [TOKEN_RESPONSE],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: TOKEN_RESPONSE.urlEndpoint,
+ response: TOKEN_RESPONSE.response,
+ responseCode: 200,
+ });
},
},
async () => {
diff --git a/e2e/specs/quarantine/batch-transaction.failing.ts b/e2e/specs/quarantine/batch-transaction.failing.ts
index 7f4af34dc18..a4d6e73d661 100644
--- a/e2e/specs/quarantine/batch-transaction.failing.ts
+++ b/e2e/specs/quarantine/batch-transaction.failing.ts
@@ -20,6 +20,8 @@ import { SmokeConfirmationsRedesigned } from '../../tags';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import { DappVariants } from '../../framework/Constants';
import { AnvilNodeOptions, LocalNodeType } from '../../framework';
+import { Mockttp } from 'mockttp';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
const LOCAL_CHAIN_NAME = 'Localhost';
@@ -65,14 +67,22 @@ async function connectTestDappToLocalhost() {
}
describe(SmokeConfirmationsRedesigned('7702 - smart account'), () => {
- const testSpecificMock = {
- POST: [],
- GET: [
- SIMULATION_ENABLED_NETWORKS_MOCK,
- mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations,
- ],
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: SIMULATION_ENABLED_NETWORKS_MOCK.urlEndpoint,
+ response: SIMULATION_ENABLED_NETWORKS_MOCK.response,
+ responseCode: 200,
+ });
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
-
beforeAll(async () => {
jest.setTimeout(2500000);
});
diff --git a/e2e/specs/quarantine/multichain/wallet-invokeMethod.failing.ts b/e2e/specs/quarantine/multichain/wallet-invokeMethod.failing.ts
index da800e849ba..549e028e4e7 100644
--- a/e2e/specs/quarantine/multichain/wallet-invokeMethod.failing.ts
+++ b/e2e/specs/quarantine/multichain/wallet-invokeMethod.failing.ts
@@ -33,6 +33,8 @@ import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
import { DappVariants } from '../../../framework/Constants';
import { LocalNodeType } from '../../../framework';
import { AnvilNodeOptions } from '../../../framework/types';
+import { setupMockRequest } from '../../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
const ANVIL_NODE_OPTIONS_WITH_GATOR = [
{
@@ -43,8 +45,14 @@ const ANVIL_NODE_OPTIONS_WITH_GATOR = [
},
},
];
-const REMOTE_FEATURE_EIP_7702_MOCK = {
- GET: [mockEvents.GET.remoteFeatureEip7702],
+const REMOTE_FEATURE_EIP_7702_MOCK = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } = mockEvents.GET.remoteFeatureEip7702;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
describe(SmokeMultiChainAPI('wallet_invokeMethod'), () => {
diff --git a/e2e/specs/quarantine/offramp.failing.ts b/e2e/specs/quarantine/offramp.failing.ts
index 3d55001f8fe..2ce766a2801 100644
--- a/e2e/specs/quarantine/offramp.failing.ts
+++ b/e2e/specs/quarantine/offramp.failing.ts
@@ -5,36 +5,20 @@ import FundActionMenu from '../../pages/UI/FundActionMenu';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import { CustomNetworks } from '../../resources/networks.e2e';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
import { SmokeTrade } from '../../tags';
import Assertions from '../../framework/Assertions';
import SellGetStartedView from '../../pages/Ramps/SellGetStartedView';
import BuildQuoteView from '../../pages/Ramps/BuildQuoteView';
import QuotesView from '../../pages/Ramps/QuotesView';
-import { startMockServer } from '../../api-mocking/mock-server';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { EventPayload, getEventsPayloads } from '../analytics/helpers';
import SoftAssert from '../../utils/SoftAssert';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
-import { Mockttp } from 'mockttp';
import TestHelpers from '../../helpers';
-let mockServer: Mockttp;
-let mockServerPort: number;
-
describe(SmokeTrade('Off-Ramp'), () => {
let shouldCheckProviderSelectedEvents = true;
const eventsToCheck: EventPayload[] = [];
- beforeAll(async () => {
- const segmentMock = {
- POST: [mockEvents.POST.segmentTrack],
- };
-
- mockServerPort = getMockServerPort();
- mockServer = await startMockServer(segmentMock, mockServerPort);
- });
-
beforeEach(async () => {
jest.setTimeout(150000);
});
@@ -48,9 +32,8 @@ describe(SmokeTrade('Off-Ramp'), () => {
.withMetaMetricsOptIn()
.build(),
restartDevice: true,
- mockServerInstance: mockServer,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
diff --git a/e2e/specs/confirmations-redesigned/transactions/per-dapp-selected-network.spec.js b/e2e/specs/quarantine/per-dapp-selected-network.failing.js
similarity index 65%
rename from e2e/specs/confirmations-redesigned/transactions/per-dapp-selected-network.spec.js
rename to e2e/specs/quarantine/per-dapp-selected-network.failing.js
index 37d9cb8baf3..6d2fdd29123 100644
--- a/e2e/specs/confirmations-redesigned/transactions/per-dapp-selected-network.spec.js
+++ b/e2e/specs/quarantine/per-dapp-selected-network.failing.js
@@ -1,22 +1,23 @@
-import { mockEvents } from '../../../api-mocking/mock-config/mock-events.js';
-import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder';
-import { withFixtures } from '../../../framework/fixtures/FixtureHelper';
-import { buildPermissions } from '../../../framework/fixtures/FixtureUtils';
-import Browser from '../../../pages/Browser/BrowserView';
-import ConfirmationFooterActions from '../../../pages/Browser/Confirmations/FooterActions';
-import ConfirmationUITypes from '../../../pages/Browser/Confirmations/ConfirmationUITypes';
-import TestDApp from '../../../pages/Browser/TestDApp';
-import NetworkEducationModal from '../../../pages/Network/NetworkEducationModal';
-import NetworkListModal from '../../../pages/Network/NetworkListModal';
-import TabBarComponent from '../../../pages/wallet/TabBarComponent';
-import WalletView from '../../../pages/wallet/WalletView';
-import { BrowserViewSelectorsIDs } from '../../../selectors/Browser/BrowserView.selectors';
-import { TestDappSelectorsWebIDs } from '../../../selectors/Browser/TestDapp.selectors';
-import { SmokeConfirmationsRedesigned } from '../../../tags';
-import Assertions from '../../../framework/Assertions';
-import Matchers from '../../../framework/Matchers';
-import { loginToApp } from '../../../viewHelper';
-import { DappVariants } from '../../../framework/Constants';
+import { mockEvents } from '../../api-mocking/mock-config/mock-events.js';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts';
+import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts';
+import { buildPermissions } from '../../framework/fixtures/FixtureUtils.ts';
+import Browser from '../../pages/Browser/BrowserView.ts';
+import ConfirmationFooterActions from '../../pages/Browser/Confirmations/FooterActions.ts';
+import ConfirmationUITypes from '../../pages/Browser/Confirmations/ConfirmationUITypes.ts';
+import TestDApp from '../../pages/Browser/TestDApp.ts';
+import NetworkEducationModal from '../../pages/Network/NetworkEducationModal.ts';
+import NetworkListModal from '../../pages/Network/NetworkListModal.ts';
+import TabBarComponent from '../../pages/wallet/TabBarComponent.ts';
+import WalletView from '../../pages/wallet/WalletView.ts';
+import { BrowserViewSelectorsIDs } from '../../selectors/Browser/BrowserView.selectors.ts';
+import { TestDappSelectorsWebIDs } from '../../selectors/Browser/TestDapp.selectors.ts';
+import { SmokeConfirmationsRedesigned } from '../../tags.js';
+import Assertions from '../../framework/Assertions.ts';
+import Matchers from '../../framework/Matchers.ts';
+import { loginToApp } from '../../viewHelper.ts';
+import { DappVariants } from '../../framework/Constants.ts';
+import { setupMockRequest } from '../../api-mocking/mockHelpers.ts';
const LOCAL_CHAIN_ID = '0x539';
const LOCAL_CHAIN_NAME = 'Localhost';
@@ -40,8 +41,15 @@ async function changeNetworkFromNetworkListModal(networkName) {
}
describe(SmokeConfirmationsRedesigned('Per Dapp Selected Network'), () => {
- const testSpecificMock = {
- GET: [mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations],
+ const testSpecificMock = async (mockServer) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
};
// Some tests depend on the MM_REMOVE_GLOBAL_NETWORK_SELECTOR environment variable being set to false.
diff --git a/e2e/specs/quarantine/security-alert-send-eth.failing.ts b/e2e/specs/quarantine/security-alert-send-eth.failing.ts
new file mode 100644
index 00000000000..b83d8a6eb7c
--- /dev/null
+++ b/e2e/specs/quarantine/security-alert-send-eth.failing.ts
@@ -0,0 +1,178 @@
+import Assertions from '../../framework/Assertions';
+import AmountView from '../../pages/Send/AmountView';
+import SendView from '../../pages/Send/SendView';
+import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView';
+
+import { loginToApp } from '../../viewHelper';
+import WalletView from '../../pages/wallet/WalletView';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
+import { withFixtures } from '../../framework/fixtures/FixtureHelper';
+import { mockEvents } from '../../api-mocking/mock-config/mock-events';
+import { SmokeConfirmationsRedesigned } from '../../tags';
+import { Mockttp } from 'mockttp';
+import {
+ setupMockRequest,
+ setupMockPostRequest,
+} from '../../api-mocking/mockHelpers';
+
+const BENIGN_ADDRESS_MOCK = '0x50587E46C5B96a3F6f9792922EC647F13E6EFAE4';
+
+describe(SmokeConfirmationsRedesigned('Security Alert API - Send flow'), () => {
+ const runTest = async (
+ testSpecificMock: (mockServer: Mockttp) => Promise,
+ alertAssertion: () => Promise,
+ ) => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder().withGanacheNetwork().build(),
+ restartDevice: true,
+ testSpecificMock,
+ },
+ async () => {
+ await loginToApp();
+ await WalletView.tapWalletSendButton();
+ await SendView.inputAddress(BENIGN_ADDRESS_MOCK);
+ await SendView.tapNextButton();
+ await AmountView.typeInTransactionAmount('0');
+ await AmountView.tapNextButton();
+ await alertAssertion();
+ },
+ );
+ };
+
+ it('should not show security alerts for benign requests', async () => {
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ /https:\/\/security-alerts\.api\.cx\.metamask\.io\/validate\/0x[0-9a-fA-F]+/,
+ {},
+ mockEvents.POST.securityAlertApiValidate.response,
+ {
+ statusCode: 201,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
+ },
+ );
+ };
+
+ await runTest(testSpecificMock, async () => {
+ try {
+ await Assertions.expectElementToNotBeVisible(
+ TransactionConfirmationView.securityAlertBanner,
+ );
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log('The banner alert is not visible');
+ }
+ });
+ });
+
+ it('should show security alerts for malicious request', async () => {
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ /https:\/\/security-alerts\.api\.cx\.metamask\.io\/validate\/0x[0-9a-fA-F]+/,
+ {},
+ {
+ block: 20733277,
+ result_type: 'Malicious',
+ reason: 'transfer_farming',
+ description: '',
+ features: ['Interaction with a known malicious address'],
+ },
+ {
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
+ },
+ );
+ };
+ // The banner is shown on old confirmations screen
+ await runTest(testSpecificMock, async () => {
+ await Assertions.expectElementToBeVisible(
+ TransactionConfirmationView.securityAlertBanner,
+ );
+ // await Assertions.expectElementToBeVisible(
+ // TransactionConfirmationView.securityAlertResponseMaliciousBanner,
+ // );
+ });
+ });
+
+ it('should show security alerts for error when validating request fails', async () => {
+ const testSpecificMock = async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: 'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
+ response: {
+ message: 'Internal Server Error',
+ },
+ responseCode: 500,
+ });
+
+ await setupMockPostRequest(
+ mockServer,
+ /https:\/\/security-alerts\.api\.cx\.metamask\.io\/validate\/0x[0-9a-fA-F]+/,
+ {},
+ {
+ error: 'Internal Server Error',
+ message: 'An unexpected error occurred on the server.',
+ },
+ {
+ statusCode: 500,
+ ignoreFields: [
+ 'id',
+ 'jsonrpc',
+ 'toNative',
+ 'networkClientId',
+ 'traceContext',
+ ],
+ },
+ );
+ };
+
+ await runTest(testSpecificMock, async () => {
+ await Assertions.expectElementToBeVisible(
+ TransactionConfirmationView.securityAlertBanner,
+ );
+ await Assertions.expectElementToBeVisible(
+ TransactionConfirmationView.securityAlertResponseFailedBanner,
+ );
+ });
+ });
+});
diff --git a/e2e/specs/quarantine/swap-deeplink.failing.ts b/e2e/specs/quarantine/swap-deeplink.failing.ts
index 80668e39653..28c4456dc21 100644
--- a/e2e/specs/quarantine/swap-deeplink.failing.ts
+++ b/e2e/specs/quarantine/swap-deeplink.failing.ts
@@ -17,13 +17,12 @@ import {
} from '../../framework/fixtures/FixtureUtils.ts';
import { SmokeTrade } from '../../tags.js';
import Assertions from '../../utils/Assertions.js';
-import { stopMockServer } from '../../api-mocking/mock-server.js';
+import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
import QuoteView from '../../pages/Bridge/QuoteView.ts';
import Matchers from '../../utils/Matchers.js';
import Gestures from '../../utils/Gestures.js';
import { Assertions as FrameworkAssertions } from '../../framework';
-import { startSwapsMockServer } from '../swaps/helpers/swap-mocks.ts';
-import { testSpecificMock } from '../swaps/helpers/constants.ts';
+import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks.ts';
import { localNodeOptions } from '../bridge/constants.ts';
const fixtureServer: FixtureServer = new FixtureServer();
@@ -44,7 +43,12 @@ describe(
await localNode.start(localNodeOptions);
const mockServerPort = getMockServerPort();
- mockServer = await startSwapsMockServer(testSpecificMock, mockServerPort);
+ // Added to pass linting - this pattern is not recommended. Check other swaps test for new patter
+ mockServer = await startMockServer(
+ {},
+ mockServerPort,
+ swapTestSpecificMock,
+ );
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder()
diff --git a/e2e/specs/quarantine/swap-segment-smoke.failing.ts b/e2e/specs/quarantine/swap-segment-smoke.failing.ts
deleted file mode 100644
index ac41c340ba4..00000000000
--- a/e2e/specs/quarantine/swap-segment-smoke.failing.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { ethers } from 'ethers';
-import { loginToApp } from '../../viewHelper';
-import QuoteView from '../../pages/swaps/QuoteView.ts';
-import TabBarComponent from '../../pages/wallet/TabBarComponent';
-import WalletView from '../../pages/wallet/WalletView';
-import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
-import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import Tenderly from '../../tenderly.js';
-import {
- loadFixture,
- startFixtureServer,
- stopFixtureServer,
-} from '../../framework/fixtures/FixtureHelper';
-import { CustomNetworks } from '../../resources/networks.e2e.js';
-import TestHelpers from '../../helpers.js';
-import FixtureServer from '../../framework/fixtures/FixtureServer';
-import { getFixturesServerPort } from '../../framework/fixtures/FixtureUtils';
-import { SmokeTrade } from '../../tags';
-import Assertions from '../../framework/Assertions';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events.js';
-import { getEventsPayloads } from '../analytics/helpers.ts';
-import {
- startMockServer,
- stopMockServer,
-} from '../../api-mocking/mock-server.js';
-import SoftAssert from '../../utils/SoftAssert';
-import { prepareSwapsTestEnvironment } from '../swaps/helpers/prepareSwapsTestEnvironment';
-import SwapView from '../../pages/swaps/SwapView';
-import QuotesModal from '../../pages/swaps/QuoteModal';
-import type { MockttpServer } from 'mockttp';
-
-const fixtureServer = new FixtureServer();
-
-let mockServer: MockttpServer;
-
-// This test was migrated to the new framework but should be reworked to use withFixtures properly
-describe(SmokeTrade('Swaps - Metametrics'), () => {
- const wallet = ethers.Wallet.createRandom();
-
- beforeAll(async () => {
- await Tenderly.addFunds(
- CustomNetworks.Tenderly.Mainnet.providerConfig.rpcUrl,
- wallet.address,
- );
-
- // Start the mock server to get the segment events
- const segmentMock = {
- POST: [mockEvents.POST.segmentTrack],
- };
- mockServer = await startMockServer(segmentMock);
-
- await TestHelpers.reverseServerPort();
- const fixture = new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly.Mainnet)
- .withMetaMetricsOptIn()
- .build();
- await startFixtureServer(fixtureServer);
- await loadFixture(fixtureServer, { fixture });
- await TestHelpers.launchApp({
- permissions: { notifications: 'YES' },
- launchArgs: {
- fixtureServerPort: `${getFixturesServerPort()}`,
- },
- });
- await loginToApp();
- await prepareSwapsTestEnvironment();
- });
-
- afterAll(async () => {
- await stopFixtureServer(fixtureServer);
- await stopMockServer(mockServer);
- });
-
- beforeEach(async () => {
- jest.setTimeout(120000);
- });
-
- it('should start a swap and cancel it to test the cancel event', async () => {
- await TabBarComponent.tapWallet();
- await TabBarComponent.tapActions();
- await WalletActionsBottomSheet.tapSwapButton();
-
- await Assertions.expectElementToBeVisible(QuoteView.getQuotes);
-
- await QuoteView.tapOnSelectDestToken();
- await QuoteView.tapSearchToken();
- await QuoteView.typeSearchToken('DAI');
- await TestHelpers.delay(3000);
- await QuoteView.selectToken('DAI');
- await QuoteView.enterSwapAmount('0.01');
- // This is to ensure we tap cancel before quotes are fetched - the cancel event is only sent if the quotes are not fetched
- await device.disableSynchronization();
- await QuoteView.tapOnGetQuotes();
- await TestHelpers.delay(1000);
- await QuoteView.tapOnCancelButton();
- await device.enableSynchronization();
- await Assertions.expectElementToBeVisible(WalletView.container);
- });
-
- it('should start a swap and open all available quotes to test the event', async () => {
- await TabBarComponent.tapWallet();
- await TabBarComponent.tapActions();
- await WalletActionsBottomSheet.tapSwapButton();
-
- await Assertions.expectElementToBeVisible(QuoteView.getQuotes);
- await QuoteView.tapOnSelectDestToken();
- await QuoteView.tapSearchToken();
- await QuoteView.typeSearchToken('DAI');
- await TestHelpers.delay(3000);
- await QuoteView.selectToken('DAI');
- await QuoteView.enterSwapAmount('0.01');
- await QuoteView.tapOnGetQuotes();
- await Assertions.expectElementToBeVisible(SwapView.quoteSummary);
- await SwapView.tapIUnderstandPriceWarning();
- await device.disableSynchronization();
- await SwapView.tapViewDetailsAllQuotes();
- await Assertions.expectElementToBeVisible(QuotesModal.header);
- await QuotesModal.close();
- await Assertions.expectElementToNotBeVisible(QuotesModal.header);
- await device.enableSynchronization();
- });
-
- it('should validate segment/metametric events for a cancel and viewing all available quotes', async () => {
- const EVENT_NAMES = {
- QUOTES_REQUEST_CANCELLED: 'Quotes Request Cancelled',
- ALL_AVAILABLE_QUOTES_OPENED: 'All Available Quotes Opened',
- };
-
- const events = await getEventsPayloads(
- mockServer,
- Object.values(EVENT_NAMES),
- );
-
- const softAssert = new SoftAssert();
-
- await softAssert.checkAndCollect(
- () => Assertions.checkIfArrayHasLength(events, 2),
- `Events: Should have 2 events`,
- );
-
- const allAvailableQuotesOpenedEvent = events.find(
- (e: SegmentEvent) => e.event === EVENT_NAMES.ALL_AVAILABLE_QUOTES_OPENED,
- ) as SegmentEvent;
-
- await softAssert.checkAndCollect(
- async () =>
- Assertions.checkIfObjectContains(
- allAvailableQuotesOpenedEvent.properties,
- {
- action: 'Quote',
- name: 'Swaps',
- token_from: 'ETH',
- token_to: 'DAI',
- request_type: 'Order',
- slippage: 2,
- custom_slippage: false,
- chain_id: '1',
- token_from_amount: '0.01',
- },
- ),
- 'All Available Quotes Opened: Check main properties',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.response_time,
- ),
- 'All Available Quotes Opened: Check response_time',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.best_quote_source,
- ),
- 'All Available Quotes Opened: Check best_quote_source',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.network_fees_USD,
- ),
- 'All Available Quotes Opened: Check network_fees_USD',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.network_fees_ETH,
- ),
- 'All Available Quotes Opened: Check network_fees_ETH',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.available_quotes,
- ),
- 'All Available Quotes Opened: Check available_quotes',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- allAvailableQuotesOpenedEvent.properties.token_to_amount,
- ),
- 'All Available Quotes Opened: Check token_to_amount',
- );
-
- const quotesRequestCancelledEvent = events.find(
- (e: SegmentEvent) => e.event === EVENT_NAMES.QUOTES_REQUEST_CANCELLED,
- ) as SegmentEvent;
-
- await softAssert.checkAndCollect(
- async () =>
- Assertions.checkIfObjectContains(
- quotesRequestCancelledEvent.properties,
- {
- action: 'Quote',
- name: 'Swaps',
- token_from: 'ETH',
- token_to: 'DAI',
- request_type: 'Order',
- custom_slippage: false,
- chain_id: '1',
- token_from_amount: '0.01',
- },
- ),
- 'Quotes Request Cancelled: Check properties',
- );
-
- await softAssert.checkAndCollect(
- () =>
- Assertions.checkIfValueIsDefined(
- quotesRequestCancelledEvent.properties.responseTime,
- ),
- 'Quotes Request Cancelled: Check responseTime',
- );
-
- softAssert.throwIfErrors();
- });
-});
-
-// TODO: move this to a shared file when migrating other tests to TypeScript
-interface SegmentEvent {
- event: string;
- properties: Record;
-}
diff --git a/e2e/specs/quarantine/swap-token-chart.failing.ts b/e2e/specs/quarantine/swap-token-chart.failing.ts
index 0763ef1aa82..66b72f89bc4 100644
--- a/e2e/specs/quarantine/swap-token-chart.failing.ts
+++ b/e2e/specs/quarantine/swap-token-chart.failing.ts
@@ -22,11 +22,10 @@ import Assertions from '../../framework/Assertions';
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors';
import Ganache from '../../../app/util/test/ganache';
-import { testSpecificMock } from '../swaps/helpers/constants';
import AdvancedSettingsView from '../../pages/Settings/AdvancedView';
import { submitSwapUnifiedUI } from '../swaps/helpers/swapUnifiedUI';
-import { stopMockServer } from '../../api-mocking/mock-server.js';
-import { startSwapsMockServer } from '../swaps/helpers/swap-mocks';
+import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
+import { testSpecificMock as swapTestSpecificMock } from '../swaps/helpers/swap-mocks';
import { defaultGanacheOptions } from '../../framework/Constants';
const fixtureServer: FixtureServer = new FixtureServer();
@@ -41,7 +40,12 @@ describe(Regression('Swap from Token view'), (): void => {
await localNode.start({ ...defaultGanacheOptions, chainId: 1 });
const mockServerPort = getMockServerPort();
- mockServer = await startSwapsMockServer(testSpecificMock, mockServerPort);
+ // Added to pass linting - this pattern is not recommended check other swaps test for new pattern
+ mockServer = await startMockServer(
+ {},
+ mockServerPort,
+ swapTestSpecificMock,
+ );
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder().withGanacheNetwork('0x1').build();
diff --git a/e2e/specs/ramps/offramp-cashout.spec.ts b/e2e/specs/ramps/offramp-cashout.spec.ts
index e2509f22a2f..0045fba6e5d 100644
--- a/e2e/specs/ramps/offramp-cashout.spec.ts
+++ b/e2e/specs/ramps/offramp-cashout.spec.ts
@@ -8,17 +8,15 @@ import WalletView from '../../pages/wallet/WalletView';
import FundActionMenu from '../../pages/UI/FundActionMenu';
import SelectPaymentMethodView from '../../pages/Ramps/SelectPaymentMethodView';
import SellGetStartedView from '../../pages/Ramps/SellGetStartedView';
-import { startMockServer } from '../../api-mocking/mock-server';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import {
EventPayload,
findEvent,
getEventsPayloads,
} from '../analytics/helpers';
import SoftAssert from '../../utils/SoftAssert';
-import { Mockttp } from 'mockttp';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
const PaymentMethods = {
SEPA_BANK_TRANSFER: 'SEPA Bank Transfer',
@@ -27,37 +25,28 @@ const expectedEvents = {
OFFRAMP_PAYMENT_METHOD_SELECTED: 'Off-ramp Payment Method Selected',
};
-let mockServer: Mockttp;
-let mockServerPort: number;
-
describe(SmokeTrade('Off-Ramp Cashout destination'), () => {
const eventsToCheck: EventPayload[] = [];
- beforeAll(async () => {
- const segmentMock = {
- POST: [mockEvents.POST.segmentTrack],
- };
-
- mockServerPort = getMockServerPort();
- mockServer = await startMockServer(segmentMock, mockServerPort);
- });
-
beforeEach(async () => {
jest.setTimeout(150000);
});
it('should change cashout destination', async () => {
+ const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
await withFixtures(
{
fixture: new FixtureBuilder()
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.FRANCE])
+ .withRampsSelectedRegion(selectedRegion)
.withRampsSelectedPaymentMethod()
.withMetaMetricsOptIn()
.build(),
restartDevice: true,
- mockServerInstance: mockServer,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
const offRampPaymentMethodSelected = findEvent(
events,
expectedEvents.OFFRAMP_PAYMENT_METHOD_SELECTED,
diff --git a/e2e/specs/ramps/offramp-token-amount.spec.ts b/e2e/specs/ramps/offramp-token-amount.spec.ts
index 00feb531d64..ea208efb35d 100644
--- a/e2e/specs/ramps/offramp-token-amount.spec.ts
+++ b/e2e/specs/ramps/offramp-token-amount.spec.ts
@@ -9,20 +9,27 @@ import { CustomNetworks } from '../../resources/networks.e2e';
import BuildQuoteView from '../../pages/Ramps/BuildQuoteView';
import Assertions from '../../framework/Assertions';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
describe(SmokeRamps('Off-ramp token amounts'), () => {
beforeEach(async () => {
jest.setTimeout(150000);
});
it('should change token amounts directly and by percentage', async () => {
+ const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
+
await withFixtures(
{
fixture: new FixtureBuilder()
.withNetworkController(CustomNetworks.Tenderly.Mainnet)
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.FRANCE])
+ .withRampsSelectedRegion(selectedRegion)
.withRampsSelectedPaymentMethod()
.build(),
restartDevice: true,
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
},
async () => {
await loginToApp();
diff --git a/e2e/specs/ramps/onramp-limits.spec.ts b/e2e/specs/ramps/onramp-limits.spec.ts
index 1a71e1b912d..c00ae669751 100644
--- a/e2e/specs/ramps/onramp-limits.spec.ts
+++ b/e2e/specs/ramps/onramp-limits.spec.ts
@@ -8,16 +8,22 @@ import WalletView from '../../pages/wallet/WalletView';
import FundActionMenu from '../../pages/UI/FundActionMenu';
import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
+import { Mockttp } from 'mockttp';
describe(SmokeTrade('On-Ramp Limits'), () => {
+ const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
it('should check order min and maxlimits', async () => {
await withFixtures(
{
fixture: new FixtureBuilder()
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.FRANCE])
+ .withRampsSelectedRegion(selectedRegion)
.withRampsSelectedPaymentMethod()
.build(),
restartDevice: true,
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
},
async () => {
await loginToApp();
diff --git a/e2e/specs/ramps/onramp-parameters.spec.ts b/e2e/specs/ramps/onramp-parameters.spec.ts
index d132b06b3ff..72b12151698 100644
--- a/e2e/specs/ramps/onramp-parameters.spec.ts
+++ b/e2e/specs/ramps/onramp-parameters.spec.ts
@@ -12,31 +12,31 @@ import TokenSelectBottomSheet from '../../pages/Ramps/TokenSelectBottomSheet';
import SelectRegionView from '../../pages/Ramps/SelectRegionView';
import SelectPaymentMethodView from '../../pages/Ramps/SelectPaymentMethodView';
import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView';
-import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
import { EventPayload, getEventsPayloads } from '../analytics/helpers';
import SoftAssert from '../../utils/SoftAssert';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
-import { Mockttp } from 'mockttp';
-import { DEFAULT_MOCKS } from '../../api-mocking/mock-responses/defaults';
import Matchers from '../../framework/Matchers';
+import { Mockttp } from 'mockttp';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
-let mockServer: Mockttp;
-let mockServerPort: number;
const eventsToCheck: EventPayload[] = [];
const setupOnRampTest = async (testFn: () => Promise) => {
+ const selectedRegion = RampsRegions[RampsRegionsEnum.SPAIN];
+
await withFixtures(
{
fixture: new FixtureBuilder()
.withNetworkController(CustomNetworks.Tenderly.Mainnet)
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.UNITED_STATES])
+ .withRampsSelectedRegion(selectedRegion)
.withMetaMetricsOptIn()
.build(),
- mockServerInstance: mockServer,
restartDevice: true,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
@@ -53,17 +53,6 @@ const setupOnRampTest = async (testFn: () => Promise) => {
describe(SmokeTrade('On-Ramp Parameters'), () => {
beforeEach(async () => {
jest.setTimeout(150000);
- mockServerPort = getMockServerPort();
- mockServer = await startMockServer(DEFAULT_MOCKS, mockServerPort);
- });
-
- // We need to manually stop the mock server after all the tests as each test
- // will create a new instance of the mockServer and the segment validation
- // does not require the app to be launched
- afterAll(async () => {
- if (mockServer) {
- await stopMockServer(mockServer);
- }
});
it('should select currency and verify display', async () => {
@@ -94,10 +83,7 @@ describe(SmokeTrade('On-Ramp Parameters'), () => {
it('should select payment method and verify display', async () => {
await setupOnRampTest(async () => {
- const paymentMethod =
- device.getPlatform() === 'ios'
- ? 'Apple Pay'
- : /^(?:Google|Revolut)\s+Pay$/i;
+ const paymentMethod = 'Apple Pay'; // This is now mocked so the dropdown will display the correct options even on Android
await BuildQuoteView.tapPaymentMethodDropdown(paymentMethod);
await SelectPaymentMethodView.tapPaymentMethodOption('Debit or Credit');
await Assertions.expectElementToNotBeVisible(
diff --git a/e2e/specs/quarantine/onramp.failing.ts b/e2e/specs/ramps/onramp.spec.ts
similarity index 94%
rename from e2e/specs/quarantine/onramp.failing.ts
rename to e2e/specs/ramps/onramp.spec.ts
index 6149124c176..41970805cd4 100644
--- a/e2e/specs/quarantine/onramp.failing.ts
+++ b/e2e/specs/ramps/onramp.spec.ts
@@ -11,28 +11,28 @@ import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView';
import QuotesView from '../../pages/Ramps/QuotesView';
import SoftAssert from '../../utils/SoftAssert';
import { EventPayload, getEventsPayloads } from '../analytics/helpers';
-import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
-import { Mockttp } from 'mockttp';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
-import { DEFAULT_MOCKS } from '../../api-mocking/mock-responses/defaults';
+import { Mockttp } from 'mockttp';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
-let mockServer: Mockttp;
-let mockServerPort: number;
const eventsToCheck: EventPayload[] = [];
const setupOnRampTest = async (testFn: () => Promise) => {
+ const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
+
await withFixtures(
{
fixture: new FixtureBuilder()
.withNetworkController(CustomNetworks.Tenderly.Mainnet)
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.FRANCE])
+ .withRampsSelectedRegion(selectedRegion)
.withMetaMetricsOptIn()
.build(),
restartDevice: true,
- mockServerInstance: mockServer,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
- const events = await getEventsPayloads(mockServerInstance);
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
+ endTestfn: async ({ mockServer }) => {
+ const events = await getEventsPayloads(mockServer);
eventsToCheck.push(...events);
},
},
@@ -50,15 +50,6 @@ describe(SmokeTrade('Onramp quote build screen'), () => {
let shouldCheckProviderSelectedEvents = true;
beforeEach(async () => {
jest.setTimeout(150000);
-
- mockServerPort = getMockServerPort();
- mockServer = await startMockServer(DEFAULT_MOCKS, mockServerPort);
- });
-
- afterAll(async () => {
- if (mockServer) {
- await stopMockServer(mockServer);
- }
});
it('should get to the Amount to buy screen, after selecting Get Started', async () => {
diff --git a/e2e/specs/ramps/ramps-account-switch.spec.ts b/e2e/specs/ramps/ramps-account-switch.spec.ts
index 2ec832e8d06..8432c5b43e1 100644
--- a/e2e/specs/ramps/ramps-account-switch.spec.ts
+++ b/e2e/specs/ramps/ramps-account-switch.spec.ts
@@ -9,10 +9,11 @@ import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet';
import BuildQuoteView from '../../pages/Ramps/BuildQuoteView';
import { SmokeTrade } from '../../tags';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
-import { getRampsApiMocks } from '../../api-mocking/mock-responses/ramps-mocks';
import { LocalNodeType } from '../../framework/types';
import { Hardfork } from '../../seeder/anvil-manager';
import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants';
+import { Mockttp } from 'mockttp';
+import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup';
// Anvil configuration for local blockchain node
const anvilLocalNodeOptions = {
@@ -22,8 +23,7 @@ const anvilLocalNodeOptions = {
chainId: 1,
};
-// Get ramps API mocks from the dedicated mock file
-const rampsApiMocks = getRampsApiMocks();
+const selectedRegion = RampsRegions[RampsRegionsEnum.FRANCE];
const setupRampsAccountSwitchTest = async (
testFunction: () => Promise,
@@ -32,7 +32,7 @@ const setupRampsAccountSwitchTest = async (
{
fixture: new FixtureBuilder()
.withImportedHdKeyringAndTwoDefaultAccountsOneImportedHdAccountKeyringController()
- .withRampsSelectedRegion(RampsRegions[RampsRegionsEnum.FRANCE])
+ .withRampsSelectedRegion(selectedRegion)
.build(),
restartDevice: true,
localNodeOptions: [
@@ -41,7 +41,9 @@ const setupRampsAccountSwitchTest = async (
options: anvilLocalNodeOptions,
},
],
- testSpecificMock: rampsApiMocks,
+ testSpecificMock: async (mockServer: Mockttp) => {
+ await setupRegionAwareOnRampMocks(mockServer, selectedRegion);
+ },
},
async () => {
await loginToApp();
diff --git a/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts b/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts
index c3234bb6002..04596dd4a01 100644
--- a/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts
+++ b/e2e/specs/snaps/test-snap-ethereum-provider.spec.ts
@@ -6,8 +6,10 @@ import Assertions from '../../framework/Assertions';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import TestSnaps from '../../pages/Browser/TestSnaps';
import ConnectBottomSheet from '../../pages/Browser/ConnectBottomSheet';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import RequestTypes from '../../pages/Browser/Confirmations/RequestTypes';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
+import { mockEvents } from '../../api-mocking/mock-config/mock-events';
jest.setTimeout(150_000);
@@ -17,8 +19,15 @@ describe(FlaskBuildTests('Ethereum Provider Snap Tests'), () => {
{
fixture: new FixtureBuilder().withMultiSRPKeyringController().build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockEvents.GET.remoteFeatureFlagsRedesignedConfirmationsFlask],
+ testSpecificMock: async (mockServer: Mockttp) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsRedesignedConfirmationsFlask;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
},
},
async () => {
diff --git a/e2e/specs/snaps/test-snap-preinstalled.spec.ts b/e2e/specs/snaps/test-snap-preinstalled.spec.ts
index 1424ecec54c..d74917839a3 100644
--- a/e2e/specs/snaps/test-snap-preinstalled.spec.ts
+++ b/e2e/specs/snaps/test-snap-preinstalled.spec.ts
@@ -5,7 +5,6 @@ import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import TestSnaps from '../../pages/Browser/TestSnaps';
import Assertions from '../../framework/Assertions';
-import { mockEvents } from '../../api-mocking/mock-config/mock-events';
import { getEventsPayloads } from '../analytics/helpers';
import TestHelpers from '../../helpers';
@@ -42,15 +41,8 @@ describe(FlaskBuildTests('Preinstalled Snap Tests'), () => {
await withFixtures(
{
fixture: new FixtureBuilder().withMetaMetricsOptIn().build(),
- testSpecificMock: { POST: [mockEvents.POST.segmentTrack] },
},
async ({ mockServer }) => {
- if (!mockServer) {
- throw new Error(
- 'Mock server is not defined, check testSpecificMock setup',
- );
- }
-
await TestSnaps.tapButton('trackEventButton');
await TestHelpers.delay(1000);
diff --git a/e2e/specs/stake/stake-action-smoke.spec.ts b/e2e/specs/stake/stake-action-smoke.spec.ts
index d52252ccead..8b07d8c3b9b 100644
--- a/e2e/specs/stake/stake-action-smoke.spec.ts
+++ b/e2e/specs/stake/stake-action-smoke.spec.ts
@@ -1,5 +1,4 @@
import { ethers } from 'ethers';
-import { MockttpServer } from 'mockttp';
import { loginToApp } from '../../viewHelper';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
@@ -10,7 +9,6 @@ import WalletView from '../../pages/wallet/WalletView';
import {
loadFixture,
startFixtureServer,
- stopFixtureServer,
} from '../../framework/fixtures/FixtureHelper';
import {
CustomNetworks,
@@ -18,10 +16,7 @@ import {
} from '../../resources/networks.e2e';
import TestHelpers from '../../helpers';
import FixtureServer from '../../framework/fixtures/FixtureServer';
-import {
- getFixturesServerPort,
- getMockServerPort,
-} from '../../framework/fixtures/FixtureUtils';
+import { getFixturesServerPort } from '../../framework/fixtures/FixtureUtils';
import { SmokeTrade } from '../../tags';
import Assertions from '../../framework/Assertions';
import StakeView from '../../pages/Stake/StakeView';
@@ -36,7 +31,6 @@ import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet';
import NetworkListModal from '../../pages/Network/NetworkListModal';
import axios, { AxiosResponse } from 'axios';
import NetworkEducationModal from '../../pages/Network/NetworkEducationModal';
-import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
interface ExitRequest {
positionTicket: string;
@@ -59,22 +53,11 @@ interface StakingAPIResponse {
accounts: StakingAccount[];
}
-interface MockEndpoint {
- urlEndpoint: string;
- response: StakingAPIResponse;
- responseCode: number;
-}
-
-interface MockConfig {
- GET: MockEndpoint[];
-}
-
const fixtureServer: FixtureServer = new FixtureServer();
describe.skip(SmokeTrade('Stake from Actions'), (): void => {
const FIRST_ROW: number = 0;
const AMOUNT_TO_SEND: string = '.005';
- let mockServer: MockttpServer;
const wallet: ethers.Wallet = ethers.Wallet.createRandom();
beforeAll(async (): Promise => {
@@ -93,11 +76,6 @@ describe.skip(SmokeTrade('Stake from Actions'), (): void => {
await loginToApp();
});
- afterAll(async (): Promise => {
- if (mockServer) await stopMockServer(mockServer);
- await stopFixtureServer(fixtureServer);
- });
-
beforeEach(async (): Promise => {
jest.setTimeout(150000);
});
@@ -280,45 +258,36 @@ describe.skip(SmokeTrade('Stake from Actions'), (): void => {
throw new Error(`No claim entries found for account ${wallet.address}`);
}
- const testSpecificMock: MockConfig = {
- GET: [
- {
- urlEndpoint: stakeAPIUrl,
- response: {
- accounts: [
- {
- account: account.account,
- lifetimeRewards: account.lifetimeRewards,
- assets: account.lifetimeRewards,
- exitRequests: [
- {
- positionTicket: account.exitRequests[0].positionTicket,
- timestamp: '1737657204000',
- totalShares: account.exitRequests[0].totalShares,
- withdrawalTimestamp: '0',
- exitQueueIndex: '157',
- claimedAssets: '36968822284547795',
- leftShares: '0',
- },
- ],
- },
- ],
- },
- responseCode: 200,
- },
- ],
- };
await device.terminateApp();
- const mockServerPort: number = getMockServerPort();
- mockServer = await startMockServer(testSpecificMock, mockServerPort);
+ // const testSpecificMockFn = async (mockServer: Mockttp) => {
+ // await setupMockRequest(mockServer, {
+ // requestMethod: 'GET',
+ // url: stakeAPIUrl,
+ // response: {
+ // accounts: [
+ // {
+ // account: account.account,
+ // lifetimeRewards: account.lifetimeRewards,
+ // assets: account.lifetimeRewards,
+ // exitRequests: [
+ // {
+ // positionTicket: account.exitRequests[0].positionTicket,
+ // timestamp: '1737657204000',
+ // totalShares: account.exitRequests[0].totalShares,
+ // withdrawalTimestamp: '0',
+ // exitQueueIndex: '157',
+ // claimedAssets: '36968822284547795',
+ // leftShares: '0',
+ // },
+ // ],
+ // },
+ // ],
+ // },
+ // responseCode: 200,
+ // });
+ // };
- await TestHelpers.launchApp({
- launchArgs: {
- fixtureServerPort: `${getFixturesServerPort()}`,
- mockServerPort: `${mockServerPort}`,
- },
- });
await loginToApp();
await WalletView.tapOnStakedEthereum();
await TokenOverview.scrollOnScreen();
diff --git a/e2e/specs/swaps/helpers/constants.ts b/e2e/specs/swaps/helpers/constants.ts
index 0dfd1fb94f6..da46b3d058a 100644
--- a/e2e/specs/swaps/helpers/constants.ts
+++ b/e2e/specs/swaps/helpers/constants.ts
@@ -3,7 +3,7 @@ import { mockEvents } from '../../../api-mocking/mock-config/mock-events.js';
const GET_QUOTE_ETH_USDC_URL =
'https://bridge.dev-api.cx.metamask.io/getQuote?walletAddress=0xcdD74C6eb517f687Aa2C786bC7484eB2F9bAe1da&destWalletAddress=0xcdD74C6eb517f687Aa2C786bC7484eB2F9bAe1da&srcChainId=1&destChainId=1&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&srcTokenAmount=1000000000000000000&insufficientBal=false&resetApproval=false&slippage=0.5';
-const GET_QUOTE_ETH_USDC_RESPONSE = [
+export const GET_QUOTE_ETH_USDC_RESPONSE = [
{
quote: {
requestId:
@@ -120,7 +120,7 @@ const GET_QUOTE_ETH_USDC_RESPONSE = [
const GET_QUOTE_ETH_WETH_URL =
'https://bridge.dev-api.cx.metamask.io/getQuote?walletAddress=0xcdD74C6eb517f687Aa2C786bC7484eB2F9bAe1da&destWalletAddress=0xcdD74C6eb517f687Aa2C786bC7484eB2F9bAe1da&srcChainId=1&destChainId=1&srcTokenAddress=0x0000000000000000000000000000000000000000&destTokenAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2&srcTokenAmount=1000000000000000000&insufficientBal=false&resetApproval=false&slippage=0.5';
-const GET_QUOTE_ETH_WETH_RESPONSE = [
+export const GET_QUOTE_ETH_WETH_RESPONSE = [
{
quote: {
requestId:
diff --git a/e2e/specs/swaps/helpers/swap-mocks.ts b/e2e/specs/swaps/helpers/swap-mocks.ts
index 5888cd49c7a..a043f999348 100644
--- a/e2e/specs/swaps/helpers/swap-mocks.ts
+++ b/e2e/specs/swaps/helpers/swap-mocks.ts
@@ -1,147 +1,36 @@
-/* eslint-disable no-console */
-import { getLocal } from 'mockttp';
-import portfinder from 'portfinder';
-
-interface MockEvent {
- urlEndpoint: string;
- responseCode: number;
- response: unknown;
-}
-
-interface MockEvents {
- [key: string]: MockEvent[];
-}
-
-/**
- * Utility function to handle direct fetch requests
- * @param {string} url - The URL to fetch from
- * @param {string} method - The HTTP method
- * @param {Headers} headers - Request headers
- * @param {string | undefined} requestBody - The request body as string
- * @returns {Promise<{statusCode: number, body: string}>} Response object
- */
-const handleDirectFetch = async (
- url: string,
- method: string,
- headers: Headers,
- requestBody: string | undefined,
+import { Mockttp } from 'mockttp';
+import { TestSpecificMock } from '../../../framework';
+import {
+ interceptProxyUrl,
+ setupMockRequest,
+} from '../../../api-mocking/mockHelpers';
+import {
+ GET_QUOTE_ETH_USDC_RESPONSE,
+ GET_QUOTE_ETH_WETH_RESPONSE,
+} from './constants';
+
+export const testSpecificMock: TestSpecificMock = async (
+ mockServer: Mockttp,
) => {
- try {
- const response = await global.fetch(url, {
- method,
- headers,
- body: ['POST', 'PUT', 'PATCH'].includes(method) ? requestBody : undefined,
- });
-
- const responseBody = await response.text();
- return {
- statusCode: response.status,
- body: responseBody,
- };
- } catch (error) {
- console.error('Error forwarding request:', url);
- return {
- statusCode: 500,
- body: JSON.stringify({ error: 'Failed to forward request' }),
- };
- }
-};
-
-/**
- * Starts the mock server and sets up mock events.
- *
- * @param {MockEvents} events - The events to mock, organised by method.
- * @param {number} [port] - Optional port number. If not provided, a free port will be used.
- * @returns {Promise} Resolves to the running mock server.
- */
-export const startSwapsMockServer = async (
- events: MockEvents,
- port: number,
-) => {
- const mockServer = getLocal();
- port = port || (await portfinder.getPortPromise());
-
- await mockServer.start(port);
- console.log(`Mockttp server running at http://localhost:${port}`);
-
- await mockServer
- .forGet('/health-check')
- .thenReply(200, 'Mock server is running');
-
- // Handle all /proxy requests
- await mockServer
- .forAnyRequest()
- .matching((request) => request.path.startsWith('/proxy'))
- .thenCallback(async (request) => {
- const urlParam = new URL(request.url).searchParams.get('url');
- if (!urlParam) {
- return {
- statusCode: 400,
- body: JSON.stringify({ error: 'Missing url parameter' }),
- };
- }
- let urlEndpoint: string = urlParam;
- const method = request.method;
-
- // Find matching mock event
- const methodEvents = events[method] || [];
- const matchingEvent = methodEvents.find(
- (event: MockEvent) => event.urlEndpoint === urlEndpoint,
- );
-
- if (matchingEvent) {
- console.log(`Mocking ${method} request to: ${urlEndpoint}`);
- console.log(`Response status: ${matchingEvent.responseCode}`);
- console.log('Response:', matchingEvent.response);
-
- return {
- statusCode: matchingEvent.responseCode,
- body: JSON.stringify(matchingEvent.response),
- };
- }
-
- // Needed in order to get a quote for locahost
- if (urlEndpoint.includes('getQuote')) {
- urlEndpoint = urlEndpoint.replace(
- 'insufficientBal=false',
- 'insufficientBal=true',
- );
- }
- // If no matching mock found, pass through to actual endpoint
- const updatedUrl =
- device.getPlatform() === 'android'
- ? urlEndpoint.replace('localhost', '127.0.0.1')
- : urlEndpoint;
-
- const requestBody =
- method === 'POST' ? await request.body.getText() : undefined;
- const headerEntries = Object.entries(request.headers)
- .filter((entry): entry is [string, string] => {
- const value = entry[1];
- return value !== undefined && typeof value === 'string';
- })
- .map(([key, value]) => [key, value] as [string, string]);
- const headers = new Headers(headerEntries);
-
- return handleDirectFetch(updatedUrl, method, headers, requestBody);
- });
+ // Mock ETH->USDC
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: /getQuote.*destTokenAddress=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/i,
+ response: GET_QUOTE_ETH_USDC_RESPONSE,
+ responseCode: 200,
+ });
- // In case any other requests are made, pass them through to the actual endpoint
- await mockServer.forUnmatchedRequest().thenCallback(async (request) => {
- const headerEntries = Object.entries(request.headers)
- .filter((entry): entry is [string, string] => {
- const value = entry[1];
- return value !== undefined && typeof value === 'string';
- })
- .map(([key, value]) => [key, value] as [string, string]);
- const headers = new Headers(headerEntries);
- return handleDirectFetch(
- request.url,
- request.method,
- headers,
- await request.body.getText(),
- );
+ // Mock ETH->WETH
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: /getQuote.*destTokenAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/i,
+ response: GET_QUOTE_ETH_WETH_RESPONSE,
+ responseCode: 200,
});
- return mockServer;
+ await interceptProxyUrl(
+ mockServer,
+ (url) => url.includes('getQuote') && url.includes('insufficientBal=false'),
+ (url) => url.replace('insufficientBal=false', 'insufficientBal=true'),
+ );
};
diff --git a/e2e/specs/swaps/swap-action-regression.spec.ts b/e2e/specs/swaps/swap-action-regression.spec.ts
index 7a843b80b48..5ca0ee032c4 100644
--- a/e2e/specs/swaps/swap-action-regression.spec.ts
+++ b/e2e/specs/swaps/swap-action-regression.spec.ts
@@ -1,113 +1,102 @@
-import { loginToApp } from '../../viewHelper';
+import { withFixtures } from '../../framework/fixtures/FixtureHelper';
+import { LocalNodeType } from '../../framework/types';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
+import Assertions from '../../framework/Assertions';
+import { defaultGanacheOptions } from '../../framework/Constants';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
-import ActivitiesView from '../../pages/Transactions/ActivitiesView';
import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
-import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
-import {
- loadFixture,
- startFixtureServer,
- stopFixtureServer,
-} from '../../framework/fixtures/FixtureHelper';
-import { Mockttp } from 'mockttp';
-import TestHelpers from '../../helpers';
-import FixtureServer from '../../framework/fixtures/FixtureServer';
-import {
- getFixturesServerPort,
- getMockServerPort,
-} from '../../framework/fixtures/FixtureUtils';
import { Regression } from '../../tags';
-import Assertions from '../../framework/Assertions';
+import ActivitiesView from '../../pages/Transactions/ActivitiesView';
import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors';
import { submitSwapUnifiedUI } from './helpers/swapUnifiedUI';
-import Ganache from '../../../app/util/test/ganache';
-import { testSpecificMock } from './helpers/constants';
-import { stopMockServer } from '../../api-mocking/mock-server.js';
-import { startSwapsMockServer } from './helpers/swap-mocks';
-import { defaultGanacheOptions } from '../../framework/Constants';
-
-const fixtureServer = new FixtureServer();
+import { loginToApp } from '../../viewHelper';
+import { prepareSwapsTestEnvironment } from './helpers/prepareSwapsTestEnvironment';
+import { testSpecificMock } from './helpers/swap-mocks';
-// eslint-disable-next-line jest/no-disabled-tests
-describe.skip(Regression('Multiple Swaps from Actions'), () => {
+describe(Regression('Multiple Swaps from Actions'), (): void => {
const FIRST_ROW: number = 0;
const SECOND_ROW: number = 1;
- let mockServer: Mockttp;
- let localNode: Ganache;
-
- beforeAll(async () => {
- jest.setTimeout(2500000);
-
- localNode = new Ganache();
- await localNode.start({ ...defaultGanacheOptions, chainId: 1 });
-
- const mockServerPort = getMockServerPort();
- mockServer = await startSwapsMockServer(testSpecificMock, mockServerPort);
-
- await TestHelpers.reverseServerPort();
- const fixture = new FixtureBuilder()
- .withGanacheNetwork('0x1')
- .withDisabledSmartTransactions()
- .build();
- await startFixtureServer(fixtureServer);
- await loadFixture(fixtureServer, { fixture });
- await TestHelpers.launchApp({
- permissions: { notifications: 'YES' },
- launchArgs: {
- fixtureServerPort: `${getFixturesServerPort()}`,
- mockServerPort: `${mockServerPort}`,
- },
- });
- await loginToApp();
- });
- afterAll(async () => {
- await stopFixtureServer(fixtureServer);
- if (mockServer) await stopMockServer(mockServer);
- if (localNode) await localNode.quit();
+ beforeEach(async (): Promise => {
+ jest.setTimeout(120000);
});
+ // TODO: Add mock responses for DAI tokens to enable these test cases
+ // ${'native'} | ${'.03'} | ${'ETH'} | ${'DAI'} | ${'0x1'}
+ // ${'unapproved'} | ${'3'} | ${'DAI'} | ${'USDC'} | ${'0x1'}
+ // ${'erc20'} | ${'10'} | ${'DAI'} | ${'ETH'} | ${'0x1'}
it.each`
- type | quantity | sourceTokenSymbol | destTokenSymbol | chainId
- ${'native'} | ${'.03'} | ${'ETH'} | ${'DAI'} | ${'0x1'}
- ${'unapproved'} | ${'3'} | ${'DAI'} | ${'USDC'} | ${'0x1'}
- ${'erc20'} | ${'10'} | ${'DAI'} | ${'ETH'} | ${'0x1'}
+ type | quantity | sourceTokenSymbol | destTokenSymbol | chainId
+ ${'native'} | ${'1'} | ${'ETH'} | ${'USDC'} | ${'0x1'}
`(
- "should swap $type token '$sourceTokenSymbol' to '$destTokenSymbol' on chainID='$chainId",
- async ({ type, quantity, sourceTokenSymbol, destTokenSymbol, chainId }) => {
- await TabBarComponent.tapActions();
- await Assertions.checkIfVisible(WalletActionsBottomSheet.swapButton);
- await WalletActionsBottomSheet.tapSwapButton();
+ "should swap $type token '$sourceTokenSymbol' to '$destTokenSymbol' on chainID='$chainId'",
+ async ({
+ type,
+ quantity,
+ sourceTokenSymbol,
+ destTokenSymbol,
+ chainId,
+ }): Promise => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder()
+ .withGanacheNetwork('0x1')
+ .withDisabledSmartTransactions()
+ .build(),
+ localNodeOptions: [
+ {
+ type: LocalNodeType.ganache,
+ options: {
+ ...defaultGanacheOptions,
+ chainId: 1,
+ },
+ },
+ ],
+ testSpecificMock,
+ restartDevice: true,
+ },
+ async () => {
+ await loginToApp();
+ await prepareSwapsTestEnvironment();
+ await TabBarComponent.tapActions();
+ await Assertions.expectElementToBeVisible(
+ WalletActionsBottomSheet.swapButton,
+ );
+ await WalletActionsBottomSheet.tapSwapButton();
- // Submit the Swap
- await submitSwapUnifiedUI(
- quantity,
- sourceTokenSymbol,
- destTokenSymbol,
- chainId,
- );
+ // Submit the Swap
+ await submitSwapUnifiedUI(
+ quantity,
+ sourceTokenSymbol,
+ destTokenSymbol,
+ chainId,
+ );
- // Check the swap activity completed
- await Assertions.checkIfVisible(ActivitiesView.title);
- await Assertions.checkIfVisible(
- ActivitiesView.swapActivityTitle(sourceTokenSymbol, destTokenSymbol),
- );
- await Assertions.checkIfElementToHaveText(
- ActivitiesView.transactionStatus(FIRST_ROW),
- ActivitiesViewSelectorsText.CONFIRM_TEXT,
- 60000,
- );
+ // Check the swap activity completed
+ await Assertions.expectElementToBeVisible(ActivitiesView.title);
+ await Assertions.expectElementToBeVisible(
+ ActivitiesView.swapActivityTitle(
+ sourceTokenSymbol,
+ destTokenSymbol,
+ ),
+ );
+ await Assertions.expectElementToHaveText(
+ ActivitiesView.transactionStatus(FIRST_ROW),
+ ActivitiesViewSelectorsText.CONFIRM_TEXT,
+ );
- // Check the token approval completed
- if (type === 'unapproved') {
- await Assertions.checkIfVisible(
- ActivitiesView.tokenApprovalActivity(sourceTokenSymbol),
- );
- await Assertions.checkIfElementToHaveText(
- ActivitiesView.transactionStatus(SECOND_ROW),
- ActivitiesViewSelectorsText.CONFIRM_TEXT,
- 60000,
- );
- }
+ // Check the token approval completed
+ if (type === 'unapproved') {
+ await Assertions.expectElementToBeVisible(
+ ActivitiesView.tokenApprovalActivity(sourceTokenSymbol),
+ );
+ await Assertions.expectElementToHaveText(
+ ActivitiesView.transactionStatus(SECOND_ROW),
+ ActivitiesViewSelectorsText.CONFIRM_TEXT,
+ );
+ }
+ },
+ );
},
);
});
diff --git a/e2e/specs/swaps/swap-action-smoke.spec.ts b/e2e/specs/swaps/swap-action-smoke.spec.ts
index d56055dbe17..86985cfe630 100644
--- a/e2e/specs/swaps/swap-action-smoke.spec.ts
+++ b/e2e/specs/swaps/swap-action-smoke.spec.ts
@@ -1,4 +1,3 @@
-import { MockttpServer } from 'mockttp';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import { LocalNodeType } from '../../framework/types';
import SoftAssert from '../../utils/SoftAssert';
@@ -7,17 +6,15 @@ import Assertions from '../../framework/Assertions';
import { defaultGanacheOptions } from '../../framework/Constants';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
-import { testSpecificMock } from './helpers/constants';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
import { SmokeTrade } from '../../tags.js';
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors';
import { EventPayload, getEventsPayloads } from '../analytics/helpers';
-import { startSwapsMockServer } from './helpers/swap-mocks';
import { submitSwapUnifiedUI } from './helpers/swapUnifiedUI';
import { loginToApp } from '../../viewHelper';
import { prepareSwapsTestEnvironment } from './helpers/prepareSwapsTestEnvironment';
import { logger } from '../../framework/logger';
+import { testSpecificMock } from './helpers/swap-mocks';
const EVENT_NAMES = {
SWAP_STARTED: 'Swap Started',
@@ -26,22 +23,10 @@ const EVENT_NAMES = {
QUOTES_RECEIVED: 'Quotes Received',
};
-// eslint-disable-next-line jest/no-disabled-tests
describe(SmokeTrade('Swap from Actions'), (): void => {
const FIRST_ROW: number = 0;
const SECOND_ROW: number = 1;
- let mockServerPort: number;
let capturedEvents: EventPayload[] = [];
- let mockServer: MockttpServer;
-
- beforeAll(async (): Promise => {
- mockServerPort = getMockServerPort();
- mockServer = (await startSwapsMockServer(
- testSpecificMock,
- mockServerPort,
- )) as MockttpServer;
- logger.debug(`Test side Mock server started on port ${mockServerPort}`);
- });
beforeEach(async (): Promise => {
jest.setTimeout(120000);
@@ -75,17 +60,13 @@ describe(SmokeTrade('Swap from Actions'), (): void => {
},
},
],
- mockServerInstance: mockServer,
+ testSpecificMock,
restartDevice: true,
- endTestfn: async ({ mockServer: mockServerInstance }) => {
+ endTestfn: async ({ mockServer }) => {
try {
// Capture all events without filtering.
// When fixing the test skipped below the filter needs to be applied there.
- capturedEvents = await getEventsPayloads(
- mockServerInstance,
- [],
- 30000,
- );
+ capturedEvents = await getEventsPayloads(mockServer, [], 30000);
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
diff --git a/e2e/specs/wallet/incoming-transactions.spec.ts b/e2e/specs/wallet/incoming-transactions.spec.ts
index 991be32f911..f1943b4de9d 100644
--- a/e2e/specs/wallet/incoming-transactions.spec.ts
+++ b/e2e/specs/wallet/incoming-transactions.spec.ts
@@ -9,7 +9,9 @@ import FixtureBuilder, {
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import ToastModal from '../../pages/wallet/ToastModal';
-import { MockApiEndpoint } from '../../framework/types';
+import { MockApiEndpoint, TestSpecificMock } from '../../framework/types';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { Mockttp } from 'mockttp';
const TOKEN_SYMBOL_MOCK = 'ABC';
const TOKEN_ADDRESS_MOCK = '0x123';
@@ -81,6 +83,20 @@ function mockAccountsApi(
};
}
+function createAccountsTestSpecificMock(
+ transactions: Record[] = [],
+): TestSpecificMock {
+ return async (mockServer: Mockttp) => {
+ const mock = mockAccountsApi(transactions);
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: mock.urlEndpoint,
+ response: mock.response,
+ responseCode: mock.responseCode,
+ });
+ };
+}
+
describe(SmokeWalletPlatform('Incoming Transactions'), () => {
beforeAll(async () => {
jest.setTimeout(2500000);
@@ -91,9 +107,7 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
{
fixture: new FixtureBuilder().withPrivacyModePreferences(false).build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockAccountsApi()],
- },
+ testSpecificMock: createAccountsTestSpecificMock(),
},
async () => {
await loginToApp();
@@ -120,9 +134,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
.withPrivacyModePreferences(false)
.build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockAccountsApi([RESPONSE_TOKEN_TRANSFER_MOCK])],
- },
+ testSpecificMock: createAccountsTestSpecificMock([
+ RESPONSE_TOKEN_TRANSFER_MOCK,
+ ]),
},
async () => {
await loginToApp();
@@ -138,9 +152,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
{
fixture: new FixtureBuilder().withPrivacyModePreferences(false).build(),
restartDevice: true,
- testSpecificMock: {
- GET: [mockAccountsApi([RESPONSE_OUTGOING_TRANSACTION_MOCK])],
- },
+ testSpecificMock: createAccountsTestSpecificMock([
+ RESPONSE_OUTGOING_TRANSACTION_MOCK,
+ ]),
},
async () => {
await loginToApp();
@@ -156,7 +170,7 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
{
fixture: new FixtureBuilder().withPrivacyModePreferences(true).build(),
restartDevice: true,
- testSpecificMock: { GET: [mockAccountsApi()] },
+ testSpecificMock: createAccountsTestSpecificMock(),
},
async () => {
await loginToApp();
@@ -182,7 +196,9 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
])
.build(),
restartDevice: true,
- testSpecificMock: { GET: [mockAccountsApi([RESPONSE_STANDARD_MOCK])] },
+ testSpecificMock: createAccountsTestSpecificMock([
+ RESPONSE_STANDARD_MOCK,
+ ]),
},
async () => {
await loginToApp();
@@ -198,7 +214,7 @@ describe(SmokeWalletPlatform('Incoming Transactions'), () => {
{
fixture: new FixtureBuilder().build(),
restartDevice: true,
- testSpecificMock: { GET: [mockAccountsApi()] },
+ testSpecificMock: createAccountsTestSpecificMock(),
},
async () => {
await loginToApp();
diff --git a/e2e/specs/wallet/request-token-flow.spec.ts b/e2e/specs/wallet/request-token-flow.spec.ts
index 866d5c5f8e6..347b75f5879 100644
--- a/e2e/specs/wallet/request-token-flow.spec.ts
+++ b/e2e/specs/wallet/request-token-flow.spec.ts
@@ -9,8 +9,6 @@ import { loginToApp } from '../../viewHelper';
import { withFixtures } from '../../framework/fixtures/FixtureHelper';
import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
import Assertions from '../../framework/Assertions';
-import { startMockServer } from '../../api-mocking/mock-server';
-import { getMockServerPort } from '../../framework/fixtures/FixtureUtils';
const SAI_CONTRACT_ADDRESS: string =
'0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359';
@@ -19,8 +17,6 @@ describe(
SmokeWalletPlatform('Request Token Flow with Unprotected Wallet'),
(): void => {
it('should complete request token flow from action button to wallet protection modal', async (): Promise => {
- const mockServerPort = getMockServerPort();
- const mockServer = await startMockServer({}, mockServerPort);
await withFixtures(
{
fixture: new FixtureBuilder()
@@ -28,7 +24,6 @@ describe(
.withSeedphraseBackedUpDisabled()
.build(),
restartDevice: true,
- mockServerInstance: mockServer,
},
async (): Promise => {
await loginToApp();
diff --git a/e2e/specs/wallet/send-ERC-token.spec.js b/e2e/specs/wallet/send-ERC-token.spec.js
index d4d1c1d8b98..b5644a0f9a3 100644
--- a/e2e/specs/wallet/send-ERC-token.spec.js
+++ b/e2e/specs/wallet/send-ERC-token.spec.js
@@ -4,7 +4,7 @@ import WalletView from '../../pages/wallet/WalletView';
import NetworkEducationModal from '../../pages/Network/NetworkEducationModal';
import AmountView from '../../pages/Send/AmountView';
import SendView from '../../pages/Send/SendView';
-import { importWalletWithRecoveryPhrase } from '../../viewHelper';
+import { importWalletWithRecoveryPhrase, loginToApp } from '../../viewHelper';
import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView';
import NetworkListModal from '../../pages/Network/NetworkListModal';
import TokenOverview from '../../pages/wallet/TokenOverview';
@@ -13,7 +13,9 @@ import ImportTokensView from '../../pages/wallet/ImportTokenFlow/ImportTokensVie
import Assertions from '../../framework/Assertions';
import { CustomNetworks } from '../../resources/networks.e2e';
import { mockEvents } from '../../api-mocking/mock-config/mock-events';
-import { startMockServer, stopMockServer } from '../../api-mocking/mock-server';
+import { setupMockRequest } from '../../api-mocking/mockHelpers';
+import { withFixtures } from '../../framework/fixtures/FixtureHelper';
+import FixtureBuilder from '../../framework/fixtures/FixtureBuilder';
const TOKEN_ADDRESS = '0x779877A7B0D9E8603169DdbD7836e478b4624789';
const SEND_ADDRESS = '0xebe6CcB6B55e1d094d9c58980Bc10Fed69932cAb';
@@ -28,87 +30,74 @@ describe(Regression('Send ERC Token'), () => {
beforeAll(async () => {
jest.setTimeout(150000);
-
- // Start mock server to force old confirmation UI
- mockServer = await startMockServer({
- GET: [mockEvents.GET.remoteFeatureFlagsOldConfirmations],
- });
-
- await TestHelpers.launchApp();
});
+ // TODO: investigate why next button is not found on import token screen
+ xit('should send erc token successfully', async () => {
+ await withFixtures(
+ {
+ fixture: new FixtureBuilder().build(),
+ restartDevice: true,
+ testSpecificMock: async (mockServer) => {
+ const { urlEndpoint, response } =
+ mockEvents.GET.remoteFeatureFlagsOldConfirmations;
+ await setupMockRequest(mockServer, {
+ requestMethod: 'GET',
+ url: urlEndpoint,
+ response,
+ responseCode: 200,
+ });
+ },
+ },
+ async () => {
+ await loginToApp();
- afterAll(async () => {
- if (mockServer) {
- await stopMockServer(mockServer);
- }
- });
+ if (isRemoveGlobalNetworkSelectorEnabled) {
+ await WalletView.tapNetworksButtonOnNavBar();
+ await NetworkListModal.scrollToBottomOfNetworkList();
+ await NetworkListModal.tapTestNetworkSwitch();
+ await NetworkListModal.scrollToBottomOfNetworkList();
+ await Assertions.expectToggleToBeOn(NetworkListModal.testNetToggle);
+ await NetworkListModal.changeNetworkTo(
+ CustomNetworks.Sepolia.providerConfig.nickname,
+ );
+ }
- it('should import wallet and go to the wallet view', async () => {
- await importWalletWithRecoveryPhrase({
- seedPhrase: process.env.MM_TEST_WALLET_SRP,
- });
- });
+ await WalletView.tapImportTokensButton();
+ await ImportTokensView.switchToCustomTab();
+ // choose network here
+ await ImportTokensView.tapOnNetworkInput();
+ await ImportTokensView.tapNetworkOption('Sepolia');
+ await ImportTokensView.typeTokenAddress(TOKEN_ADDRESS);
+ await ImportTokensView.tapSymbolInput();
+ await ImportTokensView.tapTokenSymbolText();
+ await ImportTokensView.scrollDownOnImportCustomTokens();
+ await ImportTokensView.tapOnNextButton();
+ await ConfirmAddAssetView.tapOnConfirmButton();
+ await Assertions.expectElementToBeVisible(WalletView.container);
- itif(isRemoveGlobalNetworkSelectorEnabled)(
- 'should add Sepolia testnet to my networks list',
- async () => {
- await WalletView.tapNetworksButtonOnNavBar();
- await TestHelpers.delay(2000);
- await NetworkListModal.scrollToBottomOfNetworkList();
- await NetworkListModal.tapTestNetworkSwitch();
- await NetworkListModal.scrollToBottomOfNetworkList();
- await Assertions.expectToggleToBeOn(NetworkListModal.testNetToggle);
- await NetworkListModal.changeNetworkTo(
- CustomNetworks.Sepolia.providerConfig.nickname,
- );
- },
- );
+ // Scroll to top first to ensure consistent starting position
+ await WalletView.scrollToBottomOfTokensList();
+ await TestHelpers.delay(1000);
- it('should dismiss network education modal', async () => {
- await Assertions.expectElementToBeVisible(NetworkEducationModal.container);
- await NetworkEducationModal.tapGotItButton();
- await Assertions.expectElementToNotBeVisible(
- NetworkEducationModal.container,
- );
- });
+ // Then scroll to ChainLink Token with extra stability
+ await WalletView.scrollToToken('ChainLink Token');
+ await TestHelpers.delay(1500); // Extra time for scroll to complete
- it('should Import custom token', async () => {
- await WalletView.tapImportTokensButton();
- await ImportTokensView.switchToCustomTab();
- // choose network here
- await ImportTokensView.tapOnNetworkInput();
- await ImportTokensView.tapNetworkOption('Sepolia');
- await ImportTokensView.typeTokenAddress(TOKEN_ADDRESS);
- await ImportTokensView.tapSymbolInput();
- await ImportTokensView.tapTokenSymbolText();
- await ImportTokensView.scrollDownOnImportCustomTokens();
- await ImportTokensView.tapOnNextButton();
- await ConfirmAddAssetView.tapOnConfirmButton();
- await Assertions.expectElementToBeVisible(WalletView.container);
- });
-
- it('should send token to address via asset overview screen', async () => {
- // Scroll to top first to ensure consistent starting position
- await WalletView.scrollToBottomOfTokensList();
- await TestHelpers.delay(1000);
-
- // Then scroll to ChainLink Token with extra stability
- await WalletView.scrollToToken('ChainLink Token');
- await TestHelpers.delay(1500); // Extra time for scroll to complete
-
- await WalletView.tapOnToken('ChainLink Token');
- await TestHelpers.delay(3500);
- await TokenOverview.scrollOnScreen();
- await TestHelpers.delay(3500);
- await TokenOverview.tapSendButton();
- await SendView.inputAddress(SEND_ADDRESS);
- await TestHelpers.delay(1000);
- await SendView.tapNextButton();
- await AmountView.typeInTransactionAmount('0.000001');
- await TestHelpers.delay(5000);
- await AmountView.tapNextButton();
- await Assertions.expectTextDisplayed('< 0.00001 LINK');
- await TransactionConfirmationView.tapConfirmButton();
- // await Assertions.expectTextDisplayed('Transaction submitted'); removing this assertion for now
+ await WalletView.tapOnToken('ChainLink Token');
+ await TestHelpers.delay(3500);
+ await TokenOverview.scrollOnScreen();
+ await TestHelpers.delay(3500);
+ await TokenOverview.tapSendButton();
+ await SendView.inputAddress(SEND_ADDRESS);
+ await TestHelpers.delay(1000);
+ await SendView.tapNextButton();
+ await AmountView.typeInTransactionAmount('0.000001');
+ await TestHelpers.delay(5000);
+ await AmountView.tapNextButton();
+ await Assertions.expectTextDisplayed('< 0.00001 LINK');
+ await TransactionConfirmationView.tapConfirmButton();
+ // await Assertions.expectTextDisplayed('Transaction submitted'); removing this assertion for now
+ },
+ );
});
});
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 244da3cd1ef..51d31f9f4aa 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -160,7 +160,7 @@
"or": "OR",
"import_existing_wallet": "Import existing wallet",
"bottom_sheet_title": "Choose an option to continue",
- "continue_with_srp": "Continue with Secret Recovery Phrase",
+ "continue_with_srp": "Use Secret Recovery Phrase",
"import_srp": "Import using Secret Recovery Phrase",
"sign_in_with_google": "Sign in with Google",
"sign_in_with_apple": "Sign in with Apple",
@@ -2504,6 +2504,15 @@
"cancel": "Cancel",
"confirm": "Confirm"
},
+ "gas_fee_token_modal": {
+ "title": "Select a token",
+ "title_pay_eth": "Pay with ETH",
+ "native_toggle_wallet": "Pay for network fee using the balance in your wallet.",
+ "list_balance": "Bal:",
+ "insufficient_balance": "Insufficient funds",
+ "native_toggle_metamask": "MetaMask is supplementing the balance to complete this transaction.",
+ "title_pay_with_other_tokens": "Pay with other tokens"
+ },
"transaction": {
"transaction_id": "Transaction ID",
"alert": "ALERT",
@@ -5275,7 +5284,6 @@
"account_name": "Account Name",
"networks": "Networks",
"account_address": "Account Address",
- "networks": "Networks",
"wallet": "Wallet",
"private_key": "Private key",
"private_keys": "Private keys",
@@ -5291,6 +5299,18 @@
"receiving_address": "Receiving address",
"copied": "Address copied"
},
+ "private_key_list": {
+ "list_title": "Private keys",
+ "warning_title": "Don't share your private key",
+ "warning_description": "This key grants full control of your account for the associated chain.",
+ "learn_more": "Learn more",
+ "enter_password": "Enter your password",
+ "password_placeholder": "Password",
+ "wrong_password": "Wrong password",
+ "copied": "Private key copied",
+ "continue": "Continue",
+ "cancel": "Cancel"
+ },
"accounts_list": {
"details": "Details"
},
diff --git a/shim.js b/shim.js
index 7298d42ae3c..ca3a7378e2e 100644
--- a/shim.js
+++ b/shim.js
@@ -134,6 +134,7 @@ if (enableApiCallLogs || isTest) {
)
.then((res) => res.ok)
.catch(() => false);
+
// if mockServer is off we route to original destination
global.fetch = async (url, options) =>
isMockServerAvailable
@@ -142,5 +143,63 @@ if (enableApiCallLogs || isTest) {
options,
).catch(() => originalFetch(url, options))
: originalFetch(url, options);
+
+ if (isMockServerAvailable) {
+ // Patch XMLHttpRequest for Axios and other libraries
+ const OriginalXHR = global.XMLHttpRequest;
+
+ if (OriginalXHR) {
+ global.XMLHttpRequest = function (...args) {
+ const xhr = new OriginalXHR(...args);
+ const originalOpen = xhr.open;
+
+ xhr.open = function (method, url, ...openArgs) {
+ try {
+ // Route external URLs through mock server proxy
+ if (
+ typeof url === 'string' &&
+ (url.startsWith('http://') || url.startsWith('https://'))
+ ) {
+ if (
+ !url.includes(`localhost:${mockServerPort}`) &&
+ !url.includes('/proxy')
+ ) {
+ const originalUrl = url;
+ url = `${MOCKTTP_URL}/proxy?url=${encodeURIComponent(url)}`;
+ }
+ }
+ return originalOpen.call(this, method, url, ...openArgs);
+ } catch (error) {
+ return originalOpen.call(this, method, url, ...openArgs);
+ }
+ };
+
+ return xhr;
+ };
+
+ // Copy static properties and prototype chain
+ try {
+ Object.setPrototypeOf(global.XMLHttpRequest, OriginalXHR);
+ Object.assign(global.XMLHttpRequest, OriginalXHR);
+
+ // Store reference to verify patching worked
+ global.__MOCK_XHR_PATCHED = true;
+ global.__ORIGINAL_XHR = OriginalXHR;
+
+ // eslint-disable-next-line no-console
+ console.log(
+ '[XHR Patch] Successfully patched XMLHttpRequest for E2E testing',
+ );
+ } catch (error) {
+ console.warn('[XHR Patch] Failed to copy XHR properties:', error);
+ // Restore original if copying failed
+ global.XMLHttpRequest = OriginalXHR;
+ }
+ } else {
+ console.warn(
+ '[XHR Patch] XMLHttpRequest not available, skipping patch',
+ );
+ }
+ }
})();
}