diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2d9eedf316c..f4242412746 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -335,3 +335,16 @@ jobs:
echo "All jobs passed step skipped. Block PR."
exit 1
fi
+
+ log-merge-group-failure:
+ name: Log merge group failure
+ # Only run this job if the merge group event fails, skip on forks
+ if: ${{ github.event_name == 'merge_group' && failure() && !github.event.repository.fork }}
+ needs:
+ - check-all-jobs-pass
+ uses: metamask/github-tools/.github/workflows/log-merge-group-failure.yml@6bbad335a01fce1a9ec1eabd9515542c225d46c0
+ secrets:
+ GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
+ GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }}
+ SPREADSHEET_ID: ${{ secrets.GOOGLE_MERGE_QUEUE_SPREADSHEET_ID }}
+ SHEET_NAME: ${{ secrets.GOOGLE_MERGE_QUEUE_SHEET_NAME }}
diff --git a/.js.env.example b/.js.env.example
index 38daeab2a2c..0d1d2764861 100644
--- a/.js.env.example
+++ b/.js.env.example
@@ -66,6 +66,9 @@ export SEGMENT_FLUSH_INTERVAL="1"
# example for flush when 1 event is queued
export SEGMENT_FLUSH_EVENT_LIMIT="1"
+# URL of the decoding API used to provide additional data from signature requests
+export DECODING_API_URL: 'https://signature-insights.api.cx.metamask.io/v1'
+
# URL of security alerts API used to validate dApp requests.
export SECURITY_ALERTS_API_URL="https://security-alerts.api.cx.metamask.io"
diff --git a/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.styles.ts b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.styles.ts
new file mode 100644
index 00000000000..a5d937a948c
--- /dev/null
+++ b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.styles.ts
@@ -0,0 +1,49 @@
+// Third party dependencies.
+import { StyleSheet } from 'react-native';
+
+// External dependencies.
+import { Theme } from '../../../../util/theme/models';
+
+/**
+ * Style sheet input parameters.
+ */
+export interface ButtonPillStyleSheetVars {
+ isDisabled: boolean;
+ isPressed: boolean;
+}
+
+/**
+ * Style sheet function for ButtonPill component
+ *
+ * @param params Style sheet params
+ * @param params.theme Theme object
+ * @param params.vars Arbitrary inputs this style sheet depends on
+ * @returns StyleSheet object
+ */
+const styleSheet = (params: {
+ theme: Theme;
+ vars: ButtonPillStyleSheetVars;
+}) => {
+ const {
+ theme: { colors },
+ vars: { isDisabled, isPressed }
+ } = params;
+
+ return StyleSheet.create({
+ base: {
+ backgroundColor: colors.background.alternative,
+ color: colors.text.default,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderRadius: 99,
+ opacity: isDisabled ? 0.5 : 1,
+ ...(isPressed && {
+ backgroundColor: colors.background.alternativePressed,
+ }),
+ },
+ });
+};
+
+export default styleSheet;
diff --git a/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.test.tsx b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.test.tsx
new file mode 100644
index 00000000000..41933943f63
--- /dev/null
+++ b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.test.tsx
@@ -0,0 +1,15 @@
+// Third party dependencies.
+import React from 'react';
+import { render } from '@testing-library/react-native';
+
+// Internal dependencies.
+import ButtonPill from './ButtonPill';
+
+describe('ButtonPill', () => {
+ it('should render correctly', () => {
+ const { toJSON } = render(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.tsx b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.tsx
new file mode 100644
index 00000000000..2b158cdce6d
--- /dev/null
+++ b/app/component-library/components-temp/Buttons/ButtonPill/ButtonPill.tsx
@@ -0,0 +1,69 @@
+// Third party dependencies.
+import React, { useCallback, useState } from 'react';
+import { GestureResponderEvent, TouchableOpacity, TouchableOpacityProps } from 'react-native';
+
+// External dependencies.
+import { useStyles } from '../../../hooks';
+
+// Internal dependencies.
+import stylesheet from './ButtonPill.styles';
+
+/**
+ * ButtonPill component props.
+ */
+export interface ButtonPillProps extends TouchableOpacityProps {
+ /**
+ * Optional param to disable the button.
+ */
+ isDisabled?: boolean;
+}
+
+const ButtonPill = ({
+ onPress,
+ onPressIn,
+ onPressOut,
+ style,
+ isDisabled = false,
+ children,
+ ...props
+}: ButtonPillProps) => {
+ const [isPressed, setIsPressed] = useState(false);
+ const { styles } = useStyles(stylesheet, {
+ style,
+ isPressed,
+ isDisabled,
+ });
+
+ const triggerOnPressedIn = useCallback(
+ (e: GestureResponderEvent) => {
+ setIsPressed(true);
+ onPressIn?.(e);
+ },
+ [setIsPressed, onPressIn],
+ );
+
+ const triggerOnPressedOut = useCallback(
+ (e: GestureResponderEvent) => {
+ setIsPressed(false);
+ onPressOut?.(e);
+ },
+ [setIsPressed, onPressOut],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ButtonPill;
diff --git a/app/component-library/components-temp/Buttons/ButtonPill/__snapshots__/ButtonPill.test.tsx.snap b/app/component-library/components-temp/Buttons/ButtonPill/__snapshots__/ButtonPill.test.tsx.snap
new file mode 100644
index 00000000000..c3e3b781a54
--- /dev/null
+++ b/app/component-library/components-temp/Buttons/ButtonPill/__snapshots__/ButtonPill.test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ButtonPill should render correctly 1`] = `
+
+`;
diff --git a/app/component-library/components-temp/Buttons/ButtonPill/index.ts b/app/component-library/components-temp/Buttons/ButtonPill/index.ts
new file mode 100644
index 00000000000..d983c349bc5
--- /dev/null
+++ b/app/component-library/components-temp/Buttons/ButtonPill/index.ts
@@ -0,0 +1 @@
+export { default } from './ButtonPill';
diff --git a/app/components/Base/StatusText.js b/app/components/Base/StatusText.js
index 7f337970011..dff6bae8d50 100644
--- a/app/components/Base/StatusText.js
+++ b/app/components/Base/StatusText.js
@@ -17,7 +17,7 @@ const styles = StyleSheet.create({
export const ConfirmedText = (props) => (
{
const { colors } = useTheme();
return (
= ({ address }) => {
+const UnknownEthereumAddress: React.FC<{ address: string, style?: ViewStyle }> = ({ address, style }) => {
const displayNameVariant = DisplayNameVariant.Unknown;
const { styles } = useStyles(styleSheet, { displayNameVariant });
+
return (
-
+
{renderShortAddress(address, 5)}
@@ -52,6 +53,7 @@ const Name: React.FC = ({
type,
value,
variation,
+ style,
}) => {
if (type !== NameType.EthereumAddress) {
throw new Error('Unsupported NameType: ' + type);
@@ -69,11 +71,11 @@ const Name: React.FC = ({
});
if (variant === DisplayNameVariant.Unknown) {
- return ;
+ return ;
}
return (
-
+
;
+export enum TokenStandard {
+ /** A token that conforms to the ERC20 standard. */
+ ERC20 = 'ERC20',
+ /** A token that conforms to the ERC721 standard. */
+ ERC721 = 'ERC721',
+ /** A token that conforms to the ERC1155 standard. */
+ ERC1155 = 'ERC1155',
+ /** Not a token, but rather the base asset of the selected chain. */
+ none = 'NONE',
+}
+
/**
* Describes a change in an asset's balance to a user's wallet.
*/
diff --git a/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap b/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap
index db124959996..9f098027945 100644
--- a/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap
+++ b/app/components/UI/SliderButton/__snapshots__/index.test.tsx.snap
@@ -18,7 +18,6 @@ exports[`SliderButton should render correctly 1`] = `
undefined,
]
}
- testID="swipe-to-swap-button"
>
({
context: {
NetworkController: {
+ setActiveNetwork: jest.fn(() => Promise.resolve()),
getNetworkClientById: () => ({
configuration: {
chainId: '0x1',
@@ -55,14 +58,21 @@ jest.mock('../../../../../core/Engine', () => ({
jest.mock('../../hooks/useStakingEligibility', () => ({
__esModule: true,
- default: () => ({
+ default: jest.fn(() => ({
isEligible: true,
- loading: false,
- error: null,
- refreshPooledStakingEligibility: jest
- .fn()
- .mockResolvedValueOnce({ isEligible: true }),
- }),
+ isLoadingEligibility: false,
+ refreshPooledStakingEligibility: jest.fn().mockResolvedValue({
+ isEligible: true,
+ }),
+ error: false,
+ })),
+}));
+
+jest.mock('../../hooks/useStakingChain', () => ({
+ __esModule: true,
+ default: jest.fn(() => ({
+ isStakingSupportedChain: true,
+ })),
}));
const STATE_MOCK = {
@@ -77,9 +87,9 @@ const STATE_MOCK = {
},
};
-const renderComponent = () =>
+const renderComponent = (state = STATE_MOCK) =>
renderWithProvider(, {
- state: STATE_MOCK,
+ state,
});
describe('StakeButton', () => {
@@ -92,11 +102,62 @@ describe('StakeButton', () => {
expect(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON)).toBeDefined();
});
+ it('navigates to Web view when stake button is pressed and user is not eligible', async () => {
+ (useStakingEligibility as jest.Mock).mockReturnValue({
+ isEligible: false,
+ isLoadingEligibility: false,
+ refreshPooledStakingEligibility: jest
+ .fn()
+ .mockResolvedValue({ isEligible: false }),
+ error: false,
+ });
+ const { getByTestId } = renderComponent();
+
+ fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
+ await waitFor(() => {
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.BROWSER.HOME, {
+ params: {
+ newTabUrl: `${AppConstants.STAKE.URL}?metamaskEntry=mobile`,
+ timestamp: expect.any(Number),
+ },
+ screen: Routes.BROWSER.VIEW,
+ });
+ });
+ });
+
it('navigates to Stake Input screen when stake button is pressed and user is eligible', async () => {
+ (useStakingEligibility as jest.Mock).mockReturnValue({
+ isEligible: true,
+ isLoadingEligibility: false,
+ refreshPooledStakingEligibility: jest
+ .fn()
+ .mockResolvedValue({ isEligible: true }),
+ error: false,
+ });
const { getByTestId } = renderComponent();
fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
+ await waitFor(() => {
+ expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
+ screen: Routes.STAKING.STAKE,
+ });
+ });
+ });
+ it('navigates to Stake Input screen when on unsupported network', async () => {
+ const UNSUPPORTED_NETWORK_STATE = {
+ engine: {
+ backgroundState: {
+ NetworkController: {
+ ...mockNetworkState({
+ chainId: '0x89', // Polygon
+ }),
+ },
+ },
+ },
+ };
+ const { getByTestId } = renderComponent(UNSUPPORTED_NETWORK_STATE);
+ fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
screen: Routes.STAKING.STAKE,
diff --git a/app/components/UI/Tokens/index.test.tsx b/app/components/UI/Tokens/index.test.tsx
index 838143c7b21..a68c483475c 100644
--- a/app/components/UI/Tokens/index.test.tsx
+++ b/app/components/UI/Tokens/index.test.tsx
@@ -8,12 +8,9 @@ import { createStackNavigator } from '@react-navigation/stack';
import { getAssetTestId } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds';
import { backgroundState } from '../../../util/test/initial-root-state';
import { strings } from '../../../../locales/i18n';
-import AppConstants from '../../../../app/core/AppConstants';
-import Routes from '../../../../app/constants/navigation/Routes';
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
import Engine from '../../../core/Engine';
import { createTokensBottomSheetNavDetails } from './TokensBottomSheet';
-import useStakingEligibility from '../Stake/hooks/useStakingEligibility';
// eslint-disable-next-line import/no-namespace
import * as networks from '../../../util/networks';
// eslint-disable-next-line import/no-namespace
@@ -414,54 +411,6 @@ describe('Tokens', () => {
).toBeDefined();
});
- it('renders stake button correctly', () => {
- const { getByTestId } = renderComponent(initialState);
-
- expect(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON)).toBeDefined();
- });
-
- it('navigates to Web view when stake button is pressed and user is not eligible', async () => {
- (useStakingEligibility as jest.Mock).mockReturnValue({
- isEligible: false,
- isLoadingEligibility: false,
- refreshPooledStakingEligibility: jest
- .fn()
- .mockResolvedValue({ isEligible: false }),
- error: false,
- });
- const { getByTestId } = renderComponent(initialState);
-
- fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith(Routes.BROWSER.HOME, {
- params: {
- newTabUrl: `${AppConstants.STAKE.URL}?metamaskEntry=mobile`,
- timestamp: 123,
- },
- screen: Routes.BROWSER.VIEW,
- });
- });
- });
-
- it('navigates to Stake Input screen when stake button is pressed and user is eligible', async () => {
- (useStakingEligibility as jest.Mock).mockReturnValue({
- isEligible: true,
- isLoadingEligibility: false,
- refreshPooledStakingEligibility: jest
- .fn()
- .mockResolvedValue({ isEligible: true }),
- error: false,
- });
- const { getByTestId } = renderComponent(initialState);
-
- fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
- screen: Routes.STAKING.STAKE,
- });
- });
- });
-
it('should refresh tokens and call necessary controllers', async () => {
const { getByTestId } = renderComponent(initialState);
@@ -513,48 +462,6 @@ describe('Tokens', () => {
});
});
- it('navigates to Stake Input screen only when eligible', async () => {
- (useStakingEligibility as jest.Mock).mockReturnValue({
- isEligible: true,
- isLoadingEligibility: false,
- refreshPooledStakingEligibility: jest
- .fn()
- .mockResolvedValue({ isEligible: true }),
- error: false,
- });
-
- const { getByTestId } = renderComponent(initialState);
-
- fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
-
- await waitFor(() => {
- expect(mockNavigate).toHaveBeenCalledWith('StakeScreens', {
- screen: Routes.STAKING.STAKE,
- });
- });
- });
-
- it('does not navigate to Stake Input screen if not eligible', async () => {
- (useStakingEligibility as jest.Mock).mockReturnValue({
- isEligible: false,
- isLoadingEligibility: false,
- refreshPooledStakingEligibility: jest
- .fn()
- .mockResolvedValue({ isEligible: false }),
- error: false,
- });
-
- const { getByTestId } = renderComponent(initialState);
-
- fireEvent.press(getByTestId(WalletViewSelectorsIDs.STAKE_BUTTON));
-
- await waitFor(() => {
- expect(mockNavigate).not.toHaveBeenCalledWith('StakeScreens', {
- screen: Routes.STAKING.STAKE,
- });
- });
- });
-
it('calls onRefresh and updates state', async () => {
const { getByTestId } = renderComponent(initialState);
diff --git a/app/components/Views/confirmations/Confirm/Confirm.test.tsx b/app/components/Views/confirmations/Confirm/Confirm.test.tsx
index 8007a4c9866..6f6da173c48 100644
--- a/app/components/Views/confirmations/Confirm/Confirm.test.tsx
+++ b/app/components/Views/confirmations/Confirm/Confirm.test.tsx
@@ -57,7 +57,7 @@ describe('Confirm', () => {
expect(getByText('Estimated changes')).toBeDefined();
expect(
getByText(
- 'You’re signing into a site and there are no predicted changes to your account.',
+ "You're signing into a site and there are no predicted changes to your account.",
),
).toBeDefined();
expect(getByText('Request from')).toBeDefined();
diff --git a/app/components/Views/confirmations/Confirm/Confirm.tsx b/app/components/Views/confirmations/Confirm/Confirm.tsx
index 8149098a80a..4a18a99d7d1 100644
--- a/app/components/Views/confirmations/Confirm/Confirm.tsx
+++ b/app/components/Views/confirmations/Confirm/Confirm.tsx
@@ -8,7 +8,7 @@ import Footer from '../components/Confirm/Footer';
import Info from '../components/Confirm/Info';
import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBanner';
import Title from '../components/Confirm/Title';
-import useConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled';
+import { useConfirmationRedesignEnabled } from '../hooks/useConfirmationRedesignEnabled';
import styleSheet from './Confirm.styles';
const Confirm = () => {
@@ -20,7 +20,7 @@ const Confirm = () => {
}
return (
-
+
diff --git a/app/components/Views/confirmations/Send/index.js b/app/components/Views/confirmations/Send/index.js
index ad5fb240be5..906ef236b58 100644
--- a/app/components/Views/confirmations/Send/index.js
+++ b/app/components/Views/confirmations/Send/index.js
@@ -185,8 +185,8 @@ class Send extends PureComponent {
* Resets gas and gasPrice of transaction
*/
async reset() {
- const { transaction } = this.props;
- const { gas, gasPrice } = await estimateGas(transaction);
+ const { networkClientId, transaction } = this.props;
+ const { gas, gasPrice } = await estimateGas(transaction, networkClientId);
this.props.setTransactionObject({
gas: hexToBN(gas),
gasPrice: hexToBN(gasPrice),
@@ -320,8 +320,6 @@ class Send extends PureComponent {
handleNewTxMeta = async ({
target_address,
action,
- chain_id = null,
- function_name = null, // eslint-disable-line no-unused-vars
parameters = null,
}) => {
const { addressBook, chainId, internalAccounts, selectedAddress } =
@@ -408,7 +406,10 @@ class Send extends PureComponent {
// if gas and gasPrice is not defined in the deeplink, we should define them
if (!gas && !gasPrice) {
- const { gas, gasPrice } = await estimateGas(this.props.transaction);
+ const { gas, gasPrice } = await estimateGas(
+ this.props.transaction,
+ this.props.networkClientId,
+ );
newTxMeta = {
...newTxMeta,
gas,
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx
index 0c533f651c9..31af8bd8955 100644
--- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx
@@ -12,7 +12,7 @@ describe('TypedSignV1', () => {
expect(getByText('Estimated changes')).toBeDefined();
expect(
getByText(
- 'You’re signing into a site and there are no predicted changes to your account.',
+ "You're signing into a site and there are no predicted changes to your account.",
),
).toBeDefined();
expect(getByText('Request from')).toBeDefined();
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx
new file mode 100644
index 00000000000..6f015ab9b16
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import cloneDeep from 'lodash/cloneDeep';
+import {
+ DecodingData,
+ DecodingDataChangeType,
+ DecodingDataStateChanges,
+ SignatureRequest,
+} from '@metamask/signature-controller';
+import useGetTokenStandardAndDetails from '../../../../../hooks/useGetTokenStandardAndDetails';
+import { typedSignV4ConfirmationState } from '../../../../../../../../util/test/confirm-data-helpers';
+import renderWithProvider from '../../../../../../../../util/test/renderWithProvider';
+import { memoizedGetTokenStandardAndDetails } from '../../../../../utils/token';
+import TypedSignV3V4Simulation from './Simulation';
+
+jest.mock('../../../../../hooks/useGetTokenStandardAndDetails');
+
+jest.mock('../../../../../../../../core/Engine', () => ({
+ context: {
+ NetworkController: {
+ findNetworkClientIdByChainId: () => 'mainnet',
+ },
+ },
+}));
+
+const stateChangesApprove = [
+ {
+ assetType: 'ERC20',
+ changeType: DecodingDataChangeType.Approve,
+ address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad',
+ amount: '12345',
+ contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ },
+];
+
+const mockState = (
+ mockStateChanges: DecodingDataStateChanges,
+ {
+ mockDecodingDataProps,
+ stubDecodingLoading = false,
+ }: {
+ mockDecodingDataProps?: Partial;
+ stubDecodingLoading?: boolean;
+ } = {
+ mockDecodingDataProps: {},
+ stubDecodingLoading: false,
+ },
+) => {
+ const clonedMockState = cloneDeep(typedSignV4ConfirmationState);
+ const request = clonedMockState.engine.backgroundState.SignatureController
+ .signatureRequests[
+ 'fb2029e1-b0ab-11ef-9227-05a11087c334'
+ ] as SignatureRequest;
+
+ request.decodingLoading = stubDecodingLoading;
+ request.decodingData = {
+ ...mockDecodingDataProps,
+ stateChanges: mockStateChanges,
+ };
+
+ return clonedMockState;
+};
+
+describe('PermitSimulation', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+
+ /** Reset memoized function using getTokenStandardAndDetails for each test */
+ memoizedGetTokenStandardAndDetails?.cache?.clear?.();
+ });
+
+ it('renders DecodedSimulation loader if decodingLoading is true', async () => {
+ const { queryByTestId } = renderWithProvider(, {
+ state: mockState(stateChangesApprove, {
+ stubDecodingLoading: true,
+ }),
+ });
+
+ expect(await queryByTestId('confirm-v3v4-simulation-loader')).toBeDefined();
+ });
+
+ it('renders DecodingSimulation with "Unavailable" if decoding data is empty', async () => {
+ const { getByText } = renderWithProvider(, {
+ state: mockState([]),
+ });
+
+ expect(await getByText('Estimated changes')).toBeDefined();
+ expect(await getByText('Unavailable')).toBeDefined();
+ });
+
+ it('renders DecodingSimulation for permits', async () => {
+ (
+ useGetTokenStandardAndDetails as jest.MockedFn<
+ typeof useGetTokenStandardAndDetails
+ >
+ ).mockReturnValue({
+ symbol: 'TST',
+ decimals: '4',
+ balance: undefined,
+ standard: 'ERC20',
+ decimalsNumber: 4,
+ });
+
+ const { getByText } = renderWithProvider(, {
+ state: mockState(stateChangesApprove),
+ });
+
+ expect(await getByText('Estimated changes')).toBeDefined();
+ expect(await getByText('Spending cap')).toBeDefined();
+ expect(await getByText('1.235')).toBeDefined();
+ });
+
+ it('renders PermitSimulation if decoding api returns error', async () => {
+ (
+ useGetTokenStandardAndDetails as jest.MockedFn<
+ typeof useGetTokenStandardAndDetails
+ >
+ ).mockReturnValue({
+ symbol: 'TST',
+ decimals: '2',
+ balance: undefined,
+ standard: 'ERC20',
+ decimalsNumber: 4,
+ });
+
+ const { getByText } = renderWithProvider(, {
+ state: mockState([], {
+ mockDecodingDataProps: {
+ error: { message: 'some error', type: 'SOME_ERROR' },
+ } as Partial,
+ }),
+ });
+
+ expect(await getByText('Estimated changes')).toBeDefined();
+ expect(await getByText('Spending cap')).toBeDefined();
+ expect(await getByText('0.3')).toBeDefined();
+ expect(
+ await getByText(
+ "You're giving the spender permission to spend this many tokens from your account.",
+ ),
+ ).toBeDefined();
+ });
+});
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx
new file mode 100644
index 00000000000..37265d0d0cd
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { useTypedSignSimulationEnabled } from '../../../../../hooks/useTypedSignSimulationEnabled';
+import { isRecognizedPermit } from '../../../../../utils/signature';
+import { useSignatureRequest } from '../../../../../hooks/useSignatureRequest';
+import DecodedSimulation from './TypedSignDecoded';
+import PermitSimulation from './TypedSignPermit';
+
+const TypedSignV3V4Simulation: React.FC
+ )
+ }
+ isLoading={decodingLoading}
+ isCollapsed={decodingLoading || !stateChangeFragment.length}
+ />
+ );
+};
+
+export default DecodedSimulation;
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts
new file mode 100644
index 00000000000..f4bd7dcc026
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts
@@ -0,0 +1 @@
+export { default } from './TypedSignDecoded';
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx
new file mode 100644
index 00000000000..e7ff8ddf91d
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import renderWithProvider from '../../../../../../../../../util/test/renderWithProvider';
+import { typedSignV4ConfirmationState } from '../../../../../../../../../util/test/confirm-data-helpers';
+import PermitSimulation from './TypedSignPermit';
+
+jest.mock('../../../../../../../../../core/Engine', () => ({
+ context: {
+ NetworkController: {
+ findNetworkClientIdByChainId: () => 'mainnet',
+ },
+ },
+}));
+
+describe('PermitSimulation', () => {
+ it('should render correctly for personal sign', async () => {
+ const { getByText } = renderWithProvider(, {
+ state: typedSignV4ConfirmationState,
+ });
+
+ expect(getByText('Estimated changes')).toBeDefined();
+ expect(
+ getByText(
+ "You're giving the spender permission to spend this many tokens from your account.",
+ ),
+ ).toBeDefined();
+ expect(getByText('Spending cap')).toBeDefined();
+ expect(getByText('3,000')).toBeDefined();
+ expect(getByText('0xCcCCc...ccccC')).toBeDefined();
+ });
+});
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.tsx
new file mode 100644
index 00000000000..bb326cb1463
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Hex } from '@metamask/utils';
+
+import { strings } from '../../../../../../../../../../locales/i18n';
+import { useStyles } from '../../../../../../../../../component-library/hooks';
+import Engine from '../../../../../../../../../core/Engine';
+import { safeToChecksumAddress } from '../../../../../../../../../util/address';
+import { PrimaryType } from '../../../../../../constants/signatures';
+import { useSignatureRequest } from '../../../../../../hooks/useSignatureRequest';
+import { parseTypedDataMessage } from '../../../../../../utils/signature';
+import InfoRow from '../../../../../UI/InfoRow';
+import InfoSection from '../../../../../UI/InfoRow/InfoSection';
+import PermitSimulationValueDisplay from '../components/ValueDisplay';
+
+const styleSheet = () =>
+ StyleSheet.create({
+ permitValues: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+ },
+ });
+
+function extractTokenDetailsByPrimaryType(
+ message: Record,
+ primaryType: PrimaryType,
+): object[] | unknown {
+ let tokenDetails;
+
+ switch (primaryType) {
+ case PrimaryType.PermitBatch:
+ case PrimaryType.PermitSingle:
+ tokenDetails = message?.details;
+ break;
+ case PrimaryType.PermitBatchTransferFrom:
+ case PrimaryType.PermitTransferFrom:
+ tokenDetails = message?.permitted;
+ break;
+ default:
+ break;
+ }
+
+ const isNonArrayObject = tokenDetails && !Array.isArray(tokenDetails);
+ return isNonArrayObject ? [tokenDetails] : tokenDetails;
+}
+
+const PermitSimulation = () => {
+ const { NetworkController } = Engine.context;
+ const { styles } = useStyles(styleSheet, {});
+
+ const signatureRequest = useSignatureRequest();
+
+ const chainId = signatureRequest?.chainId as Hex;
+ const msgData = signatureRequest?.messageParams?.data;
+
+ const networkClientId = NetworkController.findNetworkClientIdByChainId(
+ chainId as Hex,
+ );
+
+ if (!msgData) {
+ return null;
+ }
+
+ const {
+ domain: { verifyingContract },
+ message,
+ message: { tokenId },
+ primaryType,
+ } = parseTypedDataMessage(msgData as string);
+
+ const tokenDetails = extractTokenDetailsByPrimaryType(message, primaryType);
+
+ const isNFT = tokenId !== undefined;
+ const labelChangeType = isNFT
+ ? strings('confirm.simulation.label_change_type_permit_nft')
+ : strings('confirm.simulation.label_change_type_permit');
+
+ return (
+
+
+ {strings('confirm.simulation.info_permit')}
+
+
+
+ {Array.isArray(tokenDetails) ? (
+
+ {tokenDetails.map(
+ (
+ { token, amount }: { token: string; amount: string },
+ i: number,
+ ) => (
+
+ ),
+ )}
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export default PermitSimulation;
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/index.ts
new file mode 100644
index 00000000000..2dd1a7b5f59
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/index.ts
@@ -0,0 +1 @@
+export { default } from './TypedSignPermit';
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx
new file mode 100644
index 00000000000..f0ab367e2ee
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import NativeValueDisplay from './NativeValueDisplay';
+import { backgroundState } from '../../../../../../../../../../util/test/initial-root-state';
+import renderWithProvider from '../../../../../../../../../../util/test/renderWithProvider';
+import { fireEvent } from '@testing-library/react-native';
+
+const mockInitialState = {
+ engine: {
+ backgroundState,
+ },
+};
+
+describe('NativeValueDisplay', () => {
+ it('renders component correctly', async () => {
+ const { findByText } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ expect(await findByText('< 0.000001')).toBeDefined();
+ expect(await findByText('ETH')).toBeDefined();
+ });
+
+ it('displays modal when clicking on the value', async () => {
+ const { findByText } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ const button = await findByText('< 0.000001');
+ fireEvent.press(button);
+
+ expect(await findByText('Spending Cap')).toBeDefined();
+ });
+});
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx
new file mode 100644
index 00000000000..54ad3cd28e8
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx
@@ -0,0 +1,153 @@
+import React, { useState } from 'react';
+import { Text,TouchableOpacity, View } from 'react-native';
+import { useSelector } from 'react-redux';
+import { Hex } from '@metamask/utils';
+import { BigNumber } from 'bignumber.js';
+
+import { RootState } from '../../../../../../../../../../reducers';
+import { selectConversionRateByChainId } from '../../../../../../../../../../selectors/currencyRateController';
+import { useTheme } from '../../../../../../../../../../util/theme';
+
+import ButtonPill from '../../../../../../../../../../component-library/components-temp/Buttons/ButtonPill/ButtonPill';
+import { ButtonIconSizes } from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types';
+import ButtonIcon from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon';
+import { IconName , IconColor } from '../../../../../../../../../../component-library/components/Icons/Icon';
+
+import AssetPill from '../../../../../../../../../UI/SimulationDetails/AssetPill/AssetPill';
+import { IndividualFiatDisplay } from '../../../../../../../../../UI/SimulationDetails/FiatDisplay/FiatDisplay';
+import {
+ formatAmount,
+ formatAmountMaxPrecision,
+} from '../../../../../../../../../UI/SimulationDetails/formatAmount';
+import { AssetType } from '../../../../../../../../../UI/SimulationDetails/types';
+import { shortenString } from '../../../../../../../../../../util/notifications/methods/common';
+import { isNumberValue } from '../../../../../../../../../../util/number';
+import { calcTokenAmount } from '../../../../../../../../../../util/transactions';
+import BottomModal from '../../../../../../UI/BottomModal';
+
+/**
+ * Reusing ValueDisplay styles for now. See issue to handle abstracting UI
+ * @see {@link https://github.com/MetaMask/metamask-mobile/issues/12974}
+ */
+import styleSheet from '../ValueDisplay/ValueDisplay.styles';
+
+const NATIVE_DECIMALS = 18;
+
+interface PermitSimulationValueDisplayParams {
+ /** ID of the associated chain. */
+ chainId: Hex;
+
+ /** Change type to be displayed in value tooltip */
+ labelChangeType: string;
+
+ /** The token amount */
+ value: number | string;
+
+ /** True if value is being credited to wallet */
+ credit?: boolean;
+
+ /** True if value is being debited to wallet */
+ debit?: boolean;
+}
+
+const NativeValueDisplay: React.FC = ({
+ chainId,
+ credit,
+ debit,
+ labelChangeType,
+ value,
+}) => {
+ const [hasValueModalOpen, setHasValueModalOpen] = useState(false);
+
+ const { colors } = useTheme();
+ const styles = styleSheet(colors);
+
+ const conversionRate = useSelector((state: RootState) =>
+ selectConversionRateByChainId(state, chainId),
+ );
+
+ const tokenAmount = isNumberValue(value) ? calcTokenAmount(value, NATIVE_DECIMALS) : null;
+ const isValidTokenAmount = tokenAmount !== null && tokenAmount !== undefined && tokenAmount instanceof BigNumber;
+
+ const fiatValue = isValidTokenAmount && conversionRate
+ ? tokenAmount.times(String(conversionRate)).toNumber()
+ : undefined;
+
+ const tokenValue = isValidTokenAmount ? formatAmount('en-US', tokenAmount) : null;
+ const tokenValueMaxPrecision = isValidTokenAmount ? formatAmountMaxPrecision('en-US', tokenAmount) : null;
+
+ function handlePressTokenValue() {
+ setHasValueModalOpen(true);
+ }
+
+ return (
+
+
+
+
+
+ {credit && '+ '}
+ {debit && '- '}
+ {tokenValue !== null &&
+ shortenString(tokenValue || '', {
+ truncatedCharLimit: 15,
+ truncatedStartChars: 15,
+ truncatedEndChars: 0,
+ skipCharacterInEnd: true,
+ })}
+
+
+
+
+
+
+
+
+ {/**
+ TODO - add fiat shorten prop after tooltip logic has been updated
+ {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}
+ */}
+ {fiatValue !== undefined && (
+
+ )}
+
+ {hasValueModalOpen && (
+ /**
+ * TODO replace BottomModal instances with BottomSheet
+ * {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}}
+ */
+ setHasValueModalOpen(false)}>
+ setHasValueModalOpen(false)}
+ >
+
+
+ setHasValueModalOpen(false)}
+ iconName={IconName.ArrowLeft}
+ />
+
+ {labelChangeType}
+
+
+
+ {tokenValueMaxPrecision}
+
+
+
+
+ )}
+
+ );
+};
+
+export default NativeValueDisplay;
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts
new file mode 100644
index 00000000000..1e6534cfb1c
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts
@@ -0,0 +1 @@
+export { default } from './NativeValueDisplay';
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts
new file mode 100644
index 00000000000..9a1da0579cc
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts
@@ -0,0 +1,90 @@
+import { StyleSheet } from 'react-native';
+import { Theme } from '@metamask/design-tokens';
+import { fontStyles, colors as importedColors } from '../../../../../../../../../../styles/common';
+
+const styleSheet = (colors: Theme['colors']) =>
+ StyleSheet.create({
+ wrapper: {
+ marginLeft: 'auto',
+ maxWidth: '100%',
+ alignSelf: 'flex-end',
+ justifyContent: 'flex-end',
+ borderWidth: 0,
+ padding: 0,
+ },
+ fiatDisplay: {
+ paddingEnd: 8,
+ },
+ flexRowTokenValueAndAddress: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ borderColor: importedColors.transparent,
+ borderWidth: 0,
+ padding: 0,
+ },
+ marginStart4: {
+ marginStart: 4,
+ },
+ tokenValueTooltipContent: {
+ borderRadius: 12,
+ paddingHorizontal: 8,
+ paddingTop: 4,
+ paddingBottom: 4,
+ textAlign: 'center',
+ },
+ valueAndAddress: {
+ paddingVertical: 4,
+ paddingLeft: 8,
+ gap: 5,
+ flexDirection: 'row',
+ alignItems: 'center',
+ alignSelf: 'center',
+ },
+ valueIsCredit: {
+ backgroundColor: colors.success.muted,
+ color: colors.success.default,
+ },
+ valueIsDebit: {
+ backgroundColor: colors.error.muted,
+ color: colors.error.default,
+ },
+ valueModal: {
+ backgroundColor: colors.background.alternative,
+ paddingTop: 24,
+ paddingBottom: 34,
+ paddingHorizontal: 16,
+ borderTopLeftRadius: 8,
+ borderTopRightRadius: 8,
+ },
+ valueModalHeader: {
+ alignItems: 'center',
+ display: 'flex',
+ flexDirection: 'row',
+ paddingBottom: 16,
+ position: 'relative',
+ textAlign: 'center',
+ width: '100%',
+ },
+ valueModalHeaderIcon: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ },
+ valueModalHeaderText: {
+ color: colors.text.default,
+ ...fontStyles.bold,
+ fontSize: 14,
+ fontWeight: '700',
+ textAlign: 'center',
+ width: '100%',
+ // height of header icon
+ minHeight: 24,
+ },
+ valueModalText: {
+ textAlign: 'center',
+ },
+ });
+
+export default styleSheet;
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx
new file mode 100644
index 00000000000..8cf784909dc
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx
@@ -0,0 +1,185 @@
+import React from 'react';
+import { act } from '@testing-library/react-native';
+import SimulationValueDisplay from './ValueDisplay';
+
+import { memoizedGetTokenStandardAndDetails } from '../../../../../../../utils/token';
+import useGetTokenStandardAndDetails from '../../../../../../../hooks/useGetTokenStandardAndDetails';
+import { TokenStandard } from '../../../../../../../../../UI/SimulationDetails/types';
+import { getTokenDetails } from '../../../../../../../../../../util/address';
+import { backgroundState } from '../../../../../../../../../../util/test/initial-root-state';
+import renderWithProvider from '../../../../../../../../../../util/test/renderWithProvider';
+import { useMetrics } from '../../../../../../../../../hooks/useMetrics';
+import { MetricsEventBuilder } from '../../../../../../../../../../core/Analytics/MetricsEventBuilder';
+
+const mockInitialState = {
+ engine: {
+ backgroundState,
+ },
+};
+
+const mockTrackEvent = jest.fn();
+
+jest.mock('../../../../../../../../../hooks/useMetrics');
+jest.mock('../../../../../../../hooks/useGetTokenStandardAndDetails');
+
+jest.mock('../../../../../../../../../../util/address', () => ({
+ getTokenDetails: jest.fn(),
+ renderShortAddress: jest.requireActual(
+ '../../../../../../../../../../util/address',
+ ).renderShortAddress,
+}));
+
+describe('SimulationValueDisplay', () => {
+ beforeEach(() => {
+ (useMetrics as jest.MockedFn).mockReturnValue({
+ trackEvent: mockTrackEvent,
+ createEventBuilder: MetricsEventBuilder.createEventBuilder,
+ enable: jest.fn(),
+ addTraitsToUser: jest.fn(),
+ createDataDeletionTask: jest.fn(),
+ checkDataDeleteStatus: jest.fn(),
+ getDeleteRegulationCreationDate: jest.fn(),
+ getDeleteRegulationId: jest.fn(),
+ isDataRecorded: jest.fn(),
+ isEnabled: jest.fn(),
+ getMetaMetricsId: jest.fn(),
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+
+ /** Reset memoized function using getTokenStandardAndDetails for each test */
+ memoizedGetTokenStandardAndDetails?.cache?.clear?.();
+ });
+
+ it('renders component correctly', async () => {
+ (
+ useGetTokenStandardAndDetails as jest.MockedFn<
+ typeof useGetTokenStandardAndDetails
+ >
+ ).mockReturnValue({
+ symbol: 'TST',
+ decimals: '4',
+ balance: undefined,
+ standard: TokenStandard.ERC20,
+ decimalsNumber: 4,
+ });
+
+ const { findByText } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ expect(await findByText('0.432')).toBeDefined();
+ });
+
+ it('renders "Unlimited" for large values when canDisplayValueAsUnlimited is true', async () => {
+ (useGetTokenStandardAndDetails as jest.MockedFn).mockReturnValue({
+ symbol: 'TST',
+ decimals: '4',
+ balance: undefined,
+ standard: TokenStandard.ERC20,
+ decimalsNumber: 4,
+ });
+
+ const { findByText } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ expect(await findByText('Unlimited')).toBeDefined();
+ });
+
+ it('should invoke method to track missing decimal information for ERC20 tokens only once', async () => {
+ (
+ useGetTokenStandardAndDetails as jest.MockedFn<
+ typeof useGetTokenStandardAndDetails
+ >
+ ).mockReturnValue({
+ symbol: 'TST',
+ decimals: undefined,
+ balance: undefined,
+ standard: TokenStandard.ERC20,
+ decimalsNumber: 4,
+ });
+
+ renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ expect(mockTrackEvent).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not invoke method to track missing decimal information for ERC20 tokens', async () => {
+ (
+ useGetTokenStandardAndDetails as jest.MockedFn<
+ typeof useGetTokenStandardAndDetails
+ >
+ ).mockReturnValue({
+ symbol: 'TST',
+ decimals: '4',
+ balance: undefined,
+ standard: TokenStandard.ERC20,
+ decimalsNumber: 4,
+ });
+
+ renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ expect(mockTrackEvent).not.toHaveBeenCalled();
+ });
+
+ describe('when token is an ERC721 token', () => {
+ beforeEach(() => {
+ jest.mocked(getTokenDetails).mockResolvedValue({
+ name: 'TST',
+ symbol: 'TST',
+ standard: TokenStandard.ERC721,
+ });
+ });
+
+ it('should not invoke method to track missing decimal information', async () => {
+ renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(mockTrackEvent).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx
new file mode 100644
index 00000000000..44622075f16
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx
@@ -0,0 +1,210 @@
+import React, { useState } from 'react';
+import { TouchableOpacity, View } from 'react-native';
+import { useSelector } from 'react-redux';
+import { NetworkClientId } from '@metamask/network-controller';
+import { Hex } from '@metamask/utils';
+import { BigNumber } from 'bignumber.js';
+
+import ButtonPill from '../../../../../../../../../../component-library/components-temp/Buttons/ButtonPill/ButtonPill';
+import { ButtonIconSizes } from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types';
+import ButtonIcon from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon';
+import { IconName , IconColor } from '../../../../../../../../../../component-library/components/Icons/Icon';
+import Text from '../../../../../../../../../../component-library/components/Texts/Text';
+
+import { IndividualFiatDisplay } from '../../../../../../../../../UI/SimulationDetails/FiatDisplay/FiatDisplay';
+import {
+ formatAmount,
+ formatAmountMaxPrecision,
+} from '../../../../../../../../../UI/SimulationDetails/formatAmount';
+
+import Address from '../../../../../../UI/InfoRow/InfoValue/Address/Address';
+
+import { selectContractExchangeRates } from '../../../../../../../../../../selectors/tokenRatesController';
+
+import Logger from '../../../../../../../../../../util/Logger';
+import { shortenString } from '../../../../../../../../../../util/notifications/methods/common';
+import { isNumberValue } from '../../../../../../../../../../util/number';
+import { useTheme } from '../../../../../../../../../../util/theme';
+import { calcTokenAmount } from '../../../../../../../../../../util/transactions';
+
+import useGetTokenStandardAndDetails from '../../../../../../../hooks/useGetTokenStandardAndDetails';
+import useTrackERC20WithoutDecimalInformation from '../../../../../../../hooks/useTrackERC20WithoutDecimalInformation';
+import { TOKEN_VALUE_UNLIMITED_THRESHOLD } from '../../../../../../../utils/confirm';
+import { TokenDetailsERC20 } from '../../../../../../../utils/token';
+import BottomModal from '../../../../../../UI/BottomModal';
+
+import styleSheet from './ValueDisplay.styles';
+import { strings } from '../../../../../../../../../../../locales/i18n';
+
+interface SimulationValueDisplayParams {
+ /** ID of the associated chain. */
+ chainId: Hex;
+
+ /** Change type to be displayed in value tooltip */
+ labelChangeType: string;
+
+ /** The network client ID */
+ networkClientId?: NetworkClientId;
+
+ /**
+ * The ethereum token contract address. It is expected to be in hex format.
+ * We currently accept strings since we have a patch that accepts a custom string
+ * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch}
+ */
+ tokenContract: Hex | string | undefined;
+
+ // Optional
+
+ /** Whether a large amount can be substituted by "Unlimited" */
+ canDisplayValueAsUnlimited?: boolean;
+
+ /** True if value is being credited to wallet */
+ credit?: boolean;
+
+ /** True if value is being debited to wallet */
+ debit?: boolean;
+
+ /** The primaryType of the typed sign message */
+ primaryType?: string;
+
+ /** The tokenId for NFT */
+ tokenId?: string;
+
+ /** The token amount */
+ value?: number | string;
+}
+
+const SimulationValueDisplay: React.FC<
+ SimulationValueDisplayParams
+> = ({
+ chainId,
+ labelChangeType,
+ networkClientId,
+ primaryType,
+ tokenContract,
+ tokenId,
+ value,
+ credit,
+ debit,
+ canDisplayValueAsUnlimited = false,
+}) => {
+ const [hasValueModalOpen, setHasValueModalOpen] = useState(false);
+
+ const { colors } = useTheme();
+ const styles = styleSheet(colors);
+
+ const contractExchangeRates = useSelector(selectContractExchangeRates);
+ const exchangeRate =
+ tokenContract && contractExchangeRates
+ ? contractExchangeRates[tokenContract as `0x${string}`]?.price
+ : undefined;
+
+ const tokenDetails = useGetTokenStandardAndDetails(tokenContract, networkClientId);
+ const { decimalsNumber: tokenDecimals } = tokenDetails;
+
+ useTrackERC20WithoutDecimalInformation(
+ chainId,
+ tokenContract,
+ tokenDetails as TokenDetailsERC20,
+ );
+
+ const tokenAmount = isNumberValue(value) && !tokenId ? calcTokenAmount(value, tokenDecimals) : null;
+ const isValidTokenAmount = tokenAmount !== null && tokenAmount !== undefined && tokenAmount instanceof BigNumber;
+
+ const fiatValue = isValidTokenAmount && exchangeRate && !tokenId
+ ? tokenAmount.multipliedBy(exchangeRate).toNumber()
+ : undefined;
+
+ const tokenValue = isValidTokenAmount ? formatAmount('en-US', tokenAmount) : null;
+ const tokenValueMaxPrecision = isValidTokenAmount ? formatAmountMaxPrecision('en-US', tokenAmount) : null;
+
+ const shouldShowUnlimitedValue = canDisplayValueAsUnlimited &&
+ Number(value) > TOKEN_VALUE_UNLIMITED_THRESHOLD;
+
+ /** Temporary error capturing as we are building out Permit Simulations */
+ if (!tokenContract) {
+ Logger.error(
+ new Error(
+ `SimulationValueDisplay: Token contract address is missing where primaryType === ${primaryType}`,
+ ),
+ );
+ return null;
+ }
+
+ function handlePressTokenValue() {
+ setHasValueModalOpen(true);
+ }
+
+ return (
+
+
+
+
+
+ {credit && '+ '}
+ {debit && '- '}
+ {shouldShowUnlimitedValue
+ ? strings('confirm.unlimited')
+ : tokenValue !== null &&
+ shortenString(tokenValue || '', {
+ truncatedCharLimit: 15,
+ truncatedStartChars: 15,
+ truncatedEndChars: 0,
+ skipCharacterInEnd: true,
+ })}
+ {tokenId && `#${tokenId}`}
+
+
+
+
+
+
+
+
+ {/**
+ TODO - add fiat shorten prop after tooltip logic has been updated
+ {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}
+ */}
+ {fiatValue && }
+
+ {hasValueModalOpen && (
+ /**
+ * TODO replace BottomModal instances with BottomSheet
+ * {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}}
+ */
+ setHasValueModalOpen(false)}>
+ setHasValueModalOpen(false)}
+ >
+
+
+ setHasValueModalOpen(false)}
+ iconName={IconName.ArrowLeft}
+ />
+
+ {labelChangeType}
+
+
+
+ {tokenValueMaxPrecision}
+
+
+
+
+ )}
+
+ );
+ };
+
+export default SimulationValueDisplay;
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/index.ts
new file mode 100644
index 00000000000..33cde05cb62
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/index.ts
@@ -0,0 +1 @@
+export { default } from './ValueDisplay';
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts
new file mode 100644
index 00000000000..50cee91255f
--- /dev/null
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts
@@ -0,0 +1 @@
+export { default } from './Simulation';
diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx
index 1fd23c2fbeb..2caee661061 100644
--- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx
+++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx
@@ -1,23 +1,14 @@
import React from 'react';
-
-import useApprovalRequest from '../../../../hooks/useApprovalRequest';
import InfoRowOrigin from '../Shared/InfoRowOrigin';
import Message from './Message';
+import TypedSignV3V4Simulation from './Simulation';
-const TypedSignV3V4 = () => {
- const { approvalRequest } = useApprovalRequest();
-
- if (!approvalRequest) {
- return null;
- }
-
- return (
+const TypedSignV3V4 = () => (
<>
- {/* SIMULATION TO BE ADDED */}
+
>
);
-};
export default TypedSignV3V4;
diff --git a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx
index 0f11ada192e..838361aee88 100644
--- a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx
+++ b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx
@@ -12,7 +12,7 @@ describe('NoChangeSimulation', () => {
expect(getByText('Estimated changes')).toBeDefined();
expect(
getByText(
- 'You’re signing into a site and there are no predicted changes to your account.',
+ "You're signing into a site and there are no predicted changes to your account."
),
).toBeDefined();
});
diff --git a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx
index f610b170a57..9e8bb043755 100644
--- a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx
+++ b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx
@@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
import setSignatureRequestSecurityAlertResponse from '../../../../../../actions/signatureRequest';
import { store } from '../../../../../../store';
import { useTheme } from '../../../../../../util/theme';
-import useConfirmationRedesignEnabled from '../../../hooks/useConfirmationRedesignEnabled';
+import { useConfirmationRedesignEnabled } from '../../../hooks/useConfirmationRedesignEnabled';
import PersonalSign from '../../PersonalSign';
import TypedSign from '../../TypedSign';
import { MessageParams } from '../types';
diff --git a/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx b/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx
index adef046bb01..725d332f132 100644
--- a/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx
+++ b/app/components/Views/confirmations/components/UI/BottomModal/BottomModal.tsx
@@ -8,15 +8,21 @@ import styleSheet from './BottomModal.styles';
const OPAQUE_GRAY = '#414141';
interface BottomModalProps {
+ canCloseOnBackdropClick?: boolean;
children: ReactChild;
onClose?: () => void;
hideBackground?: boolean;
}
+/**
+ * TODO replace BottomModal instances with BottomSheet
+ * {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}}
+ */
const BottomModal = ({
+ canCloseOnBackdropClick = true,
children,
hideBackground,
- onClose,
+ onClose
}: BottomModalProps) => {
const { colors } = useTheme();
const { styles } = useStyles(styleSheet, {});
@@ -32,6 +38,7 @@ const BottomModal = ({
animationInTiming={600}
animationOutTiming={600}
onBackButtonPress={onClose}
+ onBackdropPress={canCloseOnBackdropClick ? onClose : undefined}
onSwipeComplete={onClose}
swipeDirection={'down'}
propagateSwipe
diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx
index 0417a1c9ce2..4c738c73648 100644
--- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx
+++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx
@@ -61,7 +61,7 @@ const ExpandableSection = ({
{expanded && (
-
+ setExpanded(false)} canCloseOnBackdropClick>
({
getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }),
diff --git a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts
index 140c58dcde4..9ca0e9919cc 100644
--- a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts
+++ b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts
@@ -7,7 +7,7 @@ import { selectRemoteFeatureFlags } from '../../../../selectors/featureFlagContr
import useApprovalRequest from './useApprovalRequest';
import useQRHardwareAwareness from './useQRHardwareAwareness';
-const useConfirmationRedesignEnabled = () => {
+export const useConfirmationRedesignEnabled = () => {
const { approvalRequest } = useApprovalRequest();
const { isSigningQRObject, isSyncingQRHardware } = useQRHardwareAwareness();
const { confirmation_redesign } = useSelector(selectRemoteFeatureFlags);
@@ -22,7 +22,6 @@ const useConfirmationRedesignEnabled = () => {
const isRedesignedEnabled = useMemo(
() =>
(confirmation_redesign as Record)?.signatures &&
- process.env.REDESIGNED_SIGNATURE_REQUEST === 'true' &&
// following condition will ensure that user is redirected to old designs is using QR scan aware hardware
!isSyncingQRHardware &&
!isSigningQRObject &&
@@ -43,5 +42,3 @@ const useConfirmationRedesignEnabled = () => {
return { isRedesignedEnabled };
};
-
-export default useConfirmationRedesignEnabled;
diff --git a/app/components/Views/confirmations/hooks/useGetTokenStandardAndDetails.ts b/app/components/Views/confirmations/hooks/useGetTokenStandardAndDetails.ts
new file mode 100644
index 00000000000..a361c9fbcb5
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useGetTokenStandardAndDetails.ts
@@ -0,0 +1,50 @@
+import { NetworkClientId } from '@metamask/network-controller';
+import { Hex } from '@metamask/utils';
+
+import { TokenStandard } from '../../../UI/SimulationDetails/types';
+import { useAsyncResult } from '../../../hooks/useAsyncResult';
+import {
+ ERC20_DEFAULT_DECIMALS,
+ parseTokenDetailDecimals,
+ memoizedGetTokenStandardAndDetails,
+ TokenDetailsERC20,
+} from '../utils/token';
+
+/**
+ * Returns token details for a given token contract
+ *
+ * @param tokenAddress
+ * @returns
+ */
+const useGetTokenStandardAndDetails = (
+ tokenAddress?: Hex | string | undefined,
+ networkClientId?: NetworkClientId,
+) => {
+ const { value: details } =
+ useAsyncResult(async () => {
+ if (!tokenAddress) {
+ return Promise.resolve(null);
+ }
+
+ return (await memoizedGetTokenStandardAndDetails({
+ tokenAddress,
+ networkClientId,
+ })) as TokenDetailsERC20;
+ }, [tokenAddress]);
+
+ if (!details) {
+ return { decimalsNumber: undefined };
+ }
+
+ const { decimals, standard } = details || {};
+
+ if (standard === TokenStandard.ERC20) {
+ const parsedDecimals =
+ parseTokenDetailDecimals(decimals) ?? ERC20_DEFAULT_DECIMALS;
+ details.decimalsNumber = parsedDecimals;
+ }
+
+ return details;
+};
+
+export default useGetTokenStandardAndDetails;
diff --git a/app/components/Views/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts b/app/components/Views/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts
new file mode 100644
index 00000000000..6bf37651a5c
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useTrackERC20WithoutDecimalInformation.ts
@@ -0,0 +1,50 @@
+import { useEffect } from 'react';
+import { Hex } from '@metamask/utils';
+
+import { MetaMetricsEvents } from '../../../../core/Analytics';
+import { TokenStandard } from '../../../../components/UI/SimulationDetails/types';
+import { useMetrics } from '../../../../components/hooks/useMetrics';
+import { parseTokenDetailDecimals, TokenDetailsERC20 } from '../utils/token';
+
+/**
+ * Track event that number of decimals in ERC20 is not obtained
+ *
+ * @param chainId
+ * @param tokenAddress
+ * @param tokenDetails
+ * @param metricLocation
+ */
+const useTrackERC20WithoutDecimalInformation = (
+ chainId: Hex,
+ tokenAddress: Hex | string | undefined,
+ tokenDetails?: TokenDetailsERC20,
+ metricLocation: string = 'signature_confirmation',
+) => {
+ const { trackEvent, createEventBuilder } = useMetrics();
+
+ useEffect(() => {
+ if (chainId === undefined || tokenDetails === undefined) {
+ return;
+ }
+ const { decimals, standard } = tokenDetails || {};
+
+ if (standard !== TokenStandard.ERC20) { return; }
+
+ const parsedDecimals = parseTokenDetailDecimals(decimals);
+
+ if (parsedDecimals === undefined) {
+ trackEvent(createEventBuilder(MetaMetricsEvents.INCOMPLETE_ASSET_DISPLAYED)
+ .addProperties({
+ token_decimals_available: false,
+ asset_address: tokenAddress,
+ asset_type: TokenStandard.ERC20,
+ chain_id: chainId,
+ location: metricLocation,
+ ui_customizations: ['redesigned_confirmation'],
+ })
+ .build());
+ }
+ }, [chainId, tokenAddress, tokenDetails, metricLocation, trackEvent, createEventBuilder]);
+};
+
+export default useTrackERC20WithoutDecimalInformation;
diff --git a/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts b/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts
new file mode 100644
index 00000000000..0553f8fcae2
--- /dev/null
+++ b/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts
@@ -0,0 +1,66 @@
+import { useSelector } from 'react-redux';
+import { SignTypedDataVersion } from '@metamask/eth-sig-util';
+import { MessageParamsTyped, SignatureRequest, SignatureRequestType } from '@metamask/signature-controller';
+import { selectUseTransactionSimulations } from '../../../../selectors/preferencesController';
+import { isRecognizedPermit, parseTypedDataMessage } from '../utils/signature';
+import { useSignatureRequest } from './useSignatureRequest';
+
+const NON_PERMIT_SUPPORTED_TYPES_SIGNS = [
+ {
+ domainName: 'Seaport',
+ primaryTypeList: ['BulkOrder'],
+ versionList: ['1.4', '1.5', '1.6'],
+ },
+ {
+ domainName: 'Seaport',
+ primaryTypeList: ['OrderComponents'],
+ },
+];
+
+const isNonPermitSupportedByDecodingAPI = (
+ signatureRequest: SignatureRequest,
+) => {
+ const data = signatureRequest.messageParams?.data as string;
+ if (!data) { return false; }
+
+ const {
+ domain: { name, version },
+ primaryType,
+ } = parseTypedDataMessage(data);
+
+ return NON_PERMIT_SUPPORTED_TYPES_SIGNS.some(
+ ({ domainName, primaryTypeList, versionList }) =>
+ name === domainName &&
+ primaryTypeList.includes(primaryType) &&
+ (!versionList || versionList.includes(version)),
+ );
+};
+
+export function useTypedSignSimulationEnabled() {
+ const signatureRequest = useSignatureRequest();
+ const useTransactionSimulations = useSelector(
+ selectUseTransactionSimulations,
+ );
+
+ if (!signatureRequest) {
+ return undefined;
+ }
+
+ const requestType = signatureRequest.type;
+ const signatureMethod = (signatureRequest.messageParams as MessageParamsTyped)?.version;
+
+ const isTypedSignV3V4 = requestType === SignatureRequestType.TypedSign && (
+ signatureMethod === SignTypedDataVersion.V3 ||
+ signatureMethod === SignTypedDataVersion.V4
+ );
+ const isPermit = isRecognizedPermit(signatureRequest);
+
+ const nonPermitSupportedByDecodingAPI: boolean =
+ isTypedSignV3V4 && isNonPermitSupportedByDecodingAPI(signatureRequest);
+
+ return (
+ useTransactionSimulations &&
+ isTypedSignV3V4 &&
+ (isPermit || nonPermitSupportedByDecodingAPI)
+ );
+}
diff --git a/app/components/Views/confirmations/utils/confirm.ts b/app/components/Views/confirmations/utils/confirm.ts
index 6f9b61bdf5c..f4e414f1583 100644
--- a/app/components/Views/confirmations/utils/confirm.ts
+++ b/app/components/Views/confirmations/utils/confirm.ts
@@ -1,8 +1,11 @@
import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware';
+export const TOKEN_VALUE_UNLIMITED_THRESHOLD = 10 ** 15;
+
export function isSignatureRequest(requestType: string) {
return [
ApprovalTypes.PERSONAL_SIGN,
ApprovalTypes.ETH_SIGN_TYPED_DATA,
].includes(requestType as ApprovalTypes);
}
+
diff --git a/app/components/Views/confirmations/utils/signature.test.ts b/app/components/Views/confirmations/utils/signature.test.ts
new file mode 100644
index 00000000000..08472ef3f27
--- /dev/null
+++ b/app/components/Views/confirmations/utils/signature.test.ts
@@ -0,0 +1,72 @@
+import { parseTypedDataMessage, isRecognizedPermit } from './signature';
+import { PRIMARY_TYPES_PERMIT } from '../constants/signatures';
+import { SignatureRequest } from '@metamask/signature-controller';
+
+describe('Signature Utils', () => {
+ describe('parseTypedDataMessage', () => {
+ it('should parse typed data message correctly', () => {
+ const data = JSON.stringify({
+ message: {
+ value: '123'
+ }
+ });
+ const result = parseTypedDataMessage(data);
+ expect(result).toEqual({
+ message: {
+ value: '123'
+ }
+ });
+ });
+
+ it('parses message.value as a string', () => {
+ const result = parseTypedDataMessage(
+ '{"test": "dummy", "message": { "value": 3000123} }',
+ );
+ expect(result.message.value).toBe('3000123');
+ });
+
+
+ it('should handle large message values. This prevents native JS number coercion when the value is greater than Number.MAX_SAFE_INTEGER.', () => {
+ const largeValue = '123456789012345678901234567890';
+ const data = JSON.stringify({
+ message: {
+ value: largeValue
+ }
+ });
+ const result = parseTypedDataMessage(data);
+ expect(result.message.value).toBe(largeValue);
+ });
+
+ it('throw error for invalid typedDataMessage', () => {
+ expect(() => {
+ parseTypedDataMessage('');
+ }).toThrow(new Error('Unexpected end of JSON input'));
+ });
+ });
+
+ describe('isRecognizedPermit', () => {
+ it('should return true for recognized permit types', () => {
+ const mockRequest: SignatureRequest = {
+ messageParams: {
+ data: JSON.stringify({
+ primaryType: PRIMARY_TYPES_PERMIT[0]
+ })
+ }
+ } as SignatureRequest;
+
+ expect(isRecognizedPermit(mockRequest)).toBe(true);
+ });
+
+ it('should return false for unrecognized permit types', () => {
+ const mockRequest: SignatureRequest = {
+ messageParams: {
+ data: JSON.stringify({
+ primaryType: 'UnrecognizedType'
+ })
+ }
+ } as SignatureRequest;
+
+ expect(isRecognizedPermit(mockRequest)).toBe(false);
+ });
+ });
+});
diff --git a/app/components/Views/confirmations/utils/signature.ts b/app/components/Views/confirmations/utils/signature.ts
new file mode 100644
index 00000000000..9035a2b3087
--- /dev/null
+++ b/app/components/Views/confirmations/utils/signature.ts
@@ -0,0 +1,61 @@
+import { SignatureRequest } from '@metamask/signature-controller';
+import { PRIMARY_TYPES_PERMIT } from '../constants/signatures';
+
+/**
+ * The contents of this file have been taken verbatim from
+ * metamask-extension/shared/modules/transaction.utils.ts
+ *
+ * If updating, please be mindful of this or delete this comment.
+ */
+
+const REGEX_MESSAGE_VALUE_LARGE = /"message"\s*:\s*\{[^}]*"value"\s*:\s*(\d{15,})/u;
+
+function extractLargeMessageValue(dataToParse: string): string | undefined {
+ if (typeof dataToParse !== 'string') {
+ return undefined;
+ }
+ return dataToParse.match(REGEX_MESSAGE_VALUE_LARGE)?.[1];
+}
+
+/**
+ * JSON.parse has a limitation which coerces values to scientific notation if numbers are greater than
+ * Number.MAX_SAFE_INTEGER. This can cause a loss in precision.
+ *
+ * Aside from precision concerns, if the value returned was a large number greater than 15 digits,
+ * e.g. 3.000123123123121e+26, passing the value to BigNumber will throw the error:
+ * Error: new BigNumber() number type has more than 15 significant digits
+ *
+ * Note that using JSON.parse reviver cannot help since the value will be coerced by the time it
+ * reaches the reviver function.
+ *
+ * This function has a workaround to extract the large value from the message and replace
+ * the message value with the string value.
+ *
+ * @param dataToParse
+ * @returns
+ */
+export const parseTypedDataMessage = (dataToParse: string) => {
+ const result = JSON.parse(dataToParse);
+
+ const messageValue = extractLargeMessageValue(dataToParse);
+ if (result.message?.value) {
+ result.message.value = messageValue || String(result.message.value);
+ }
+ return result;
+};
+
+/**
+ * Returns true if the request is a recognized Permit Typed Sign signature request
+ *
+ * @param request - The signature request to check
+ */
+export const isRecognizedPermit = (request: SignatureRequest) => {
+ if (!request) {
+ return false;
+ }
+
+ const data = (request as SignatureRequest).messageParams?.data as string;
+
+ const { primaryType } = parseTypedDataMessage(data);
+ return PRIMARY_TYPES_PERMIT.includes(primaryType);
+};
diff --git a/app/components/Views/confirmations/utils/token.ts b/app/components/Views/confirmations/utils/token.ts
new file mode 100644
index 00000000000..4014dd1f528
--- /dev/null
+++ b/app/components/Views/confirmations/utils/token.ts
@@ -0,0 +1,99 @@
+import { memoize } from 'lodash';
+import { Hex } from '@metamask/utils';
+import { AssetsContractController } from '@metamask/assets-controllers';
+import { NetworkClientId } from '@metamask/network-controller';
+import { getTokenDetails } from '../../../../util/address';
+
+export type TokenDetailsERC20 = Awaited<
+ ReturnType<
+ ReturnType['getDetails']
+ >
+> & { decimalsNumber: number };
+
+export type TokenDetailsERC721 = Awaited<
+ ReturnType<
+ ReturnType['getDetails']
+ >
+>;
+
+export type TokenDetailsERC1155 = Awaited<
+ ReturnType<
+ ReturnType['getDetails']
+ >
+>;
+
+export type TokenDetails =
+ | TokenDetailsERC20
+ | TokenDetailsERC721
+ | TokenDetailsERC1155;
+
+export const ERC20_DEFAULT_DECIMALS = 18;
+
+export const parseTokenDetailDecimals = (
+ decStr?: string,
+): number | undefined => {
+ if (!decStr) {
+ return undefined;
+ }
+
+ for (const radix of [10, 16]) {
+ const parsedDec = parseInt(decStr, radix);
+ if (isFinite(parsedDec)) {
+ return parsedDec;
+ }
+ }
+ return undefined;
+};
+
+export const memoizedGetTokenStandardAndDetails = memoize(
+ async ({
+ tokenAddress,
+ tokenId,
+ userAddress,
+ networkClientId,
+ }: {
+ tokenAddress?: Hex | string;
+ userAddress?: string;
+ tokenId?: string;
+ networkClientId?: NetworkClientId;
+ }): Promise> => {
+ try {
+ if (!tokenAddress) {
+ return {};
+ }
+
+ return (await getTokenDetails(
+ tokenAddress,
+ userAddress,
+ tokenId,
+ networkClientId,
+ )) as TokenDetails;
+ } catch {
+ return {};
+ }
+ },
+);
+
+/**
+ * Fetches the decimals for the given token address.
+ *
+ * @param address - The ethereum token contract address. It is expected to be in hex format.
+ * We currently accept strings since we have a patch that accepts a custom string
+ * {@see .yarn/patches/@metamask-eth-json-rpc-middleware-npm-14.0.1-b6c2ccbe8c.patch}
+ */
+export const fetchErc20Decimals = async (
+ address: Hex | string,
+ networkClientId?: NetworkClientId,
+): Promise => {
+ try {
+ const { decimals: decStr } = (await memoizedGetTokenStandardAndDetails({
+ tokenAddress: address,
+ networkClientId,
+ })) as TokenDetailsERC20;
+ const decimals = parseTokenDetailDecimals(decStr);
+
+ return decimals ?? ERC20_DEFAULT_DECIMALS;
+ } catch {
+ return ERC20_DEFAULT_DECIMALS;
+ }
+};
diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts
index 7d132663576..7f48671fa74 100644
--- a/app/core/AppConstants.ts
+++ b/app/core/AppConstants.ts
@@ -137,6 +137,7 @@ export default {
'https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/',
STAKING_RISK_DISCLOSURE: 'https://consensys.io/staking-risk-disclosures',
},
+ DECODING_API_URL: process.env.DECODING_API_URL || 'https://signature-insights.api.cx.metamask.io/v1',
ERRORS: {
INFURA_BLOCKED_MESSAGE:
'EthQuery - RPC Error - This service is not available in your country',
diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts
index a0fe49693c3..740c50e7b45 100644
--- a/app/core/Engine/Engine.ts
+++ b/app/core/Engine/Engine.ts
@@ -1441,6 +1441,10 @@ export class Engine {
}),
// This casting expected due to mismatch of browser and react-native version of Sentry traceContext
trace: trace as unknown as SignatureControllerOptions['trace'],
+ decodingApiUrl: AppConstants.DECODING_API_URL,
+ // TODO: check preferences useExternalServices
+ isDecodeSignatureRequestEnabled: () =>
+ preferencesController.state.useTransactionSimulations,
}),
LoggingController: loggingController,
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
diff --git a/app/reducers/transaction/index.js b/app/reducers/transaction/index.js
index 5d1ab2efb94..3b37c620150 100644
--- a/app/reducers/transaction/index.js
+++ b/app/reducers/transaction/index.js
@@ -110,6 +110,8 @@ const transactionReducer = (state = initialState, action) => {
...getTxData(action.transaction),
},
...txMeta,
+ // Retain the securityAlertResponses from the old state
+ securityAlertResponses: state.securityAlertResponses,
};
}
case 'SET_TOKENS_TRANSACTION': {
diff --git a/app/selectors/currencyRateController.test.ts b/app/selectors/currencyRateController.test.ts
index e48f6b957a4..61a29d38137 100644
--- a/app/selectors/currencyRateController.test.ts
+++ b/app/selectors/currencyRateController.test.ts
@@ -2,6 +2,7 @@ import {
selectConversionRate,
selectCurrentCurrency,
selectCurrencyRates,
+ selectConversionRateByChainId,
} from './currencyRateController';
import { isTestNet } from '../../app/util/networks';
import { CurrencyRateState } from '@metamask/assets-controllers';
@@ -64,6 +65,41 @@ describe('CurrencyRateController Selectors', () => {
});
});
+ describe('selectConversionRateByChainId', () => {
+ const mockChainId = '1';
+ const mockNativeCurrency = 'ETH';
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns undefined if on a testnet and fiat is disabled', () => {
+ (isTestNet as jest.Mock).mockReturnValue(true);
+
+ const result = selectConversionRateByChainId.resultFunc(
+ mockCurrencyRateState.currencyRates as unknown as CurrencyRateState['currencyRates'],
+ mockChainId as `0x${string}`,
+ false,
+ mockNativeCurrency,
+ );
+
+ expect(result).toBeUndefined();
+ });
+
+ it('returns the conversion rate for the native currency of the chain id', () => {
+ (isTestNet as jest.Mock).mockReturnValue(false);
+
+ const result = selectConversionRateByChainId.resultFunc(
+ mockCurrencyRateState.currencyRates as unknown as CurrencyRateState['currencyRates'],
+ mockChainId as `0x${string}`,
+ true,
+ mockNativeCurrency,
+ );
+
+ expect(result).toBe(3000);
+ });
+ });
+
describe('selectCurrentCurrency', () => {
it('returns the current currency from the state', () => {
const result = selectCurrentCurrency.resultFunc(
diff --git a/app/selectors/currencyRateController.ts b/app/selectors/currencyRateController.ts
index 715ebeb4e8b..296cf3eedd5 100644
--- a/app/selectors/currencyRateController.ts
+++ b/app/selectors/currencyRateController.ts
@@ -1,7 +1,11 @@
import { createSelector } from 'reselect';
import { CurrencyRateState } from '@metamask/assets-controllers';
import { RootState } from '../reducers';
-import { selectChainId, selectTicker } from './networkController';
+import {
+ selectChainId,
+ selectNativeCurrencyByChainId,
+ selectTicker,
+} from './networkController';
import { isTestNet } from '../../app/util/networks';
const selectCurrencyRateControllerState = (state: RootState) =>
@@ -56,3 +60,22 @@ export const selectConversionRateFoAllChains = createSelector(
(currencyRateControllerState: CurrencyRateState) =>
currencyRateControllerState?.currencyRates,
);
+
+export const selectConversionRateByChainId = createSelector(
+ selectConversionRateFoAllChains,
+ (_state: RootState, chainId: string) => chainId,
+ (state: RootState) => state.settings.showFiatOnTestnets,
+ selectNativeCurrencyByChainId,
+ (
+ currencyRates: CurrencyRateState['currencyRates'],
+ chainId,
+ showFiatOnTestnets,
+ nativeCurrency,
+ ) => {
+ if (isTestNet(chainId) && !showFiatOnTestnets) {
+ return undefined;
+ }
+
+ return currencyRates?.[nativeCurrency]?.conversionRate;
+ },
+);
diff --git a/app/util/address/index.ts b/app/util/address/index.ts
index c3ce4fc2043..a961ed6cda5 100644
--- a/app/util/address/index.ts
+++ b/app/util/address/index.ts
@@ -640,12 +640,14 @@ export const getTokenDetails = async (
tokenAddress: string,
userAddress?: string,
tokenId?: string,
+ networkClientId?: NetworkClientId,
) => {
const { AssetsContractController } = Engine.context;
const tokenData = await AssetsContractController.getTokenStandardAndDetails(
tokenAddress,
userAddress,
tokenId,
+ networkClientId,
);
const { standard, name, symbol, decimals } = tokenData;
if (standard === ERC721 || standard === ERC1155) {
diff --git a/app/util/notifications/methods/common.test.ts b/app/util/notifications/methods/common.test.ts
index a60317e5ec1..d5ea5ddc285 100644
--- a/app/util/notifications/methods/common.test.ts
+++ b/app/util/notifications/methods/common.test.ts
@@ -1,6 +1,7 @@
import {
formatMenuItemDate,
parseNotification,
+ shortenString,
getLeadingZeroCount,
formatAmount,
getUsdAmount,
@@ -218,3 +219,30 @@ describe('parseNotification', () => {
});
});
});
+
+describe('shortenString', () => {
+ it('should return the same string if it is shorter than TRUNCATED_NAME_CHAR_LIMIT', () => {
+ expect(shortenString('string')).toStrictEqual('string');
+ });
+
+ it('should return the shortened string according to the specified options', () => {
+ expect(
+ shortenString('0x1234567890123456789012345678901234567890', {
+ truncatedCharLimit: 10,
+ truncatedStartChars: 4,
+ truncatedEndChars: 4,
+ }),
+ ).toStrictEqual('0x12...7890');
+ });
+
+ it('should shorten the string and remove all characters from the end if skipCharacterInEnd is true', () => {
+ expect(
+ shortenString('0x1234567890123456789012345678901234567890', {
+ truncatedCharLimit: 10,
+ truncatedStartChars: 4,
+ truncatedEndChars: 4,
+ skipCharacterInEnd: true,
+ }),
+ ).toStrictEqual('0x12...');
+ });
+});
diff --git a/app/util/notifications/methods/common.ts b/app/util/notifications/methods/common.ts
index 5c90b4d5b5d..2ee0ce6e18d 100644
--- a/app/util/notifications/methods/common.ts
+++ b/app/util/notifications/methods/common.ts
@@ -313,20 +313,25 @@ export const TRUNCATED_ADDRESS_END_CHARS = 5;
*/
export function shortenString(
stringToShorten = '',
- { truncatedCharLimit, truncatedStartChars, truncatedEndChars } = {
- truncatedCharLimit: TRUNCATED_NAME_CHAR_LIMIT,
- truncatedStartChars: TRUNCATED_ADDRESS_START_CHARS,
- truncatedEndChars: TRUNCATED_ADDRESS_END_CHARS,
- },
+ {
+ truncatedCharLimit = TRUNCATED_NAME_CHAR_LIMIT,
+ truncatedStartChars = TRUNCATED_ADDRESS_START_CHARS,
+ truncatedEndChars = TRUNCATED_ADDRESS_END_CHARS,
+ skipCharacterInEnd = false,
+ }: {
+ truncatedCharLimit?: number;
+ truncatedStartChars?: number;
+ truncatedEndChars?: number;
+ skipCharacterInEnd?: boolean;
+ } = {},
) {
if (stringToShorten.length < truncatedCharLimit) {
return stringToShorten;
}
- return `${stringToShorten.slice(
- 0,
- truncatedStartChars,
- )}...${stringToShorten.slice(-truncatedEndChars)}`;
+ return `${stringToShorten.slice(0, truncatedStartChars)}...${
+ skipCharacterInEnd ? '' : stringToShorten.slice(-truncatedEndChars)
+ }`;
}
export const sortNotifications = (
diff --git a/app/util/number/index.js b/app/util/number/index.js
index 894716562aa..05e8cbececd 100644
--- a/app/util/number/index.js
+++ b/app/util/number/index.js
@@ -349,7 +349,7 @@ export function isBN(value) {
/**
* Determines if a string is a valid decimal
*
- * @param {string} value - String to check
+ * @param {number | string} value - String to check
* @returns {boolean} - True if the string is a valid decimal
*/
export function isDecimal(value) {
@@ -380,6 +380,22 @@ export function isNumber(str) {
return regex.number.test(str);
}
+/**
+ * Determines if a value is a number
+ *
+ * @param {number | string | null | undefined} value - Value to check
+ * @returns {boolean} - True if the value is a valid number
+ */
+export function isNumberValue(value) {
+ if (value === null || value === undefined) { return false; }
+
+ if (typeof value === 'number') {
+ return !Number.isNaN(value) && Number.isFinite(value);
+ }
+
+ return isDecimal(value);
+}
+
export const dotAndCommaDecimalFormatter = (value) => {
const valueStr = String(value);
diff --git a/app/util/number/index.test.ts b/app/util/number/index.test.ts
index 436f18c0c29..5849713f223 100644
--- a/app/util/number/index.test.ts
+++ b/app/util/number/index.test.ts
@@ -20,6 +20,7 @@ import {
isBN,
isDecimal,
isNumber,
+ isNumberValue,
isNumberScientificNotationWhenString,
isZeroValue,
limitToMaximumDecimalPlaces,
@@ -925,6 +926,42 @@ describe('Number utils :: isNumber', () => {
});
});
+describe('Number utils :: isNumberValue', () => {
+ it('should return true for valid number types', () => {
+ expect(isNumberValue(1650.7)).toBe(true);
+ expect(isNumberValue(1000)).toBe(true);
+ expect(isNumberValue(0.0001)).toBe(true);
+ expect(isNumberValue(-0.0001)).toBe(true);
+ expect(isNumberValue(1)).toBe(true);
+ expect(isNumberValue(1e-10)).toBe(true);
+ });
+
+ it('should be a valid number string types', () => {
+ expect(isNumberValue('1650.7')).toBe(true);
+ expect(isNumberValue('1000')).toBe(true);
+ expect(isNumberValue('.01')).toBe(true);
+ expect(isNumberValue('0.0001')).toBe(true);
+ expect(isNumberValue('0001')).toBe(true);
+ expect(isNumberValue('-0.0001')).toBe(true);
+ expect(isNumberValue('1')).toBe(true);
+ expect(isNumberValue('1e-10')).toBe(true);
+ });
+
+ it('should not be a valid number ', () => {
+ expect(isNumberValue('..7')).toBe(false);
+ expect(isNumberValue('1..1')).toBe(false);
+ expect(isNumberValue('0..')).toBe(false);
+ expect(isNumberValue('a.0001')).toBe(false);
+ expect(isNumberValue('00a01')).toBe(false);
+ expect(isNumberValue('1,.')).toBe(false);
+ expect(isNumberValue('1,')).toBe(false);
+ expect(isNumberValue('.')).toBe(false);
+ expect(isNumberValue('a¡1')).toBe(false);
+ expect(isNumberValue(undefined)).toBe(false);
+ expect(isNumberValue(null)).toBe(false);
+ });
+});
+
describe('Number utils :: dotAndCommaDecimalFormatter', () => {
it('should return the number if it does not contain a dot or comma', () => {
expect(dotAndCommaDecimalFormatter('1650')).toBe('1650');
diff --git a/app/util/test/confirm-data-helpers.ts b/app/util/test/confirm-data-helpers.ts
index 408f70b8788..485228a065a 100644
--- a/app/util/test/confirm-data-helpers.ts
+++ b/app/util/test/confirm-data-helpers.ts
@@ -1,4 +1,8 @@
-import { MessageParamsTyped } from '@metamask/signature-controller';
+import {
+ MessageParamsTyped,
+ SignatureRequestStatus,
+ SignatureRequestType
+} from '@metamask/signature-controller';
import { backgroundState } from './initial-root-state';
import { Hex } from '@metamask/utils';
@@ -209,6 +213,70 @@ export const typedSignV3ConfirmationState = {
},
};
+export const typedSignV4ConfirmationState = {
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ ApprovalController: {
+ pendingApprovals: {
+ 'fb2029e1-b0ab-11ef-9227-05a11087c334': {
+ id: 'fb2029e1-b0ab-11ef-9227-05a11087c334',
+ origin: 'metamask.github.io',
+ type: 'eth_signTypedData',
+ time: 1733143817088,
+ requestData: {
+ data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x935e73edb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}',
+ from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
+ version: 'V4',
+ requestId: 14,
+ signatureMethod: 'eth_signTypedData_v4',
+ origin: 'https://metamask.github.io',
+ metamaskId: 'fb2029e0-b0ab-11ef-9227-05a11087c334',
+ meta: {
+ url: 'https://metamask.github.io/test-dapp/',
+ title: 'E2E Test Dapp',
+ icon: { uri: 'https://metamask.github.io/metamask-fox.svg' },
+ analytics: { request_source: 'In-App-Browser' },
+ },
+ },
+ requestState: null,
+ expectsResult: true,
+ },
+ },
+ pendingApprovalCount: 1,
+ approvalFlows: [],
+ },
+ SignatureController: {
+ signatureRequests: {
+ 'fb2029e1-b0ab-11ef-9227-05a11087c334': {
+ id: 'fb2029e1-b0ab-11ef-9227-05a11087c334',
+ chainId: '0x1' as Hex,
+ type: SignatureRequestType.TypedSign,
+ messageParams: {
+ data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x935e73edb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}',
+ from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477',
+ version: 'V4',
+ requestId: 14,
+ signatureMethod: 'eth_signTypedData_v4',
+ origin: 'https://metamask.github.io',
+ metamaskId: 'fb2029e0-b0ab-11ef-9227-05a11087c334',
+ meta: {
+ url: 'https://metamask.github.io/test-dapp/',
+ title: 'E2E Test Dapp',
+ icon: { uri: 'https://metamask.github.io/metamask-fox.svg' },
+ analytics: { request_source: 'In-App-Browser' },
+ },
+ },
+ networkClientId: '1',
+ status: SignatureRequestStatus.Unapproved,
+ time: 1733143817088
+ },
+ },
+ },
+ },
+ },
+};
+
export const securityAlertResponse = {
block: 21572398,
result_type: 'Malicious',
diff --git a/bitrise.yml b/bitrise.yml
index 80960c25e40..0cfc20b1827 100644
--- a/bitrise.yml
+++ b/bitrise.yml
@@ -160,9 +160,8 @@ stages:
- run_tag_smoke_assets_android: {}
- run_tag_smoke_confirmations_ios: {}
- run_tag_smoke_confirmations_android: {}
- - run_tag_smoke_ramps_android: {}
- # - run_tag_smoke_swaps_ios: {}
- # - run_tag_smoke_swaps_android: {}
+ - run_tag_smoke_swaps_ios: {}
+ - run_tag_smoke_swaps_android: {}
- run_tag_smoke_core_ios: {}
- run_tag_smoke_core_android: {}
- run_tag_multichain_permissions_ios: {}
diff --git a/e2e/api-mocking/mock-config/mock-events.js b/e2e/api-mocking/mock-config/mock-events.js
index 0e3423416b1..755fe4d39da 100644
--- a/e2e/api-mocking/mock-config/mock-events.js
+++ b/e2e/api-mocking/mock-config/mock-events.js
@@ -38,21 +38,37 @@ export const mockEvents = {
securityAlertApiSupportedChains: {
urlEndpoint: 'https://security-alerts.api.cx.metamask.io/supportedChains',
response: [
- '0xa4b1',
- '0xa86a',
- '0x2105',
- '0x138d5',
- '0x38',
- '0xe708',
- '0x1',
- '0x1b6e6',
- '0xcc',
- '0xa',
- '0x89',
- '0x82750',
- '0xaa36a7',
- '0x144'
- ],
+ '0xa4b1',
+ '0xa86a',
+ '0x2105',
+ '0x138d5',
+ '0x38',
+ '0xe708',
+ '0x1',
+ '0x1b6e6',
+ '0xcc',
+ '0xa',
+ '0x89',
+ '0x82750',
+ '0xaa36a7',
+ '0x144',
+ ],
+ responseCode: 200,
+ },
+
+ remoteFeatureFlags: {
+ urlEndpoint:
+ 'https://client-config.api.cx.metamask.io/v1/flags?client=mobile&distribution=main&environment=dev',
+ response: [
+ {
+ mobileMinimumVersions: {
+ appMinimumBuild: 1243,
+ appleMinimumOS: 6,
+ androidMinimumAPIVersion: 21,
+ },
+ },
+ { confirmation_redesign: { signatures: false } },
+ ],
responseCode: 200,
},
},
@@ -77,7 +93,8 @@ export const mockEvents = {
},
securityAlertApiValidate: {
- urlEndpoint: 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
+ urlEndpoint:
+ 'https://security-alerts.api.cx.metamask.io/validate/0xaa36a7',
response: {
block: 20733513,
result_type: 'Benign',
@@ -93,9 +110,9 @@ export const mockEvents = {
{
from: '0x76cf1cdd1fcc252442b50d6e97207228aa4aefc3',
to: '0x50587e46c5b96a3f6f9792922ec647f13e6efae4',
- value: '0x0'
- }
- ]
+ value: '0x0',
+ },
+ ],
},
responseCode: 201,
},
diff --git a/e2e/api-specs/json-rpc-coverage.js b/e2e/api-specs/json-rpc-coverage.js
index ca5b30f828a..bcc75bc049c 100644
--- a/e2e/api-specs/json-rpc-coverage.js
+++ b/e2e/api-specs/json-rpc-coverage.js
@@ -22,6 +22,7 @@ import ConfirmationsRejectRule from './ConfirmationsRejectionRule';
import { createDriverTransport } from './helpers';
import { BrowserViewSelectorsIDs } from '../selectors/Browser/BrowserView.selectors';
import { getGanachePort } from '../fixtures/utils';
+import { mockEvents } from '../api-mocking/mock-config/mock-events';
const port = getGanachePort(8545, process.pid);
const chainId = 1337;
@@ -156,6 +157,10 @@ const main = async () => {
const server = mockServer(port, openrpcDocument);
server.start();
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
await withFixtures(
{
dapp: true,
@@ -163,6 +168,7 @@ const main = async () => {
ganacheOptions: defaultGanacheOptions,
disableGanache: true,
restartDevice: true,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/pages/Transactions/ActivitiesView.js b/e2e/pages/Transactions/ActivitiesView.js
index 089bb7f7839..3e8a0dfc1e7 100644
--- a/e2e/pages/Transactions/ActivitiesView.js
+++ b/e2e/pages/Transactions/ActivitiesView.js
@@ -1,15 +1,38 @@
-import { ActivitiesViewSelectorsIDs, ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors';
+import {
+ ActivitiesViewSelectorsIDs,
+ ActivitiesViewSelectorsText,
+} from '../../selectors/Transactions/ActivitiesView.selectors';
import Matchers from '../../utils/Matchers';
import Gestures from '../../utils/Gestures';
+import { CommonSelectorsIDs } from '../../selectors/Common.selectors';
class ActivitiesView {
+ static FIRST_ROW = 0;
+ static SECOND_ROW = 1;
+
get title() {
return Matchers.getElementByText(ActivitiesViewSelectorsText.TITLE);
}
get container() {
+ return Matchers.getElementByID(ActivitiesViewSelectorsIDs.CONTAINER);
+ }
+
+ get confirmedLabel() {
+ return Matchers.getElementByText(ActivitiesViewSelectorsText.CONFIRM_TEXT);
+ }
+
+ get firstTransactionStatus() {
+ return Matchers.getElementByID(
+ CommonSelectorsIDs.TRANSACTION_STATUS,
+ ActivitiesView.FIRST_ROW,
+ );
+ }
+
+ get secondTransactionStatus() {
return Matchers.getElementByID(
- ActivitiesViewSelectorsIDs.CONTAINER,
+ CommonSelectorsIDs.TRANSACTION_STATUS,
+ ActivitiesView.SECOND_ROW,
);
}
@@ -20,14 +43,27 @@ class ActivitiesView {
return title;
}
- swapActivity(sourceToken, destinationToken) {
+ generateApprovedTokenActivityLabel(sourceToken) {
+ let title = ActivitiesViewSelectorsText.APPROVE;
+ title = title.replace('{{sourceToken}}', sourceToken);
+ title = title.replace('{{upTo}}', '.*');
+ return new RegExp(`^${title}`);
+ }
+
+ swapActivityTitle(sourceToken, destinationToken) {
return Matchers.getElementByText(
this.generateSwapActivityLabel(sourceToken, destinationToken),
);
}
+ tokenApprovalActivity(sourceToken) {
+ return Matchers.getElementByText(
+ this.generateApprovedTokenActivityLabel(sourceToken),
+ );
+ }
+
async tapOnSwapActivity(sourceToken, destinationToken) {
- const element = this.swapActivity(sourceToken, destinationToken);
+ const element = this.swapActivityTitle(sourceToken, destinationToken);
await Gestures.waitAndTap(element);
}
diff --git a/e2e/pages/swaps/QuoteView.js b/e2e/pages/swaps/QuoteView.js
index dac368cb41f..a81a3f4fd24 100644
--- a/e2e/pages/swaps/QuoteView.js
+++ b/e2e/pages/swaps/QuoteView.js
@@ -53,8 +53,8 @@ class QuoteView {
await Gestures.typeTextAndHideKeyboard(this.searchToken, symbol);
}
- async selectToken(symbol) {
- const element = Matchers.getElementByText(symbol, 1);
+ async selectToken(symbol, index = 1) {
+ const element = Matchers.getElementByText(symbol, index);
await Gestures.waitAndTap(element);
}
diff --git a/e2e/pages/swaps/SwapView.js b/e2e/pages/swaps/SwapView.js
index 46abf95cb5b..d780208621d 100644
--- a/e2e/pages/swaps/SwapView.js
+++ b/e2e/pages/swaps/SwapView.js
@@ -5,6 +5,7 @@ import {
import Matchers from '../../utils/Matchers';
import Gestures from '../../utils/Gestures';
+import TestHelpers from '../..//helpers';
class SwapView {
get quoteSummary() {
@@ -19,8 +20,10 @@ class SwapView {
return Matchers.getElementByText(SwapViewSelectorsTexts.FETCHING_QUOTES);
}
- get swipeToSwapButton() {
- return Matchers.getElementByID(SwapsViewSelectors.SWIPE_TO_SWAP_BUTTON);
+ get swapButton() {
+ return device.getPlatform() === 'ios'
+ ? Matchers.getElementByID(SwapsViewSelectors.SWAP_BUTTON)
+ : Matchers.getElementByLabel(SwapsViewSelectors.SWAP_BUTTON);
}
get iUnderstandLabel() {
@@ -34,25 +37,19 @@ class SwapView {
return title;
}
- async swipeToSwap() {
- const percentage = device.getPlatform() === 'ios' ? 0.72 : 0.95;
-
- // Swipe could happen at the same time when gas fees are falshing
- // and that's when the swipe button becomes disabled
- // that's the need to retry
- await Gestures.swipe(this.swipeToSwapButton, 'right', 'fast', percentage);
- await Gestures.swipe(this.swipeToSwapButton, 'right', 'fast', percentage);
+ // Function to check if the button is enabled
+ async isButtonEnabled(element) {
+ const attributes = await element.getAttributes();
+ return attributes.enabled === true; // Check if enabled is true
}
- swapCompleteLabel(sourceTokenSymbol, destTokenSymbol) {
- return Matchers.getElementByText(
- this.generateSwapCompleteLabel(sourceTokenSymbol, destTokenSymbol),
- );
+ async tapSwapButton() {
+ await Gestures.waitAndTap(this.swapButton);
}
async tapIUnderstandPriceWarning() {
try {
- await Gestures.waitAndTap(this.iUnderstandLabel, 5000);
+ await Gestures.waitAndTap(this.iUnderstandLabel, 3000);
} catch (e) {
// eslint-disable-next-line no-console
console.log(`Price warning not displayed: ${e}`);
diff --git a/e2e/resources/networks.e2e.js b/e2e/resources/networks.e2e.js
index be8c299175a..f8caf3f1627 100644
--- a/e2e/resources/networks.e2e.js
+++ b/e2e/resources/networks.e2e.js
@@ -103,14 +103,42 @@ const CustomNetworks = {
},
},
Tenderly: {
- isCustomNetwork: true,
- providerConfig: {
- type: 'rpc',
- chainId: '0x1',
- rpcUrl: `https://rpc.tenderly.co/fork/bbfe5a2e-2426-4512-a5f8-46ce85fe9ad6`,
- nickname: 'Tenderly',
- ticker: 'ETH',
+ Mainnet: {
+ providerConfig: {
+ type: 'rpc',
+ chainId: toHex('1'),
+ rpcUrl: 'https://virtual.mainnet.rpc.tenderly.co/3472e4b3-594b-488a-a8b1-93593194615f',
+ nickname: 'Tenderly - Mainnet',
+ ticker: 'ETH',
+ },
+ },
+ Polygon: {
+ providerConfig: {
+ type: 'rpc',
+ chainId: toHex('137'),
+ rpcUrl: 'https://virtual.polygon.rpc.tenderly.co/e834a81e-69ba-49e9-a6a5-be5b6eea3cdc',
+ nickname: 'Polygon',
+ ticker: 'POL',
+ },
+ },
+ Linea: {
+ providerConfig: {
+ type: 'rpc',
+ chainId: toHex('59144'),
+ rpcUrl: 'https://virtual.linea.rpc.tenderly.co/2c429ceb-43db-45bc-9d84-21a40d21e0d2',
+ nickname: 'Linea',
+ ticker: 'ETH',
+ },
},
+ Optimism: {
+ providerConfig: {
+ type: 'rpc',
+ chainId: toHex('10'),
+ rpcUrl: 'https://virtual.optimism.rpc.tenderly.co/3170a58e-fa67-4ccc-9697-b13aff0f5c1a',
+ nickname: 'Optimism',
+ ticker: 'ETH',
+ },
+ }
},
Gnosis: {
providerConfig: {
diff --git a/e2e/selectors/Common.selectors.js b/e2e/selectors/Common.selectors.js
index 1e98c4c559e..62f0a69cad7 100644
--- a/e2e/selectors/Common.selectors.js
+++ b/e2e/selectors/Common.selectors.js
@@ -13,7 +13,7 @@ export const CommonSelectorsIDs = {
NAV_IOS_BACK: 'nav-ios-back',
FOX_SCREEN: 'fox-screen',
NAVBAR_TITLE_NETWORKS_TEXT: 'navbar-title-networks',
- STATUS_CONFIRMED: 'status-confirmed-text',
+ TRANSACTION_STATUS: 'transaction-status',
ANDROID_PROGRESS_BAR: 'android.widget.ProgressBar',
};
diff --git a/e2e/selectors/Transactions/ActivitiesView.selectors.js b/e2e/selectors/Transactions/ActivitiesView.selectors.js
index b7e9c7f1fe5..adb9f23562e 100644
--- a/e2e/selectors/Transactions/ActivitiesView.selectors.js
+++ b/e2e/selectors/Transactions/ActivitiesView.selectors.js
@@ -17,6 +17,7 @@ export const ActivitiesViewSelectorsText = {
SENT_TOKENS_MESSAGE_TEXT: (unit) => getSentUnitMessage(unit),
SET_APPROVAL_FOR_ALL_METHOD: enContent.transactions.set_approval_for_all,
SWAP: enContent.swaps.transaction_label.swap,
+ APPROVE: enContent.swaps.transaction_label.approve,
TITLE: enContent.transactions_view.title,
};
diff --git a/e2e/selectors/swaps/SwapsView.selectors.js b/e2e/selectors/swaps/SwapsView.selectors.js
index 257643747fb..d379e8c4a20 100644
--- a/e2e/selectors/swaps/SwapsView.selectors.js
+++ b/e2e/selectors/swaps/SwapsView.selectors.js
@@ -1,10 +1,9 @@
import enContent from '../../../locales/languages/en.json';
export const SwapsViewSelectors = {
- SWIPE_TO_SWAP_BUTTON: 'swipe-to-swap-button',
+ SWAP_BUTTON: 'swap-button',
QUOTE_SUMMARY: 'swap-quote-summary',
GAS_FEE: 'swap-gas-fee',
- SWAP_BUTTON: 'swap-button',
};
export const SwapViewSelectorsTexts = {
diff --git a/e2e/specs/confirmations/signatures/ethereum-sign.spec.js b/e2e/specs/confirmations/signatures/ethereum-sign.spec.js
index 4be24f172ca..b51be2c81b6 100644
--- a/e2e/specs/confirmations/signatures/ethereum-sign.spec.js
+++ b/e2e/specs/confirmations/signatures/ethereum-sign.spec.js
@@ -12,6 +12,7 @@ import {
import { SmokeConfirmations } from '../../../tags';
import TestHelpers from '../../../helpers';
import Assertions from '../../../utils/Assertions';
+import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
describe(SmokeConfirmations('Ethereum Sign'), () => {
beforeAll(async () => {
@@ -20,6 +21,10 @@ describe(SmokeConfirmations('Ethereum Sign'), () => {
});
it('Sign in with Ethereum', async () => {
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
await withFixtures(
{
dapp: true,
@@ -29,6 +34,7 @@ describe(SmokeConfirmations('Ethereum Sign'), () => {
.build(),
restartDevice: true,
ganacheOptions: defaultGanacheOptions,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/confirmations/signatures/personal-sign.spec.js b/e2e/specs/confirmations/signatures/personal-sign.spec.js
index 0e3acf0579c..4fc37496141 100644
--- a/e2e/specs/confirmations/signatures/personal-sign.spec.js
+++ b/e2e/specs/confirmations/signatures/personal-sign.spec.js
@@ -12,8 +12,13 @@ import {
import { SmokeConfirmations } from '../../../tags';
import TestHelpers from '../../../helpers';
import Assertions from '../../../utils/Assertions';
+import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
describe(SmokeConfirmations('Personal Sign'), () => {
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
beforeAll(async () => {
jest.setTimeout(2500000);
await TestHelpers.reverseServerPort();
@@ -29,6 +34,7 @@ describe(SmokeConfirmations('Personal Sign'), () => {
.build(),
restartDevice: true,
ganacheOptions: defaultGanacheOptions,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.js b/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.js
index ff4a19e0137..77f0f3b3721 100644
--- a/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.js
+++ b/e2e/specs/confirmations/signatures/security-alert-signatures.mock.spec.js
@@ -60,7 +60,10 @@ describe(SmokeConfirmations('Security Alert API - Signature'), () => {
it('should sign typed message', async () => {
const testSpecificMock = {
- GET: [mockEvents.GET.securityAlertApiSupportedChains],
+ GET: [
+ mockEvents.GET.securityAlertApiSupportedChains,
+ mockEvents.GET.remoteFeatureFlags,
+ ],
POST: [
{
...mockEvents.POST.securityAlertApiValidate,
@@ -83,7 +86,10 @@ describe(SmokeConfirmations('Security Alert API - Signature'), () => {
it('should show security alert for malicious request', async () => {
const testSpecificMock = {
- GET: [mockEvents.GET.securityAlertApiSupportedChains],
+ GET: [
+ mockEvents.GET.securityAlertApiSupportedChains,
+ mockEvents.GET.remoteFeatureFlags,
+ ],
POST: [
{
...mockEvents.POST.securityAlertApiValidate,
@@ -108,6 +114,7 @@ describe(SmokeConfirmations('Security Alert API - Signature'), () => {
const testSpecificMock = {
GET: [
mockEvents.GET.securityAlertApiSupportedChains,
+ mockEvents.GET.remoteFeatureFlags,
{
urlEndpoint:
'https://static.cx.metamask.io/api/v1/confirmations/ppom/ppom_version.json',
diff --git a/e2e/specs/confirmations/signatures/typed-sign-v3.spec.js b/e2e/specs/confirmations/signatures/typed-sign-v3.spec.js
index 86d958aa561..26cf84334ae 100644
--- a/e2e/specs/confirmations/signatures/typed-sign-v3.spec.js
+++ b/e2e/specs/confirmations/signatures/typed-sign-v3.spec.js
@@ -12,8 +12,13 @@ import {
import { SmokeConfirmations } from '../../../tags';
import TestHelpers from '../../../helpers';
import Assertions from '../../../utils/Assertions';
+import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
describe(SmokeConfirmations('Typed Sign V3'), () => {
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
beforeAll(async () => {
jest.setTimeout(2500000);
await TestHelpers.reverseServerPort();
@@ -29,6 +34,7 @@ describe(SmokeConfirmations('Typed Sign V3'), () => {
.build(),
restartDevice: true,
ganacheOptions: defaultGanacheOptions,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/confirmations/signatures/typed-sign-v4.spec.js b/e2e/specs/confirmations/signatures/typed-sign-v4.spec.js
index 4de3dd9b006..c82b6e7a17b 100644
--- a/e2e/specs/confirmations/signatures/typed-sign-v4.spec.js
+++ b/e2e/specs/confirmations/signatures/typed-sign-v4.spec.js
@@ -12,8 +12,13 @@ import {
import { SmokeConfirmations } from '../../../tags';
import TestHelpers from '../../../helpers';
import Assertions from '../../../utils/Assertions';
+import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
describe(SmokeConfirmations('Typed Sign V4'), () => {
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
beforeAll(async () => {
jest.setTimeout(2500000);
await TestHelpers.reverseServerPort();
@@ -29,6 +34,7 @@ describe(SmokeConfirmations('Typed Sign V4'), () => {
.build(),
restartDevice: true,
ganacheOptions: defaultGanacheOptions,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/confirmations/signatures/typed-sign.spec.js b/e2e/specs/confirmations/signatures/typed-sign.spec.js
index bee5b2a5e8a..c6b9cfed353 100644
--- a/e2e/specs/confirmations/signatures/typed-sign.spec.js
+++ b/e2e/specs/confirmations/signatures/typed-sign.spec.js
@@ -12,8 +12,13 @@ import {
import { SmokeConfirmations } from '../../../tags';
import TestHelpers from '../../../helpers';
import Assertions from '../../../utils/Assertions';
+import { mockEvents } from '../../../api-mocking/mock-config/mock-events';
describe(SmokeConfirmations('Typed Sign'), () => {
+ const testSpecificMock = {
+ GET: [mockEvents.GET.remoteFeatureFlags],
+ };
+
beforeAll(async () => {
jest.setTimeout(2500000);
await TestHelpers.reverseServerPort();
@@ -29,6 +34,7 @@ describe(SmokeConfirmations('Typed Sign'), () => {
.build(),
restartDevice: true,
ganacheOptions: defaultGanacheOptions,
+ testSpecificMock,
},
async () => {
await loginToApp();
diff --git a/e2e/specs/quarantine/send-to-contact.failing.js b/e2e/specs/quarantine/send-to-contact.failing.js
new file mode 100644
index 00000000000..8e5427cab2e
--- /dev/null
+++ b/e2e/specs/quarantine/send-to-contact.failing.js
@@ -0,0 +1,77 @@
+'use strict';
+
+import { SmokeConfirmations } from '../../tags';
+
+import AmountView from '../../pages/Send/AmountView';
+import SendView from '../../pages/Send/SendView';
+import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView';
+import { loginToApp } from '../../viewHelper';
+import TabBarComponent from '../../pages/wallet/TabBarComponent';
+import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
+import enContent from '../../../locales/languages/en.json';
+import FixtureBuilder from '../../fixtures/fixture-builder';
+import {
+ loadFixture,
+ startFixtureServer,
+ stopFixtureServer,
+} from '../../fixtures/fixture-helper';
+import { CustomNetworks } from '../../resources/networks.e2e';
+import TestHelpers from '../../helpers';
+import FixtureServer from '../../fixtures/fixture-server';
+import { getFixturesServerPort } from '../../fixtures/utils';
+import Assertions from '../../utils/Assertions';
+
+const fixtureServer = new FixtureServer();
+
+describe(SmokeConfirmations('Send ETH'), () => {
+ const TOKEN_NAME = enContent.unit.eth;
+ const AMOUNT = '0.12345';
+
+ beforeEach(async () => {
+ await TestHelpers.reverseServerPort();
+ const fixture = new FixtureBuilder()
+ .withNetworkController(CustomNetworks.Tenderly)
+ .withAddressBookController({
+ addressBook: {
+ '0x1': {
+ '0x2f318C334780961FB129D2a6c30D0763d9a5C970': {
+ address: '0x2f318C334780961FB129D2a6c30D0763d9a5C970',
+ chainId: '0x1',
+ isEns: false,
+ memo: '',
+ name: 'Test Name 1',
+ },
+ },
+ },
+ })
+ .build();
+ await startFixtureServer(fixtureServer);
+ await loadFixture(fixtureServer, { fixture });
+ await device.launchApp({
+ permissions: { notifications: 'YES' },
+ launchArgs: { fixtureServerPort: `${getFixturesServerPort()}` },
+ });
+ await loginToApp();
+ });
+
+ afterAll(async () => {
+ await stopFixtureServer(fixtureServer);
+ });
+
+ it('should send ETH to a contact from inside the wallet', async () => {
+ await TabBarComponent.tapActions();
+ await WalletActionsBottomSheet.tapSendButton();
+ await SendView.scrollToSavedAccount();
+
+ await SendView.tapAccountName('Test Name 1');
+
+ await SendView.tapNextButton();
+
+ await AmountView.typeInTransactionAmount(AMOUNT);
+ await AmountView.tapNextButton();
+ await Assertions.checkIfTextIsDisplayed('Test Name 1');
+ await TransactionConfirmationView.tapConfirmButton();
+ await TabBarComponent.tapActivity();
+ await Assertions.checkIfTextIsDisplayed(`${AMOUNT} ${TOKEN_NAME}`);
+ });
+});
diff --git a/e2e/specs/ramps/offramp.spec.js b/e2e/specs/ramps/offramp.spec.js
index 027c6051f29..dab60443aba 100644
--- a/e2e/specs/ramps/offramp.spec.js
+++ b/e2e/specs/ramps/offramp.spec.js
@@ -38,7 +38,7 @@ describe(SmokeRamps('Off-Ramp'), () => {
beforeAll(async () => {
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly)
+ .withNetworkController(CustomNetworks.Tenderly.Mainnet)
.build();
await startFixtureServer(fixtureServer);
await loadFixture(fixtureServer, { fixture });
diff --git a/e2e/specs/swaps/swap-action-regression.spec.js b/e2e/specs/swaps/swap-action-regression.spec.js
index 4f70e048cb6..86b0dd32e64 100644
--- a/e2e/specs/swaps/swap-action-regression.spec.js
+++ b/e2e/specs/swaps/swap-action-regression.spec.js
@@ -1,11 +1,10 @@
'use strict';
+import { ethers } from 'ethers';
import { loginToApp } from '../../viewHelper';
-import Onboarding from '../../pages/swaps/OnBoarding';
import QuoteView from '../../pages/swaps/QuoteView';
import SwapView from '../../pages/swaps/SwapView';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
-import DetailsBottomSheet from '../../pages/Transactions/TransactionDetailsModal';
import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
import WalletView from '../../pages/wallet/WalletView';
import FixtureBuilder from '../../fixtures/fixture-builder';
@@ -15,20 +14,38 @@ import {
stopFixtureServer,
} from '../../fixtures/fixture-helper';
import { CustomNetworks } from '../../resources/networks.e2e';
+import NetworkListModal from '../../pages/Network/NetworkListModal';
+import NetworkEducationModal from '../../pages/Network/NetworkEducationModal';
import TestHelpers from '../../helpers';
import FixtureServer from '../../fixtures/fixture-server';
import { getFixturesServerPort } from '../../fixtures/utils';
-import { SmokeSwaps } from '../../tags';
+import { Regression } from '../../tags';
+import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet.js';
+import ImportAccountView from '../../pages/importAccount/ImportAccountView';
+import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView';
import Assertions from '../../utils/Assertions';
+import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet';
+
+import Tenderly from '../../tenderly';
const fixtureServer = new FixtureServer();
+const firstElement = 0;
+
+describe(Regression('Multiple Swaps from Actions'), () => {
+ let educationModalTapped = false;
+ let currentNetwork = CustomNetworks.Tenderly.Mainnet.providerConfig.nickname;
+ const wallet = ethers.Wallet.createRandom();
-describe(SmokeSwaps('Multiple Swaps from Actions'), () => {
- let swapOnboarded = true; // TODO: Set it to false once we show the onboarding page again.
beforeAll(async () => {
+ jest.setTimeout(2500000);
+ await Tenderly.addFunds(
+ CustomNetworks.Tenderly.Mainnet.providerConfig.rpcUrl,
+ wallet.address,
+ );
+
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly)
+ .withNetworkController(CustomNetworks.Tenderly.Mainnet)
.build();
await startFixtureServer(fixtureServer);
await loadFixture(fixtureServer, { fixture });
@@ -43,43 +60,65 @@ describe(SmokeSwaps('Multiple Swaps from Actions'), () => {
await stopFixtureServer(fixtureServer);
});
- beforeEach(async () => {
- jest.setTimeout(150000);
+ it('should be able to import account', async () => {
+ await WalletView.tapIdenticon();
+ await Assertions.checkIfVisible(AccountListBottomSheet.accountList);
+ await AccountListBottomSheet.tapAddAccountButton();
+ await AddAccountBottomSheet.tapImportAccount();
+ await Assertions.checkIfVisible(ImportAccountView.container);
+ await ImportAccountView.enterPrivateKey(wallet.privateKey);
+ await Assertions.checkIfVisible(SuccessImportAccountView.container);
+ await SuccessImportAccountView.tapCloseButton();
+ await AccountListBottomSheet.swipeToDismissAccountsModal();
+ await Assertions.checkIfVisible(WalletView.container);
});
+
it.each`
- quantity | sourceTokenSymbol | destTokenSymbol
- ${'1'} | ${'ETH'} | ${'WETH'}
- ${'1'} | ${'WETH'} | ${'ETH'}
+ type | quantity | sourceTokenSymbol | destTokenSymbol | network
+ ${'native'} | ${'.03'} | ${'ETH'} | ${'DAI'} | ${CustomNetworks.Tenderly.Mainnet}
+ ${'unapproved'} | ${'3'} | ${'DAI'} | ${'USDC'} | ${CustomNetworks.Tenderly.Mainnet}
+ ${'erc20'} | ${'10'} | ${'DAI'} | ${'ETH'} | ${CustomNetworks.Tenderly.Mainnet}
`(
- "should Swap $quantity '$sourceTokenSymbol' to '$destTokenSymbol'",
- async ({ quantity, sourceTokenSymbol, destTokenSymbol }) => {
+ "should swap $type token '$sourceTokenSymbol' to '$destTokenSymbol' on '$network.providerConfig.nickname'",
+ async ({ type, quantity, sourceTokenSymbol, destTokenSymbol, network }) => {
await TabBarComponent.tapWallet();
+
+ if (network.providerConfig.nickname !== currentNetwork) {
+ await WalletView.tapNetworksButtonOnNavBar();
+ await Assertions.checkIfToggleIsOn(NetworkListModal.testNetToggle);
+ await NetworkListModal.changeNetworkTo(
+ network.providerConfig.nickname,
+ false,
+ );
+ await NetworkEducationModal.tapGotItButton();
+ await TestHelpers.delay(3000);
+ currentNetwork = network.providerConfig.nickname;
+ }
+
await Assertions.checkIfVisible(WalletView.container);
await TabBarComponent.tapActions();
await WalletActionsBottomSheet.tapSwapButton();
- if (!swapOnboarded) {
- await Onboarding.tapStartSwapping();
- swapOnboarded = true;
- }
await Assertions.checkIfVisible(QuoteView.getQuotes);
- //Select source token, if ETH then can skip because already selected
- if (sourceTokenSymbol !== 'ETH') {
+ //Select source token, if native token can skip because already selected
+ if (type !== 'native' && type !== 'wrap') {
await QuoteView.tapOnSelectSourceToken();
await QuoteView.tapSearchToken();
await QuoteView.typeSearchToken(sourceTokenSymbol);
- await TestHelpers.delay(1000);
+
await QuoteView.selectToken(sourceTokenSymbol);
}
await QuoteView.enterSwapAmount(quantity);
//Select destination token
await QuoteView.tapOnSelectDestToken();
- await QuoteView.tapSearchToken();
- await QuoteView.typeSearchToken(destTokenSymbol);
- await TestHelpers.delay(1000);
- await QuoteView.selectToken(destTokenSymbol);
+ if (destTokenSymbol !== 'ETH') {
+ await QuoteView.tapSearchToken();
+ await QuoteView.typeSearchToken(destTokenSymbol);
+ await TestHelpers.delay(2000);
+ await QuoteView.selectToken(destTokenSymbol);
+ } else await QuoteView.selectToken(destTokenSymbol, firstElement);
//Make sure slippage is zero for wrapped tokens
if (sourceTokenSymbol === 'WETH' || destTokenSymbol === 'WETH') {
@@ -89,50 +128,59 @@ describe(SmokeSwaps('Multiple Swaps from Actions'), () => {
);
}
await QuoteView.tapOnGetQuotes();
- await Assertions.checkIfVisible(SwapView.fetchingQuotes);
await Assertions.checkIfVisible(SwapView.quoteSummary);
await Assertions.checkIfVisible(SwapView.gasFee);
await SwapView.tapIUnderstandPriceWarning();
- await SwapView.swipeToSwap();
+ await SwapView.tapSwapButton();
+ //Wait for Swap to complete
try {
- await Assertions.checkIfVisible(
- SwapView.swapCompleteLabel(sourceTokenSymbol, destTokenSymbol),
+ await Assertions.checkIfTextIsDisplayed(
+ SwapView.generateSwapCompleteLabel(
+ sourceTokenSymbol,
+ destTokenSymbol,
+ ),
30000,
);
} catch (e) {
// eslint-disable-next-line no-console
- console.log(`Toast message is slow to appear or did not appear: ${e}`);
+ console.log(`Swap complete didn't pop up: ${e}`);
}
await device.enableSynchronization();
- await TestHelpers.delay(5000);
+ await TestHelpers.delay(10000);
+
+ // Check the swap activity completed
await TabBarComponent.tapActivity();
await Assertions.checkIfVisible(ActivitiesView.title);
await Assertions.checkIfVisible(
- ActivitiesView.swapActivity(sourceTokenSymbol, destTokenSymbol),
- );
- await ActivitiesView.tapOnSwapActivity(
- sourceTokenSymbol,
- destTokenSymbol,
+ ActivitiesView.swapActivityTitle(sourceTokenSymbol, destTokenSymbol),
);
+ // TODO: Commenting this out until Tenderly issue is resolved
+ //await Assertions.checkIfElementToHaveText(ActivitiesView.firstTransactionStatus, ActivitiesViewSelectorsText.CONFIRM_TEXT, 60000);
- try {
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
- } catch (e) {
- await ActivitiesView.tapOnSwapActivity(
- sourceTokenSymbol,
- destTokenSymbol,
+ // Check the token approval completed
+ if (type === 'unapproved') {
+ await Assertions.checkIfVisible(
+ ActivitiesView.tokenApprovalActivity(sourceTokenSymbol),
);
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
+ // TODO: Commenting this out until Tenderly issue is resolved
+ //await Assertions.checkIfElementToHaveText(ActivitiesView.secondTransactionStatus, ActivitiesViewSelectorsText.CONFIRM_TEXT, 60000);
}
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
- await Assertions.checkIfElementToHaveText(
- DetailsBottomSheet.title,
- DetailsBottomSheet.generateExpectedTitle(sourceTokenSymbol, destTokenSymbol),
+ // TODO: The following hack is needed to update the token balance until bug is fixed
+ await TabBarComponent.tapWallet();
+ await WalletView.tapNetworksButtonOnNavBar();
+ await NetworkListModal.changeNetworkTo('Localhost', false);
+ if (!educationModalTapped) {
+ await NetworkEducationModal.tapGotItButton();
+ }
+ await NetworkListModal.changeNetworkTo(
+ network.providerConfig.nickname,
+ false,
);
- await Assertions.checkIfVisible(DetailsBottomSheet.statusConfirmed);
- await DetailsBottomSheet.tapOnCloseIcon();
- await Assertions.checkIfNotVisible(DetailsBottomSheet.title);
+ if (!educationModalTapped) {
+ await NetworkEducationModal.tapGotItButton();
+ educationModalTapped = true;
+ }
},
);
});
diff --git a/e2e/specs/swaps/swap-action-smoke.spec.js b/e2e/specs/swaps/swap-action-smoke.spec.js
index 7fbb29baba4..db50f431aa7 100644
--- a/e2e/specs/swaps/swap-action-smoke.spec.js
+++ b/e2e/specs/swaps/swap-action-smoke.spec.js
@@ -1,33 +1,49 @@
'use strict';
+import { ethers } from 'ethers';
import { loginToApp } from '../../viewHelper';
-import Onboarding from '../../pages/swaps/OnBoarding';
import QuoteView from '../../pages/swaps/QuoteView';
import SwapView from '../../pages/swaps/SwapView';
import TabBarComponent from '../../pages/wallet/TabBarComponent';
-import ActivitiesView from '../../pages/Transactions/ActivitiesView';
-import DetailsBottomSheet from '../../pages/Transactions/TransactionDetailsModal';
+import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet.js';
+import WalletView from '../../pages/wallet/WalletView';
import WalletActionsBottomSheet from '../../pages/wallet/WalletActionsBottomSheet';
import FixtureBuilder from '../../fixtures/fixture-builder';
+import Tenderly from '../../tenderly';
import {
loadFixture,
startFixtureServer,
stopFixtureServer,
} from '../../fixtures/fixture-helper';
import { CustomNetworks } from '../../resources/networks.e2e';
+import NetworkListModal from '../../pages/Network/NetworkListModal';
+import NetworkEducationModal from '../../pages/Network/NetworkEducationModal';
import TestHelpers from '../../helpers';
import FixtureServer from '../../fixtures/fixture-server';
import { getFixturesServerPort } from '../../fixtures/utils';
import { SmokeSwaps } from '../../tags';
+import ImportAccountView from '../../pages/importAccount/ImportAccountView';
+import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView';
import Assertions from '../../utils/Assertions';
+import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet';
+import ActivitiesView from '../../pages/Transactions/ActivitiesView';
const fixtureServer = new FixtureServer();
+const firstElement = 0;
describe(SmokeSwaps('Swap from Actions'), () => {
- let swapOnboarded = true; // TODO: Set it to false once we show the onboarding page again.
+ let educationModalTapped = false;
+ let currentNetwork = CustomNetworks.Tenderly.Mainnet.providerConfig.nickname;
+ const wallet = ethers.Wallet.createRandom();
+
beforeAll(async () => {
+ await Tenderly.addFunds(
+ CustomNetworks.Tenderly.Mainnet.providerConfig.rpcUrl,
+ wallet.address,
+ );
+
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly)
+ .withNetworkController(CustomNetworks.Tenderly.Mainnet)
.build();
await startFixtureServer(fixtureServer);
await loadFixture(fixtureServer, { fixture });
@@ -43,42 +59,66 @@ describe(SmokeSwaps('Swap from Actions'), () => {
});
beforeEach(async () => {
- jest.setTimeout(150000);
+ jest.setTimeout(120000);
+ });
+
+ it('should be able to import account', async () => {
+ await WalletView.tapIdenticon();
+ await Assertions.checkIfVisible(AccountListBottomSheet.accountList);
+ await AccountListBottomSheet.tapAddAccountButton();
+ await AddAccountBottomSheet.tapImportAccount();
+ await Assertions.checkIfVisible(ImportAccountView.container);
+ await ImportAccountView.enterPrivateKey(wallet.privateKey);
+ await Assertions.checkIfVisible(SuccessImportAccountView.container);
+ await SuccessImportAccountView.tapCloseButton();
+ await AccountListBottomSheet.swipeToDismissAccountsModal();
+ await Assertions.checkIfVisible(WalletView.container);
});
it.each`
- quantity | sourceTokenSymbol | destTokenSymbol
- ${'.05'} | ${'ETH'} | ${'USDT'}
- ${'100'} | ${'USDT'} | ${'ETH'}
+ type | quantity | sourceTokenSymbol | destTokenSymbol | network
+ ${'wrap'} | ${'.03'} | ${'ETH'} | ${'WETH'} | ${CustomNetworks.Tenderly.Mainnet}
+ ${'unwrap'} | ${'.01'} | ${'WETH'} | ${'ETH'} | ${CustomNetworks.Tenderly.Mainnet}
`(
- "should Swap $quantity '$sourceTokenSymbol' to '$destTokenSymbol'",
- async ({ quantity, sourceTokenSymbol, destTokenSymbol }) => {
+ "should swap $type token '$sourceTokenSymbol' to '$destTokenSymbol' on '$network.providerConfig.nickname'",
+ async ({ type, quantity, sourceTokenSymbol, destTokenSymbol, network }) => {
await TabBarComponent.tapWallet();
- await TabBarComponent.tapActions();
- await WalletActionsBottomSheet.tapSwapButton();
- if (!swapOnboarded) {
- await Onboarding.tapStartSwapping();
- swapOnboarded = true;
+ if (network.providerConfig.nickname !== currentNetwork) {
+ await WalletView.tapNetworksButtonOnNavBar();
+ await Assertions.checkIfToggleIsOn(NetworkListModal.testNetToggle);
+ await NetworkListModal.changeNetworkTo(
+ network.providerConfig.nickname,
+ false,
+ );
+ await NetworkEducationModal.tapGotItButton();
+ await TestHelpers.delay(3000);
+ currentNetwork = network.providerConfig.nickname;
}
+
+ await Assertions.checkIfVisible(WalletView.container);
+ await TabBarComponent.tapActions();
+ await WalletActionsBottomSheet.tapSwapButton();
await Assertions.checkIfVisible(QuoteView.getQuotes);
- //Select source token, if ETH then can skip because already selected
- if (sourceTokenSymbol !== 'ETH') {
+ //Select source token, if native tiken can skip because already selected
+ if (type !== 'native' && type !== 'wrap') {
await QuoteView.tapOnSelectSourceToken();
await QuoteView.tapSearchToken();
await QuoteView.typeSearchToken(sourceTokenSymbol);
- await TestHelpers.delay(1000);
+
await QuoteView.selectToken(sourceTokenSymbol);
}
await QuoteView.enterSwapAmount(quantity);
//Select destination token
await QuoteView.tapOnSelectDestToken();
- await QuoteView.tapSearchToken();
- await QuoteView.typeSearchToken(destTokenSymbol);
- await TestHelpers.delay(1000);
- await QuoteView.selectToken(destTokenSymbol);
+ if (destTokenSymbol !== 'ETH') {
+ await QuoteView.tapSearchToken();
+ await QuoteView.typeSearchToken(destTokenSymbol);
+ await TestHelpers.delay(2000);
+ await QuoteView.selectToken(destTokenSymbol);
+ } else await QuoteView.selectToken(destTokenSymbol, firstElement);
//Make sure slippage is zero for wrapped tokens
if (sourceTokenSymbol === 'WETH' || destTokenSymbol === 'WETH') {
@@ -92,44 +132,58 @@ describe(SmokeSwaps('Swap from Actions'), () => {
await Assertions.checkIfVisible(SwapView.quoteSummary);
await Assertions.checkIfVisible(SwapView.gasFee);
await SwapView.tapIUnderstandPriceWarning();
- await SwapView.swipeToSwap();
+ await Assertions.checkIfVisible(SwapView.swapButton);
+ await TestHelpers.delay(2000);
+ await SwapView.tapSwapButton();
+ //Wait for Swap to complete
try {
- await Assertions.checkIfVisible(
- SwapView.swapCompleteLabel(sourceTokenSymbol, destTokenSymbol),
+ await Assertions.checkIfTextIsDisplayed(
+ SwapView.generateSwapCompleteLabel(
+ sourceTokenSymbol,
+ destTokenSymbol,
+ ),
30000,
);
} catch (e) {
// eslint-disable-next-line no-console
- console.log(`Toast message is slow to appear or did not appear: ${e}`);
+ console.log(`Swap complete didn't pop up: ${e}`);
}
await device.enableSynchronization();
- await TestHelpers.delay(5000);
+ await TestHelpers.delay(10000);
+
+ // Check the swap activity completed
await TabBarComponent.tapActivity();
await Assertions.checkIfVisible(ActivitiesView.title);
await Assertions.checkIfVisible(
- ActivitiesView.swapActivity(sourceTokenSymbol, destTokenSymbol),
- );
- await ActivitiesView.tapOnSwapActivity(
- sourceTokenSymbol,
- destTokenSymbol,
+ ActivitiesView.swapActivityTitle(sourceTokenSymbol, destTokenSymbol),
);
+ // TODO: Commenting this out until Tenderly issue is resolved
+ //await Assertions.checkIfElementToHaveText(ActivitiesView.firstTransactionStatus, ActivitiesViewSelectorsText.CONFIRM_TEXT, 60000);
- try {
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
- } catch (e) {
- await ActivitiesView.tapOnSwapActivity(
- sourceTokenSymbol,
- destTokenSymbol,
+ // Check the token approval completed
+ if (type === 'unapproved') {
+ await Assertions.checkIfVisible(
+ ActivitiesView.tokenApprovalActivity(sourceTokenSymbol),
);
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
+ // TODO: Commenting this out until Tenderly issue is resolved
+ //await Assertions.checkIfElementToHaveText(ActivitiesView.secondTransactionStatus, ActivitiesViewSelectorsText.CONFIRM_TEXT, 60000);
+ }
+
+ // TODO: The following hack is needed to update the token balance until bug is fixed
+ await TabBarComponent.tapWallet();
+ await WalletView.tapNetworksButtonOnNavBar();
+ await NetworkListModal.changeNetworkTo('Localhost', false);
+ if (!educationModalTapped) {
+ await NetworkEducationModal.tapGotItButton();
}
- await Assertions.checkIfElementToHaveText(
- DetailsBottomSheet.title,
- DetailsBottomSheet.generateExpectedTitle(sourceTokenSymbol, destTokenSymbol),
+ await NetworkListModal.changeNetworkTo(
+ network.providerConfig.nickname,
+ false,
);
- await Assertions.checkIfVisible(DetailsBottomSheet.statusConfirmed);
- await DetailsBottomSheet.tapOnCloseIcon();
- await Assertions.checkIfNotVisible(DetailsBottomSheet.title);
+ if (!educationModalTapped) {
+ await NetworkEducationModal.tapGotItButton();
+ educationModalTapped = true;
+ }
},
);
});
diff --git a/e2e/specs/quarantine/swap-token-chart.failing.js b/e2e/specs/swaps/swap-token-chart.spec.js
similarity index 57%
rename from e2e/specs/quarantine/swap-token-chart.failing.js
rename to e2e/specs/swaps/swap-token-chart.spec.js
index 707df3d608b..c3ed9183b9e 100644
--- a/e2e/specs/quarantine/swap-token-chart.failing.js
+++ b/e2e/specs/swaps/swap-token-chart.spec.js
@@ -1,4 +1,5 @@
'use strict';
+import { ethers } from 'ethers';
import { loginToApp } from '../../viewHelper';
import Onboarding from '../../pages/swaps/OnBoarding';
import QuoteView from '../../pages/swaps/QuoteView';
@@ -17,20 +18,29 @@ import TestHelpers from '../../helpers';
import FixtureServer from '../../fixtures/fixture-server';
import { getFixturesServerPort } from '../../fixtures/utils';
import { Regression } from '../../tags';
+import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet.js';
+import ImportAccountView from '../../pages/importAccount/ImportAccountView';
+import SuccessImportAccountView from '../../pages/importAccount/SuccessImportAccountView';
import Assertions from '../../utils/Assertions';
+import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet';
import ActivitiesView from '../../pages/Transactions/ActivitiesView';
-import DetailsBottomSheet from '../../pages/Transactions/TransactionDetailsModal';
+import { ActivitiesViewSelectorsText } from '../../selectors/Transactions/ActivitiesView.selectors';
+import Tenderly from '../../tenderly';
const fixtureServer = new FixtureServer();
-const sourceTokenSymbol = 'USDT';
-const destTokenSymbol = 'DAI';
describe(Regression('Swap from Token view'), () => {
const swapOnboarded = true; // TODO: Set it to false once we show the onboarding page again.
+ const wallet = ethers.Wallet.createRandom();
+
beforeAll(async () => {
+ await Tenderly.addFunds(
+ CustomNetworks.Tenderly.Mainnet.providerConfig.rpcUrl,
+ wallet.address,
+ );
await TestHelpers.reverseServerPort();
const fixture = new FixtureBuilder()
- .withNetworkController(CustomNetworks.Tenderly)
+ .withNetworkController(CustomNetworks.Tenderly.Mainnet)
.build();
await startFixtureServer(fixtureServer);
await loadFixture(fixtureServer, { fixture });
@@ -49,7 +59,22 @@ describe(Regression('Swap from Token view'), () => {
jest.setTimeout(150000);
});
- it('should complete a USDT to ETH swap from the token chart', async () => {
+ it('should be able to import account', async () => {
+ await WalletView.tapIdenticon();
+ await Assertions.checkIfVisible(AccountListBottomSheet.accountList);
+ await AccountListBottomSheet.tapAddAccountButton();
+ await AddAccountBottomSheet.tapImportAccount();
+ await Assertions.checkIfVisible(ImportAccountView.container);
+ await ImportAccountView.enterPrivateKey(wallet.privateKey);
+ await Assertions.checkIfVisible(SuccessImportAccountView.container);
+ await SuccessImportAccountView.tapCloseButton();
+ await AccountListBottomSheet.swipeToDismissAccountsModal();
+ await Assertions.checkIfVisible(WalletView.container);
+ });
+
+ it('should complete a USDC to DAI swap from the token chart', async () => {
+ const sourceTokenSymbol = 'ETH',
+ destTokenSymbol = 'DAI';
await TabBarComponent.tapWallet();
await Assertions.checkIfVisible(WalletView.container);
await WalletView.tapOnToken('Ethereum');
@@ -58,12 +83,7 @@ describe(Regression('Swap from Token view'), () => {
await TokenOverview.tapSwapButton();
if (!swapOnboarded) await Onboarding.tapStartSwapping();
await Assertions.checkIfVisible(QuoteView.getQuotes);
- await QuoteView.tapOnSelectSourceToken();
- await QuoteView.tapSearchToken();
- await QuoteView.typeSearchToken(sourceTokenSymbol);
- await TestHelpers.delay(1000);
- await QuoteView.selectToken(sourceTokenSymbol);
- await QuoteView.enterSwapAmount('10');
+ await QuoteView.enterSwapAmount('.5');
await QuoteView.tapOnSelectDestToken();
await QuoteView.tapSearchToken();
await QuoteView.typeSearchToken(destTokenSymbol);
@@ -74,43 +94,34 @@ describe(Regression('Swap from Token view'), () => {
await Assertions.checkIfVisible(SwapView.quoteSummary);
await Assertions.checkIfVisible(SwapView.gasFee);
await SwapView.tapIUnderstandPriceWarning();
- await SwapView.swipeToSwap();
+ await SwapView.tapSwapButton();
+ await TestHelpers.delay(2000);
+ //Wait for Swap to complete
try {
- await Assertions.checkIfVisible(
- SwapView.swapCompleteLabel(sourceTokenSymbol, destTokenSymbol),
- 100000,
+ await Assertions.checkIfTextIsDisplayed(
+ SwapView.generateSwapCompleteLabel(
+ sourceTokenSymbol,
+ destTokenSymbol,
+ ),
+ 30000,
);
} catch (e) {
// eslint-disable-next-line no-console
- console.log(`Toast message is slow to appear or did not appear: ${e}`);
+ console.log(`Swap complete didn't pop up: ${e}`);
}
await device.enableSynchronization();
- await TestHelpers.delay(5000);
- await TokenOverview.tapBackButton();
+ await TestHelpers.delay(10000);
+
+ // Check the swap activity completed
await TabBarComponent.tapActivity();
await Assertions.checkIfVisible(ActivitiesView.title);
await Assertions.checkIfVisible(
- ActivitiesView.swapActivity(sourceTokenSymbol, destTokenSymbol),
+ ActivitiesView.swapActivityTitle(sourceTokenSymbol, destTokenSymbol),
);
- await ActivitiesView.tapOnSwapActivity(sourceTokenSymbol, destTokenSymbol);
-
- try {
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
- } catch (e) {
- await ActivitiesView.tapOnSwapActivity(
- sourceTokenSymbol,
- destTokenSymbol,
- );
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
- }
-
- await Assertions.checkIfVisible(DetailsBottomSheet.title);
await Assertions.checkIfElementToHaveText(
- DetailsBottomSheet.title,
- DetailsBottomSheet.generateExpectedTitle(sourceTokenSymbol, destTokenSymbol),
+ ActivitiesView.firstTransactionStatus,
+ ActivitiesViewSelectorsText.CONFIRM_TEXT,
+ 60000,
);
- await Assertions.checkIfVisible(DetailsBottomSheet.statusConfirmed);
- await DetailsBottomSheet.tapOnCloseIcon();
- await Assertions.checkIfNotVisible(DetailsBottomSheet.title);
});
});
diff --git a/e2e/specs/swaps/token-details.spec.js b/e2e/specs/swaps/token-details.spec.js
index c40493153fc..4c7407e6a5f 100644
--- a/e2e/specs/swaps/token-details.spec.js
+++ b/e2e/specs/swaps/token-details.spec.js
@@ -6,10 +6,9 @@ import {
importWalletWithRecoveryPhrase,
switchToSepoliaNetwork,
} from '../../viewHelper';
-import { CustomNetworks } from '../../resources/networks.e2e';
import Assertions from '../../utils/Assertions';
import CommonView from '../../pages/CommonView';
-import TestHelpers from '../../helpers'
+import TestHelpers from '../../helpers';
describe(SmokeSwaps('Token Chart Tests'), () => {
beforeAll(async () => {
@@ -23,7 +22,10 @@ describe(SmokeSwaps('Token Chart Tests'), () => {
it('should view the token chart', async () => {
await WalletView.tapOnToken();
- await Assertions.checkIfElementNotToHaveText(TokenOverview.tokenPrice, '$0');
+ await Assertions.checkIfElementNotToHaveText(
+ TokenOverview.tokenPrice,
+ '$0',
+ );
await TokenOverview.tapChartPeriod1d();
await Assertions.checkIfVisible(TokenOverview.chartPeriod1d);
@@ -46,9 +48,10 @@ describe(SmokeSwaps('Token Chart Tests'), () => {
});
it('should not display the chart when using Sepolia test network', async () => {
+ const sepoliaTokenSymbol = 'S';
await switchToSepoliaNetwork();
- await WalletView.tapOnToken(CustomNetworks.Sepolia.providerConfig.ticker);
- await Assertions.checkIfNotVisible(TokenOverview.noChartData);
+ await WalletView.tapOnToken(sepoliaTokenSymbol);
+ await Assertions.checkIfVisible(TokenOverview.noChartData, 60000);
await Assertions.checkIfElementToHaveText(TokenOverview.tokenPrice, '$0');
});
});
diff --git a/e2e/tenderly.js b/e2e/tenderly.js
new file mode 100644
index 00000000000..3212112001b
--- /dev/null
+++ b/e2e/tenderly.js
@@ -0,0 +1,27 @@
+import axios from 'axios';
+
+export default class Tenderly {
+
+ static async addFunds(rpcURL, account, amount = '0xDE0B6B3A764000000') {
+ const data = {
+ jsonrpc: '2.0',
+ method: 'tenderly_setBalance',
+ params: [[account], amount],
+ id: '1234',
+ };
+
+ const response = await axios.post(rpcURL, data, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.data.error) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `ERROR: Failed to add funds to Tenderly VirtualTestNet\n${response.data.error}`,
+ );
+ return null;
+ }
+ }
+}
diff --git a/jest.config.js b/jest.config.js
index bbc092b33f5..d54cc45b1f8 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -9,8 +9,6 @@ process.env.MM_SECURITY_ALERTS_API_ENABLED = 'true';
process.env.PORTFOLIO_VIEW = 'true';
process.env.SECURITY_ALERTS_API_URL = 'https://example.com';
-process.env.REDESIGNED_SIGNATURE_REQUEST = 'true';
-
process.env.LAUNCH_DARKLY_URL =
'https://client-config.dev-api.cx.metamask.io/v1';
diff --git a/locales/languages/en.json b/locales/languages/en.json
index bb36399ef21..369786e4273 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -3611,10 +3611,23 @@
"balance": "Balance",
"network": "Network",
"simulation": {
+ "decoded_tooltip_bid_nft": "The NFT will be reflected in your wallet, when the bid is accepted.",
+ "decoded_tooltip_list_nft": "Expect changes only if someone buys your NFTs.",
+ "info_permit": "You're giving the spender permission to spend this many tokens from your account.",
+ "label_change_type_bidding": "You bid",
+ "label_change_type_listing": "You list",
+ "label_change_type_nft_listing": "Listing price",
+ "label_change_type_permit": "Spending cap",
+ "label_change_type_permit_nft": "Withdraw",
+ "label_change_type_receive": "You receive",
+ "label_change_type_revoke": "Revoke",
+ "label_change_type_transfer": "You send",
+ "personal_sign_info": "You're signing into a site and there are no predicted changes to your account.",
"title": "Estimated changes",
- "personal_sign_info": "You’re signing into a site and there are no predicted changes to your account.",
- "tooltip": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee."
- }
+ "tooltip": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee.",
+ "unavailable": "Unavailable"
+ },
+ "unlimited": "Unlimited"
},
"change_in_simulation_modal": {
"title": "Results have changed",