From 24dbe308e6f21c3436aea637c58ecdef53b86a48 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 24 Oct 2024 16:44:27 -0700
Subject: [PATCH 01/25] chore: fetch dest token exchange rates and save to
bridge state
---
ui/ducks/bridge/actions.ts | 3 +-
ui/ducks/bridge/bridge.test.ts | 91 +++++++++++++++++++
ui/ducks/bridge/bridge.ts | 27 +++++-
.../bridge/prepare/prepare-bridge-page.tsx | 31 ++++++-
4 files changed, 148 insertions(+), 4 deletions(-)
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index a61d2fdcd8fd..e8c1b5d7c5b9 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -11,7 +11,7 @@ import { forceUpdateMetamaskState } from '../../store/actions';
import { submitRequestToBackground } from '../../store/background-connection';
import { QuoteRequest } from '../../pages/bridge/types';
import { MetaMaskReduxDispatch } from '../../store/store';
-import { bridgeSlice } from './bridge';
+import { bridgeSlice, setDestTokenExchangeRates } from './bridge';
const {
setToChainId,
@@ -27,6 +27,7 @@ export {
setToToken,
setFromToken,
setFromTokenInputValue,
+ setDestTokenExchangeRates,
};
const callBridgeControllerMethod = (
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index dc9596fcafba..daaf8f9c843b 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -10,6 +10,7 @@ import {
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
} from '../../../app/scripts/controllers/bridge/types';
+import * as util from '../../helpers/utils/util';
import bridgeReducer from './bridge';
import {
setBridgeFeatureFlags,
@@ -22,6 +23,7 @@ import {
setToChainId,
updateQuoteRequestParams,
resetBridgeState,
+ setDestTokenExchangeRates,
} from './actions';
const middleware = [thunk];
@@ -147,6 +149,7 @@ describe('Ducks - Bridge', () => {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ toTokenExchangeRate: null,
});
});
});
@@ -205,6 +208,94 @@ describe('Ducks - Bridge', () => {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ toTokenExchangeRate: null,
+ });
+ });
+ });
+ 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
+ const mockStore = configureMockStore(middleware)(
+ createBridgeMockStore({}, {}),
+ );
+ const state = mockStore.getState().bridge;
+ const fetchTokenExchangeRatesSpy = jest
+ .spyOn(util, 'fetchTokenExchangeRates')
+ .mockResolvedValue({
+ '0x0000000000000000000000000000000000000000': 0.356628,
+ });
+
+ await mockStore.dispatch(
+ setDestTokenExchangeRates({
+ chainId: CHAIN_IDS.LINEA_MAINNET,
+ tokenAddress: zeroAddress(),
+ currency: 'usd',
+ }) as never,
+ );
+
+ expect(fetchTokenExchangeRatesSpy).toHaveBeenCalledTimes(1);
+ expect(fetchTokenExchangeRatesSpy).toHaveBeenCalledWith(
+ 'usd',
+ ['0x0000000000000000000000000000000000000000'],
+ CHAIN_IDS.LINEA_MAINNET,
+ );
+
+ const actions = mockStore.getActions();
+ expect(actions).toHaveLength(2);
+ expect(actions[0].type).toStrictEqual(
+ 'bridge/setDestTokenExchangeRates/pending',
+ );
+ expect(actions[1].type).toStrictEqual(
+ 'bridge/setDestTokenExchangeRates/fulfilled',
+ );
+ const newState = bridgeReducer(state, actions[1]);
+ expect(newState).toStrictEqual({
+ toChainId: null,
+ toTokenExchangeRate: 0.356628,
+ });
+ });
+
+ it('fetches token prices and updates dest exchange rates in state, erc20 dest token', async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const mockStore = configureMockStore(middleware)(
+ createBridgeMockStore({}, {}),
+ );
+ const state = mockStore.getState().bridge;
+ const fetchTokenExchangeRatesSpy = jest
+ .spyOn(util, 'fetchTokenExchangeRates')
+ .mockResolvedValue({
+ '0x0000000000000000000000000000000000000000': 0.356628,
+ '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359': 0.999881,
+ });
+
+ await mockStore.dispatch(
+ setDestTokenExchangeRates({
+ chainId: CHAIN_IDS.LINEA_MAINNET,
+ tokenAddress:
+ '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359'.toLowerCase(),
+ currency: 'usd',
+ }) as never,
+ );
+
+ expect(fetchTokenExchangeRatesSpy).toHaveBeenCalledTimes(1);
+ expect(fetchTokenExchangeRatesSpy).toHaveBeenCalledWith(
+ 'usd',
+ ['0x3c499c542cef5e3811e1192ce70d8cc03d5c3359'],
+ CHAIN_IDS.LINEA_MAINNET,
+ );
+
+ const actions = mockStore.getActions();
+ expect(actions).toHaveLength(2);
+ expect(actions[0].type).toStrictEqual(
+ 'bridge/setDestTokenExchangeRates/pending',
+ );
+ expect(actions[1].type).toStrictEqual(
+ 'bridge/setDestTokenExchangeRates/fulfilled',
+ );
+ const newState = bridgeReducer(state, actions[1]);
+ expect(newState).toStrictEqual({
+ toChainId: null,
+ toTokenExchangeRate: 0.999881,
});
});
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index c75030c7591d..487fc2a985b6 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -1,15 +1,17 @@
-import { createSlice } from '@reduxjs/toolkit';
-
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { Hex } from '@metamask/utils';
+import { getAddress } from 'ethers/lib/utils';
import { swapsSlice } from '../swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { SwapsEthToken } from '../../selectors';
+import { fetchTokenExchangeRates } from '../../helpers/utils/util';
export type BridgeState = {
toChainId: Hex | null;
fromToken: SwapsTokenObject | SwapsEthToken | null;
toToken: SwapsTokenObject | SwapsEthToken | null;
fromTokenInputValue: string | null;
+ toTokenExchangeRate: number | null;
};
const initialState: BridgeState = {
@@ -17,8 +19,24 @@ const initialState: BridgeState = {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ toTokenExchangeRate: null,
};
+export const setDestTokenExchangeRates = createAsyncThunk(
+ 'bridge/setDestTokenExchangeRates',
+ async (request: { chainId: Hex; tokenAddress: string; currency: string }) => {
+ const { chainId, tokenAddress, currency } = request;
+ const exchangeRates = await fetchTokenExchangeRates(
+ currency,
+ [tokenAddress],
+ chainId,
+ );
+ return {
+ toTokenExchangeRate: exchangeRates?.[getAddress(tokenAddress)],
+ };
+ },
+);
+
const bridgeSlice = createSlice({
name: 'bridge',
initialState: { ...initialState },
@@ -40,6 +58,11 @@ const bridgeSlice = createSlice({
...initialState,
}),
},
+ extraReducers: (builder) => {
+ builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
+ state.toTokenExchangeRate = action.payload.toTokenExchangeRate ?? null;
+ });
+ },
});
export { bridgeSlice };
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index b0553407686d..8cd40f7da0d6 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -2,7 +2,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import classnames from 'classnames';
import { debounce } from 'lodash';
+import { Hex } from '@metamask/utils';
import {
+ setDestTokenExchangeRates,
setFromChain,
setFromToken,
setFromTokenInputValue,
@@ -42,6 +44,8 @@ import { calcTokenValue } from '../../../../shared/lib/swaps-utils';
import { BridgeQuoteCard } from '../quotes/bridge-quote-card';
import { isValidQuoteRequest } from '../utils/quote';
import { getProviderConfig } from '../../../../shared/modules/selectors/networks';
+import { getCurrentCurrency } from '../../../selectors';
+import { SECOND } from '../../../../shared/constants/time';
import { BridgeInputGroup } from './bridge-input-group';
const PrepareBridgePage = () => {
@@ -49,6 +53,8 @@ const PrepareBridgePage = () => {
const t = useI18nContext();
+ const currentCurrency = useSelector(getCurrentCurrency);
+
const fromToken = useSelector(getFromToken);
const fromTokens = useSelector(getFromTokens);
const fromTopAssets = useSelector(getFromTopAssets);
@@ -125,6 +131,18 @@ const PrepareBridgePage = () => {
debouncedUpdateQuoteRequestInController(quoteParams);
}, Object.values(quoteParams));
+ const debouncedFetchToExchangeRate = debounce(
+ async (toChainId: Hex, toTokenAddress: string) =>
+ dispatch(
+ setDestTokenExchangeRates({
+ chainId: toChainId,
+ tokenAddress: toTokenAddress,
+ currency: currentCurrency,
+ }),
+ ),
+ SECOND,
+ );
+
return (
@@ -192,6 +210,12 @@ const PrepareBridgePage = () => {
fromChain?.chainId && dispatch(setToChain(fromChain.chainId));
fromChain?.chainId && dispatch(setToChainId(fromChain.chainId));
dispatch(setToToken(fromToken));
+ fromChain?.chainId &&
+ fromToken?.address &&
+ debouncedFetchToExchangeRate(
+ fromChain.chainId,
+ fromToken.address,
+ );
}}
/>
@@ -200,7 +224,12 @@ const PrepareBridgePage = () => {
className="bridge-box"
header={t('bridgeTo')}
token={toToken}
- onAssetChange={(token) => dispatch(setToToken(token))}
+ onAssetChange={(token) => {
+ dispatch(setToToken(token));
+ toChain?.chainId &&
+ token?.address &&
+ debouncedFetchToExchangeRate(toChain.chainId, token.address);
+ }}
networkProps={{
network: toChain,
networks: toChains,
From 832dc616a587914e3308c27e81d573b82e8c107d Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 5 Nov 2024 15:02:07 -0800
Subject: [PATCH 02/25] chore: fetch src token exchange rate
---
ui/ducks/bridge/actions.ts | 7 +++-
ui/ducks/bridge/bridge.test.ts | 2 ++
ui/ducks/bridge/bridge.ts | 18 +++++++++++
ui/pages/bridge/index.tsx | 28 ++++++++++++----
.../bridge/prepare/prepare-bridge-page.tsx | 32 +++++++++++++------
5 files changed, 71 insertions(+), 16 deletions(-)
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index e8c1b5d7c5b9..5c2c9aa71f88 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -11,7 +11,11 @@ import { forceUpdateMetamaskState } from '../../store/actions';
import { submitRequestToBackground } from '../../store/background-connection';
import { QuoteRequest } from '../../pages/bridge/types';
import { MetaMaskReduxDispatch } from '../../store/store';
-import { bridgeSlice, setDestTokenExchangeRates } from './bridge';
+import {
+ bridgeSlice,
+ setDestTokenExchangeRates,
+ setSrcTokenExchangeRates,
+} from './bridge';
const {
setToChainId,
@@ -28,6 +32,7 @@ export {
setFromToken,
setFromTokenInputValue,
setDestTokenExchangeRates,
+ setSrcTokenExchangeRates,
};
const callBridgeControllerMethod = (
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index daaf8f9c843b..2d05c8b31a91 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -150,6 +150,7 @@ describe('Ducks - Bridge', () => {
toToken: null,
fromTokenInputValue: null,
toTokenExchangeRate: null,
+ fromTokenExchangeRate: null,
});
});
});
@@ -209,6 +210,7 @@ describe('Ducks - Bridge', () => {
toToken: null,
fromTokenInputValue: null,
toTokenExchangeRate: null,
+ fromTokenExchangeRate: null,
});
});
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 487fc2a985b6..7feeeac78083 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -11,6 +11,7 @@ export type BridgeState = {
fromToken: SwapsTokenObject | SwapsEthToken | null;
toToken: SwapsTokenObject | SwapsEthToken | null;
fromTokenInputValue: string | null;
+ fromTokenExchangeRate: number | null;
toTokenExchangeRate: number | null;
};
@@ -19,9 +20,23 @@ const initialState: BridgeState = {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ fromTokenExchangeRate: null,
toTokenExchangeRate: null,
};
+export const setSrcTokenExchangeRates = createAsyncThunk(
+ 'bridge/setSrcTokenExchangeRates',
+ async (request: { chainId: Hex; tokenAddress: string; currency: string }) => {
+ const { chainId, tokenAddress, currency } = request;
+ const exchangeRates = await fetchTokenExchangeRates(
+ currency,
+ [tokenAddress],
+ chainId,
+ );
+ return exchangeRates?.[getAddress(tokenAddress)];
+ },
+);
+
export const setDestTokenExchangeRates = createAsyncThunk(
'bridge/setDestTokenExchangeRates',
async (request: { chainId: Hex; tokenAddress: string; currency: string }) => {
@@ -62,6 +77,9 @@ const bridgeSlice = createSlice({
builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
state.toTokenExchangeRate = action.payload.toTokenExchangeRate ?? null;
});
+ builder.addCase(setSrcTokenExchangeRates.fulfilled, (state, action) => {
+ state.fromTokenExchangeRate = action.payload ?? null;
+ });
},
});
diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx
index 687057094005..8c87f1bdae5e 100644
--- a/ui/pages/bridge/index.tsx
+++ b/ui/pages/bridge/index.tsx
@@ -1,6 +1,7 @@
import React, { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Switch, useHistory } from 'react-router-dom';
+import { zeroAddress } from 'ethereumjs-util';
import { I18nContext } from '../../contexts/i18n';
import { clearSwapsState } from '../../ducks/swaps/swaps';
import {
@@ -16,16 +17,24 @@ import {
ButtonIconSize,
IconName,
} from '../../components/component-library';
-import { getIsBridgeChain, getIsBridgeEnabled } from '../../selectors';
import { getProviderConfig } from '../../../shared/modules/selectors/networks';
+import {
+ getCurrentCurrency,
+ getIsBridgeChain,
+ getIsBridgeEnabled,
+} from '../../selectors';
import useBridging from '../../hooks/bridge/useBridging';
import {
Content,
Footer,
Header,
} from '../../components/multichain/pages/page';
-import { resetBridgeState, setFromChain } from '../../ducks/bridge/actions';
import { useSwapsFeatureFlags } from '../swaps/hooks/useSwapsFeatureFlags';
+import {
+ resetBridgeState,
+ setFromChain,
+ setSrcTokenExchangeRates,
+} from '../../ducks/bridge/actions';
import PrepareBridgePage from './prepare/prepare-bridge-page';
import { BridgeCTAButton } from './prepare/bridge-cta-button';
@@ -42,13 +51,20 @@ const CrossChainSwap = () => {
const isBridgeEnabled = useSelector(getIsBridgeEnabled);
const providerConfig = useSelector(getProviderConfig);
const isBridgeChain = useSelector(getIsBridgeChain);
+ const currency = useSelector(getCurrentCurrency);
useEffect(() => {
- isBridgeChain &&
- isBridgeEnabled &&
- providerConfig &&
+ if (isBridgeChain && isBridgeEnabled && providerConfig && currency) {
dispatch(setFromChain(providerConfig.chainId));
- }, [isBridgeChain, isBridgeEnabled, providerConfig]);
+ dispatch(
+ setSrcTokenExchangeRates({
+ chainId: providerConfig.chainId,
+ tokenAddress: zeroAddress(),
+ currency,
+ }),
+ );
+ }
+ }, [isBridgeChain, isBridgeEnabled, providerConfig, currency]);
const resetControllerAndInputStates = async () => {
await dispatch(resetBridgeState());
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 8cd40f7da0d6..1044872c504f 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -3,11 +3,13 @@ import { useSelector, useDispatch } from 'react-redux';
import classnames from 'classnames';
import { debounce } from 'lodash';
import { Hex } from '@metamask/utils';
+import { zeroAddress } from 'ethereumjs-util';
import {
setDestTokenExchangeRates,
setFromChain,
setFromToken,
setFromTokenInputValue,
+ setSrcTokenExchangeRates,
setToChain,
setToChainId,
setToToken,
@@ -53,7 +55,7 @@ const PrepareBridgePage = () => {
const t = useI18nContext();
- const currentCurrency = useSelector(getCurrentCurrency);
+ const currency = useSelector(getCurrentCurrency);
const fromToken = useSelector(getFromToken);
const fromTokens = useSelector(getFromTokens);
@@ -131,15 +133,17 @@ const PrepareBridgePage = () => {
debouncedUpdateQuoteRequestInController(quoteParams);
}, Object.values(quoteParams));
+ const debouncedFetchFromExchangeRate = debounce(
+ (chainId: Hex, tokenAddress: string) => {
+ dispatch(setSrcTokenExchangeRates({ chainId, tokenAddress, currency }));
+ },
+ SECOND,
+ );
+
const debouncedFetchToExchangeRate = debounce(
- async (toChainId: Hex, toTokenAddress: string) =>
- dispatch(
- setDestTokenExchangeRates({
- chainId: toChainId,
- tokenAddress: toTokenAddress,
- currency: currentCurrency,
- }),
- ),
+ (chainId: Hex, tokenAddress: string) => {
+ dispatch(setDestTokenExchangeRates({ chainId, tokenAddress, currency }));
+ },
SECOND,
);
@@ -156,6 +160,9 @@ const PrepareBridgePage = () => {
onAssetChange={(token) => {
dispatch(setFromToken(token));
dispatch(setFromTokenInputValue(null));
+ fromChain?.chainId &&
+ token?.address &&
+ debouncedFetchFromExchangeRate(fromChain.chainId, token.address);
}}
networkProps={{
network: fromChain,
@@ -216,6 +223,13 @@ const PrepareBridgePage = () => {
fromChain.chainId,
fromToken.address,
);
+ toChain?.chainId &&
+ toToken?.address &&
+ toToken.address !== zeroAddress() &&
+ debouncedFetchFromExchangeRate(
+ toChain.chainId,
+ toToken.address,
+ );
}}
/>
From 359a5b2ca7ae18380c2e0d80c3e93d928d102a90 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 25 Oct 2024 11:47:17 -0700
Subject: [PATCH 03/25] chore: calculate quote metadata in selectors
---
shared/constants/bridge.ts | 4 +
.../data/bridge/mock-quotes-native-erc20.json | 2 +-
test/jest/mock-store.js | 9 +
ui/ducks/bridge/bridge.test.ts | 1 +
ui/ducks/bridge/bridge.ts | 6 +
ui/ducks/bridge/selectors.test.ts | 192 ++++++++++++-
ui/ducks/bridge/selectors.ts | 227 +++++++++++++---
ui/pages/bridge/index.tsx | 4 +
ui/pages/bridge/prepare/bridge-cta-button.tsx | 16 +-
.../bridge/prepare/bridge-input-group.tsx | 12 +-
.../bridge-quote-card.test.tsx.snap | 12 +-
.../bridge-quotes-modal.test.tsx.snap | 4 +-
ui/pages/bridge/quotes/bridge-quote-card.tsx | 31 ++-
.../bridge/quotes/bridge-quotes-modal.tsx | 19 +-
ui/pages/bridge/types.ts | 18 ++
ui/pages/bridge/utils/quote.test.ts | 252 ++++++++++++++++++
ui/pages/bridge/utils/quote.ts | 157 +++++++++--
17 files changed, 865 insertions(+), 101 deletions(-)
create mode 100644 ui/pages/bridge/utils/quote.test.ts
diff --git a/shared/constants/bridge.ts b/shared/constants/bridge.ts
index 10f2587d3fbd..8ad27dce4944 100644
--- a/shared/constants/bridge.ts
+++ b/shared/constants/bridge.ts
@@ -26,3 +26,7 @@ export const BRIDGE_CLIENT_ID = 'extension';
export const ETH_USDT_ADDRESS = '0xdac17f958d2ee523a2206206994597c13d831ec7';
export const METABRIDGE_ETHEREUM_ADDRESS =
'0x0439e60F02a8900a951603950d8D4527f400C3f1';
+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';
diff --git a/test/data/bridge/mock-quotes-native-erc20.json b/test/data/bridge/mock-quotes-native-erc20.json
index fb6ecfcc0b73..f7efe7950ba0 100644
--- a/test/data/bridge/mock-quotes-native-erc20.json
+++ b/test/data/bridge/mock-quotes-native-erc20.json
@@ -289,6 +289,6 @@
"data": "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002714711487800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b657441646170746572563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000008900000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c499c542cef5e3811e1192ce70d8cc03d5c33590000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000004f94ae6af800000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000c6437c6145a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc4123506490000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001960000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000018c0000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000084ad69fa4f00000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000904ee8f0b86000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc156080000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000828415565b0000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000001734d0800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000005e0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000023375dc15608000000000000000000000000000000000000000000000000000000000000000011000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000002e00000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000012556e69737761705633000000000000000000000000000000000000000000000000000000000000000023375dc1560800000000000000000000000000000000000000000000000000000000000173dbd3000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002b42000000000000000000000000000000000000060001f40b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000008ecb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce5000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000020000000000000000000000004200000000000000000000000000000000000006000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000000000869584cd00000000000000000000000010000000000000000000000000000000000000110000000000000000000000000000000000000000974132b87a5cb75e32f034280000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003f9e43204a24f476db20f2518722627a122d31a1bc7c63fc15412e6a327295a9460b76bea5bb53b1f73fa6a15811055f6bada592d2e9e6c8cf48a855ce6968951c",
"gasLimit": 664389
},
- "estimatedProcessingTimeInSeconds": 1560
+ "estimatedProcessingTimeInSeconds": 15
}
]
diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js
index 4720bf427372..f75b0df8bd09 100644
--- a/test/jest/mock-store.js
+++ b/test/jest/mock-store.js
@@ -719,6 +719,15 @@ export const createBridgeMockStore = (
{ chainId: CHAIN_IDS.MAINNET },
{ chainId: CHAIN_IDS.LINEA_MAINNET },
),
+ gasFeeEstimates: {
+ high: {
+ suggestedMaxFeePerGas: '0.00010456',
+ suggestedMaxPriorityFeePerGas: '0.0001',
+ },
+ },
+ currencyRates: {
+ ETH: { conversionRate: 2524.25 },
+ },
...metamaskStateOverrides,
bridgeState: {
...(swapsStore.metamask.bridgeState ?? {}),
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index 2d05c8b31a91..a972d074f512 100644
--- a/ui/ducks/bridge/bridge.test.ts
+++ b/ui/ducks/bridge/bridge.test.ts
@@ -149,6 +149,7 @@ describe('Ducks - Bridge', () => {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ sortOrder: 0,
toTokenExchangeRate: null,
fromTokenExchangeRate: null,
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 7feeeac78083..22a780081499 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -5,6 +5,7 @@ import { swapsSlice } from '../swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { SwapsEthToken } from '../../selectors';
import { fetchTokenExchangeRates } from '../../helpers/utils/util';
+import { SortOrder } from '../../pages/bridge/types';
export type BridgeState = {
toChainId: Hex | null;
@@ -13,6 +14,7 @@ export type BridgeState = {
fromTokenInputValue: string | null;
fromTokenExchangeRate: number | null;
toTokenExchangeRate: number | null;
+ sortOrder: SortOrder;
};
const initialState: BridgeState = {
@@ -22,6 +24,7 @@ const initialState: BridgeState = {
fromTokenInputValue: null,
fromTokenExchangeRate: null,
toTokenExchangeRate: null,
+ sortOrder: SortOrder.COST_ASC,
};
export const setSrcTokenExchangeRates = createAsyncThunk(
@@ -72,6 +75,9 @@ const bridgeSlice = createSlice({
resetInputFields: () => ({
...initialState,
}),
+ setSortOrder: (state, action) => {
+ state.sortOrder = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index e39f73f2fa15..416959b34d66 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -4,9 +4,14 @@ import {
CHAIN_IDS,
FEATURED_RPCS,
} from '../../../shared/constants/network';
-import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge';
+import {
+ ALLOWED_BRIDGE_CHAIN_IDS,
+ BRIDGE_QUOTE_MAX_ETA_SECONDS,
+} 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';
+import { SortOrder } from '../../pages/bridge/types';
import {
getAllBridgeableNetworks,
getBridgeQuotes,
@@ -570,4 +575,189 @@ describe('Bridge selectors', () => {
});
});
});
+
+ describe('getBridgeQuotes', () => {
+ it('should return empty values when quotes are not present', () => {
+ const state = createBridgeMockStore();
+
+ const result = getBridgeQuotes(state as never);
+
+ expect(result).toStrictEqual({
+ activeQuote: undefined,
+ isLoading: false,
+ quotesLastFetchedMs: undefined,
+ recommendedQuote: undefined,
+ sortedQuotes: [],
+ });
+ });
+
+ it('should sort quotes by adjustedReturn', () => {
+ const state = createBridgeMockStore({
+ bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20 },
+ });
+
+ const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
+ state as never,
+ );
+
+ const quoteMetadataKeys = [
+ 'adjustedReturn',
+ 'toTokenAmount',
+ 'sentAmount',
+ 'totalNetworkFee',
+ 'swapRate',
+ ];
+ expect(
+ quoteMetadataKeys.every((k) =>
+ Object.keys(activeQuote ?? {}).includes(k),
+ ),
+ ).toBe(true);
+ expect(activeQuote?.quote.requestId).toStrictEqual(
+ '381c23bc-e3e4-48fe-bc53-257471e388ad',
+ );
+ expect(recommendedQuote?.quote.requestId).toStrictEqual(
+ '381c23bc-e3e4-48fe-bc53-257471e388ad',
+ );
+ expect(sortedQuotes).toHaveLength(2);
+ sortedQuotes.forEach((quote, idx) => {
+ expect(
+ quoteMetadataKeys.every((k) => Object.keys(quote ?? {}).includes(k)),
+ ).toBe(true);
+ expect(quote?.quote.requestId).toStrictEqual(
+ mockBridgeQuotesNativeErc20[idx]?.quote.requestId,
+ );
+ });
+ });
+
+ it('should sort quotes by ETA', () => {
+ const state = createBridgeMockStore({
+ bridgeSliceOverrides: { sortOrder: SortOrder.ETA_ASC },
+ bridgeStateOverrides: {
+ quotes: [
+ ...mockBridgeQuotesNativeErc20,
+ {
+ ...mockBridgeQuotesNativeErc20[0],
+ estimatedProcessingTimeInSeconds: 1,
+ quote: {
+ ...mockBridgeQuotesNativeErc20[0].quote,
+ requestId: 'fastestQuote',
+ },
+ },
+ ],
+ },
+ });
+
+ const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
+ state as never,
+ );
+
+ expect(activeQuote?.quote.requestId).toStrictEqual('fastestQuote');
+ expect(recommendedQuote?.quote.requestId).toStrictEqual('fastestQuote');
+ expect(sortedQuotes).toHaveLength(3);
+ expect(sortedQuotes[0]?.quote.requestId).toStrictEqual('fastestQuote');
+ expect(sortedQuotes[1]?.quote.requestId).toStrictEqual(
+ mockBridgeQuotesNativeErc20[1]?.quote.requestId,
+ );
+ expect(sortedQuotes[2]?.quote.requestId).toStrictEqual(
+ 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({
+ bridgeSliceOverrides: {
+ sortOrder: SortOrder.ETA_ASC,
+ toTokenExchangeRate: 0.998781,
+ toNativeExchangeRate: 0.354073,
+ },
+ bridgeStateOverrides: {
+ quotes: [
+ ...mockBridgeQuotesNativeErc20,
+ {
+ ...mockBridgeQuotesNativeErc20[0],
+ estimatedProcessingTimeInSeconds: 1,
+ quote: {
+ ...mockBridgeQuotesNativeErc20[0].quote,
+ requestId: 'fastestQuote',
+ destTokenAmount: '1',
+ },
+ },
+ ],
+ },
+ });
+
+ 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?.fiat?.toString()).toStrictEqual('25.2425');
+ expect(totalNetworkFee?.fiat?.toString()).toStrictEqual(
+ '2.52459306428938562',
+ );
+ expect(toTokenAmount?.fiat?.toString()).toStrictEqual('24.226654664163');
+ expect(adjustedReturn?.fiat?.toString()).toStrictEqual(
+ '21.70206159987361438',
+ );
+ expect(cost?.fiat?.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',
+ );
+ });
+ });
});
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 86f4c8155b17..187e9a0a8230 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -1,12 +1,22 @@
-import { NetworkConfiguration } from '@metamask/network-controller';
-import { uniqBy } from 'lodash';
+import {
+ NetworkConfiguration,
+ NetworkState,
+} from '@metamask/network-controller';
+import { orderBy, uniqBy } from 'lodash';
import { createSelector } from 'reselect';
+import { GasFeeEstimates } from '@metamask/gas-fee-controller';
+import { BigNumber } from 'bignumber.js';
import {
getIsBridgeEnabled,
getSwapsDefaultToken,
SwapsEthToken,
} from '../../selectors/selectors';
-import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../../shared/constants/bridge';
+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 {
BridgeControllerState,
BridgeFeatureFlagsKey,
@@ -15,21 +25,37 @@ import {
} from '../../../app/scripts/controllers/bridge/types';
import { createDeepEqualSelector } from '../../../shared/modules/selectors/util';
import {
- NetworkState,
getProviderConfig,
getNetworkConfigurationsByChainId,
} from '../../../shared/modules/selectors/networks';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
+import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants';
+import {
+ QuoteMetadata,
+ QuoteResponse,
+ SortOrder,
+} from '../../pages/bridge/types';
+import {
+ calcAdjustedReturn,
+ calcCost,
+ calcSentAmount,
+ calcSwapRate,
+ calcToAmount,
+ calcTotalNetworkFee,
+ isNativeAddress,
+} from '../../pages/bridge/utils/quote';
+import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
import { BridgeState } from './bridge';
-type BridgeAppState = NetworkState & {
- metamask: { bridgeState: BridgeControllerState } & {
- useExternalServices: boolean;
- };
+type BridgeAppState = {
+ metamask: { bridgeState: BridgeControllerState } & NetworkState & {
+ useExternalServices: boolean;
+ currencyRates: { [currency: string]: { conversionRate: number } };
+ };
bridge: BridgeState;
};
@@ -140,46 +166,179 @@ export const getBridgeQuotesConfig = (state: BridgeAppState) =>
BridgeFeatureFlagsKey.EXTENSION_CONFIG
] ?? {};
+const _getBridgeFeesPerGas = createSelector(
+ getGasFeeEstimates,
+ (gasFeeEstimates) => ({
+ estimatedBaseFeeInDecGwei: (gasFeeEstimates as GasFeeEstimates)
+ ?.estimatedBaseFee,
+ maxPriorityFeePerGasInDecGwei: (gasFeeEstimates as GasFeeEstimates)?.[
+ BRIDGE_PREFERRED_GAS_ESTIMATE
+ ]?.suggestedMaxPriorityFeePerGas,
+ maxFeePerGas: decGWEIToHexWEI(
+ (gasFeeEstimates as GasFeeEstimates)?.high?.suggestedMaxFeePerGas,
+ ),
+ maxPriorityFeePerGas: decGWEIToHexWEI(
+ (gasFeeEstimates as GasFeeEstimates)?.high?.suggestedMaxPriorityFeePerGas,
+ ),
+ }),
+);
+
+const _getBridgeSortOrder = (state: BridgeAppState) => state.bridge.sortOrder;
+
+// A dest network can be selected before it's imported
+// The cached exchange rate won't be available so the rate from the bridge state is used
+const _getToTokenExchangeRate = createSelector(
+ (state) => state.metamask.currencyRates,
+ (state: BridgeAppState) => state.bridge.toTokenExchangeRate,
+ getToChain,
+ getToToken,
+ (cachedCurrencyRates, toTokenExchangeRate, toChain, toToken) => {
+ return (
+ toTokenExchangeRate ??
+ (isNativeAddress(toToken?.address) && toChain?.nativeCurrency
+ ? cachedCurrencyRates[toChain.nativeCurrency]?.conversionRate
+ : null)
+ );
+ },
+);
+
+const _getQuotesWithMetadata = createDeepEqualSelector(
+ (state) => state.metamask.bridgeState.quotes,
+ _getToTokenExchangeRate,
+ (state: BridgeAppState) => state.bridge.fromTokenExchangeRate,
+ getConversionRate,
+ _getBridgeFeesPerGas,
+ (
+ quotes,
+ toTokenExchangeRate,
+ fromTokenExchangeRate,
+ nativeExchangeRate,
+ { estimatedBaseFeeInDecGwei, maxPriorityFeePerGasInDecGwei },
+ ): (QuoteResponse & QuoteMetadata)[] => {
+ return quotes.map((quote: QuoteResponse) => {
+ const toTokenAmount = calcToAmount(quote.quote, toTokenExchangeRate);
+ const totalNetworkFee = calcTotalNetworkFee(
+ quote,
+ estimatedBaseFeeInDecGwei,
+ maxPriorityFeePerGasInDecGwei,
+ nativeExchangeRate,
+ );
+ const sentAmount = calcSentAmount(
+ quote.quote,
+ isNativeAddress(quote.quote.srcAsset.address)
+ ? nativeExchangeRate
+ : fromTokenExchangeRate,
+ );
+ const adjustedReturn = calcAdjustedReturn(
+ toTokenAmount.fiat,
+ totalNetworkFee.fiat,
+ );
+
+ return {
+ ...quote,
+ toTokenAmount,
+ sentAmount,
+ totalNetworkFee,
+ adjustedReturn,
+ swapRate: calcSwapRate(sentAmount.raw, toTokenAmount.raw),
+ cost: calcCost(adjustedReturn.fiat, sentAmount.fiat),
+ };
+ });
+ },
+);
+
+const _getSortedQuotesWithMetadata = createDeepEqualSelector(
+ _getQuotesWithMetadata,
+ _getBridgeSortOrder,
+ (quotesWithMetadata, sortOrder) => {
+ switch (sortOrder) {
+ case SortOrder.ETA_ASC:
+ return orderBy(
+ quotesWithMetadata,
+ (quote) => quote.estimatedProcessingTimeInSeconds,
+ 'asc',
+ );
+ case SortOrder.COST_ASC:
+ default:
+ return orderBy(quotesWithMetadata, ({ cost }) => cost.fiat, 'asc');
+ }
+ },
+);
+
+const _getRecommendedQuote = createDeepEqualSelector(
+ _getSortedQuotesWithMetadata,
+ _getBridgeSortOrder,
+ (sortedQuotesWithMetadata, sortOrder) => {
+ if (!sortedQuotesWithMetadata.length) {
+ return undefined;
+ }
+
+ const bestReturnValue = BigNumber.max(
+ sortedQuotesWithMetadata.map(
+ ({ adjustedReturn }) => adjustedReturn.fiat ?? 0,
+ ),
+ );
+
+ const isFastestQuoteValueReasonable = (
+ adjustedReturnInFiat: BigNumber | null,
+ ) =>
+ adjustedReturnInFiat
+ ? adjustedReturnInFiat
+ .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.fiat)
+ : isBestPricedQuoteETAReasonable(
+ quote.estimatedProcessingTimeInSeconds,
+ );
+ }) ?? sortedQuotesWithMetadata[0]
+ );
+ },
+);
+
export const getBridgeQuotes = createSelector(
- (state: BridgeAppState) => state.metamask.bridgeState.quotes,
- (state: BridgeAppState) => state.metamask.bridgeState.quotesLastFetched,
- (state: BridgeAppState) =>
+ _getSortedQuotesWithMetadata,
+ _getRecommendedQuote,
+ (state) => state.metamask.bridgeState.quotesLastFetched,
+ (state) =>
state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING,
(state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount,
getBridgeQuotesConfig,
getQuoteRequest,
(
- quotes,
+ sortedQuotesWithMetadata,
+ recommendedQuote,
quotesLastFetchedMs,
isLoading,
quotesRefreshCount,
{ maxRefreshCount },
{ insufficientBal },
- ) => {
- return {
- quotes,
- quotesLastFetchedMs,
- isLoading,
- quotesRefreshCount,
- isQuoteGoingToRefresh: insufficientBal
- ? false
- : quotesRefreshCount < maxRefreshCount,
- };
- },
-);
-
-export const getRecommendedQuote = createSelector(
- getBridgeQuotes,
- ({ quotes }) => {
- return quotes[0];
- },
+ ) => ({
+ sortedQuotes: sortedQuotesWithMetadata,
+ recommendedQuote,
+ activeQuote: recommendedQuote,
+ quotesLastFetchedMs,
+ isLoading,
+ quotesRefreshCount,
+ isQuoteGoingToRefresh: insufficientBal
+ ? false
+ : quotesRefreshCount < maxRefreshCount,
+ }),
);
-export const getToAmount = createSelector(getRecommendedQuote, (quote) =>
- quote
+export const getToAmount = createSelector(getBridgeQuotes, ({ activeQuote }) =>
+ activeQuote
? calcTokenAmount(
- quote.quote.destTokenAmount,
- quote.quote.destAsset.decimals,
+ activeQuote.quote.destTokenAmount,
+ activeQuote.quote.destAsset.decimals,
)
: undefined,
);
diff --git a/ui/pages/bridge/index.tsx b/ui/pages/bridge/index.tsx
index 8c87f1bdae5e..6dd54b424d06 100644
--- a/ui/pages/bridge/index.tsx
+++ b/ui/pages/bridge/index.tsx
@@ -35,6 +35,7 @@ import {
setFromChain,
setSrcTokenExchangeRates,
} from '../../ducks/bridge/actions';
+import { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates';
import PrepareBridgePage from './prepare/prepare-bridge-page';
import { BridgeCTAButton } from './prepare/bridge-cta-button';
@@ -82,6 +83,9 @@ const CrossChainSwap = () => {
};
}, []);
+ // Needed for refreshing gas estimates
+ useGasFeeEstimates(providerConfig?.id);
+
const redirectToDefaultRoute = async () => {
history.push({
pathname: DEFAULT_ROUTE,
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index 06d784f2e0ea..81889c037dbe 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -2,14 +2,13 @@ import React, { useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Button } from '../../../components/component-library';
import {
- getBridgeQuotes,
getFromAmount,
getFromChain,
getFromToken,
- getRecommendedQuote,
getToAmount,
getToChain,
getToToken,
+ getBridgeQuotes,
} from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import useSubmitBridgeTransaction from '../hooks/useSubmitBridgeTransaction';
@@ -27,13 +26,18 @@ export const BridgeCTAButton = () => {
const fromAmount = useSelector(getFromAmount);
const toAmount = useSelector(getToAmount);
- const { isLoading } = useSelector(getBridgeQuotes);
- const quoteResponse = useSelector(getRecommendedQuote);
+ const { isLoading, activeQuote } = useSelector(getBridgeQuotes);
const { submitBridgeTransaction } = useSubmitBridgeTransaction();
const isTxSubmittable =
- fromToken && toToken && fromChain && toChain && fromAmount && toAmount;
+ fromToken &&
+ toToken &&
+ fromChain &&
+ toChain &&
+ fromAmount &&
+ toAmount &&
+ activeQuote;
const label = useMemo(() => {
if (isLoading && !isTxSubmittable) {
@@ -59,7 +63,7 @@ export const BridgeCTAButton = () => {
data-testid="bridge-cta-button"
onClick={() => {
if (isTxSubmittable) {
- dispatch(submitBridgeTransaction(quoteResponse));
+ dispatch(submitBridgeTransaction(activeQuote));
}
}}
disabled={!isTxSubmittable}
diff --git a/ui/pages/bridge/prepare/bridge-input-group.tsx b/ui/pages/bridge/prepare/bridge-input-group.tsx
index 266af5b9a3cc..0dbecf6cffdd 100644
--- a/ui/pages/bridge/prepare/bridge-input-group.tsx
+++ b/ui/pages/bridge/prepare/bridge-input-group.tsx
@@ -28,10 +28,7 @@ import {
CHAIN_ID_TOKEN_IMAGE_MAP,
} from '../../../../shared/constants/network';
import useLatestBalance from '../../../hooks/bridge/useLatestBalance';
-import {
- getBridgeQuotes,
- getRecommendedQuote,
-} from '../../../ducks/bridge/selectors';
+import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
const generateAssetFromToken = (
chainId: Hex,
@@ -82,8 +79,7 @@ export const BridgeInputGroup = ({
>) => {
const t = useI18nContext();
- const { isLoading } = useSelector(getBridgeQuotes);
- const recommendedQuote = useSelector(getRecommendedQuote);
+ const { isLoading, activeQuote } = useSelector(getBridgeQuotes);
const tokenFiatValue = useTokenFiatAmount(
token?.address || undefined,
@@ -134,9 +130,7 @@ export const BridgeInputGroup = ({
type={TextFieldType.Number}
className="amount-input"
placeholder={
- isLoading && !recommendedQuote
- ? t('bridgeCalculatingAmount')
- : '0'
+ isLoading && !activeQuote ? t('bridgeCalculatingAmount') : '0'
}
onChange={(e) => {
onAmountChange?.(e.target.value);
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 cb7b5afb4c77..fea4026e216f 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
@@ -90,7 +90,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
- 1 USDC = 0.9989 USDC
+ 0.998877142857142857142857142857142857
@@ -131,13 +131,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.01 ETH
+ 0.00100007141025952
- $0.01
+ 2.52443025734759336
@@ -263,7 +263,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
- 1 ETH = 2465.4630 USDC
+ 2443.8902
@@ -304,13 +304,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.01 ETH
+ 0.00100012486628784
- $0.01
+ 2.52456519372708012
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 41d8a03d1ac1..8d55f0abce94 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
@@ -103,7 +103,7 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
- $0.01
+ 2.52443025734759336
- $0.01
+ 2.52445909711870752
{
const t = useI18nContext();
- const recommendedQuote = useSelector(getRecommendedQuote);
- const { isLoading, isQuoteGoingToRefresh } = useSelector(getBridgeQuotes);
-
- const { etaInMinutes, totalFees, quoteRate } =
- getQuoteDisplayData(recommendedQuote);
+ const { isLoading, isQuoteGoingToRefresh, activeQuote } =
+ useSelector(getBridgeQuotes);
const secondsUntilNextRefresh = useCountdownTimer();
const [showAllQuotes, setShowAllQuotes] = useState(false);
- if (isLoading && !recommendedQuote) {
+ if (isLoading && !activeQuote) {
return (
@@ -37,7 +31,7 @@ export const BridgeQuoteCard = () => {
);
}
- return etaInMinutes && totalFees && quoteRate ? (
+ return activeQuote ? (
{
+
-
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 7e78e515af6e..ebe2db59838b 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -17,17 +17,18 @@ import {
TextAlign,
TextVariant,
} from '../../../helpers/constants/design-system';
-import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
-import { getQuoteDisplayData } from '../utils/quote';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
+import { formatEtaInMinutes } from '../utils/quote';
export const BridgeQuotesModal = ({
onClose,
...modalProps
}: Omit, 'children'>) => {
- const { quotes } = useSelector(getBridgeQuotes);
const t = useI18nContext();
+ const { sortedQuotes } = useSelector(getBridgeQuotes);
+
return (
@@ -49,12 +50,16 @@ export const BridgeQuotesModal = ({
})}
- {quotes.map((quote, index) => {
- const { totalFees, etaInMinutes } = getQuoteDisplayData(quote);
+ {sortedQuotes.map((quote, index) => {
+ const { totalNetworkFee, estimatedProcessingTimeInSeconds } = quote;
return (
- {totalFees?.fiat}
- {t('bridgeTimingMinutes', [etaInMinutes])}
+ {totalNetworkFee?.fiat?.toString()}
+
+ {t('bridgeTimingMinutes', [
+ formatEtaInMinutes(estimatedProcessingTimeInSeconds),
+ ])}
+
);
})}
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index a1ee163eca48..b9bf9bf54e7c 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -1,3 +1,21 @@
+import { BigNumber } from 'bignumber.js';
+
+// Values derived from the quote response
+export type QuoteMetadata = {
+ totalNetworkFee: { raw: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
+ toTokenAmount: { raw: BigNumber; fiat: BigNumber | null };
+ adjustedReturn: { fiat: BigNumber | null }; // destTokenAmount - totalNetworkFee
+ sentAmount: { raw: BigNumber; fiat: BigNumber | null }; // srcTokenAmount + metabridgeFee
+ swapRate: BigNumber; // destTokenAmount / sentAmount
+ cost: { fiat: BigNumber | null }; // sentAmount - adjustedReturn
+};
+
+// Sort order set by the user
+export enum SortOrder {
+ COST_ASC,
+ ETA_ASC,
+}
+
// Types copied from Metabridge API
export enum BridgeFlag {
EXTENSION_CONFIG = 'extension-config',
diff --git a/ui/pages/bridge/utils/quote.test.ts b/ui/pages/bridge/utils/quote.test.ts
new file mode 100644
index 000000000000..6d03177d1fe7
--- /dev/null
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -0,0 +1,252 @@
+import { BigNumber } from 'bignumber.js';
+import { zeroAddress } from '../../../__mocks__/ethereumjs-util';
+import {
+ calcAdjustedReturn,
+ calcSentAmount,
+ calcSwapRate,
+ calcToAmount,
+ calcTotalNetworkFee,
+ formatEtaInMinutes,
+} from './quote';
+
+const ERC20_TOKEN = {
+ decimals: 6,
+ address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85'.toLowerCase(),
+};
+const NATIVE_TOKEN = { decimals: 18, address: zeroAddress() };
+
+describe('Bridge quote utils', () => {
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1009000000000000000',
+ { toTokenExchangeRate: 1, toNativeExchangeRate: 2521.73 },
+ { raw: '1.009', fiat: '2544.42557' },
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '2543140000',
+ { toTokenExchangeRate: 0.999781, toNativeExchangeRate: 0.352999 },
+ { raw: '2543.14', fiat: '2542.58305234' },
+ ],
+ [
+ 'erc20 with null exchange rates',
+ ERC20_TOKEN,
+ '2543140000',
+ { toTokenExchangeRate: null, toNativeExchangeRate: null },
+ { raw: '2543.14', fiat: undefined },
+ ],
+ ])(
+ 'calcToAmount: toToken is %s',
+ (
+ _: string,
+ destAsset: { decimals: number; address: string },
+ destTokenAmount: string,
+ {
+ toTokenExchangeRate,
+ toNativeExchangeRate,
+ }: { toTokenExchangeRate: number; toNativeExchangeRate: number },
+ { raw, fiat }: { raw: string; fiat: string },
+ ) => {
+ const result = calcToAmount(
+ {
+ destAsset,
+ destTokenAmount,
+ } as never,
+ toTokenExchangeRate,
+ toNativeExchangeRate,
+ );
+ expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1009000000000000000',
+ {
+ '0x0000000000000000000000000000000000000000': 1.0000825923770915,
+ },
+ 2515.02,
+ {
+ raw: '1.143217728',
+ fiat: '2875.21545027456',
+ },
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ {
+ '0x0b2c639c533813f4aa9d7837caf62653d097ff85': 0.999781,
+ },
+ 2517.14,
+ { raw: '100.512', fiat: '100.489987872' },
+ ],
+ [
+ 'erc20 with null exchange rates',
+ ERC20_TOKEN,
+ '2543140000',
+ {},
+ null,
+ { raw: '2543.652', fiat: undefined },
+ ],
+ ])(
+ 'calcSentAmount: fromToken is %s',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ fromTokenExchangeRates: Record,
+ fromNativeExchangeRate: number,
+ { raw, fiat }: { raw: string; fiat: string },
+ ) => {
+ const result = calcSentAmount(
+ {
+ srcAsset,
+ srcTokenAmount,
+ feeData: {
+ metabridge: {
+ amount: Math.pow(8 * 10, srcAsset.decimals / 2),
+ },
+ },
+ } as never,
+ fromTokenExchangeRates,
+ fromNativeExchangeRate,
+ );
+ expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de0b6b3a7640000',
+ { raw: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ undefined,
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { raw: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ undefined,
+ ],
+ [
+ 'erc20 with approval',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { raw: '4.4703601424e-7', fiat: '0.0011253774029680608' },
+ 1092677,
+ ],
+ [
+ 'erc20 with relayer fee',
+ ERC20_TOKEN,
+ '100000000',
+ '0x0de0b6b3a7640000',
+ { raw: '1.00000022351800712', fiat: '2517.4205626887014840304' },
+ undefined,
+ ],
+ [
+ 'native with relayer fee',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de1b6b3a7640000',
+ { raw: '0.000281698494717776', fiat: '0.70915342457242365792' },
+ undefined,
+ ],
+ ])(
+ 'calcTotalNetworkFee: fromToken is %s',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ value: string,
+ { raw, fiat }: { raw: string; fiat: string },
+ approvalGasLimit?: number,
+ ) => {
+ const feeData = { metabridge: { amount: 0 } };
+ const result = calcTotalNetworkFee(
+ {
+ trade: { value, gasLimit: 1092677 },
+ approval: approvalGasLimit
+ ? { gasLimit: approvalGasLimit }
+ : undefined,
+ quote: { srcAsset, srcTokenAmount, feeData },
+ } as never,
+ '0x19870',
+ '0x186a0',
+ 2517.42,
+ );
+ expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'available',
+ new BigNumber('100'),
+ new BigNumber('5'),
+ new BigNumber('95'),
+ ],
+ ['unavailable', null, null, null],
+ ])(
+ 'calcAdjustedReturn: fiat amounts are %s',
+ (
+ _: string,
+ destTokenAmountInFiat: BigNumber,
+ totalNetworkFeeInFiat: BigNumber,
+ fiat: string,
+ ) => {
+ const result = calcAdjustedReturn(
+ destTokenAmountInFiat,
+ totalNetworkFeeInFiat,
+ );
+ expect(result.fiat).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ ['< 1', new BigNumber('100'), new BigNumber('5'), new BigNumber('0.05')],
+ ['>= 1', new BigNumber('1'), new BigNumber('2000'), new BigNumber('2000')],
+ ['0', new BigNumber('1'), new BigNumber('0'), new BigNumber('0')],
+ ])(
+ 'calcSwapRate: %s rate',
+ (
+ _: string,
+ sentAmount: BigNumber,
+ destTokenAmount: BigNumber,
+ rate: string,
+ ) => {
+ const result = calcSwapRate(sentAmount, destTokenAmount);
+ expect(result).toStrictEqual(rate);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ ['exact', 120, '2'],
+ ['rounded down', 2000, '33'],
+ ])(
+ 'formatEtaInMinutes: %s conversion',
+ (_: string, estimatedProcessingTimeInSeconds: number, minutes: string) => {
+ const result = formatEtaInMinutes(estimatedProcessingTimeInSeconds);
+ expect(result).toStrictEqual(minutes);
+ },
+ );
+});
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index b5945f64a9df..98018fb11bd6 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -1,5 +1,13 @@
+import { zeroAddress } from 'ethereumjs-util';
+import { BigNumber } from 'bignumber.js';
import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils';
-import { QuoteResponse, QuoteRequest } from '../types';
+import { QuoteResponse, QuoteRequest, Quote } from '../types';
+import {
+ hexToDecimal,
+ sumDecimals,
+} from '../../../../shared/modules/conversion.utils';
+
+export const isNativeAddress = (address?: string) => address === zeroAddress();
export const isValidQuoteRequest = (
partialRequest: Partial,
@@ -33,27 +41,138 @@ export const isValidQuoteRequest = (
);
};
-export const getQuoteDisplayData = (quoteResponse?: QuoteResponse) => {
- const { quote, estimatedProcessingTimeInSeconds } = quoteResponse ?? {};
- if (!quoteResponse || !quote || !estimatedProcessingTimeInSeconds) {
- return {};
+export const calcToAmount = (
+ { destTokenAmount, destAsset }: Quote,
+ exchangeRate: number | null,
+) => {
+ const normalizedDestAmount = calcTokenAmount(
+ destTokenAmount,
+ destAsset.decimals,
+ );
+ return {
+ raw: normalizedDestAmount,
+ fiat: exchangeRate ? normalizedDestAmount.mul(exchangeRate) : null,
+ };
+};
+
+export const calcSentAmount = (
+ { srcTokenAmount, srcAsset, feeData }: Quote,
+ exchangeRate: number | null,
+) => {
+ const normalizedSentAmount = calcTokenAmount(
+ new BigNumber(srcTokenAmount).plus(feeData.metabridge.amount),
+ srcAsset.decimals,
+ );
+ return {
+ raw: normalizedSentAmount,
+ fiat: exchangeRate
+ ? normalizedSentAmount.mul(exchangeRate.toString())
+ : null,
+ };
+};
+
+const calcRelayerFee = (
+ bridgeQuote: QuoteResponse,
+ nativeExchangeRate?: number,
+) => {
+ const {
+ quote: { srcAsset, srcTokenAmount, feeData },
+ trade,
+ } = bridgeQuote;
+ const relayerFeeInNative = calcTokenAmount(
+ new BigNumber(hexToDecimal(trade.value)).minus(
+ isNativeAddress(srcAsset.address)
+ ? new BigNumber(srcTokenAmount).plus(feeData.metabridge.amount)
+ : 0,
+ ),
+ 18,
+ );
+ return {
+ raw: relayerFeeInNative,
+ fiat: nativeExchangeRate
+ ? relayerFeeInNative.mul(nativeExchangeRate)
+ : null,
+ };
+};
+
+const calcTotalGasFee = (
+ bridgeQuote: QuoteResponse,
+ estimatedBaseFeeInDecGwei: string,
+ maxPriorityFeePerGasInDecGwei: string,
+ nativeExchangeRate?: number,
+ l1GasInDecGwei?: BigNumber,
+) => {
+ const { approval, trade } = bridgeQuote;
+ const totalGasLimitInDec = sumDecimals(
+ trade.gasLimit?.toString() ?? '0',
+ approval?.gasLimit?.toString() ?? '0',
+ );
+ const feePerGasInDecGwei = sumDecimals(
+ estimatedBaseFeeInDecGwei,
+ maxPriorityFeePerGasInDecGwei,
+ );
+ const gasFeesInDecGwei = totalGasLimitInDec.times(feePerGasInDecGwei);
+
+ if (l1GasInDecGwei) {
+ gasFeesInDecGwei.add(l1GasInDecGwei);
}
- const etaInMinutes = (estimatedProcessingTimeInSeconds / 60).toFixed();
- const quoteRate = `1 ${quote.srcAsset.symbol} = ${calcTokenAmount(
- quote.destTokenAmount,
- quote.destAsset.decimals,
- )
- .div(calcTokenAmount(quote.srcTokenAmount, quote.srcAsset.decimals))
- .toFixed(4)
- .toString()} ${quote.destAsset.symbol}`;
+ const gasFeesInDecEth = new BigNumber(
+ gasFeesInDecGwei.shiftedBy(9).toString(),
+ );
+ const gasFeesInUSD = nativeExchangeRate
+ ? gasFeesInDecEth.times(nativeExchangeRate)
+ : null;
+
+ return {
+ raw: gasFeesInDecEth,
+ fiat: gasFeesInUSD,
+ };
+};
+export const calcTotalNetworkFee = (
+ bridgeQuote: QuoteResponse,
+ estimatedBaseFeeInDecGwei: string,
+ maxPriorityFeePerGasInDecGwei: string,
+ nativeExchangeRate?: number,
+) => {
+ const normalizedGasFee = calcTotalGasFee(
+ bridgeQuote,
+ estimatedBaseFeeInDecGwei,
+ maxPriorityFeePerGasInDecGwei,
+ nativeExchangeRate,
+ );
+ const normalizedRelayerFee = calcRelayerFee(bridgeQuote, nativeExchangeRate);
return {
- etaInMinutes,
- totalFees: {
- amount: '0.01 ETH', // TODO implement gas + relayer fee
- fiat: '$0.01',
- },
- quoteRate,
+ raw: normalizedGasFee.raw.plus(normalizedRelayerFee.raw),
+ fiat: normalizedGasFee.fiat?.plus(normalizedRelayerFee.fiat || '0') ?? null,
};
};
+
+export const calcAdjustedReturn = (
+ destTokenAmountInFiat: BigNumber | null,
+ totalNetworkFeeInFiat: BigNumber | null,
+) => ({
+ fiat:
+ destTokenAmountInFiat && totalNetworkFeeInFiat
+ ? destTokenAmountInFiat.minus(totalNetworkFeeInFiat)
+ : null,
+});
+
+export const calcSwapRate = (
+ sentAmount: BigNumber,
+ destTokenAmount: BigNumber,
+) => destTokenAmount.div(sentAmount);
+
+export const calcCost = (
+ adjustedReturnInFiat: BigNumber | null,
+ sentAmountInFiat: BigNumber | null,
+) => ({
+ fiat:
+ adjustedReturnInFiat && sentAmountInFiat
+ ? adjustedReturnInFiat.minus(sentAmountInFiat)
+ : null,
+});
+
+export const formatEtaInMinutes = (estimatedProcessingTimeInSeconds: number) =>
+ (estimatedProcessingTimeInSeconds / 60).toFixed();
From 7fc8ec0548005a8ce7c8e6e5f0d6f66e968bb717 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 14 Nov 2024 12:37:08 -0800
Subject: [PATCH 04/25] chore: add L1 fees for Base and Optimism
---
.../bridge/bridge-controller.test.ts | 183 +++-
.../controllers/bridge/bridge-controller.ts | 80 +-
app/scripts/controllers/bridge/types.ts | 12 +-
app/scripts/metamask-controller.js | 4 +
.../data/bridge/mock-quotes-erc20-native.json | 894 ++++++++++++++++++
.../bridge/mock-quotes-native-erc20-eth.json | 258 +++++
ui/ducks/bridge/selectors.ts | 1 +
ui/pages/bridge/types.ts | 4 +
ui/pages/bridge/utils/quote.test.ts | 73 +-
ui/pages/bridge/utils/quote.ts | 29 +-
10 files changed, 1509 insertions(+), 29 deletions(-)
create mode 100644 test/data/bridge/mock-quotes-erc20-native.json
create mode 100644 test/data/bridge/mock-quotes-native-erc20-eth.json
diff --git a/app/scripts/controllers/bridge/bridge-controller.test.ts b/app/scripts/controllers/bridge/bridge-controller.test.ts
index 8369d910f78b..5cadcb1bd375 100644
--- a/app/scripts/controllers/bridge/bridge-controller.test.ts
+++ b/app/scripts/controllers/bridge/bridge-controller.test.ts
@@ -1,4 +1,6 @@
import nock from 'nock';
+import { BigNumber } from 'bignumber.js';
+import { add0x } from '@metamask/utils';
import { BRIDGE_API_BASE_URL } from '../../../../shared/constants/bridge';
import { CHAIN_IDS } from '../../../../shared/constants/network';
import { SWAPS_API_V2_BASE_URL } from '../../../../shared/constants/swaps';
@@ -7,6 +9,13 @@ import { flushPromises } from '../../../../test/lib/timer-helpers';
// eslint-disable-next-line import/no-restricted-paths
import * as bridgeUtil from '../../../../ui/pages/bridge/bridge.util';
import * as balanceUtils from '../../../../shared/modules/bridge-utils/balance';
+import mockBridgeQuotesErc20Native from '../../../../test/data/bridge/mock-quotes-erc20-native.json';
+import mockBridgeQuotesNativeErc20 from '../../../../test/data/bridge/mock-quotes-native-erc20.json';
+import mockBridgeQuotesNativeErc20Eth from '../../../../test/data/bridge/mock-quotes-native-erc20-eth.json';
+// TODO: Remove restricted import
+// eslint-disable-next-line import/no-restricted-paths
+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';
@@ -35,6 +44,7 @@ jest.mock('@ethersproject/providers', () => {
Web3Provider: jest.fn(),
};
});
+const getLayer1GasFeeMock = jest.fn();
describe('BridgeController', function () {
let bridgeController: BridgeController;
@@ -42,6 +52,7 @@ describe('BridgeController', function () {
beforeAll(function () {
bridgeController = new BridgeController({
messenger: messengerMock,
+ getLayer1GasFee: getLayer1GasFeeMock,
});
});
@@ -278,7 +289,7 @@ describe('BridgeController', function () {
.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
- resolve([1, 2, 3] as never);
+ resolve(mockBridgeQuotesNativeErc20Eth as never);
}, 5000);
});
});
@@ -286,7 +297,10 @@ describe('BridgeController', function () {
fetchBridgeQuotesSpy.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
- resolve([5, 6, 7] as never);
+ resolve([
+ ...mockBridgeQuotesNativeErc20Eth,
+ ...mockBridgeQuotesNativeErc20Eth,
+ ] as never);
}, 10000);
});
});
@@ -363,7 +377,7 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: false },
- quotes: [1, 2, 3],
+ quotes: mockBridgeQuotesNativeErc20Eth,
quotesLoadingStatus: 1,
}),
);
@@ -377,7 +391,10 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: false },
- quotes: [5, 6, 7],
+ quotes: [
+ ...mockBridgeQuotesNativeErc20Eth,
+ ...mockBridgeQuotesNativeErc20Eth,
+ ],
quotesLoadingStatus: 1,
quotesRefreshCount: 2,
}),
@@ -394,7 +411,10 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: false },
- quotes: [5, 6, 7],
+ quotes: [
+ ...mockBridgeQuotesNativeErc20Eth,
+ ...mockBridgeQuotesNativeErc20Eth,
+ ],
quotesLoadingStatus: 2,
quotesRefreshCount: 3,
}),
@@ -404,6 +424,7 @@ describe('BridgeController', function () {
);
expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1);
+ expect(getLayer1GasFeeMock).not.toHaveBeenCalled();
});
it('updateBridgeQuoteRequestParams should only poll once if insufficientBal=true', async function () {
@@ -426,7 +447,7 @@ describe('BridgeController', function () {
.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
- resolve([1, 2, 3] as never);
+ resolve(mockBridgeQuotesNativeErc20Eth as never);
}, 5000);
});
});
@@ -434,7 +455,10 @@ describe('BridgeController', function () {
fetchBridgeQuotesSpy.mockImplementation(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
- resolve([5, 6, 7] as never);
+ resolve([
+ ...mockBridgeQuotesNativeErc20Eth,
+ ...mockBridgeQuotesNativeErc20Eth,
+ ] as never);
}, 10000);
});
});
@@ -503,7 +527,7 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: true },
- quotes: [1, 2, 3],
+ quotes: mockBridgeQuotesNativeErc20Eth,
quotesLoadingStatus: 1,
quotesRefreshCount: 1,
}),
@@ -519,7 +543,7 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: true },
- quotes: [1, 2, 3],
+ quotes: mockBridgeQuotesNativeErc20Eth,
quotesLoadingStatus: 1,
quotesRefreshCount: 1,
}),
@@ -527,6 +551,7 @@ describe('BridgeController', function () {
const secondFetchTime =
bridgeController.state.bridgeState.quotesLastFetched;
expect(secondFetchTime).toStrictEqual(firstFetchTime);
+ expect(getLayer1GasFeeMock).not.toHaveBeenCalled();
});
it('updateBridgeQuoteRequestParams should not trigger quote polling if request is invalid', function () {
@@ -574,6 +599,7 @@ describe('BridgeController', function () {
address: '0x123',
provider: jest.fn(),
} as never);
+
const allowance = await bridgeController.getBridgeERC20Allowance(
'0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
'0xa',
@@ -581,4 +607,143 @@ describe('BridgeController', function () {
expect(allowance).toBe('100000000000000000000');
});
});
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'should append l1GasFees if srcChain is 10 and srcToken is erc20',
+ mockBridgeQuotesErc20Native,
+ add0x(decimalToHex(new BigNumber('2608710388388').mul(2).toFixed())),
+ 12,
+ ],
+ [
+ 'should append l1GasFees if srcChain is 10 and srcToken is native',
+ mockBridgeQuotesNativeErc20,
+ add0x(decimalToHex(new BigNumber('2608710388388').toFixed())),
+ 2,
+ ],
+ [
+ 'should not append l1GasFees if srcChain is not 10',
+ mockBridgeQuotesNativeErc20Eth,
+ undefined,
+ 0,
+ ],
+ ])(
+ 'updateBridgeQuoteRequestParams: %s',
+ async (
+ _: string,
+ quoteResponse: QuoteResponse[],
+ l1GasFeesInHexWei: string,
+ getLayer1GasFeeMockCallCount: number,
+ ) => {
+ jest.useFakeTimers();
+ const stopAllPollingSpy = jest.spyOn(bridgeController, 'stopAllPolling');
+ const startPollingByNetworkClientIdSpy = jest.spyOn(
+ bridgeController,
+ 'startPollingByNetworkClientId',
+ );
+ const hasSufficientBalanceSpy = jest
+ .spyOn(balanceUtils, 'hasSufficientBalance')
+ .mockResolvedValue(false);
+ messengerMock.call.mockReturnValue({
+ address: '0x123',
+ provider: jest.fn(),
+ } as never);
+ getLayer1GasFeeMock.mockResolvedValue('0x25F63418AA4');
+
+ const fetchBridgeQuotesSpy = jest
+ .spyOn(bridgeUtil, 'fetchBridgeQuotes')
+ .mockImplementationOnce(async () => {
+ return await new Promise((resolve) => {
+ return setTimeout(() => {
+ resolve(quoteResponse as never);
+ }, 1000);
+ });
+ });
+
+ const quoteParams = {
+ srcChainId: 10,
+ destChainId: 1,
+ srcTokenAddress: '0x4200000000000000000000000000000000000006',
+ destTokenAddress: '0x0000000000000000000000000000000000000000',
+ srcTokenAmount: '991250000000000000',
+ };
+ const quoteRequest = {
+ ...quoteParams,
+ slippage: 0.5,
+ walletAddress: '0x123',
+ };
+ await bridgeController.updateBridgeQuoteRequestParams(quoteParams);
+
+ expect(stopAllPollingSpy).toHaveBeenCalledTimes(1);
+ expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledTimes(1);
+ expect(hasSufficientBalanceSpy).toHaveBeenCalledTimes(1);
+ expect(startPollingByNetworkClientIdSpy).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ ...quoteRequest,
+ insufficientBal: true,
+ },
+ );
+
+ expect(bridgeController.state.bridgeState).toStrictEqual(
+ expect.objectContaining({
+ quoteRequest: { ...quoteRequest, walletAddress: undefined },
+ quotes: DEFAULT_BRIDGE_CONTROLLER_STATE.quotes,
+ quotesLastFetched: DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched,
+ quotesLoadingStatus:
+ DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus,
+ }),
+ );
+
+ // // Loading state
+ jest.advanceTimersByTime(500);
+ await flushPromises();
+ expect(fetchBridgeQuotesSpy).toHaveBeenCalledTimes(1);
+ expect(fetchBridgeQuotesSpy).toHaveBeenCalledWith(
+ {
+ ...quoteRequest,
+ insufficientBal: true,
+ },
+ expect.any(AbortSignal),
+ );
+ expect(
+ bridgeController.state.bridgeState.quotesLastFetched,
+ ).toStrictEqual(undefined);
+
+ expect(bridgeController.state.bridgeState).toEqual(
+ expect.objectContaining({
+ quoteRequest: { ...quoteRequest, insufficientBal: true },
+ quotes: [],
+ quotesLoadingStatus: 0,
+ }),
+ );
+
+ // After first fetch
+ jest.advanceTimersByTime(1500);
+ await flushPromises();
+ const { quotes } = bridgeController.state.bridgeState;
+ expect(bridgeController.state.bridgeState).toEqual(
+ expect.objectContaining({
+ quoteRequest: { ...quoteRequest, insufficientBal: true },
+ quotesLoadingStatus: 1,
+ quotesRefreshCount: 1,
+ }),
+ );
+ quotes.forEach((quote) => {
+ const expectedQuote = l1GasFeesInHexWei
+ ? { ...quote, l1GasFeesInHexWei }
+ : quote;
+ expect(quote).toStrictEqual(expectedQuote);
+ });
+
+ const firstFetchTime =
+ bridgeController.state.bridgeState.quotesLastFetched ?? 0;
+ expect(firstFetchTime).toBeGreaterThan(0);
+
+ expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(
+ getLayer1GasFeeMockCallCount,
+ );
+ },
+ );
});
diff --git a/app/scripts/controllers/bridge/bridge-controller.ts b/app/scripts/controllers/bridge/bridge-controller.ts
index 2518e9caa9bd..bbe016ac7aea 100644
--- a/app/scripts/controllers/bridge/bridge-controller.ts
+++ b/app/scripts/controllers/bridge/bridge-controller.ts
@@ -6,6 +6,8 @@ import { Contract } from '@ethersproject/contracts';
import { abiERC20 } from '@metamask/metamask-eth-abis';
import { Web3Provider } from '@ethersproject/providers';
import { BigNumber } from '@ethersproject/bignumber';
+import { TransactionParams } from '@metamask/transaction-controller';
+import type { ChainId } from '@metamask/controller-utils';
import {
fetchBridgeFeatureFlags,
fetchBridgeQuotes,
@@ -16,14 +18,23 @@ import {
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { fetchTopAssetsList } from '../../../../ui/pages/swaps/swaps.util';
-import { decimalToHex } from '../../../../shared/modules/conversion.utils';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { QuoteRequest } from '../../../../ui/pages/bridge/types';
+import {
+ decimalToHex,
+ sumHexes,
+} from '../../../../shared/modules/conversion.utils';
+import {
+ L1GasFees,
+ QuoteRequest,
+ QuoteResponse,
+ TxData,
+ // TODO: Remove restricted import
+ // eslint-disable-next-line import/no-restricted-paths
+} from '../../../../ui/pages/bridge/types';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { isValidQuoteRequest } from '../../../../ui/pages/bridge/utils/quote';
import { hasSufficientBalance } from '../../../../shared/modules/bridge-utils/balance';
+import { CHAIN_IDS } from '../../../../shared/constants/network';
import {
BRIDGE_CONTROLLER_NAME,
DEFAULT_BRIDGE_CONTROLLER_STATE,
@@ -53,7 +64,21 @@ export default class BridgeController extends StaticIntervalPollingController<
> {
#abortController: AbortController | undefined;
- constructor({ messenger }: { messenger: BridgeControllerMessenger }) {
+ #getLayer1GasFee: (params: {
+ transactionParams: TransactionParams;
+ chainId: ChainId;
+ }) => Promise;
+
+ constructor({
+ messenger,
+ getLayer1GasFee,
+ }: {
+ messenger: BridgeControllerMessenger;
+ getLayer1GasFee: (params: {
+ transactionParams: TransactionParams;
+ chainId: ChainId;
+ }) => Promise;
+ }) {
super({
name: BRIDGE_CONTROLLER_NAME,
metadata,
@@ -91,6 +116,8 @@ export default class BridgeController extends StaticIntervalPollingController<
`${BRIDGE_CONTROLLER_NAME}:getBridgeERC20Allowance`,
this.getBridgeERC20Allowance.bind(this),
);
+
+ this.#getLayer1GasFee = getLayer1GasFee;
}
_executePoll = async (
@@ -226,10 +253,12 @@ export default class BridgeController extends StaticIntervalPollingController<
this.stopAllPolling();
}
+ const quotesWithL1GasFees = await this.#appendL1GasFees(quotes);
+
this.update((_state) => {
_state.bridgeState = {
..._state.bridgeState,
- quotes,
+ quotes: quotesWithL1GasFees,
quotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.FETCHED,
quotesRefreshCount: newQuotesRefreshCount,
@@ -253,6 +282,45 @@ export default class BridgeController extends StaticIntervalPollingController<
}
};
+ #appendL1GasFees = async (
+ quotes: QuoteResponse[],
+ ): Promise<(QuoteResponse & L1GasFees)[]> => {
+ return await Promise.all(
+ quotes.map(async (quoteResponse) => {
+ const { quote, trade, approval } = quoteResponse;
+ const chainId = add0x(decimalToHex(quote.srcChainId)) as ChainId;
+ if (
+ [CHAIN_IDS.OPTIMISM.toString(), CHAIN_IDS.BASE.toString()].includes(
+ chainId,
+ )
+ ) {
+ const getTxParams = (txData: TxData) => ({
+ from: txData.from,
+ to: txData.to,
+ value: txData.value,
+ data: txData.data,
+ gasLimit: txData.gasLimit?.toString(),
+ });
+ const approvalL1GasFees = approval
+ ? await this.#getLayer1GasFee({
+ transactionParams: getTxParams(approval),
+ chainId,
+ })
+ : '0';
+ const tradeL1GasFees = await this.#getLayer1GasFee({
+ transactionParams: getTxParams(trade),
+ chainId,
+ });
+ return {
+ ...quoteResponse,
+ l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees),
+ };
+ }
+ return quoteResponse;
+ }),
+ );
+ };
+
#setTopAssets = async (
chainId: Hex,
stateKey: 'srcTopAssets' | 'destTopAssets',
diff --git a/app/scripts/controllers/bridge/types.ts b/app/scripts/controllers/bridge/types.ts
index 577a9fa99836..c9e221b82418 100644
--- a/app/scripts/controllers/bridge/types.ts
+++ b/app/scripts/controllers/bridge/types.ts
@@ -9,9 +9,13 @@ import {
NetworkControllerGetSelectedNetworkClientAction,
} from '@metamask/network-controller';
import { SwapsTokenObject } from '../../../../shared/constants/swaps';
-// TODO: Remove restricted import
-// eslint-disable-next-line import/no-restricted-paths
-import { QuoteRequest, QuoteResponse } from '../../../../ui/pages/bridge/types';
+import {
+ L1GasFees,
+ QuoteRequest,
+ QuoteResponse,
+ // TODO: Remove restricted import
+ // eslint-disable-next-line import/no-restricted-paths
+} from '../../../../ui/pages/bridge/types';
import BridgeController from './bridge-controller';
import { BRIDGE_CONTROLLER_NAME, RequestStatus } from './constants';
@@ -39,7 +43,7 @@ export type BridgeControllerState = {
destTokens: Record;
destTopAssets: { address: string }[];
quoteRequest: Partial;
- quotes: QuoteResponse[];
+ quotes: (QuoteResponse & L1GasFees)[];
quotesLastFetched?: number;
quotesLoadingStatus?: RequestStatus;
quotesRefreshCount: number;
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 548b2f9d940c..29e6f09a3aa9 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -2178,6 +2178,10 @@ export default class MetamaskController extends EventEmitter {
});
this.bridgeController = new BridgeController({
messenger: bridgeControllerMessenger,
+ // TODO: Remove once TransactionController exports this action type
+ getLayer1GasFee: this.txController.getLayer1GasFee.bind(
+ this.txController,
+ ),
});
const smartTransactionsControllerMessenger =
diff --git a/test/data/bridge/mock-quotes-erc20-native.json b/test/data/bridge/mock-quotes-erc20-native.json
new file mode 100644
index 000000000000..cd4a1963c6fc
--- /dev/null
+++ b/test/data/bridge/mock-quotes-erc20-native.json
@@ -0,0 +1,894 @@
+[
+ {
+ "quote": {
+ "requestId": "a63df72a-75ae-4416-a8ab-aff02596c75c",
+ "srcChainId": 10,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "991225000000000000",
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["stargate"],
+ "steps": [
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "stargate",
+ "displayName": "StargateV2 (Fast mode)",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/5685c638772f533edad80fcb210b4bb89e30a50f/src/assets/icons/bridges/stargate.png"
+ },
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 10,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3136",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "991225000000000000"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x1c8598b5db2e",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000564a6010a660000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000003804bdedbea3f94faf8c8fac5ec841251d96cf5e64e8706ada4688877885e5249520000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7374617267617465563200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000001c8598b5db2e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000759e000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c83dc7c11df600d7293f778cb365d3dfcc1ffa2221cf5447a8f2ea407a97792135d9f585ecb68916479dfa1f071f169cbe1cfec831b5ad01f4e4caa09204e5181c",
+ "gasLimit": 641446
+ },
+ "estimatedProcessingTimeInSeconds": 64
+ },
+ {
+ "quote": {
+ "requestId": "aad73198-a64d-4310-b12d-9dcc81c412e2",
+ "srcChainId": 10,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "991147696728676903",
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["celer"],
+ "steps": [
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "celer",
+ "displayName": "Celer cBridge",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/cbridge.svg"
+ },
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "991147696728676903"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000e7bf43c55551b1036e796e7fd3b125d1f9903e2e000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000050f68486970f93a855b27794b8141d32a89a1e0a5ef360034a2f60a4b917c188380000a4b1420000000000000000000000000000000000000600000000000000000dc1a09f859b20002c03873900002777000000000000000000000000000000002d68122053030bf8df41a8bb8c6f0a9de411c7d94eed376b7d91234e1585fd9f77dcf974dd25160d0c2c16c8382d8aa85b0edd429edff19b4d4cdcf50d0a9d4d1c",
+ "gasLimit": 203352
+ },
+ "estimatedProcessingTimeInSeconds": 53
+ },
+ {
+ "quote": {
+ "requestId": "6cfd4952-c9b2-4aec-9349-af39c212f84b",
+ "srcChainId": 10,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "991112862890876485",
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["across"],
+ "steps": [
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "across",
+ "displayName": "Across",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png"
+ },
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "991112862890876485"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000e397c4883ec89ed4fc9d258f00c689708b2799c9000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a518700000000000000000000000000000000000000000000000000000000000000902340ab8f6a57ef0c43231b98141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b100007dd39298f9ad673645ebffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b710000000000000000000000000000000088d06e7971021eee573a0ab6bc3e22039fc1c5ded5d12c4cf2b6311f47f909e06197aa8b2f647ae78ae33a6ea5d23f7c951c0e1686abecd01d7c796990d56f391c",
+ "gasLimit": 177423
+ },
+ "estimatedProcessingTimeInSeconds": 15
+ },
+ {
+ "quote": {
+ "requestId": "2c2ba7d8-3922-4081-9f27-63b7d5cc1986",
+ "srcChainId": 10,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "990221346602370184",
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "symbol": "WETH",
+ "decimals": 18,
+ "name": "Wrapped ETH",
+ "coinKey": "WETH",
+ "logoURI": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png",
+ "priceUSD": "3136",
+ "icon": "https://static.debank.com/image/op_token/logo_url/0x4200000000000000000000000000000000000006/61844453e63cf81301f845d7864236f6.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["hop"],
+ "steps": [
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "hop",
+ "displayName": "Hop",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/hop.png"
+ },
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 10,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3136",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3135.46",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "990221346602370184"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c6966694164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000000484ca360ae0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000001168a464edd170000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b080000000000000000000000000000000000000000000000000dac6213fc70c84400000000000000000000000000000000000000000000000000000000673a3b0800000000000000000000000086ca30bef97fb651b8d866d45503684b90cb3312000000000000000000000000710bda329b2a6224e4b44833de30f38e7f81d5640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000067997b63db4b9059d22e50750707b46a6d48dfbb32e50d85fc3bff1170ed9ca30000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000dc1a09f859b2000000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003686f700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d0000000000000000000000005215e9fd223bc909083fbdb2860213873046e45d000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000043ccfd60b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000099d00cde1f22e8afd37d7f103ec3c6c1eb835ace46e502ec8c5ab51413e539461b89c0e26892efd1de1cbfe4222b5589e76231080252197507cce4fb72a30b031b",
+ "gasLimit": 547501
+ },
+ "estimatedProcessingTimeInSeconds": 24.159
+ },
+ {
+ "quote": {
+ "requestId": "a77bc7b2-e8c8-4463-89db-5dd239d6aacc",
+ "srcChainId": 10,
+ "srcAsset": {
+ "chainId": 10,
+ "address": "0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "name": "Wrapped Ether",
+ "decimals": 18,
+ "icon": "https://media.socket.tech/tokens/all/WETH",
+ "logoURI": "https://media.socket.tech/tokens/all/WETH",
+ "chainAgnosticId": "ETH"
+ },
+ "srcTokenAmount": "991250000000000000",
+ "destChainId": 42161,
+ "destAsset": {
+ "chainId": 42161,
+ "address": "0x0000000000000000000000000000000000000000",
+ "symbol": "ETH",
+ "name": "Ethereum",
+ "decimals": 18,
+ "icon": "https://media.socket.tech/tokens/all/ETH",
+ "logoURI": "https://media.socket.tech/tokens/all/ETH",
+ "chainAgnosticId": null
+ },
+ "destTokenAmount": "991147696728676903",
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "chainId": 10,
+ "address": "0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "name": "Wrapped Ether",
+ "decimals": 18,
+ "icon": "https://media.socket.tech/tokens/all/WETH",
+ "logoURI": "https://media.socket.tech/tokens/all/WETH",
+ "chainAgnosticId": "ETH"
+ }
+ }
+ },
+ "bridgeId": "socket",
+ "bridges": ["celer"],
+ "steps": [
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "celer",
+ "displayName": "Celer",
+ "icon": "https://socketicons.s3.amazonaws.com/Celer+Light.png"
+ },
+ "srcAsset": {
+ "chainId": 10,
+ "address": "0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "name": "Wrapped Ether",
+ "decimals": 18,
+ "icon": "https://media.socket.tech/tokens/all/WETH",
+ "logoURI": "https://media.socket.tech/tokens/all/WETH",
+ "chainAgnosticId": "ETH"
+ },
+ "destAsset": {
+ "chainId": 42161,
+ "address": "0x0000000000000000000000000000000000000000",
+ "symbol": "ETH",
+ "name": "Ethereum",
+ "decimals": 18,
+ "icon": "https://media.socket.tech/tokens/all/ETH",
+ "logoURI": "https://media.socket.tech/tokens/all/ETH",
+ "chainAgnosticId": null
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "991147696728676903"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000f736f636b6574416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000003a23f943181408eac424116af7b7790c94cb97a50000000000000000000000003a23f943181408eac424116af7b7790c94cb97a5000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a5187000000000000000000000000000000000000000000000000000000000000004c0000001252106ce9141d32a89a1e0a5ef360034a2f60a4b917c18838420000000000000000000000000000000000000600000000000000000dc1a09f859b20000000a4b1245fa5dd00002777000000000000000000000000000000000000000022be703a074ef6089a301c364c2bbf391d51067ea5cd91515c9ec5421cdaabb23451cd2086f3ebe3e19ff138f3a9be154dcae6033838cc5fabeeb0d260b075cb1c",
+ "gasLimit": 182048
+ },
+ "estimatedProcessingTimeInSeconds": 360
+ },
+ {
+ "quote": {
+ "requestId": "4f2154d9b330221b2ad461adf63acc2c",
+ "srcChainId": 10,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "id": "10_0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "name": "Wrapped ETH",
+ "decimals": 18,
+ "usdPrice": 3135.9632118339764,
+ "coingeckoId": "weth",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg",
+ "volatility": 2,
+ "axelarNetworkSymbol": "WETH",
+ "subGraphIds": [],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "989989428114299041",
+ "destAsset": {
+ "id": "42161_0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
+ "symbol": "ETH",
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 42161,
+ "name": "ETH",
+ "decimals": 18,
+ "usdPrice": 3133.259355489038,
+ "coingeckoId": "ethereum",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg",
+ "volatility": 2,
+ "axelarNetworkSymbol": "ETH",
+ "subGraphIds": ["chainflip-bridge"],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/eth.svg"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "id": "10_0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "name": "Wrapped ETH",
+ "decimals": 18,
+ "usdPrice": 3135.9632118339764,
+ "coingeckoId": "weth",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg",
+ "volatility": 2,
+ "axelarNetworkSymbol": "WETH",
+ "subGraphIds": [],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg"
+ }
+ }
+ },
+ "bridgeId": "squid",
+ "bridges": ["axelar"],
+ "steps": [
+ {
+ "action": "swap",
+ "srcChainId": 10,
+ "destChainId": 10,
+ "protocol": {
+ "name": "Uniswap V3",
+ "displayName": "Uniswap V3"
+ },
+ "srcAsset": {
+ "id": "10_0x4200000000000000000000000000000000000006",
+ "symbol": "WETH",
+ "address": "0x4200000000000000000000000000000000000006",
+ "chainId": 10,
+ "name": "Wrapped ETH",
+ "decimals": 18,
+ "usdPrice": 3135.9632118339764,
+ "coingeckoId": "weth",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg",
+ "axelarNetworkSymbol": "WETH",
+ "subGraphIds": [],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/weth.svg"
+ },
+ "destAsset": {
+ "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85",
+ "symbol": "USDC",
+ "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85",
+ "chainId": 10,
+ "name": "USDC",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "3100880215"
+ },
+ {
+ "action": "swap",
+ "srcChainId": 10,
+ "destChainId": 10,
+ "protocol": {
+ "name": "Uniswap V3",
+ "displayName": "Uniswap V3"
+ },
+ "srcAsset": {
+ "id": "10_0x0b2c639c533813f4aa9d7837caf62653d097ff85",
+ "symbol": "USDC",
+ "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85",
+ "chainId": 10,
+ "name": "USDC",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc", "cctp-uusdc-optimism-to-noble"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "destAsset": {
+ "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607",
+ "symbol": "USDC.e",
+ "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
+ "chainId": 10,
+ "name": "USDC.e",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC.e",
+ "subGraphIds": [],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "srcAmount": "3100880215",
+ "destAmount": "3101045779"
+ },
+ {
+ "action": "swap",
+ "srcChainId": 10,
+ "destChainId": 10,
+ "protocol": {
+ "name": "Uniswap V3",
+ "displayName": "Uniswap V3"
+ },
+ "srcAsset": {
+ "id": "10_0x7f5c764cbc14f9669b88837ca1490cca17c31607",
+ "symbol": "USDC.e",
+ "address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
+ "chainId": 10,
+ "name": "USDC.e",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC.e",
+ "subGraphIds": [],
+ "enabled": true,
+ "subGraphOnly": false,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "destAsset": {
+ "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "symbol": "USDC.axl",
+ "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "chainId": 10,
+ "name": " USDC (Axelar)",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "interchainTokenId": null,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "axlUSDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg"
+ },
+ "srcAmount": "3101045779",
+ "destAmount": "3101521947"
+ },
+ {
+ "action": "bridge",
+ "srcChainId": 10,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "axelar",
+ "displayName": "Axelar"
+ },
+ "srcAsset": {
+ "id": "10_0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "symbol": "USDC.axl",
+ "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "chainId": 10,
+ "name": " USDC (Axelar)",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "interchainTokenId": null,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "axlUSDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg"
+ },
+ "destAsset": {
+ "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "symbol": "USDC.axl",
+ "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "chainId": 42161,
+ "name": " USDC (Axelar)",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "interchainTokenId": null,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "axlUSDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg"
+ },
+ "srcAmount": "3101521947",
+ "destAmount": "3101521947"
+ },
+ {
+ "action": "swap",
+ "srcChainId": 42161,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "Pancakeswap V3",
+ "displayName": "Pancakeswap V3"
+ },
+ "srcAsset": {
+ "id": "42161_0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "symbol": "USDC.axl",
+ "address": "0xeb466342c4d449bc9f53a865d5cb90586f405215",
+ "chainId": 42161,
+ "name": " USDC (Axelar)",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "interchainTokenId": null,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "axlUSDC",
+ "subGraphOnly": false,
+ "subGraphIds": ["uusdc"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/usdc.svg"
+ },
+ "destAsset": {
+ "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831",
+ "symbol": "USDC",
+ "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
+ "chainId": 42161,
+ "name": "USDC",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC",
+ "subGraphOnly": false,
+ "subGraphIds": [
+ "uusdc",
+ "cctp-uusdc-arbitrum-to-noble",
+ "chainflip-bridge"
+ ],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "srcAmount": "3101521947",
+ "destAmount": "3100543869"
+ },
+ {
+ "action": "swap",
+ "srcChainId": 42161,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "Uniswap V3",
+ "displayName": "Uniswap V3"
+ },
+ "srcAsset": {
+ "id": "42161_0xaf88d065e77c8cc2239327c5edb3a432268e5831",
+ "symbol": "USDC",
+ "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
+ "chainId": 42161,
+ "name": "USDC",
+ "decimals": 6,
+ "usdPrice": 1.0003003590332982,
+ "coingeckoId": "usd-coin",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg",
+ "axelarNetworkSymbol": "USDC",
+ "subGraphOnly": false,
+ "subGraphIds": [
+ "uusdc",
+ "cctp-uusdc-arbitrum-to-noble",
+ "chainflip-bridge"
+ ],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/usdc.svg"
+ },
+ "destAsset": {
+ "id": "42161_0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
+ "symbol": "WETH",
+ "address": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1",
+ "chainId": 42161,
+ "name": "Wrapped ETH",
+ "decimals": 18,
+ "usdPrice": 3135.9632118339764,
+ "interchainTokenId": null,
+ "coingeckoId": "weth",
+ "type": "evm",
+ "logoURI": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg",
+ "axelarNetworkSymbol": "WETH",
+ "subGraphOnly": false,
+ "subGraphIds": ["arbitrum-weth-wei"],
+ "enabled": true,
+ "active": true,
+ "icon": "https://raw.githubusercontent.com/axelarnetwork/axelar-configs/main/images/tokens/weth.svg"
+ },
+ "srcAmount": "3100543869",
+ "destAmount": "989989428114299041"
+ }
+ ]
+ },
+ "approval": {
+ "chainId": 10,
+ "to": "0x4200000000000000000000000000000000000006",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x00",
+ "data": "0x095ea7b3000000000000000000000000b90357f2b86dbfd59c3502215d4060f71df8ca0e0000000000000000000000000000000000000000000000000de0b6b3a7640000",
+ "gasLimit": 29122
+ },
+ "trade": {
+ "chainId": 10,
+ "to": "0xB90357f2b86dbfD59c3502215d4060f71DF8ca0e",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x4653ce53e6b1",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000e73717569644164617074657256320000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b60000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d666000000000000000000000000000000000000000000000000000000000000a4b1000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000716a8b9dd056055c84b7a2ba0a016099465a51870000000000000000000000000000000000000000000000000000000000001a14846a1bc600000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000000000000000000000000000000000000000000d200000000000000000000000000000000000000000000000000000000000000d600000000000000000000000000000000000000000000000000000000000000dc0000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c188380000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000046000000000000000000000000000000000000000000000000000000000000005e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000098000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf00000000000000000000000042000000000000000000000000000000000000060000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff8500000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c0000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000b8833d8e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff850000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8d3ad5700000000000000000000000000000000000000000000000000000000b8c346b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000b2c639c533813f4aa9d7837caf62653d097ff85000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf0000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c31607000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ce16f69375520ab01377ce7b88f5ba8c48f8d66600000000000000000000000000000000000000000000000000000000b8d6341300000000000000000000000000000000000000000000000000000000b8ca89fa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000007f5c764cbc14f9669b88837ca1490cca17c316070000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000761786c55534443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008417262697472756d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002a307863653136463639333735353230616230313337376365374238386635424138433438463844363636000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c100000000000000000000000000000000000000000000000000000000000000040000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000003600000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000009200000000000000000000000000000000000000000000000000000000000000a8000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f4052150000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000032226588378236fd0c7c4053999f88ac0e5cac77000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f405215000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000064000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8dd781b00000000000000000000000000000000000000000000000000000000b8bb9ee30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eb466342c4d449bc9f53a865d5cb90586f40521500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000e404e45aaf000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000001f4000000000000000000000000ea749fd6ba492dbc14c24fe8a3d08769229b896c00000000000000000000000000000000000000000000000000000000b8ce8b7d0000000000000000000000000000000000000000000000000db72b79f837011c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c18838000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c000000000000000000000000000000004f2154d9b330221b2ad461adf63acc2c0000000000000000000000003c17c95cdb5887c334bfae85750ce00e1a720a76eff35e60db6c9f3b8384a6d63db3c56f1ce6545b50ba2f250429055ca77e7e6203ddd65a7a4d89ae1af3d61b1c",
+ "gasLimit": 710342
+ },
+ "estimatedProcessingTimeInSeconds": 20
+ }
+]
diff --git a/test/data/bridge/mock-quotes-native-erc20-eth.json b/test/data/bridge/mock-quotes-native-erc20-eth.json
new file mode 100644
index 000000000000..0afd77760e75
--- /dev/null
+++ b/test/data/bridge/mock-quotes-native-erc20-eth.json
@@ -0,0 +1,258 @@
+[
+ {
+ "quote": {
+ "requestId": "34c4136d-8558-4d87-bdea-eef8d2d30d6d",
+ "srcChainId": 1,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "3104367033",
+ "destAsset": {
+ "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+ "chainId": 42161,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9998000399920016",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["across"],
+ "steps": [
+ {
+ "action": "swap",
+ "srcChainId": 1,
+ "destChainId": 1,
+ "protocol": {
+ "name": "0x",
+ "displayName": "0x",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png"
+ },
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destAsset": {
+ "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "chainId": 1,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9997000899730081",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "3104701473"
+ },
+ {
+ "action": "bridge",
+ "srcChainId": 1,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "across",
+ "displayName": "Across",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/acrossv2.png"
+ },
+ "srcAsset": {
+ "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "chainId": 1,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9997000899730081",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "destAsset": {
+ "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+ "chainId": 42161,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9998000399920016",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "srcAmount": "3104701473",
+ "destAmount": "3104367033"
+ }
+ ]
+ },
+ "trade": {
+ "chainId": 1,
+ "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x0de0b6b3a7640000",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b400000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de51520000000000000000000000000000000000000000000000000000000000000a003a3f733200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000000000000094027363a1fac5600d1f7e8a4c50087ff1f32a09359512d2379d46b331c6033cc7b000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066163726f73730000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a094cc69295a8f2a3016ede239627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000620541d325b000000000000000000000000000000000000000000000000000000000673656d70000000000000000000000000000000000000000000000000000000000000080ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000d00dfeeddeadbeef8932eb23bad9bddb5cf81426f78279a53c6c3b71dcbfe555f9a744b18195d9b52032871d6f3c5a558275c08a71c2b6214801f5161be976f49181b854a3ebcbe1f2b896133b03314a5ff2746e6494c43e59d0c9ee1c",
+ "gasLimit": 540076
+ },
+ "estimatedProcessingTimeInSeconds": 45
+ },
+ {
+ "quote": {
+ "requestId": "5bf0f2f0-655c-4e13-a545-1ebad6f9d2bc",
+ "srcChainId": 1,
+ "srcTokenAmount": "991250000000000000",
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destChainId": 42161,
+ "destTokenAmount": "3104601473",
+ "destAsset": {
+ "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+ "chainId": 42161,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9998000399920016",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "feeData": {
+ "metabridge": {
+ "amount": "8750000000000000",
+ "asset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ }
+ }
+ },
+ "bridgeId": "lifi",
+ "bridges": ["celercircle"],
+ "steps": [
+ {
+ "action": "swap",
+ "srcChainId": 1,
+ "destChainId": 1,
+ "protocol": {
+ "name": "0x",
+ "displayName": "0x",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/exchanges/zerox.png"
+ },
+ "srcAsset": {
+ "address": "0x0000000000000000000000000000000000000000",
+ "chainId": 1,
+ "symbol": "ETH",
+ "decimals": 18,
+ "name": "ETH",
+ "coinKey": "ETH",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
+ "priceUSD": "3145.41",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
+ },
+ "destAsset": {
+ "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "chainId": 1,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9997000899730081",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "srcAmount": "991250000000000000",
+ "destAmount": "3104701473"
+ },
+ {
+ "action": "bridge",
+ "srcChainId": 1,
+ "destChainId": 42161,
+ "protocol": {
+ "name": "celercircle",
+ "displayName": "Circle CCTP",
+ "icon": "https://raw.githubusercontent.com/lifinance/types/main/src/assets/icons/bridges/circle.png"
+ },
+ "srcAsset": {
+ "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "chainId": 1,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9997000899730081",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "destAsset": {
+ "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
+ "chainId": 42161,
+ "symbol": "USDC",
+ "decimals": 6,
+ "name": "USD Coin",
+ "coinKey": "USDC",
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
+ "priceUSD": "0.9998000399920016",
+ "icon": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ "srcAmount": "3104701473",
+ "destAmount": "3104601473"
+ }
+ ]
+ },
+ "trade": {
+ "chainId": 1,
+ "to": "0x0439e60F02a8900a951603950d8D4527f400C3f1",
+ "from": "0x141d32a89a1e0a5ef360034a2f60a4b917c18838",
+ "value": "0x0de0b6b3a7640000",
+ "data": "0x3ce33bff000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000d6c696669416461707465725632000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a800000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000000000000000000000000000000000000000a4b10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000dc1a09f859b20000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000001f161421c8e000000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de515200000000000000000000000000000000000000000000000000000000000009248fab066300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000200b431adcab44c6fe13ade53dbd3b714f57922ab5b776924a913685ad0fe680f6c000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000141d32a89a1e0a5ef360034a2f60a4b917c1883800000000000000000000000000000000000000000000000000000000b8211d6e000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b63656c6572636972636c65000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6d6574616d61736b2d6272696467650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000005c42213bc0b00000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dc1a09f859b200000000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f471000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004e41fff991f0000000000000000000000001231deb6f5749ef6ce6943a275a1d3e7486f4eae000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000b909399a00000000000000000000000000000000000000000000000000000000000000a0c0452b52ecb7cf70409b16cd627ab300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a15600000000000000000000000070bf6634ee8cb27d04478f184b9b8bb13e5f4710000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2010001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012438c9c147000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000000000005000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000ad01c20d5886137e056775af56915de824c8fce50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000047896dca097909ba9db4c9631bce0e53090bce14a9b7d203e21fa80cee7a16fa049aa1ef7d663c2ec3148e698e01774b62ddedc9c2dcd21994e549cd6f318f971b",
+ "gasLimit": 682910
+ },
+ "estimatedProcessingTimeInSeconds": 1029.717
+ }
+]
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 187e9a0a8230..3aea2fcbd5df 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -35,6 +35,7 @@ import { calcTokenAmount } from '../../../shared/lib/transactions-controller-uti
// eslint-disable-next-line import/no-restricted-paths
import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants';
import {
+ L1GasFees,
QuoteMetadata,
QuoteResponse,
SortOrder,
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index b9bf9bf54e7c..4be72f200650 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -1,5 +1,9 @@
import { BigNumber } from 'bignumber.js';
+export type L1GasFees = {
+ l1GasFeesInHexWei?: string; // l1 fees for approval and trade in hex wei, appended by controller
+};
+
// Values derived from the quote response
export type QuoteMetadata = {
totalNetworkFee: { raw: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
diff --git a/ui/pages/bridge/utils/quote.test.ts b/ui/pages/bridge/utils/quote.test.ts
index 6d03177d1fe7..6afc148385f2 100644
--- a/ui/pages/bridge/utils/quote.test.ts
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -1,5 +1,5 @@
import { BigNumber } from 'bignumber.js';
-import { zeroAddress } from '../../../__mocks__/ethereumjs-util';
+import { zeroAddress } from 'ethereumjs-util';
import {
calcAdjustedReturn,
calcSentAmount,
@@ -195,6 +195,77 @@ describe('Bridge quote utils', () => {
},
);
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de0b6b3a7640000',
+ { raw: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ undefined,
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { raw: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ undefined,
+ ],
+ [
+ 'erc20 with approval',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { raw: '0.000003055746402628', fiat: '0.00769259710890377976' },
+ 1092677,
+ ],
+ [
+ 'erc20 with relayer fee',
+ ERC20_TOKEN,
+ '100000000',
+ '0x0de0b6b3a7640000',
+ { raw: '1.000002832228395508', fiat: '2517.42712990840741974936' },
+ undefined,
+ ],
+ [
+ 'native with relayer fee',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de1b6b3a7640000',
+ { raw: '0.000284307205106164', fiat: '0.71572064427835937688' },
+ undefined,
+ ],
+ ])(
+ 'calcTotalNetworkFee: fromToken is %s with l1GasFee',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ value: string,
+ { raw, fiat }: { raw: string; fiat: string },
+ approvalGasLimit?: number,
+ ) => {
+ const feeData = { metabridge: { amount: 0 } };
+ const result = calcTotalNetworkFee(
+ {
+ trade: { value, gasLimit: 1092677 },
+ approval: approvalGasLimit
+ ? { gasLimit: approvalGasLimit }
+ : undefined,
+ quote: { srcAsset, srcTokenAmount, feeData },
+ l1GasFeesInHexWei: '0x25F63418AA4',
+ } as never,
+ '0.00010456',
+ '0.0001',
+ 2517.42,
+ );
+ expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
[
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 98018fb11bd6..cff8e489a510 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -1,11 +1,18 @@
import { zeroAddress } from 'ethereumjs-util';
import { BigNumber } from 'bignumber.js';
import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils';
-import { QuoteResponse, QuoteRequest, Quote } from '../types';
+import { QuoteResponse, QuoteRequest, Quote, L1GasFees } from '../types';
import {
hexToDecimal,
sumDecimals,
} from '../../../../shared/modules/conversion.utils';
+<<<<<<< HEAD
+=======
+import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
+import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay';
+import { Numeric } from '../../../../shared/modules/Numeric';
+import { EtherDenomination } from '../../../../shared/constants/common';
+>>>>>>> 61f029ee7a (chore: add L1 fees for Base and Optimism)
export const isNativeAddress = (address?: string) => address === zeroAddress();
@@ -96,13 +103,12 @@ const calcRelayerFee = (
};
const calcTotalGasFee = (
- bridgeQuote: QuoteResponse,
+ bridgeQuote: QuoteResponse & L1GasFees,
estimatedBaseFeeInDecGwei: string,
maxPriorityFeePerGasInDecGwei: string,
nativeExchangeRate?: number,
- l1GasInDecGwei?: BigNumber,
) => {
- const { approval, trade } = bridgeQuote;
+ const { approval, trade, l1GasFeesInHexWei } = bridgeQuote;
const totalGasLimitInDec = sumDecimals(
trade.gasLimit?.toString() ?? '0',
approval?.gasLimit?.toString() ?? '0',
@@ -111,11 +117,16 @@ const calcTotalGasFee = (
estimatedBaseFeeInDecGwei,
maxPriorityFeePerGasInDecGwei,
);
- const gasFeesInDecGwei = totalGasLimitInDec.times(feePerGasInDecGwei);
- if (l1GasInDecGwei) {
- gasFeesInDecGwei.add(l1GasInDecGwei);
- }
+ const l1GasFeesInDecGWei = Numeric.from(
+ l1GasFeesInHexWei ?? '0',
+ 16,
+ EtherDenomination.WEI,
+ ).toDenomination(EtherDenomination.GWEI);
+
+ const gasFeesInDecGwei = totalGasLimitInDec
+ .times(feePerGasInDecGwei)
+ .add(l1GasFeesInDecGWei);
const gasFeesInDecEth = new BigNumber(
gasFeesInDecGwei.shiftedBy(9).toString(),
@@ -131,7 +142,7 @@ const calcTotalGasFee = (
};
export const calcTotalNetworkFee = (
- bridgeQuote: QuoteResponse,
+ bridgeQuote: QuoteResponse & L1GasFees,
estimatedBaseFeeInDecGwei: string,
maxPriorityFeePerGasInDecGwei: string,
nativeExchangeRate?: number,
From 612c33b77f8c798fe0784d8fc9735bce0c3d4674 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 25 Oct 2024 13:43:08 -0700
Subject: [PATCH 05/25] chore: display quote metadata
---
ui/ducks/bridge/selectors.test.ts | 10 -------
ui/ducks/bridge/selectors.ts | 11 --------
ui/pages/bridge/prepare/bridge-cta-button.tsx | 10 +------
.../bridge/prepare/prepare-bridge-page.tsx | 10 ++++---
.../bridge-quote-card.test.tsx.snap | 12 ++++-----
.../bridge-quotes-modal.test.tsx.snap | 6 +++++
ui/pages/bridge/quotes/bridge-quote-card.tsx | 26 ++++++++++++-------
.../bridge/quotes/bridge-quotes-modal.tsx | 6 +++--
8 files changed, 39 insertions(+), 52 deletions(-)
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index 416959b34d66..e0ef95890969 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -22,7 +22,6 @@ import {
getFromTokens,
getFromTopAssets,
getIsBridgeTx,
- getToAmount,
getToChain,
getToChains,
getToToken,
@@ -397,15 +396,6 @@ describe('Bridge selectors', () => {
});
});
- describe('getToAmount', () => {
- it('returns hardcoded 0', () => {
- const state = createBridgeMockStore();
- const result = getToAmount(state as never);
-
- expect(result).toStrictEqual(undefined);
- });
- });
-
describe('getToTokens', () => {
it('returns dest tokens from controller state when toChainId is defined', () => {
const state = createBridgeMockStore(
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 3aea2fcbd5df..20c35541ef89 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -30,12 +30,10 @@ import {
} from '../../../shared/modules/selectors/networks';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
-import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants';
import {
- L1GasFees,
QuoteMetadata,
QuoteResponse,
SortOrder,
@@ -335,15 +333,6 @@ export const getBridgeQuotes = createSelector(
}),
);
-export const getToAmount = createSelector(getBridgeQuotes, ({ activeQuote }) =>
- activeQuote
- ? calcTokenAmount(
- activeQuote.quote.destTokenAmount,
- activeQuote.quote.destAsset.decimals,
- )
- : undefined,
-);
-
export const getIsBridgeTx = createDeepEqualSelector(
getFromChain,
getToChain,
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index 81889c037dbe..7355e6579dfa 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -5,7 +5,6 @@ import {
getFromAmount,
getFromChain,
getFromToken,
- getToAmount,
getToChain,
getToToken,
getBridgeQuotes,
@@ -24,20 +23,13 @@ export const BridgeCTAButton = () => {
const toChain = useSelector(getToChain);
const fromAmount = useSelector(getFromAmount);
- const toAmount = useSelector(getToAmount);
const { isLoading, activeQuote } = useSelector(getBridgeQuotes);
const { submitBridgeTransaction } = useSubmitBridgeTransaction();
const isTxSubmittable =
- fromToken &&
- toToken &&
- fromChain &&
- toChain &&
- fromAmount &&
- toAmount &&
- activeQuote;
+ fromToken && toToken && fromChain && toChain && fromAmount && activeQuote;
const label = useMemo(() => {
if (isLoading && !isTxSubmittable) {
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 1044872c504f..8fde7817af9a 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -16,6 +16,7 @@ import {
updateQuoteRequestParams,
} from '../../../ducks/bridge/actions';
import {
+ getBridgeQuotes,
getFromAmount,
getFromChain,
getFromChains,
@@ -23,7 +24,6 @@ import {
getFromTokens,
getFromTopAssets,
getQuoteRequest,
- getToAmount,
getToChain,
getToChains,
getToToken,
@@ -71,11 +71,11 @@ const PrepareBridgePage = () => {
const toChain = useSelector(getToChain);
const fromAmount = useSelector(getFromAmount);
- const toAmount = useSelector(getToAmount);
const providerConfig = useSelector(getProviderConfig);
const quoteRequest = useSelector(getQuoteRequest);
+ const { activeQuote } = useSelector(getBridgeQuotes);
const fromTokenListGenerator = useTokensWithFiltering(
fromTokens,
@@ -261,8 +261,10 @@ const PrepareBridgePage = () => {
testId: 'to-amount',
readOnly: true,
disabled: true,
- value: toAmount?.toString() ?? '0',
- className: toAmount ? 'amount-input defined' : 'amount-input',
+ value: activeQuote?.toTokenAmount?.raw.toFixed(2) ?? '0',
+ className: activeQuote?.toTokenAmount.raw
+ ? 'amount-input defined'
+ : 'amount-input',
}}
/>
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 fea4026e216f..38c305ea7ece 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
@@ -90,7 +90,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
- 0.998877142857142857142857142857142857
+ 1 USDC = 1.00
@@ -131,13 +131,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.00100007141025952
+ 0.001000
- 2.52443025734759336
+ 2.52
@@ -263,7 +263,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
- 2443.8902
+ 1 ETH = 2443.89
@@ -304,13 +304,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.00100012486628784
+ 0.001000
- 2.52456519372708012
+ 2.52
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 8d55f0abce94..350e287a1017 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
@@ -105,6 +105,9 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
>
2.52443025734759336
+
@@ -119,6 +122,9 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
>
2.52445909711870752
+
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 6ac7ee51c411..3ec2074a6adc 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -51,16 +51,22 @@ export const BridgeQuoteCard = () => {
formatEtaInMinutes(activeQuote.estimatedProcessingTimeInSeconds),
])}
/>
-
-
+ {activeQuote.swapRate && (
+
+ )}
+ {activeQuote.totalNetworkFee && (
+
+ )}
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index ebe2db59838b..68e8a539b203 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -51,10 +51,12 @@ export const BridgeQuotesModal = ({
{sortedQuotes.map((quote, index) => {
- const { totalNetworkFee, estimatedProcessingTimeInSeconds } = quote;
+ const { totalNetworkFee, estimatedProcessingTimeInSeconds, cost } =
+ quote;
return (
- {totalNetworkFee?.fiat?.toString()}
+ {totalNetworkFee.fiat?.toString()}
+ {cost.fiat ? cost.fiat.toFixed(2) : ''}
{t('bridgeTimingMinutes', [
formatEtaInMinutes(estimatedProcessingTimeInSeconds),
From 25b7602e1c4ac9f4f7b40e522f8cd5d40ba1d6a1 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 22 Oct 2024 17:05:09 -0700
Subject: [PATCH 06/25] chore: update bridge quote card
---
app/_locales/en/messages.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index f51c9708fc20..3d0e91a0a1a0 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -873,7 +873,7 @@
"message": "Select token and amount"
},
"bridgeTimingMinutes": {
- "message": "$1 minutes",
+ "message": "$1 min",
"description": "$1 is the ticker symbol of a an asset the user is being prompted to purchase"
},
"bridgeTimingTooltipText": {
From 102a229509f3dab88467fefcae6eee4cd11b820a Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 25 Oct 2024 13:58:46 -0700
Subject: [PATCH 07/25] chore: format token amounts
---
.../bridge-quote-card.test.tsx.snap | 16 +++++------
.../bridge-quotes-modal.test.tsx.snap | 24 +++++++++++++---
ui/pages/bridge/quotes/bridge-quote-card.tsx | 26 ++++++++++++++---
.../bridge/quotes/bridge-quotes-modal.tsx | 28 +++++++++++++++----
ui/pages/bridge/utils/quote.ts | 13 ++++++---
5 files changed, 81 insertions(+), 26 deletions(-)
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 38c305ea7ece..6b69b8ec9a6c 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
@@ -61,7 +61,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
- 1 minutes
+ 1 min
@@ -90,7 +90,7 @@ exports[`BridgeQuoteCard should render the recommended quote 1`] = `
- 1 USDC = 1.00
+ 1 USDC = 1.00 USDC
@@ -131,13 +131,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.001000
+ 0.001000 ETH
- 2.52
+ $2.52
@@ -234,7 +234,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
- 1 minutes
+ 1 min
@@ -263,7 +263,7 @@ exports[`BridgeQuoteCard should render the recommended quote while loading new q
- 1 ETH = 2443.89
+ 1 ETH = 2443.89 USDC
@@ -304,13 +304,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.001000
+ 0.001000 ETH
- 2.52
+ $2.52
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 350e287a1017..9a1ab5dc9878 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
@@ -100,10 +100,13 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
+
- 2.52443025734759336
+ $2.52
- 1 minutes
+ across
+
+
+ 1 min
+
- 2.52445909711870752
+ $2.52
- 26 minutes
+ celercircle
+
+
+ 26 min
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 3ec2074a6adc..767d44d60032 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -8,9 +8,15 @@ import {
} from '../../../components/component-library';
import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ formatFiatAmount,
+ formatTokenAmount,
+ formatEtaInMinutes,
+} from '../utils/quote';
import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
import MascotBackgroundAnimation from '../../swaps/mascot-background-animation/mascot-background-animation';
-import { formatEtaInMinutes } from '../utils/quote';
+import { getCurrentCurrency } from '../../../selectors';
+import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { QuoteInfoRow } from './quote-info-row';
import { BridgeQuotesModal } from './bridge-quotes-modal';
@@ -18,6 +24,8 @@ export const BridgeQuoteCard = () => {
const t = useI18nContext();
const { isLoading, isQuoteGoingToRefresh, activeQuote } =
useSelector(getBridgeQuotes);
+ const currency = useSelector(getCurrentCurrency);
+ const ticker = useSelector(getNativeCurrency);
const secondsUntilNextRefresh = useCountdownTimer();
@@ -56,15 +64,25 @@ export const BridgeQuoteCard = () => {
label={t('quoteRate')}
description={`1 ${
activeQuote.quote.srcAsset.symbol
- } = ${activeQuote.swapRate.toFixed(2)}`}
+ } = ${formatTokenAmount(
+ activeQuote.swapRate,
+ activeQuote.quote.destAsset.symbol,
+ )}`}
/>
)}
{activeQuote.totalNetworkFee && (
)}
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 68e8a539b203..fe072e0a72ca 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -17,9 +17,10 @@ import {
TextAlign,
TextVariant,
} from '../../../helpers/constants/design-system';
-import { useI18nContext } from '../../../hooks/useI18nContext';
+import { formatEtaInMinutes, formatFiatAmount } from '../utils/quote';
import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
-import { formatEtaInMinutes } from '../utils/quote';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import { getCurrentCurrency } from '../../../selectors';
export const BridgeQuotesModal = ({
onClose,
@@ -28,6 +29,7 @@ export const BridgeQuotesModal = ({
const t = useI18nContext();
const { sortedQuotes } = useSelector(getBridgeQuotes);
+ const currency = useSelector(getCurrentCurrency);
return (
@@ -51,12 +53,26 @@ export const BridgeQuotesModal = ({
{sortedQuotes.map((quote, index) => {
- const { totalNetworkFee, estimatedProcessingTimeInSeconds, cost } =
- quote;
+ const {
+ totalNetworkFee,
+ sentAmount,
+ adjustedReturn,
+ estimatedProcessingTimeInSeconds,
+ quote: { bridges },
+ } = quote;
return (
- {totalNetworkFee.fiat?.toString()}
- {cost.fiat ? cost.fiat.toFixed(2) : ''}
+ {formatFiatAmount(adjustedReturn.fiat, currency)}
+ {formatFiatAmount(totalNetworkFee?.fiat, currency)}
+
+ {adjustedReturn.fiat && sentAmount.fiat
+ ? `-${formatFiatAmount(
+ sentAmount.fiat.minus(adjustedReturn.fiat),
+ currency,
+ )}`
+ : ''}
+
+ {bridges[0]}
{t('bridgeTimingMinutes', [
formatEtaInMinutes(estimatedProcessingTimeInSeconds),
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index cff8e489a510..27e7edc8d1c8 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -6,13 +6,9 @@ import {
hexToDecimal,
sumDecimals,
} from '../../../../shared/modules/conversion.utils';
-<<<<<<< HEAD
-=======
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
-import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay';
import { Numeric } from '../../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../../shared/constants/common';
->>>>>>> 61f029ee7a (chore: add L1 fees for Base and Optimism)
export const isNativeAddress = (address?: string) => address === zeroAddress();
@@ -187,3 +183,12 @@ export const calcCost = (
export const formatEtaInMinutes = (estimatedProcessingTimeInSeconds: number) =>
(estimatedProcessingTimeInSeconds / 60).toFixed();
+
+export const formatTokenAmount = (
+ amount: BigNumber,
+ symbol: string,
+ precision: number = 2,
+) => `${amount.toFixed(precision)} ${symbol}`;
+
+export const formatFiatAmount = (amount: BigNumber | null, currency: string) =>
+ amount ? formatCurrency(amount.toString(), currency) : undefined;
From 93d41cfe7c43a159722f0e48d818584d36060ce0 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Fri, 25 Oct 2024 14:02:04 -0700
Subject: [PATCH 08/25] chore: sort quotes on header click
---
test/jest/mock-store.js | 1 +
ui/ducks/bridge/actions.ts | 2 ++
.../bridge/quotes/bridge-quotes-modal.tsx | 29 +++++++++++++------
3 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/test/jest/mock-store.js b/test/jest/mock-store.js
index f75b0df8bd09..41706fc77937 100644
--- a/test/jest/mock-store.js
+++ b/test/jest/mock-store.js
@@ -711,6 +711,7 @@ export const createBridgeMockStore = (
...swapsStore,
bridge: {
toChainId: null,
+ sortOrder: 0,
...bridgeSliceOverrides,
},
metamask: {
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index 5c2c9aa71f88..76ac75bb7b75 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -23,6 +23,7 @@ const {
setToToken,
setFromTokenInputValue,
resetInputFields,
+ setSortOrder,
} = bridgeSlice.actions;
export {
@@ -33,6 +34,7 @@ export {
setFromTokenInputValue,
setDestTokenExchangeRates,
setSrcTokenExchangeRates,
+ setSortOrder,
};
const callBridgeControllerMethod = (
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index fe072e0a72ca..0a9de383e7f2 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { useSelector } from 'react-redux';
import { IconName } from '@metamask/snaps-sdk/jsx';
+import { useDispatch, useSelector } from 'react-redux';
import {
Box,
Button,
@@ -21,12 +21,15 @@ import { formatEtaInMinutes, formatFiatAmount } from '../utils/quote';
import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getCurrentCurrency } from '../../../selectors';
+import { setSortOrder } from '../../../ducks/bridge/actions';
+import { SortOrder } from '../types';
export const BridgeQuotesModal = ({
onClose,
...modalProps
}: Omit, 'children'>) => {
const t = useI18nContext();
+ const dispatch = useDispatch();
const { sortedQuotes } = useSelector(getBridgeQuotes);
const currency = useSelector(getCurrentCurrency);
@@ -42,14 +45,22 @@ export const BridgeQuotesModal = ({
- {[t('bridgeOverallCost'), t('time')].map((label) => {
- return (
-
- );
- })}
+
+
{sortedQuotes.map((quote, index) => {
From 4760e4490157879db6578b9cdbe16e4e6ec67a8a Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 31 Oct 2024 16:11:51 -0700
Subject: [PATCH 09/25] chore: reusable layout wrappers for Bridge page
---
ui/pages/bridge/layout/column.tsx | 23 +++++++++
ui/pages/bridge/layout/index.tsx | 5 ++
ui/pages/bridge/layout/row.tsx | 27 ++++++++++
ui/pages/bridge/layout/tooltip.tsx | 83 ++++++++++++++++++++++++++++++
4 files changed, 138 insertions(+)
create mode 100644 ui/pages/bridge/layout/column.tsx
create mode 100644 ui/pages/bridge/layout/index.tsx
create mode 100644 ui/pages/bridge/layout/row.tsx
create mode 100644 ui/pages/bridge/layout/tooltip.tsx
diff --git a/ui/pages/bridge/layout/column.tsx b/ui/pages/bridge/layout/column.tsx
new file mode 100644
index 000000000000..6f5b2847b5e5
--- /dev/null
+++ b/ui/pages/bridge/layout/column.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import {
+ Container,
+ ContainerProps,
+} from '../../../components/component-library';
+import {
+ BlockSize,
+ Display,
+ FlexDirection,
+} from '../../../helpers/constants/design-system';
+
+const Column = (props: ContainerProps<'div'>) => {
+ return (
+
+ );
+};
+
+export default Column;
diff --git a/ui/pages/bridge/layout/index.tsx b/ui/pages/bridge/layout/index.tsx
new file mode 100644
index 000000000000..d519d211f500
--- /dev/null
+++ b/ui/pages/bridge/layout/index.tsx
@@ -0,0 +1,5 @@
+import Column from './column';
+import Row from './row';
+import Tooltip from './tooltip';
+
+export { Column, Row, Tooltip };
diff --git a/ui/pages/bridge/layout/row.tsx b/ui/pages/bridge/layout/row.tsx
new file mode 100644
index 000000000000..eeb94a7e06f7
--- /dev/null
+++ b/ui/pages/bridge/layout/row.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import {
+ Container,
+ ContainerProps,
+} from '../../../components/component-library';
+import {
+ AlignItems,
+ Display,
+ FlexDirection,
+ FlexWrap,
+ JustifyContent,
+} from '../../../helpers/constants/design-system';
+
+const Row = (props: ContainerProps<'div'>) => {
+ return (
+
+ );
+};
+
+export default Row;
diff --git a/ui/pages/bridge/layout/tooltip.tsx b/ui/pages/bridge/layout/tooltip.tsx
new file mode 100644
index 000000000000..b6781c9bf480
--- /dev/null
+++ b/ui/pages/bridge/layout/tooltip.tsx
@@ -0,0 +1,83 @@
+import React, { useState } from 'react';
+import {
+ Box,
+ Popover,
+ PopoverHeader,
+ PopoverPosition,
+ PopoverProps,
+ Text,
+} from '../../../components/component-library';
+import {
+ JustifyContent,
+ TextAlign,
+ TextColor,
+} from '../../../helpers/constants/design-system';
+
+const Tooltip = React.forwardRef(
+ ({
+ children,
+ title,
+ triggerElement,
+ disabled = false,
+ ...props
+ }: PopoverProps<'div'> & {
+ triggerElement: React.ReactElement;
+ disabled?: boolean;
+ }) => {
+ 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);
+
+ return (
+ <>
+
+ {triggerElement}
+
+ {!disabled && (
+
+
+ {title}
+
+
+ {children}
+
+
+ )}
+ >
+ );
+ },
+);
+
+export default Tooltip;
From e71513799522069c9f601269347c3adab2b14926 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 5 Nov 2024 18:40:13 -0800
Subject: [PATCH 10/25] chore: set fromToken when navigating from asset
---
ui/hooks/bridge/useBridging.test.ts | 8 ++--
ui/hooks/bridge/useBridging.ts | 11 +++--
ui/hooks/useTokensWithFiltering.ts | 16 +++++++
.../bridge/prepare/prepare-bridge-page.tsx | 43 +++++++++++++++++++
4 files changed, 68 insertions(+), 10 deletions(-)
diff --git a/ui/hooks/bridge/useBridging.test.ts b/ui/hooks/bridge/useBridging.test.ts
index 6e3f3b534e35..9fe02c439048 100644
--- a/ui/hooks/bridge/useBridging.test.ts
+++ b/ui/hooks/bridge/useBridging.test.ts
@@ -123,19 +123,19 @@ describe('useBridging', () => {
// @ts-expect-error This is missing from the Mocha type definitions
it.each([
[
- '/cross-chain/swaps/prepare-swap-page',
+ '/cross-chain/swaps/prepare-swap-page?token=0x0000000000000000000000000000000000000000',
ETH_SWAPS_TOKEN_OBJECT,
'Home',
undefined,
],
[
- '/cross-chain/swaps/prepare-swap-page',
+ '/cross-chain/swaps/prepare-swap-page?token=0x0000000000000000000000000000000000000000',
ETH_SWAPS_TOKEN_OBJECT,
MetaMetricsSwapsEventSource.TokenView,
'&token=native',
],
[
- '/cross-chain/swaps/prepare-swap-page',
+ '/cross-chain/swaps/prepare-swap-page?token=0x00232f2jksdauo',
{
iconUrl: 'https://icon.url',
symbol: 'TEST',
@@ -174,7 +174,7 @@ describe('useBridging', () => {
result.current.openBridgeExperience(location, token, urlSuffix);
- expect(mockDispatch.mock.calls).toHaveLength(2);
+ expect(mockDispatch.mock.calls).toHaveLength(1);
expect(mockHistoryPush.mock.calls).toHaveLength(1);
expect(mockHistoryPush).toHaveBeenCalledWith(expectedUrl);
expect(openTabSpy).not.toHaveBeenCalled();
diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts
index c4ae1cca57a3..327951fcccc1 100644
--- a/ui/hooks/bridge/useBridging.ts
+++ b/ui/hooks/bridge/useBridging.ts
@@ -28,7 +28,6 @@ import {
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
import { isHardwareKeyring } from '../../helpers/utils/hardware';
import { getPortfolioUrl } from '../../helpers/utils/portfolio';
-import { setSwapsFromToken } from '../../ducks/swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { getProviderConfig } from '../../../shared/modules/selectors/networks';
///: END:ONLY_INCLUDE_IF
@@ -74,9 +73,6 @@ const useBridging = () => {
chain_id: providerConfig.chainId,
},
});
- dispatch(
- setSwapsFromToken({ ...token, address: token.address.toLowerCase() }),
- );
if (usingHardwareWallet && global.platform.openExtensionInBrowser) {
global.platform.openExtensionInBrowser(
PREPARE_SWAP_ROUTE,
@@ -84,7 +80,11 @@ const useBridging = () => {
false,
);
} else {
- history.push(CROSS_CHAIN_SWAP_ROUTE + PREPARE_SWAP_ROUTE);
+ history.push(
+ `${
+ CROSS_CHAIN_SWAP_ROUTE + PREPARE_SWAP_ROUTE
+ }?token=${token.address.toLowerCase()}`,
+ );
}
} else {
const portfolioUrl = getPortfolioUrl(
@@ -115,7 +115,6 @@ const useBridging = () => {
[
isBridgeSupported,
isBridgeChain,
- setSwapsFromToken,
dispatch,
usingHardwareWallet,
history,
diff --git a/ui/hooks/useTokensWithFiltering.ts b/ui/hooks/useTokensWithFiltering.ts
index a7ff3f2513ac..d729ce3c1fdc 100644
--- a/ui/hooks/useTokensWithFiltering.ts
+++ b/ui/hooks/useTokensWithFiltering.ts
@@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
import { isEqual } from 'lodash';
import { ChainId, hexToBN } from '@metamask/controller-utils';
import { Hex } from '@metamask/utils';
+import { useParams } from 'react-router-dom';
import {
getAllTokens,
getCurrentCurrency,
@@ -39,6 +40,8 @@ export const useTokensWithFiltering = (
sortOrder: TokenBucketPriority = TokenBucketPriority.owned,
chainId?: ChainId | Hex,
) => {
+ const { token: tokenAddressFromUrl } = useParams();
+
// Only includes non-native tokens
const allDetectedTokens = useSelector(getAllTokens);
const { address: selectedAddress, balance: balanceOnActiveChain } =
@@ -123,6 +126,18 @@ export const useTokensWithFiltering = (
yield nativeToken;
}
+ if (tokenAddressFromUrl) {
+ const tokenListItem =
+ tokenList?.[tokenAddressFromUrl] ??
+ tokenList?.[tokenAddressFromUrl.toLowerCase()];
+ if (tokenListItem) {
+ const tokenWithTokenListData = buildTokenData(tokenListItem);
+ if (tokenWithTokenListData) {
+ yield tokenWithTokenListData;
+ }
+ }
+ }
+
if (sortOrder === TokenBucketPriority.owned) {
for (const tokenWithBalance of sortedErc20TokensWithBalances) {
const cachedTokenData =
@@ -171,6 +186,7 @@ export const useTokensWithFiltering = (
currentCurrency,
chainId,
tokenList,
+ tokenAddressFromUrl,
],
);
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 8fde7817af9a..f6f02edb95c3 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -4,6 +4,7 @@ import classnames from 'classnames';
import { debounce } from 'lodash';
import { Hex } from '@metamask/utils';
import { zeroAddress } from 'ethereumjs-util';
+import { useHistory, useLocation } from 'react-router-dom';
import {
setDestTokenExchangeRates,
setFromChain,
@@ -147,6 +148,48 @@ const PrepareBridgePage = () => {
SECOND,
);
+ const { search } = useLocation();
+ const history = useHistory();
+
+ useEffect(() => {
+ if (!fromChain?.chainId || Object.keys(fromTokens).length === 0) {
+ return;
+ }
+
+ const searchParams = new URLSearchParams(search);
+ const tokenAddressFromUrl = searchParams.get('token');
+ if (!tokenAddressFromUrl) {
+ return;
+ }
+
+ const removeTokenFromUrl = () => {
+ const newParams = new URLSearchParams(searchParams);
+ newParams.delete('token');
+ history.replace({
+ search: newParams.toString(),
+ });
+ };
+
+ switch (tokenAddressFromUrl) {
+ case fromToken?.address?.toLowerCase():
+ // If the token is already set, remove the query param
+ removeTokenFromUrl();
+ break;
+ case fromTokens[tokenAddressFromUrl]?.address?.toLowerCase(): {
+ // If there is a matching fromToken, set it as the fromToken
+ const matchedToken = fromTokens[tokenAddressFromUrl];
+ dispatch(setFromToken(matchedToken));
+ debouncedFetchFromExchangeRate(fromChain.chainId, matchedToken.address);
+ removeTokenFromUrl();
+ break;
+ }
+ default:
+ // Otherwise remove query param
+ removeTokenFromUrl();
+ break;
+ }
+ }, [fromChain, fromToken, fromTokens, search]);
+
return (
From 04faf4d19a502b9f16c21c06fb7fe36cea9d42fa Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 22 Oct 2024 18:32:38 -0700
Subject: [PATCH 11/25] chore: show all quotes in a modal
---
app/_locales/en/messages.json | 3 +
ui/ducks/bridge/selectors.ts | 7 +-
.../quotes/bridge-quotes-modal.stories.tsx | 102 ++++++++
.../bridge/quotes/bridge-quotes-modal.tsx | 218 +++++++++++++-----
ui/pages/bridge/quotes/index.scss | 81 +++++++
ui/pages/bridge/utils/quote.ts | 2 +-
6 files changed, 347 insertions(+), 66 deletions(-)
create mode 100644 ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 3d0e91a0a1a0..8abefa79fdde 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -863,6 +863,9 @@
"bridgeFrom": {
"message": "Bridge from"
},
+ "bridgeLowest": {
+ "message": "Lowest"
+ },
"bridgeOverallCost": {
"message": "Overall cost"
},
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 20c35541ef89..15104d0aaa69 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -182,7 +182,8 @@ const _getBridgeFeesPerGas = createSelector(
}),
);
-const _getBridgeSortOrder = (state: BridgeAppState) => state.bridge.sortOrder;
+export const getBridgeSortOrder = (state: BridgeAppState) =>
+ state.bridge.sortOrder;
// A dest network can be selected before it's imported
// The cached exchange rate won't be available so the rate from the bridge state is used
@@ -248,7 +249,7 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
const _getSortedQuotesWithMetadata = createDeepEqualSelector(
_getQuotesWithMetadata,
- _getBridgeSortOrder,
+ getBridgeSortOrder,
(quotesWithMetadata, sortOrder) => {
switch (sortOrder) {
case SortOrder.ETA_ASC:
@@ -266,7 +267,7 @@ const _getSortedQuotesWithMetadata = createDeepEqualSelector(
const _getRecommendedQuote = createDeepEqualSelector(
_getSortedQuotesWithMetadata,
- _getBridgeSortOrder,
+ getBridgeSortOrder,
(sortedQuotesWithMetadata, sortOrder) => {
if (!sortedQuotesWithMetadata.length) {
return undefined;
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
new file mode 100644
index 000000000000..2badd6f989e7
--- /dev/null
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import configureStore from '../../../store/store';
+import { BridgeQuotesModal } from './bridge-quotes-modal';
+import { createBridgeMockStore } from '../../../../test/jest/mock-store';
+import mockBridgeQuotesErc20Erc20 from '../../../../test/data/bridge/mock-quotes-erc20-erc20.json';
+
+const storybook = {
+ title: 'Pages/Bridge/BridgeQuotesModal',
+ component: BridgeQuotesModal,
+};
+
+export const NoTokenPricesAvailableStory = () => {
+ return {}} isOpen={true} />;
+};
+NoTokenPricesAvailableStory.storyName = 'Token Prices Not Available';
+NoTokenPricesAvailableStory.decorators = [
+ (story) => (
+
+ {story()}
+
+ ),
+];
+
+export const DefaultStory = () => {
+ return {}} isOpen={true} />;
+};
+DefaultStory.storyName = 'Default';
+DefaultStory.decorators = [
+ (story) => (
+
+ {story()}
+
+ ),
+];
+
+export const PositiveArbitrage = () => {
+ return {}} isOpen={true} />;
+};
+PositiveArbitrage.decorators = [
+ (story) => (
+
+ {story()}
+
+ ),
+];
+
+export default storybook;
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index 0a9de383e7f2..f053c2be36fc 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -1,10 +1,9 @@
-import React from 'react';
+import React, { useState } from 'react';
import { IconName } from '@metamask/snaps-sdk/jsx';
import { useDispatch, useSelector } from 'react-redux';
import {
Box,
Button,
- ButtonVariant,
Icon,
IconSize,
Modal,
@@ -17,12 +16,21 @@ import {
TextAlign,
TextVariant,
} from '../../../helpers/constants/design-system';
-import { formatEtaInMinutes, formatFiatAmount } from '../utils/quote';
-import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
+import {
+ formatEtaInMinutes,
+ formatFiatAmount,
+ formatTokenAmount,
+} from '../utils/quote';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getCurrentCurrency } from '../../../selectors';
import { setSortOrder } from '../../../ducks/bridge/actions';
-import { SortOrder } from '../types';
+import { SortOrder, QuoteMetadata, QuoteResponse } from '../types';
+import { Footer } from '../../../components/multichain/pages/page';
+import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
+import {
+ getBridgeQuotes,
+ getBridgeSortOrder,
+} from '../../../ducks/bridge/selectors';
export const BridgeQuotesModal = ({
onClose,
@@ -31,69 +39,155 @@ export const BridgeQuotesModal = ({
const t = useI18nContext();
const dispatch = useDispatch();
- const { sortedQuotes } = useSelector(getBridgeQuotes);
+ const { sortedQuotes, activeQuote, recommendedQuote } =
+ useSelector(getBridgeQuotes);
+ const sortOrder = useSelector(getBridgeSortOrder);
const currency = useSelector(getCurrentCurrency);
+ const { isLoading } = useSelector(getBridgeQuotes);
+
+ const secondsUntilNextRefresh = useCountdownTimer();
+
+ const [expandedQuote, setExpandedQuote] = useState<
+ (QuoteResponse & QuoteMetadata) | undefined
+ >(undefined);
return (
-
-
-
- {t('swapSelectAQuote')}
-
-
-
-
-
-
-
- {sortedQuotes.map((quote, index) => {
- const {
- totalNetworkFee,
- sentAmount,
- adjustedReturn,
- estimatedProcessingTimeInSeconds,
- quote: { bridges },
- } = quote;
- return (
-
- {formatFiatAmount(adjustedReturn.fiat, currency)}
- {formatFiatAmount(totalNetworkFee?.fiat, currency)}
-
- {adjustedReturn.fiat && sentAmount.fiat
- ? `-${formatFiatAmount(
- sentAmount.fiat.minus(adjustedReturn.fiat),
- currency,
- )}`
- : ''}
-
- {bridges[0]}
-
- {t('bridgeTimingMinutes', [
- formatEtaInMinutes(estimatedProcessingTimeInSeconds),
- ])}
-
-
- );
- })}
-
-
+ {expandedQuote ? (
+
+ setExpandedQuote(undefined)}>
+
+ {t('swapQuoteDetails')}
+
+
+
+ {JSON.stringify(expandedQuote)}
+
+
+
+ ) : (
+
+
+
+ {t('swapSelectAQuote')}
+
+
+
+ {/* HEADERS */}
+
+
+ dispatch(setSortOrder(SortOrder.ADJUSTED_RETURN_DESC))
+ }
+ className={
+ sortOrder === SortOrder.ADJUSTED_RETURN_DESC
+ ? 'active-sort'
+ : ''
+ }
+ >
+
+ {t('bridgeOverallCost')}
+
+ dispatch(setSortOrder(SortOrder.ETA_ASC))}
+ className={sortOrder === SortOrder.ETA_ASC ? 'active-sort' : ''}
+ >
+
+ {t('time')}
+
+
+ {/* QUOTE LIST */}
+
+ {sortedQuotes.map((quote, index) => {
+ const {
+ totalNetworkFee,
+ estimatedProcessingTimeInSeconds,
+ toTokenAmount,
+ cost,
+ quote: { destAsset, bridges, requestId },
+ } = quote;
+ const isQuoteActive = requestId === activeQuote?.quote.requestId;
+ const isQuoteRecommended =
+ requestId === recommendedQuote?.quote.requestId;
+
+ return (
+ setExpandedQuote(quote)}
+ >
+ {isQuoteActive && (
+
+ )}
+
+ {cost.fiat && (
+
+ {isQuoteRecommended && (
+
+ {t(
+ sortOrder === SortOrder.ADJUSTED_RETURN_DESC
+ ? 'bridgeLowest'
+ : 'bridgeFastest',
+ )}
+
+ )}
+ {formatFiatAmount(cost.fiat, currency)}
+
+ )}
+
+
+ {formatFiatAmount(toTokenAmount.fiat, currency) ??
+ formatTokenAmount(
+ toTokenAmount.raw,
+ destAsset.symbol,
+ )}
+
+
+
+
+ {formatFiatAmount(totalNetworkFee?.fiat, currency)}
+
+
+
+
+
+ {bridges[0]}
+
+ {t('bridgeTimingMinutes', [
+ formatEtaInMinutes(estimatedProcessingTimeInSeconds),
+ ])}
+
+
+
+
+ );
+ })}
+
+
+ {!isLoading && (
+ {t('swapNewQuoteIn', [secondsUntilNextRefresh])}
+ )}
+
+
+ )}
);
};
diff --git a/ui/pages/bridge/quotes/index.scss b/ui/pages/bridge/quotes/index.scss
index 6d52c9e1e753..ba0d8ac62e16 100644
--- a/ui/pages/bridge/quotes/index.scss
+++ b/ui/pages/bridge/quotes/index.scss
@@ -64,20 +64,101 @@
}
.quotes-modal {
+ .mm-modal-content__dialog {
+ display: flex;
+ position: relative;
+ height: 100%;
+ }
+
&__column-header {
display: flex;
flex-direction: row;
justify-content: space-between;
+ padding: 12px 16px 4px 16px;
+ color: var(--color-text-alternative);
+
+ .active-sort {
+ color: var(--color-text-default);
+ }
+
+ span, p {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding-right: 26px;
+ color: inherit;
+ }
}
&__quotes {
display: flex;
flex-direction: column;
+ .active-quote {
+ background: var(--color-primary-muted);
+ }
+
&__row {
display: flex;
flex-direction: row;
justify-content: space-between;
+ height: 66px;
+ align-items: center;
+ flex-shrink: 0;
+ align-self: stretch;
+ padding: 12px 16px;
+ position: relative;
+
+ &-bar {
+ position: absolute;
+ left: 4px;
+ top: 4px;
+ height: 58px;
+ width: 4px;
+ border-radius: 8px;
+ background: var(--color-primary-default);
+ }
+
+ span {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ &-left {
+ flex-direction: column;
+ gap: 4px;
+
+ span {
+ flex-direction: row;
+ gap: 10px;
+
+ .description {
+ color: var(--color-primary-default);
+ }
+
+ & > span {
+ align-items: center;
+ gap: 4px;
+ }
+ }
+ }
+
+ &-right {
+ flex-direction: row;
+ gap: 10px;
+ }
}
}
+
+ &__timer {
+ display: flex;
+ position: absolute;
+ width: 100%;
+ height: 70px;
+ justify-content: center;
+ align-items: center;
+ left: 4px;
+ bottom: 4px;
+ color: var(--text-alternative-soft);
+ }
}
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 27e7edc8d1c8..73db9fa818bc 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -177,7 +177,7 @@ export const calcCost = (
) => ({
fiat:
adjustedReturnInFiat && sentAmountInFiat
- ? adjustedReturnInFiat.minus(sentAmountInFiat)
+ ? sentAmountInFiat.minus(adjustedReturnInFiat)
: null,
});
From 7c24aecd9cd627343e4b531e73c3c9483ad348c5 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 24 Oct 2024 14:54:22 -0700
Subject: [PATCH 12/25] chore: enable selecting alternative quote
---
ui/ducks/bridge/actions.ts | 2 ++
ui/ducks/bridge/bridge.ts | 11 +++++++-
ui/ducks/bridge/selectors.ts | 28 +++++++++++++++++--
.../bridge/prepare/prepare-bridge-page.tsx | 9 +++---
.../bridge/quotes/bridge-quotes-modal.tsx | 5 ++--
5 files changed, 46 insertions(+), 9 deletions(-)
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index 76ac75bb7b75..766689cb8cda 100644
--- a/ui/ducks/bridge/actions.ts
+++ b/ui/ducks/bridge/actions.ts
@@ -24,6 +24,7 @@ const {
setFromTokenInputValue,
resetInputFields,
setSortOrder,
+ setSelectedQuote,
} = bridgeSlice.actions;
export {
@@ -35,6 +36,7 @@ export {
setDestTokenExchangeRates,
setSrcTokenExchangeRates,
setSortOrder,
+ setSelectedQuote,
};
const callBridgeControllerMethod = (
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 22a780081499..91d790ad83a2 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -5,7 +5,11 @@ import { swapsSlice } from '../swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { SwapsEthToken } from '../../selectors';
import { fetchTokenExchangeRates } from '../../helpers/utils/util';
-import { SortOrder } from '../../pages/bridge/types';
+import {
+ QuoteMetadata,
+ QuoteResponse,
+ SortOrder,
+} from '../../pages/bridge/types';
export type BridgeState = {
toChainId: Hex | null;
@@ -15,6 +19,7 @@ export type BridgeState = {
fromTokenExchangeRate: number | null;
toTokenExchangeRate: number | null;
sortOrder: SortOrder;
+ selectedQuote: (QuoteResponse & QuoteMetadata) | null; // Alternate quote selected by user. When quotes refresh, the best match will be activated.
};
const initialState: BridgeState = {
@@ -25,6 +30,7 @@ const initialState: BridgeState = {
fromTokenExchangeRate: null,
toTokenExchangeRate: null,
sortOrder: SortOrder.COST_ASC,
+ selectedQuote: null,
};
export const setSrcTokenExchangeRates = createAsyncThunk(
@@ -78,6 +84,9 @@ const bridgeSlice = createSlice({
setSortOrder: (state, action) => {
state.sortOrder = action.payload;
},
+ setSelectedQuote: (state, action) => {
+ state.selectedQuote = action.payload;
+ },
},
extraReducers: (builder) => {
builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 15104d0aaa69..07f6f5c54c62 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -34,6 +34,7 @@ import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
// eslint-disable-next-line import/no-restricted-paths
import { RequestStatus } from '../../../app/scripts/controllers/bridge/constants';
import {
+ L1GasFees,
QuoteMetadata,
QuoteResponse,
SortOrder,
@@ -215,7 +216,7 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
nativeExchangeRate,
{ estimatedBaseFeeInDecGwei, maxPriorityFeePerGasInDecGwei },
): (QuoteResponse & QuoteMetadata)[] => {
- return quotes.map((quote: QuoteResponse) => {
+ const newQuotes = quotes.map((quote: QuoteResponse) => {
const toTokenAmount = calcToAmount(quote.quote, toTokenExchangeRate);
const totalNetworkFee = calcTotalNetworkFee(
quote,
@@ -244,6 +245,8 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
cost: calcCost(adjustedReturn.fiat, sentAmount.fiat),
};
});
+
+ return newQuotes;
},
);
@@ -304,9 +307,29 @@ const _getRecommendedQuote = createDeepEqualSelector(
},
);
+// Identifies each quote by aggregator, bridge, steps and value
+const getDedupeString = ({ quote }: QuoteResponse & L1GasFees) =>
+ `${quote.bridgeId}-${quote.bridges[0]}-${quote.steps.length}`;
+
+const _getSelectedQuote = createSelector(
+ (state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount,
+ (state: BridgeAppState) => state.bridge.selectedQuote,
+ _getSortedQuotesWithMetadata,
+ (quotesRefreshCount, selectedQuote, sortedQuotesWithMetadata) =>
+ quotesRefreshCount <= 1
+ ? selectedQuote
+ : // Find match for selectedQuote in new quotes
+ sortedQuotesWithMetadata.find((quote) =>
+ selectedQuote
+ ? getDedupeString(quote) === getDedupeString(selectedQuote)
+ : false,
+ ),
+);
+
export const getBridgeQuotes = createSelector(
_getSortedQuotesWithMetadata,
_getRecommendedQuote,
+ _getSelectedQuote,
(state) => state.metamask.bridgeState.quotesLastFetched,
(state) =>
state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING,
@@ -316,6 +339,7 @@ export const getBridgeQuotes = createSelector(
(
sortedQuotesWithMetadata,
recommendedQuote,
+ selectedQuote,
quotesLastFetchedMs,
isLoading,
quotesRefreshCount,
@@ -324,7 +348,7 @@ export const getBridgeQuotes = createSelector(
) => ({
sortedQuotes: sortedQuotesWithMetadata,
recommendedQuote,
- activeQuote: recommendedQuote,
+ activeQuote: selectedQuote ?? recommendedQuote,
quotesLastFetchedMs,
isLoading,
quotesRefreshCount,
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index f6f02edb95c3..13525f35c372 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -11,6 +11,7 @@ import {
setFromToken,
setFromTokenInputValue,
setSrcTokenExchangeRates,
+ setSelectedQuote,
setToChain,
setToChainId,
setToToken,
@@ -123,10 +124,10 @@ const PrepareBridgePage = () => {
);
const debouncedUpdateQuoteRequestInController = useCallback(
- debounce(
- (p: Partial) => dispatch(updateQuoteRequestParams(p)),
- 300,
- ),
+ debounce((p: Partial) => {
+ dispatch(updateQuoteRequestParams(p));
+ dispatch(setSelectedQuote(null));
+ }, 300),
[],
);
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
index f053c2be36fc..f8d65f015b95 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -23,7 +23,7 @@ import {
} from '../utils/quote';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getCurrentCurrency } from '../../../selectors';
-import { setSortOrder } from '../../../ducks/bridge/actions';
+import { setSelectedQuote, setSortOrder } from '../../../ducks/bridge/actions';
import { SortOrder, QuoteMetadata, QuoteResponse } from '../types';
import { Footer } from '../../../components/multichain/pages/page';
import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
@@ -69,7 +69,8 @@ export const BridgeQuotesModal = ({
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx
index 274ade65a4d1..7de52fef1d58 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.test.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.test.tsx
@@ -20,6 +20,7 @@ describe('BridgeQuoteCard', () => {
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
destNetworkAllowlist: [CHAIN_IDS.OPTIMISM],
+ extensionConfig: { maxRefreshCount: 5, refreshRate: 30000 },
},
{ fromTokenInputValue: 1 },
{
@@ -28,9 +29,6 @@ describe('BridgeQuoteCard', () => {
quotes: mockBridgeQuotesErc20Erc20,
getQuotesLastFetched: Date.now(),
quotesLoadingStatus: RequestStatus.FETCHED,
- bridgeFeatureFlags: {
- extensionConfig: { maxRefreshCount: 5, refreshRate: 30000 },
- },
},
);
const { container } = renderWithProvider(
diff --git a/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
index 24f2e40231c5..bbdf9b47fa47 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
@@ -39,16 +39,15 @@ DefaultStory.decorators = [
store={configureStore(
createBridgeMockStore({
bridgeSliceOverrides: {
- fromNativeExchangeRate: 1,
fromTokenExchangeRate: 0.99,
toNativeExchangeRate: 1,
toTokenExchangeRate: 0.99,
- sortOrder: SortOrder.ADJUSTED_RETURN_DESC,
+ sortOrder: SortOrder.COST_ASC,
},
bridgeStateOverrides: { quotes: mockBridgeQuotesErc20Erc20 },
metamaskStateOverrides: {
currencyRates: {
- ETH: { conversionRate: 2514.5 },
+ ETH: { conversionRate: 2514.5 }, //1
},
marketData: {
'0x1': {
@@ -77,7 +76,6 @@ PositiveArbitrage.decorators = [
store={configureStore(
createBridgeMockStore({
bridgeSliceOverrides: {
- fromNativeExchangeRate: 1,
fromTokenExchangeRate: 0.99,
toNativeExchangeRate: 1,
toTokenExchangeRate: 2.1,
diff --git a/ui/pages/bridge/utils/quote.test.ts b/ui/pages/bridge/utils/quote.test.ts
index d3b0ceaf8137..883f2423b1f5 100644
--- a/ui/pages/bridge/utils/quote.test.ts
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -22,21 +22,21 @@ describe('Bridge quote utils', () => {
'native',
NATIVE_TOKEN,
'1009000000000000000',
- { toTokenExchangeRate: 1, toNativeExchangeRate: 2521.73 },
+ 2521.73,
{ raw: '1.009', fiat: '2544.42557' },
],
[
'erc20',
ERC20_TOKEN,
'2543140000',
- { toTokenExchangeRate: 0.999781, toNativeExchangeRate: 0.352999 },
+ 0.999781,
{ raw: '2543.14', fiat: '2542.58305234' },
],
[
'erc20 with null exchange rates',
ERC20_TOKEN,
'2543140000',
- { toTokenExchangeRate: null, toNativeExchangeRate: null },
+ null,
{ raw: '2543.14', fiat: undefined },
],
])(
@@ -45,10 +45,7 @@ describe('Bridge quote utils', () => {
_: string,
destAsset: { decimals: number; address: string },
destTokenAmount: string,
- {
- toTokenExchangeRate,
- toNativeExchangeRate,
- }: { toTokenExchangeRate: number; toNativeExchangeRate: number },
+ toTokenExchangeRate: number,
{ raw, fiat }: { raw: string; fiat: string },
) => {
const result = calcToAmount(
@@ -57,7 +54,6 @@ describe('Bridge quote utils', () => {
destTokenAmount,
} as never,
toTokenExchangeRate,
- toNativeExchangeRate,
);
expect(result.raw?.toString()).toStrictEqual(raw);
expect(result.fiat?.toString()).toStrictEqual(fiat);
@@ -70,7 +66,6 @@ describe('Bridge quote utils', () => {
'native',
NATIVE_TOKEN,
'1009000000000000000',
- 1.0000825923770915,
2515.02,
{
raw: '1.143217728',
@@ -82,14 +77,12 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
0.999781,
- 2517.14,
{ raw: '100.512', fiat: '100.489987872' },
],
[
'erc20 with null exchange rates',
ERC20_TOKEN,
'2543140000',
- {},
null,
{ raw: '2543.652', fiat: undefined },
],
@@ -100,7 +93,6 @@ describe('Bridge quote utils', () => {
srcAsset: { decimals: number; address: string },
srcTokenAmount: string,
fromTokenExchangeRate: number,
- fromNativeExchangeRate: number,
{ raw, fiat }: { raw: string; fiat: string },
) => {
const result = calcSentAmount(
@@ -114,7 +106,6 @@ describe('Bridge quote utils', () => {
},
} as never,
fromTokenExchangeRate,
- fromNativeExchangeRate,
);
expect(result.raw?.toString()).toStrictEqual(raw);
expect(result.fiat?.toString()).toStrictEqual(fiat);
@@ -182,8 +173,8 @@ describe('Bridge quote utils', () => {
: undefined,
quote: { srcAsset, srcTokenAmount, feeData },
} as never,
- '0x19870',
- '0x186a0',
+ '0.00010456',
+ '0.0001',
2517.42,
);
expect(result.raw?.toString()).toStrictEqual(raw);
From bff17b7c88f80e9fd3bfa3910f493cecbbfbd360 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 19 Nov 2024 14:16:02 -0800
Subject: [PATCH 16/25] fix: stringify exchange rates before multiplying
---
ui/pages/bridge/utils/quote.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 1e8379417538..3bcb418cc9b8 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -55,7 +55,9 @@ export const calcToAmount = (
);
return {
raw: normalizedDestAmount,
- fiat: exchangeRate ? normalizedDestAmount.mul(exchangeRate) : null,
+ fiat: exchangeRate
+ ? normalizedDestAmount.mul(exchangeRate.toString())
+ : null,
};
};
@@ -94,7 +96,7 @@ const calcRelayerFee = (
return {
raw: relayerFeeInNative,
fiat: nativeExchangeRate
- ? relayerFeeInNative.mul(nativeExchangeRate)
+ ? relayerFeeInNative.mul(nativeExchangeRate.toString())
: null,
};
};
@@ -129,7 +131,7 @@ const calcTotalGasFee = (
gasFeesInDecGwei.shiftedBy(9).toString(),
);
const gasFeesInUSD = nativeExchangeRate
- ? gasFeesInDecEth.times(nativeExchangeRate)
+ ? gasFeesInDecEth.times(nativeExchangeRate.toString())
: null;
return {
From 067af93f09d0176fc95efc1aa13fa384d442689d Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Tue, 19 Nov 2024 16:28:15 -0800
Subject: [PATCH 17/25] chore: move header icon to end
---
.../bridge-quotes-modal.test.tsx.snap | 8 ++++----
ui/pages/bridge/quotes/bridge-quotes-modal.tsx | 15 ++++++++++++++-
2 files changed, 18 insertions(+), 5 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 09c78928f82f..0f01617702ca 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
@@ -62,10 +62,6 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
-
@@ -75,6 +71,10 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
Net cost
+
dispatch(setSortOrder(sortOrderOption))}
- startIconName={sortOrder === sortOrderOption ? icon : undefined}
+ startIconName={
+ sortOrder === sortOrderOption && sortOrder === SortOrder.ETA_ASC
+ ? icon
+ : undefined
+ }
startIconProps={{
size: IconSize.Xs,
}}
+ endIconName={
+ sortOrder === sortOrderOption &&
+ sortOrder === SortOrder.COST_ASC
+ ? icon
+ : undefined
+ }
+ endIconProps={{
+ size: IconSize.Xs,
+ }}
color={
sortOrder === sortOrderOption
? TextColor.primaryDefault
From 5b9d431e7a9a6119b9ec68585cf40da6b750d1e0 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 20 Nov 2024 10:15:34 -0800
Subject: [PATCH 18/25] fix: scroll in bridge-quotes-modal
---
.../quotes/__snapshots__/bridge-quotes-modal.test.tsx.snap | 1 +
ui/pages/bridge/quotes/bridge-quotes-modal.tsx | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
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 0f01617702ca..137dc246864e 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
@@ -92,6 +92,7 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
{/* QUOTE LIST */}
-
+
{sortedQuotes.map((quote, index) => {
const {
totalNetworkFee,
From 90a4262254d57af0981b30e6104a44eef2c5aed4 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 20 Nov 2024 10:16:19 -0800
Subject: [PATCH 19/25] chore: rename raw to amount
---
ui/ducks/bridge/selectors.test.ts | 18 +++----
ui/ducks/bridge/selectors.ts | 2 +-
.../bridge/prepare/prepare-bridge-page.tsx | 4 +-
ui/pages/bridge/quotes/bridge-quote-card.tsx | 8 +++-
.../bridge/quotes/bridge-quotes-modal.tsx | 4 +-
ui/pages/bridge/types.ts | 6 +--
ui/pages/bridge/utils/quote.test.ts | 48 +++++++++----------
ui/pages/bridge/utils/quote.ts | 10 ++--
8 files changed, 52 insertions(+), 48 deletions(-)
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index ebd6aa789c5f..b26de01e8fa3 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -525,16 +525,16 @@ describe('Bridge selectors', () => {
cost: { fiat: new BigNumber('0.15656287141025952') },
sentAmount: {
fiat: new BigNumber('14'),
- raw: new BigNumber('14'),
+ amount: new BigNumber('14'),
},
swapRate: new BigNumber('0.998877142857142857142857142857142857'),
toTokenAmount: {
fiat: new BigNumber('13.8444372'),
- raw: new BigNumber('13.98428'),
+ amount: new BigNumber('13.98428'),
},
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
- raw: new BigNumber('0.00100007141025952'),
+ amount: new BigNumber('0.00100007141025952'),
},
};
@@ -592,16 +592,16 @@ describe('Bridge selectors', () => {
cost: { fiat: new BigNumber('0.15656287141025952') },
sentAmount: {
fiat: new BigNumber('14'),
- raw: new BigNumber('14'),
+ amount: new BigNumber('14'),
},
swapRate: new BigNumber('0.998877142857142857142857142857142857'),
toTokenAmount: {
fiat: new BigNumber('13.8444372'),
- raw: new BigNumber('13.98428'),
+ amount: new BigNumber('13.98428'),
},
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
- raw: new BigNumber('0.00100007141025952'),
+ amount: new BigNumber('0.00100007141025952'),
},
};
expect(result.sortedQuotes).toHaveLength(2);
@@ -664,16 +664,16 @@ describe('Bridge selectors', () => {
cost: { fiat: new BigNumber('0.15656287141025952') },
sentAmount: {
fiat: new BigNumber('14'),
- raw: new BigNumber('14'),
+ amount: new BigNumber('14'),
},
swapRate: new BigNumber('0.998877142857142857142857142857142857'),
toTokenAmount: {
fiat: new BigNumber('13.8444372'),
- raw: new BigNumber('13.98428'),
+ amount: new BigNumber('13.98428'),
},
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
- raw: new BigNumber('0.00100007141025952'),
+ amount: new BigNumber('0.00100007141025952'),
},
};
expect(result.sortedQuotes).toHaveLength(2);
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 07f6f5c54c62..01c7068e9193 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -241,7 +241,7 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
sentAmount,
totalNetworkFee,
adjustedReturn,
- swapRate: calcSwapRate(sentAmount.raw, toTokenAmount.raw),
+ swapRate: calcSwapRate(sentAmount.amount, toTokenAmount.amount),
cost: calcCost(adjustedReturn.fiat, sentAmount.fiat),
};
});
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index 13525f35c372..ba6a540719d3 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -305,8 +305,8 @@ const PrepareBridgePage = () => {
testId: 'to-amount',
readOnly: true,
disabled: true,
- value: activeQuote?.toTokenAmount?.raw.toFixed(2) ?? '0',
- className: activeQuote?.toTokenAmount.raw
+ value: activeQuote?.toTokenAmount?.amount.toFixed(2) ?? '0',
+ className: activeQuote?.toTokenAmount.amount
? 'amount-input defined'
: 'amount-input',
}}
diff --git a/ui/pages/bridge/quotes/bridge-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index 7f4464233d81..8adf675afbb3 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -80,11 +80,15 @@ export const BridgeQuoteCard = () => {
currency,
2,
) ??
- formatTokenAmount(activeQuote.totalNetworkFee?.raw, ticker, 6)
+ formatTokenAmount(activeQuote.totalNetworkFee?.amount, ticker, 6)
}
secondaryDescription={
activeQuote.totalNetworkFee?.fiat
- ? formatTokenAmount(activeQuote.totalNetworkFee?.raw, ticker, 6)
+ ? formatTokenAmount(
+ 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 5f8ee3874c17..0f4986aa18fc 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -158,7 +158,7 @@ export const BridgeQuotesModal = ({
])
: t('quotedNetworkFee', [
formatTokenAmount(
- totalNetworkFee.raw,
+ totalNetworkFee.amount,
nativeCurrency,
),
]),
@@ -169,7 +169,7 @@ export const BridgeQuotesModal = ({
[
formatFiatAmount(toTokenAmount.fiat, currency, 0) ??
formatTokenAmount(
- toTokenAmount.raw,
+ toTokenAmount.amount,
destAsset.symbol,
0,
),
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index 4be72f200650..82fc6ffd5248 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -6,10 +6,10 @@ export type L1GasFees = {
// Values derived from the quote response
export type QuoteMetadata = {
- totalNetworkFee: { raw: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
- toTokenAmount: { raw: BigNumber; fiat: BigNumber | null };
+ totalNetworkFee: { amount: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
+ toTokenAmount: { amount: BigNumber; fiat: BigNumber | null };
adjustedReturn: { fiat: BigNumber | null }; // destTokenAmount - totalNetworkFee
- sentAmount: { raw: BigNumber; fiat: BigNumber | null }; // srcTokenAmount + metabridgeFee
+ sentAmount: { amount: BigNumber; fiat: BigNumber | null }; // srcTokenAmount + metabridgeFee
swapRate: BigNumber; // destTokenAmount / sentAmount
cost: { fiat: BigNumber | null }; // sentAmount - adjustedReturn
};
diff --git a/ui/pages/bridge/utils/quote.test.ts b/ui/pages/bridge/utils/quote.test.ts
index 883f2423b1f5..e6287a33e141 100644
--- a/ui/pages/bridge/utils/quote.test.ts
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -23,21 +23,21 @@ describe('Bridge quote utils', () => {
NATIVE_TOKEN,
'1009000000000000000',
2521.73,
- { raw: '1.009', fiat: '2544.42557' },
+ { amount: '1.009', fiat: '2544.42557' },
],
[
'erc20',
ERC20_TOKEN,
'2543140000',
0.999781,
- { raw: '2543.14', fiat: '2542.58305234' },
+ { amount: '2543.14', fiat: '2542.58305234' },
],
[
'erc20 with null exchange rates',
ERC20_TOKEN,
'2543140000',
null,
- { raw: '2543.14', fiat: undefined },
+ { amount: '2543.14', fiat: undefined },
],
])(
'calcToAmount: toToken is %s',
@@ -46,7 +46,7 @@ describe('Bridge quote utils', () => {
destAsset: { decimals: number; address: string },
destTokenAmount: string,
toTokenExchangeRate: number,
- { raw, fiat }: { raw: string; fiat: string },
+ { amount, fiat }: { amount: string; fiat: string },
) => {
const result = calcToAmount(
{
@@ -55,7 +55,7 @@ describe('Bridge quote utils', () => {
} as never,
toTokenExchangeRate,
);
- expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
);
@@ -68,7 +68,7 @@ describe('Bridge quote utils', () => {
'1009000000000000000',
2515.02,
{
- raw: '1.143217728',
+ amount: '1.143217728',
fiat: '2875.21545027456',
},
],
@@ -77,14 +77,14 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
0.999781,
- { raw: '100.512', fiat: '100.489987872' },
+ { amount: '100.512', fiat: '100.489987872' },
],
[
'erc20 with null exchange rates',
ERC20_TOKEN,
'2543140000',
null,
- { raw: '2543.652', fiat: undefined },
+ { amount: '2543.652', fiat: undefined },
],
])(
'calcSentAmount: fromToken is %s',
@@ -93,7 +93,7 @@ describe('Bridge quote utils', () => {
srcAsset: { decimals: number; address: string },
srcTokenAmount: string,
fromTokenExchangeRate: number,
- { raw, fiat }: { raw: string; fiat: string },
+ { amount, fiat }: { amount: string; fiat: string },
) => {
const result = calcSentAmount(
{
@@ -107,7 +107,7 @@ describe('Bridge quote utils', () => {
} as never,
fromTokenExchangeRate,
);
- expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
);
@@ -119,7 +119,7 @@ describe('Bridge quote utils', () => {
NATIVE_TOKEN,
'1000000000000000000',
'0x0de0b6b3a7640000',
- { raw: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ { amount: '2.2351800712e-7', fiat: '0.0005626887014840304' },
undefined,
],
[
@@ -127,7 +127,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x00',
- { raw: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ { amount: '2.2351800712e-7', fiat: '0.0005626887014840304' },
undefined,
],
[
@@ -135,7 +135,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x00',
- { raw: '4.4703601424e-7', fiat: '0.0011253774029680608' },
+ { amount: '4.4703601424e-7', fiat: '0.0011253774029680608' },
1092677,
],
[
@@ -143,7 +143,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x0de0b6b3a7640000',
- { raw: '1.00000022351800712', fiat: '2517.4205626887014840304' },
+ { amount: '1.00000022351800712', fiat: '2517.4205626887014840304' },
undefined,
],
[
@@ -151,7 +151,7 @@ describe('Bridge quote utils', () => {
NATIVE_TOKEN,
'1000000000000000000',
'0x0de1b6b3a7640000',
- { raw: '0.000281698494717776', fiat: '0.70915342457242365792' },
+ { amount: '0.000281698494717776', fiat: '0.70915342457242365792' },
undefined,
],
])(
@@ -161,7 +161,7 @@ describe('Bridge quote utils', () => {
srcAsset: { decimals: number; address: string },
srcTokenAmount: string,
value: string,
- { raw, fiat }: { raw: string; fiat: string },
+ { amount, fiat }: { amount: string; fiat: string },
approvalGasLimit?: number,
) => {
const feeData = { metabridge: { amount: 0 } };
@@ -177,7 +177,7 @@ describe('Bridge quote utils', () => {
'0.0001',
2517.42,
);
- expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
);
@@ -189,7 +189,7 @@ describe('Bridge quote utils', () => {
NATIVE_TOKEN,
'1000000000000000000',
'0x0de0b6b3a7640000',
- { raw: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ { amount: '0.000002832228395508', fiat: '0.00712990840741974936' },
undefined,
],
[
@@ -197,7 +197,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x00',
- { raw: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ { amount: '0.000002832228395508', fiat: '0.00712990840741974936' },
undefined,
],
[
@@ -205,7 +205,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x00',
- { raw: '0.000003055746402628', fiat: '0.00769259710890377976' },
+ { amount: '0.000003055746402628', fiat: '0.00769259710890377976' },
1092677,
],
[
@@ -213,7 +213,7 @@ describe('Bridge quote utils', () => {
ERC20_TOKEN,
'100000000',
'0x0de0b6b3a7640000',
- { raw: '1.000002832228395508', fiat: '2517.42712990840741974936' },
+ { amount: '1.000002832228395508', fiat: '2517.42712990840741974936' },
undefined,
],
[
@@ -221,7 +221,7 @@ describe('Bridge quote utils', () => {
NATIVE_TOKEN,
'1000000000000000000',
'0x0de1b6b3a7640000',
- { raw: '0.000284307205106164', fiat: '0.71572064427835937688' },
+ { amount: '0.000284307205106164', fiat: '0.71572064427835937688' },
undefined,
],
])(
@@ -231,7 +231,7 @@ describe('Bridge quote utils', () => {
srcAsset: { decimals: number; address: string },
srcTokenAmount: string,
value: string,
- { raw, fiat }: { raw: string; fiat: string },
+ { amount, fiat }: { amount: string; fiat: string },
approvalGasLimit?: number,
) => {
const feeData = { metabridge: { amount: 0 } };
@@ -248,7 +248,7 @@ describe('Bridge quote utils', () => {
'0.0001',
2517.42,
);
- expect(result.raw?.toString()).toStrictEqual(raw);
+ expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
);
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 3bcb418cc9b8..6b179fe33712 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -54,7 +54,7 @@ export const calcToAmount = (
destAsset.decimals,
);
return {
- raw: normalizedDestAmount,
+ amount: normalizedDestAmount,
fiat: exchangeRate
? normalizedDestAmount.mul(exchangeRate.toString())
: null,
@@ -70,7 +70,7 @@ export const calcSentAmount = (
srcAsset.decimals,
);
return {
- raw: normalizedSentAmount,
+ amount: normalizedSentAmount,
fiat: exchangeRate
? normalizedSentAmount.mul(exchangeRate.toString())
: null,
@@ -94,7 +94,7 @@ const calcRelayerFee = (
18,
);
return {
- raw: relayerFeeInNative,
+ amount: relayerFeeInNative,
fiat: nativeExchangeRate
? relayerFeeInNative.mul(nativeExchangeRate.toString())
: null,
@@ -135,7 +135,7 @@ const calcTotalGasFee = (
: null;
return {
- raw: gasFeesInDecEth,
+ amount: gasFeesInDecEth,
fiat: gasFeesInUSD,
};
};
@@ -154,7 +154,7 @@ export const calcTotalNetworkFee = (
);
const normalizedRelayerFee = calcRelayerFee(bridgeQuote, nativeExchangeRate);
return {
- raw: normalizedGasFee.raw.plus(normalizedRelayerFee.raw),
+ amount: normalizedGasFee.amount.plus(normalizedRelayerFee.amount),
fiat: normalizedGasFee.fiat?.plus(normalizedRelayerFee.fiat || '0') ?? null,
};
};
From 63770688e4313640bcbc5df4dd3840f30f8246e8 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 20 Nov 2024 10:19:29 -0800
Subject: [PATCH 20/25] chore: add comment to clarify fiat currency
---
ui/pages/bridge/types.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index 82fc6ffd5248..0c6eb8288797 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -5,6 +5,7 @@ export type L1GasFees = {
};
// Values derived from the quote response
+// fiat values are calculated based on the user's selected currency
export type QuoteMetadata = {
totalNetworkFee: { amount: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
toTokenAmount: { amount: BigNumber; fiat: BigNumber | null };
From 35a38b355ef934e3496f3b9996b30a86045ebe8d Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 20 Nov 2024 10:19:41 -0800
Subject: [PATCH 21/25] chore: rename quote identifier function
---
ui/ducks/bridge/selectors.ts | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 01c7068e9193..6c48e73e2891 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -307,8 +307,9 @@ const _getRecommendedQuote = createDeepEqualSelector(
},
);
-// Identifies each quote by aggregator, bridge, steps and value
-const getDedupeString = ({ quote }: QuoteResponse & L1GasFees) =>
+// Generates a pseudo-unique string that identifies each quote
+// by aggregator, bridge, steps and value
+const _getQuoteIdentifier = ({ quote }: QuoteResponse & L1GasFees) =>
`${quote.bridgeId}-${quote.bridges[0]}-${quote.steps.length}`;
const _getSelectedQuote = createSelector(
@@ -321,7 +322,7 @@ const _getSelectedQuote = createSelector(
: // Find match for selectedQuote in new quotes
sortedQuotesWithMetadata.find((quote) =>
selectedQuote
- ? getDedupeString(quote) === getDedupeString(selectedQuote)
+ ? _getQuoteIdentifier(quote) === _getQuoteIdentifier(selectedQuote)
: false,
),
);
From aca3fe2fc1eb3bdbe75f0b7581d820be519f5c9c Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Wed, 20 Nov 2024 10:21:57 -0800
Subject: [PATCH 22/25] chore: use template string for route path
---
ui/hooks/bridge/useBridging.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/ui/hooks/bridge/useBridging.ts b/ui/hooks/bridge/useBridging.ts
index 327951fcccc1..62945e3e7a92 100644
--- a/ui/hooks/bridge/useBridging.ts
+++ b/ui/hooks/bridge/useBridging.ts
@@ -81,9 +81,7 @@ const useBridging = () => {
);
} else {
history.push(
- `${
- CROSS_CHAIN_SWAP_ROUTE + PREPARE_SWAP_ROUTE
- }?token=${token.address.toLowerCase()}`,
+ `${CROSS_CHAIN_SWAP_ROUTE}${PREPARE_SWAP_ROUTE}?token=${token.address.toLowerCase()}`,
);
}
} else {
From 19d916ac16d9f60251a658ef8d2d9d95cd11b0be Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 21 Nov 2024 11:18:25 -0800
Subject: [PATCH 23/25] chore: include gasFees in metadata
---
ui/ducks/bridge/selectors.test.ts | 49 ++++++++++++++++---------
ui/ducks/bridge/selectors.ts | 11 ++++--
ui/pages/bridge/types.ts | 1 +
ui/pages/bridge/utils/quote.test.ts | 55 ++++++++++++++---------------
ui/pages/bridge/utils/quote.ts | 23 ++----------
5 files changed, 72 insertions(+), 67 deletions(-)
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index b26de01e8fa3..b92c8e60e4f0 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -532,6 +532,10 @@ describe('Bridge selectors', () => {
fiat: new BigNumber('13.8444372'),
amount: new BigNumber('13.98428'),
},
+ gasFee: {
+ amount: new BigNumber('7.141025952e-8'),
+ fiat: new BigNumber('7.141025952e-8'),
+ },
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
amount: new BigNumber('0.00100007141025952'),
@@ -599,6 +603,10 @@ describe('Bridge selectors', () => {
fiat: new BigNumber('13.8444372'),
amount: new BigNumber('13.98428'),
},
+ gasFee: {
+ amount: new BigNumber('7.141025952e-8'),
+ fiat: new BigNumber('7.141025952e-8'),
+ },
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
amount: new BigNumber('0.00100007141025952'),
@@ -671,6 +679,10 @@ describe('Bridge selectors', () => {
fiat: new BigNumber('13.8444372'),
amount: new BigNumber('13.98428'),
},
+ gasFee: {
+ amount: new BigNumber('7.141025952e-8'),
+ fiat: new BigNumber('7.141025952e-8'),
+ },
totalNetworkFee: {
fiat: new BigNumber('0.00100007141025952'),
amount: new BigNumber('0.00100007141025952'),
@@ -721,9 +733,11 @@ describe('Bridge selectors', () => {
});
it('should sort quotes by adjustedReturn', () => {
- const state = createBridgeMockStore({
- bridgeStateOverrides: { quotes: mockBridgeQuotesNativeErc20 },
- });
+ const state = createBridgeMockStore(
+ {},
+ {},
+ { quotes: mockBridgeQuotesNativeErc20 },
+ );
const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
state as never,
@@ -759,9 +773,10 @@ describe('Bridge selectors', () => {
});
it('should sort quotes by ETA', () => {
- const state = createBridgeMockStore({
- bridgeSliceOverrides: { sortOrder: SortOrder.ETA_ASC },
- bridgeStateOverrides: {
+ const state = createBridgeMockStore(
+ {},
+ { sortOrder: SortOrder.ETA_ASC },
+ {
quotes: [
...mockBridgeQuotesNativeErc20,
{
@@ -774,7 +789,7 @@ describe('Bridge selectors', () => {
},
],
},
- });
+ );
const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
state as never,
@@ -793,9 +808,10 @@ describe('Bridge selectors', () => {
});
it('should recommend 2nd cheapest quote if ETA exceeds 1 hour', () => {
- const state = createBridgeMockStore({
- bridgeSliceOverrides: { sortOrder: SortOrder.COST_ASC },
- bridgeStateOverrides: {
+ const state = createBridgeMockStore(
+ {},
+ { sortOrder: SortOrder.COST_ASC },
+ {
quotes: [
mockBridgeQuotesNativeErc20[1],
{
@@ -809,7 +825,7 @@ describe('Bridge selectors', () => {
},
],
},
- });
+ );
const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
state as never,
@@ -831,13 +847,14 @@ describe('Bridge selectors', () => {
});
it('should recommend 2nd fastest quote if adjustedReturn is less than 80% of cheapest quote', () => {
- const state = createBridgeMockStore({
- bridgeSliceOverrides: {
+ const state = createBridgeMockStore(
+ {},
+ {
sortOrder: SortOrder.ETA_ASC,
toTokenExchangeRate: 0.998781,
toNativeExchangeRate: 0.354073,
},
- bridgeStateOverrides: {
+ {
quotes: [
...mockBridgeQuotesNativeErc20,
{
@@ -851,14 +868,14 @@ describe('Bridge selectors', () => {
},
],
},
- metamaskStateOverrides: {
+ {
currencyRates: {
ETH: {
conversionRate: 2524.25,
},
},
},
- });
+ );
const { activeQuote, recommendedQuote, sortedQuotes } = getBridgeQuotes(
state as never,
diff --git a/ui/ducks/bridge/selectors.ts b/ui/ducks/bridge/selectors.ts
index 6c48e73e2891..b78a0d09de51 100644
--- a/ui/ducks/bridge/selectors.ts
+++ b/ui/ducks/bridge/selectors.ts
@@ -42,10 +42,11 @@ import {
import {
calcAdjustedReturn,
calcCost,
+ calcRelayerFee,
calcSentAmount,
calcSwapRate,
calcToAmount,
- calcTotalNetworkFee,
+ calcTotalGasFee,
isNativeAddress,
} from '../../pages/bridge/utils/quote';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
@@ -218,12 +219,17 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
): (QuoteResponse & QuoteMetadata)[] => {
const newQuotes = quotes.map((quote: QuoteResponse) => {
const toTokenAmount = calcToAmount(quote.quote, toTokenExchangeRate);
- const totalNetworkFee = calcTotalNetworkFee(
+ const gasFee = calcTotalGasFee(
quote,
estimatedBaseFeeInDecGwei,
maxPriorityFeePerGasInDecGwei,
nativeExchangeRate,
);
+ const relayerFee = calcRelayerFee(quote, nativeExchangeRate);
+ const totalNetworkFee = {
+ amount: gasFee.amount.plus(relayerFee.amount),
+ fiat: gasFee.fiat?.plus(relayerFee.fiat || '0') ?? null,
+ };
const sentAmount = calcSentAmount(
quote.quote,
isNativeAddress(quote.quote.srcAsset.address)
@@ -241,6 +247,7 @@ const _getQuotesWithMetadata = createDeepEqualSelector(
sentAmount,
totalNetworkFee,
adjustedReturn,
+ gasFee,
swapRate: calcSwapRate(sentAmount.amount, toTokenAmount.amount),
cost: calcCost(adjustedReturn.fiat, sentAmount.fiat),
};
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index 0c6eb8288797..61143bb9de68 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -7,6 +7,7 @@ export type L1GasFees = {
// Values derived from the quote response
// fiat values are calculated based on the user's selected currency
export type QuoteMetadata = {
+ gasFee: { amount: BigNumber; fiat: BigNumber | null };
totalNetworkFee: { amount: BigNumber; fiat: BigNumber | null }; // gasFees + relayerFees
toTokenAmount: { amount: BigNumber; fiat: BigNumber | null };
adjustedReturn: { fiat: BigNumber | null }; // destTokenAmount - totalNetworkFee
diff --git a/ui/pages/bridge/utils/quote.test.ts b/ui/pages/bridge/utils/quote.test.ts
index e6287a33e141..eec342517f83 100644
--- a/ui/pages/bridge/utils/quote.test.ts
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -5,7 +5,8 @@ import {
calcSentAmount,
calcSwapRate,
calcToAmount,
- calcTotalNetworkFee,
+ calcTotalGasFee,
+ calcRelayerFee,
formatEtaInMinutes,
} from './quote';
@@ -155,7 +156,7 @@ describe('Bridge quote utils', () => {
undefined,
],
])(
- 'calcTotalNetworkFee: fromToken is %s',
+ 'calcTotalGasFee and calcRelayerFee: fromToken is %s',
(
_: string,
srcAsset: { decimals: number; address: string },
@@ -165,18 +166,17 @@ describe('Bridge quote utils', () => {
approvalGasLimit?: number,
) => {
const feeData = { metabridge: { amount: 0 } };
- const result = calcTotalNetworkFee(
- {
- trade: { value, gasLimit: 1092677 },
- approval: approvalGasLimit
- ? { gasLimit: approvalGasLimit }
- : undefined,
- quote: { srcAsset, srcTokenAmount, feeData },
- } as never,
- '0.00010456',
- '0.0001',
- 2517.42,
- );
+ const quote = {
+ trade: { value, gasLimit: 1092677 },
+ approval: approvalGasLimit ? { gasLimit: approvalGasLimit } : undefined,
+ quote: { srcAsset, srcTokenAmount, feeData },
+ } as never;
+ const gasFee = calcTotalGasFee(quote, '0.00010456', '0.0001', 2517.42);
+ const relayerFee = calcRelayerFee(quote, 2517.42);
+ const result = {
+ amount: gasFee.amount.plus(relayerFee.amount),
+ fiat: gasFee.fiat?.plus(relayerFee.fiat || '0') ?? null,
+ };
expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
@@ -225,7 +225,7 @@ describe('Bridge quote utils', () => {
undefined,
],
])(
- 'calcTotalNetworkFee: fromToken is %s with l1GasFee',
+ 'calcTotalGasFee and calcRelayerFee: fromToken is %s with l1GasFee',
(
_: string,
srcAsset: { decimals: number; address: string },
@@ -235,19 +235,18 @@ describe('Bridge quote utils', () => {
approvalGasLimit?: number,
) => {
const feeData = { metabridge: { amount: 0 } };
- const result = calcTotalNetworkFee(
- {
- trade: { value, gasLimit: 1092677 },
- approval: approvalGasLimit
- ? { gasLimit: approvalGasLimit }
- : undefined,
- quote: { srcAsset, srcTokenAmount, feeData },
- l1GasFeesInHexWei: '0x25F63418AA4',
- } as never,
- '0.00010456',
- '0.0001',
- 2517.42,
- );
+ const quote = {
+ trade: { value, gasLimit: 1092677 },
+ approval: approvalGasLimit ? { gasLimit: approvalGasLimit } : undefined,
+ quote: { srcAsset, srcTokenAmount, feeData },
+ l1GasFeesInHexWei: '0x25F63418AA4',
+ } as never;
+ const gasFee = calcTotalGasFee(quote, '0.00010456', '0.0001', 2517.42);
+ const relayerFee = calcRelayerFee(quote, 2517.42);
+ const result = {
+ amount: gasFee.amount.plus(relayerFee.amount),
+ fiat: gasFee.fiat?.plus(relayerFee.fiat || '0') ?? null,
+ };
expect(result.amount?.toString()).toStrictEqual(amount);
expect(result.fiat?.toString()).toStrictEqual(fiat);
},
diff --git a/ui/pages/bridge/utils/quote.ts b/ui/pages/bridge/utils/quote.ts
index 6b179fe33712..2fff7e9c1b18 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -77,7 +77,7 @@ export const calcSentAmount = (
};
};
-const calcRelayerFee = (
+export const calcRelayerFee = (
bridgeQuote: QuoteResponse,
nativeExchangeRate?: number,
) => {
@@ -101,7 +101,7 @@ const calcRelayerFee = (
};
};
-const calcTotalGasFee = (
+export const calcTotalGasFee = (
bridgeQuote: QuoteResponse & L1GasFees,
estimatedBaseFeeInDecGwei: string,
maxPriorityFeePerGasInDecGwei: string,
@@ -140,25 +140,6 @@ const calcTotalGasFee = (
};
};
-export const calcTotalNetworkFee = (
- bridgeQuote: QuoteResponse & L1GasFees,
- estimatedBaseFeeInDecGwei: string,
- maxPriorityFeePerGasInDecGwei: string,
- nativeExchangeRate?: number,
-) => {
- const normalizedGasFee = calcTotalGasFee(
- bridgeQuote,
- estimatedBaseFeeInDecGwei,
- maxPriorityFeePerGasInDecGwei,
- nativeExchangeRate,
- );
- const normalizedRelayerFee = calcRelayerFee(bridgeQuote, nativeExchangeRate);
- return {
- amount: normalizedGasFee.amount.plus(normalizedRelayerFee.amount),
- fiat: normalizedGasFee.fiat?.plus(normalizedRelayerFee.fiat || '0') ?? null,
- };
-};
-
export const calcAdjustedReturn = (
destTokenAmountInFiat: BigNumber | null,
totalNetworkFeeInFiat: BigNumber | null,
From 3efe5490ad8bb1c15ad11bf57307bfb0f82354f7 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 21 Nov 2024 16:09:13 -0800
Subject: [PATCH 24/25] fix: remove toAmount rounding
---
ui/pages/bridge/prepare/prepare-bridge-page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index ba6a540719d3..aea037c71f13 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -305,7 +305,7 @@ const PrepareBridgePage = () => {
testId: 'to-amount',
readOnly: true,
disabled: true,
- value: activeQuote?.toTokenAmount?.amount.toFixed(2) ?? '0',
+ value: activeQuote?.toTokenAmount?.amount.toFixed() ?? '0',
className: activeQuote?.toTokenAmount.amount
? 'amount-input defined'
: 'amount-input',
From 47cdde0b8a4df6216a932ef76c955a71501b4cb0 Mon Sep 17 00:00:00 2001
From: Micaela Estabillo
Date: Thu, 21 Nov 2024 17:15:32 -0800
Subject: [PATCH 25/25] refactor: share util method for fetching exchange rates
---
ui/ducks/bridge/bridge.ts | 27 ++++-----------------------
ui/ducks/bridge/utils.ts | 19 +++++++++++++++++++
2 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index 91d790ad83a2..edb0c9ca0d13 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -1,15 +1,14 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { Hex } from '@metamask/utils';
-import { getAddress } from 'ethers/lib/utils';
import { swapsSlice } from '../swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { SwapsEthToken } from '../../selectors';
-import { fetchTokenExchangeRates } from '../../helpers/utils/util';
import {
QuoteMetadata,
QuoteResponse,
SortOrder,
} from '../../pages/bridge/types';
+import { getTokenExchangeRate } from './utils';
export type BridgeState = {
toChainId: Hex | null;
@@ -35,30 +34,12 @@ const initialState: BridgeState = {
export const setSrcTokenExchangeRates = createAsyncThunk(
'bridge/setSrcTokenExchangeRates',
- async (request: { chainId: Hex; tokenAddress: string; currency: string }) => {
- const { chainId, tokenAddress, currency } = request;
- const exchangeRates = await fetchTokenExchangeRates(
- currency,
- [tokenAddress],
- chainId,
- );
- return exchangeRates?.[getAddress(tokenAddress)];
- },
+ getTokenExchangeRate,
);
export const setDestTokenExchangeRates = createAsyncThunk(
'bridge/setDestTokenExchangeRates',
- async (request: { chainId: Hex; tokenAddress: string; currency: string }) => {
- const { chainId, tokenAddress, currency } = request;
- const exchangeRates = await fetchTokenExchangeRates(
- currency,
- [tokenAddress],
- chainId,
- );
- return {
- toTokenExchangeRate: exchangeRates?.[getAddress(tokenAddress)],
- };
- },
+ getTokenExchangeRate,
);
const bridgeSlice = createSlice({
@@ -90,7 +71,7 @@ const bridgeSlice = createSlice({
},
extraReducers: (builder) => {
builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
- state.toTokenExchangeRate = action.payload.toTokenExchangeRate ?? null;
+ state.toTokenExchangeRate = action.payload ?? null;
});
builder.addCase(setSrcTokenExchangeRates.fulfilled, (state, action) => {
state.fromTokenExchangeRate = action.payload ?? null;
diff --git a/ui/ducks/bridge/utils.ts b/ui/ducks/bridge/utils.ts
index 853c344310fe..de45111cc10b 100644
--- a/ui/ducks/bridge/utils.ts
+++ b/ui/ducks/bridge/utils.ts
@@ -1,9 +1,11 @@
import { Hex } from '@metamask/utils';
import { BigNumber } from 'bignumber.js';
+import { getAddress } from 'ethers/lib/utils';
import { decGWEIToHexWEI } from '../../../shared/modules/conversion.utils';
import { Numeric } from '../../../shared/modules/Numeric';
import { TxData } from '../../pages/bridge/types';
import { getTransaction1559GasFeeEstimates } from '../../pages/swaps/swaps.util';
+import { fetchTokenExchangeRates } from '../../helpers/utils/util';
// We don't need to use gas multipliers here because the gasLimit from Bridge API already included it
export const getHexMaxGasLimit = (gasLimit: number) => {
@@ -45,3 +47,20 @@ export const getTxGasEstimates = async ({
maxPriorityFeePerGas: undefined,
};
};
+
+export const getTokenExchangeRate = async (request: {
+ chainId: Hex;
+ tokenAddress: string;
+ currency: string;
+}) => {
+ const { chainId, tokenAddress, currency } = request;
+ const exchangeRates = await fetchTokenExchangeRates(
+ currency,
+ [tokenAddress],
+ chainId,
+ );
+ return (
+ exchangeRates?.[tokenAddress.toLowerCase()] ??
+ exchangeRates?.[getAddress(tokenAddress)]
+ );
+};