From bf556fbafac908d71770abbc22af558a3ff9e5dc Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 16:43:19 -0800
Subject: [PATCH 01/69] fix: set chain before navigating to bridge experience
---
ui/components/app/wallet-overview/coin-buttons.tsx | 3 ++-
ui/pages/asset/components/token-buttons.tsx | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/ui/components/app/wallet-overview/coin-buttons.tsx b/ui/components/app/wallet-overview/coin-buttons.tsx
index 6509b4f415c0..2819e24cad0b 100644
--- a/ui/components/app/wallet-overview/coin-buttons.tsx
+++ b/ui/components/app/wallet-overview/coin-buttons.tsx
@@ -463,10 +463,11 @@ const CoinButtons = ({
});
}, [chainId, defaultSwapsToken]);
- const handleBridgeOnClick = useCallback(() => {
+ const handleBridgeOnClick = useCallback(async () => {
if (!defaultSwapsToken) {
return;
}
+ await setCorrectChain();
openBridgeExperience(
'Home',
defaultSwapsToken,
diff --git a/ui/pages/asset/components/token-buttons.tsx b/ui/pages/asset/components/token-buttons.tsx
index 7a1a71132fbb..c61cb5eeedb3 100644
--- a/ui/pages/asset/components/token-buttons.tsx
+++ b/ui/pages/asset/components/token-buttons.tsx
@@ -335,7 +335,8 @@ const TokenButtons = ({
/>
}
label={t('bridge')}
- onClick={() => {
+ onClick={async () => {
+ await setCorrectChain();
openBridgeExperience(MetaMetricsSwapsEventSource.TokenView, {
...token,
iconUrl: token.image,
From 493fcb2f39d56aab6a5b1099fee63c2ff5173829 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:09:50 -0800
Subject: [PATCH 02/69] chore: bridge transaction settings modal + slippage
---
app/_locales/en/messages.json | 3 +
ui/ducks/bridge/actions.ts | 2 +
ui/ducks/bridge/bridge.ts | 6 +
ui/ducks/bridge/selectors.ts | 2 +
...dge-transaction-settings-modal.stories.tsx | 51 +++++
.../bridge-transaction-settings-modal.tsx | 213 ++++++++++++++++++
ui/pages/bridge/prepare/index.scss | 25 +-
.../bridge/prepare/prepare-bridge-page.tsx | 4 +
8 files changed, 304 insertions(+), 2 deletions(-)
create mode 100644 ui/pages/bridge/prepare/bridge-transaction-settings-modal.stories.tsx
create mode 100644 ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index f42a539277d1..6eaa9f3fb7ef 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -1559,6 +1559,9 @@
"message": "Use $1 to customize the gas price. This can be confusing if you aren’t familiar. Interact at your own risk.",
"description": "$1 is key 'advanced' (text: 'Advanced') separated here so that it can be passed in with bold font-weight"
},
+ "customSlippage": {
+ "message": "Custom"
+ },
"customSpendLimit": {
"message": "Custom spend limit"
},
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index 766689cb8cda..1c199bd65d38 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -25,6 +25,7 @@ const {
resetInputFields,
setSortOrder,
setSelectedQuote,
+ setSlippage,
} = bridgeSlice.actions;
export {
@@ -37,6 +38,7 @@ export {
setSrcTokenExchangeRates,
setSortOrder,
setSelectedQuote,
+ setSlippage,
};
const callBridgeControllerMethod = (
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 7abdb8c751e8..6ed9eef8d8be 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -8,6 +8,7 @@ import {
QuoteResponse,
SortOrder,
} from '../../pages/bridge/types';
+import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge';
import { getTokenExchangeRate } from './utils';
export type BridgeState = {
@@ -19,6 +20,7 @@ export type BridgeState = {
toTokenExchangeRate: number | null; // Exchange rate from the selected token to the default currency (can be fiat or crypto)
sortOrder: SortOrder;
selectedQuote: (QuoteResponse & QuoteMetadata) | null; // Alternate quote selected by user. When quotes refresh, the best match will be activated.
+ slippage: number;
};
const initialState: BridgeState = {
@@ -30,6 +32,7 @@ const initialState: BridgeState = {
toTokenExchangeRate: null,
sortOrder: SortOrder.COST_ASC,
selectedQuote: null,
+ slippage: BRIDGE_DEFAULT_SLIPPAGE,
};
export const setSrcTokenExchangeRates = createAsyncThunk(
@@ -68,6 +71,9 @@ const bridgeSlice = createSlice({
setSelectedQuote: (state, action) => {
state.selectedQuote = action.payload;
},
+ setSlippage: (state, action) => {
+ state.slippage = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(setDestTokenExchangeRates.pending, (state) => {
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 2fd3d9586deb..8b118e6311d8 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -173,6 +173,8 @@ export const getToToken = (
export const getFromAmount = (state: BridgeAppState): string | null =>
state.bridge.fromTokenInputValue;
+export const getSlippage = (state: BridgeAppState) => state.bridge.slippage;
+
export const getQuoteRequest = (state: BridgeAppState) => {
const { quoteRequest } = state.metamask.bridgeState;
return quoteRequest;
diff --git a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.stories.tsx b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.stories.tsx
new file mode 100644
index 000000000000..9802ecce9e6f
--- /dev/null
+++ b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.stories.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from '../../../store/store';
+import { createBridgeMockStore } from '../../../../test/jest/mock-store';
+import { BridgeTransactionSettingsModal } from './bridge-transaction-settings-modal';
+
+const storybook = {
+ title: 'Pages/Bridge/TransactionSettingsModal',
+ component: BridgeTransactionSettingsModal,
+};
+
+export const DefaultStory = () => {
+ return {}} />;
+};
+
+DefaultStory.storyName = 'Default';
+DefaultStory.decorators = [
+ (Story) => (
+
+
+
+ ),
+];
+
+export default storybook;
diff --git a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
new file mode 100644
index 000000000000..558e3763657a
--- /dev/null
+++ b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
@@ -0,0 +1,213 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ Button,
+ ButtonPrimary,
+ ButtonPrimarySize,
+ ButtonSize,
+ ButtonVariant,
+ IconName,
+ Modal,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ PopoverPosition,
+ Text,
+ TextField,
+ TextFieldType,
+} from '../../../components/component-library';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ BackgroundColor,
+ BlockSize,
+ BorderColor,
+ BorderRadius,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import { getSlippage } from '../../../ducks/bridge/selectors';
+import { setSlippage } from '../../../ducks/bridge/actions';
+import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker';
+import { MetaMetricsEventName } from '../../../../shared/constants/metametrics';
+import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../../shared/constants/bridge';
+import { Column, Row, Tooltip } from '../layout';
+
+const HARDCODED_SLIPPAGE_OPTIONS = [BRIDGE_DEFAULT_SLIPPAGE, 3];
+
+export const BridgeTransactionSettingsModal = ({
+ onClose,
+ isOpen,
+}: Omit, 'children'>) => {
+ const t = useI18nContext();
+ const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker();
+
+ const dispatch = useDispatch();
+
+ const slippage = useSelector(getSlippage);
+
+ const [localSlippage, setLocalSlippage] = useState(
+ slippage,
+ );
+ const [customSlippage, setCustomSlippage] = useState(
+ slippage && HARDCODED_SLIPPAGE_OPTIONS.includes(slippage)
+ ? undefined
+ : slippage,
+ );
+ const [showCustomButton, setShowCustomButton] = useState(true);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.value = customSlippage?.toString() ?? '';
+ inputRef.current.focus();
+ }
+ }, [customSlippage]);
+
+ return (
+
+
+
+ {t('transactionSettings')}
+
+
+ {t('swapsMaxSlippage')}
+
+ {t('swapSlippageTooltip')}
+
+
+
+ {HARDCODED_SLIPPAGE_OPTIONS.map((hardcodedSlippage) => {
+ return (
+ {
+ setLocalSlippage(hardcodedSlippage);
+ setCustomSlippage(undefined);
+ }}
+ variant={ButtonVariant.Secondary}
+ borderColor={
+ localSlippage === hardcodedSlippage && showCustomButton
+ ? BorderColor.primaryDefault
+ : BorderColor.borderDefault
+ }
+ borderWidth={
+ localSlippage === hardcodedSlippage && showCustomButton
+ ? 2
+ : 1
+ }
+ backgroundColor={
+ localSlippage === hardcodedSlippage && showCustomButton
+ ? BackgroundColor.primaryMuted
+ : BackgroundColor.backgroundDefault
+ }
+ >
+
+ {hardcodedSlippage}%
+
+
+ );
+ })}
+ {showCustomButton ? (
+ {
+ setShowCustomButton(false);
+ inputRef?.current?.focus();
+ }}
+ >
+
+ {customSlippage === undefined
+ ? t('customSlippage')
+ : `${customSlippage}%`}
+
+
+ ) : (
+ {
+ // Remove characters that are not numbers or decimal points if rendering a controlled or pasted value
+ const cleanedValue = e.target.value.replace(/[^0-9.]+/gu, '');
+ setLocalSlippage(undefined);
+ setCustomSlippage(Number(cleanedValue));
+ }}
+ autoFocus={true}
+ onBlur={() => setShowCustomButton(true)}
+ onKeyPress={(e?: React.KeyboardEvent) => {
+ // Only allow numbers and at most one decimal point
+ if (
+ e &&
+ !/^[0-9]*\.{0,1}[0-9]*$/u.test(
+ `${customSlippage ?? ''}${e.key}`,
+ )
+ ) {
+ e.preventDefault();
+ }
+ }}
+ endAccessory={% }
+ />
+ )}
+
+
+
+ {
+ const newSlippage = localSlippage || customSlippage;
+ newSlippage &&
+ trackCrossChainSwapsEvent({
+ event: MetaMetricsEventName.InputChanged,
+ properties: {
+ input: 'slippage',
+ value: newSlippage.toString(),
+ },
+ });
+ dispatch(setSlippage(newSlippage));
+ onClose();
+ }}
+ >
+ {t('submit')}
+
+
+
+
+ );
+};
diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss
index 079c057c59de..91fd4f242a20 100644
--- a/ui/pages/bridge/prepare/index.scss
+++ b/ui/pages/bridge/prepare/index.scss
@@ -164,9 +164,30 @@
color: var(--color-icon-alternative);
transition: all 0.3s ease-in-out;
}
+ }
+}
+
+.bridge-settings-modal {
+ .mm-button-secondary {
+ &:hover {
+ background-color: revert-layer;
+ }
+ }
+
+ .mm-text-field {
+ height: 32px;
+ width: 94px;
- &:disabled {
- cursor: not-allowed;
+ &--focused,
+ &:focus-visible {
+ outline: none;
+ }
+
+ input {
+ font-size: var(--font-size-2);
+ padding-top: 1px;
+ width: 100%;
+ height: 32px;
}
}
}
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 1533fc1a9c20..470b2765e4e0 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -22,6 +22,7 @@ import {
getFromTokens,
getFromTopAssets,
getQuoteRequest,
+ getSlippage,
getToChain,
getToChains,
getToToken,
@@ -74,6 +75,7 @@ const PrepareBridgePage = () => {
const fromAmount = useSelector(getFromAmount);
const providerConfig = useSelector(getProviderConfig);
+ const slippage = useSelector(getSlippage);
const quoteRequest = useSelector(getQuoteRequest);
const { activeQuote } = useSelector(getBridgeQuotes);
@@ -114,6 +116,7 @@ const PrepareBridgePage = () => {
// Otherwise quotes get filtered out by the bridge-api when the wallet's real
// balance is less than the tenderly balance
insufficientBal: Boolean(providerConfig?.rpcUrl?.includes('tenderly')),
+ slippage,
}),
[
fromToken,
@@ -122,6 +125,7 @@ const PrepareBridgePage = () => {
toChain?.chainId,
fromAmount,
providerConfig,
+ slippage,
],
);
From 3f0e30a9486284bd39cb8a4db105f6e038a610b9 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:13:52 -0800
Subject: [PATCH 03/69] chore: style bridge tooltip
---
ui/pages/bridge/layout/tooltip.tsx | 95 ++++++++++++++++++++++--------
1 file changed, 69 insertions(+), 26 deletions(-)
diff --git a/ui/pages/bridge/layout/tooltip.tsx b/ui/pages/bridge/layout/tooltip.tsx
index b6781c9bf480..00f52bf0b617 100644
--- a/ui/pages/bridge/layout/tooltip.tsx
+++ b/ui/pages/bridge/layout/tooltip.tsx
@@ -1,6 +1,10 @@
import React, { useState } from 'react';
import {
Box,
+ Icon,
+ IconName,
+ IconSize,
+ PolymorphicRef,
Popover,
PopoverHeader,
PopoverPosition,
@@ -8,38 +12,64 @@ import {
Text,
} from '../../../components/component-library';
import {
+ Display,
+ IconColor,
JustifyContent,
TextAlign,
TextColor,
} from '../../../helpers/constants/design-system';
+import Column from './column';
const Tooltip = React.forwardRef(
- ({
- children,
- title,
- triggerElement,
- disabled = false,
- ...props
- }: PopoverProps<'div'> & {
- triggerElement: React.ReactElement;
- disabled?: boolean;
- }) => {
+ (
+ {
+ children,
+ title,
+ triggerElement,
+ disabled = false,
+ onClose,
+ iconName,
+ style,
+ ...props
+ }: PopoverProps<'div'> & {
+ triggerElement?: React.ReactElement;
+ disabled?: boolean;
+ onClose?: () => void;
+ iconName?: IconName;
+ },
+ ref?: PolymorphicRef<'div'>,
+ ) => {
const [isOpen, setIsOpen] = useState(false);
const [referenceElement, setReferenceElement] =
useState(null);
const handleMouseEnter = () => setIsOpen(true);
const handleMouseLeave = () => setIsOpen(false);
- const setBoxRef = (ref: HTMLSpanElement | null) => setReferenceElement(ref);
+ const setBoxRef = (newRef: HTMLSpanElement | null) =>
+ setReferenceElement(newRef);
return (
- <>
+
- {triggerElement}
+ {triggerElement ??
+ (iconName && (
+
+ )) ?? (
+
+ )}
{!disabled && (
-
- {title}
-
-
- {children}
-
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {children}
+
+
)}
- >
+
);
},
);
From d2a367d32af08f2387832042a5980e9d16addd84 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:18:01 -0800
Subject: [PATCH 04/69] chore: alternative-soft color definitions
---
ui/helpers/constants/design-system.ts | 3 +++
ui/pages/bridge/index.scss | 37 +++++++++++++++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/ui/helpers/constants/design-system.ts b/ui/helpers/constants/design-system.ts
index 8374e812b017..b82b379536c6 100644
--- a/ui/helpers/constants/design-system.ts
+++ b/ui/helpers/constants/design-system.ts
@@ -54,6 +54,7 @@ export enum Color {
export enum BackgroundColor {
backgroundDefault = 'background-default',
backgroundAlternative = 'background-alternative',
+ backgroundAlternativeSoft = 'background-alternative-soft',
backgroundHover = 'background-hover',
backgroundPressed = 'background-pressed',
overlayDefault = 'overlay-default',
@@ -109,6 +110,7 @@ export enum BorderColor {
export enum TextColor {
textDefault = 'text-default',
textAlternative = 'text-alternative',
+ textAlternativeSoft = 'text-alternative-soft',
textMuted = 'text-muted',
overlayInverse = 'overlay-inverse',
primaryDefault = 'primary-default',
@@ -139,6 +141,7 @@ export enum TextColor {
export enum IconColor {
iconDefault = 'icon-default',
iconAlternative = 'icon-alternative',
+ iconAlternativeSoft = 'icon-alternative-soft',
iconMuted = 'icon-muted',
overlayInverse = 'overlay-inverse',
primaryDefault = 'primary-default',
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss
index e9d6009676f9..9dd3b3759478 100644
--- a/ui/pages/bridge/index.scss
+++ b/ui/pages/bridge/index.scss
@@ -30,3 +30,40 @@
}
}
}
+
+// TODO add to design-tokens package
+.mm-avatar-base--size-xxs {
+ --size: 12px;
+}
+
+[data-theme='light'],
+.light {
+ --color-background-alternative-soft: #F9FAFB;
+ --color-text-alternative-soft: #6A737D;
+ --color-icon-alternative-soft: #6A737D;
+}
+
+[data-theme='dark'],
+.dark {
+ --color-background-alternative-soft: #1f2124;
+ --color-text-alternative-soft: #848c96;
+ --color-icon-alternative-soft: #848c96;
+}
+
+.mm-box--color-text-alternative-soft {
+ color: var(--color-text-alternative-soft);
+}
+
+.mm-box--color-icon-alternative-soft {
+ color: var(--color-icon-alternative-soft);
+}
+
+.mm-box--background-color-background-alternative-soft {
+ background-color: var(--color-background-alternative-soft);
+}
+
+.bridge__container {
+ height: 100%;
+ min-width: 360px;
+ max-width: 480px;
+}
From b1884338d66efe1cf7ed5aeb1f493222780425b4 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:31:10 -0800
Subject: [PATCH 05/69] chore: use multichain asset type for src/dest token
lists
---
ui/ducks/bridge/bridge.ts | 7 +-
ui/ducks/bridge/selectors.ts | 6 +-
ui/hooks/bridge/useLatestBalance.ts | 15 +-
ui/hooks/useTokensWithFiltering.ts | 132 +++++++++---------
.../bridge/prepare/bridge-input-group.tsx | 3 +-
.../bridge/prepare/prepare-bridge-page.tsx | 5 +-
ui/pages/bridge/types.ts | 12 ++
ui/pages/bridge/utils/quote.ts | 3 +-
8 files changed, 94 insertions(+), 89 deletions(-)
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 6ed9eef8d8be..31b2f27bf508 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -1,9 +1,8 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { Hex } from '@metamask/utils';
import { swapsSlice } from '../swaps/swaps';
-import { SwapsTokenObject } from '../../../shared/constants/swaps';
-import { SwapsEthToken } from '../../selectors';
import {
+ BridgeToken,
QuoteMetadata,
QuoteResponse,
SortOrder,
@@ -13,8 +12,8 @@ import { getTokenExchangeRate } from './utils';
export type BridgeState = {
toChainId: Hex | null;
- fromToken: SwapsTokenObject | SwapsEthToken | null;
- toToken: SwapsTokenObject | SwapsEthToken | null;
+ fromToken: BridgeToken;
+ toToken: BridgeToken;
fromTokenInputValue: string | null;
fromTokenExchangeRate: number | null; // Exchange rate from selected token to the default currency (can be fiat or crypto)
toTokenExchangeRate: number | null; // Exchange rate from the selected token to the default currency (can be fiat or crypto)
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 8b118e6311d8..532f4988f90a 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -39,6 +39,7 @@ import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants';
import {
L1GasFees,
+ BridgeToken,
QuoteMetadata,
QuoteResponse,
SortOrder,
@@ -53,6 +54,7 @@ import {
calcTotalGasFee,
isNativeAddress,
} from '../../pages/bridge/utils/quote';
+import { AssetType } from '../../../shared/constants/transaction';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import {
@@ -164,9 +166,7 @@ export const getFromToken = (
: getSwapsDefaultToken(state);
};
-export const getToToken = (
- state: BridgeAppState,
-): SwapsTokenObject | SwapsEthToken | null => {
+export const getToToken = (state: BridgeAppState): BridgeToken => {
return state.bridge.toToken;
};
diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts
index 6aaf7da68c0c..21c7d0a561f2 100644
--- a/ui/hooks/bridge/useLatestBalance.ts
+++ b/ui/hooks/bridge/useLatestBalance.ts
@@ -1,23 +1,23 @@
import { useSelector } from 'react-redux';
import { Hex } from '@metamask/utils';
import { Numeric } from '../../../shared/modules/Numeric';
-import { DEFAULT_PRECISION } from '../useCurrencyDisplay';
import { getCurrentChainId } from '../../../shared/modules/selectors/networks';
-import { getSelectedInternalAccount, SwapsEthToken } from '../../selectors';
+import { getSelectedInternalAccount } from '../../selectors';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance';
import { useAsyncResult } from '../useAsyncResult';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
+import { BridgeToken } from '../../pages/bridge/types';
/**
* Custom hook to fetch and format the latest balance of a given token or native asset.
*
* @param token - The token object for which the balance is to be fetched. Can be null.
* @param chainId - The chain ID to be used for fetching the balance. Optional.
- * @returns An object containing the formatted balance as a string.
+ * @returns An object containing the balanceAmount as a string.
*/
const useLatestBalance = (
- token: SwapsTokenObject | SwapsEthToken | null,
+ token: SwapsTokenObject | BridgeToken,
chainId?: Hex,
) => {
const { address: selectedAddress } = useSelector(getSelectedInternalAccount);
@@ -52,13 +52,6 @@ const useLatestBalance = (
const tokenDecimals = token?.decimals ? Number(token.decimals) : 1;
return {
- formattedBalance:
- token && latestBalance
- ? latestBalance
- .shiftedBy(tokenDecimals)
- .round(DEFAULT_PRECISION)
- .toString()
- : undefined,
balanceAmount:
token && latestBalance
? calcTokenAmount(latestBalance.toString(), tokenDecimals)
diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts
index ef155eb9ca1c..87f723ac24d3 100644
--- a/ui/hooks/useTokensWithFiltering.ts
+++ b/ui/hooks/useTokensWithFiltering.ts
@@ -1,9 +1,10 @@
import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
-import { ChainId, hexToBN } from '@metamask/controller-utils';
+import { ChainId } from '@metamask/controller-utils';
import { Hex } from '@metamask/utils';
import { useParams } from 'react-router-dom';
+import { zeroAddress } from 'ethereumjs-util';
import {
getAllTokens,
getCurrentCurrency,
@@ -12,13 +13,7 @@ import {
getTokenExchangeRates,
} from '../selectors';
import { getConversionRate } from '../ducks/metamask/metamask';
-import {
- SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
- SwapsTokenObject,
- TokenBucketPriority,
-} from '../../shared/constants/swaps';
-import { getValueFromWeiHex } from '../../shared/modules/conversion.utils';
-import { EtherDenomination } from '../../shared/constants/common';
+import { SwapsTokenObject } from '../../shared/constants/swaps';
import {
AssetWithDisplayData,
ERC20Asset,
@@ -26,18 +21,25 @@ import {
TokenWithBalance,
} from '../components/multichain/asset-picker-amount/asset-picker-modal/types';
import { AssetType } from '../../shared/constants/transaction';
-import { isSwapsDefaultTokenSymbol } from '../../shared/modules/swaps.utils';
+import { isNativeAddress } from '../pages/bridge/utils/quote';
+import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../shared/constants/network';
+import { getCurrentChainId } from '../../shared/modules/selectors/networks';
import { useTokenTracker } from './useTokenTracker';
-import { getRenderableTokenData } from './useTokensToSearch';
+import { useMultichainBalances } from './useMultichainBalances';
-/*
+/**
* Returns a token list generator that filters and sorts tokens based on
* query match, balance/popularity, all other tokens
+ *
+ * @param tokenList - a mapping of token addresses in the selected chainId to token metadata from the bridge-api
+ * @param topTokens - a list of top tokens from the swap-api
+ * @param tokenAddressAllowlistByChainId - a mapping of all supported chainIds to a Set of allowed token addresses
+ * @param chainId - the selected src/dest chainId
*/
export const useTokensWithFiltering = (
tokenList: Record,
topTokens: { address: string }[],
- sortOrder: TokenBucketPriority = TokenBucketPriority.owned,
+ tokenAddressAllowlistByChainId: Record>,
chainId?: ChainId | Hex,
) => {
const { token: tokenAddressFromUrl } = useParams();
@@ -65,6 +67,7 @@ export const useTokensWithFiltering = (
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency);
+ const currentChainId = useSelector(getCurrentChainId);
const sortedErc20TokensWithBalances = useMemo(
() =>
@@ -74,6 +77,9 @@ export const useTokensWithFiltering = (
[erc20TokensWithBalances],
);
+ const { assetsWithBalance: multichainTokensWithBalance } =
+ useMultichainBalances();
+
const filteredTokenListGenerator = useCallback(
(
shouldAddToken: (
@@ -89,51 +95,40 @@ export const useTokensWithFiltering = (
| AssetWithDisplayData
| undefined => {
if (chainId && shouldAddToken(token.symbol, token.address, chainId)) {
- return getRenderableTokenData(
- {
- ...token,
- type: isSwapsDefaultTokenSymbol(token.symbol, chainId)
- ? AssetType.native
- : AssetType.token,
- image: token.iconUrl,
- chainId,
- },
- tokenConversionRates,
- conversionRate,
- currentCurrency,
- chainId,
- tokenList,
- );
+ // Only tokens on the active chain are shown here
+ const sharedFields = { ...token, chainId };
+
+ if (isNativeAddress(token.address)) {
+ return {
+ ...sharedFields,
+ type: AssetType.native,
+ address: zeroAddress(),
+ image:
+ CHAIN_ID_TOKEN_IMAGE_MAP[
+ chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
+ ],
+ balance: currentChainId === chainId ? balanceOnActiveChain : '',
+ string: currentChainId === chainId ? balanceOnActiveChain : '',
+ };
+ }
+
+ return {
+ ...sharedFields,
+ type: AssetType.token,
+ image: token.iconUrl,
+ // Only tokens with 0 balance are processed here so hardcode empty string
+ balance: '',
+ string: '',
+ address: token.address || zeroAddress(),
+ };
}
+
return undefined;
};
return (function* (): Generator<
AssetWithDisplayData | AssetWithDisplayData
> {
- const balance = hexToBN(balanceOnActiveChain);
- const srcBalanceFields =
- sortOrder === TokenBucketPriority.owned
- ? {
- balance: balanceOnActiveChain,
- string: getValueFromWeiHex({
- value: balance,
- numberOfDecimals: 4,
- toDenomination: EtherDenomination.ETH,
- }),
- chainId,
- }
- : {};
- const nativeToken = buildTokenData({
- ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
- chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
- ],
- ...srcBalanceFields,
- });
- if (nativeToken) {
- yield nativeToken;
- }
-
if (tokenAddressFromUrl) {
const tokenListItem =
tokenList?.[tokenAddressFromUrl] ??
@@ -146,25 +141,27 @@ export const useTokensWithFiltering = (
}
}
- if (sortOrder === TokenBucketPriority.owned) {
- for (const tokenWithBalance of sortedErc20TokensWithBalances) {
- const cachedTokenData =
- tokenWithBalance.address &&
- tokenList &&
- (tokenList[tokenWithBalance.address] ??
- tokenList[tokenWithBalance.address.toLowerCase()]);
- if (cachedTokenData) {
- const combinedTokenData = buildTokenData({
- ...tokenWithBalance,
- ...(cachedTokenData ?? {}),
- });
- if (combinedTokenData) {
- yield combinedTokenData;
- }
- }
+ const isTokenBlocked = (tokenAddress: string, tokenChainId: string) =>
+ !tokenAddressAllowlistByChainId[tokenChainId]?.has(
+ tokenAddress.toLowerCase(),
+ );
+ // Yield multichain tokens with balances and are not blocked
+ for (const token of multichainTokensWithBalance) {
+ if (
+ shouldAddToken(
+ token.symbol,
+ token.address ?? undefined,
+ token.chainId,
+ ) &&
+ (token.address
+ ? !isTokenBlocked(token.address, token.chainId)
+ : true)
+ ) {
+ yield { ...token, address: token.address || zeroAddress() };
}
}
+ // Yield topTokens from selected chain
for (const topToken of topTokens) {
const tokenListItem =
tokenList?.[topToken.address] ??
@@ -177,6 +174,7 @@ export const useTokensWithFiltering = (
}
}
+ // Yield other tokens from selected chain
for (const token of Object.values(tokenList)) {
const tokenWithTokenListData = buildTokenData(token);
if (tokenWithTokenListData) {
@@ -186,7 +184,7 @@ export const useTokensWithFiltering = (
})();
},
[
- balanceOnActiveChain,
+ multichainTokensWithBalance,
sortedErc20TokensWithBalances,
topTokens,
tokenConversionRates,
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 2f8ea8fda1c9..dbb722fe2ca3 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -33,6 +33,7 @@ import {
getValidationErrors,
} from '../../../ducks/bridge/selectors';
import { TextColor } from '../../../helpers/constants/design-system';
+import { BridgeToken } from '../types';
const generateAssetFromToken = (
chainId: Hex,
@@ -75,7 +76,7 @@ export const BridgeInputGroup = ({
}: {
className: string;
onAmountChange?: (value: string) => void;
- token: SwapsTokenObject | SwapsEthToken | null;
+ token: BridgeToken | null;
amountFieldProps?: Pick<
React.ComponentProps,
'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' | 'className'
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 470b2765e4e0..4b7cc62b107e 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -80,16 +80,17 @@ const PrepareBridgePage = () => {
const quoteRequest = useSelector(getQuoteRequest);
const { activeQuote } = useSelector(getBridgeQuotes);
+ const tokenAddressAllowlistByChainId = useBridgeTokens();
const fromTokenListGenerator = useTokensWithFiltering(
fromTokens,
fromTopAssets,
- TokenBucketPriority.owned,
+ tokenAddressAllowlistByChainId,
fromChain?.chainId,
);
const toTokenListGenerator = useTokensWithFiltering(
toTokens,
toTopAssets,
- TokenBucketPriority.top,
+ tokenAddressAllowlistByChainId,
toChain?.chainId,
);
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index db6d7e8e1394..332590240c6d 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -1,5 +1,10 @@
import { BigNumber } from 'bignumber.js';
import { ChainConfiguration } from '../../../shared/types/bridge';
+import {
+ AssetWithDisplayData,
+ ERC20Asset,
+ NativeAsset,
+} from '../../components/multichain/asset-picker-amount/asset-picker-modal/types';
export type L1GasFees = {
l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller
@@ -23,6 +28,13 @@ export enum SortOrder {
ETA_ASC = 'time_descending',
}
+export type BridgeToken =
+ | ((AssetWithDisplayData | AssetWithDisplayData) & {
+ aggregators?: string[];
+ address: string;
+ })
+ | null;
+
// Types copied from Metabridge API
export enum BridgeFlag {
EXTENSION_CONFIG = 'extension-config',
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 05acc09db520..2a810a8df8fc 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -11,7 +11,8 @@ import { Numeric } from '../../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../../shared/constants/common';
import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay';
-export const isNativeAddress = (address?: string) => address === zeroAddress();
+export const isNativeAddress = (address?: string | null) =>
+ address === zeroAddress() || address === '' || !address;
export const isValidQuoteRequest = (
partialRequest: Partial,
From ed5b419c2dcc37507dc481bb9da05b8291087f97 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:32:18 -0800
Subject: [PATCH 06/69] chore: useBridgeTokens hook that loads address
allowlists
---
ui/hooks/bridge/useBridgeTokens.ts | 34 ++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 ui/hooks/bridge/useBridgeTokens.ts
diff --git a/ui/hooks/bridge/useBridgeTokens.ts b/ui/hooks/bridge/useBridgeTokens.ts
new file mode 100644
index 000000000000..c4575dcf5648
--- /dev/null
+++ b/ui/hooks/bridge/useBridgeTokens.ts
@@ -0,0 +1,34 @@
+import { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { getAllBridgeableNetworks } from '../../ducks/bridge/selectors';
+import { fetchBridgeTokens } from '../../pages/bridge/bridge.util';
+
+export const useBridgeTokens = () => {
+ const allBridgeChains = useSelector(getAllBridgeableNetworks);
+
+ const [tokenAllowlistByChainId, setTokenAllowlistByChainId] = useState<
+ Record>
+ >({});
+
+ useEffect(() => {
+ const tokenAllowlistPromises = Promise.allSettled(
+ allBridgeChains.map(
+ async ({ chainId }) =>
+ await fetchBridgeTokens(chainId).then((tokens) => ({
+ [chainId]: new Set(Object.keys(tokens)),
+ })),
+ ),
+ );
+
+ (async () => {
+ const results = await tokenAllowlistPromises;
+ const tokenAllowlistResults = results.reduce(
+ (acc, { value }) => ({ ...acc, ...value }),
+ {},
+ );
+ setTokenAllowlistByChainId(tokenAllowlistResults);
+ })();
+ }, [allBridgeChains.length]);
+
+ return tokenAllowlistByChainId;
+};
From 360596cdbc67ba8f62666cc150d755a19e091895 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:34:43 -0800
Subject: [PATCH 07/69] chore: style quote card
---
app/_locales/en/messages.json | 36 ++-
shared/constants/bridge.ts | 3 +-
ui/pages/bridge/quotes/bridge-quote-card.tsx | 303 +++++++++++++------
ui/pages/bridge/quotes/index.scss | 63 ----
ui/pages/bridge/quotes/quote-info-row.tsx | 51 ----
5 files changed, 232 insertions(+), 224 deletions(-)
delete mode 100644 ui/pages/bridge/quotes/quote-info-row.tsx
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 6eaa9f3fb7ef..d047796db383 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -746,6 +746,9 @@
"beCareful": {
"message": "Be careful"
},
+ "bestPrice": {
+ "message": "Best price"
+ },
"beta": {
"message": "Beta"
},
@@ -911,13 +914,13 @@
"message": "Swapping $1 for $2",
"description": "$1 is the amount of the source asset, $2 is the amount of the destination asset"
},
+ "bridgeTerms": {
+ "message": "Terms"
+ },
"bridgeTimingMinutes": {
"message": "$1 min",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
},
- "bridgeTimingTooltipText": {
- "message": "This is the estimated time it will take for the bridging to be complete."
- },
"bridgeTo": {
"message": "Bridge to"
},
@@ -979,6 +982,9 @@
"bridgeTypeDirectionTo": {
"message": "To"
},
+ "bridging": {
+ "message": "Bridging"
+ },
"browserNotSupported": {
"message": "Your browser is not supported..."
},
@@ -988,6 +994,9 @@
"builtAroundTheWorld": {
"message": "MetaMask is designed and built around the world."
},
+ "bulletpoint": {
+ "message": "·"
+ },
"busy": {
"message": "Busy"
},
@@ -2121,9 +2130,6 @@
"estimatedFeeTooltip": {
"message": "Amount paid to process the transaction on network."
},
- "estimatedTime": {
- "message": "Estimated time"
- },
"ethGasPriceFetchWarning": {
"message": "Backup gas price is provided as the main gas estimation service is unavailable right now."
},
@@ -2501,6 +2507,12 @@
"holdToRevealUnlockedLabel": {
"message": "hold to reveal circle unlocked"
},
+ "howQuotesWork": {
+ "message": "How quotes work"
+ },
+ "howQuotesWorkExplanation": {
+ "message": "This quote has the best return of the quotes we searched. This is based on the swap rate, which includes bridging fees and a $1% MetaMask fee, minus gas fees. Gas fees depend on how busy the network is and how complex the transaction is."
+ },
"id": {
"message": "ID"
},
@@ -3249,6 +3261,9 @@
"networkFee": {
"message": "Network fee"
},
+ "networkFees": {
+ "message": "Network fees"
+ },
"networkIsBusy": {
"message": "Network is busy. Gas prices are high and estimates are less accurate."
},
@@ -4490,12 +4505,13 @@
"quotedReceiveAmount": {
"message": "$1 receive amount"
},
- "quotedReceivingAmount": {
- "message": "$1 receiving"
- },
+ "quotedTotalCost": { "message": "$1 total cost" },
"rank": {
"message": "Rank"
},
+ "rateIncludesMMFee": {
+ "message": "Rate includes $1% fee"
+ },
"reAddAccounts": {
"message": "re-add any other accounts"
},
@@ -6692,7 +6708,7 @@
"message": "View activity"
},
"viewAllQuotes": {
- "message": "view all quotes"
+ "message": "More quotes"
},
"viewContact": {
"message": "View contact"
diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts
index 06e0d55b8195..ef7cb7f8a785 100644
--- a/shared/constants/bridge.ts
+++ b/shared/constants/bridge.ts
@@ -29,7 +29,7 @@ export const METABRIDGE_ETHEREUM_ADDRESS =
export const BRIDGE_QUOTE_MAX_ETA_SECONDS = 60 * 60; // 1 hour
export const BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE = 0.8; // if a quote returns in x times less return than the best quote, ignore it
-export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'medium';
+export const BRIDGE_PREFERRED_GAS_ESTIMATE = 'high';
export const BRIDGE_DEFAULT_SLIPPAGE = 0.5;
export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record<
@@ -46,3 +46,4 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record<
[CHAIN_IDS.ZKSYNC_ERA]: 'ZkSync Era',
[CHAIN_IDS.BASE]: 'Base',
};
+export const BRIDGE_MM_FEE_RATE = 0.875;
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 72c09403f033..2d8abc58c5cf 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -1,20 +1,26 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import {
- Box,
- Button,
- ButtonVariant,
Text,
+ PopoverPosition,
+ IconName,
+ ButtonLink,
+ Icon,
+ IconSize,
+ AvatarNetwork,
+ AvatarNetworkSize,
} from '../../../components/component-library';
-import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
+import {
+ getBridgeQuotes,
+ getFromChain,
+ getToChain,
+} from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
formatCurrencyAmount,
formatTokenAmount,
formatEtaInMinutes,
} from '../utils/quote';
-import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
-import MascotBackgroundAnimation from '../../swaps/mascot-background-animation/mascot-background-animation';
import { getCurrentCurrency } from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker';
@@ -22,116 +28,215 @@ import { useRequestProperties } from '../../../hooks/bridge/events/useRequestPro
import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties';
import { useQuoteProperties } from '../../../hooks/bridge/events/useQuoteProperties';
import { MetaMetricsEventName } from '../../../../shared/constants/metametrics';
+import {
+ AlignItems,
+ BackgroundColor,
+ BlockSize,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import { Row, Column, Tooltip } from '../layout';
+import { BRIDGE_MM_FEE_RATE } from '../../../../shared/constants/bridge';
+import {
+ CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP,
+ NETWORK_TO_NAME_MAP,
+} from '../../../../shared/constants/network';
+import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils';
+import { TERMS_OF_USE_LINK } from '../../../../shared/constants/terms';
import { BridgeQuotesModal } from './bridge-quotes-modal';
-import { QuoteInfoRow } from './quote-info-row';
export const BridgeQuoteCard = () => {
const t = useI18nContext();
- const { isLoading, isQuoteGoingToRefresh, activeQuote } =
- useSelector(getBridgeQuotes);
+ const { activeQuote } = useSelector(getBridgeQuotes);
const currency = useSelector(getCurrentCurrency);
const ticker = useSelector(getNativeCurrency);
- const secondsUntilNextRefresh = useCountdownTimer();
-
const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker();
const { quoteRequestProperties } = useRequestProperties();
const requestMetadataProperties = useRequestMetadataProperties();
const quoteListProperties = useQuoteProperties();
- const [showAllQuotes, setShowAllQuotes] = useState(false);
+ const fromChain = useSelector(getFromChain);
+ const toChain = useSelector(getToChain);
- if (isLoading && !activeQuote) {
- return (
-
-
-
- );
- }
+ const [showAllQuotes, setShowAllQuotes] = useState(false);
- return activeQuote ? (
-
+ return (
+ <>
setShowAllQuotes(false)}
/>
-
- {!isLoading && isQuoteGoingToRefresh && (
- {t('swapNewQuoteIn', [secondsUntilNextRefresh])}
- )}
-
-
-
-
- {activeQuote.swapRate && (
-
- )}
- {activeQuote.totalNetworkFee && (
-
- )}
-
+ {activeQuote ? (
+
+
+
+ {t('bestPrice')}
+
+ {t('howQuotesWorkExplanation', [BRIDGE_MM_FEE_RATE])}
+
+
+
+ {
+ quoteRequestProperties &&
+ requestMetadataProperties &&
+ quoteListProperties &&
+ trackCrossChainSwapsEvent({
+ event: MetaMetricsEventName.AllQuotesOpened,
+ properties: {
+ ...quoteRequestProperties,
+ ...requestMetadataProperties,
+ ...quoteListProperties,
+ },
+ });
+ setShowAllQuotes(true);
+ }}
+ >
+ {t('viewAllQuotes')}
+
+
+
+
+
+
+ {t('bridging')}
+
+
+
+
+ {
+ NETWORK_TO_NAME_MAP[
+ decimalToPrefixedHex(
+ activeQuote.quote.srcChainId,
+ ) as keyof typeof NETWORK_TO_NAME_MAP
+ ].split(' ')[0]
+ }
+
+
+
+
+ {
+ NETWORK_TO_NAME_MAP[
+ decimalToPrefixedHex(
+ activeQuote.quote.destChainId,
+ ) as keyof typeof NETWORK_TO_NAME_MAP
+ ].split(' ')[0]
+ }
+
+
+
-
-
- {t('swapIncludesMMFee', [0.875])}
- {
- quoteRequestProperties &&
- requestMetadataProperties &&
- quoteListProperties &&
- trackCrossChainSwapsEvent({
- event: MetaMetricsEventName.AllQuotesOpened,
- properties: {
- ...quoteRequestProperties,
- ...requestMetadataProperties,
- ...quoteListProperties,
- },
- });
- setShowAllQuotes(true);
- }}
- >
- {t('viewAllQuotes')}
-
-
-
- {t('termsOfService')}
-
-
-
- ) : null;
+
+
+ {t('networkFees')}
+
+
+
+ {formatCurrencyAmount(
+ activeQuote.totalNetworkFee?.valueInCurrency,
+ currency,
+ 2,
+ ) ??
+ formatTokenAmount(
+ activeQuote.totalNetworkFee?.amount,
+ ticker,
+ 6,
+ )}
+
+
+ {t('bulletpoint')}
+
+
+ {activeQuote.totalNetworkFee?.valueInCurrency
+ ? formatTokenAmount(
+ activeQuote.totalNetworkFee?.amount,
+ ticker,
+ 6,
+ )
+ : undefined}
+
+
+
+
+
+ {t('time')}
+
+
+ {t('bridgeTimingMinutes', [
+ formatEtaInMinutes(
+ activeQuote.estimatedProcessingTimeInSeconds,
+ ),
+ ])}
+
+
+
+
+ {t('rateIncludesMMFee', [BRIDGE_MM_FEE_RATE])}
+
+
+ {t('bulletpoint')}
+
+
+ {t('bridgeTerms')}
+
+
+
+
+ ) : null}
+ >
+ );
};
diff --git a/ui/pages/bridge/quotes/index.scss b/ui/pages/bridge/quotes/index.scss
index 0d9eafa9ad69..ffd58f48d8fe 100644
--- a/ui/pages/bridge/quotes/index.scss
+++ b/ui/pages/bridge/quotes/index.scss
@@ -1,68 +1,5 @@
@use "design-system";
-.quote-card {
- flex-direction: column;
- display: flex;
- text-align: center;
-
- p {
- font-size: 12px;
- }
-
-
- &__content {
- padding: 16px;
- gap: 4px;
- }
-
- &__timer {
- height: 32px;
-
- p {
- color: var(--color-text-alternative);
- }
- }
-
- .bridge-box > &__footer {
- gap: 0;
-
- p {
- color: var(--color-text-alternative);
- }
- }
-
- &__info-row {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
-
- &__label {
- display: inline-flex;
- gap: 4px;
-
- p {
- font-weight: var(--font-weight-medium);
- font-size: 14px;
- white-space: nowrap;
- }
-
- &__tooltip {
- align-items: center;
- height: 100%;
- }
- }
-
- &__description {
- display: inline-flex;
- gap: 4px;
-
- &__secondary {
- color: var(--color-text-alternative);
- }
- }
- }
-}
-
.quotes-modal {
.mm-modal-content__dialog {
display: flex;
diff --git a/ui/pages/bridge/quotes/quote-info-row.tsx b/ui/pages/bridge/quotes/quote-info-row.tsx
deleted file mode 100644
index d1cccc62ed8e..000000000000
--- a/ui/pages/bridge/quotes/quote-info-row.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react';
-import {
- Box,
- Icon,
- IconName,
- IconSize,
- Text,
-} from '../../../components/component-library';
-import Tooltip from '../../../components/ui/tooltip';
-import { IconColor } from '../../../helpers/constants/design-system';
-
-export const QuoteInfoRow = ({
- label,
- tooltipText,
- description,
- secondaryDescription,
-}: {
- label: string;
- tooltipText?: string;
- description: string;
- secondaryDescription?: string;
-}) => {
- return (
-
-
- {label}
- {tooltipText && (
-
-
-
- )}
-
-
-
-
- {secondaryDescription}
-
- {description}
-
-
- );
-};
From 3a704de9ede7174cdd68e955779ae864f79761cc Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:37:53 -0800
Subject: [PATCH 08/69] chore: fix quotes modal colors
---
.../bridge/quotes/bridge-quotes-modal.tsx | 61 +++++++++----------
1 file changed, 29 insertions(+), 32 deletions(-)
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index d251b8ab72f5..48c07f06bd9b 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -117,15 +117,19 @@ export const BridgeQuotesModal = ({
color={
sortOrder === sortOrderOption
? TextColor.primaryDefault
- : TextColor.textAlternative
+ : TextColor.textAlternativeSoft
}
>
{label}
@@ -198,47 +202,40 @@ export const BridgeQuotesModal = ({
{[
totalNetworkFee?.valueInCurrency
- ? t('quotedNetworkFee', [
+ ? t('quotedTotalCost', [
formatCurrencyAmount(
totalNetworkFee.valueInCurrency,
currency,
0,
),
])
- : t('quotedNetworkFee', [
+ : t('quotedTotalCost', [
formatTokenAmount(
totalNetworkFee.amount,
nativeCurrency,
),
]),
- t(
- sortOrder === SortOrder.ETA_ASC
- ? 'quotedReceivingAmount'
- : 'quotedReceiveAmount',
- [
- formatCurrencyAmount(
- toTokenAmount.valueInCurrency,
- currency,
+ t('quotedReceiveAmount', [
+ formatCurrencyAmount(
+ toTokenAmount.valueInCurrency,
+ currency,
+ 0,
+ ) ??
+ formatTokenAmount(
+ toTokenAmount.amount,
+ destAsset.symbol,
0,
- ) ??
- formatTokenAmount(
- toTokenAmount.amount,
- destAsset.symbol,
- 0,
- ),
- ],
- ),
- ]
- [sortOrder === SortOrder.ETA_ASC ? 'reverse' : 'slice']()
- .map((content) => (
-
- {content}
-
- ))}
+ ),
+ ]),
+ ].map((content) => (
+
+ {content}
+
+ ))}
From 87712e85a8afe168a3a4b8a10f086155ad837c07 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:38:40 -0800
Subject: [PATCH 09/69] fix: trim whitespace from formatted token amounts
---
ui/pages/bridge/utils/quote.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 2a810a8df8fc..225bac1e13b2 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -174,9 +174,9 @@ export const formatEtaInMinutes = (estimatedProcessingTimeInSeconds: number) =>
export const formatTokenAmount = (
amount: BigNumber,
- symbol: string,
- precision: number = 2,
-) => `${amount.toFixed(precision)} ${symbol}`;
+ symbol: string = '',
+ precision: number = DEFAULT_PRECISION,
+) => [amount.toFixed(precision), symbol].join(' ').trim();
export const formatCurrencyAmount = (
amount: BigNumber | null,
From f78457a69fe31c7288326704cddd99d1aebe2c55 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:39:19 -0800
Subject: [PATCH 10/69] chore: validate bridge-api token list response
---
ui/pages/bridge/bridge.util.ts | 8 +++++++-
ui/pages/bridge/utils/validators.ts | 12 +++++++++++-
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/bridge.util.ts b/ui/pages/bridge/bridge.util.ts
index 13db68ba2ca4..b108bcbe88bd 100644
--- a/ui/pages/bridge/bridge.util.ts
+++ b/ui/pages/bridge/bridge.util.ts
@@ -50,6 +50,7 @@ import {
validateResponse,
QUOTE_RESPONSE_VALIDATORS,
FEE_DATA_VALIDATORS,
+ TOKEN_AGGREGATOR_VALIDATORS,
} from './utils/validators';
const CLIENT_ID_HEADER = { 'X-Client-Id': BRIDGE_CLIENT_ID };
@@ -124,7 +125,12 @@ export async function fetchBridgeTokens(
tokens.forEach((token: unknown) => {
if (
- validateResponse(TOKEN_VALIDATORS, token, url) &&
+ validateResponse(
+ TOKEN_VALIDATORS.concat(TOKEN_AGGREGATOR_VALIDATORS),
+ token,
+ url,
+ false, // Don't log errors for tokens
+ ) &&
!(
isSwapsDefaultTokenSymbol(token.symbol, chainId) ||
isSwapsDefaultTokenAddress(token.address, chainId)
diff --git a/ui/pages/bridge/utils/validators.ts b/ui/pages/bridge/utils/validators.ts
index a07eae493c79..08fc3519ef52 100644
--- a/ui/pages/bridge/utils/validators.ts
+++ b/ui/pages/bridge/utils/validators.ts
@@ -16,8 +16,9 @@ export const validateResponse = (
validators: Validator[],
data: unknown,
urlUsed: string,
+ logError = true,
): data is ExpectedResponse => {
- return validateData(validators, data, urlUsed);
+ return validateData(validators, data, urlUsed, logError);
};
export const isValidNumber = (v: unknown): v is number => typeof v === 'number';
@@ -53,6 +54,15 @@ export const FEATURE_FLAG_VALIDATORS = [
},
];
+export const TOKEN_AGGREGATOR_VALIDATORS = [
+ {
+ property: 'aggregators',
+ type: 'object',
+ validator: (v: unknown): v is number[] =>
+ isValidObject(v) && Object.values(v).every(isValidString),
+ },
+];
+
export const TOKEN_VALIDATORS = [
{ property: 'decimals', type: 'number' },
{ property: 'address', type: 'string', validator: isValidHexAddress },
From e97aa6e0f650828a6dcea5e1c70f02f4b1090ad6 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:41:27 -0800
Subject: [PATCH 11/69] chore: bridge asset-picker button
---
.../components/bridge-asset-picker-button.tsx | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx
diff --git a/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx b/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx
new file mode 100644
index 000000000000..b04da2b981fa
--- /dev/null
+++ b/ui/pages/bridge/prepare/components/bridge-asset-picker-button.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import {
+ SelectButtonProps,
+ SelectButtonSize,
+} from '../../../../components/component-library/select-button/select-button.types';
+import {
+ AvatarNetwork,
+ AvatarNetworkSize,
+ AvatarToken,
+ BadgeWrapper,
+ IconName,
+ SelectButton,
+ Text,
+} from '../../../../components/component-library';
+import {
+ AlignItems,
+ BackgroundColor,
+ BorderColor,
+ BorderRadius,
+ Display,
+ OverflowWrap,
+ TextVariant,
+} from '../../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { AssetPicker } from '../../../../components/multichain/asset-picker-amount/asset-picker';
+
+export const BridgeAssetPickerButton = ({
+ asset,
+ networkProps,
+ networkImageSrc,
+ ...props
+}: {
+ networkImageSrc?: string;
+} & SelectButtonProps<'div'> &
+ Pick, 'asset' | 'networkProps'>) => {
+ const t = useI18nContext();
+
+ return (
+
+ {asset?.symbol ?? t('bridgeTo')}
+
+ }
+ startAccessory={
+ asset ? (
+
+ ) : undefined
+ }
+ >
+ {asset ? (
+
+ ) : undefined}
+
+ ) : undefined
+ }
+ {...props}
+ />
+ );
+};
From ca976159272bbb470fddca0f54231acf86db26be Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:42:29 -0800
Subject: [PATCH 12/69] fix: navigate to bridge when HW wallet is connected
---
ui/hooks/bridge/useBridging.ts | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts
index 2b7ffb0083c9..a464c2e1a37a 100644
--- a/ui/hooks/bridge/useBridging.ts
+++ b/ui/hooks/bridge/useBridging.ts
@@ -32,6 +32,9 @@ import { isHardwareKeyring } from '../../helpers/utils/hardware';
import { getPortfolioUrl } from '../../helpers/utils/portfolio';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { getProviderConfig } from '../../../shared/modules/selectors/networks';
+// eslint-disable-next-line import/no-restricted-paths
+import { getEnvironmentType } from '../../../app/scripts/lib/util';
+import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../shared/constants/app';
import { useCrossChainSwapsEventTracker } from './useCrossChainSwapsEventTracker';
///: END:ONLY_INCLUDE_IF
@@ -93,16 +96,18 @@ const useBridging = () => {
chain_id: providerConfig.chainId,
},
});
- if (usingHardwareWallet && global.platform.openExtensionInBrowser) {
- global.platform.openExtensionInBrowser(
- PREPARE_SWAP_ROUTE,
- null,
- false,
- );
+ const environmentType = getEnvironmentType();
+ const environmentTypeIsFullScreen =
+ environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
+ const bridgeRoute = `${CROSS_CHAIN_SWAP_ROUTE}${PREPARE_SWAP_ROUTE}?token=${token.address.toLowerCase()}`;
+ if (
+ usingHardwareWallet &&
+ global.platform.openExtensionInBrowser &&
+ !environmentTypeIsFullScreen
+ ) {
+ global.platform.openExtensionInBrowser(bridgeRoute);
} else {
- history.push(
- `${CROSS_CHAIN_SWAP_ROUTE}${PREPARE_SWAP_ROUTE}?token=${token.address.toLowerCase()}`,
- );
+ history.push(bridgeRoute);
}
} else {
const portfolioUrl = getPortfolioUrl(
From 5dd8db97051223b0eb8edc36844b08fb2a888a7e Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:43:24 -0800
Subject: [PATCH 13/69] chore: add size props to MM Mascot animation
---
.../mascot-background-animation.js | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js b/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js
index a8fddf9943d7..b5e8a04dbc5a 100644
--- a/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js
+++ b/ui/pages/swaps/mascot-background-animation/mascot-background-animation.js
@@ -1,10 +1,11 @@
/* eslint-disable @metamask/design-tokens/color-no-hex*/
import EventEmitter from 'events';
import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
import Mascot from '../../../components/ui/mascot';
-export default function MascotBackgroundAnimation() {
+export default function MascotBackgroundAnimation({ height, width }) {
const animationEventEmitter = useRef(new EventEmitter());
return (
@@ -220,11 +221,16 @@ export default function MascotBackgroundAnimation() {
>
);
}
+
+MascotBackgroundAnimation.propTypes = {
+ height: PropTypes.string,
+ width: PropTypes.string,
+};
From fa97379574f83545700125509f5be5e725463b65 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:44:40 -0800
Subject: [PATCH 14/69] chore: style CTA button
---
app/_locales/en/messages.json | 2 +-
ui/pages/bridge/prepare/bridge-cta-button.tsx | 77 +++++++++++++++----
2 files changed, 65 insertions(+), 14 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index d047796db383..5169d783e967 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -875,7 +875,7 @@
"message": "Bridge, don't send"
},
"bridgeEnterAmount": {
- "message": "Enter amount"
+ "message": "Select amount"
},
"bridgeExplorerLinkViewOn": {
"message": "View on $1"
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index c8738a6551de..dab5c5c44132 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -1,6 +1,10 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
-import { Button } from '../../../components/component-library';
+import {
+ ButtonPrimary,
+ ButtonPrimarySize,
+ Text,
+} from '../../../components/component-library';
import {
getFromAmount,
getFromChain,
@@ -12,6 +16,12 @@ import {
} from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import useSubmitBridgeTransaction from '../hooks/useSubmitBridgeTransaction';
+import {
+ BlockSize,
+ TextAlign,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
import { useIsTxSubmittable } from '../../../hooks/bridge/useIsTxSubmittable';
import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker';
@@ -19,6 +29,8 @@ import { useRequestProperties } from '../../../hooks/bridge/events/useRequestPro
import { useRequestMetadataProperties } from '../../../hooks/bridge/events/useRequestMetadataProperties';
import { useTradeProperties } from '../../../hooks/bridge/events/useTradeProperties';
import { MetaMetricsEventName } from '../../../../shared/constants/metametrics';
+import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps';
+import { getNativeCurrency } from '../../../ducks/metamask/metamask';
export const BridgeCTAButton = () => {
const t = useI18nContext();
@@ -36,10 +48,22 @@ export const BridgeCTAButton = () => {
const { submitBridgeTransaction } = useSubmitBridgeTransaction();
- const { isNoQuotesAvailable, isInsufficientBalance } =
- useSelector(getValidationErrors);
+ const {
+ isNoQuotesAvailable,
+ isInsufficientBalance: isInsufficientBalance_,
+ isInsufficientGasBalance: isInsufficientGasBalance_,
+ isInsufficientGasForQuote: isInsufficientGasForQuote_,
+ } = useSelector(getValidationErrors);
const { balanceAmount } = useLatestBalance(fromToken, fromChain?.chainId);
+ const { balanceAmount: nativeAssetBalance } = useLatestBalance(
+ fromChain?.chainId
+ ? SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ]
+ : null,
+ fromChain?.chainId,
+ );
const isTxSubmittable = useIsTxSubmittable();
const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker();
@@ -47,7 +71,17 @@ export const BridgeCTAButton = () => {
const requestMetadataProperties = useRequestMetadataProperties();
const tradeProperties = useTradeProperties();
+ const ticker = useSelector(getNativeCurrency);
const [isQuoteExpired, setIsQuoteExpired] = useState(false);
+
+ const isInsufficientBalance = isInsufficientBalance_(balanceAmount);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const isInsufficientGasBalance =
+ isInsufficientGasBalance_(nativeAssetBalance);
+ const isInsufficientGasForQuote =
+ isInsufficientGasForQuote_(nativeAssetBalance);
+
useEffect(() => {
let timeout: NodeJS.Timeout;
// Reset the isQuoteExpired if quote fethching restarts
@@ -70,14 +104,14 @@ export const BridgeCTAButton = () => {
}
if (isLoading && !isTxSubmittable) {
- return t('swapFetchingQuotes');
+ return '';
}
- if (isNoQuotesAvailable) {
- return t('swapQuotesNotAvailableErrorTitle');
+ if (isInsufficientGasBalance || isNoQuotesAvailable) {
+ return '';
}
- if (isInsufficientBalance(balanceAmount)) {
+ if (isInsufficientBalance || isInsufficientGasForQuote) {
return t('alertReasonInsufficientBalance');
}
@@ -89,7 +123,7 @@ export const BridgeCTAButton = () => {
}
if (isTxSubmittable) {
- return t('confirm');
+ return t('submit');
}
return t('swapSelectToken');
@@ -97,17 +131,25 @@ export const BridgeCTAButton = () => {
isLoading,
fromAmount,
toToken,
+ ticker,
isTxSubmittable,
balanceAmount,
isInsufficientBalance,
isQuoteExpired,
+ isInsufficientGasBalance,
+ isInsufficientGasForQuote,
]);
- return (
- {
- if (activeQuote && isTxSubmittable) {
+ if (activeQuote && isTxSubmittable && !isSubmitting) {
+ setIsSubmitting(true);
quoteRequestProperties &&
requestMetadataProperties &&
tradeProperties &&
@@ -122,9 +164,18 @@ export const BridgeCTAButton = () => {
submitBridgeTransaction(activeQuote);
}
}}
- disabled={!isTxSubmittable || isQuoteExpired}
+ disabled={!isTxSubmittable || isSubmitting || isQuoteExpired}
+ >
+ {label}
+
+ ) : (
+
{label}
-
+
);
};
From fbef5b1f77be2207b8acc93f267c2415461d0774 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:54:07 -0800
Subject: [PATCH 15/69] chore: update page layout
---
app/_locales/en/messages.json | 10 +
ui/pages/bridge/index.scss | 25 --
ui/pages/bridge/index.tsx | 120 +++----
ui/pages/bridge/prepare/index.scss | 160 +++------
.../bridge/prepare/prepare-bridge-page.tsx | 311 ++++++++++++++----
5 files changed, 357 insertions(+), 269 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 5169d783e967..eb8bac6cc831 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -865,6 +865,9 @@
"message": "Approve $1 for bridge",
"description": "Used in the transaction display list to describe a transaction that is an approve call on a token that is to be bridged. $1 is the symbol of a token that has been approved."
},
+ "bridgeApprovalWarning": {
+ "message": "You are allowing access to the specified amount, $1 $2. The contract will not access any additional funds."
+ },
"bridgeCalculatingAmount": {
"message": "Calculating..."
},
@@ -2379,6 +2382,7 @@
"gotIt": {
"message": "Got it"
},
+ "grantExactAccess": { "message": "Grant exact access" },
"grantedToWithColon": {
"message": "Granted to:"
},
@@ -6839,6 +6843,9 @@
"whatsThis": {
"message": "What's this?"
},
+ "willApproveAmountForBridging": {
+ "message": "This will approve $1 for bridging."
+ },
"withdrawing": {
"message": "Withdrawing"
},
@@ -6872,6 +6879,9 @@
"yourNFTmayBeAtRisk": {
"message": "Your NFT may be at risk"
},
+ "yourNetworks": {
+ "message": "Your networks"
+ },
"yourPrivateSeedPhrase": {
"message": "Your Secret Recovery Phrase"
},
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss
index 9dd3b3759478..473cbbe3d7ad 100644
--- a/ui/pages/bridge/index.scss
+++ b/ui/pages/bridge/index.scss
@@ -5,31 +5,6 @@
@import 'transaction-details/index';
-.bridge {
- max-height: 100vh;
- width: 360px;
- position: relative;
-
- &__container {
- width: 100%;
-
- .multichain-page-footer {
- position: absolute;
- width: 100%;
- height: 80px;
- bottom: 0;
- padding: 16px;
- display: flex;
-
- button {
- flex: 1;
- height: 100%;
- font-size: 14px;
- font-weight: 500;
- }
- }
- }
-}
// TODO add to design-tokens package
.mm-avatar-base--size-xxs {
diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx
index 9eddeffee798..10e9984ad1b9 100644
--- a/ui/pages/bridge/index.tsx
+++ b/ui/pages/bridge/index.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Switch, useHistory } from 'react-router-dom';
import { I18nContext } from '../../contexts/i18n';
@@ -16,25 +16,21 @@ import {
ButtonIconSize,
IconName,
} from '../../components/component-library';
-import { getProviderConfig } from '../../../shared/modules/selectors/networks';
import {
- getCurrentCurrency,
- getIsBridgeChain,
- getIsBridgeEnabled,
-} from '../../selectors';
+ getCurrentChainId,
+ getSelectedNetworkClientId,
+} from '../../../shared/modules/selectors/networks';
+import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors';
import useBridging from '../../hooks/bridge/useBridging';
-import {
- Content,
- Footer,
- Header,
-} from '../../components/multichain/pages/page';
+import { Content, Header, Page } from '../../components/multichain/pages/page';
import { useSwapsFeatureFlags } from '../swaps/hooks/useSwapsFeatureFlags';
import { resetBridgeState, setFromChain } from '../../ducks/bridge/actions';
import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates';
import { useBridgeExchangeRates } from '../../hooks/bridge/useBridgeExchangeRates';
import { useQuoteFetchEvents } from '../../hooks/bridge/useQuoteFetchEvents';
+import { TextVariant } from '../../helpers/constants/design-system';
import PrepareBridgePage from './prepare/prepare-bridge-page';
-import { BridgeCTAButton } from './prepare/bridge-cta-button';
+import { BridgeTransactionSettingsModal } from './prepare/bridge-transaction-settings-modal';
const CrossChainSwap = () => {
const t = useContext(I18nContext);
@@ -47,15 +43,15 @@ const CrossChainSwap = () => {
const dispatch = useDispatch();
const isBridgeEnabled = useSelector(getIsBridgeEnabled);
- const providerConfig = useSelector(getProviderConfig);
const isBridgeChain = useSelector(getIsBridgeChain);
- const currency = useSelector(getCurrentCurrency);
+ const selectedNetworkClientId = useSelector(getSelectedNetworkClientId);
+ const chainId = useSelector(getCurrentChainId);
useEffect(() => {
- if (isBridgeChain && isBridgeEnabled && providerConfig) {
- dispatch(setFromChain(providerConfig.chainId));
+ if (isBridgeChain && isBridgeEnabled && chainId) {
+ dispatch(setFromChain(chainId));
}
- }, [isBridgeChain, isBridgeEnabled, providerConfig, currency]);
+ }, [isBridgeChain, isBridgeEnabled, chainId]);
const resetControllerAndInputStates = async () => {
await dispatch(resetBridgeState());
@@ -74,7 +70,7 @@ const CrossChainSwap = () => {
}, []);
// Needed for refreshing gas estimates
- useGasFeeEstimates(providerConfig?.id);
+ useGasFeeEstimates(selectedNetworkClientId);
// Needed for fetching exchange rates for tokens that have not been imported
useBridgeExchangeRates();
// Emits events related to quote-fetching
@@ -90,46 +86,56 @@ const CrossChainSwap = () => {
await resetControllerAndInputStates();
};
+ const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
+
return (
-
-
-
- }
- endAccessory={
-
- }
- >
- {t('bridge')}
-
-
-
- {
- return ;
- }}
- />
-
-
-
-
-
+
+
+ }
+ endAccessory={
+ {
+ setIsSettingsModalOpen(true);
+ }}
+ />
+ }
+ >
+ {t('bridge')}
+
+
+
+ {
+ return (
+ <>
+ {
+ setIsSettingsModalOpen(false);
+ }}
+ />
+
+ >
+ );
+ }}
+ />
+
+
+
);
};
diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss
index 91fd4f242a20..b1ad6586fc7c 100644
--- a/ui/pages/bridge/prepare/index.scss
+++ b/ui/pages/bridge/prepare/index.scss
@@ -1,72 +1,40 @@
@use "design-system";
.prepare-bridge-page {
- display: flex;
- flex-flow: column;
flex: 1;
- width: 100%;
- gap: 24px;
-
- &__content {
- display: flex;
- flex-direction: column;
- padding: 16px 0 16px 0;
- border-radius: 8px;
- border: 1px solid var(--color-border-muted);
- }
-
- .bridge-box {
- display: flex;
- flex-direction: column;
- gap: 4px;
- justify-content: center;
- padding: 8px 16px 8px 16px;
- }
-
- &__input-row {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- width: 298px;
- gap: 16px;
-
- input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
- -moz-appearance: none;
- display: none;
- }
-
- input[type="number"]:hover::-webkit-inner-spin-button {
- -webkit-appearance: none;
- -moz-appearance: none;
- display: none;
- }
-
- .mm-text-field {
- background-color: inherit;
+ .mm-text-field {
+ background-color: inherit;
- &--focused {
- outline: none;
- }
+ &--focused {
+ outline: none;
}
+ }
- .defined {
+ .defined {
+ & > .mm-input--disabled, p {
opacity: 1;
-
- & > .mm-input--disabled {
- opacity: 1;
- }
}
}
+ .mm-select-button__content {
+ max-height: 100%;
+ overflow: hidden;
+ }
+
.amount-input {
border: none;
+ padding: 0;
+ width: 100%;
+ gap: 4px;
+ height: fit-content;
+ font-weight: 400;
+ font-size: 36px;
input {
- text-align: right;
- padding-right: 0;
- font-size: 24px;
- font-weight: 700;
+ text-align: left;
+ font-weight: 400;
+ font-size: 36px;
+ padding: 0;
&:focus,
&:focus-visible {
@@ -79,66 +47,8 @@
}
}
- &__amounts-row {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- width: 298px;
- height: 22px;
-
- p,
- span {
- color: var(--color-text-alternative);
- font-size: 12px;
- }
- }
-
- .asset-picker {
- border: 1px solid var(--color-border-muted);
- height: 40px;
- min-width: fit-content;
- max-width: fit-content;
- background-color: inherit;
-
- p {
- font-size: 14px;
- font-weight: 500;
- }
-
- .mm-avatar-token {
- height: 24px;
- width: 24px;
- border: 1px solid var(--color-border-muted);
- }
-
- .mm-badge-wrapper__badge-container .mm-avatar-base {
- height: 10px;
- width: 10px;
- border: none;
- }
- }
-
&__switch-tokens {
- display: flex;
- justify-content: center;
- align-items: center;
-
- &::before,
- &::after {
- content: '';
- border-top: 1px solid var(--color-border-muted);
- flex-grow: 1;
- }
-
button {
- border-radius: 50%;
- padding: 10px;
- border: 1px solid var(--color-border-muted);
- transition: all 0.3s ease-in-out;
- cursor: pointer;
- width: 40px;
- height: 40px;
-
&:hover:enabled {
background: var(--color-background-default-hover);
@@ -161,10 +71,32 @@
}
.mm-icon {
- color: var(--color-icon-alternative);
transition: all 0.3s ease-in-out;
}
- }
+ }
+
+ .mascot-background-animation__animation {
+ margin-top: 24px;
+ margin-bottom: 16px;
+ }
+
+ .highlight {
+ padding: 16px;
+ background: var(--color-background-default);
+ border: none;
+ border-radius: 8px;
+
+ [data-theme='light'],
+ .light {
+ box-shadow: 0px 0px 2px 0px #E2E4E9, 0px 0px 16px 0px rgba(226, 228, 233, 0.16);
+ }
+
+ [data-theme='dark'],
+ .dark {
+ box-shadow: 0px 0px 2px 0px #18191B, 0px 0px 16px 0px #18191B;
+ }
+
+ }
}
.bridge-settings-modal {
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 4b7cc62b107e..04509d1a3a3a 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -1,8 +1,15 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classnames from 'classnames';
import { debounce } from 'lodash';
import { useHistory, useLocation } from 'react-router-dom';
+import { BigNumber } from 'bignumber.js';
import {
setFromChain,
setFromToken,
@@ -12,6 +19,7 @@ import {
setToChainId,
setToToken,
updateQuoteRequestParams,
+ resetBridgeState,
} from '../../../ducks/bridge/actions';
import {
getBridgeQuotes,
@@ -20,30 +28,45 @@ import {
getFromChains,
getFromToken,
getFromTokens,
- getFromTopAssets,
getQuoteRequest,
getSlippage,
getToChain,
getToChains,
getToToken,
getToTokens,
- getToTopAssets,
+ getFromAmountInCurrency,
+ getValidationErrors,
+ getBridgeQuotesConfig,
} from '../../../ducks/bridge/selectors';
import {
+ BannerAlert,
+ BannerAlertSeverity,
Box,
ButtonIcon,
IconName,
+ PopoverPosition,
+ Text,
} from '../../../components/component-library';
-import { BlockSize } from '../../../helpers/constants/design-system';
+import {
+ BackgroundColor,
+ BlockSize,
+ Display,
+ FlexDirection,
+ IconColor,
+ JustifyContent,
+ TextAlign,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
-import { TokenBucketPriority } from '../../../../shared/constants/swaps';
+import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps';
import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering';
import { setActiveNetwork } from '../../../store/actions';
import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
import { QuoteRequest } from '../types';
import { calcTokenValue } from '../../../../shared/lib/swaps-utils';
import { BridgeQuoteCard } from '../quotes/bridge-quote-card';
-import { isValidQuoteRequest } from '../utils/quote';
+import { formatTokenAmount, isValidQuoteRequest } from '../utils/quote';
import { getProviderConfig } from '../../../../shared/modules/selectors/networks';
import {
CrossChainSwapsEventProperties,
@@ -52,7 +75,16 @@ import {
import { useRequestProperties } from '../../../hooks/bridge/events/useRequestProperties';
import { MetaMetricsEventName } from '../../../../shared/constants/metametrics';
import { isNetworkAdded } from '../../../ducks/bridge/utils';
+import { Footer } from '../../../components/multichain/pages/page';
+import MascotBackgroundAnimation from '../../swaps/mascot-background-animation/mascot-background-animation';
+import { Column, Row, Tooltip } from '../layout';
+import useRamps from '../../../hooks/ramps/useRamps/useRamps';
+import { getNativeCurrency } from '../../../ducks/metamask/metamask';
+import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
+import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
+import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens';
import { BridgeInputGroup } from './bridge-input-group';
+import { BridgeCTAButton } from './bridge-cta-button';
const PrepareBridgePage = () => {
const dispatch = useDispatch();
@@ -60,12 +92,18 @@ const PrepareBridgePage = () => {
const t = useI18nContext();
const fromToken = useSelector(getFromToken);
- const fromTokens = useSelector(getFromTokens);
- const fromTopAssets = useSelector(getFromTopAssets);
+ const {
+ fromTokens,
+ fromTopAssets,
+ isLoading: isFromTokensLoading,
+ } = useSelector(getFromTokens);
const toToken = useSelector(getToToken);
- const toTokens = useSelector(getToTokens);
- const toTopAssets = useSelector(getToTopAssets);
+ const {
+ toTokens,
+ toTopAssets,
+ isLoading: isToTokensLoading,
+ } = useSelector(getToTokens);
const fromChains = useSelector(getFromChains);
const toChains = useSelector(getToChains);
@@ -73,12 +111,16 @@ const PrepareBridgePage = () => {
const toChain = useSelector(getToChain);
const fromAmount = useSelector(getFromAmount);
+ const fromAmountInFiat = useSelector(getFromAmountInCurrency);
const providerConfig = useSelector(getProviderConfig);
const slippage = useSelector(getSlippage);
const quoteRequest = useSelector(getQuoteRequest);
- const { activeQuote } = useSelector(getBridgeQuotes);
+ const { isLoading, activeQuote, isQuoteGoingToRefresh } =
+ useSelector(getBridgeQuotes);
+
+ const { refreshRate } = useSelector(getBridgeQuotesConfig);
const tokenAddressAllowlistByChainId = useBridgeTokens();
const fromTokenListGenerator = useTokensWithFiltering(
@@ -99,13 +141,22 @@ const PrepareBridgePage = () => {
const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
+ useEffect(() => {
+ // Reset controller and inputs on load
+ dispatch(resetBridgeState());
+ }, []);
+
const quoteParams = useMemo(
() => ({
srcTokenAddress: fromToken?.address,
destTokenAddress: toToken?.address || undefined,
srcTokenAmount:
- fromAmount && fromAmount !== '' && fromToken?.decimals
- ? calcTokenValue(fromAmount, fromToken.decimals).toString()
+ fromAmount && fromToken?.decimals
+ ? calcTokenValue(
+ // Treat empty or incomplete amount as 0 to reject NaN
+ ['', '.'].includes(fromAmount) ? '0' : fromAmount,
+ fromToken.decimals,
+ ).toFixed()
: undefined,
srcChainId: fromChain?.chainId
? Number(hexToDecimal(fromChain.chainId))
@@ -196,72 +247,106 @@ const PrepareBridgePage = () => {
}, [fromChain, fromToken, fromTokens, search]);
return (
-
-
- {
- dispatch(setFromTokenInputValue(e));
- }}
- onAssetChange={(token) => {
- token?.address &&
- trackInputEvent({
- input: 'token_source',
- value: token.address,
- });
- dispatch(setFromToken(token));
+
+ {
+ dispatch(setFromTokenInputValue(e));
+ }}
+ onAssetChange={(token) => {
+ dispatch(setFromToken(token));
+ dispatch(setFromTokenInputValue(null));
+ token?.address &&
+ trackInputEvent({
+ input: 'token_source',
+ value: token.address,
+ });
+ dispatch(setFromToken(token));
+ dispatch(setFromTokenInputValue(null));
+ }}
+ networkProps={{
+ network: fromChain,
+ networks: fromChains,
+ onNetworkChange: (networkConfig) => {
+ trackInputEvent({
+ input: 'chain_source',
+ value: networkConfig.chainId,
+ });
+ if (networkConfig.chainId === toChain?.chainId) {
+ dispatch(setToChainId(null));
+ }
+ if (isNetworkAdded(networkConfig)) {
+ dispatch(
+ setActiveNetwork(
+ networkConfig.rpcEndpoints[
+ networkConfig.defaultRpcEndpointIndex
+ ].networkClientId,
+ ),
+ );
+ }
+ dispatch(setFromChain(networkConfig.chainId));
+ dispatch(setFromToken(null));
dispatch(setFromTokenInputValue(null));
- }}
- networkProps={{
- network: fromChain,
- networks: fromChains,
- onNetworkChange: (networkConfig) => {
- trackInputEvent({
- input: 'chain_source',
- value: networkConfig.chainId,
- });
- if (networkConfig.chainId === toChain?.chainId) {
- dispatch(setToChainId(null));
- }
- if (isNetworkAdded(networkConfig)) {
- dispatch(
- setActiveNetwork(
- networkConfig.rpcEndpoints[
- networkConfig.defaultRpcEndpointIndex
- ].networkClientId,
- ),
- );
- }
- dispatch(setFromChain(networkConfig.chainId));
- dispatch(setFromToken(null));
- dispatch(setFromTokenInputValue(null));
- },
- header: t('bridgeFrom'),
- }}
- customTokenListGenerator={
- fromTokens && fromTopAssets ? fromTokenListGenerator : undefined
- }
- amountFieldProps={{
- testId: 'from-amount',
- autoFocus: true,
- value: fromAmount || undefined,
- }}
- isMultiselectEnabled={true}
- />
+ },
+ header: t('yourNetworks'),
+ }}
+ isMultiselectEnabled
+ customTokenListGenerator={
+ fromTokens && fromTopAssets ? fromTokenListGenerator : undefined
+ }
+ onMaxButtonClick={(value: string) => {
+ dispatch(setFromTokenInputValue(value));
+ }}
+ amountInFiat={fromAmountInFiat}
+ amountFieldProps={{
+ testId: 'from-amount',
+ autoFocus: true,
+ value: fromAmount || undefined,
+ }}
+ isTokenListLoading={isFromTokensLoading}
+ />
-
+
+
{
setRotateSwitchTokens(!rotateSwitchTokens);
@@ -288,7 +373,6 @@ const PrepareBridgePage = () => {
{
@@ -311,25 +395,106 @@ const PrepareBridgePage = () => {
dispatch(setToChain(networkConfig.chainId));
},
header: t('bridgeTo'),
+ shouldDisableNetwork: ({ chainId }) =>
+ chainId === fromChain?.chainId,
}}
customTokenListGenerator={
toChain && toTokens && toTopAssets
? toTokenListGenerator
: fromTokenListGenerator
}
+ amountInFiat={
+ activeQuote?.toTokenAmount?.valueInCurrency || undefined
+ }
amountFieldProps={{
testId: 'to-amount',
readOnly: true,
disabled: true,
value: activeQuote?.toTokenAmount?.amount.toFixed() ?? '0',
- className: activeQuote?.toTokenAmount.amount
+ autoFocus: false,
+ className: activeQuote?.toTokenAmount?.amount
? 'amount-input defined'
: 'amount-input',
}}
+ isTokenListLoading={isToTokensLoading}
/>
-
-
-
+
+ {isLoading && !activeQuote ? (
+ <>
+
+ {t('swapFetchingQuotes')}
+
+
+ >
+ ) : null}
+
+
+
+
+ {activeQuote && isQuoteGoingToRefresh && (
+
+ )}
+
+
+
+ {activeQuote?.approval && fromAmount && fromToken ? (
+
+
+ {t('willApproveAmountForBridging', [
+ formatTokenAmount(
+ new BigNumber(fromAmount),
+ fromToken.symbol,
+ ),
+ ])}
+
+ {fromAmount && (
+
+ {t('bridgeApprovalWarning', [
+ fromAmount,
+ fromToken.symbol,
+ ])}
+
+ )}
+
+ ) : null}
+
+
+
+
+
);
};
From 9dbb90092b1877f294c1155ca3144d99118e8aa8 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:55:10 -0800
Subject: [PATCH 16/69] chore: update countdown timer hook
---
ui/hooks/bridge/useCountdownTimer.ts | 15 ++++++---------
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 2 ++
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/ui/hooks/bridge/useCountdownTimer.ts b/ui/hooks/bridge/useCountdownTimer.ts
index 39e7ac9d2eca..ad022f7d4274 100644
--- a/ui/hooks/bridge/useCountdownTimer.ts
+++ b/ui/hooks/bridge/useCountdownTimer.ts
@@ -1,18 +1,17 @@
-import { Duration } from 'luxon';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import {
getBridgeQuotes,
getBridgeQuotesConfig,
} from '../../ducks/bridge/selectors';
-import { SECOND } from '../../../shared/constants/time';
+const STEP = 1000;
/**
* Custom hook that provides a countdown timer based on the last fetched quotes timestamp.
*
* This hook calculates the remaining time until the next refresh interval and updates every second.
*
- * @returns The formatted remaining time in 'm:ss' format.
+ * @returns The remaining time in milliseconds.
*/
export const useCountdownTimer = () => {
const { quotesLastFetchedMs } = useSelector(getBridgeQuotes);
@@ -22,18 +21,16 @@ export const useCountdownTimer = () => {
useEffect(() => {
if (quotesLastFetchedMs) {
- setTimeRemaining(
- refreshRate - (Date.now() - quotesLastFetchedMs) + SECOND,
- );
+ setTimeRemaining(refreshRate - (Date.now() - quotesLastFetchedMs) + STEP);
}
}, [quotesLastFetchedMs]);
useEffect(() => {
const interval = setInterval(() => {
- setTimeRemaining(Math.max(0, timeRemaining - SECOND));
- }, SECOND);
+ setTimeRemaining(Math.max(0, timeRemaining - STEP));
+ }, STEP);
return () => clearInterval(interval);
}, [timeRemaining]);
- return Duration.fromMillis(timeRemaining).toFormat('m:ss');
+ return timeRemaining;
};
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 04509d1a3a3a..74eafa06802c 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -139,6 +139,8 @@ const PrepareBridgePage = () => {
const { flippedRequestProperties } = useRequestProperties();
const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker();
+ const millisecondsUntilNextRefresh = useCountdownTimer();
+
const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
useEffect(() => {
From fe55c26b8e57fc00b3883d0a45c856bf36e264af Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:57:18 -0800
Subject: [PATCH 17/69] chore: banner alerts for validations
---
app/_locales/en/messages.json | 9 ++++
ui/ducks/bridge/selectors.ts | 13 +++--
.../bridge/prepare/prepare-bridge-page.tsx | 49 +++++++++++++++++++
3 files changed, 66 insertions(+), 5 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index eb8bac6cc831..9baaaaecb31d 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -985,6 +985,12 @@
"bridgeTypeDirectionTo": {
"message": "To"
},
+ "bridgeValidationInsufficientGasMessage": {
+ "message": "You don't have enough $1 to pay the gas fee for this bridge. Enter a smaller amount or buy more $1."
+ },
+ "bridgeValidationInsufficientGasTitle": {
+ "message": "More $1 needed for gas"
+ },
"bridging": {
"message": "Bridging"
},
@@ -3502,6 +3508,9 @@
"noNetworksFound": {
"message": "No networks found for the given search query"
},
+ "noOptionsAvailableMessage": {
+ "message": "This trade route isn't available right now. Try changing the amount, network, or token and we'll find the best option."
+ },
"noSnaps": {
"message": "You don't have any snaps installed."
},
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 532f4988f90a..51b57c0d0990 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -493,14 +493,14 @@ export const getFromAmountInCurrency = createSelector(
export const getValidationErrors = createDeepEqualSelector(
getBridgeQuotes,
- getFromAmount,
_getValidatedSrcAmount,
getFromToken,
+ getFromAmount,
(
{ activeQuote, quotesLastFetchedMs, isLoading },
- fromAmount,
validatedSrcAmount,
fromToken,
+ fromTokenInputValue,
) => {
return {
isNoQuotesAvailable: Boolean(
@@ -517,7 +517,7 @@ export const getValidationErrors = createDeepEqualSelector(
},
// Shown after fetching quotes
isInsufficientGasForQuote: (balance?: BigNumber) => {
- if (balance && activeQuote && fromToken) {
+ if (balance && activeQuote && fromToken && fromTokenInputValue) {
return isNativeAddress(fromToken.address)
? balance
.sub(activeQuote.totalNetworkFee.amount)
@@ -528,10 +528,13 @@ export const getValidationErrors = createDeepEqualSelector(
return false;
},
isInsufficientBalance: (balance?: BigNumber) =>
- fromAmount && balance !== undefined ? balance.lt(fromAmount) : false,
+ validatedSrcAmount && balance !== undefined
+ ? balance.lt(validatedSrcAmount)
+ : false,
isEstimatedReturnLow:
activeQuote?.sentAmount?.valueInCurrency &&
- activeQuote?.adjustedReturn?.valueInCurrency
+ activeQuote?.adjustedReturn?.valueInCurrency &&
+ fromTokenInputValue
? activeQuote.adjustedReturn.valueInCurrency.lt(
new BigNumber(
BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE,
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 74eafa06802c..6227d9dcc187 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -122,6 +122,18 @@ const PrepareBridgePage = () => {
const { refreshRate } = useSelector(getBridgeQuotesConfig);
+ const ticker = useSelector(getNativeCurrency);
+ const { isNoQuotesAvailable, isInsufficientGasForQuote } =
+ useSelector(getValidationErrors);
+ const { openBuyCryptoInPdapp } = useRamps();
+
+ const { balanceAmount: nativeAssetBalance } = useLatestBalance(
+ SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ fromChain?.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ],
+ fromChain?.chainId,
+ );
+
const tokenAddressAllowlistByChainId = useBridgeTokens();
const fromTokenListGenerator = useTokensWithFiltering(
fromTokens,
@@ -148,6 +160,17 @@ const PrepareBridgePage = () => {
dispatch(resetBridgeState());
}, []);
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ if (isInsufficientGasForQuote(nativeAssetBalance)) {
+ scrollRef.current?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start',
+ });
+ }
+ }, [isInsufficientGasForQuote(nativeAssetBalance)]);
+
const quoteParams = useMemo(
() => ({
srcTokenAddress: fromToken?.address,
@@ -495,6 +518,32 @@ const PrepareBridgePage = () => {
+ {isNoQuotesAvailable && (
+
+ )}
+ {!isLoading &&
+ activeQuote &&
+ isInsufficientGasForQuote(nativeAssetBalance) && (
+ openBuyCryptoInPdapp()}
+ />
+ )}
);
From bf3e8d293aaa19740a34f77ac988cde106d779d8 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 17:58:42 -0800
Subject: [PATCH 18/69] chore: style input fields
---
app/_locales/en/messages.json | 6 +
.../bridge/prepare/bridge-input-group.tsx | 296 +++++++++++-------
2 files changed, 183 insertions(+), 119 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 9baaaaecb31d..9cd23d990e04 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -2988,6 +2988,12 @@
"low": {
"message": "Low"
},
+ "lowEstimatedReturnTooltipMessage": {
+ "message": "Either your rate or your fees are less favorable than usual. It looks like you'll get back less than $1% of the amount you’re bridging."
+ },
+ "lowEstimatedReturnTooltipTitle": {
+ "message": "Low estimated return"
+ },
"lowGasSettingToolTipMessage": {
"message": "Use $1 to wait for a cheaper price. Time estimates are much less accurate as prices are somewhat unpredictable.",
"description": "$1 is key 'low' separated here so that it can be passed in with bold font-weight"
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index dbb722fe2ca3..85366eb6f9c7 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -1,186 +1,244 @@
-import React from 'react';
-import { Hex } from '@metamask/utils';
+import React, { useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
-import { SwapsTokenObject } from '../../../../shared/constants/swaps';
+import { BigNumber } from 'bignumber.js';
import {
- Box,
Text,
TextField,
TextFieldType,
+ ButtonLink,
+ PopoverPosition,
+ Button,
+ ButtonSize,
} from '../../../components/component-library';
import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker';
import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs';
-import CurrencyDisplay from '../../../components/ui/currency-display';
import { useI18nContext } from '../../../hooks/useI18nContext';
-import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
-import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount';
-import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils';
-import Tooltip from '../../../components/ui/tooltip';
-import { SwapsEthToken } from '../../../selectors';
+import { getCurrentCurrency } from '../../../selectors';
+import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote';
+import { Column, Row, Tooltip } from '../layout';
import {
- ERC20Asset,
- NativeAsset,
-} from '../../../components/multichain/asset-picker-amount/asset-picker-modal/types';
-import { zeroAddress } from '../../../__mocks__/ethereumjs-util';
+ Display,
+ FontWeight,
+ TextAlign,
+ JustifyContent,
+ TextVariant,
+ TextColor,
+} from '../../../helpers/constants/design-system';
import { AssetType } from '../../../../shared/constants/transaction';
-import {
- CHAIN_ID_TO_CURRENCY_SYMBOL_MAP,
- CHAIN_ID_TOKEN_IMAGE_MAP,
-} from '../../../../shared/constants/network';
+import { BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE } from '../../../../shared/constants/bridge';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
import {
getBridgeQuotes,
getValidationErrors,
} from '../../../ducks/bridge/selectors';
-import { TextColor } from '../../../helpers/constants/design-system';
+import { shortenString } from '../../../helpers/utils/util';
import { BridgeToken } from '../types';
-
-const generateAssetFromToken = (
- chainId: Hex,
- tokenDetails: SwapsTokenObject | SwapsEthToken,
-): ERC20Asset | NativeAsset => {
- if ('iconUrl' in tokenDetails && tokenDetails.address !== zeroAddress()) {
- return {
- type: AssetType.token,
- image: tokenDetails.iconUrl,
- symbol: tokenDetails.symbol,
- address: tokenDetails.address,
- chainId,
- };
- }
-
- return {
- type: AssetType.native,
- image:
- CHAIN_ID_TOKEN_IMAGE_MAP[
- chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
- ],
- symbol:
- CHAIN_ID_TO_CURRENCY_SYMBOL_MAP[
- chainId as keyof typeof CHAIN_ID_TO_CURRENCY_SYMBOL_MAP
- ],
- chainId,
- };
-};
+import { BridgeAssetPickerButton } from './components/bridge-asset-picker-button';
export const BridgeInputGroup = ({
- className,
header,
token,
onAssetChange,
onAmountChange,
networkProps,
+ isTokenListLoading,
customTokenListGenerator,
+ amountFieldProps,
+ amountInFiat,
+ onMaxButtonClick,
isMultiselectEnabled,
- amountFieldProps = {},
}: {
- className: string;
+ amountInFiat?: BigNumber;
onAmountChange?: (value: string) => void;
token: BridgeToken | null;
- amountFieldProps?: Pick<
+ amountFieldProps: Pick<
React.ComponentProps,
'testId' | 'autoFocus' | 'value' | 'readOnly' | 'disabled' | 'className'
>;
+ onMaxButtonClick?: (value: string) => void;
} & Pick<
React.ComponentProps,
| 'networkProps'
| 'header'
| 'customTokenListGenerator'
| 'onAssetChange'
+ | 'isTokenListLoading'
| 'isMultiselectEnabled'
>) => {
const t = useI18nContext();
- const { isLoading, activeQuote } = useSelector(getBridgeQuotes);
- const { isInsufficientBalance } = useSelector(getValidationErrors);
+ const { isLoading } = useSelector(getBridgeQuotes);
+ const { isInsufficientBalance, isEstimatedReturnLow } =
+ useSelector(getValidationErrors);
+ const currency = useSelector(getCurrentCurrency);
- const tokenFiatValue = useTokenFiatAmount(
- token?.address || undefined,
- amountFieldProps?.value?.toString() || '0x0',
- token?.symbol,
- {
- showFiat: true,
- },
- true,
- );
- const ethFiatValue = useEthFiatAmount(
- amountFieldProps?.value?.toString() || '0x0',
- { showFiat: true },
- true,
- );
+ const selectedChainId = networkProps?.network?.chainId;
- const { formattedBalance, balanceAmount } = useLatestBalance(
- token,
- networkProps?.network?.chainId,
- );
+ const blockExplorerUrl =
+ networkProps?.network?.defaultBlockExplorerUrlIndex === undefined
+ ? undefined
+ : networkProps.network.blockExplorerUrls?.[
+ networkProps.network.defaultBlockExplorerUrlIndex
+ ];
+
+ const { balanceAmount } = useLatestBalance(token, selectedChainId);
+
+ const inputRef = useRef(null);
+
+ const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true);
+
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.value = amountFieldProps?.value?.toString() ?? '';
+ inputRef.current.focus();
+ }
+ }, [amountFieldProps]);
const isAmountReadOnly =
amountFieldProps?.readOnly || amountFieldProps?.disabled;
return (
-
-
+
+
+ ) => {
+ // Only allow numbers and at most one decimal point
+ if (
+ e &&
+ !/^[0-9]*\.{0,1}[0-9]*$/u.test(
+ `${amountFieldProps.value ?? ''}${e.key}`,
+ )
+ ) {
+ e.preventDefault();
+ }
+ }}
+ onChange={(e) => {
+ // Remove characters that are not numbers or decimal points if rendering a controlled or pasted value
+ const cleanedValue = e.target.value.replace(/[^0-9.]+/gu, '');
+ onAmountChange?.(cleanedValue);
+ }}
+ {...amountFieldProps}
+ />
-
-
+ isAmountReadOnly && !token ? (
+
+ {t('bridgeTo')}
+
+ ) : (
+
+ )
+ }
+
+
+
+
+
+ {isAmountReadOnly &&
+ isEstimatedReturnLow &&
+ isLowReturnTooltipOpen && (
+ setIsLowReturnTooltipOpen(false)}
+ triggerElement={ }
+ flip={false}
+ offset={[0, 80]}
+ >
+ {t('lowEstimatedReturnTooltipMessage', [
+ BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE * 100,
+ ])}
+
+ )}
+ {
- onAmountChange?.(e.target.value);
- }}
- {...amountFieldProps}
- />
-
-
-
+ textAlign={TextAlign.End}
+ ellipsis
+ >
+ {isAmountReadOnly && isLoading && amountFieldProps.value === '0'
+ ? t('bridgeCalculatingAmount')
+ : undefined}
+ {amountInFiat && formatCurrencyAmount(amountInFiat, currency, 2)}
+
+
+
- {formattedBalance ? `${t('balance')}: ${formattedBalance}` : ' '}
+ {isAmountReadOnly &&
+ token &&
+ selectedChainId &&
+ blockExplorerUrl &&
+ token.type === AssetType.token
+ ? shortenString(token.address, {
+ truncatedCharLimit: 11,
+ truncatedStartChars: 4,
+ truncatedEndChars: 4,
+ skipCharacterInEnd: false,
+ })
+ : undefined}
+ {!isAmountReadOnly && balanceAmount
+ ? formatTokenAmount(balanceAmount, token?.symbol)
+ : undefined}
+ {onMaxButtonClick &&
+ token &&
+ token.type !== AssetType.native &&
+ balanceAmount && (
+ onMaxButtonClick(balanceAmount?.toString())}
+ >
+ {t('max')}
+
+ )}
-
-
-
+
+
);
};
From c8a3c123b89c54cc7e010f1959d4c68c1d795095 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 18:00:25 -0800
Subject: [PATCH 19/69] fix: incorrect quote sorting
---
ui/ducks/bridge/selectors.ts | 54 ++++--------------------------------
1 file changed, 6 insertions(+), 48 deletions(-)
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 51b57c0d0990..bd91928795ea 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -271,8 +271,8 @@ export const getToTokenConversionRate = createDeepEqualSelector(
},
);
-const _getQuotesWithMetadata = createDeepEqualSelector(
- (state) => state.metamask.bridgeState.quotes,
+const _getQuotesWithMetadata = createSelector(
+ (state: BridgeAppState) => state.metamask.bridgeState.quotes,
getToTokenConversionRate,
getFromTokenConversionRate,
getConversionRate,
@@ -330,7 +330,7 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
},
);
-const _getSortedQuotesWithMetadata = createDeepEqualSelector(
+const _getSortedQuotesWithMetadata = createSelector(
_getQuotesWithMetadata,
getBridgeSortOrder,
(quotesWithMetadata, sortOrder) => {
@@ -341,56 +341,16 @@ const _getSortedQuotesWithMetadata = createDeepEqualSelector(
(quote) => quote.estimatedProcessingTimeInSeconds,
'asc',
);
- case SortOrder.COST_ASC:
default:
return orderBy(
quotesWithMetadata,
- ({ cost }) => cost.valueInCurrency,
+ ({ cost }) => cost.valueInCurrency?.toNumber(),
'asc',
);
}
},
);
-const _getRecommendedQuote = createDeepEqualSelector(
- _getSortedQuotesWithMetadata,
- getBridgeSortOrder,
- (sortedQuotesWithMetadata, sortOrder) => {
- if (!sortedQuotesWithMetadata.length) {
- return undefined;
- }
-
- const bestReturnValue = BigNumber.max(
- sortedQuotesWithMetadata.map(
- ({ adjustedReturn }) => adjustedReturn.valueInCurrency ?? 0,
- ),
- );
-
- const isFastestQuoteValueReasonable = (
- adjustedReturnInCurrency: BigNumber | null,
- ) =>
- adjustedReturnInCurrency
- ? adjustedReturnInCurrency
- .div(bestReturnValue)
- .gte(BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE)
- : true;
-
- const isBestPricedQuoteETAReasonable = (
- estimatedProcessingTimeInSeconds: number,
- ) => estimatedProcessingTimeInSeconds < BRIDGE_QUOTE_MAX_ETA_SECONDS;
-
- return (
- sortedQuotesWithMetadata.find((quote) => {
- return sortOrder === SortOrder.ETA_ASC
- ? isFastestQuoteValueReasonable(quote.adjustedReturn.valueInCurrency)
- : isBestPricedQuoteETAReasonable(
- quote.estimatedProcessingTimeInSeconds,
- );
- }) ?? sortedQuotesWithMetadata[0]
- );
- },
-);
-
// Generates a pseudo-unique string that identifies each quote
// by aggregator, bridge, steps and value
const _getQuoteIdentifier = ({ quote }: QuoteResponse & L1GasFees) =>
@@ -413,7 +373,6 @@ const _getSelectedQuote = createSelector(
export const getBridgeQuotes = createSelector(
_getSortedQuotesWithMetadata,
- _getRecommendedQuote,
_getSelectedQuote,
(state) => state.metamask.bridgeState.quotesLastFetched,
(state) =>
@@ -425,7 +384,6 @@ export const getBridgeQuotes = createSelector(
getQuoteRequest,
(
sortedQuotesWithMetadata,
- recommendedQuote,
selectedQuote,
quotesLastFetchedMs,
isLoading,
@@ -436,8 +394,8 @@ export const getBridgeQuotes = createSelector(
{ insufficientBal },
) => ({
sortedQuotes: sortedQuotesWithMetadata,
- recommendedQuote,
- activeQuote: selectedQuote ?? recommendedQuote,
+ recommendedQuote: sortedQuotesWithMetadata[0],
+ activeQuote: selectedQuote ?? sortedQuotesWithMetadata[0],
quotesLastFetchedMs,
isLoading,
quoteFetchError,
From 5e4b888cb20538c9c2a48904556149aab2aee715 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 18:02:13 -0800
Subject: [PATCH 20/69] chore: add loading state for bridge tokens
---
.../controllers/bridge/bridge-controller.ts | 30 ++++++-
app/scripts/controllers/bridge/constants.ts | 2 +
app/scripts/controllers/bridge/types.ts | 2 +
ui/ducks/bridge/selectors.ts | 87 +++++++++++--------
4 files changed, 82 insertions(+), 39 deletions(-)
diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts
index 031725530f52..4770c342587c 100644
--- a/app/scripts/controllers/bridge/bridge-controller.ts
+++ b/app/scripts/controllers/bridge/bridge-controller.ts
@@ -224,13 +224,35 @@ export default class BridgeController extends StaticIntervalPollingController {
- await this.#setTopAssets(chainId, 'srcTopAssets');
- await this.#setTokens(chainId, 'srcTokens');
+ this.update((state) => {
+ state.bridgeState.srcTokensLoadingStatus = RequestStatus.LOADING;
+ return state;
+ });
+ try {
+ await this.#setTopAssets(chainId, 'srcTopAssets');
+ await this.#setTokens(chainId, 'srcTokens');
+ } finally {
+ this.update((state) => {
+ state.bridgeState.srcTokensLoadingStatus = RequestStatus.FETCHED;
+ return state;
+ });
+ }
};
selectDestNetwork = async (chainId: Hex) => {
- await this.#setTopAssets(chainId, 'destTopAssets');
- await this.#setTokens(chainId, 'destTokens');
+ this.update((state) => {
+ state.bridgeState.destTokensLoadingStatus = RequestStatus.LOADING;
+ return state;
+ });
+ try {
+ await this.#setTopAssets(chainId, 'destTopAssets');
+ await this.#setTokens(chainId, 'destTokens');
+ } finally {
+ this.update((state) => {
+ state.bridgeState.destTokensLoadingStatus = RequestStatus.FETCHED;
+ return state;
+ });
+ }
};
#fetchBridgeQuotes = async ({
diff --git a/app/scripts/controllers/bridge/constants.ts b/app/scripts/controllers/bridge/constants.ts
index 2d507418b5d9..4903a9ee2858 100644
--- a/app/scripts/controllers/bridge/constants.ts
+++ b/app/scripts/controllers/bridge/constants.ts
@@ -27,6 +27,8 @@ export const DEFAULT_BRIDGE_CONTROLLER_STATE: BridgeControllerState = {
},
},
srcTokens: {},
+ srcTokensLoadingStatus: undefined,
+ destTokensLoadingStatus: undefined,
srcTopAssets: [],
destTokens: {},
destTopAssets: [],
diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts
index 6a28eb9d6ffd..7cdfa43cabd0 100644
--- a/app/scripts/controllers/bridge/types.ts
+++ b/app/scripts/controllers/bridge/types.ts
@@ -37,6 +37,8 @@ export type BridgeControllerState = {
bridgeFeatureFlags: BridgeFeatureFlags;
srcTokens: Record;
srcTopAssets: { address: string }[];
+ srcTokensLoadingStatus?: RequestStatus;
+ destTokensLoadingStatus?: RequestStatus;
destTokens: Record;
destTopAssets: { address: string }[];
quoteRequest: Partial;
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index bd91928795ea..9c133fbf256f 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -6,19 +6,17 @@ import { orderBy, uniqBy } from 'lodash';
import { createSelector } from 'reselect';
import { GasFeeEstimates } from '@metamask/gas-fee-controller';
import { BigNumber } from 'bignumber.js';
+import { calcTokenAmount } from '@metamask/notification-services-controller/push-services';
import {
getIsBridgeEnabled,
getMarketData,
- getSwapsDefaultToken,
getUSDConversionRate,
getUSDConversionRateByChainId,
selectConversionRateByChainId,
- SwapsEthToken,
} from '../../selectors/selectors';
import {
ALLOWED_BRIDGE_CHAIN_IDS,
BRIDGE_PREFERRED_GAS_ESTIMATE,
- BRIDGE_QUOTE_MAX_ETA_SECONDS,
BRIDGE_QUOTE_MAX_RETURN_DIFFERENCE_PERCENTAGE,
} from '../../../shared/constants/bridge';
import {
@@ -28,11 +26,11 @@ import {
// eslint-disable-next-line import/no-restricted-paths
} from '../../../app/scripts/controllers/bridge/types';
import { createDeepEqualSelector } from '../../../shared/modules/selectors/util';
+import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps';
import {
getProviderConfig,
getNetworkConfigurationsByChainId,
} from '../../../shared/modules/selectors/networks';
-import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
@@ -56,7 +54,6 @@ import {
} from '../../pages/bridge/utils/quote';
import { AssetType } from '../../../shared/constants/transaction';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
-import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import {
exchangeRatesFromNativeAndCurrencyRates,
exchangeRateFromMarketData,
@@ -117,18 +114,11 @@ export const getFromChain = createDeepEqualSelector(
);
export const getToChains = createDeepEqualSelector(
- getFromChain,
getAllBridgeableNetworks,
(state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags,
- (
- fromChain,
- allBridgeableNetworks,
- bridgeFeatureFlags,
- ): NetworkConfiguration[] =>
+ (allBridgeableNetworks, bridgeFeatureFlags): NetworkConfiguration[] =>
allBridgeableNetworks.filter(
({ chainId }) =>
- fromChain?.chainId &&
- chainId !== fromChain.chainId &&
bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].chains[
chainId
]?.isActiveDest,
@@ -141,30 +131,57 @@ export const getToChain = createDeepEqualSelector(
(toChains, toChainId): NetworkConfiguration | undefined =>
toChains.find(({ chainId }) => chainId === toChainId),
);
+export const getFromTokens = createDeepEqualSelector(
+ (state: BridgeAppState) => state.metamask.bridgeState.srcTokens,
+ (state: BridgeAppState) => state.metamask.bridgeState.srcTopAssets,
+ (state: BridgeAppState) =>
+ state.metamask.bridgeState.srcTokensLoadingStatus === RequestStatus.LOADING,
+ (fromTokens, fromTopAssets, isLoading) => {
+ return {
+ isLoading,
+ fromTokens: fromTokens ?? {},
+ fromTopAssets: fromTopAssets ?? [],
+ };
+ },
+);
-export const getFromTokens = (state: BridgeAppState) => {
- return state.metamask.bridgeState.srcTokens ?? {};
-};
-
-export const getFromTopAssets = (state: BridgeAppState) => {
- return state.metamask.bridgeState.srcTopAssets ?? [];
-};
-
-export const getToTopAssets = (state: BridgeAppState) => {
- return state.bridge.toChainId ? state.metamask.bridgeState.destTopAssets : [];
-};
-
-export const getToTokens = (state: BridgeAppState) => {
- return state.bridge.toChainId ? state.metamask.bridgeState.destTokens : {};
-};
+export const getToTokens = createDeepEqualSelector(
+ (state: BridgeAppState) => state.metamask.bridgeState.destTokens,
+ (state: BridgeAppState) => state.metamask.bridgeState.destTopAssets,
+ (state: BridgeAppState) =>
+ state.metamask.bridgeState.destTokensLoadingStatus ===
+ RequestStatus.LOADING,
+ (toTokens, toTopAssets, isLoading) => {
+ return {
+ isLoading,
+ toTokens: toTokens ?? {},
+ toTopAssets: toTopAssets ?? [],
+ };
+ },
+);
-export const getFromToken = (
- state: BridgeAppState,
-): SwapsTokenObject | SwapsEthToken | null => {
- return state.bridge.fromToken?.address
- ? state.bridge.fromToken
- : getSwapsDefaultToken(state);
-};
+export const getFromToken = createSelector(
+ (state: BridgeAppState) => state.bridge.fromToken,
+ getFromChain,
+ (fromToken, fromChain): BridgeToken => {
+ if (!fromChain?.chainId) {
+ return null;
+ }
+ return fromToken?.address
+ ? fromToken
+ : {
+ ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ],
+ chainId: fromChain.chainId,
+ image:
+ SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ].iconUrl,
+ type: AssetType.native,
+ };
+ },
+);
export const getToToken = (state: BridgeAppState): BridgeToken => {
return state.bridge.toToken;
From 643398a4cfa0564da31d738a50602114cafc6592 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 5 Dec 2024 18:13:47 -0800
Subject: [PATCH 21/69] test: wip prepare-bridge-page tests
---
.../prepare/prepare-bridge-page.stories.tsx | 248 ++++++++++++++++++
1 file changed, 248 insertions(+)
create mode 100644 ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx
new file mode 100644
index 000000000000..3e955d3e34ef
--- /dev/null
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.stories.tsx
@@ -0,0 +1,248 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from '../../../store/store';
+import { createBridgeMockStore } from '../../../../test/jest/mock-store';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
+import { RequestStatus } from '../../../../app/scripts/controllers/bridge/constants';
+import CrossChainSwap from '../index';
+import { MemoryRouter } from 'react-router-dom';
+import {
+ CROSS_CHAIN_SWAP_ROUTE,
+ PREPARE_SWAP_ROUTE,
+} from '../../../helpers/constants/routes';
+import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json';
+
+const storybook = {
+ title: 'Pages/Bridge/CrossChainSwapPage',
+ component: CrossChainSwap,
+};
+
+const Wrapper = ({ children }) => (
+
+
+ {children}
+
+
+);
+
+const mockFeatureFlags = {
+ srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
+ destNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
+ extensionSupport: true,
+ extensionConfig: {
+ refreshRate: 30000,
+ maxRefreshCount: 5,
+ },
+};
+const mockBridgeSlice = {
+ toChainId: CHAIN_IDS.LINEA_MAINNET,
+ toNativeExchangeRate: 1,
+ toTokenExchangeRate: 0.99,
+ fromTokenInputValue: '1',
+};
+export const DefaultStory = () => {
+ return ;
+};
+DefaultStory.storyName = 'Default';
+DefaultStory.decorators = [
+ (Story) => (
+
+
+
+
+
+ ),
+];
+
+export const LoadingStory = () => {
+ return ;
+};
+LoadingStory.storyName = 'Loading Quotes';
+LoadingStory.decorators = [
+ (Story) => (
+
+
+
+
+
+ ),
+];
+
+export const NoQuotesStory = () => {
+ return ;
+};
+NoQuotesStory.storyName = 'No Quotes';
+NoQuotesStory.decorators = [
+ (Story) => (
+
+
+
+
+
+ ),
+];
+
+export const QuotesFetchedStory = () => {
+ return ;
+};
+QuotesFetchedStory.storyName = 'Quotes Available';
+QuotesFetchedStory.decorators = [
+ (Story) => (
+
+
+
+
+
+ ),
+];
+
+export default storybook;
From 16b348a77baadea71861b512867654a0baa58861 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 6 Dec 2024 15:04:41 -0800
Subject: [PATCH 22/69] chore: display multiple network avatars in
PickerNetwork + AvatarGroup
---
.../multichain/avatar-group/avatar-group.tsx | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/ui/components/multichain/avatar-group/avatar-group.tsx b/ui/components/multichain/avatar-group/avatar-group.tsx
index 39af437980d9..aa7a4ced0f0f 100644
--- a/ui/components/multichain/avatar-group/avatar-group.tsx
+++ b/ui/components/multichain/avatar-group/avatar-group.tsx
@@ -61,7 +61,17 @@ export const AvatarGroup: React.FC = ({
{visibleMembers.map((member, i) => {
return (
From deca243b6ec222f20bc3ce4cc3105f551455db43 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 20:32:05 -0800
Subject: [PATCH 23/69] fix: tmp network icon
---
.../component-library/avatar-network/avatar-network.scss | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ui/components/component-library/avatar-network/avatar-network.scss b/ui/components/component-library/avatar-network/avatar-network.scss
index a71418b3e073..72204363a9d0 100644
--- a/ui/components/component-library/avatar-network/avatar-network.scss
+++ b/ui/components/component-library/avatar-network/avatar-network.scss
@@ -4,7 +4,9 @@
}
&__network-image {
- width: 100%;
+ // TODO undo this when we have new images that are more zoomed in
+ // width: 100%;
+ min-width: 120%;
&--blurred {
filter: blur(8px);
From 19e6bdc522673bee04e450524094eda517562747 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 6 Dec 2024 09:56:43 -0800
Subject: [PATCH 24/69] fix: rm bridge input field style
---
ui/pages/bridge/prepare/bridge-input-group.tsx | 1 +
ui/pages/bridge/prepare/index.scss | 9 ---------
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 85366eb6f9c7..1759bad1f6c3 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -101,6 +101,7 @@ export const BridgeInputGroup = ({
Date: Fri, 6 Dec 2024 15:45:16 -0800
Subject: [PATCH 25/69] fix: bridge slippage modal style + input
---
.../bridge-transaction-settings-modal.tsx | 38 ++++++++++---------
ui/pages/bridge/prepare/index.scss | 3 +-
2 files changed, 22 insertions(+), 19 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
index 558e3763657a..8e03827103de 100644
--- a/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
+++ b/ui/pages/bridge/prepare/bridge-transaction-settings-modal.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Button,
@@ -56,14 +56,6 @@ export const BridgeTransactionSettingsModal = ({
: slippage,
);
const [showCustomButton, setShowCustomButton] = useState(true);
- const inputRef = useRef(null);
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.value = customSlippage?.toString() ?? '';
- inputRef.current.focus();
- }
- }, [customSlippage]);
return (
@@ -87,7 +79,9 @@ export const BridgeTransactionSettingsModal = ({
{
+ onClick={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
setLocalSlippage(hardcodedSlippage);
setCustomSlippage(undefined);
}}
@@ -120,7 +114,7 @@ export const BridgeTransactionSettingsModal = ({
);
})}
- {showCustomButton ? (
+ {showCustomButton && (
{
+ onClick={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
setShowCustomButton(false);
- inputRef?.current?.focus();
}}
>
- ) : (
+ )}
+ {!showCustomButton && (
{
// Remove characters that are not numbers or decimal points if rendering a controlled or pasted value
const cleanedValue = e.target.value.replace(/[^0-9.]+/gu, '');
setLocalSlippage(undefined);
- setCustomSlippage(Number(cleanedValue));
+ setCustomSlippage(
+ cleanedValue.length > 0 ? Number(cleanedValue) : undefined,
+ );
}}
autoFocus={true}
- onBlur={() => setShowCustomButton(true)}
+ onBlur={() => {
+ console.log('====blur');
+ setShowCustomButton(true);
+ }}
+ onFocus={() => {
+ console.log('====focus');
+ setShowCustomButton(false);
+ }}
onKeyPress={(e?: React.KeyboardEvent) => {
// Only allow numbers and at most one decimal point
if (
diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss
index f613a44e3d6d..4afc3d6b3b52 100644
--- a/ui/pages/bridge/prepare/index.scss
+++ b/ui/pages/bridge/prepare/index.scss
@@ -86,14 +86,13 @@
.dark {
box-shadow: 0px 0px 2px 0px #18191B, 0px 0px 16px 0px #18191B;
}
-
}
}
.bridge-settings-modal {
.mm-button-secondary {
&:hover {
- background-color: revert-layer;
+ background-color: var(--color-background-default-hover)
}
}
From ad144999068dc61cadb1438b78570d58b9b21e4f Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 6 Dec 2024 17:01:23 -0800
Subject: [PATCH 26/69] fix: reset dest token if dest chain is selected as src
chain
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 6227d9dcc187..cb7bc3214c5e 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -300,6 +300,7 @@ const PrepareBridgePage = () => {
});
if (networkConfig.chainId === toChain?.chainId) {
dispatch(setToChainId(null));
+ dispatch(setToToken(null));
}
if (isNetworkAdded(networkConfig)) {
dispatch(
From f2faa9aa1c90834820304a1dc5737b6de508a0d4 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 15:52:54 -0800
Subject: [PATCH 27/69] fix: unset selected toToken on dest network change
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index cb7bc3214c5e..ab19115cb9d9 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -419,6 +419,7 @@ const PrepareBridgePage = () => {
});
dispatch(setToChainId(networkConfig.chainId));
dispatch(setToChain(networkConfig.chainId));
+ dispatch(setToToken(null));
},
header: t('bridgeTo'),
shouldDisableNetwork: ({ chainId }) =>
From 981b4049a1b8cfb464dd06744c834c00ec0fa08f Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 16:32:24 -0800
Subject: [PATCH 28/69] chore: shorten bridge network names in quote card
---
ui/pages/bridge/quotes/bridge-quote-card.tsx | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 2d8abc58c5cf..b2d323e6a212 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -37,11 +37,11 @@ import {
TextVariant,
} from '../../../helpers/constants/design-system';
import { Row, Column, Tooltip } from '../layout';
-import { BRIDGE_MM_FEE_RATE } from '../../../../shared/constants/bridge';
import {
- CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP,
- NETWORK_TO_NAME_MAP,
-} from '../../../../shared/constants/network';
+ BRIDGE_MM_FEE_RATE,
+ NETWORK_TO_SHORT_NETWORK_NAME_MAP,
+} from '../../../../shared/constants/bridge';
+import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../shared/constants/network';
import { decimalToPrefixedHex } from '../../../../shared/modules/conversion.utils';
import { TERMS_OF_USE_LINK } from '../../../../shared/constants/terms';
import { BridgeQuotesModal } from './bridge-quotes-modal';
@@ -133,10 +133,10 @@ export const BridgeQuoteCard = () => {
/>
{
- NETWORK_TO_NAME_MAP[
+ NETWORK_TO_SHORT_NETWORK_NAME_MAP[
decimalToPrefixedHex(
activeQuote.quote.srcChainId,
- ) as keyof typeof NETWORK_TO_NAME_MAP
+ ) as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP
].split(' ')[0]
}
@@ -155,10 +155,10 @@ export const BridgeQuoteCard = () => {
/>
{
- NETWORK_TO_NAME_MAP[
+ NETWORK_TO_SHORT_NETWORK_NAME_MAP[
decimalToPrefixedHex(
activeQuote.quote.destChainId,
- ) as keyof typeof NETWORK_TO_NAME_MAP
+ ) as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP
].split(' ')[0]
}
From 7be5f505b2472cb5fb3a46f04f1ac7b6ef6a06b4 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 17:03:21 -0800
Subject: [PATCH 29/69] fix: bridge asset picker button
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index ab19115cb9d9..307b592443d4 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -260,7 +260,9 @@ const PrepareBridgePage = () => {
case fromTokens[tokenAddressFromUrl]?.address?.toLowerCase(): {
// If there is a matching fromToken, set it as the fromToken
const matchedToken = fromTokens[tokenAddressFromUrl];
- dispatch(setFromToken(matchedToken));
+ dispatch(
+ setFromToken({ ...matchedToken, image: matchedToken.iconUrl }),
+ );
removeTokenFromUrl();
break;
}
From 80962a2956d271adb05d1b198b5b1ac2a51320ef Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 17:14:37 -0800
Subject: [PATCH 30/69] chore: hardware wallet messaging
---
app/_locales/en/messages.json | 6 ++++
.../bridge/prepare/prepare-bridge-page.tsx | 33 +++++++++++++------
2 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 9cd23d990e04..1090c72aa707 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -868,6 +868,9 @@
"bridgeApprovalWarning": {
"message": "You are allowing access to the specified amount, $1 $2. The contract will not access any additional funds."
},
+ "bridgeApprovalWarningForHardware": {
+ "message": "You will need to allow access to $1 $2 for bridging, and then approve bridging to USDC. This will require two separate confirmations."
+ },
"bridgeCalculatingAmount": {
"message": "Calculating..."
},
@@ -6861,6 +6864,9 @@
"willApproveAmountForBridging": {
"message": "This will approve $1 for bridging."
},
+ "willApproveAmountForBridgingHardware": {
+ "message": "You’ll need to confirm two transactions on your hardware wallet."
+ },
"withdrawing": {
"message": "Withdrawing"
},
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 307b592443d4..a5c6608803b5 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -83,6 +83,8 @@ import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens';
+import { getCurrentKeyring } from '../../../selectors';
+import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import { BridgeInputGroup } from './bridge-input-group';
import { BridgeCTAButton } from './bridge-cta-button';
@@ -122,6 +124,10 @@ const PrepareBridgePage = () => {
const { refreshRate } = useSelector(getBridgeQuotesConfig);
+ const keyring = useSelector(getCurrentKeyring);
+ // @ts-expect-error keyring type is wrong maybe?
+ const isUsingHardwareWallet = isHardwareKeyring(keyring.type);
+
const ticker = useSelector(getNativeCurrency);
const { isNoQuotesAvailable, isInsufficientGasForQuote } =
useSelector(getValidationErrors);
@@ -497,12 +503,14 @@ const PrepareBridgePage = () => {
variant={TextVariant.bodyXs}
textAlign={TextAlign.Center}
>
- {t('willApproveAmountForBridging', [
- formatTokenAmount(
- new BigNumber(fromAmount),
- fromToken.symbol,
- ),
- ])}
+ {isUsingHardwareWallet
+ ? t('willApproveAmountForBridgingHardware')
+ : t('willApproveAmountForBridging', [
+ formatTokenAmount(
+ new BigNumber(fromAmount),
+ fromToken.symbol,
+ ),
+ ])}
{fromAmount && (
{
offset={[-48, 8]}
title={t('grantExactAccess')}
>
- {t('bridgeApprovalWarning', [
- fromAmount,
- fromToken.symbol,
- ])}
+ {isUsingHardwareWallet
+ ? t('bridgeApprovalWarningForHardware', [
+ fromAmount,
+ fromToken.symbol,
+ ])
+ : t('bridgeApprovalWarning', [
+ fromAmount,
+ fromToken.symbol,
+ ])}
)}
From b1eaa18183e3a205a3c42b6eff11400e03b2a42f Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 18:22:42 -0800
Subject: [PATCH 31/69] feat: shrink input text as char count increases
---
.../bridge/prepare/bridge-input-group.tsx | 21 ++++++++++++++++++-
ui/pages/bridge/prepare/index.scss | 9 --------
2 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 1759bad1f6c3..17a981cd341c 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -101,7 +101,26 @@ export const BridgeInputGroup = ({
Date: Sun, 8 Dec 2024 20:14:19 -0800
Subject: [PATCH 32/69] chore: shorten token amounts
---
ui/pages/bridge/prepare/bridge-input-group.tsx | 8 +++++---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 8 ++++++--
ui/pages/bridge/quotes/bridge-quote-card.tsx | 7 ++++---
ui/pages/bridge/quotes/bridge-quotes-modal.tsx | 6 ++++--
ui/pages/bridge/utils/quote.ts | 9 +++++++--
5 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 17a981cd341c..e573909d3503 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -13,7 +13,7 @@ import {
import { AssetPicker } from '../../../components/multichain/asset-picker-amount/asset-picker';
import { TabName } from '../../../components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-tabs';
import { useI18nContext } from '../../../hooks/useI18nContext';
-import { getCurrentCurrency } from '../../../selectors';
+import { getCurrentCurrency, getLocale } from '../../../selectors';
import { formatCurrencyAmount, formatTokenAmount } from '../utils/quote';
import { Column, Row, Tooltip } from '../layout';
import {
@@ -87,6 +87,8 @@ export const BridgeInputGroup = ({
const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true);
+ const locale = useSelector(getLocale);
+
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = amountFieldProps?.value?.toString() ?? '';
@@ -244,7 +246,7 @@ export const BridgeInputGroup = ({
})
: undefined}
{!isAmountReadOnly && balanceAmount
- ? formatTokenAmount(balanceAmount, token?.symbol)
+ ? formatTokenAmount(locale, balanceAmount, token?.symbol)
: undefined}
{onMaxButtonClick &&
token &&
@@ -252,7 +254,7 @@ export const BridgeInputGroup = ({
balanceAmount && (
onMaxButtonClick(balanceAmount?.toString())}
+ onClick={() => onMaxButtonClick(balanceAmount?.toFixed())}
>
{t('max')}
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index a5c6608803b5..c49d04389c0e 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -83,7 +83,7 @@ import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens';
-import { getCurrentKeyring } from '../../../selectors';
+import { getCurrentKeyring, getLocale } from '../../../selectors';
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import { BridgeInputGroup } from './bridge-input-group';
import { BridgeCTAButton } from './bridge-cta-button';
@@ -127,6 +127,7 @@ const PrepareBridgePage = () => {
const keyring = useSelector(getCurrentKeyring);
// @ts-expect-error keyring type is wrong maybe?
const isUsingHardwareWallet = isHardwareKeyring(keyring.type);
+ const locale = useSelector(getLocale);
const ticker = useSelector(getNativeCurrency);
const { isNoQuotesAvailable, isInsufficientGasForQuote } =
@@ -445,7 +446,9 @@ const PrepareBridgePage = () => {
testId: 'to-amount',
readOnly: true,
disabled: true,
- value: activeQuote?.toTokenAmount?.amount.toFixed() ?? '0',
+ value: activeQuote?.toTokenAmount?.amount
+ ? formatTokenAmount(locale, activeQuote.toTokenAmount.amount)
+ : '0',
autoFocus: false,
className: activeQuote?.toTokenAmount?.amount
? 'amount-input defined'
@@ -507,6 +510,7 @@ const PrepareBridgePage = () => {
? t('willApproveAmountForBridgingHardware')
: t('willApproveAmountForBridging', [
formatTokenAmount(
+ locale,
new BigNumber(fromAmount),
fromToken.symbol,
),
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index b2d323e6a212..ba9bddc0c30c 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -21,7 +21,7 @@ import {
formatTokenAmount,
formatEtaInMinutes,
} from '../utils/quote';
-import { getCurrentCurrency } from '../../../selectors';
+import { getCurrentCurrency, getLocale } from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { useCrossChainSwapsEventTracker } from '../../../hooks/bridge/useCrossChainSwapsEventTracker';
import { useRequestProperties } from '../../../hooks/bridge/events/useRequestProperties';
@@ -59,6 +59,7 @@ export const BridgeQuoteCard = () => {
const fromChain = useSelector(getFromChain);
const toChain = useSelector(getToChain);
+ const locale = useSelector(getLocale);
const [showAllQuotes, setShowAllQuotes] = useState(false);
@@ -180,9 +181,9 @@ export const BridgeQuoteCard = () => {
2,
) ??
formatTokenAmount(
+ locale,
activeQuote.totalNetworkFee?.amount,
ticker,
- 6,
)}
@@ -191,9 +192,9 @@ export const BridgeQuoteCard = () => {
{activeQuote.totalNetworkFee?.valueInCurrency
? formatTokenAmount(
+ locale,
activeQuote.totalNetworkFee?.amount,
ticker,
- 6,
)
: undefined}
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 48c07f06bd9b..78880132bd1a 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -24,7 +24,7 @@ import {
formatTokenAmount,
} from '../utils/quote';
import { useI18nContext } from '../../../hooks/useI18nContext';
-import { getCurrentCurrency } from '../../../selectors';
+import { getCurrentCurrency, getLocale } from '../../../selectors';
import { setSelectedQuote, setSortOrder } from '../../../ducks/bridge/actions';
import { SortOrder } from '../types';
import {
@@ -52,6 +52,7 @@ export const BridgeQuotesModal = ({
const sortOrder = useSelector(getBridgeSortOrder);
const currency = useSelector(getCurrentCurrency);
const nativeCurrency = useSelector(getNativeCurrency);
+ const locale = useSelector(getLocale);
const trackCrossChainSwapsEvent = useCrossChainSwapsEventTracker();
const { quoteRequestProperties } = useRequestProperties();
@@ -211,6 +212,7 @@ export const BridgeQuotesModal = ({
])
: t('quotedTotalCost', [
formatTokenAmount(
+ locale,
totalNetworkFee.amount,
nativeCurrency,
),
@@ -222,9 +224,9 @@ export const BridgeQuotesModal = ({
0,
) ??
formatTokenAmount(
+ locale,
toTokenAmount.amount,
destAsset.symbol,
- 0,
),
]),
].map((content) => (
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 225bac1e13b2..b820471136da 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -10,6 +10,7 @@ import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
import { Numeric } from '../../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../../shared/constants/common';
import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay';
+import { formatAmount } from '../../confirmations/components/simulation-details/formatAmount';
export const isNativeAddress = (address?: string | null) =>
address === zeroAddress() || address === '' || !address;
@@ -173,10 +174,14 @@ export const formatEtaInMinutes = (estimatedProcessingTimeInSeconds: number) =>
(estimatedProcessingTimeInSeconds / 60).toFixed();
export const formatTokenAmount = (
+ locale: string,
amount: BigNumber,
symbol: string = '',
- precision: number = DEFAULT_PRECISION,
-) => [amount.toFixed(precision), symbol].join(' ').trim();
+) => {
+ const stringifiedAmount = formatAmount(locale, amount);
+
+ return [stringifiedAmount, symbol].join(' ').trim();
+};
export const formatCurrencyAmount = (
amount: BigNumber | null,
From 6ad727b955f1f91d25a3cec3b0f903693aecd38b Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 20:14:37 -0800
Subject: [PATCH 33/69] chore: show shorter bridge network names
---
ui/pages/bridge/quotes/bridge-quote-card.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index ba9bddc0c30c..18b83adbce54 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -138,7 +138,7 @@ export const BridgeQuoteCard = () => {
decimalToPrefixedHex(
activeQuote.quote.srcChainId,
) as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP
- ].split(' ')[0]
+ ]
}
@@ -160,7 +160,7 @@ export const BridgeQuoteCard = () => {
decimalToPrefixedHex(
activeQuote.quote.destChainId,
) as keyof typeof NETWORK_TO_SHORT_NETWORK_NAME_MAP
- ].split(' ')[0]
+ ]
}
From ea6fc7c2127eee832ff246e9c021138b125c2807 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Sun, 8 Dec 2024 20:26:12 -0800
Subject: [PATCH 34/69] fix: set timeout between switch button clicks
---
.../bridge/prepare/prepare-bridge-page.tsx | 22 ++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index c49d04389c0e..3c57e9f203a7 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -85,6 +85,7 @@ import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
import { useBridgeTokens } from '../../../hooks/bridge/useBridgeTokens';
import { getCurrentKeyring, getLocale } from '../../../selectors';
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
+import { SECOND } from '../../../../shared/constants/time';
import { BridgeInputGroup } from './bridge-input-group';
import { BridgeCTAButton } from './bridge-cta-button';
@@ -162,6 +163,22 @@ const PrepareBridgePage = () => {
const [rotateSwitchTokens, setRotateSwitchTokens] = useState(false);
+ // Background updates are debounced when the switch button is clicked
+ // To prevent putting the frontend in an unexpected state, prevent the user
+ // from switching tokens within the debounce period
+ const [isSwitchingTemporarilyDisabled, setIsSwitchingTemporarilyDisabled] =
+ useState(false);
+ useEffect(() => {
+ setIsSwitchingTemporarilyDisabled(true);
+ const switchButtonTimer = setTimeout(() => {
+ setIsSwitchingTemporarilyDisabled(false);
+ }, SECOND);
+
+ return () => {
+ clearTimeout(switchButtonTimer);
+ };
+ }, [rotateSwitchTokens]);
+
useEffect(() => {
// Reset controller and inputs on load
dispatch(resetBridgeState());
@@ -382,7 +399,10 @@ const PrepareBridgePage = () => {
ariaLabel="switch-tokens"
iconName={IconName.Arrow2Down}
color={IconColor.iconAlternativeSoft}
- disabled={!isValidQuoteRequest(quoteRequest, false)}
+ disabled={
+ isSwitchingTemporarilyDisabled ||
+ !isValidQuoteRequest(quoteRequest, false)
+ }
onClick={() => {
setRotateSwitchTokens(!rotateSwitchTokens);
flippedRequestProperties &&
From e9f01d1e3dd8cd938ba92c98b9e3aea8038aa072 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 08:12:59 -0800
Subject: [PATCH 35/69] fix: snapshot tests
---
.../__snapshots__/app-header.test.js.snap | 2 +-
.../__snapshots__/asset-page.test.tsx.snap | 8 +-
.../bridge/__snapshots__/index.test.tsx.snap | 26 +-
.../bridge-cta-button.test.tsx.snap | 9 +-
.../bridge-quote-card.test.tsx.snap | 424 ++++++++----------
.../bridge-quotes-modal.test.tsx.snap | 16 +-
6 files changed, 217 insertions(+), 268 deletions(-)
diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
index ea37f1e209ec..7178d027a64c 100644
--- a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
+++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
@@ -602,7 +602,7 @@ exports[`App Header unlocked state matches snapshot: unlocked 1`] = `
-
+
diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
index dc3cf4c408de..11296db72e40 100644
--- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
+++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
@@ -243,7 +243,7 @@ exports[`AssetPage should render a native asset 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
@@ -562,7 +562,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
@@ -788,7 +788,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `
class="mm-box mm-text mm-text--body-md-medium mm-box--margin-right-1 mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-success-default"
>
$15,128.00
-
+
(
+
1512800.00
@@ -1067,7 +1067,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
diff --git a/ui/pages/bridge/__snapshots__/index.test.tsx.snap b/ui/pages/bridge/__snapshots__/index.test.tsx.snap
index b9a6a4c83797..1138c4c5dbba 100644
--- a/ui/pages/bridge/__snapshots__/index.test.tsx.snap
+++ b/ui/pages/bridge/__snapshots__/index.test.tsx.snap
@@ -3,13 +3,13 @@
exports[`Bridge renders the component with initial props 1`] = `
diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
index f225adec3b6d..5e2af1093266 100644
--- a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
+++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
@@ -2,13 +2,10 @@
exports[`BridgeCTAButton should render the component's initial state 1`] = `
-
Select token
-
+
`;
diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
index 6b69b8ec9a6c..19f7f926aa2a 100644
--- a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
+++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
@@ -7,177 +7,156 @@ exports[`BridgeQuoteCard should not render when there is no quote 1`] = `
exports[`BridgeQuoteCard should render the recommended quote 1`] = `
-
- New quotes in 0:30
-
-
-
+
+
+
+ Bridging
+
+
+
+
- Quote rate
+ OP Mainnet
-
-
+
- 1 USDC = 1.00 USDC
+ Polygon
+
+ Network fees
+
-
-
+ ·
+
- $2.52
+ 0.001 ETH
-
-
@@ -186,171 +165,156 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
exports[`BridgeQuoteCard should render the recommended quote while loading new quotes 1`] = `
-
+
+
+
+ Bridging
+
+
+
+
- Quote rate
+ OP Mainnet
-
-
+
- 1 ETH = 2443.89 USDC
+ Polygon
+
+ Network fees
+
-
-
+ ·
+
- $2.52
+ 0.001 ETH
-
-
diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
index 137dc246864e..8840497af170 100644
--- a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
+++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
@@ -66,7 +66,7 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
class="mm-box mm-text mm-text--inherit mm-box--color-primary-default"
>
Net cost
@@ -77,13 +77,13 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
/>
Time
@@ -111,12 +111,12 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
- $3 network fee
+ $3 total cost
- 14 USDC receive amount
+ 13.98 USDC receive amount
- $3 network fee
+ $3 total cost
- 14 USDC receive amount
+ 13.8 USDC receive amount
Date: Mon, 9 Dec 2024 10:42:18 -0800
Subject: [PATCH 36/69] chore: expire bridge quotes after 30s
---
ui/pages/bridge/prepare/bridge-cta-button.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index dab5c5c44132..3479cbd00771 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -138,6 +138,7 @@ export const BridgeCTAButton = () => {
isQuoteExpired,
isInsufficientGasBalance,
isInsufficientGasForQuote,
+ isQuoteExpired,
]);
return activeQuote ? (
From 272698a513d5567b764a3c560e8b54addd7933f3 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 12:43:41 -0800
Subject: [PATCH 37/69] chore: enable unimported networks as destination
---
ui/ducks/bridge/selectors.ts | 12 +++++++++---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 6 +++++-
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 9c133fbf256f..1bd2d02872f6 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -1,4 +1,5 @@
import {
+ AddNetworkFields,
NetworkConfiguration,
NetworkState,
} from '@metamask/network-controller';
@@ -54,6 +55,7 @@ import {
} from '../../pages/bridge/utils/quote';
import { AssetType } from '../../../shared/constants/transaction';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
+import { FEATURED_RPCS } from '../../../shared/constants/network';
import {
exchangeRatesFromNativeAndCurrencyRates,
exchangeRateFromMarketData,
@@ -116,8 +118,11 @@ export const getFromChain = createDeepEqualSelector(
export const getToChains = createDeepEqualSelector(
getAllBridgeableNetworks,
(state: BridgeAppState) => state.metamask.bridgeState?.bridgeFeatureFlags,
- (allBridgeableNetworks, bridgeFeatureFlags): NetworkConfiguration[] =>
- allBridgeableNetworks.filter(
+ (
+ allBridgeableNetworks,
+ bridgeFeatureFlags,
+ ): (AddNetworkFields | NetworkConfiguration)[] =>
+ uniqBy([...allBridgeableNetworks, ...FEATURED_RPCS], 'chainId').filter(
({ chainId }) =>
bridgeFeatureFlags[BridgeFeatureFlagsKey.EXTENSION_CONFIG].chains[
chainId
@@ -128,9 +133,10 @@ export const getToChains = createDeepEqualSelector(
export const getToChain = createDeepEqualSelector(
getToChains,
(state: BridgeAppState) => state.bridge.toChainId,
- (toChains, toChainId): NetworkConfiguration | undefined =>
+ (toChains, toChainId): NetworkConfiguration | AddNetworkFields | undefined =>
toChains.find(({ chainId }) => chainId === toChainId),
);
+
export const getFromTokens = createDeepEqualSelector(
(state: BridgeAppState) => state.metamask.bridgeState.srcTokens,
(state: BridgeAppState) => state.metamask.bridgeState.srcTopAssets,
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 3c57e9f203a7..2e2bbae3662e 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -401,9 +401,13 @@ const PrepareBridgePage = () => {
color={IconColor.iconAlternativeSoft}
disabled={
isSwitchingTemporarilyDisabled ||
- !isValidQuoteRequest(quoteRequest, false)
+ !isValidQuoteRequest(quoteRequest, false) ||
+ !isNetworkAdded(toChain)
}
onClick={() => {
+ if (!isNetworkAdded(toChain)) {
+ return;
+ }
setRotateSwitchTokens(!rotateSwitchTokens);
flippedRequestProperties &&
trackCrossChainSwapsEvent({
From dc0bd74297172a6fe3bf87070297afdd3994ea7e Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 12:52:32 -0800
Subject: [PATCH 38/69] chore: prevent wrapping in quote card
---
ui/pages/bridge/quotes/bridge-quote-card.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 18b83adbce54..5d965aabcfef 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -132,7 +132,7 @@ export const BridgeQuoteCard = () => {
size={AvatarNetworkSize.Xs}
backgroundColor={BackgroundColor.transparent}
/>
-
+
{
NETWORK_TO_SHORT_NETWORK_NAME_MAP[
decimalToPrefixedHex(
@@ -154,7 +154,7 @@ export const BridgeQuoteCard = () => {
size={AvatarNetworkSize.Xs}
backgroundColor={BackgroundColor.transparent}
/>
-
+
{
NETWORK_TO_SHORT_NETWORK_NAME_MAP[
decimalToPrefixedHex(
From f6b856b5103d72153823be524d42dbc3f996cbf0 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 13:04:59 -0800
Subject: [PATCH 39/69] chore: indicate when ETA is <1 min
---
ui/pages/bridge/utils/quote.ts | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index b820471136da..838f95c28189 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -170,8 +170,14 @@ export const calcCost = (
: null,
});
-export const formatEtaInMinutes = (estimatedProcessingTimeInSeconds: number) =>
- (estimatedProcessingTimeInSeconds / 60).toFixed();
+export const formatEtaInMinutes = (
+ estimatedProcessingTimeInSeconds: number,
+) => {
+ if (estimatedProcessingTimeInSeconds < 60) {
+ return `< 1`;
+ }
+ return (estimatedProcessingTimeInSeconds / 60).toFixed();
+};
export const formatTokenAmount = (
locale: string,
From 6efe093953d491cb6f707a7cf3d1433e3bb7dac7 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 13:33:59 -0800
Subject: [PATCH 40/69] fix: hide gas balance alert when user has insufficient
src amount balance
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 2e2bbae3662e..0df20b6c3ccc 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -131,8 +131,11 @@ const PrepareBridgePage = () => {
const locale = useSelector(getLocale);
const ticker = useSelector(getNativeCurrency);
- const { isNoQuotesAvailable, isInsufficientGasForQuote } =
- useSelector(getValidationErrors);
+ const {
+ isNoQuotesAvailable,
+ isInsufficientGasForQuote,
+ isInsufficientBalance,
+ } = useSelector(getValidationErrors);
const { openBuyCryptoInPdapp } = useRamps();
const { balanceAmount: nativeAssetBalance } = useLatestBalance(
@@ -142,6 +145,11 @@ const PrepareBridgePage = () => {
fromChain?.chainId,
);
+ const { balanceAmount: srcTokenBalance } = useLatestBalance(
+ fromToken,
+ fromChain?.chainId,
+ );
+
const tokenAddressAllowlistByChainId = useBridgeTokens();
const fromTokenListGenerator = useTokensWithFiltering(
fromTokens,
@@ -574,6 +582,7 @@ const PrepareBridgePage = () => {
)}
{!isLoading &&
activeQuote &&
+ !isInsufficientBalance(srcTokenBalance) &&
isInsufficientGasForQuote(nativeAssetBalance) && (
Date: Mon, 9 Dec 2024 14:47:37 -0800
Subject: [PATCH 41/69] fix: blank CTA button when refreshing unsubmittable
quote
---
ui/pages/bridge/prepare/bridge-cta-button.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index 3479cbd00771..ea0892abaf8c 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -99,11 +99,11 @@ export const BridgeCTAButton = () => {
}, [isQuoteGoingToRefresh, quotesRefreshCount]);
const label = useMemo(() => {
- if (isQuoteExpired) {
+ if (isQuoteExpired && !isNoQuotesAvailable) {
return t('bridgeQuoteExpired');
}
- if (isLoading && !isTxSubmittable) {
+ if (isLoading && !isTxSubmittable && !activeQuote) {
return '';
}
From 83a1356477996af324b526c93e5ec0409950a9f1 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 18:54:53 -0800
Subject: [PATCH 42/69] fix: update snapshots
---
.../quotes/__snapshots__/bridge-quote-card.test.tsx.snap | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
index 19f7f926aa2a..4501caa1f862 100644
--- a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
+++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
@@ -69,6 +69,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
OP Mainnet
@@ -87,6 +88,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
Polygon
@@ -227,6 +229,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
OP Mainnet
@@ -245,6 +248,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
Polygon
From 7cea999def85b1dffba6462a753dab3da2eab695 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Mon, 9 Dec 2024 21:35:24 -0800
Subject: [PATCH 43/69] fix: unit tests
---
ui/ducks/bridge/bridge.test.ts | 20 +++++++++++++++
ui/ducks/bridge/selectors.test.ts | 31 +++++++++++++++++-------
ui/hooks/bridge/useLatestBalance.test.ts | 14 +++++------
ui/pages/bridge/bridge.util.test.ts | 13 +++++++---
4 files changed, 59 insertions(+), 19 deletions(-)
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index 7b00c95d09a4..f60fd8000484 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -11,6 +11,7 @@ import {
// eslint-disable-next-line import/no-restricted-paths
} from '../../../app/scripts/controllers/bridge/types';
import * as util from '../../helpers/utils/util';
+import { BRIDGE_DEFAULT_SLIPPAGE } from '../../../shared/constants/bridge';
import bridgeReducer from './bridge';
import {
setBridgeFeatureFlags,
@@ -24,6 +25,7 @@ import {
updateQuoteRequestParams,
resetBridgeState,
setDestTokenExchangeRates,
+ setSlippage,
} from './actions';
const middleware = [thunk];
@@ -36,6 +38,21 @@ describe('Ducks - Bridge', () => {
store.clearActions();
});
+ describe('setSlippage', () => {
+ it('calls the "bridge/setSlippage" action', () => {
+ const state = store.getState().bridge;
+ const actionPayload = 0.1;
+
+ store.dispatch(setSlippage(actionPayload as never) as never);
+
+ // Check redux state
+ const actions = store.getActions();
+ expect(actions[0].type).toStrictEqual('bridge/setSlippage');
+ const newState = bridgeReducer(state, actions[0]);
+ expect(newState.slippage).toStrictEqual(actionPayload);
+ });
+ });
+
describe('setToChainId', () => {
it('calls the "bridge/setToChainId" action', () => {
const state = store.getState().bridge;
@@ -149,6 +166,7 @@ describe('Ducks - Bridge', () => {
toChainId: null,
fromToken: null,
toToken: null,
+ slippage: BRIDGE_DEFAULT_SLIPPAGE,
fromTokenInputValue: null,
sortOrder: 'cost_ascending',
toTokenExchangeRate: null,
@@ -213,6 +231,7 @@ describe('Ducks - Bridge', () => {
fromTokenExchangeRate: null,
fromTokenInputValue: null,
selectedQuote: null,
+ slippage: BRIDGE_DEFAULT_SLIPPAGE,
sortOrder: 'cost_ascending',
toChainId: null,
toToken: null,
@@ -220,6 +239,7 @@ describe('Ducks - Bridge', () => {
});
});
});
+
describe('setDestTokenExchangeRates', () => {
it('fetches token prices and updates dest exchange rates in state, native dest token', async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index 344fab115311..314abef3b843 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -204,7 +204,7 @@ describe('Bridge selectors', () => {
});
describe('getToChains', () => {
- it('excludes selected providerConfig and disabled chains from options', () => {
+ it('includes selected providerConfig and disabled chains from options', () => {
const state = createBridgeMockStore({
featureFlagOverrides: {
extensionConfig: {
@@ -216,6 +216,7 @@ describe('Bridge selectors', () => {
},
[CHAIN_IDS.OPTIMISM]: { isActiveSrc: false, isActiveDest: true },
[CHAIN_IDS.POLYGON]: { isActiveSrc: false, isActiveDest: true },
+ [CHAIN_IDS.BSC]: { isActiveSrc: false, isActiveDest: true },
},
},
},
@@ -225,14 +226,20 @@ describe('Bridge selectors', () => {
});
const result = getToChains(state as never);
- expect(result).toHaveLength(3);
+ expect(result).toHaveLength(5);
expect(result[0]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.ARBITRUM }),
+ expect.objectContaining({ chainId: CHAIN_IDS.LINEA_MAINNET }),
);
expect(result[1]).toStrictEqual(
- expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
+ expect.objectContaining({ chainId: CHAIN_IDS.ARBITRUM }),
);
expect(result[2]).toStrictEqual(
+ expect.objectContaining({ chainId: CHAIN_IDS.BSC }),
+ );
+ expect(result[3]).toStrictEqual(
+ expect.objectContaining({ chainId: CHAIN_IDS.OPTIMISM }),
+ );
+ expect(result[4]).toStrictEqual(
expect.objectContaining({ chainId: CHAIN_IDS.POLYGON }),
);
});
@@ -383,12 +390,13 @@ describe('Bridge selectors', () => {
expect(result).toStrictEqual({
address: '0x0000000000000000000000000000000000000000',
- balance: '0',
+ chainId: '0x1',
decimals: 18,
iconUrl: './images/eth_logo.svg',
+ image: './images/eth_logo.svg',
name: 'Ether',
- string: '0',
symbol: 'ETH',
+ type: 'NATIVE',
});
});
@@ -400,12 +408,13 @@ describe('Bridge selectors', () => {
expect(result).toStrictEqual({
address: '0x0000000000000000000000000000000000000000',
- balance: '0',
+ chainId: '0x1',
decimals: 18,
iconUrl: './images/eth_logo.svg',
+ image: './images/eth_logo.svg',
name: 'Ether',
- string: '0',
symbol: 'ETH',
+ type: 'NATIVE',
});
});
});
@@ -463,7 +472,11 @@ describe('Bridge selectors', () => {
const result = getToTokens(state as never);
expect(result).toStrictEqual({
- '0x00': { address: '0x00', symbol: 'TEST' },
+ isLoading: false,
+ toTokens: {
+ '0x00': { address: '0x00', symbol: 'TEST' },
+ },
+ toTopAssets: [],
});
});
diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts
index 6d79672e4550..25f0d0936791 100644
--- a/ui/hooks/bridge/useLatestBalance.test.ts
+++ b/ui/hooks/bridge/useLatestBalance.test.ts
@@ -1,4 +1,4 @@
-import { BigNumber } from 'ethers';
+import { BigNumber } from 'bignumber.js';
import { renderHookWithProvider } from '../../../test/lib/render-helpers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { createBridgeMockStore } from '../../../test/jest/mock-store';
@@ -47,8 +47,8 @@ describe('useLatestBalance', () => {
global.ethereumProvider = provider as any;
});
- it('returns formattedBalance for native asset in current chain', async () => {
- mockGetBalance.mockResolvedValue(BigNumber.from('1000000000000000000'));
+ it('returns balanceAmount for native asset in current chain', async () => {
+ mockGetBalance.mockResolvedValue(new BigNumber('1000000000000000000'));
const { result, waitForNextUpdate } = renderUseLatestBalance(
{ address: zeroAddress(), decimals: 18 },
@@ -57,7 +57,7 @@ describe('useLatestBalance', () => {
);
await waitForNextUpdate();
- expect(result.current.formattedBalance).toStrictEqual('1');
+ expect(result.current.balanceAmount).toStrictEqual(new BigNumber('1'));
expect(mockGetBalance).toHaveBeenCalledTimes(1);
expect(mockGetBalance).toHaveBeenCalledWith(
@@ -66,8 +66,8 @@ describe('useLatestBalance', () => {
expect(mockFetchTokenBalance).toHaveBeenCalledTimes(0);
});
- it('returns formattedBalance for ERC20 asset in current chain', async () => {
- mockFetchTokenBalance.mockResolvedValueOnce(BigNumber.from('15390000'));
+ it('returns balanceAmount for ERC20 asset in current chain', async () => {
+ mockFetchTokenBalance.mockResolvedValueOnce(new BigNumber('15390000'));
const { result, waitForNextUpdate } = renderUseLatestBalance(
{ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', decimals: '6' },
@@ -76,7 +76,7 @@ describe('useLatestBalance', () => {
);
await waitForNextUpdate();
- expect(result.current.formattedBalance).toStrictEqual('15.39');
+ expect(result.current.balanceAmount).toStrictEqual(new BigNumber('15.39'));
expect(mockFetchTokenBalance).toHaveBeenCalledTimes(1);
expect(mockFetchTokenBalance).toHaveBeenCalledWith(
diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts
index a22cc39876b4..cb2eed49574e 100644
--- a/ui/pages/bridge/bridge.util.test.ts
+++ b/ui/pages/bridge/bridge.util.test.ts
@@ -167,6 +167,12 @@ describe('Bridge utils', () => {
},
{
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986',
+ decimals: 16,
+ symbol: 'DEF',
+ aggregators: ['lifi'],
+ },
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f987',
symbol: 'DEF',
},
{
@@ -198,10 +204,11 @@ describe('Bridge utils', () => {
name: 'Ether',
symbol: 'ETH',
},
- '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': {
- address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986': {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f986',
decimals: 16,
- symbol: 'ABC',
+ symbol: 'DEF',
+ aggregators: ['lifi'],
},
});
});
From 09a334896712606366231981aa6f8509d8023c1c Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 16:38:55 -0800
Subject: [PATCH 44/69] chore: fetch exchange rates for selected token with
unimported network
---
ui/ducks/bridge/actions.ts | 2 ++
ui/ducks/bridge/bridge.ts | 13 +++++++++++++
ui/ducks/bridge/selectors.ts | 22 +++++++++++++++++++++-
ui/hooks/bridge/useBridgeExchangeRates.ts | 18 +++++++++++++++++-
4 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index 1c199bd65d38..0bcd3701853a 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -14,6 +14,7 @@ import { MetaMaskReduxDispatch } from '../../store/store';
import {
bridgeSlice,
setDestTokenExchangeRates,
+ setDestTokenUsdExchangeRates,
setSrcTokenExchangeRates,
} from './bridge';
@@ -35,6 +36,7 @@ export {
setFromToken,
setFromTokenInputValue,
setDestTokenExchangeRates,
+ setDestTokenUsdExchangeRates,
setSrcTokenExchangeRates,
setSortOrder,
setSelectedQuote,
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 31b2f27bf508..9835f484630a 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -17,6 +17,7 @@ export type BridgeState = {
fromTokenInputValue: string | null;
fromTokenExchangeRate: number | null; // Exchange rate from selected token to the default currency (can be fiat or crypto)
toTokenExchangeRate: number | null; // Exchange rate from the selected token to the default currency (can be fiat or crypto)
+ toTokenUsdExchangeRate: number | null; // Exchange rate from the selected token to the USD. This is needed for metrics
sortOrder: SortOrder;
selectedQuote: (QuoteResponse & QuoteMetadata) | null; // Alternate quote selected by user. When quotes refresh, the best match will be activated.
slippage: number;
@@ -29,6 +30,7 @@ const initialState: BridgeState = {
fromTokenInputValue: null,
fromTokenExchangeRate: null,
toTokenExchangeRate: null,
+ toTokenUsdExchangeRate: null,
sortOrder: SortOrder.COST_ASC,
selectedQuote: null,
slippage: BRIDGE_DEFAULT_SLIPPAGE,
@@ -44,6 +46,11 @@ export const setDestTokenExchangeRates = createAsyncThunk(
getTokenExchangeRate,
);
+export const setDestTokenUsdExchangeRates = createAsyncThunk(
+ 'bridge/setDestTokenUsdExchangeRates',
+ getTokenExchangeRate,
+);
+
const bridgeSlice = createSlice({
name: 'bridge',
initialState: { ...initialState },
@@ -78,12 +85,18 @@ const bridgeSlice = createSlice({
builder.addCase(setDestTokenExchangeRates.pending, (state) => {
state.toTokenExchangeRate = null;
});
+ builder.addCase(setDestTokenUsdExchangeRates.pending, (state) => {
+ state.toTokenUsdExchangeRate = null;
+ });
builder.addCase(setSrcTokenExchangeRates.pending, (state) => {
state.fromTokenExchangeRate = null;
});
builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
state.toTokenExchangeRate = action.payload ?? null;
});
+ builder.addCase(setDestTokenUsdExchangeRates.fulfilled, (state, action) => {
+ state.toTokenUsdExchangeRate = action.payload ?? null;
+ });
builder.addCase(setSrcTokenExchangeRates.fulfilled, (state, action) => {
state.fromTokenExchangeRate = action.payload ?? null;
});
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 1bd2d02872f6..c7b3a7ca694a 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -268,11 +268,31 @@ export const getToTokenConversionRate = createDeepEqualSelector(
getToChain,
getMarketData,
getToToken,
+ getNetworkConfigurationsByChainId,
(state) => ({
state,
toTokenExchangeRate: state.bridge.toTokenExchangeRate,
+ toTokenUsdExchangeRate: state.bridge.toTokenUsdExchangeRate,
}),
- (toChain, marketData, toToken, { state, toTokenExchangeRate }) => {
+ (
+ toChain,
+ marketData,
+ toToken,
+ allNetworksByChainId,
+ { state, toTokenExchangeRate, toTokenUsdExchangeRate },
+ ) => {
+ // When the toChain is not imported, the exchange rate to native asset is not available
+ // The rate in the bridge state is used instead
+ if (
+ toChain?.chainId &&
+ !allNetworksByChainId[toChain.chainId] &&
+ toTokenExchangeRate
+ ) {
+ return {
+ valueInCurrency: toTokenExchangeRate,
+ usd: toTokenUsdExchangeRate,
+ };
+ }
if (toChain?.chainId && toToken && marketData) {
const { chainId } = toChain;
diff --git a/ui/hooks/bridge/useBridgeExchangeRates.ts b/ui/hooks/bridge/useBridgeExchangeRates.ts
index ef3c6669a2c8..20f70b17dfd6 100644
--- a/ui/hooks/bridge/useBridgeExchangeRates.ts
+++ b/ui/hooks/bridge/useBridgeExchangeRates.ts
@@ -5,11 +5,16 @@ import {
getQuoteRequest,
getToChain,
} from '../../ducks/bridge/selectors';
-import { getCurrentCurrency, getMarketData } from '../../selectors';
+import {
+ getCurrentCurrency,
+ getMarketData,
+ getParticipateInMetaMetrics,
+} from '../../selectors';
import { decimalToPrefixedHex } from '../../../shared/modules/conversion.utils';
import { getCurrentChainId } from '../../../shared/modules/selectors/networks';
import {
setDestTokenExchangeRates,
+ setDestTokenUsdExchangeRates,
setSrcTokenExchangeRates,
} from '../../ducks/bridge/bridge';
import { exchangeRateFromMarketData } from '../../ducks/bridge/utils';
@@ -19,6 +24,7 @@ export const useBridgeExchangeRates = () => {
const { activeQuote } = useSelector(getBridgeQuotes);
const chainId = useSelector(getCurrentChainId);
const toChain = useSelector(getToChain);
+ const isMetaMetricsEnabled = useSelector(getParticipateInMetaMetrics);
const dispatch = useDispatch();
@@ -78,6 +84,16 @@ export const useBridgeExchangeRates = () => {
currency,
}),
);
+ // If the selected currency is not USD, fetch the USD exchange rate for metrics
+ if (isMetaMetricsEnabled && currency !== 'usd') {
+ dispatch(
+ setDestTokenUsdExchangeRates({
+ chainId: toChainId,
+ tokenAddress: toTokenAddress,
+ currency: 'usd',
+ }),
+ );
+ }
}
}
}, [toChainId, toTokenAddress]);
From b8f0bc6d3f852e61ea1f2927fa95a81f38c9bfcc Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 16:39:27 -0800
Subject: [PATCH 45/69] fix: hide 0 token balance
---
ui/hooks/useTokensWithFiltering.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts
index 87f723ac24d3..f4b5f10c9753 100644
--- a/ui/hooks/useTokensWithFiltering.ts
+++ b/ui/hooks/useTokensWithFiltering.ts
@@ -118,7 +118,7 @@ export const useTokensWithFiltering = (
image: token.iconUrl,
// Only tokens with 0 balance are processed here so hardcode empty string
balance: '',
- string: '',
+ string: undefined,
address: token.address || zeroAddress(),
};
}
From c31472f7f119c3c6e68ee69e32f2f42fe4a38ab1 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 16:47:52 -0800
Subject: [PATCH 46/69] fix: only track network change if new value is
different
---
.../bridge/prepare/prepare-bridge-page.tsx | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 0df20b6c3ccc..8fcc416177c1 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -328,10 +328,11 @@ const PrepareBridgePage = () => {
network: fromChain,
networks: fromChains,
onNetworkChange: (networkConfig) => {
- trackInputEvent({
- input: 'chain_source',
- value: networkConfig.chainId,
- });
+ networkConfig.chainId !== fromChain?.chainId &&
+ trackInputEvent({
+ input: 'chain_source',
+ value: networkConfig.chainId,
+ });
if (networkConfig.chainId === toChain?.chainId) {
dispatch(setToChainId(null));
dispatch(setToToken(null));
@@ -454,10 +455,11 @@ const PrepareBridgePage = () => {
network: toChain,
networks: toChains,
onNetworkChange: (networkConfig) => {
- trackInputEvent({
- input: 'chain_destination',
- value: networkConfig.chainId,
- });
+ networkConfig.chainId !== toChain?.chainId &&
+ trackInputEvent({
+ input: 'chain_destination',
+ value: networkConfig.chainId,
+ });
dispatch(setToChainId(networkConfig.chainId));
dispatch(setToChain(networkConfig.chainId));
dispatch(setToToken(null));
From 45f9de62289ae25c450e70481d3aad67fc40f1d9 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 17:05:01 -0800
Subject: [PATCH 47/69] fix: include value in Input Changed events
---
ui/hooks/bridge/useCrossChainSwapsEventTracker.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts
index f28244cba995..ad4b3698fe84 100644
--- a/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts
+++ b/ui/hooks/bridge/useCrossChainSwapsEventTracker.ts
@@ -104,6 +104,7 @@ export const useCrossChainSwapsEventTracker = () => {
action_type: ActionType.CROSSCHAIN_V1,
...properties,
},
+ value: 'value' in properties ? (properties.value as never) : undefined,
});
},
[trackEvent],
From 298226dc1dd06c258e6b6033e7aff810a44f2286 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 17:05:26 -0800
Subject: [PATCH 48/69] chore: misc style updates
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 2 +-
ui/pages/bridge/quotes/bridge-quotes-modal.tsx | 4 ++--
ui/pages/bridge/utils/quote.ts | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 8fcc416177c1..8051c06afc88 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -471,7 +471,7 @@ const PrepareBridgePage = () => {
customTokenListGenerator={
toChain && toTokens && toTopAssets
? toTokenListGenerator
- : fromTokenListGenerator
+ : undefined
}
amountInFiat={
activeQuote?.toTokenAmount?.valueInCurrency || undefined
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 78880132bd1a..15e1cf593b92 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -181,7 +181,7 @@ export const BridgeQuotesModal = ({
paddingInline={4}
paddingTop={3}
paddingBottom={3}
- style={{ position: 'relative', height: 78 }}
+ style={{ position: 'relative' }}
>
{isQuoteActive && (
Date: Tue, 10 Dec 2024 17:59:40 -0800
Subject: [PATCH 49/69] fix: snapshots
---
.../multichain/app-header/__snapshots__/app-header.test.js.snap | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
index 7178d027a64c..ea37f1e209ec 100644
--- a/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
+++ b/ui/components/multichain/app-header/__snapshots__/app-header.test.js.snap
@@ -602,7 +602,7 @@ exports[`App Header unlocked state matches snapshot: unlocked 1`] = `
-
+
From 1f25deed05b6035a4b214fc6ddddfa0bfe46fed3 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 18:21:13 -0800
Subject: [PATCH 50/69] fix: selectors unit tests
---
ui/ducks/bridge/selectors.test.ts | 192 ++++--------------------------
1 file changed, 22 insertions(+), 170 deletions(-)
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index 314abef3b843..cf65fe38fde6 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -6,10 +6,7 @@ import {
CHAIN_IDS,
FEATURED_RPCS,
} from '../../../shared/constants/network';
-import {
- ALLOWED_BRIDGE_CHAIN_IDS,
- BRIDGE_QUOTE_MAX_ETA_SECONDS,
-} from '../../../shared/constants/bridge';
+import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge';
import { mockNetworkState } from '../../../test/stub/networks';
import mockErc20Erc20Quotes from '../../../test/data/bridge/mock-quotes-erc20-erc20.json';
import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json';
@@ -22,13 +19,11 @@ import {
getFromChains,
getFromToken,
getFromTokens,
- getFromTopAssets,
getIsBridgeTx,
getToChain,
getToChains,
getToToken,
getToTokens,
- getToTopAssets,
getValidationErrors,
} from './selectors';
@@ -480,19 +475,6 @@ describe('Bridge selectors', () => {
});
});
- it('returns empty dest tokens from controller state when toChainId is undefined', () => {
- const state = createBridgeMockStore({
- bridgeStateOverrides: {
- destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
- },
- });
- const result = getToTokens(state as never);
-
- expect(result).toStrictEqual({});
- });
- });
-
- describe('getToTopAssets', () => {
it('returns dest top assets from controller state when toChainId is defined', () => {
const state = createBridgeMockStore({
bridgeSliceOverrides: { toChainId: '0x1' },
@@ -501,21 +483,11 @@ describe('Bridge selectors', () => {
destTopAssets: [{ address: '0x00', symbol: 'TEST' }],
},
});
- const result = getToTopAssets(state as never);
-
- expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]);
- });
-
- it('returns empty dest top assets from controller state when toChainId is undefined', () => {
- const state = createBridgeMockStore({
- bridgeStateOverrides: {
- destTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
- destTopAssets: [{ address: '0x00', symbol: 'TEST' }],
- },
- });
- const result = getToTopAssets(state as never);
+ const result = getToTokens(state as never);
- expect(result).toStrictEqual([]);
+ expect(result.toTopAssets).toStrictEqual([
+ { address: '0x00', symbol: 'TEST' },
+ ]);
});
});
@@ -525,17 +497,20 @@ describe('Bridge selectors', () => {
bridgeSliceOverrides: { toChainId: '0x1' },
bridgeStateOverrides: {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
+ srcTopAssets: [{ address: '0x01', symbol: 'SYMB' }],
},
});
const result = getFromTokens(state as never);
expect(result).toStrictEqual({
- '0x00': { address: '0x00', symbol: 'TEST' },
+ fromTokens: {
+ '0x00': { address: '0x00', symbol: 'TEST' },
+ },
+ fromTopAssets: [{ address: '0x01', symbol: 'SYMB' }],
+ isLoading: false,
});
});
- });
- describe('getFromTopAssets', () => {
it('returns src top assets from controller state', () => {
const state = createBridgeMockStore({
bridgeSliceOverrides: { toChainId: '0x1' },
@@ -544,9 +519,11 @@ describe('Bridge selectors', () => {
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
},
});
- const result = getFromTopAssets(state as never);
+ const result = getFromTokens(state as never);
- expect(result).toStrictEqual([{ address: '0x00', symbol: 'TEST' }]);
+ expect(result.fromTopAssets).toStrictEqual([
+ { address: '0x00', symbol: 'TEST' },
+ ]);
});
});
@@ -936,136 +913,6 @@ describe('Bridge selectors', () => {
mockBridgeQuotesNativeErc20[0]?.quote.requestId,
);
});
-
- it('should recommend 2nd cheapest quote if ETA exceeds 1 hour', () => {
- const state = createBridgeMockStore({
- bridgeSliceOverrides: { sortOrder: SortOrder.COST_ASC },
- bridgeStateOverrides: {
- quotes: [
- mockBridgeQuotesNativeErc20[1],
- {
- ...mockBridgeQuotesNativeErc20[0],
- estimatedProcessingTimeInSeconds:
- BRIDGE_QUOTE_MAX_ETA_SECONDS + 1,
- quote: {
- ...mockBridgeQuotesNativeErc20[0].quote,
- requestId: 'cheapestQuoteWithLongETA',
- },
- },
- ],
- },
- });
-
- const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
- state as never,
- );
-
- expect(activeQuote?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(recommendedQuote?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(sortedQuotes).toHaveLength(2);
- expect(sortedQuotes[0]?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(sortedQuotes[1]?.quote.requestId).toStrictEqual(
- 'cheapestQuoteWithLongETA',
- );
- });
-
- it('should recommend 2nd fastest quote if adjustedReturn is less than 80% of cheapest quote', () => {
- const state = createBridgeMockStore({
- featureFlagOverrides: {
- extensionConfig: {
- chains: {
- '0xa': { isActiveSrc: true, isActiveDest: false },
- '0x89': { isActiveSrc: false, isActiveDest: true },
- },
- },
- },
- bridgeSliceOverrides: {
- toChainId: '0x89',
- fromToken: { address: zeroAddress(), symbol: 'ETH' },
- toToken: { address: zeroAddress(), symbol: 'TEST' },
- fromTokenExchangeRate: 2524.25,
- sortOrder: SortOrder.ETA_ASC,
- toTokenExchangeRate: 0.998781,
- },
- bridgeStateOverrides: {
- quotes: [
- ...mockBridgeQuotesNativeErc20,
- {
- ...mockBridgeQuotesNativeErc20[0],
- estimatedProcessingTimeInSeconds: 1,
- quote: {
- ...mockBridgeQuotesNativeErc20[0].quote,
- requestId: 'fastestQuote',
- destTokenAmount: '1',
- },
- },
- ],
- },
- metamaskStateOverrides: {
- currencyRates: {
- ETH: {
- conversionRate: 2524.25,
- },
- POL: {
- conversionRate: 0.354073,
- usdConversionRate: 1,
- },
- },
- marketData: {},
- ...mockNetworkState(
- { chainId: CHAIN_IDS.MAINNET },
- { chainId: CHAIN_IDS.LINEA_MAINNET },
- { chainId: CHAIN_IDS.POLYGON },
- { chainId: CHAIN_IDS.OPTIMISM },
- ),
- },
- });
-
- const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
- state as never,
- );
- const {
- sentAmount,
- totalNetworkFee,
- toTokenAmount,
- adjustedReturn,
- cost,
- } = activeQuote ?? {};
-
- expect(activeQuote?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(recommendedQuote?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(sentAmount?.valueInCurrency?.toString()).toStrictEqual('25.2425');
- expect(totalNetworkFee?.valueInCurrency?.toString()).toStrictEqual(
- '2.52459306428938562',
- );
- expect(toTokenAmount?.valueInCurrency?.toString()).toStrictEqual(
- '24.226654664163',
- );
- expect(adjustedReturn?.valueInCurrency?.toString()).toStrictEqual(
- '21.70206159987361438',
- );
- expect(cost?.valueInCurrency?.toString()).toStrictEqual(
- '3.54043840012638562',
- );
- expect(sortedQuotes).toHaveLength(3);
- expect(sortedQuotes[0]?.quote.requestId).toStrictEqual('fastestQuote');
- expect(sortedQuotes[1]?.quote.requestId).toStrictEqual(
- '4277a368-40d7-4e82-aa67-74f29dc5f98a',
- );
- expect(sortedQuotes[2]?.quote.requestId).toStrictEqual(
- '381c23bc-e3e4-48fe-bc53-257471e388ad',
- );
- });
});
describe('getValidationErrors', () => {
@@ -1102,12 +949,14 @@ describe('Bridge selectors', () => {
const state = createBridgeMockStore({
bridgeSliceOverrides: {
toChainId: '0x1',
- fromTokenInputValue: '0.001',
+ fromToken: { decimals: 6, address: zeroAddress() },
+ fromChain: { chainId: CHAIN_IDS.MAINNET },
},
bridgeStateOverrides: {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
quotesLastFetched: Date.now(),
+ quoteRequest: { srcTokenAmount: '1000' },
},
});
const result = getValidationErrors(state as never);
@@ -1151,12 +1000,14 @@ describe('Bridge selectors', () => {
const state = createBridgeMockStore({
bridgeSliceOverrides: {
toChainId: '0x1',
- fromTokenInputValue: '0.001',
+ fromToken: { decimals: 6, address: zeroAddress() },
+ fromChain: { chainId: CHAIN_IDS.MAINNET },
},
bridgeStateOverrides: {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
quotesLastFetched: Date.now(),
+ quoteRequest: { srcTokenAmount: '1000' },
},
});
const result = getValidationErrors(state as never);
@@ -1338,6 +1189,7 @@ describe('Bridge selectors', () => {
toChainId: '0x89',
fromToken: { address: zeroAddress(), symbol: 'ETH' },
toToken: { address: zeroAddress(), symbol: 'TEST' },
+ fromTokenInputValue: '1',
fromTokenExchangeRate: 2524.25,
toTokenExchangeRate: 0.798781,
},
From f137a50cf6e8eba4b186ef68e9cf4128fc218484 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 18:41:38 -0800
Subject: [PATCH 51/69] fix: type issues
---
ui/ducks/bridge/selectors.ts | 34 +++++++++++++++----------
ui/hooks/bridge/useBridgeTokens.ts | 10 +++++---
ui/hooks/useTokensWithFiltering.test.ts | 9 ++++---
3 files changed, 33 insertions(+), 20 deletions(-)
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index c7b3a7ca694a..c948639a6756 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -55,7 +55,10 @@ import {
} from '../../pages/bridge/utils/quote';
import { AssetType } from '../../../shared/constants/transaction';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
-import { FEATURED_RPCS } from '../../../shared/constants/network';
+import {
+ CHAIN_ID_TOKEN_IMAGE_MAP,
+ FEATURED_RPCS,
+} from '../../../shared/constants/network';
import {
exchangeRatesFromNativeAndCurrencyRates,
exchangeRateFromMarketData,
@@ -173,19 +176,22 @@ export const getFromToken = createSelector(
if (!fromChain?.chainId) {
return null;
}
- return fromToken?.address
- ? fromToken
- : {
- ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
- fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
- ],
- chainId: fromChain.chainId,
- image:
- SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
- fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
- ].iconUrl,
- type: AssetType.native,
- };
+ if (fromToken?.address) {
+ return fromToken;
+ }
+ return {
+ ...SWAPS_CHAINID_DEFAULT_TOKEN_MAP[
+ fromChain.chainId as keyof typeof SWAPS_CHAINID_DEFAULT_TOKEN_MAP
+ ],
+ chainId: fromChain.chainId,
+ image:
+ CHAIN_ID_TOKEN_IMAGE_MAP[
+ fromChain.chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
+ ],
+ balance: '0',
+ string: '0',
+ type: AssetType.native,
+ };
},
);
diff --git a/ui/hooks/bridge/useBridgeTokens.ts b/ui/hooks/bridge/useBridgeTokens.ts
index c4575dcf5648..e704981b570b 100644
--- a/ui/hooks/bridge/useBridgeTokens.ts
+++ b/ui/hooks/bridge/useBridgeTokens.ts
@@ -22,9 +22,13 @@ export const useBridgeTokens = () => {
(async () => {
const results = await tokenAllowlistPromises;
- const tokenAllowlistResults = results.reduce(
- (acc, { value }) => ({ ...acc, ...value }),
- {},
+ const tokenAllowlistResults = Object.fromEntries(
+ results.map((result) => {
+ if (result.status === 'fulfilled') {
+ return Object.entries(result.value)[0];
+ }
+ return [];
+ }),
);
setTokenAllowlistByChainId(tokenAllowlistResults);
})();
diff --git a/ui/hooks/useTokensWithFiltering.test.ts b/ui/hooks/useTokensWithFiltering.test.ts
index 0a523b69bd74..a44c2d1579f2 100644
--- a/ui/hooks/useTokensWithFiltering.test.ts
+++ b/ui/hooks/useTokensWithFiltering.test.ts
@@ -4,7 +4,6 @@ import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens';
import {
SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
SwapsTokenObject,
- TokenBucketPriority,
} from '../../shared/constants/swaps';
import { useTokensWithFiltering } from './useTokensWithFiltering';
@@ -42,7 +41,9 @@ describe('useTokensWithFiltering should return token list generator', () => {
useTokensWithFiltering(
MOCK_TOKEN_LIST_BY_ADDRESS,
MOCK_TOP_ASSETS,
- TokenBucketPriority.top,
+ {
+ [TEST_CHAIN_ID]: new Set(Object.keys(MOCK_TOKEN_LIST_BY_ADDRESS)),
+ },
TEST_CHAIN_ID,
),
mockStore,
@@ -106,7 +107,9 @@ describe('useTokensWithFiltering should return token list generator', () => {
useTokensWithFiltering(
MOCK_TOKEN_LIST_BY_ADDRESS,
MOCK_TOP_ASSETS,
- TokenBucketPriority.owned,
+ {
+ [TEST_CHAIN_ID]: new Set(Object.keys(MOCK_TOKEN_LIST_BY_ADDRESS)),
+ },
TEST_CHAIN_ID,
),
mockStore,
From 1c94604e3c0c906868fbcf3625b45e5661980573 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 20:21:54 -0800
Subject: [PATCH 52/69] fix: double borders
---
.../multichain/avatar-group/avatar-group.tsx | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/ui/components/multichain/avatar-group/avatar-group.tsx b/ui/components/multichain/avatar-group/avatar-group.tsx
index aa7a4ced0f0f..39af437980d9 100644
--- a/ui/components/multichain/avatar-group/avatar-group.tsx
+++ b/ui/components/multichain/avatar-group/avatar-group.tsx
@@ -61,17 +61,7 @@ export const AvatarGroup: React.FC = ({
{visibleMembers.map((member, i) => {
return (
From 856bf01c64b4c816794d7d4c150a0f84b3665f62 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 20:22:15 -0800
Subject: [PATCH 53/69] fix: scss lint issues
---
ui/pages/bridge/index.scss | 6 +++---
ui/pages/bridge/prepare/index.scss | 10 ++++++----
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/ui/pages/bridge/index.scss b/ui/pages/bridge/index.scss
index 473cbbe3d7ad..011b386e34a2 100644
--- a/ui/pages/bridge/index.scss
+++ b/ui/pages/bridge/index.scss
@@ -13,9 +13,9 @@
[data-theme='light'],
.light {
- --color-background-alternative-soft: #F9FAFB;
- --color-text-alternative-soft: #6A737D;
- --color-icon-alternative-soft: #6A737D;
+ --color-background-alternative-soft: #f9fafb;
+ --color-text-alternative-soft: #6a737d;
+ --color-icon-alternative-soft: #6a737d;
}
[data-theme='dark'],
diff --git a/ui/pages/bridge/prepare/index.scss b/ui/pages/bridge/prepare/index.scss
index 0412399c3317..cfefc86e52e0 100644
--- a/ui/pages/bridge/prepare/index.scss
+++ b/ui/pages/bridge/prepare/index.scss
@@ -2,6 +2,7 @@
.prepare-bridge-page {
flex: 1;
+
.mm-text-field {
background-color: inherit;
@@ -11,7 +12,8 @@
}
.defined {
- & > .mm-input--disabled, p {
+ & > .mm-input--disabled,
+ p {
opacity: 1;
}
}
@@ -70,12 +72,12 @@
[data-theme='light'],
.light {
- box-shadow: 0px 0px 2px 0px #E2E4E9, 0px 0px 16px 0px rgba(226, 228, 233, 0.16);
+ box-shadow: 0 0 2px 0 #e2e4e9, 0 0 16px 0 rgba(226, 228, 233, 0.16);
}
[data-theme='dark'],
.dark {
- box-shadow: 0px 0px 2px 0px #18191B, 0px 0px 16px 0px #18191B;
+ box-shadow: 0 0 2px 0 #18191b, 0 0 16px 0 #18191b;
}
}
}
@@ -83,7 +85,7 @@
.bridge-settings-modal {
.mm-button-secondary {
&:hover {
- background-color: var(--color-background-default-hover)
+ background-color: var(--color-background-default-hover);
}
}
From 0918c0284e36a47d0b4c8990bfe28fe2b8525777 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 20:33:37 -0800
Subject: [PATCH 54/69] fix: snapshots
---
.../components/__snapshots__/asset-page.test.tsx.snap | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
index 11296db72e40..dc3cf4c408de 100644
--- a/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
+++ b/ui/pages/asset/components/__snapshots__/asset-page.test.tsx.snap
@@ -243,7 +243,7 @@ exports[`AssetPage should render a native asset 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
@@ -562,7 +562,7 @@ exports[`AssetPage should render an ERC20 asset without prices 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
@@ -788,7 +788,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `
class="mm-box mm-text mm-text--body-md-medium mm-box--margin-right-1 mm-box--margin-left-1 mm-box--display-inline-block mm-box--color-success-default"
>
$15,128.00
-
+
(
+
1512800.00
@@ -1067,7 +1067,7 @@ exports[`AssetPage should render an ERC20 token with prices 1`] = `
data-testid="multichain-token-list-item-value"
>
0
-
+
TEST
From c207a3d5bfd11275d9a927694f3f123956d58477 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 10 Dec 2024 22:07:16 -0800
Subject: [PATCH 55/69] fix: unit tests
---
.../bridge/bridge-controller.test.ts | 21 +++++++++++++------
ui/ducks/bridge/bridge.test.ts | 2 ++
ui/ducks/bridge/selectors.test.ts | 4 ++++
ui/hooks/bridge/useBridgeTokens.ts | 1 +
ui/hooks/bridge/useCountdownTimer.test.ts | 7 +++----
5 files changed, 25 insertions(+), 10 deletions(-)
diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts
index 9ffb95832350..3b0d095fa0c3 100644
--- a/app/scripts/controllers/bridge/bridge-controller.test.ts
+++ b/app/scripts/controllers/bridge/bridge-controller.test.ts
@@ -18,7 +18,7 @@ import { QuoteResponse } from '../../../../ui/pages/bridge/types';
import { decimalToHex } from '../../../../shared/modules/conversion.utils';
import BridgeController from './bridge-controller';
import { BridgeControllerMessenger } from './types';
-import { DEFAULT_BRIDGE_CONTROLLER_STATE } from './constants';
+import { DEFAULT_BRIDGE_CONTROLLER_STATE, RequestStatus } from './constants';
const EMPTY_INIT_STATE = {
bridgeState: DEFAULT_BRIDGE_CONTROLLER_STATE,
@@ -106,6 +106,7 @@ describe('BridgeController', function () {
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
symbol: 'ABC',
decimals: 16,
+ aggregators: ['lifl', 'socket'],
},
{
address: '0x1291478912',
@@ -171,6 +172,12 @@ describe('BridgeController', function () {
it('selectDestNetwork should set the bridge dest tokens and top assets', async function () {
await bridgeController.selectDestNetwork('0xa');
expect(bridgeController.state.bridgeState.destTokens).toStrictEqual({
+ '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ symbol: 'ABC',
+ decimals: 16,
+ aggregators: ['lifl', 'socket'],
+ },
'0x0000000000000000000000000000000000000000': {
address: '0x0000000000000000000000000000000000000000',
decimals: 18,
@@ -178,12 +185,10 @@ describe('BridgeController', function () {
name: 'Ether',
symbol: 'ETH',
},
- '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': {
- address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
- symbol: 'ABC',
- decimals: 16,
- },
});
+ expect(
+ bridgeController.state.bridgeState.destTokensLoadingStatus,
+ ).toStrictEqual(RequestStatus.FETCHED);
expect(bridgeController.state.bridgeState.destTopAssets).toStrictEqual([
{ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', symbol: 'ABC' },
]);
@@ -208,8 +213,12 @@ describe('BridgeController', function () {
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
symbol: 'ABC',
decimals: 16,
+ aggregators: ['lifl', 'socket'],
},
});
+ expect(
+ bridgeController.state.bridgeState.srcTokensLoadingStatus,
+ ).toStrictEqual(RequestStatus.FETCHED);
expect(bridgeController.state.bridgeState.srcTopAssets).toStrictEqual([
{
address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index f60fd8000484..1e1151df6734 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -171,6 +171,7 @@ describe('Ducks - Bridge', () => {
sortOrder: 'cost_ascending',
toTokenExchangeRate: null,
fromTokenExchangeRate: null,
+ toTokenUsdExchangeRate: null,
});
});
});
@@ -236,6 +237,7 @@ describe('Ducks - Bridge', () => {
toChainId: null,
toToken: null,
toTokenExchangeRate: null,
+ toTokenUsdExchangeRate: null,
});
});
});
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index cf65fe38fde6..50ca56eff7ce 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -392,6 +392,8 @@ describe('Bridge selectors', () => {
name: 'Ether',
symbol: 'ETH',
type: 'NATIVE',
+ balance: '0',
+ string: '0',
});
});
@@ -410,6 +412,8 @@ describe('Bridge selectors', () => {
name: 'Ether',
symbol: 'ETH',
type: 'NATIVE',
+ balance: '0',
+ string: '0',
});
});
});
diff --git a/ui/hooks/bridge/useBridgeTokens.ts b/ui/hooks/bridge/useBridgeTokens.ts
index e704981b570b..acddb7ec2fb0 100644
--- a/ui/hooks/bridge/useBridgeTokens.ts
+++ b/ui/hooks/bridge/useBridgeTokens.ts
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { getAllBridgeableNetworks } from '../../ducks/bridge/selectors';
import { fetchBridgeTokens } from '../../pages/bridge/bridge.util';
+// This hook is used to fetch the bridge tokens for all bridgeable networks
export const useBridgeTokens = () => {
const allBridgeChains = useSelector(getAllBridgeableNetworks);
diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts
index 0adc18f68c15..55360c729a44 100644
--- a/ui/hooks/bridge/useCountdownTimer.test.ts
+++ b/ui/hooks/bridge/useCountdownTimer.test.ts
@@ -1,6 +1,7 @@
import { renderHookWithProvider } from '../../../test/lib/render-helpers';
import { createBridgeMockStore } from '../../../test/jest/mock-store';
import { flushPromises } from '../../../test/lib/timer-helpers';
+import { SECOND } from '../../../shared/constants/time';
import { useCountdownTimer } from './useCountdownTimer';
jest.useFakeTimers();
@@ -30,13 +31,11 @@ describe('useCountdownTimer', () => {
let i = 0;
while (i <= 40) {
const secondsLeft = Math.min(41, 40 - i + 2);
- expect(result.current).toStrictEqual(
- `0:${secondsLeft < 10 ? '0' : ''}${secondsLeft}`,
- );
+ expect(result.current).toStrictEqual(secondsLeft * SECOND);
i += 10;
jest.advanceTimersByTime(10000);
await flushPromises();
}
- expect(result.current).toStrictEqual('0:00');
+ expect(result.current).toStrictEqual(0);
});
});
From d674217a3114824f5acefadfcf3686b6d8434932 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 08:27:18 -0800
Subject: [PATCH 56/69] fix: useLatestBalance type
---
ui/hooks/bridge/useLatestBalance.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/ui/hooks/bridge/useLatestBalance.ts b/ui/hooks/bridge/useLatestBalance.ts
index 21c7d0a561f2..98ee8dfd4f4c 100644
--- a/ui/hooks/bridge/useLatestBalance.ts
+++ b/ui/hooks/bridge/useLatestBalance.ts
@@ -3,11 +3,9 @@ import { Hex } from '@metamask/utils';
import { Numeric } from '../../../shared/modules/Numeric';
import { getCurrentChainId } from '../../../shared/modules/selectors/networks';
import { getSelectedInternalAccount } from '../../selectors';
-import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { calcLatestSrcBalance } from '../../../shared/modules/bridge-utils/balance';
import { useAsyncResult } from '../useAsyncResult';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
-import { BridgeToken } from '../../pages/bridge/types';
/**
* Custom hook to fetch and format the latest balance of a given token or native asset.
@@ -17,7 +15,11 @@ import { BridgeToken } from '../../pages/bridge/types';
* @returns An object containing the balanceAmount as a string.
*/
const useLatestBalance = (
- token: SwapsTokenObject | BridgeToken,
+ token: {
+ address: string;
+ decimals: number;
+ symbol: string;
+ } | null,
chainId?: Hex,
) => {
const { address: selectedAddress } = useSelector(getSelectedInternalAccount);
From eafa31a6c839c760cfbec8485f23eb8a18c60586 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 09:16:23 -0800
Subject: [PATCH 57/69] chore: click to copy dest address
---
.../bridge/prepare/bridge-input-group.tsx | 27 ++++++++++---------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index e573909d3503..5ed2ba7d52b4 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { BigNumber } from 'bignumber.js';
+import { getAddress } from 'ethers/lib/utils';
import {
Text,
TextField,
@@ -33,6 +34,8 @@ import {
} from '../../../ducks/bridge/selectors';
import { shortenString } from '../../../helpers/utils/util';
import { BridgeToken } from '../types';
+import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
+import { MINUTE } from '../../../../shared/constants/time';
import { BridgeAssetPickerButton } from './components/bridge-asset-picker-button';
export const BridgeInputGroup = ({
@@ -71,24 +74,20 @@ export const BridgeInputGroup = ({
const { isInsufficientBalance, isEstimatedReturnLow } =
useSelector(getValidationErrors);
const currency = useSelector(getCurrentCurrency);
+ const locale = useSelector(getLocale);
const selectedChainId = networkProps?.network?.chainId;
-
- const blockExplorerUrl =
- networkProps?.network?.defaultBlockExplorerUrlIndex === undefined
- ? undefined
- : networkProps.network.blockExplorerUrls?.[
- networkProps.network.defaultBlockExplorerUrlIndex
- ];
-
const { balanceAmount } = useLatestBalance(token, selectedChainId);
+ const [, handleCopy] = useCopyToClipboard(MINUTE) as [
+ boolean,
+ (text: string) => void,
+ ];
+
const inputRef = useRef(null);
const [isLowReturnTooltipOpen, setIsLowReturnTooltipOpen] = useState(true);
- const locale = useSelector(getLocale);
-
useEffect(() => {
if (inputRef.current) {
inputRef.current.value = amountFieldProps?.value?.toString() ?? '';
@@ -222,7 +221,6 @@ export const BridgeInputGroup = ({
{amountInFiat && formatCurrencyAmount(amountInFiat, currency, 2)}
-
{
+ if (isAmountReadOnly && token && selectedChainId) {
+ handleCopy(getAddress(token.address));
+ }
+ }}
+ as={isAmountReadOnly ? 'a' : 'p'}
>
{isAmountReadOnly &&
token &&
selectedChainId &&
- blockExplorerUrl &&
token.type === AssetType.token
? shortenString(token.address, {
truncatedCharLimit: 11,
From aa4fb7d9b3de3f2bcce745d4f2323aff42b76588 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 10:17:51 -0800
Subject: [PATCH 58/69] chore: refactor token list and add tests
---
test/data/bridge/mock-token-data.ts | 102 ++++++++
test/jest/mock-store.js | 100 +-------
.../useTokensWithFiltering.test.ts.snap | 192 +++++++++++++++
.../bridge/useTokensWithFiltering.test.ts | 108 +++++++++
ui/hooks/bridge/useTokensWithFiltering.ts | 218 ++++++++++++++++++
ui/hooks/useTokensWithFiltering.test.ts | 160 -------------
ui/hooks/useTokensWithFiltering.ts | 200 ----------------
.../bridge/prepare/prepare-bridge-page.tsx | 2 +-
8 files changed, 623 insertions(+), 459 deletions(-)
create mode 100644 test/data/bridge/mock-token-data.ts
create mode 100644 ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap
create mode 100644 ui/hooks/bridge/useTokensWithFiltering.test.ts
create mode 100644 ui/hooks/bridge/useTokensWithFiltering.ts
delete mode 100644 ui/hooks/useTokensWithFiltering.test.ts
delete mode 100644 ui/hooks/useTokensWithFiltering.ts
diff --git a/test/data/bridge/mock-token-data.ts b/test/data/bridge/mock-token-data.ts
new file mode 100644
index 000000000000..0eafa4802ea5
--- /dev/null
+++ b/test/data/bridge/mock-token-data.ts
@@ -0,0 +1,102 @@
+import { CHAIN_IDS } from '@metamask/transaction-controller';
+
+export const mockTokenData = {
+ allTokens: {
+ [CHAIN_IDS.MAINNET]: {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ balance: 'a',
+ decimals: 6,
+ },
+ {
+ address: '0x514910771af9ca656af840dff83e8264ecf986ca',
+ balance: 'e',
+ },
+ ],
+ '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': [
+ {
+ address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ balance: 'e',
+ },
+ ],
+ },
+ [CHAIN_IDS.LINEA_MAINNET]: {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ balance: 'e',
+ },
+ ],
+ '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': [
+ {
+ address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ balance: 'e',
+ },
+ ],
+ },
+ },
+ accountsByChainId: {
+ [CHAIN_IDS.MAINNET]: {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
+ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ balance: '0xa',
+ },
+ '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': {
+ address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b',
+ balance: '0xe',
+ },
+ },
+ [CHAIN_IDS.LINEA_MAINNET]: {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
+ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ balance: '0xe',
+ },
+ '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': {
+ address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b',
+ balance: '0xe',
+ },
+ },
+ },
+ tokensChainsCache: {
+ [CHAIN_IDS.MAINNET]: {
+ timestamp: 111111,
+ data: [
+ {
+ address: '0x514910771af9ca656af840dff83e8264ecf986ca',
+ symbol: 'LINK',
+ decimals: 18,
+ },
+ {
+ address: '0xc00e94cb662c3520282e6f5717214004a7f26888',
+ symbol: 'COMP',
+ decimals: 18,
+ },
+ ],
+ },
+ [CHAIN_IDS.LINEA_MAINNET]: {
+ timestamp: 111111,
+ data: {
+ '0x514910771af9ca656af840dff83e8264ecf986ca': {
+ address: '0x514910771af9ca656af840dff83e8264ecf986ca',
+ symbol: 'LINK',
+ decimals: 18,
+ },
+ '0xc00e94cb662c3520282e6f5717214004a7f26888': {
+ address: '0xc00e94cb662c3520282e6f5717214004a7f26888',
+ symbol: 'COMP',
+ decimals: 18,
+ },
+ },
+ },
+ },
+ tokenBalances: {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
+ '0x5': {},
+ '0x1': {
+ '0x514910771af9ca656af840dff83e8264ecf986ca': '0x1',
+ '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': '0x738',
+ },
+ },
+ },
+};
diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js
index 84d18e1a3803..309249ff2d6b 100644
--- a/test/jest/mock-store.js
+++ b/test/jest/mock-store.js
@@ -6,6 +6,7 @@ import { mockNetworkState } from '../stub/networks';
import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge/constants';
import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge-status/constants';
import { BRIDGE_PREFERRED_GAS_ESTIMATE } from '../../shared/constants/bridge';
+import { mockTokenData } from '../data/bridge/mock-token-data';
export const createGetSmartTransactionFeesApiResponse = () => {
return {
@@ -750,104 +751,7 @@ export const createBridgeMockStore = (
},
},
},
- allTokens: {
- [CHAIN_IDS.MAINNET]: {
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
- {
- address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
- balance: 'a',
- decimals: 6,
- },
- {
- address: '0x514910771af9ca656af840dff83e8264ecf986ca',
- balance: 'e',
- },
- ],
- '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': [
- {
- address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
- balance: 'e',
- },
- ],
- },
- [CHAIN_IDS.LINEA_MAINNET]: {
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
- {
- address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
- balance: 'e',
- },
- ],
- '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': [
- {
- address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
- balance: 'e',
- },
- ],
- },
- },
- accountsByChainId: {
- [CHAIN_IDS.MAINNET]: {
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
- address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
- balance: '0xa',
- },
- '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': {
- address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b',
- balance: '0xe',
- },
- },
- [CHAIN_IDS.LINEA_MAINNET]: {
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
- address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
- balance: '0xe',
- },
- '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b': {
- address: '0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b',
- balance: '0xe',
- },
- },
- },
- tokensChainsCache: {
- [CHAIN_IDS.MAINNET]: {
- timestamp: 111111,
- data: [
- {
- address: '0x514910771af9ca656af840dff83e8264ecf986ca',
- symbol: 'LINK',
- decimals: 18,
- },
- {
- address: '0xc00e94cb662c3520282e6f5717214004a7f26888',
- symbol: 'COMP',
- decimals: 18,
- },
- ],
- },
- [CHAIN_IDS.LINEA_MAINNET]: {
- timestamp: 111111,
- data: {
- '0x514910771af9ca656af840dff83e8264ecf986ca': {
- address: '0x514910771af9ca656af840dff83e8264ecf986ca',
- symbol: 'LINK',
- decimals: 18,
- },
- '0xc00e94cb662c3520282e6f5717214004a7f26888': {
- address: '0xc00e94cb662c3520282e6f5717214004a7f26888',
- symbol: 'COMP',
- decimals: 18,
- },
- },
- },
- },
- tokenBalances: {
- '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
- '0x5': {},
- '0x1': {
- '0x514910771af9ca656af840dff83e8264ecf986ca': '0x1',
- '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': '0x738',
- },
- },
- },
+ ...mockTokenData,
...metamaskStateOverrides,
bridgeState: {
...(swapsStore.metamask.bridgeState ?? {}),
diff --git a/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap b/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap
new file mode 100644
index 000000000000..8fceeffe9730
--- /dev/null
+++ b/ui/hooks/bridge/__snapshots__/useTokensWithFiltering.test.ts.snap
@@ -0,0 +1,192 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`useTokensWithFiltering should not return tokens that are not in the allowlist 1`] = `
+[
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0.000000000000000014",
+ "chainId": "0xe708",
+ "decimals": 18,
+ "image": "./images/eth_logo.svg",
+ "string": "0.000000000000000014",
+ "symbol": "ETH",
+ "tokenFiatAmount": 3.53395e-14,
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0.00000000000000001",
+ "chainId": "0x1",
+ "decimals": 18,
+ "image": "./images/eth_logo.svg",
+ "string": "0.00000000000000001",
+ "symbol": "ETH",
+ "tokenFiatAmount": 2.5242500000000003e-14,
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
+ "aggregators": [],
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 18,
+ "erc20": true,
+ "erc721": false,
+ "iconUrl": "images/contract/sushi.svg",
+ "image": "images/contract/sushi.svg",
+ "name": "SushiSwap",
+ "string": undefined,
+ "symbol": "SUSHI",
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0x0",
+ "chainId": "0x1",
+ "decimals": 18,
+ "iconUrl": "./images/eth_logo.svg",
+ "image": "./images/eth_logo.svg",
+ "name": "Ether",
+ "string": "0x0",
+ "symbol": "ETH",
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ "aggregators": [],
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 18,
+ "erc20": true,
+ "iconUrl": "images/contract/uni.svg",
+ "image": "images/contract/uni.svg",
+ "name": "Uniswap",
+ "string": undefined,
+ "symbol": "UNI",
+ "type": "TOKEN",
+ },
+]
+`;
+
+exports[`useTokensWithFiltering should return all tokens when chainId === activeChainId, sorted by balance 1`] = `
+[
+ {
+ "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ "balance": "0.00184",
+ "chainId": "0x1",
+ "decimals": 6,
+ "image": undefined,
+ "isNative": false,
+ "string": "0.00184",
+ "tokenFiatAmount": 0.004232,
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0.000000000000000014",
+ "chainId": "0xe708",
+ "decimals": 18,
+ "image": "./images/eth_logo.svg",
+ "string": "0.000000000000000014",
+ "symbol": "ETH",
+ "tokenFiatAmount": 3.53395e-14,
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0.00000000000000001",
+ "chainId": "0x1",
+ "decimals": 18,
+ "image": "./images/eth_logo.svg",
+ "string": "0.00000000000000001",
+ "symbol": "ETH",
+ "tokenFiatAmount": 2.5242500000000003e-14,
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x514910771af9ca656af840dff83e8264ecf986ca",
+ "balance": "1",
+ "chainId": "0x1",
+ "image": undefined,
+ "isNative": false,
+ "string": "1",
+ "tokenFiatAmount": null,
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 6,
+ "string": undefined,
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x6b3595068778dd592e39a122f4f5a5cf09c90fe2",
+ "aggregators": [],
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 18,
+ "erc20": true,
+ "erc721": false,
+ "iconUrl": "images/contract/sushi.svg",
+ "image": "images/contract/sushi.svg",
+ "name": "SushiSwap",
+ "string": undefined,
+ "symbol": "SUSHI",
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0x0",
+ "chainId": "0x1",
+ "decimals": 18,
+ "iconUrl": "./images/eth_logo.svg",
+ "image": "./images/eth_logo.svg",
+ "name": "Ether",
+ "string": "0x0",
+ "symbol": "ETH",
+ "type": "NATIVE",
+ },
+ {
+ "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984",
+ "aggregators": [],
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 18,
+ "erc20": true,
+ "iconUrl": "images/contract/uni.svg",
+ "image": "images/contract/uni.svg",
+ "name": "Uniswap",
+ "string": undefined,
+ "symbol": "UNI",
+ "type": "TOKEN",
+ },
+ {
+ "address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
+ "aggregators": [],
+ "balance": "",
+ "chainId": "0x1",
+ "decimals": 6,
+ "erc20": true,
+ "iconUrl": "images/contract/usdt.svg",
+ "image": "images/contract/usdt.svg",
+ "name": "Tether USD",
+ "string": undefined,
+ "symbol": "USDT",
+ "type": "TOKEN",
+ },
+ {
+ "address": "0x0000000000000000000000000000000000000000",
+ "balance": "0x0",
+ "chainId": "0x1",
+ "decimals": 18,
+ "iconUrl": "./images/eth_logo.svg",
+ "image": "./images/eth_logo.svg",
+ "name": "Ether",
+ "string": "0x0",
+ "symbol": "ETH",
+ "type": "NATIVE",
+ },
+]
+`;
diff --git a/ui/hooks/bridge/useTokensWithFiltering.test.ts b/ui/hooks/bridge/useTokensWithFiltering.test.ts
new file mode 100644
index 000000000000..e6903756bcfd
--- /dev/null
+++ b/ui/hooks/bridge/useTokensWithFiltering.test.ts
@@ -0,0 +1,108 @@
+import { renderHookWithProvider } from '../../../test/lib/render-helpers';
+import { createBridgeMockStore } from '../../../test/jest/mock-store';
+import { STATIC_MAINNET_TOKEN_LIST } from '../../../shared/constants/tokens';
+import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../shared/constants/swaps';
+import { CHAIN_IDS } from '../../../shared/constants/network';
+import { useTokensWithFiltering } from './useTokensWithFiltering';
+
+const mockUseTokenTracker = jest
+ .fn()
+ .mockReturnValue({ tokensWithBalances: [] });
+jest.mock('../useTokenTracker', () => ({
+ useTokenTracker: () => mockUseTokenTracker(),
+}));
+
+const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[CHAIN_IDS.MAINNET];
+
+describe('useTokensWithFiltering', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return all tokens when chainId === activeChainId, sorted by balance', () => {
+ const mockStore = createBridgeMockStore({
+ metamaskStateOverrides: {
+ completedOnboarding: true,
+ allDetectedTokens: {
+ '0x1': {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ decimals: 6,
+ }, // USDC
+ ],
+ },
+ },
+ },
+ });
+ const { result } = renderHookWithProvider(
+ () =>
+ useTokensWithFiltering(
+ {
+ [NATIVE_TOKEN.address]: NATIVE_TOKEN,
+ ...STATIC_MAINNET_TOKEN_LIST,
+ },
+ [
+ { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI
+ { address: NATIVE_TOKEN.address },
+ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC
+ { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT
+ ],
+ {
+ [CHAIN_IDS.MAINNET]: new Set(
+ Object.keys(STATIC_MAINNET_TOKEN_LIST),
+ ),
+ },
+ CHAIN_IDS.MAINNET,
+ ),
+ mockStore,
+ );
+ // The first 10 tokens returned
+ const first10Tokens = [...result.current(() => true)].slice(0, 10);
+ expect(first10Tokens).toMatchSnapshot();
+ });
+
+ it('should not return tokens that are not in the allowlist', () => {
+ const mockStore = createBridgeMockStore({
+ metamaskStateOverrides: {
+ completedOnboarding: true,
+ allDetectedTokens: {
+ '0x1': {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ decimals: 6,
+ }, // USDC
+ ],
+ },
+ },
+ },
+ });
+ const { result } = renderHookWithProvider(
+ () =>
+ useTokensWithFiltering(
+ {
+ [NATIVE_TOKEN.address]: NATIVE_TOKEN,
+ ...STATIC_MAINNET_TOKEN_LIST,
+ },
+ [
+ { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI
+ { address: NATIVE_TOKEN.address },
+ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC
+ { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT
+ ],
+ // Only 1 token in allowlist
+ {
+ [CHAIN_IDS.MAINNET]: new Set([
+ '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ ]),
+ },
+ CHAIN_IDS.MAINNET,
+ ),
+ mockStore,
+ );
+ // The first 5 tokens returned
+ const first5Tokens = [...result.current(() => true)].slice(0, 5);
+ expect(first5Tokens).toMatchSnapshot();
+ });
+});
diff --git a/ui/hooks/bridge/useTokensWithFiltering.ts b/ui/hooks/bridge/useTokensWithFiltering.ts
new file mode 100644
index 000000000000..56b16ddc4b68
--- /dev/null
+++ b/ui/hooks/bridge/useTokensWithFiltering.ts
@@ -0,0 +1,218 @@
+import { useCallback } from 'react';
+import { useSelector } from 'react-redux';
+import { isEqual } from 'lodash';
+import { ChainId } from '@metamask/controller-utils';
+import { Hex } from '@metamask/utils';
+import { useParams } from 'react-router-dom';
+import { zeroAddress } from 'ethereumjs-util';
+import {
+ getAllDetectedTokensForSelectedAddress,
+ getCurrentCurrency,
+ getSelectedInternalAccountWithBalance,
+ getTokenExchangeRates,
+} from '../../selectors';
+import { getConversionRate } from '../../ducks/metamask/metamask';
+import { SwapsTokenObject } from '../../../shared/constants/swaps';
+import {
+ AssetWithDisplayData,
+ ERC20Asset,
+ NativeAsset,
+} from '../../components/multichain/asset-picker-amount/asset-picker-modal/types';
+import { AssetType } from '../../../shared/constants/transaction';
+import { isNativeAddress } from '../../pages/bridge/utils/quote';
+import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../../shared/constants/network';
+import { getCurrentChainId } from '../../../shared/modules/selectors/networks';
+import { Token } from '../../components/app/assets/token-list/token-list';
+import { useMultichainBalances } from '../useMultichainBalances';
+
+type FilterPredicate = (
+ symbol: string,
+ address?: string,
+ tokenChainId?: string,
+) => boolean;
+
+/**
+ * Returns a token list generator that filters and sorts tokens in this order
+ * - matches URL token parameter
+ * - matches search query
+ * - highest balance in selected currency
+ * - detected tokens (with balance)
+ * - popularity
+ * - all other tokens
+ *
+ * @param tokenList - a mapping of token addresses in the selected chainId to token metadata from the bridge-api
+ * @param topTokens - a list of top tokens from the swap-api
+ * @param tokenAddressAllowlistByChainId - a mapping of all supported chainIds to a Set of allowed token addresses
+ * @param chainId - the selected src/dest chainId
+ */
+export const useTokensWithFiltering = (
+ tokenList: Record,
+ topTokens: { address: string }[],
+ tokenAddressAllowlistByChainId: Record>,
+ chainId?: ChainId | Hex,
+) => {
+ const { token: tokenAddressFromUrl } = useParams();
+ const allDetectedTokens: Record = useSelector(
+ getAllDetectedTokensForSelectedAddress,
+ );
+
+ const { balance } = useSelector(getSelectedInternalAccountWithBalance);
+
+ const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
+ const conversionRate = useSelector(getConversionRate);
+ const currentCurrency = useSelector(getCurrentCurrency);
+ const currentChainId = useSelector(getCurrentChainId);
+
+ const { assetsWithBalance: multichainTokensWithBalance } =
+ useMultichainBalances();
+
+ // This transforms the token object from the bridge-api into the format expected by the AssetPicker
+ const buildTokenData = (
+ token?: SwapsTokenObject,
+ ): AssetWithDisplayData | undefined => {
+ if (!chainId || !token) {
+ return undefined;
+ }
+ // Only tokens on the active chain are processed here here
+ const sharedFields = { ...token, chainId };
+
+ if (isNativeAddress(token.address)) {
+ return {
+ ...sharedFields,
+ type: AssetType.native,
+ address: zeroAddress(),
+ image:
+ CHAIN_ID_TOKEN_IMAGE_MAP[
+ chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
+ ],
+ balance: currentChainId === chainId ? balance : '',
+ string: currentChainId === chainId ? balance : '',
+ };
+ }
+
+ return {
+ ...sharedFields,
+ type: AssetType.token,
+ image: token.iconUrl,
+ // Only tokens with 0 balance are processed here so hardcode empty string
+ balance: '',
+ string: undefined,
+ address: token.address || zeroAddress(),
+ };
+ };
+
+ // This returns whether the token is blocked by any of the supported chainIds
+ const isTokenBlocked = (tokenAddress: string, tokenChainId: string) =>
+ !tokenAddressAllowlistByChainId[tokenChainId]?.has(
+ tokenAddress.toLowerCase(),
+ );
+
+ // shouldAddToken is a filter condition passed in from the AssetPicker that determines whether a token should be included
+ const filteredTokenListGenerator = useCallback(
+ (shouldAddToken: FilterPredicate) =>
+ (function* (): Generator<
+ AssetWithDisplayData | AssetWithDisplayData
+ > {
+ // If a token address is in the URL (e.g. from a deep link), yield that token first
+ if (tokenAddressFromUrl) {
+ const token =
+ tokenList?.[tokenAddressFromUrl] ??
+ tokenList?.[tokenAddressFromUrl.toLowerCase()];
+ if (
+ shouldAddToken(token.symbol, token.address ?? undefined, chainId)
+ ) {
+ const tokenWithData = buildTokenData(token);
+ if (tokenWithData) {
+ yield tokenWithData;
+ }
+ }
+ }
+
+ // Yield multichain tokens with balances and are not blocked
+ for (const token of multichainTokensWithBalance) {
+ if (
+ shouldAddToken(
+ token.symbol,
+ token.address ?? undefined,
+ token.chainId,
+ ) &&
+ (token.address
+ ? !isTokenBlocked(token.address, token.chainId)
+ : true)
+ ) {
+ // If there's no address, set it to the native address in swaps/bridge
+ yield { ...token, address: token.address || zeroAddress() };
+ }
+ }
+
+ // Yield all detected tokens for all supported chains
+ for (const token of Object.values(allDetectedTokens).flat()) {
+ if (
+ shouldAddToken(
+ token.symbol,
+ token.address ?? undefined,
+ token.chainId,
+ ) &&
+ (token.address
+ ? !isTokenBlocked(token.address, token.chainId)
+ : true)
+ ) {
+ yield {
+ ...token,
+ type: AssetType.token,
+ // Balance is not 0 but is not in the data so hardcode 0
+ // If a detected token is selected useLatestBalance grabs the on-chain balance
+ balance: '',
+ string: undefined,
+ };
+ }
+ }
+
+ // Yield topTokens from selected chain
+ for (const token_ of topTokens) {
+ const matchedToken =
+ tokenList?.[token_.address] ??
+ tokenList?.[token_.address.toLowerCase()];
+ if (
+ matchedToken &&
+ shouldAddToken(
+ matchedToken.symbol,
+ matchedToken.address ?? undefined,
+ chainId,
+ )
+ ) {
+ const token = buildTokenData(matchedToken);
+ if (token) {
+ yield token;
+ }
+ }
+ }
+
+ // Yield other tokens from selected chain
+ for (const token_ of Object.values(tokenList)) {
+ if (
+ token_ &&
+ shouldAddToken(token_.symbol, token_.address ?? undefined, chainId)
+ ) {
+ const token = buildTokenData(token_);
+ if (token) {
+ yield token;
+ }
+ }
+ }
+ })(),
+ [
+ multichainTokensWithBalance,
+ topTokens,
+ tokenConversionRates,
+ conversionRate,
+ currentCurrency,
+ chainId,
+ tokenList,
+ tokenAddressFromUrl,
+ allDetectedTokens,
+ ],
+ );
+
+ return filteredTokenListGenerator;
+};
diff --git a/ui/hooks/useTokensWithFiltering.test.ts b/ui/hooks/useTokensWithFiltering.test.ts
deleted file mode 100644
index a44c2d1579f2..000000000000
--- a/ui/hooks/useTokensWithFiltering.test.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { renderHookWithProvider } from '../../test/lib/render-helpers';
-import { createBridgeMockStore } from '../../test/jest/mock-store';
-import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens';
-import {
- SWAPS_CHAINID_DEFAULT_TOKEN_MAP,
- SwapsTokenObject,
-} from '../../shared/constants/swaps';
-import { useTokensWithFiltering } from './useTokensWithFiltering';
-
-const mockUseTokenTracker = jest
- .fn()
- .mockReturnValue({ tokensWithBalances: [] });
-jest.mock('./useTokenTracker', () => ({
- useTokenTracker: () => mockUseTokenTracker(),
-}));
-
-const TEST_CHAIN_ID = '0x1';
-const NATIVE_TOKEN = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[TEST_CHAIN_ID];
-
-const MOCK_TOP_ASSETS = [
- { address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2' }, // UNI
- { address: NATIVE_TOKEN.address },
- { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' }, // USDC
- { address: '0xdac17f958d2ee523a2206206994597c13d831ec7' }, // USDT
-];
-
-const MOCK_TOKEN_LIST_BY_ADDRESS: Record = {
- [NATIVE_TOKEN.address]: NATIVE_TOKEN,
- ...STATIC_MAINNET_TOKEN_LIST,
-};
-
-describe('useTokensWithFiltering should return token list generator', () => {
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('when chainId === activeChainId and sorted by topAssets', () => {
- const mockStore = createBridgeMockStore();
- const { result } = renderHookWithProvider(
- () =>
- useTokensWithFiltering(
- MOCK_TOKEN_LIST_BY_ADDRESS,
- MOCK_TOP_ASSETS,
- {
- [TEST_CHAIN_ID]: new Set(Object.keys(MOCK_TOKEN_LIST_BY_ADDRESS)),
- },
- TEST_CHAIN_ID,
- ),
- mockStore,
- );
-
- expect(result.current).toHaveLength(1);
- expect(typeof result.current).toStrictEqual('function');
- const tokenGenerator = result.current(() => true);
- expect(tokenGenerator.next().value).toStrictEqual({
- address: '0x0000000000000000000000000000000000000000',
- balance: undefined,
- decimals: 18,
- iconUrl: './images/eth_logo.svg',
- identiconAddress: null,
- image: './images/eth_logo.svg',
- name: 'Ether',
- primaryLabel: 'ETH',
- rawFiat: '',
- chainId: '0x1',
- rightPrimaryLabel: undefined,
- rightSecondaryLabel: '',
- secondaryLabel: 'Ether',
- symbol: 'ETH',
- type: 'NATIVE',
- });
- expect(tokenGenerator.next().value).toStrictEqual({
- address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
- aggregators: [],
- balance: undefined,
- decimals: 18,
- erc20: true,
- erc721: false,
- chainId: '0x1',
- iconUrl:
- 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0x6b3595068778dd592e39a122f4f5a5cf09c90fe2.png',
- identiconAddress: null,
- image: 'images/contract/sushi.svg',
- name: 'SushiSwap',
- primaryLabel: 'SUSHI',
- rawFiat: '',
- rightPrimaryLabel: undefined,
- rightSecondaryLabel: '',
- secondaryLabel: 'SushiSwap',
- symbol: 'SUSHI',
- type: 'TOKEN',
- });
- });
-
- it('when chainId === activeChainId and sorted by balance', () => {
- const mockStore = createBridgeMockStore();
- mockUseTokenTracker.mockReturnValue({
- tokensWithBalances: [
- {
- address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
- balance: '0xa',
- },
- ],
- });
- const { result } = renderHookWithProvider(
- () =>
- useTokensWithFiltering(
- MOCK_TOKEN_LIST_BY_ADDRESS,
- MOCK_TOP_ASSETS,
- {
- [TEST_CHAIN_ID]: new Set(Object.keys(MOCK_TOKEN_LIST_BY_ADDRESS)),
- },
- TEST_CHAIN_ID,
- ),
- mockStore,
- );
-
- expect(result.current).toHaveLength(1);
- expect(typeof result.current).toStrictEqual('function');
- const tokenGenerator = result.current(() => true);
- expect(tokenGenerator.next().value).toStrictEqual({
- address: '0x0000000000000000000000000000000000000000',
- balance: '0x0',
- decimals: 18,
- iconUrl: './images/eth_logo.svg',
- identiconAddress: null,
- image: './images/eth_logo.svg',
- name: 'Ether',
- chainId: '0x1',
- primaryLabel: 'ETH',
- rawFiat: '0',
- rightPrimaryLabel: '0 ETH',
- rightSecondaryLabel: '$0.00 USD',
- secondaryLabel: 'Ether',
- string: '0',
- symbol: 'ETH',
- type: 'NATIVE',
- });
- expect(tokenGenerator.next().value).toStrictEqual({
- address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
- aggregators: [],
- balance: '0xa',
- decimals: 6,
- erc20: true,
- iconUrl:
- 'https://static.cx.metamask.io/api/v1/tokenIcons/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png',
- identiconAddress: null,
- image: 'images/contract/usdt.svg',
- name: 'Tether USD',
- chainId: '0x1',
- primaryLabel: 'USDT',
- rawFiat: '',
- rightPrimaryLabel: undefined,
- rightSecondaryLabel: '',
- secondaryLabel: 'Tether USD',
- symbol: 'USDT',
- type: 'TOKEN',
- });
- });
-});
diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts
deleted file mode 100644
index f4b5f10c9753..000000000000
--- a/ui/hooks/useTokensWithFiltering.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-import { useCallback, useMemo } from 'react';
-import { useSelector } from 'react-redux';
-import { isEqual } from 'lodash';
-import { ChainId } from '@metamask/controller-utils';
-import { Hex } from '@metamask/utils';
-import { useParams } from 'react-router-dom';
-import { zeroAddress } from 'ethereumjs-util';
-import {
- getAllTokens,
- getCurrentCurrency,
- getSelectedInternalAccountWithBalance,
- getShouldHideZeroBalanceTokens,
- getTokenExchangeRates,
-} from '../selectors';
-import { getConversionRate } from '../ducks/metamask/metamask';
-import { SwapsTokenObject } from '../../shared/constants/swaps';
-import {
- AssetWithDisplayData,
- ERC20Asset,
- NativeAsset,
- TokenWithBalance,
-} from '../components/multichain/asset-picker-amount/asset-picker-modal/types';
-import { AssetType } from '../../shared/constants/transaction';
-import { isNativeAddress } from '../pages/bridge/utils/quote';
-import { CHAIN_ID_TOKEN_IMAGE_MAP } from '../../shared/constants/network';
-import { getCurrentChainId } from '../../shared/modules/selectors/networks';
-import { useTokenTracker } from './useTokenTracker';
-import { useMultichainBalances } from './useMultichainBalances';
-
-/**
- * Returns a token list generator that filters and sorts tokens based on
- * query match, balance/popularity, all other tokens
- *
- * @param tokenList - a mapping of token addresses in the selected chainId to token metadata from the bridge-api
- * @param topTokens - a list of top tokens from the swap-api
- * @param tokenAddressAllowlistByChainId - a mapping of all supported chainIds to a Set of allowed token addresses
- * @param chainId - the selected src/dest chainId
- */
-export const useTokensWithFiltering = (
- tokenList: Record,
- topTokens: { address: string }[],
- tokenAddressAllowlistByChainId: Record>,
- chainId?: ChainId | Hex,
-) => {
- const { token: tokenAddressFromUrl } = useParams();
-
- // Only includes non-native tokens
- const allDetectedTokens = useSelector(getAllTokens);
- const { address: selectedAddress, balance: balanceOnActiveChain } =
- useSelector(getSelectedInternalAccountWithBalance);
-
- const allDetectedTokensForChainAndAddress = chainId
- ? allDetectedTokens?.[chainId]?.[selectedAddress] ?? []
- : [];
-
- const shouldHideZeroBalanceTokens = useSelector(
- getShouldHideZeroBalanceTokens,
- );
- const {
- tokensWithBalances: erc20TokensWithBalances,
- }: { tokensWithBalances: TokenWithBalance[] } = useTokenTracker({
- tokens: allDetectedTokensForChainAndAddress,
- address: selectedAddress,
- hideZeroBalanceTokens: Boolean(shouldHideZeroBalanceTokens),
- });
-
- const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
- const conversionRate = useSelector(getConversionRate);
- const currentCurrency = useSelector(getCurrentCurrency);
- const currentChainId = useSelector(getCurrentChainId);
-
- const sortedErc20TokensWithBalances = useMemo(
- () =>
- erc20TokensWithBalances.toSorted(
- (a, b) => Number(b.string) - Number(a.string),
- ),
- [erc20TokensWithBalances],
- );
-
- const { assetsWithBalance: multichainTokensWithBalance } =
- useMultichainBalances();
-
- const filteredTokenListGenerator = useCallback(
- (
- shouldAddToken: (
- symbol: string,
- address?: string,
- tokenChainId?: string,
- ) => boolean,
- ) => {
- const buildTokenData = (
- token: SwapsTokenObject,
- ):
- | AssetWithDisplayData
- | AssetWithDisplayData
- | undefined => {
- if (chainId && shouldAddToken(token.symbol, token.address, chainId)) {
- // Only tokens on the active chain are shown here
- const sharedFields = { ...token, chainId };
-
- if (isNativeAddress(token.address)) {
- return {
- ...sharedFields,
- type: AssetType.native,
- address: zeroAddress(),
- image:
- CHAIN_ID_TOKEN_IMAGE_MAP[
- chainId as keyof typeof CHAIN_ID_TOKEN_IMAGE_MAP
- ],
- balance: currentChainId === chainId ? balanceOnActiveChain : '',
- string: currentChainId === chainId ? balanceOnActiveChain : '',
- };
- }
-
- return {
- ...sharedFields,
- type: AssetType.token,
- image: token.iconUrl,
- // Only tokens with 0 balance are processed here so hardcode empty string
- balance: '',
- string: undefined,
- address: token.address || zeroAddress(),
- };
- }
-
- return undefined;
- };
-
- return (function* (): Generator<
- AssetWithDisplayData | AssetWithDisplayData
- > {
- if (tokenAddressFromUrl) {
- const tokenListItem =
- tokenList?.[tokenAddressFromUrl] ??
- tokenList?.[tokenAddressFromUrl.toLowerCase()];
- if (tokenListItem) {
- const tokenWithTokenListData = buildTokenData(tokenListItem);
- if (tokenWithTokenListData) {
- yield tokenWithTokenListData;
- }
- }
- }
-
- const isTokenBlocked = (tokenAddress: string, tokenChainId: string) =>
- !tokenAddressAllowlistByChainId[tokenChainId]?.has(
- tokenAddress.toLowerCase(),
- );
- // Yield multichain tokens with balances and are not blocked
- for (const token of multichainTokensWithBalance) {
- if (
- shouldAddToken(
- token.symbol,
- token.address ?? undefined,
- token.chainId,
- ) &&
- (token.address
- ? !isTokenBlocked(token.address, token.chainId)
- : true)
- ) {
- yield { ...token, address: token.address || zeroAddress() };
- }
- }
-
- // Yield topTokens from selected chain
- for (const topToken of topTokens) {
- const tokenListItem =
- tokenList?.[topToken.address] ??
- tokenList?.[topToken.address.toLowerCase()];
- if (tokenListItem) {
- const tokenWithTokenListData = buildTokenData(tokenListItem);
- if (tokenWithTokenListData) {
- yield tokenWithTokenListData;
- }
- }
- }
-
- // Yield other tokens from selected chain
- for (const token of Object.values(tokenList)) {
- const tokenWithTokenListData = buildTokenData(token);
- if (tokenWithTokenListData) {
- yield tokenWithTokenListData;
- }
- }
- })();
- },
- [
- multichainTokensWithBalance,
- sortedErc20TokensWithBalances,
- topTokens,
- tokenConversionRates,
- conversionRate,
- currentCurrency,
- chainId,
- tokenList,
- tokenAddressFromUrl,
- ],
- );
-
- return filteredTokenListGenerator;
-};
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 8051c06afc88..1433c627af21 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -60,7 +60,7 @@ import {
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../../../../shared/constants/swaps';
-import { useTokensWithFiltering } from '../../../hooks/useTokensWithFiltering';
+import { useTokensWithFiltering } from '../../../hooks/bridge/useTokensWithFiltering';
import { setActiveNetwork } from '../../../store/actions';
import { hexToDecimal } from '../../../../shared/modules/conversion.utils';
import { QuoteRequest } from '../types';
From 725c5382896473de4c1df08ad8ff66cda827e0b8 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 10:38:07 -0800
Subject: [PATCH 59/69] test: update snapshots
---
.../bridge-cta-button.test.tsx.snap | 31 +++++++++++++++++++
.../bridge/prepare/bridge-cta-button.test.tsx | 24 +++++++-------
.../bridge-quote-card.test.tsx.snap | 8 ++---
.../bridge-quotes-modal.test.tsx.snap | 6 ++--
4 files changed, 49 insertions(+), 20 deletions(-)
diff --git a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
index 5e2af1093266..0b46d2764523 100644
--- a/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
+++ b/ui/pages/bridge/prepare/__snapshots__/bridge-cta-button.test.tsx.snap
@@ -1,5 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`BridgeCTAButton should disable the component when quotes are loading and there are no existing quotes 1`] = `
+
+`;
+
+exports[`BridgeCTAButton should enable the component when quotes are loading and there are existing quotes 1`] = `
+
+
+ Submit
+
+
+`;
+
+exports[`BridgeCTAButton should render the component when amount and dest token is missing 1`] = `
+
+
+ Select token and amount
+
+
+`;
+
exports[`BridgeCTAButton should render the component's initial state 1`] = `
{
},
bridgeSliceOverrides: { fromTokenInputValue: 1 },
});
- const { container, getByText, getByRole } = renderWithProvider(
+ const { container, getByText } = renderWithProvider(
,
configureStore(mockStore),
);
@@ -30,7 +30,6 @@ describe('BridgeCTAButton', () => {
expect(container).toMatchSnapshot();
expect(getByText('Select token')).toBeInTheDocument();
- expect(getByRole('button')).toBeDisabled();
});
it('should render the component when amount is missing', () => {
@@ -54,13 +53,12 @@ describe('BridgeCTAButton', () => {
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
});
- const { getByText, getByRole } = renderWithProvider(
+ const { getByText } = renderWithProvider(
,
configureStore(mockStore),
);
- expect(getByText('Enter amount')).toBeInTheDocument();
- expect(getByRole('button')).toBeDisabled();
+ expect(getByText('Select amount')).toBeInTheDocument();
});
it('should render the component when amount and dest token is missing', () => {
@@ -84,13 +82,13 @@ describe('BridgeCTAButton', () => {
toChainId: CHAIN_IDS.LINEA_MAINNET,
},
});
- const { getByText, getByRole } = renderWithProvider(
+ const { getByText, container } = renderWithProvider(
,
configureStore(mockStore),
);
expect(getByText('Select token and amount')).toBeInTheDocument();
- expect(getByRole('button')).toBeDisabled();
+ expect(container).toMatchSnapshot();
});
it('should render the component when tx is submittable', () => {
@@ -124,7 +122,7 @@ describe('BridgeCTAButton', () => {
configureStore(mockStore),
);
- expect(getByText('Confirm')).toBeInTheDocument();
+ expect(getByText('Submit')).toBeInTheDocument();
expect(getByRole('button')).not.toBeDisabled();
});
@@ -160,13 +158,12 @@ describe('BridgeCTAButton', () => {
quotesLoadingStatus: RequestStatus.LOADING,
},
});
- const { getByText, getByRole } = renderWithProvider(
+ const { container } = renderWithProvider(
,
configureStore(mockStore),
);
- expect(getByText('Fetching quotes...')).toBeInTheDocument();
- expect(getByRole('button')).toBeDisabled();
+ expect(container).toMatchSnapshot();
});
it('should enable the component when quotes are loading and there are existing quotes', () => {
@@ -201,12 +198,13 @@ describe('BridgeCTAButton', () => {
quotesLoadingStatus: RequestStatus.LOADING,
},
});
- const { getByText, getByRole } = renderWithProvider(
+ const { getByText, getByRole, container } = renderWithProvider(
,
configureStore(mockStore),
);
- expect(getByText('Confirm')).toBeInTheDocument();
+ expect(getByText('Submit')).toBeInTheDocument();
expect(getByRole('button')).not.toBeDisabled();
+ expect(container).toMatchSnapshot();
});
});
diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
index 4501caa1f862..4b0b41df569d 100644
--- a/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
+++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quote-card.test.tsx.snap
@@ -59,7 +59,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
class="mm-box mm-container mm-container--max-width-undefined mm-box--display-flex mm-box--gap-1 mm-box--flex-direction-row mm-box--flex-wrap-nowrap mm-box--justify-content-space-between mm-box--align-items-center"
>
Date: Wed, 11 Dec 2024 11:32:03 -0800
Subject: [PATCH 60/69] use multichain asset in bridge
---
ui/pages/bridge/types.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index 332590240c6d..f8aabd51f8e8 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -29,7 +29,7 @@ export enum SortOrder {
}
export type BridgeToken =
- | ((AssetWithDisplayData
| AssetWithDisplayData) & {
+ | (AssetWithDisplayData & {
aggregators?: string[];
address: string;
})
From 8b8ce3a11bab47c6344c497437aab58f0068be86 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 12:38:45 -0800
Subject: [PATCH 61/69] fix: unused translations
---
app/_locales/de/messages.json | 12 ------------
app/_locales/el/messages.json | 12 ------------
app/_locales/en/messages.json | 14 ++++----------
app/_locales/es/messages.json | 12 ------------
app/_locales/fr/messages.json | 12 ------------
app/_locales/hi/messages.json | 12 ------------
app/_locales/id/messages.json | 12 ------------
app/_locales/ja/messages.json | 13 +------------
app/_locales/ko/messages.json | 13 +------------
app/_locales/pt/messages.json | 13 +------------
app/_locales/ru/messages.json | 12 ------------
app/_locales/tl/messages.json | 13 +------------
app/_locales/tr/messages.json | 13 +------------
app/_locales/vi/messages.json | 13 +------------
app/_locales/zh_CN/messages.json | 13 +------------
ui/pages/bridge/quotes/bridge-quote-card.tsx | 2 +-
16 files changed, 12 insertions(+), 179 deletions(-)
diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index 6d0878b57d5c..27d17aa94179 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Token und Betrag auswählen"
},
- "bridgeTimingTooltipText": {
- "message": "Dies ist die voraussichtliche Dauer, bis das Bridging abgeschlossen ist."
- },
"bridgeTo": {
"message": "Bridge nach"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Dazu gehören Gas-Gebühren (die an Krypto-Miner gezahlt werden) und Relayer-Gebühren (die für die Bereitstellung komplexer Dienste wie Bridging entrichtet werden).\nDie Gebühren richten sich nach dem Netzwerk-Traffic und der Komplexität der Transaktionen. MetaMask profitiert von keiner der Gebühren."
- },
"browserNotSupported": {
"message": "Ihr Browser wird nicht unterstützt …"
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Betrag, der für die Bearbeitung der Transaktion im Netzwerk gezahlt wurde."
},
- "estimatedTime": {
- "message": "Geschätzte Dauer"
- },
"ethGasPriceFetchWarning": {
"message": "Der Gas-Preis, der sich aus der Gas-Hauptschätzungsdienst ergibt, ist derzeit nicht verfügbar."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Gesamt"
},
- "totalFees": {
- "message": "Gesamtgebühren"
- },
"totalVolume": {
"message": "Gesamtvolumen"
},
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index 58045c5b0578..ba948b6aabb9 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Επιλέξτε token και ποσό"
},
- "bridgeTimingTooltipText": {
- "message": "Αυτός είναι ο εκτιμώμενος χρόνος που θα χρειαστεί για να ολοκληρωθεί η διασύνδεση."
- },
"bridgeTo": {
"message": "Γέφυρα σε"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Αυτό περιλαμβάνει τα τέλη συναλλαγών (που καταβάλλονται στους αναλυτές κρυπτονομισμάτων) και τέλη αποδεκτών (που καταβάλλονται για την παροχή σύνθετων υπηρεσιών όπως η διασύνδεση).\nΤα τέλη βασίζονται στην κίνηση του δικτύου και την πολυπλοκότητα των συναλλαγών. Το MetaMask δεν επωφελείται από κανένα από τα δύο τέλη."
- },
"browserNotSupported": {
"message": "Το Πρόγραμμα Περιήγησής σας δεν υποστηρίζεται..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Ποσό που καταβλήθηκε για τη διεκπεραίωση της συναλλαγής στο δίκτυο."
},
- "estimatedTime": {
- "message": "Εκτιμώμενος χρόνος"
- },
"ethGasPriceFetchWarning": {
"message": "Η εφεδρική τιμή του τέλους συναλλαγής παρέχεται καθώς η κύρια υπηρεσία εκτίμησης τελών συναλλαγής, δεν είναι διαθέσιμη αυτή τη στιγμή."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Σύνολο"
},
- "totalFees": {
- "message": "Συνολικά τέλη"
- },
"totalVolume": {
"message": "Συνολικός όγκος"
},
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 1090c72aa707..7fc208a80b63 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -933,9 +933,6 @@
"bridgeToChain": {
"message": "Bridge to $1"
},
- "bridgeTotalFeesTooltipText": {
- "message": "This includes gas fees (paid to crypto miners) and relayer fees (paid to power complex services like bridging).\nFees are based on network traffic and transaction complexity. MetaMask does not profit from either fee."
- },
"bridgeTxDetailsBaseFee": {
"message": "Base fee (GWEI)"
},
@@ -3153,6 +3150,9 @@
"message": "+ $1 more networks",
"description": "$1 is the number of networks"
},
+ "moreQuotes": {
+ "message": "More quotes"
+ },
"multichainAddEthereumChainConfirmationDescription": {
"message": "You're adding this network to MetaMask and giving this site permission to use it."
},
@@ -4521,9 +4521,6 @@
"quoteRate": {
"message": "Quote rate"
},
- "quotedNetworkFee": {
- "message": "$1 network fee"
- },
"quotedReceiveAmount": {
"message": "$1 receive amount"
},
@@ -6387,9 +6384,6 @@
"total": {
"message": "Total"
},
- "totalFees": {
- "message": "Total fees"
- },
"totalVolume": {
"message": "Total volume"
},
@@ -6730,7 +6724,7 @@
"message": "View activity"
},
"viewAllQuotes": {
- "message": "More quotes"
+ "message": "view all quotes"
},
"viewContact": {
"message": "View contact"
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index f5bec514637e..dd3fe6d66d48 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Seleccione token y monto"
},
- "bridgeTimingTooltipText": {
- "message": "Este es el tiempo estimado que tardará en completarse el puenteo."
- },
"bridgeTo": {
"message": "Puentear hacia"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Esto incluye las tarifas de gas (pagadas a los mineros de criptomonedas) y las tarifas de repetidores (pagadas para alimentar servicios complejos como el puenteo).\nLas tarifas se basan en el tráfico de la red y la complejidad de las transacciones. MetaMask no lucra con ninguna de las dos tarifas."
- },
"browserNotSupported": {
"message": "Su explorador no es compatible..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Monto pagado para procesar la transacción en la red."
},
- "estimatedTime": {
- "message": "Tiempo estimado"
- },
"ethGasPriceFetchWarning": {
"message": "Se muestra el precio del gas de respaldo, ya que el servicio para calcular el precio del gas principal no se encuentra disponible en este momento."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Total"
},
- "totalFees": {
- "message": "Tarifas totales"
- },
"totalVolume": {
"message": "Volúmen total"
},
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index 4d5e59b25c52..de32754a3492 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Sélectionnez le jeton et le montant"
},
- "bridgeTimingTooltipText": {
- "message": "Il s’agit d’une estimation du temps nécessaire pour que la passerelle soit établie."
- },
"bridgeTo": {
"message": "Passerelle vers"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Cela comprend les frais de gaz (payés aux mineurs de crypto-monnaies) et les frais pour relayeurs (payés pour assurer des services complexes tels que l’établissement de passerelles).\nLes frais sont basés sur le trafic réseau et la complexité des transactions. MetaMask ne tire aucun profit de ces frais."
- },
"browserNotSupported": {
"message": "Votre navigateur internet n’est pas compatible..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Montant payé pour traiter la transaction sur le réseau."
},
- "estimatedTime": {
- "message": "Temps estimé"
- },
"ethGasPriceFetchWarning": {
"message": "Le prix de carburant de sauvegarde est fourni, car le service principal d’estimation du carburant est momentanément indisponible."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Total"
},
- "totalFees": {
- "message": "Frais totaux"
- },
"totalVolume": {
"message": "Volume total"
},
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 2d4bcc52b891..9b9ad396c53a 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "टोकन और रकम का चयन करें"
},
- "bridgeTimingTooltipText": {
- "message": "ब्रिजिंग का काम पूरा होने में यह अनुमानित समय लगेगा।"
- },
"bridgeTo": {
"message": "इसपर ब्रिज करें"
},
- "bridgeTotalFeesTooltipText": {
- "message": "इसमें गैस शुल्क (क्रिप्टो माइनरों (miners) को भुगतान) और रीलेयर (relayer) शुल्क (ब्रिजिंग जैसी जटिल सेवाओं को मजबूत करने के लिए भुगतान) शामिल हैं।\nशुल्क नेटवर्क ट्रैफ़िक और ट्रांसेक्शन जटिलता पर आधारित हैं। MetaMask को किसी भी शुल्क से लाभ नहीं होता है।"
- },
"browserNotSupported": {
"message": "आपका ब्राउज़र सपोर्टेड नहीं है..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "नेटवर्क पर ट्रांसेक्शन को प्रोसेस करने के लिए भुगतान की गई राशि।"
},
- "estimatedTime": {
- "message": "अनुमानित समय"
- },
"ethGasPriceFetchWarning": {
"message": "बैकअप गैस प्राइस दिया गया है क्योंकि मेन गैस एस्टीमेशन सर्विस अभी उपलब्ध नहीं है।"
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "कुलयोग"
},
- "totalFees": {
- "message": "कुल शुल्क"
- },
"totalVolume": {
"message": "टोटल वॉल्यूम"
},
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 9135a2e56bcf..8f386f0cc95e 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Pilih token dan jumlah"
},
- "bridgeTimingTooltipText": {
- "message": "Ini merupakan estimasi waktu yang diperlukan untuk menyelesaikan bridge."
- },
"bridgeTo": {
"message": "Bridge ke"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Ini termasuk biaya gas (dibayarkan kepada penambang kripto) dan biaya relayer (dibayarkan untuk menjalankan layanan kompleks seperti bridge).\nBiaya didasarkan pada lalu lintas jaringan dan kompleksitas transaksi. MetaMask tidak mendapat keuntungan dari kedua biaya tersebut."
- },
"browserNotSupported": {
"message": "Browser Anda tidak didukung..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Jumlah yang dibayarkan untuk memproses transaksi di jaringan."
},
- "estimatedTime": {
- "message": "Estimasi waktu"
- },
"ethGasPriceFetchWarning": {
"message": "Biaya gas cadangan diberikan karena layanan estimasi gas utama saat ini tidak tersedia."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Total"
},
- "totalFees": {
- "message": "Total biaya"
- },
"totalVolume": {
"message": "Volume total"
},
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 8404c4eb3af4..bca8d08db97f 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "トークンと金額を選択"
},
- "bridgeTimingTooltipText": {
- "message": "これは、ブリッジが完了するまでの推定時間です。"
- },
"bridgeTo": {
"message": "ブリッジ先:"
},
- "bridgeTotalFeesTooltipText": {
- "message": "これには、(仮想通貨マイナーに支払われる) ガス代と (ブリッジなどの複雑なサービスを供給するために支払われる) リレイヤー手数料が含まれます。\n手数料はネットワークトラフィックとトランザクションの複雑性に基づいています。MetaMaskはどちらの手数料からも利益を得ることはありません。"
- },
+
"browserNotSupported": {
"message": "ご使用のブラウザはサポートされていません..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "ネットワーク上のトランザクションの処理に支払われる金額"
},
- "estimatedTime": {
- "message": "推定所要時間"
- },
"ethGasPriceFetchWarning": {
"message": "現在メインのガスの見積もりサービスが利用できないため、バックアップのガス価格が提供されています。"
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "合計"
},
- "totalFees": {
- "message": "合計手数料"
- },
"totalVolume": {
"message": "合計量"
},
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index cbd48592c7d0..8c89ab592576 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "토큰 및 금액 선택"
},
- "bridgeTimingTooltipText": {
- "message": "브릿지가 완료되는 데 걸리는 예상 시간입니다."
- },
"bridgeTo": {
"message": "브릿지 대상"
},
- "bridgeTotalFeesTooltipText": {
- "message": "가스비(암호화폐 채굴자에게 지급)와 릴레이어 수수료(브릿지와 같은 복잡한 서비스 제공에 지급)가 포함됩니다.\n수수료는 네트워크 트래픽과 트랜잭션 복잡성에 따라 달라집니다. MetaMask는 어떤 수수료에서도 수익을 얻지 않습니다."
- },
+
"browserNotSupported": {
"message": "지원되지 않는 브라우저입니다..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "네트워크에서 트랜잭션을 처리하기 위해 지불한 금액입니다."
},
- "estimatedTime": {
- "message": "예상 시간"
- },
"ethGasPriceFetchWarning": {
"message": "현재 주요 가스 견적 서비스를 사용할 수 없으므로 백업 가스 가격을 제공합니다."
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "합계"
},
- "totalFees": {
- "message": "총 수수료"
- },
"totalVolume": {
"message": "총 거래량"
},
diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json
index 3a01b378c686..5a5137fe8c08 100644
--- a/app/_locales/pt/messages.json
+++ b/app/_locales/pt/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "Selecionar token e valor"
},
- "bridgeTimingTooltipText": {
- "message": "Este é o tempo estimado para a conclusão da ponte."
- },
"bridgeTo": {
"message": "Ponte para"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Isso inclui as taxas de gás (pagas aos mineradores de criptmoedas) e taxas de retransmissão (pagas para promover serviços complexos como pontes).\nAs taxas são baseadas no tráfego da rede e na complexidade da transação. A MetaMask não lucra com nenhuma dessas taxas."
- },
+
"browserNotSupported": {
"message": "Seu navegador não é compatível..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "Valor pago para processar a transação na rede."
},
- "estimatedTime": {
- "message": "Tempo estimado"
- },
"ethGasPriceFetchWarning": {
"message": "O preço de backup do gás é fornecido porque a estimativa de gás principal está indisponível no momento."
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "Total"
},
- "totalFees": {
- "message": "Taxas totais"
- },
"totalVolume": {
"message": "Volume total"
},
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index 385670721c1f..bee509f85e99 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -861,15 +861,9 @@
"bridgeSelectTokenAndAmount": {
"message": "Выберите токен и сумму"
},
- "bridgeTimingTooltipText": {
- "message": "Это примерное время, которое потребуется для создания моста."
- },
"bridgeTo": {
"message": "Мост в"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Сюда входят плата за газ (выплачивается майнерам криптовалюты) и плата ретранслятору (выплачивается за услуги энергетического комплекса, такие как мостовое соединение).\nРазмер комиссии зависит от сетевого трафика и сложности транзакции. MetaMask не получает прибыли ни от одной комиссии."
- },
"browserNotSupported": {
"message": "Ваш браузер не поддерживается..."
},
@@ -1989,9 +1983,6 @@
"estimatedFeeTooltip": {
"message": "Сумма, уплаченная за обработку транзакции в сети."
},
- "estimatedTime": {
- "message": "Примерное время"
- },
"ethGasPriceFetchWarning": {
"message": "Указана резервная цена газа, поскольку основной сервис определения цены газа сейчас недоступен."
},
@@ -6118,9 +6109,6 @@
"total": {
"message": "Итого"
},
- "totalFees": {
- "message": "Итого комиссий"
- },
"totalVolume": {
"message": "Общий объем"
},
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index a13c9c6d3006..91d6762de059 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "Piliin ang token at halaga"
},
- "bridgeTimingTooltipText": {
- "message": "Ito ang tinatayang tagal para makumpleto ang pag-bridge."
- },
"bridgeTo": {
"message": "I-bridge papunta sa"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Kasama rito ang mga bayad sa gas (binabayaran sa mga crypto miner) at mga bayad sa tagapaghatid (binabayaran sa mga serbisyo ng power complex gaya ng pag-bridge).\nNakabatay ang mga bayad sa trapiko sa network at kung paano kakumplikado ang transaksyon. Hindi kumikita ang MetaMask sa anumang bayad."
- },
+
"browserNotSupported": {
"message": "Hindi sinusuportahan ang iyong browser..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "Halaga na binayaran para iproseso ang transaksyon sa network."
},
- "estimatedTime": {
- "message": "Tinatayang oras"
- },
"ethGasPriceFetchWarning": {
"message": "Ang backup na presyo ng gas ay ibinigay dahil ang pangunahing serbisyo ng pagtantya ng gas ay hindi available sa ngayon."
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "Kabuuan"
},
- "totalFees": {
- "message": "Kabuuang bayad"
- },
"totalVolume": {
"message": "Kabuuang volume"
},
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index 8f761e554caa..2f0dc5a89ac2 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "Token ve miktar seçin"
},
- "bridgeTimingTooltipText": {
- "message": "Bu, köprü işleminin tamamlanacağı tahmini süredir."
- },
"bridgeTo": {
"message": "Şuraya köprü:"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Buna, gaz ücretleri (kripto madencilerine ödenen) ve düzenleyici ücretleri (köprü gibi güç kompleksi hizmetlerine ödenen) dahildir. Ücretler ağ trafiğine ve işlemin karmaşıklığına dayanır. MetaMask iki ücretten de kazanç sağlamaz."
- },
+
"browserNotSupported": {
"message": "Tarayıcınız desteklenmiyor..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "Ağda işlemi gerçekleştirmek için ödenen tutar."
},
- "estimatedTime": {
- "message": "Tahmini süre"
- },
"ethGasPriceFetchWarning": {
"message": "Ana gaz tahmini hizmeti olarak sunulan yedek gaz fiyatı şu anda kullanılamıyor."
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "Toplam"
},
- "totalFees": {
- "message": "Toplam ücretler"
- },
"totalVolume": {
"message": "Toplam hacim"
},
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index 883f08f49a7e..b5779f736262 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "Chọn token và số tiền"
},
- "bridgeTimingTooltipText": {
- "message": "Đây là thời gian ước tính để hoàn thành cầu nối."
- },
"bridgeTo": {
"message": "Cầu nối đến"
},
- "bridgeTotalFeesTooltipText": {
- "message": "Bao gồm phí gas (trả cho các thợ đào tiền mã hóa) và phí sàn chuyển tiếp (trả để cung cấp các dịch vụ phức tạp như cầu nối).\nPhí được tính dựa trên lưu lượng mạng và độ phức tạp của giao dịch. MetaMask không thu lợi từ bất kỳ khoản phí nào."
- },
+
"browserNotSupported": {
"message": "Trình duyệt của bạn không được hỗ trợ..."
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "Số tiền được chi trả để xử lý giao dịch trên mạng."
},
- "estimatedTime": {
- "message": "Thời gian ước tính"
- },
"ethGasPriceFetchWarning": {
"message": "Giá gas dự phòng được cung cấp vì dịch vụ ước tính giá gas chính hiện không hoạt động."
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "Tổng"
},
- "totalFees": {
- "message": "Tổng phí"
- },
"totalVolume": {
"message": "Tổng khối lượng giao dịch"
},
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index fb7ed008d6f6..14eb163d7bb3 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -861,15 +861,10 @@
"bridgeSelectTokenAndAmount": {
"message": "选择代币和金额"
},
- "bridgeTimingTooltipText": {
- "message": "这是完成桥接所需的预估时间。"
- },
"bridgeTo": {
"message": "桥接至"
},
- "bridgeTotalFeesTooltipText": {
- "message": "这包括燃料费(支付给加密货币矿工)和中继器费用(用于为桥接等复杂服务提供动力)。\n费用根据网络流量和交易复杂性而定。MetaMask 不会从这两项费用中获利。"
- },
+
"browserNotSupported": {
"message": "您的浏览器不受支持……"
},
@@ -1989,9 +1984,6 @@
"estimatedFeeTooltip": {
"message": "为在网络上处理交易而支付的金额。"
},
- "estimatedTime": {
- "message": "预估时间"
- },
"ethGasPriceFetchWarning": {
"message": "由于目前主要的燃料估算服务不可用,因此提供了备用燃料价格。"
},
@@ -6118,9 +6110,6 @@
"total": {
"message": "共计"
},
- "totalFees": {
- "message": "总费用"
- },
"totalVolume": {
"message": "总交易额"
},
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 5d965aabcfef..d44d66e0e98f 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -107,7 +107,7 @@ export const BridgeQuoteCard = () => {
setShowAllQuotes(true);
}}
>
- {t('viewAllQuotes')}
+ {t('moreQuotes')}
From ba26267a2f34a7065696016e358b07649858e3b9 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 12:39:52 -0800
Subject: [PATCH 62/69] fix: test data
---
test/jest/mock-store.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js
index 309249ff2d6b..8281eae43390 100644
--- a/test/jest/mock-store.js
+++ b/test/jest/mock-store.js
@@ -721,6 +721,11 @@ export const createBridgeMockStore = (
const swapsStore = createSwapsMockStore();
return {
...swapsStore,
+ // For initial state of dest asset picker
+ swaps: {
+ ...swapsStore.swaps,
+ topAssets: [],
+ },
bridge: {
toChainId: null,
sortOrder: 'cost_ascending',
@@ -754,7 +759,7 @@ export const createBridgeMockStore = (
...mockTokenData,
...metamaskStateOverrides,
bridgeState: {
- ...(swapsStore.metamask.bridgeState ?? {}),
+ ...DEFAULT_BRIDGE_CONTROLLER_STATE,
bridgeFeatureFlags: {
...featureFlagOverrides,
extensionConfig: {
@@ -763,8 +768,6 @@ export const createBridgeMockStore = (
...featureFlagOverrides.extensionConfig,
},
},
- quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes,
- quoteRequest: DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest,
...bridgeStateOverrides,
},
bridgeStatusState: {
From 25f183a4e5611ed476049c32785575e7b3f90747 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 12:49:50 -0800
Subject: [PATCH 63/69] fix: prepare-bridge-page tests
---
.../prepare-bridge-page.test.tsx.snap | 621 +++++++-----------
.../prepare/prepare-bridge-page.test.tsx | 43 +-
2 files changed, 288 insertions(+), 376 deletions(-)
diff --git a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
index b4873c7e1c89..d5b980d13fd6 100644
--- a/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
+++ b/ui/pages/bridge/prepare/__snapshots__/prepare-bridge-page.test.tsx.snap
@@ -3,125 +3,109 @@
exports[`PrepareBridgePage should render the component, with initial state 1`] = `
-
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
+
+ ETH
+
+
+
+
+
+
+
-
+ $0.00
-
-
- $0.00
-
-
+
+
+
-
-
-
-
-
-
- ?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Bridge to
+
+
+
+
+
+
+
-
- $0.00
-
+ Select token and amount
+
@@ -239,131 +188,109 @@ exports[`PrepareBridgePage should render the component, with initial state 1`] =
exports[`PrepareBridgePage should render the component, with inputs set 1`] = `
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+ ETH
+
+
+
+
+
+
+
-
+ $0.00
-
-
- $5,805.77
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Bridge to
+
+
+
+
+
+
+
-
- $0.00
-
+ Select token and amount
+
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
index 6803deb301aa..de8fdfc25b36 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { act } from '@testing-library/react';
import * as reactRouterUtils from 'react-router-dom-v5-compat';
+import { zeroAddress } from 'ethereumjs-util';
import { fireEvent, renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import { createBridgeMockStore } from '../../../../test/jest/mock-store';
@@ -42,6 +43,34 @@ describe('PrepareBridgePage', () => {
},
},
},
+ metamaskStateOverrides: {
+ completedOnboarding: true,
+ allDetectedTokens: {
+ '0x1': {
+ '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': [
+ {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ decimals: 6,
+ }, // USDC
+ ],
+ },
+ },
+ },
+ bridgeStateOverrides: {
+ srcTokens: {
+ '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2': {
+ address: '0x6b3595068778dd592e39a122f4f5a5cf09c90fe2',
+ }, // UNI,
+ [zeroAddress()]: { address: zeroAddress() },
+ '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984': {
+ address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
+ decimals: 6,
+ }, // USDC
+ },
+ srcTopAssets: [
+ { address: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984' },
+ ],
+ },
});
const { container, getByRole, getByTestId } = renderWithProvider(
,
@@ -57,7 +86,7 @@ describe('PrepareBridgePage', () => {
await act(() => {
fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } });
});
- expect(getByTestId('from-amount').closest('input')).toHaveValue(2);
+ expect(getByTestId('from-amount').closest('input')).toHaveValue('2');
expect(getByTestId('to-amount')).toBeInTheDocument();
expect(getByTestId('to-amount').closest('input')).toBeDisabled();
@@ -126,21 +155,25 @@ describe('PrepareBridgePage', () => {
expect(container).toMatchSnapshot();
expect(getByRole('button', { name: /ETH/u })).toBeInTheDocument();
- expect(getByRole('button', { name: /UNI/u })).toBeInTheDocument();
+ expect(getByRole('button', { name: /Bridge to/u })).toBeInTheDocument();
expect(getByTestId('from-amount')).toBeInTheDocument();
expect(getByTestId('from-amount').closest('input')).not.toBeDisabled();
- expect(getByTestId('from-amount').closest('input')).toHaveValue(1);
+
+ await act(() => {
+ fireEvent.change(getByTestId('from-amount'), { target: { value: '1' } });
+ });
+ expect(getByTestId('from-amount').closest('input')).toHaveValue('1');
await act(() => {
fireEvent.change(getByTestId('from-amount'), { target: { value: '2' } });
});
- expect(getByTestId('from-amount').closest('input')).toHaveValue(2);
+ expect(getByTestId('from-amount').closest('input')).toHaveValue('2');
expect(getByTestId('to-amount')).toBeInTheDocument();
expect(getByTestId('to-amount').closest('input')).toBeDisabled();
- expect(getByTestId('switch-tokens').closest('button')).not.toBeDisabled();
+ expect(getByTestId('switch-tokens').closest('button')).toBeDisabled();
});
it('should throw an error if token decimals are not defined', async () => {
From 33202d6adc753c9c5b08bf23a6551e6f0feeab63 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 13:03:10 -0800
Subject: [PATCH 64/69] fix: unit tests
---
.../app/wallet-overview/eth-overview.test.js | 28 +++++++++++++++----
ui/ducks/bridge/selectors.test.ts | 2 +-
ui/pages/asset/components/asset-page.test.tsx | 8 +++---
3 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/ui/components/app/wallet-overview/eth-overview.test.js b/ui/components/app/wallet-overview/eth-overview.test.js
index 104764251cf8..254e1eb9d1d4 100644
--- a/ui/components/app/wallet-overview/eth-overview.test.js
+++ b/ui/components/app/wallet-overview/eth-overview.test.js
@@ -253,7 +253,25 @@ describe('EthOverview', () => {
});
it('should open the Bridge URI when clicking on Bridge button on supported network', async () => {
- const { queryByTestId } = renderWithProvider( , store);
+ const mockedStore = configureMockStore([thunk])({
+ ...store,
+ metamask: {
+ ...mockStore.metamask,
+ ...mockNetworkState({ chainId: '0xa86a' }),
+ useExternalServices: true,
+ bridgeState: {
+ bridgeFeatureFlags: {
+ extensionConfig: {
+ support: false,
+ },
+ },
+ },
+ },
+ });
+ const { queryByTestId } = renderWithProvider(
+ ,
+ mockedStore,
+ );
const bridgeButton = queryByTestId(ETH_OVERVIEW_BRIDGE);
@@ -261,15 +279,15 @@ describe('EthOverview', () => {
expect(bridgeButton).not.toBeDisabled();
fireEvent.click(bridgeButton);
- expect(openTabSpy).toHaveBeenCalledTimes(1);
- await waitFor(() =>
+ await waitFor(() => {
+ expect(openTabSpy).toHaveBeenCalledTimes(1);
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining(
'/bridge?metamaskEntry=ext_bridge_button',
),
- }),
- );
+ });
+ });
});
it('should open the MMI PD Swaps URI when clicking on Swap button with a Custody account', async () => {
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index 50ca56eff7ce..47abbeb1853c 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -838,7 +838,7 @@ describe('Bridge selectors', () => {
isLoading: false,
isQuoteGoingToRefresh: false,
quotesLastFetchedMs: undefined,
- quotesRefreshCount: undefined,
+ quotesRefreshCount: 0,
recommendedQuote: undefined,
quotesInitialLoadTimeMs: undefined,
sortedQuotes: [],
diff --git a/ui/pages/asset/components/asset-page.test.tsx b/ui/pages/asset/components/asset-page.test.tsx
index 28e232a0ba0b..71ca5483b50c 100644
--- a/ui/pages/asset/components/asset-page.test.tsx
+++ b/ui/pages/asset/components/asset-page.test.tsx
@@ -291,13 +291,13 @@ describe('AssetPage', () => {
expect(bridgeButton).not.toBeDisabled();
fireEvent.click(bridgeButton as HTMLElement);
- expect(openTabSpy).toHaveBeenCalledTimes(1);
- await waitFor(() =>
+ await waitFor(() => {
+ expect(openTabSpy).toHaveBeenCalledTimes(1);
expect(openTabSpy).toHaveBeenCalledWith({
url: `https://portfolio.test/bridge?metamaskEntry=ext_bridge_button&metametricsId=&metricsEnabled=false&marketingEnabled=false&token=${token.address}`,
- }),
- );
+ });
+ });
});
it('should not show the Bridge button if chain id is not supported', async () => {
From 4b88a1c8748b867b46ccb5905648d0608015888a Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 13:52:44 -0800
Subject: [PATCH 65/69] fix: show total cost
---
.../bridge-quotes-modal.test.tsx.snap | 4 ++--
.../quotes/bridge-quotes-modal.test.tsx | 23 +++++++++++++++++++
.../bridge/quotes/bridge-quotes-modal.tsx | 8 +++++--
3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
index 998b4c439d7c..2c401feffa78 100644
--- a/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
+++ b/ui/pages/bridge/quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap
@@ -111,7 +111,7 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
- $3 total cost
+ $14 total cost
- $3 total cost
+ $14 total cost
{
@@ -16,6 +18,27 @@ describe('BridgeQuotesModal', () => {
getQuotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.FETCHED,
},
+ bridgeSliceOverrides: {
+ fromTokenExchangeRate: 1,
+ toTokenExchangeRate: 0.99,
+ },
+ metamaskStateOverrides: {
+ currencyRates: {
+ ETH: {
+ conversionRate: 1,
+ },
+ POL: {
+ conversionRate: 1,
+ usdConversionRate: 1,
+ },
+ },
+ ...mockNetworkState(
+ { chainId: CHAIN_IDS.MAINNET },
+ { chainId: CHAIN_IDS.LINEA_MAINNET },
+ { chainId: CHAIN_IDS.POLYGON },
+ { chainId: CHAIN_IDS.OPTIMISM },
+ ),
+ },
});
const { baseElement } = renderWithProvider(
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 15e1cf593b92..e9cb65f612fe 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -146,6 +146,7 @@ export const BridgeQuotesModal = ({
estimatedProcessingTimeInSeconds,
toTokenAmount,
cost,
+ sentAmount,
quote: { destAsset, bridges, requestId },
} = quote;
const isQuoteActive = requestId === activeQuote?.quote.requestId;
@@ -202,10 +203,13 @@ export const BridgeQuotesModal = ({
formatCurrencyAmount(cost.valueInCurrency, currency, 0)}
{[
- totalNetworkFee?.valueInCurrency
+ totalNetworkFee?.valueInCurrency &&
+ sentAmount?.valueInCurrency
? t('quotedTotalCost', [
formatCurrencyAmount(
- totalNetworkFee.valueInCurrency,
+ totalNetworkFee.valueInCurrency.plus(
+ sentAmount.valueInCurrency,
+ ),
currency,
0,
),
From 64b683457c54415fef037543639029d573b5ef7f Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 14:20:51 -0800
Subject: [PATCH 66/69] chore: add autofocus prop to AssetPicker
---
ui/pages/bridge/prepare/bridge-input-group.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 5ed2ba7d52b4..16ecf9e1bf2b 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -160,6 +160,7 @@ export const BridgeInputGroup = ({
customTokenListGenerator={customTokenListGenerator}
isTokenListLoading={isTokenListLoading}
isMultiselectEnabled={isMultiselectEnabled}
+ autoFocus={false}
>
{(onClickHandler, networkImageSrc) =>
isAmountReadOnly && !token ? (
From dbfd3838fe27952e8060e1542cee0033bade0d07 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 15:21:37 -0800
Subject: [PATCH 67/69] fix: e2e tests
---
app/scripts/constants/sentry-state.ts | 2 ++
test/e2e/tests/metrics/errors.spec.js | 2 ++
.../errors-after-init-opt-in-background-state.json | 2 ++
3 files changed, 6 insertions(+)
diff --git a/app/scripts/constants/sentry-state.ts b/app/scripts/constants/sentry-state.ts
index c0b075c2b28b..68926c02dc89 100644
--- a/app/scripts/constants/sentry-state.ts
+++ b/app/scripts/constants/sentry-state.ts
@@ -104,8 +104,10 @@ export const SENTRY_BACKGROUND_STATE = {
},
destTokens: {},
destTopAssets: [],
+ destTokensLoadingStatus: false,
srcTokens: {},
srcTopAssets: [],
+ srcTokensLoadingStatus: false,
quoteRequest: {
walletAddress: false,
srcTokenAddress: true,
diff --git a/test/e2e/tests/metrics/errors.spec.js b/test/e2e/tests/metrics/errors.spec.js
index 7ddbe6c1117a..12a44e26fbf1 100644
--- a/test/e2e/tests/metrics/errors.spec.js
+++ b/test/e2e/tests/metrics/errors.spec.js
@@ -874,6 +874,8 @@ describe('Sentry errors', function () {
srcTokenAmount: true,
walletAddress: false,
},
+ destTokensLoadingStatus: false,
+ srcTokensLoadingStatus: false,
quotesLastFetched: true,
quotesLoadingStatus: true,
quotesRefreshCount: true,
diff --git a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
index cae1a6ae8951..b00f37993b78 100644
--- a/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
+++ b/test/e2e/tests/metrics/state-snapshots/errors-after-init-opt-in-background-state.json
@@ -76,6 +76,8 @@
"srcTokens": {},
"srcTopAssets": {},
"destTokens": {},
+ "destTokensLoadingStatus": "undefined",
+ "srcTokensLoadingStatus": "undefined",
"destTopAssets": {},
"quoteRequest": {
"srcTokenAddress": "0x0000000000000000000000000000000000000000",
From 5b891c35b16410c986c652f49bde1adbb6c26b24 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 11 Dec 2024 15:41:24 -0800
Subject: [PATCH 68/69] chore: rm TODO
---
.../component-library/avatar-network/avatar-network.scss | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/ui/components/component-library/avatar-network/avatar-network.scss b/ui/components/component-library/avatar-network/avatar-network.scss
index 72204363a9d0..a71418b3e073 100644
--- a/ui/components/component-library/avatar-network/avatar-network.scss
+++ b/ui/components/component-library/avatar-network/avatar-network.scss
@@ -4,9 +4,7 @@
}
&__network-image {
- // TODO undo this when we have new images that are more zoomed in
- // width: 100%;
- min-width: 120%;
+ width: 100%;
&--blurred {
filter: blur(8px);
From 919e1177e5e6209a78b1688117ff7d0ea3cfda3a Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 12 Dec 2024 08:42:39 -0800
Subject: [PATCH 69/69] fix: imports and input field id
---
ui/hooks/bridge/useLatestBalance.test.ts | 2 +-
ui/pages/bridge/bridge.util.test.ts | 2 +-
ui/pages/bridge/prepare/bridge-input-group.tsx | 1 +
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/ui/hooks/bridge/useLatestBalance.test.ts b/ui/hooks/bridge/useLatestBalance.test.ts
index 25f0d0936791..094f70f2233f 100644
--- a/ui/hooks/bridge/useLatestBalance.test.ts
+++ b/ui/hooks/bridge/useLatestBalance.test.ts
@@ -2,7 +2,7 @@ import { BigNumber } from 'bignumber.js';
import { renderHookWithProvider } from '../../../test/lib/render-helpers';
import { CHAIN_IDS } from '../../../shared/constants/network';
import { createBridgeMockStore } from '../../../test/jest/mock-store';
-import { zeroAddress } from '../../__mocks__/ethereumjs-util';
+import { zeroAddress } from 'ethereumjs-util';
import { createTestProviderTools } from '../../../test/stub/provider';
import * as tokenutil from '../../../shared/lib/token-util';
import useLatestBalance from './useLatestBalance';
diff --git a/ui/pages/bridge/bridge.util.test.ts b/ui/pages/bridge/bridge.util.test.ts
index cb2eed49574e..beb6c96ba770 100644
--- a/ui/pages/bridge/bridge.util.test.ts
+++ b/ui/pages/bridge/bridge.util.test.ts
@@ -1,8 +1,8 @@
+import { zeroAddress } from 'ethereumjs-util';
import fetchWithCache from '../../../shared/lib/fetch-with-cache';
import { CHAIN_IDS } from '../../../shared/constants/network';
import mockBridgeQuotesErc20Erc20 from '../../../test/data/bridge/mock-quotes-erc20-erc20.json';
import mockBridgeQuotesNativeErc20 from '../../../test/data/bridge/mock-quotes-native-erc20.json';
-import { zeroAddress } from '../../__mocks__/ethereumjs-util';
import {
fetchBridgeFeatureFlags,
fetchBridgeQuotes,
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 16ecf9e1bf2b..c7dac296025a 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -103,6 +103,7 @@ export const BridgeInputGroup = ({