From b38244b877cea0e1aace85dc45d1dab9c74b0258 Mon Sep 17 00:00:00 2001
From: Salim TOUBAL
Date: Fri, 22 Nov 2024 19:22:11 +0100
Subject: [PATCH 01/40] fix: add unit test for assets polling loops (#28646)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
the goal of this PR is to add unit tests for all multichain assets
polling loops
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28646?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
ui/hooks/useAccountTrackerPolling.test.ts | 143 +++++++++++++++++++
ui/hooks/useCurrencyRatePolling.test.ts | 128 +++++++++++++++++
ui/hooks/useTokenDetectionPolling.test.ts | 157 +++++++++++++++++++++
ui/hooks/useTokenRatesPolling.test.ts | 160 ++++++++++++++++++++++
4 files changed, 588 insertions(+)
create mode 100644 ui/hooks/useAccountTrackerPolling.test.ts
create mode 100644 ui/hooks/useCurrencyRatePolling.test.ts
create mode 100644 ui/hooks/useTokenDetectionPolling.test.ts
create mode 100644 ui/hooks/useTokenRatesPolling.test.ts
diff --git a/ui/hooks/useAccountTrackerPolling.test.ts b/ui/hooks/useAccountTrackerPolling.test.ts
new file mode 100644
index 000000000000..010085ddce61
--- /dev/null
+++ b/ui/hooks/useAccountTrackerPolling.test.ts
@@ -0,0 +1,143 @@
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import {
+ accountTrackerStartPolling,
+ accountTrackerStopPollingByPollingToken,
+} from '../store/actions';
+import useAccountTrackerPolling from './useAccountTrackerPolling';
+
+let mockPromises: Promise[];
+
+jest.mock('../store/actions', () => ({
+ accountTrackerStartPolling: jest.fn().mockImplementation((input) => {
+ const promise = Promise.resolve(`${input}_tracking`);
+ mockPromises.push(promise);
+ return promise;
+ }),
+ accountTrackerStopPollingByPollingToken: jest.fn(),
+}));
+
+let originalPortfolioView: string | undefined;
+
+describe('useAccountTrackerPolling', () => {
+ beforeEach(() => {
+ // Mock process.env.PORTFOLIO_VIEW
+ originalPortfolioView = process.env.PORTFOLIO_VIEW;
+ process.env.PORTFOLIO_VIEW = 'true'; // Set your desired mock value here
+
+ mockPromises = [];
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ // Restore the original value
+ process.env.PORTFOLIO_VIEW = originalPortfolioView;
+ });
+
+ it('should poll account trackers for network client IDs when enabled and stop on dismount', async () => {
+ process.env.PORTFOLIO_VIEW = 'true';
+
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ selectedNetworkClientId: 'selectedNetworkClientId',
+ networkConfigurationsByChainId: {
+ '0x1': {
+ chainId: '0x1',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId',
+ },
+ ],
+ },
+ '0x89': {
+ chainId: '0x89',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId2',
+ },
+ ],
+ },
+ },
+ },
+ };
+
+ const { unmount } = renderHookWithProvider(
+ () => useAccountTrackerPolling(),
+ state,
+ );
+
+ // Should poll each client ID
+ await Promise.all(mockPromises);
+ expect(accountTrackerStartPolling).toHaveBeenCalledTimes(2);
+ expect(accountTrackerStartPolling).toHaveBeenCalledWith(
+ 'selectedNetworkClientId',
+ );
+ expect(accountTrackerStartPolling).toHaveBeenCalledWith(
+ 'selectedNetworkClientId2',
+ );
+
+ // Stop polling on dismount
+ unmount();
+ expect(accountTrackerStopPollingByPollingToken).toHaveBeenCalledTimes(2);
+ expect(accountTrackerStopPollingByPollingToken).toHaveBeenCalledWith(
+ 'selectedNetworkClientId_tracking',
+ );
+ });
+
+ it('should not poll if onboarding is not completed', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: false,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useAccountTrackerPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(accountTrackerStartPolling).toHaveBeenCalledTimes(0);
+ expect(accountTrackerStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when locked', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: false,
+ completedOnboarding: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useAccountTrackerPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(accountTrackerStartPolling).toHaveBeenCalledTimes(0);
+ expect(accountTrackerStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when no network client IDs are provided', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useAccountTrackerPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(accountTrackerStartPolling).toHaveBeenCalledTimes(0);
+ expect(accountTrackerStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/ui/hooks/useCurrencyRatePolling.test.ts b/ui/hooks/useCurrencyRatePolling.test.ts
new file mode 100644
index 000000000000..9ad405a31c46
--- /dev/null
+++ b/ui/hooks/useCurrencyRatePolling.test.ts
@@ -0,0 +1,128 @@
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import {
+ currencyRateStartPolling,
+ currencyRateStopPollingByPollingToken,
+} from '../store/actions';
+import useCurrencyRatePolling from './useCurrencyRatePolling';
+
+let mockPromises: Promise[];
+
+jest.mock('../store/actions', () => ({
+ currencyRateStartPolling: jest.fn().mockImplementation((input) => {
+ const promise = Promise.resolve(`${input}_rates`);
+ mockPromises.push(promise);
+ return promise;
+ }),
+ currencyRateStopPollingByPollingToken: jest.fn(),
+}));
+
+describe('useCurrencyRatePolling', () => {
+ beforeEach(() => {
+ mockPromises = [];
+ jest.clearAllMocks();
+ });
+
+ it('should poll currency rates for native currencies when enabled and stop on dismount', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useCurrencyRateCheck: true,
+ selectedNetworkClientId: 'selectedNetworkClientId',
+ networkConfigurationsByChainId: {
+ '0x1': {
+ nativeCurrency: 'ETH',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId',
+ },
+ ],
+ },
+ '0x89': {
+ nativeCurrency: 'BNB',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId2',
+ },
+ ],
+ },
+ },
+ },
+ };
+
+ const { unmount } = renderHookWithProvider(
+ () => useCurrencyRatePolling(),
+ state,
+ );
+
+ await Promise.all(mockPromises);
+ expect(currencyRateStartPolling).toHaveBeenCalledTimes(1);
+ expect(currencyRateStartPolling).toHaveBeenCalledWith(['ETH', 'BNB']);
+
+ // Stop polling on dismount
+ unmount();
+ expect(currencyRateStopPollingByPollingToken).toHaveBeenCalledTimes(1);
+ expect(currencyRateStopPollingByPollingToken).toHaveBeenCalledWith(
+ 'ETH,BNB_rates',
+ );
+ });
+
+ it('should not poll if onboarding is not completed', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: false,
+ useCurrencyRateCheck: true,
+ networkConfigurationsByChainId: {
+ '0x1': { nativeCurrency: 'ETH' },
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useCurrencyRatePolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(currencyRateStartPolling).toHaveBeenCalledTimes(0);
+ expect(currencyRateStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when locked', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: false,
+ completedOnboarding: true,
+ useCurrencyRateCheck: true,
+ networkConfigurationsByChainId: {
+ '0x1': { nativeCurrency: 'ETH' },
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useCurrencyRatePolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(currencyRateStartPolling).toHaveBeenCalledTimes(0);
+ expect(currencyRateStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when currency rate checking is disabled', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useCurrencyRateCheck: false,
+ networkConfigurationsByChainId: {
+ '0x1': { nativeCurrency: 'ETH' },
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useCurrencyRatePolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(currencyRateStartPolling).toHaveBeenCalledTimes(0);
+ expect(currencyRateStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/ui/hooks/useTokenDetectionPolling.test.ts b/ui/hooks/useTokenDetectionPolling.test.ts
new file mode 100644
index 000000000000..bae369ffd525
--- /dev/null
+++ b/ui/hooks/useTokenDetectionPolling.test.ts
@@ -0,0 +1,157 @@
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import {
+ tokenDetectionStartPolling,
+ tokenDetectionStopPollingByPollingToken,
+} from '../store/actions';
+import useTokenDetectionPolling from './useTokenDetectionPolling';
+
+let mockPromises: Promise[];
+
+jest.mock('../store/actions', () => ({
+ tokenDetectionStartPolling: jest.fn().mockImplementation((input) => {
+ const promise = Promise.resolve(`${input}_detection`);
+ mockPromises.push(promise);
+ return promise;
+ }),
+ tokenDetectionStopPollingByPollingToken: jest.fn(),
+}));
+let originalPortfolioView: string | undefined;
+
+describe('useTokenDetectionPolling', () => {
+ beforeEach(() => {
+ // Mock process.env.PORTFOLIO_VIEW
+ originalPortfolioView = process.env.PORTFOLIO_VIEW;
+ process.env.PORTFOLIO_VIEW = 'true'; // Set your desired mock value here
+
+ mockPromises = [];
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ // Restore the original value
+ process.env.PORTFOLIO_VIEW = originalPortfolioView;
+ });
+
+ it('should poll token detection for chain IDs when enabled and stop on dismount', async () => {
+ process.env.PORTFOLIO_VIEW = 'true';
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useTokenDetection: true,
+ selectedNetworkClientId: 'selectedNetworkClientId',
+ networkConfigurationsByChainId: {
+ '0x1': {
+ chainId: '0x1',
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId',
+ },
+ ],
+ },
+ '0x89': {
+ chainId: '0x89',
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId2',
+ },
+ ],
+ },
+ },
+ },
+ };
+
+ const { unmount } = renderHookWithProvider(
+ () => useTokenDetectionPolling(),
+ state,
+ );
+
+ // Should poll each chain
+ await Promise.all(mockPromises);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledTimes(1);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledWith(['0x1', '0x89']);
+
+ // Stop polling on dismount
+ unmount();
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledTimes(1);
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledWith(
+ '0x1,0x89_detection',
+ );
+ });
+
+ it('should not poll if onboarding is not completed', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: false,
+ useTokenDetection: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenDetectionPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when locked', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: false,
+ completedOnboarding: true,
+ useTokenDetection: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenDetectionPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when token detection is disabled', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useTokenDetection: false,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenDetectionPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when no chains are provided', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useTokenDetection: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenDetectionPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenDetectionStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenDetectionStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/ui/hooks/useTokenRatesPolling.test.ts b/ui/hooks/useTokenRatesPolling.test.ts
new file mode 100644
index 000000000000..9383527e6fbd
--- /dev/null
+++ b/ui/hooks/useTokenRatesPolling.test.ts
@@ -0,0 +1,160 @@
+import { renderHookWithProvider } from '../../test/lib/render-helpers';
+import {
+ tokenRatesStartPolling,
+ tokenRatesStopPollingByPollingToken,
+} from '../store/actions';
+import useTokenRatesPolling from './useTokenRatesPolling';
+
+let mockPromises: Promise[];
+
+jest.mock('../store/actions', () => ({
+ tokenRatesStartPolling: jest.fn().mockImplementation((input) => {
+ const promise = Promise.resolve(`${input}_rates`);
+ mockPromises.push(promise);
+ return promise;
+ }),
+ tokenRatesStopPollingByPollingToken: jest.fn(),
+}));
+
+let originalPortfolioView: string | undefined;
+describe('useTokenRatesPolling', () => {
+ beforeEach(() => {
+ // Mock process.env.PORTFOLIO_VIEW
+ originalPortfolioView = process.env.PORTFOLIO_VIEW;
+ process.env.PORTFOLIO_VIEW = 'true'; // Set your desired mock value here
+
+ mockPromises = [];
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ // Restore the original value
+ process.env.PORTFOLIO_VIEW = originalPortfolioView;
+ });
+
+ it('should poll token rates when enabled and stop on dismount', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useCurrencyRateCheck: true,
+ selectedNetworkClientId: 'selectedNetworkClientId',
+ networkConfigurationsByChainId: {
+ '0x1': {
+ chainId: '0x1',
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId',
+ },
+ ],
+ },
+ '0x89': {
+ chainId: '0x89',
+ rpcEndpoints: [
+ {
+ networkClientId: 'selectedNetworkClientId2',
+ },
+ ],
+ },
+ },
+ },
+ };
+
+ const { unmount } = renderHookWithProvider(
+ () => useTokenRatesPolling(),
+ state,
+ );
+
+ // Should poll each chain
+ await Promise.all(mockPromises);
+ expect(tokenRatesStartPolling).toHaveBeenCalledTimes(2);
+ expect(tokenRatesStartPolling).toHaveBeenCalledWith('0x1');
+ expect(tokenRatesStartPolling).toHaveBeenCalledWith('0x89');
+ // Stop polling on dismount
+ unmount();
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledTimes(2);
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledWith(
+ '0x1_rates',
+ );
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledWith(
+ '0x89_rates',
+ );
+ });
+
+ it('should not poll if onboarding is not completed', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: false,
+ useCurrencyRateCheck: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ '0x89': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenRatesPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenRatesStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when locked', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: false,
+ completedOnboarding: true,
+ useCurrencyRateCheck: true,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ '0x89': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenRatesPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenRatesStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when rate checking is disabled', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useCurrencyRateCheck: false,
+ networkConfigurationsByChainId: {
+ '0x1': {},
+ '0x89': {},
+ },
+ },
+ };
+
+ renderHookWithProvider(() => useTokenRatesPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenRatesStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+
+ it('should not poll when no chains are provided', async () => {
+ const state = {
+ metamask: {
+ isUnlocked: true,
+ completedOnboarding: true,
+ useCurrencyRateCheck: true,
+ networkConfigurationsByChainId: {},
+ },
+ };
+
+ renderHookWithProvider(() => useTokenRatesPolling(), state);
+
+ await Promise.all(mockPromises);
+ expect(tokenRatesStartPolling).toHaveBeenCalledTimes(0);
+ expect(tokenRatesStopPollingByPollingToken).toHaveBeenCalledTimes(0);
+ });
+});
From ad9a74841748e4353c2e1ee749208dc1cf099c28 Mon Sep 17 00:00:00 2001
From: jiexi
Date: Fri, 22 Nov 2024 12:10:59 -0800
Subject: [PATCH 02/40] fix: Reset streams on BFCache events (#24950)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Previously, Chrome's BFCache (Back Forward) strategy was to evict a page
from cache if it received a message over any connected port streams. The
port stream would remain open and the background script would NOT
receive an onDisconnect. As long as the cached page did not receive a
message, the port would still function when the page became active
again. This was problematic because MetaMask was likely to send a
message to the still connected cached page at some point due to the
nature of notifications, which would evict the page from cache,
neutralizing the performance benefit of the BFCache for the end user.
Now, Chrome's BFCache strategy is to trigger an onDisconnect for the
background script, but NOT the cached page. The port stream is invalid
despite not being closed on the cached page side. This is problematic
because we do not listen for events relevant to when a BFCached page
becomes active and thus do not reset the invalid stream.
To address both strategies, we now listen for the `pageshow` and
`pagehide` events. When a page is entering a BFCached state, we
preemptively end the port stream connection (even if the user is on an
older version of chrome that would have kept it alive). When a BFCached
page is restored to an active state, we establish a port stream
connection. We know the port stream must be restored/reset because we
were the ones responsible for preemptively ending it earlier in the
lifecycle. Combining these two changes allows us to handle both the old
and new BFCache strategies without having to target them by versions
separately.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/24950?quickstart=1)
## **Related issues**
See:
https://developer.chrome.com/blog/bfcache-extension-messaging-changes?hl=en
See: https://github.com/MetaMask/metamask-extension/issues/13373
See: https://web.dev/articles/bfcache (useful links at bottom)
See: https://github.com/w3c/webextensions/issues/474
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/2582
## **Manual testing steps**
Steps are for macOS. Using chrome 123 or newer
**Testing with old BFCache strategy**
1. Close the entire chrome app
1. run `open /Applications/Google\ Chrome.app --args
--disable-features=DisconnectExtensionMessagePortWhenPageEntersBFCache`
1. Visit `http://www.brainjar.com/java/host/test.html`
1. Open console
1. Enter `await window.ethereum.request({method: 'eth_chainId'})`, which
should be responsive
1. Visit `chrome://terms/`
1. Use the back button to go back to the brainjar test page
1. Enter `await window.ethereum.request({method: 'eth_chainId'})`, which
should be responsive
**Testing with the new BFCache strategy**
Repeat the steps above, but use `--enable-features` instead of `disable`
MetaMask Behavior should look the same regardless of browser's BFCache
strategy
## **Screenshots/Recordings**
BFCache Behavior
https://github.com/MetaMask/metamask-extension/assets/918701/efeb1591-5fde-44c8-b0a3-3573dfb97806
Prerender Behavior (to show affected chromium browsers still reset
streams correctly)
https://github.com/MetaMask/metamask-extension/assets/918701/7461bf64-b5b0-4e70-96d5-416cf5bf6b7c
## **Pre-merge author checklist**
- [x] I’ve followed [MetaMask Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/scripts/contentscript.js | 16 +++++++
app/scripts/streams/provider-stream.ts | 32 +++++++++----
test/e2e/provider/bfcache.spec.js | 65 ++++++++++++++++++++++++++
3 files changed, 105 insertions(+), 8 deletions(-)
create mode 100644 test/e2e/provider/bfcache.spec.js
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index 7c427a1821c5..a38ac9fe5833 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -1,8 +1,10 @@
import { getIsBrowserPrerenderBroken } from '../../shared/modules/browser-runtime.utils';
import shouldInjectProvider from '../../shared/modules/provider-injection';
import {
+ destroyStreams,
initStreams,
onDisconnectDestroyStreams,
+ setupExtensionStreams,
} from './streams/provider-stream';
import {
isDetectedPhishingSite,
@@ -33,6 +35,20 @@ const start = () => {
);
});
}
+
+ window.addEventListener('pageshow', (event) => {
+ if (event.persisted) {
+ console.warn('BFCached page has become active. Restoring the streams.');
+ setupExtensionStreams();
+ }
+ });
+
+ window.addEventListener('pagehide', (event) => {
+ if (event.persisted) {
+ console.warn('Page may become BFCached. Destroying the streams.');
+ destroyStreams();
+ }
+ });
}
};
diff --git a/app/scripts/streams/provider-stream.ts b/app/scripts/streams/provider-stream.ts
index 9616253815e8..82b159130242 100644
--- a/app/scripts/streams/provider-stream.ts
+++ b/app/scripts/streams/provider-stream.ts
@@ -33,7 +33,7 @@ let legacyExtMux: ObjectMultiplex,
let extensionMux: ObjectMultiplex,
extensionChannel: Substream,
- extensionPort: browser.Runtime.Port,
+ extensionPort: browser.Runtime.Port | null,
extensionStream: PortStream | null,
pageMux: ObjectMultiplex,
pageChannel: Substream;
@@ -65,7 +65,7 @@ const setupPageStreams = () => {
// The field below is used to ensure that replay is done only once for each restart.
let METAMASK_EXTENSION_CONNECT_SENT = false;
-const setupExtensionStreams = () => {
+export const setupExtensionStreams = () => {
METAMASK_EXTENSION_CONNECT_SENT = true;
extensionPort = browser.runtime.connect({ name: CONTENT_SCRIPT });
extensionStream = new PortStream(extensionPort);
@@ -226,19 +226,35 @@ const onMessageSetUpExtensionStreams = (msg: MessageType) => {
return undefined;
};
+/**
+ * Ends two-way communication streams between browser extension and
+ * the local per-page browser context.
+ */
+export function destroyStreams() {
+ if (!extensionPort) {
+ return;
+ }
+ extensionPort.onDisconnect.removeListener(onDisconnectDestroyStreams);
+
+ destroyExtensionStreams();
+ destroyLegacyExtensionStreams();
+
+ extensionPort.disconnect();
+ extensionPort = null;
+
+ METAMASK_EXTENSION_CONNECT_SENT = false;
+}
+
/**
* This listener destroys the extension streams when the extension port is disconnected,
* so that streams may be re-established later when the extension port is reconnected.
*
* @param [err] - Stream connection error
*/
-export const onDisconnectDestroyStreams = (err: unknown) => {
+export function onDisconnectDestroyStreams(err: unknown) {
const lastErr = err || checkForLastError();
- extensionPort.onDisconnect.removeListener(onDisconnectDestroyStreams);
-
- destroyExtensionStreams();
- destroyLegacyExtensionStreams();
+ destroyStreams();
/**
* If an error is found, reset the streams. When running two or more dapps, resetting the service
@@ -251,7 +267,7 @@ export const onDisconnectDestroyStreams = (err: unknown) => {
console.warn(`${lastErr} Resetting the streams.`);
setTimeout(setupExtensionStreams, 1000);
}
-};
+}
/**
* Initializes two-way communication streams between the browser extension and
diff --git a/test/e2e/provider/bfcache.spec.js b/test/e2e/provider/bfcache.spec.js
new file mode 100644
index 000000000000..4d2c5d2bb1e7
--- /dev/null
+++ b/test/e2e/provider/bfcache.spec.js
@@ -0,0 +1,65 @@
+const { strict: assert } = require('assert');
+const {
+ withFixtures,
+ defaultGanacheOptions,
+ DAPP_URL,
+ openDapp,
+} = require('../helpers');
+const FixtureBuilder = require('../fixture-builder');
+
+const triggerBFCache = async (driver) => {
+ await driver.executeScript(`
+ window.addEventListener('pageshow', (event) => {
+ if (event.persisted) {
+ window.restoredFromBFCache = true
+ }
+ });
+ `);
+
+ await driver.driver.get(`chrome://terms/`);
+
+ await driver.driver.navigate().back();
+
+ const restoredFromBFCache = await driver.executeScript(
+ `return window.restoredFromBFCache`,
+ );
+
+ if (!restoredFromBFCache) {
+ assert.fail(new Error('Failed to trigger BFCache'));
+ }
+};
+
+describe('BFCache', function () {
+ it('has a working provider stream when a dapp is restored from BFCache', async function () {
+ await withFixtures(
+ {
+ dapp: true,
+ fixtures: new FixtureBuilder().build(),
+ ganacheOptions: defaultGanacheOptions,
+ title: this.test.fullTitle(),
+ },
+ async ({ driver }) => {
+ await openDapp(driver, undefined, DAPP_URL);
+
+ const request = JSON.stringify({
+ jsonrpc: '2.0',
+ method: 'eth_chainId',
+ params: [],
+ id: 0,
+ });
+
+ const initialResult = await driver.executeScript(
+ `return window.ethereum.request(${request})`,
+ );
+ assert.equal(initialResult, '0x539');
+
+ await triggerBFCache(driver);
+
+ const bfcacheResult = await driver.executeScript(
+ `return window.ethereum.request(${request})`,
+ );
+ assert.equal(bfcacheResult, '0x539');
+ },
+ );
+ });
+});
From 9bdfd03bfafb46f10c11ba104bcd60d37fc370eb Mon Sep 17 00:00:00 2001
From: Brian Bergeron
Date: Fri, 22 Nov 2024 13:27:38 -0800
Subject: [PATCH 03/40] fix: market data for native tokens with non zero
addresses (#28584)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Draft
When querying the price API, the native token is usually represented by
the zero address. But this is not the case on some chains like polygon,
whose native token has a contract
`0x0000000000000000000000000000000000001010`.
Depends on: https://github.com/MetaMask/core/pull/4952
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28584?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
(pre-req - you may need to enable the `PORTFOLIO_VIEW=true` flag)
1. Onboard with an SRP that have POL tokens.
2. Connect and switch to Polygon
3. View the POL token on the tokens section on the home page.
- Does it contain percentages and market data.
5. View the POL asset page.
- Does it contain the market details view; and percentage sections?
## **Screenshots/Recordings**
| Before | After |
|--------|--------|
| ![Screenshot 2024-11-22 at 16 42
25](https://github.com/user-attachments/assets/51e67809-b53f-4a29-a345-ddda516a08b2)
| ![Screenshot 2024-11-22 at 16 29
38](https://github.com/user-attachments/assets/87245972-5a03-4acb-85d0-dfe01660b038)
|
| ![Screenshot 2024-11-22 at 16 42
41](https://github.com/user-attachments/assets/b87a9cc3-dcb5-4479-8406-3a832f9f926f)
| ![Screenshot 2024-11-22 at 16 29
46](https://github.com/user-attachments/assets/20ab36e9-0d55-45d6-a57b-0c05e8c533b9)
|
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: Prithpal Sooriya
---
...s-controllers-npm-45.1.0-d914c453f0.patch} | 40 +-----------
...-assets-controllers-patch-9e00573eb4.patch | 62 -------------------
package.json | 2 +-
...gated-percentage-overview-cross-chains.tsx | 9 ++-
.../aggregated-percentage-overview.test.tsx | 19 +++---
.../aggregated-percentage-overview.tsx | 8 ++-
.../app/wallet-overview/coin-overview.tsx | 7 ++-
.../percentage-and-amount-change.test.tsx | 38 +++++++++---
.../percentage-and-amount-change.tsx | 8 ++-
.../token-list-item/token-list-item.tsx | 10 +--
ui/pages/asset/components/asset-page.tsx | 4 +-
yarn.lock | 18 +++---
12 files changed, 83 insertions(+), 142 deletions(-)
rename .yarn/patches/{@metamask-assets-controllers-npm-45.0.0-31810ece32.patch => @metamask-assets-controllers-npm-45.1.0-d914c453f0.patch} (51%)
delete mode 100644 .yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch
diff --git a/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch b/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch
similarity index 51%
rename from .yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch
rename to .yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch
index 77a2e7f21cfb..ca412ba89489 100644
--- a/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch
+++ b/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch
@@ -1,39 +1,3 @@
-diff --git a/dist/TokenDetectionController.cjs b/dist/TokenDetectionController.cjs
-index 8fd5efde7a3c24080f8a43f79d10300e8c271245..66f656d9a55f1154024a8c18a9fe27b4ed39a21d 100644
---- a/dist/TokenDetectionController.cjs
-+++ b/dist/TokenDetectionController.cjs
-@@ -250,17 +250,20 @@ _TokenDetectionController_intervalId = new WeakMap(), _TokenDetectionController_
- }
- });
- this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange',
-- // TODO: Either fix this lint violation or explain why it's necessary to ignore.
-- // eslint-disable-next-line @typescript-eslint/no-misused-promises
-- async (selectedAccount) => {
-- const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
-- if (isSelectedAccountIdChanged) {
-- __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
-- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
-- selectedAddress: selectedAccount.address,
-- });
-- }
-- });
-+ // TODO: Either fix this lint violation or explain why it's necessary to ignore.
-+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
-+ async (selectedAccount) => {
-+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState');
-+ const chainIds = Object.keys(networkConfigurationsByChainId);
-+ const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id;
-+ if (isSelectedAccountIdChanged) {
-+ __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f");
-+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, {
-+ selectedAddress: selectedAccount.address,
-+ chainIds,
-+ });
-+ }
-+ });
- }, _TokenDetectionController_stopPolling = function _TokenDetectionController_stopPolling() {
- if (__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")) {
- clearInterval(__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f"));
diff --git a/dist/assetsUtil.cjs b/dist/assetsUtil.cjs
index 48571b8c1b78e94d88e1837e986b5f8735ac651b..61246f51500c8cab48f18296a73629fb73454caa 100644
--- a/dist/assetsUtil.cjs
@@ -56,7 +20,7 @@ index 48571b8c1b78e94d88e1837e986b5f8735ac651b..61246f51500c8cab48f18296a73629fb
// because most cid v0s appear to be incompatible with IPFS subdomains
return {
diff --git a/dist/token-prices-service/codefi-v2.mjs b/dist/token-prices-service/codefi-v2.mjs
-index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..bf8ec7819f678c2f185d6a85d7e3ea81f055a309 100644
+index a13403446a2376d4d905a9ef733941798da89c88..3c8229f9ea40f4c1ee760a22884e1066dac82ec7 100644
--- a/dist/token-prices-service/codefi-v2.mjs
+++ b/dist/token-prices-service/codefi-v2.mjs
@@ -12,8 +12,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
@@ -65,7 +29,7 @@ index e7eaad2cfa8b233c4fd42a51f745233a1cc5c387..bf8ec7819f678c2f185d6a85d7e3ea81
import { hexToNumber } from "@metamask/utils";
-import $cockatiel from "cockatiel";
-const { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } = $cockatiel;
-+import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel"
++import { circuitBreaker, ConsecutiveBreaker, ExponentialBackoff, handleAll, retry, wrap, CircuitState } from "cockatiel";
/**
* The list of currencies that can be supplied as the `vsCurrency` parameter to
* the `/spot-prices` endpoint, in lowercase form.
diff --git a/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch b/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch
deleted file mode 100644
index 1b9e5a4ba848..000000000000
--- a/.yarn/patches/@metamask-assets-controllers-patch-9e00573eb4.patch
+++ /dev/null
@@ -1,62 +0,0 @@
-diff --git a/dist/TokenDetectionController.cjs b/dist/TokenDetectionController.cjs
-index ab23c95d667357db365f925c4c4acce4736797f8..8fd5efde7a3c24080f8a43f79d10300e8c271245 100644
---- a/dist/TokenDetectionController.cjs
-+++ b/dist/TokenDetectionController.cjs
-@@ -204,13 +204,10 @@ class TokenDetectionController extends (0, polling_controller_1.StaticIntervalPo
- // Try detecting tokens via Account API first if conditions allow
- if (supportedNetworks && chainsToDetectUsingAccountAPI.length > 0) {
- const apiResult = await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_attemptAccountAPIDetection).call(this, chainsToDetectUsingAccountAPI, addressToDetect, supportedNetworks);
-- // If API succeeds and no chains are left for RPC detection, we can return early
-- if (apiResult?.result === 'success' &&
-- chainsToDetectUsingRpc.length === 0) {
-- return;
-+ // If the account API call failed, have those chains fall back to RPC detection
-+ if (apiResult?.result === 'failed') {
-+ __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_addChainsToRpcDetection).call(this, chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI, clientNetworks);
- }
-- // If API fails or chainsToDetectUsingRpc still has items, add chains to RPC detection
-- __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_addChainsToRpcDetection).call(this, chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI, clientNetworks);
- }
- // Proceed with RPC detection if there are chains remaining in chainsToDetectUsingRpc
- if (chainsToDetectUsingRpc.length > 0) {
-@@ -446,8 +443,7 @@ async function _TokenDetectionController_addDetectedTokensViaAPI({ selectedAddre
- const tokenBalancesByChain = await __classPrivateFieldGet(this, _TokenDetectionController_accountsAPI, "f")
- .getMultiNetworksBalances(selectedAddress, chainIds, supportedNetworks)
- .catch(() => null);
-- if (!tokenBalancesByChain ||
-- Object.keys(tokenBalancesByChain).length === 0) {
-+ if (tokenBalancesByChain === null) {
- return { result: 'failed' };
- }
- // Process each chain ID individually
-diff --git a/dist/TokenDetectionController.mjs b/dist/TokenDetectionController.mjs
-index f75eb5c2c74f2a9d15a79760985111171dc938e1..ebc30bb915cc39dabf49f9e0da84a7948ae1ed48 100644
---- a/dist/TokenDetectionController.mjs
-+++ b/dist/TokenDetectionController.mjs
-@@ -205,13 +205,10 @@ export class TokenDetectionController extends StaticIntervalPollingController()
- // Try detecting tokens via Account API first if conditions allow
- if (supportedNetworks && chainsToDetectUsingAccountAPI.length > 0) {
- const apiResult = await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_attemptAccountAPIDetection).call(this, chainsToDetectUsingAccountAPI, addressToDetect, supportedNetworks);
-- // If API succeeds and no chains are left for RPC detection, we can return early
-- if (apiResult?.result === 'success' &&
-- chainsToDetectUsingRpc.length === 0) {
-- return;
-+ // If the account API call failed, have those chains fall back to RPC detection
-+ if (apiResult?.result === 'failed') {
-+ __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_addChainsToRpcDetection).call(this, chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI, clientNetworks);
- }
-- // If API fails or chainsToDetectUsingRpc still has items, add chains to RPC detection
-- __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_addChainsToRpcDetection).call(this, chainsToDetectUsingRpc, chainsToDetectUsingAccountAPI, clientNetworks);
- }
- // Proceed with RPC detection if there are chains remaining in chainsToDetectUsingRpc
- if (chainsToDetectUsingRpc.length > 0) {
-@@ -446,8 +443,7 @@ async function _TokenDetectionController_addDetectedTokensViaAPI({ selectedAddre
- const tokenBalancesByChain = await __classPrivateFieldGet(this, _TokenDetectionController_accountsAPI, "f")
- .getMultiNetworksBalances(selectedAddress, chainIds, supportedNetworks)
- .catch(() => null);
-- if (!tokenBalancesByChain ||
-- Object.keys(tokenBalancesByChain).length === 0) {
-+ if (tokenBalancesByChain === null) {
- return { result: 'failed' };
- }
- // Process each chain ID individually
diff --git a/package.json b/package.json
index 0f963c3aab2b..45896fdabd1f 100644
--- a/package.json
+++ b/package.json
@@ -294,7 +294,7 @@
"@metamask/address-book-controller": "^6.0.0",
"@metamask/announcement-controller": "^7.0.0",
"@metamask/approval-controller": "^7.0.0",
- "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A45.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch",
+ "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A45.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch",
"@metamask/base-controller": "^7.0.0",
"@metamask/bitcoin-wallet-snap": "^0.8.2",
"@metamask/browser-passworder": "^4.3.0",
diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx
index fe3698e2fc2f..6f7de0c95c7c 100644
--- a/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx
+++ b/ui/components/app/wallet-overview/aggregated-percentage-overview-cross-chains.tsx
@@ -1,7 +1,9 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
-import { zeroAddress, toChecksumAddress } from 'ethereumjs-util';
+import { toChecksumAddress } from 'ethereumjs-util';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
+import { Hex } from '@metamask/utils';
import {
getCurrentCurrency,
getSelectedAccount,
@@ -89,8 +91,9 @@ export const AggregatedPercentageOverviewCrossChains = () => {
item.tokensWithBalances,
);
const nativePricePercentChange1d =
- crossChainMarketData?.[item.chainId]?.[zeroAddress()]
- ?.pricePercentChange1d;
+ crossChainMarketData?.[item.chainId]?.[
+ getNativeTokenAddress(item.chainId as Hex)
+ ]?.pricePercentChange1d;
const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo(
item.nativeFiatValue,
diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
index 8da096151908..7610890d48da 100644
--- a/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
+++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.test.tsx
@@ -8,6 +8,7 @@ import {
getShouldHideZeroBalanceTokens,
getTokensMarketData,
getPreferences,
+ getCurrentChainId,
} from '../../../selectors';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
import { AggregatedPercentageOverview } from './aggregated-percentage-overview';
@@ -26,20 +27,22 @@ jest.mock('../../../selectors', () => ({
getPreferences: jest.fn(),
getShouldHideZeroBalanceTokens: jest.fn(),
getTokensMarketData: jest.fn(),
+ getCurrentChainId: jest.fn(),
}));
jest.mock('../../../hooks/useAccountTotalFiatBalance', () => ({
useAccountTotalFiatBalance: jest.fn(),
}));
-const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock;
-const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock;
-const mockGetPreferences = getPreferences as jest.Mock;
-const mockGetSelectedAccount = getSelectedAccount as unknown as jest.Mock;
-const mockGetShouldHideZeroBalanceTokens =
- getShouldHideZeroBalanceTokens as jest.Mock;
-
+const mockGetIntlLocale = jest.mocked(getIntlLocale);
+const mockGetCurrentCurrency = jest.mocked(getCurrentCurrency);
+const mockGetPreferences = jest.mocked(getPreferences);
+const mockGetSelectedAccount = jest.mocked(getSelectedAccount);
+const mockGetShouldHideZeroBalanceTokens = jest.mocked(
+ getShouldHideZeroBalanceTokens,
+);
const mockGetTokensMarketData = getTokensMarketData as jest.Mock;
+const mockGetCurrentChainId = jest.mocked(getCurrentChainId);
const selectedAccountMock = {
id: 'd51c0116-de36-4e77-b35b-408d4ea82d01',
@@ -166,7 +169,7 @@ describe('AggregatedPercentageOverview', () => {
mockGetSelectedAccount.mockReturnValue(selectedAccountMock);
mockGetShouldHideZeroBalanceTokens.mockReturnValue(false);
mockGetTokensMarketData.mockReturnValue(marketDataMock);
-
+ mockGetCurrentChainId.mockReturnValue('0x1');
jest.clearAllMocks();
});
diff --git a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
index 8c609610daa1..89bc94dab774 100644
--- a/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
+++ b/ui/components/app/wallet-overview/aggregated-percentage-overview.tsx
@@ -1,13 +1,15 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
-import { zeroAddress, toChecksumAddress } from 'ethereumjs-util';
+import { toChecksumAddress } from 'ethereumjs-util';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
import {
getCurrentCurrency,
getSelectedAccount,
getShouldHideZeroBalanceTokens,
getTokensMarketData,
getPreferences,
+ getCurrentChainId,
} from '../../../selectors';
import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance';
@@ -37,6 +39,7 @@ export const AggregatedPercentageOverview = () => {
const fiatCurrency = useSelector(getCurrentCurrency);
const { privacyMode } = useSelector(getPreferences);
const selectedAccount = useSelector(getSelectedAccount);
+ const currentChainId = useSelector(getCurrentChainId);
const shouldHideZeroBalanceTokens = useSelector(
getShouldHideZeroBalanceTokens,
);
@@ -63,7 +66,8 @@ export const AggregatedPercentageOverview = () => {
}
// native token
const nativePricePercentChange1d =
- tokensMarketData?.[zeroAddress()]?.pricePercentChange1d;
+ tokensMarketData?.[getNativeTokenAddress(currentChainId)]
+ ?.pricePercentChange1d;
const nativeFiat1dAgo = getCalculatedTokenAmount1dAgo(
item.fiatBalance,
nativePricePercentChange1d,
diff --git a/ui/components/app/wallet-overview/coin-overview.tsx b/ui/components/app/wallet-overview/coin-overview.tsx
index bf054d993e74..3be53776581d 100644
--- a/ui/components/app/wallet-overview/coin-overview.tsx
+++ b/ui/components/app/wallet-overview/coin-overview.tsx
@@ -7,11 +7,11 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import classnames from 'classnames';
-import { zeroAddress } from 'ethereumjs-util';
import { CaipChainId } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import { InternalAccount } from '@metamask/keyring-api';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
import {
Box,
ButtonIcon,
@@ -231,7 +231,10 @@ export const CoinOverview = ({
return (
{
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
diff --git a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx
index abff9f40da8d..439c030a59cd 100644
--- a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx
+++ b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.test.tsx
@@ -2,11 +2,13 @@ import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { zeroAddress } from 'ethereumjs-util';
+import { MarketDataDetails } from '@metamask/assets-controllers';
import { getIntlLocale } from '../../../../../ducks/locale/locale';
import {
getCurrentCurrency,
getSelectedAccountCachedBalance,
getTokensMarketData,
+ getCurrentChainId,
} from '../../../../../selectors';
import {
getConversionRate,
@@ -26,6 +28,7 @@ jest.mock('../../../../../selectors', () => ({
getCurrentCurrency: jest.fn(),
getSelectedAccountCachedBalance: jest.fn(),
getTokensMarketData: jest.fn(),
+ getCurrentChainId: jest.fn(),
}));
jest.mock('../../../../../ducks/metamask/metamask', () => ({
@@ -33,13 +36,15 @@ jest.mock('../../../../../ducks/metamask/metamask', () => ({
getNativeCurrency: jest.fn(),
}));
-const mockGetIntlLocale = getIntlLocale as unknown as jest.Mock;
-const mockGetCurrentCurrency = getCurrentCurrency as jest.Mock;
-const mockGetSelectedAccountCachedBalance =
- getSelectedAccountCachedBalance as jest.Mock;
-const mockGetConversionRate = getConversionRate as jest.Mock;
-const mockGetNativeCurrency = getNativeCurrency as jest.Mock;
-const mockGetTokensMarketData = getTokensMarketData as jest.Mock;
+const mockGetIntlLocale = jest.mocked(getIntlLocale);
+const mockGetCurrentCurrency = jest.mocked(getCurrentCurrency);
+const mockGetSelectedAccountCachedBalance = jest.mocked(
+ getSelectedAccountCachedBalance,
+);
+const mockGetConversionRate = jest.mocked(getConversionRate);
+const mockGetNativeCurrency = jest.mocked(getNativeCurrency);
+const mockGetTokensMarketData = jest.mocked(getTokensMarketData);
+const mockGetCurrentChainId = jest.mocked(getCurrentChainId);
describe('PercentageChange Component', () => {
beforeEach(() => {
@@ -51,9 +56,9 @@ describe('PercentageChange Component', () => {
mockGetTokensMarketData.mockReturnValue({
[zeroAddress()]: {
pricePercentChange1d: 2,
- },
+ } as MarketDataDetails,
});
-
+ mockGetCurrentChainId.mockReturnValue('0x1');
jest.clearAllMocks();
});
@@ -108,4 +113,19 @@ describe('PercentageChange Component', () => {
expect(percentageElement).toBeInTheDocument();
expect(numberElement).toBeInTheDocument();
});
+
+ it('should display percentage for non-zero native tokens (MATIC)', () => {
+ mockGetTokensMarketData.mockReturnValue({
+ '0x0000000000000000000000000000000000001010': {
+ pricePercentChange1d: 2,
+ } as MarketDataDetails,
+ });
+ mockGetCurrentCurrency.mockReturnValue('POL');
+ mockGetCurrentChainId.mockReturnValue('0x89');
+ render();
+ const percentageElement = screen.getByText('(+1.00%)');
+ const numberElement = screen.getByText('+POL 12.21');
+ expect(percentageElement).toBeInTheDocument();
+ expect(numberElement).toBeInTheDocument();
+ });
});
diff --git a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx
index be9921e88793..f1ba436ef47f 100644
--- a/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx
+++ b/ui/components/multichain/token-list-item/price/percentage-and-amount-change/percentage-and-amount-change.tsx
@@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { BigNumber } from 'bignumber.js';
-import { isHexString, zeroAddress } from 'ethereumjs-util';
+import { isHexString } from 'ethereumjs-util';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
import { Text, Box } from '../../../../component-library';
import {
Display,
@@ -9,6 +10,7 @@ import {
TextVariant,
} from '../../../../../helpers/constants/design-system';
import {
+ getCurrentChainId,
getCurrentCurrency,
getSelectedAccountCachedBalance,
getTokensMarketData,
@@ -66,10 +68,12 @@ export const PercentageAndAmountChange = ({
const conversionRate = useSelector(getConversionRate);
const nativeCurrency = useSelector(getNativeCurrency);
const marketData = useSelector(getTokensMarketData);
+ const currentChainId = useSelector(getCurrentChainId);
const balanceChange = useMemo(() => {
// Extracts the 1-day percentage change in price from marketData using the zero address as a key.
- const percentage1d = marketData?.[zeroAddress()]?.pricePercentChange1d;
+ const percentage1d =
+ marketData?.[getNativeTokenAddress(currentChainId)]?.pricePercentChange1d;
// Checks if the balanceValue is in hex format. This is important for cryptocurrency balances which are often represented in hex.
if (isHexString(balanceValue)) {
diff --git a/ui/components/multichain/token-list-item/token-list-item.tsx b/ui/components/multichain/token-list-item/token-list-item.tsx
index 540d2d8be98a..ef49ec3126cb 100644
--- a/ui/components/multichain/token-list-item/token-list-item.tsx
+++ b/ui/components/multichain/token-list-item/token-list-item.tsx
@@ -2,7 +2,8 @@ import React, { useContext, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import classnames from 'classnames';
-import { zeroAddress } from 'ethereumjs-util';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
+import { Hex } from '@metamask/utils';
import {
AlignItems,
BackgroundColor,
@@ -336,13 +337,14 @@ export const TokenListItem = ({
diff --git a/ui/pages/asset/components/asset-page.tsx b/ui/pages/asset/components/asset-page.tsx
index 19ce592f0071..0f4529861dbc 100644
--- a/ui/pages/asset/components/asset-page.tsx
+++ b/ui/pages/asset/components/asset-page.tsx
@@ -3,8 +3,8 @@ import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { EthMethod } from '@metamask/keyring-api';
import { isEqual } from 'lodash';
+import { getNativeTokenAddress } from '@metamask/assets-controllers';
import { Hex } from '@metamask/utils';
-import { zeroAddress } from 'ethereumjs-util';
import {
getCurrentCurrency,
getDataCollectionForMarketing,
@@ -140,7 +140,7 @@ const AssetPage = ({
const address =
type === AssetType.token
? toChecksumHexAddress(asset.address)
- : zeroAddress();
+ : getNativeTokenAddress(chainId);
const balance = calculateTokenBalance({
isNative: type === AssetType.native,
diff --git a/yarn.lock b/yarn.lock
index bc208e1a8f92..db952a4b1e70 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4934,9 +4934,9 @@ __metadata:
languageName: node
linkType: hard
-"@metamask/assets-controllers@npm:45.0.0":
- version: 45.0.0
- resolution: "@metamask/assets-controllers@npm:45.0.0"
+"@metamask/assets-controllers@npm:45.1.0":
+ version: 45.1.0
+ resolution: "@metamask/assets-controllers@npm:45.1.0"
dependencies:
"@ethereumjs/util": "npm:^8.1.0"
"@ethersproject/abi": "npm:^5.7.0"
@@ -4969,13 +4969,13 @@ __metadata:
"@metamask/keyring-controller": ^19.0.0
"@metamask/network-controller": ^22.0.0
"@metamask/preferences-controller": ^15.0.0
- checksum: 10/0ad51464cf060f1c2cab56c2c8d9daa5f29987e8ead69c0e029fb8357fa5c629434116de5663dc38a57c11b3736b6c7d9b1db9b6892a453fbc3f9c6965d42295
+ checksum: 10/7e366739c2b3fc8000aaa8cd302d3e2c3958e29e7c88f3e7e188c4ec46454cf9e894c1e230a84092bba8e6c5274b301dfdb4e55a0ba4322bdcb9e7325ad5a5e5
languageName: node
linkType: hard
-"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A45.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch":
- version: 45.0.0
- resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A45.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch::version=45.0.0&hash=8e5354"
+"@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A45.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch":
+ version: 45.1.0
+ resolution: "@metamask/assets-controllers@patch:@metamask/assets-controllers@npm%3A45.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch::version=45.1.0&hash=86167d"
dependencies:
"@ethereumjs/util": "npm:^8.1.0"
"@ethersproject/abi": "npm:^5.7.0"
@@ -5008,7 +5008,7 @@ __metadata:
"@metamask/keyring-controller": ^19.0.0
"@metamask/network-controller": ^22.0.0
"@metamask/preferences-controller": ^15.0.0
- checksum: 10/823627b5bd23829d81a54291f74c4ddf52d0732a840c121c4ae7f1fc468dd98f3fc1e64b7f8a9bbaaa76cd6670082f2976e5e6ecf872e04c212a5c8ec5fe4916
+ checksum: 10/985ec7dffb75aaff8eea00f556157e42cd5db063cbfa94dfd4f070c5b9d98b1315f3680fa7370f4c734a1688598bbda9c44a7c33c342e1d123d6ee2edd6120fc
languageName: node
linkType: hard
@@ -26816,7 +26816,7 @@ __metadata:
"@metamask/announcement-controller": "npm:^7.0.0"
"@metamask/api-specs": "npm:^0.9.3"
"@metamask/approval-controller": "npm:^7.0.0"
- "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A45.0.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.0.0-31810ece32.patch"
+ "@metamask/assets-controllers": "patch:@metamask/assets-controllers@npm%3A45.1.0#~/.yarn/patches/@metamask-assets-controllers-npm-45.1.0-d914c453f0.patch"
"@metamask/auto-changelog": "npm:^2.1.0"
"@metamask/base-controller": "npm:^7.0.0"
"@metamask/bitcoin-wallet-snap": "npm:^0.8.2"
From 6eb7ccf4f4e4f080cb7d7550b3742202d22a5bea Mon Sep 17 00:00:00 2001
From: micaelae <100321200+micaelae@users.noreply.github.com>
Date: Fri, 22 Nov 2024 13:50:35 -0800
Subject: [PATCH 04/40] chore: sort and display all bridge quotes (#27731)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Changes
- Fetch exchange rates on src/dest token selection
- Calculate quote metadata and implement sorting
- Create and style modal for displaying all bridge quotes
- Autofill src token if navigating from asset page
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27731?quickstart=1)
## **Related issues**
Fixes: N/A
## **Manual testing steps**
1. Request quotes
2. View all quotes
3. Toggle sorting and inspect output
4. Verify that modal matches mocks
5. Try selecting alternate quote
## **Screenshots/Recordings**
### **Before**
Mocks:
https://www.figma.com/design/IuOIRmU3wI0IdJIfol0ESu/Cross-Chain-Swaps?node-id=1374-7239&m=dev
### **After**
![Screenshot 2024-11-12 at 5 08
49 PM](https://github.com/user-attachments/assets/f9db89b7-2753-4259-a28d-b5eb7e908323)
## **Pre-merge author checklist**
- [X] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/_locales/en/messages.json | 13 +-
.../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 +
shared/constants/bridge.ts | 4 +
.../data/bridge/mock-quotes-erc20-native.json | 894 ++++++++++++++++++
.../bridge/mock-quotes-native-erc20-eth.json | 258 +++++
.../data/bridge/mock-quotes-native-erc20.json | 2 +-
test/jest/mock-store.js | 12 +
ui/ducks/bridge/actions.ts | 12 +-
ui/ducks/bridge/bridge.test.ts | 103 +-
ui/ducks/bridge/bridge.ts | 41 +-
ui/ducks/bridge/selectors.test.ts | 379 +++++++-
ui/ducks/bridge/selectors.ts | 262 ++++-
ui/ducks/bridge/utils.ts | 19 +
ui/hooks/bridge/useBridging.test.ts | 8 +-
ui/hooks/bridge/useBridging.ts | 9 +-
ui/hooks/bridge/useCountdownTimer.test.ts | 5 +-
ui/hooks/useTokensWithFiltering.ts | 16 +
ui/pages/bridge/index.tsx | 32 +-
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 ++
ui/pages/bridge/prepare/bridge-cta-button.tsx | 12 +-
.../bridge/prepare/bridge-input-group.tsx | 12 +-
.../prepare/prepare-bridge-page.test.tsx | 7 +
.../bridge/prepare/prepare-bridge-page.tsx | 107 ++-
.../bridge-quote-card.test.tsx.snap | 16 +-
.../bridge-quotes-modal.test.tsx.snap | 135 ++-
.../bridge/quotes/bridge-quote-card.test.tsx | 4 +-
ui/pages/bridge/quotes/bridge-quote-card.tsx | 71 +-
.../quotes/bridge-quotes-modal.stories.tsx | 106 +++
.../bridge/quotes/bridge-quotes-modal.tsx | 201 +++-
ui/pages/bridge/quotes/index.scss | 20 +-
ui/pages/bridge/types.ts | 24 +
ui/pages/bridge/utils/quote.test.ts | 309 ++++++
ui/pages/bridge/utils/quote.ts | 175 +++-
39 files changed, 3412 insertions(+), 273 deletions(-)
create mode 100644 test/data/bridge/mock-quotes-erc20-native.json
create mode 100644 test/data/bridge/mock-quotes-native-erc20-eth.json
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
create mode 100644 ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
create mode 100644 ui/pages/bridge/utils/quote.test.ts
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index f51c9708fc20..2d603ddc5156 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -863,8 +863,8 @@
"bridgeFrom": {
"message": "Bridge from"
},
- "bridgeOverallCost": {
- "message": "Overall cost"
+ "bridgeNetCost": {
+ "message": "Net cost"
},
"bridgeSelectNetwork": {
"message": "Select network"
@@ -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": {
@@ -4373,6 +4373,13 @@
"quoteRate": {
"message": "Quote rate"
},
+ "quotedNetworkFee": { "message": "$1 network fee" },
+ "quotedReceiveAmount": {
+ "message": "$1 receive amount"
+ },
+ "quotedReceivingAmount": {
+ "message": "$1 receiving"
+ },
"rank": {
"message": "Rank"
},
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/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-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/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..a3543e485bb7 100644
--- a/test/jest/mock-store.js
+++ b/test/jest/mock-store.js
@@ -4,6 +4,7 @@ import { KeyringType } from '../../shared/constants/keyring';
import { ETH_EOA_METHODS } from '../../shared/constants/eth-methods';
import { mockNetworkState } from '../stub/networks';
import { DEFAULT_BRIDGE_CONTROLLER_STATE } from '../../app/scripts/controllers/bridge/constants';
+import { BRIDGE_PREFERRED_GAS_ESTIMATE } from '../../shared/constants/bridge';
export const createGetSmartTransactionFeesApiResponse = () => {
return {
@@ -711,6 +712,7 @@ export const createBridgeMockStore = (
...swapsStore,
bridge: {
toChainId: null,
+ sortOrder: 0,
...bridgeSliceOverrides,
},
metamask: {
@@ -719,6 +721,16 @@ export const createBridgeMockStore = (
{ chainId: CHAIN_IDS.MAINNET },
{ chainId: CHAIN_IDS.LINEA_MAINNET },
),
+ gasFeeEstimates: {
+ estimatedBaseFee: '0.00010456',
+ [BRIDGE_PREFERRED_GAS_ESTIMATE]: {
+ suggestedMaxFeePerGas: '0.00018456',
+ suggestedMaxPriorityFeePerGas: '0.0001',
+ },
+ },
+ currencyRates: {
+ ETH: { conversionRate: 2524.25 },
+ },
...metamaskStateOverrides,
bridgeState: {
...(swapsStore.metamask.bridgeState ?? {}),
diff --git a/ui/ducks/bridge/actions.ts b/ui/ducks/bridge/actions.ts
index a61d2fdcd8fd..766689cb8cda 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 } from './bridge';
+import {
+ bridgeSlice,
+ setDestTokenExchangeRates,
+ setSrcTokenExchangeRates,
+} from './bridge';
const {
setToChainId,
@@ -19,6 +23,8 @@ const {
setToToken,
setFromTokenInputValue,
resetInputFields,
+ setSortOrder,
+ setSelectedQuote,
} = bridgeSlice.actions;
export {
@@ -27,6 +33,10 @@ export {
setToToken,
setFromToken,
setFromTokenInputValue,
+ setDestTokenExchangeRates,
+ setSrcTokenExchangeRates,
+ setSortOrder,
+ setSelectedQuote,
};
const callBridgeControllerMethod = (
diff --git a/ui/ducks/bridge/bridge.test.ts b/ui/ducks/bridge/bridge.test.ts
index dc9596fcafba..5a395fa23036 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];
@@ -143,10 +145,14 @@ describe('Ducks - Bridge', () => {
expect(actions[0].type).toStrictEqual('bridge/resetInputFields');
const newState = bridgeReducer(state, actions[0]);
expect(newState).toStrictEqual({
+ selectedQuote: null,
toChainId: null,
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ sortOrder: 0,
+ toTokenExchangeRate: null,
+ fromTokenExchangeRate: null,
});
});
});
@@ -201,10 +207,103 @@ describe('Ducks - Bridge', () => {
expect(actions[0].type).toStrictEqual('bridge/resetInputFields');
const newState = bridgeReducer(state, actions[0]);
expect(newState).toStrictEqual({
- toChainId: null,
fromToken: null,
- toToken: null,
+ fromTokenExchangeRate: null,
fromTokenInputValue: null,
+ selectedQuote: null,
+ sortOrder: 0,
+ toChainId: null,
+ toToken: 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,
+ sortOrder: 0,
+ });
+ });
+
+ 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,
+ sortOrder: 0,
});
});
});
diff --git a/ui/ducks/bridge/bridge.ts b/ui/ducks/bridge/bridge.ts
index c75030c7591d..edb0c9ca0d13 100644
--- a/ui/ducks/bridge/bridge.ts
+++ b/ui/ducks/bridge/bridge.ts
@@ -1,15 +1,24 @@
-import { createSlice } from '@reduxjs/toolkit';
-
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { Hex } from '@metamask/utils';
import { swapsSlice } from '../swaps/swaps';
import { SwapsTokenObject } from '../../../shared/constants/swaps';
import { SwapsEthToken } from '../../selectors';
+import {
+ QuoteMetadata,
+ QuoteResponse,
+ SortOrder,
+} from '../../pages/bridge/types';
+import { getTokenExchangeRate } from './utils';
export type BridgeState = {
toChainId: Hex | null;
fromToken: SwapsTokenObject | SwapsEthToken | null;
toToken: SwapsTokenObject | SwapsEthToken | null;
fromTokenInputValue: string | null;
+ 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 = {
@@ -17,8 +26,22 @@ const initialState: BridgeState = {
fromToken: null,
toToken: null,
fromTokenInputValue: null,
+ fromTokenExchangeRate: null,
+ toTokenExchangeRate: null,
+ sortOrder: SortOrder.COST_ASC,
+ selectedQuote: null,
};
+export const setSrcTokenExchangeRates = createAsyncThunk(
+ 'bridge/setSrcTokenExchangeRates',
+ getTokenExchangeRate,
+);
+
+export const setDestTokenExchangeRates = createAsyncThunk(
+ 'bridge/setDestTokenExchangeRates',
+ getTokenExchangeRate,
+);
+
const bridgeSlice = createSlice({
name: 'bridge',
initialState: { ...initialState },
@@ -39,6 +62,20 @@ const bridgeSlice = createSlice({
resetInputFields: () => ({
...initialState,
}),
+ setSortOrder: (state, action) => {
+ state.sortOrder = action.payload;
+ },
+ setSelectedQuote: (state, action) => {
+ state.selectedQuote = action.payload;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(setDestTokenExchangeRates.fulfilled, (state, action) => {
+ state.toTokenExchangeRate = action.payload ?? null;
+ });
+ builder.addCase(setSrcTokenExchangeRates.fulfilled, (state, action) => {
+ state.fromTokenExchangeRate = action.payload ?? null;
+ });
},
});
diff --git a/ui/ducks/bridge/selectors.test.ts b/ui/ducks/bridge/selectors.test.ts
index e39f73f2fa15..b92c8e60e4f0 100644
--- a/ui/ducks/bridge/selectors.test.ts
+++ b/ui/ducks/bridge/selectors.test.ts
@@ -1,12 +1,18 @@
+import { BigNumber } from 'bignumber.js';
import { createBridgeMockStore } from '../../../test/jest/mock-store';
import {
BUILT_IN_NETWORKS,
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,
@@ -17,7 +23,6 @@ import {
getFromTokens,
getFromTopAssets,
getIsBridgeTx,
- getToAmount,
getToChain,
getToChains,
getToToken,
@@ -392,15 +397,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(
@@ -498,7 +494,12 @@ describe('Bridge selectors', () => {
it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=5', () => {
const state = createBridgeMockStore(
{ extensionConfig: { maxRefreshCount: 5 } },
- { toChainId: '0x1' },
+ {
+ toChainId: '0x1',
+ fromTokenExchangeRate: 1,
+ toTokenExchangeRate: 0.99,
+ toNativeExchangeRate: 0.354073,
+ },
{
quoteRequest: { insufficientBal: false },
quotes: mockErc20Erc20Quotes,
@@ -508,11 +509,51 @@ describe('Bridge selectors', () => {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
},
+ {
+ currencyRates: {
+ ETH: {
+ conversionRate: 1,
+ },
+ },
+ },
);
- const result = getBridgeQuotes(state as never);
+ const recommendedQuoteMetadata = {
+ adjustedReturn: {
+ fiat: expect.any(Object),
+ },
+ cost: { fiat: new BigNumber('0.15656287141025952') },
+ sentAmount: {
+ fiat: new BigNumber('14'),
+ amount: new BigNumber('14'),
+ },
+ swapRate: new BigNumber('0.998877142857142857142857142857142857'),
+ toTokenAmount: {
+ 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'),
+ },
+ };
+
+ const result = getBridgeQuotes(state as never);
+ expect(result.sortedQuotes).toHaveLength(2);
expect(result).toStrictEqual({
- quotes: mockErc20Erc20Quotes,
+ sortedQuotes: expect.any(Array),
+ recommendedQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
+ activeQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
quotesLastFetchedMs: 100,
isLoading: false,
quotesRefreshCount: 5,
@@ -523,7 +564,12 @@ describe('Bridge selectors', () => {
it('returns quote list and fetch data, insufficientBal=false,quotesRefreshCount=2', () => {
const state = createBridgeMockStore(
{ extensionConfig: { maxRefreshCount: 5 } },
- { toChainId: '0x1' },
+ {
+ toChainId: '0x1',
+ fromTokenExchangeRate: 1,
+ toTokenExchangeRate: 0.99,
+ toNativeExchangeRate: 0.354073,
+ },
{
quoteRequest: { insufficientBal: false },
quotes: mockErc20Erc20Quotes,
@@ -533,11 +579,57 @@ describe('Bridge selectors', () => {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
},
+ {
+ currencyRates: {
+ ETH: {
+ conversionRate: 1,
+ },
+ },
+ },
);
const result = getBridgeQuotes(state as never);
+ const recommendedQuoteMetadata = {
+ adjustedReturn: {
+ fiat: new BigNumber('13.84343712858974048'),
+ },
+ cost: { fiat: new BigNumber('0.15656287141025952') },
+ sentAmount: {
+ fiat: new BigNumber('14'),
+ amount: new BigNumber('14'),
+ },
+ swapRate: new BigNumber('0.998877142857142857142857142857142857'),
+ toTokenAmount: {
+ 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'),
+ },
+ };
+ expect(result.sortedQuotes).toHaveLength(2);
+ const EXPECTED_SORTED_COSTS = [
+ { fiat: new BigNumber('0.15656287141025952') },
+ { fiat: new BigNumber('0.33900008283534464') },
+ ];
+ result.sortedQuotes.forEach((quote, idx) => {
+ expect(quote.cost).toStrictEqual(EXPECTED_SORTED_COSTS[idx]);
+ });
expect(result).toStrictEqual({
- quotes: mockErc20Erc20Quotes,
+ sortedQuotes: expect.any(Array),
+ recommendedQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
+ activeQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
quotesLastFetchedMs: 100,
isLoading: false,
quotesRefreshCount: 2,
@@ -548,7 +640,12 @@ describe('Bridge selectors', () => {
it('returns quote list and fetch data, insufficientBal=true', () => {
const state = createBridgeMockStore(
{ extensionConfig: { maxRefreshCount: 5 } },
- { toChainId: '0x1' },
+ {
+ toChainId: '0x1',
+ fromTokenExchangeRate: 1,
+ toTokenExchangeRate: 0.99,
+ toNativeExchangeRate: 0.354073,
+ },
{
quoteRequest: { insufficientBal: true },
quotes: mockErc20Erc20Quotes,
@@ -558,11 +655,58 @@ describe('Bridge selectors', () => {
srcTokens: { '0x00': { address: '0x00', symbol: 'TEST' } },
srcTopAssets: [{ address: '0x00', symbol: 'TEST' }],
},
+ {
+ currencyRates: {
+ ETH: {
+ conversionRate: 1,
+ },
+ },
+ },
);
const result = getBridgeQuotes(state as never);
+ const recommendedQuoteMetadata = {
+ adjustedReturn: {
+ fiat: new BigNumber('13.84343712858974048'),
+ },
+ cost: { fiat: new BigNumber('0.15656287141025952') },
+ sentAmount: {
+ fiat: new BigNumber('14'),
+ amount: new BigNumber('14'),
+ },
+ swapRate: new BigNumber('0.998877142857142857142857142857142857'),
+ toTokenAmount: {
+ 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'),
+ },
+ };
+ expect(result.sortedQuotes).toHaveLength(2);
+ const EXPECTED_SORTED_COSTS = [
+ { fiat: new BigNumber('0.15656287141025952') },
+ { fiat: new BigNumber('0.33900008283534464') },
+ ];
+ result.sortedQuotes.forEach((quote, idx) => {
+ expect(quote.cost).toStrictEqual(EXPECTED_SORTED_COSTS[idx]);
+ });
+
expect(result).toStrictEqual({
- quotes: mockErc20Erc20Quotes,
+ sortedQuotes: expect.any(Array),
+ recommendedQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
+ activeQuote: {
+ ...mockErc20Erc20Quotes[0],
+ ...recommendedQuoteMetadata,
+ },
quotesLastFetchedMs: 100,
isLoading: false,
quotesRefreshCount: 1,
@@ -570,4 +714,203 @@ 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,
+ isQuoteGoingToRefresh: false,
+ quotesLastFetchedMs: undefined,
+ quotesRefreshCount: undefined,
+ recommendedQuote: undefined,
+ sortedQuotes: [],
+ });
+ });
+
+ it('should sort quotes by adjustedReturn', () => {
+ const state = createBridgeMockStore(
+ {},
+ {},
+ { 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(
+ {},
+ { sortOrder: SortOrder.ETA_ASC },
+ {
+ 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(
+ {},
+ { sortOrder: SortOrder.COST_ASC },
+ {
+ 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(
+ {},
+ {
+ sortOrder: SortOrder.ETA_ASC,
+ toTokenExchangeRate: 0.998781,
+ toNativeExchangeRate: 0.354073,
+ },
+ {
+ quotes: [
+ ...mockBridgeQuotesNativeErc20,
+ {
+ ...mockBridgeQuotesNativeErc20[0],
+ estimatedProcessingTimeInSeconds: 1,
+ quote: {
+ ...mockBridgeQuotesNativeErc20[0].quote,
+ requestId: 'fastestQuote',
+ destTokenAmount: '1',
+ },
+ },
+ ],
+ },
+ {
+ currencyRates: {
+ ETH: {
+ conversionRate: 2524.25,
+ },
+ },
+ },
+ );
+
+ 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..b78a0d09de51 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,38 @@ 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 { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
+import { getConversionRate, getGasFeeEstimates } from '../metamask/metamask';
// 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,
+} from '../../pages/bridge/types';
+import {
+ calcAdjustedReturn,
+ calcCost,
+ calcRelayerFee,
+ calcSentAmount,
+ calcSwapRate,
+ calcToAmount,
+ calcTotalGasFee,
+ 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,48 +167,203 @@ 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,
+ ),
+ }),
+);
+
+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
+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)[] => {
+ const newQuotes = quotes.map((quote: QuoteResponse) => {
+ const toTokenAmount = calcToAmount(quote.quote, toTokenExchangeRate);
+ 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)
+ ? nativeExchangeRate
+ : fromTokenExchangeRate,
+ );
+ const adjustedReturn = calcAdjustedReturn(
+ toTokenAmount.fiat,
+ totalNetworkFee.fiat,
+ );
+
+ return {
+ ...quote,
+ toTokenAmount,
+ sentAmount,
+ totalNetworkFee,
+ adjustedReturn,
+ gasFee,
+ swapRate: calcSwapRate(sentAmount.amount, toTokenAmount.amount),
+ cost: calcCost(adjustedReturn.fiat, sentAmount.fiat),
+ };
+ });
+
+ return newQuotes;
+ },
+);
+
+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]
+ );
+ },
+);
+
+// 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(
+ (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
+ ? _getQuoteIdentifier(quote) === _getQuoteIdentifier(selectedQuote)
+ : false,
+ ),
+);
+
export const getBridgeQuotes = createSelector(
- (state: BridgeAppState) => state.metamask.bridgeState.quotes,
- (state: BridgeAppState) => state.metamask.bridgeState.quotesLastFetched,
- (state: BridgeAppState) =>
+ _getSortedQuotesWithMetadata,
+ _getRecommendedQuote,
+ _getSelectedQuote,
+ (state) => state.metamask.bridgeState.quotesLastFetched,
+ (state) =>
state.metamask.bridgeState.quotesLoadingStatus === RequestStatus.LOADING,
(state: BridgeAppState) => state.metamask.bridgeState.quotesRefreshCount,
getBridgeQuotesConfig,
getQuoteRequest,
(
- quotes,
+ sortedQuotesWithMetadata,
+ recommendedQuote,
+ selectedQuote,
quotesLastFetchedMs,
isLoading,
quotesRefreshCount,
{ maxRefreshCount },
{ insufficientBal },
- ) => {
- return {
- quotes,
- quotesLastFetchedMs,
- isLoading,
- quotesRefreshCount,
- isQuoteGoingToRefresh: insufficientBal
- ? false
- : quotesRefreshCount < maxRefreshCount,
- };
- },
-);
-
-export const getRecommendedQuote = createSelector(
- getBridgeQuotes,
- ({ quotes }) => {
- return quotes[0];
- },
-);
-
-export const getToAmount = createSelector(getRecommendedQuote, (quote) =>
- quote
- ? calcTokenAmount(
- quote.quote.destTokenAmount,
- quote.quote.destAsset.decimals,
- )
- : undefined,
+ ) => ({
+ sortedQuotes: sortedQuotesWithMetadata,
+ recommendedQuote,
+ activeQuote: selectedQuote ?? recommendedQuote,
+ quotesLastFetchedMs,
+ isLoading,
+ quotesRefreshCount,
+ isQuoteGoingToRefresh: insufficientBal
+ ? false
+ : quotesRefreshCount < maxRefreshCount,
+ }),
);
export const getIsBridgeTx = createDeepEqualSelector(
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)]
+ );
+};
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..62945e3e7a92 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,9 @@ 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 +113,6 @@ const useBridging = () => {
[
isBridgeSupported,
isBridgeChain,
- setSwapsFromToken,
dispatch,
usingHardwareWallet,
history,
diff --git a/ui/hooks/bridge/useCountdownTimer.test.ts b/ui/hooks/bridge/useCountdownTimer.test.ts
index f2cd1190b1ba..293fe1ac679b 100644
--- a/ui/hooks/bridge/useCountdownTimer.test.ts
+++ b/ui/hooks/bridge/useCountdownTimer.test.ts
@@ -17,14 +17,11 @@ describe('useCountdownTimer', () => {
const quotesLastFetched = Date.now();
const { result } = renderUseCountdownTimer(
createBridgeMockStore(
- {},
+ { extensionConfig: { maxRefreshCount: 5, refreshRate: 40000 } },
{},
{
quotesLastFetched,
quotesRefreshCount: 0,
- bridgeFeatureFlags: {
- extensionConfig: { maxRefreshCount: 5, refreshRate: 40000 },
- },
},
),
);
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/index.tsx b/ui/pages/bridge/index.tsx
index 687057094005..6dd54b424d06 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,25 @@ 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 { useGasFeeEstimates } from '../../hooks/useGasFeeEstimates';
import PrepareBridgePage from './prepare/prepare-bridge-page';
import { BridgeCTAButton } from './prepare/bridge-cta-button';
@@ -42,13 +52,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());
@@ -66,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/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;
diff --git a/ui/pages/bridge/prepare/bridge-cta-button.tsx b/ui/pages/bridge/prepare/bridge-cta-button.tsx
index 06d784f2e0ea..7355e6579dfa 100644
--- a/ui/pages/bridge/prepare/bridge-cta-button.tsx
+++ b/ui/pages/bridge/prepare/bridge-cta-button.tsx
@@ -2,14 +2,12 @@ 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';
@@ -25,15 +23,13 @@ export const BridgeCTAButton = () => {
const toChain = useSelector(getToChain);
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 && activeQuote;
const label = useMemo(() => {
if (isLoading && !isTxSubmittable) {
@@ -59,7 +55,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/prepare/prepare-bridge-page.test.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
index aba35d5b89be..95248bdbb0bc 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.test.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { act } from '@testing-library/react';
+import * as reactRouterUtils from 'react-router-dom-v5-compat';
import { fireEvent, renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import { createBridgeMockStore } from '../../../../test/jest/mock-store';
@@ -23,6 +24,9 @@ describe('PrepareBridgePage', () => {
});
it('should render the component, with initial state', async () => {
+ jest
+ .spyOn(reactRouterUtils, 'useSearchParams')
+ .mockReturnValue([{ get: () => null }] as never);
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.OPTIMISM],
@@ -54,6 +58,9 @@ describe('PrepareBridgePage', () => {
});
it('should render the component, with inputs set', async () => {
+ jest
+ .spyOn(reactRouterUtils, 'useSearchParams')
+ .mockReturnValue([{ get: () => '0x3103910' }, jest.fn()] as never);
const mockStore = createBridgeMockStore(
{
srcNetworkAllowlist: [CHAIN_IDS.MAINNET, CHAIN_IDS.LINEA_MAINNET],
diff --git a/ui/pages/bridge/prepare/prepare-bridge-page.tsx b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
index b0553407686d..aea037c71f13 100644
--- a/ui/pages/bridge/prepare/prepare-bridge-page.tsx
+++ b/ui/pages/bridge/prepare/prepare-bridge-page.tsx
@@ -2,16 +2,23 @@ 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 { zeroAddress } from 'ethereumjs-util';
+import { useHistory, useLocation } from 'react-router-dom';
import {
+ setDestTokenExchangeRates,
setFromChain,
setFromToken,
setFromTokenInputValue,
+ setSrcTokenExchangeRates,
+ setSelectedQuote,
setToChain,
setToChainId,
setToToken,
updateQuoteRequestParams,
} from '../../../ducks/bridge/actions';
import {
+ getBridgeQuotes,
getFromAmount,
getFromChain,
getFromChains,
@@ -19,7 +26,6 @@ import {
getFromTokens,
getFromTopAssets,
getQuoteRequest,
- getToAmount,
getToChain,
getToChains,
getToToken,
@@ -42,6 +48,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 +57,8 @@ const PrepareBridgePage = () => {
const t = useI18nContext();
+ const currency = useSelector(getCurrentCurrency);
+
const fromToken = useSelector(getFromToken);
const fromTokens = useSelector(getFromTokens);
const fromTopAssets = useSelector(getFromTopAssets);
@@ -63,11 +73,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,
@@ -114,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),
[],
);
@@ -125,6 +135,62 @@ const PrepareBridgePage = () => {
debouncedUpdateQuoteRequestInController(quoteParams);
}, Object.values(quoteParams));
+ const debouncedFetchFromExchangeRate = debounce(
+ (chainId: Hex, tokenAddress: string) => {
+ dispatch(setSrcTokenExchangeRates({ chainId, tokenAddress, currency }));
+ },
+ SECOND,
+ );
+
+ const debouncedFetchToExchangeRate = debounce(
+ (chainId: Hex, tokenAddress: string) => {
+ dispatch(setDestTokenExchangeRates({ chainId, tokenAddress, currency }));
+ },
+ 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 (
@@ -138,6 +204,9 @@ const PrepareBridgePage = () => {
onAssetChange={(token) => {
dispatch(setFromToken(token));
dispatch(setFromTokenInputValue(null));
+ fromChain?.chainId &&
+ token?.address &&
+ debouncedFetchFromExchangeRate(fromChain.chainId, token.address);
}}
networkProps={{
network: fromChain,
@@ -192,6 +261,19 @@ 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,
+ );
+ toChain?.chainId &&
+ toToken?.address &&
+ toToken.address !== zeroAddress() &&
+ debouncedFetchFromExchangeRate(
+ toChain.chainId,
+ toToken.address,
+ );
}}
/>
@@ -200,7 +282,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,
@@ -218,8 +305,10 @@ const PrepareBridgePage = () => {
testId: 'to-amount',
readOnly: true,
disabled: true,
- value: toAmount?.toString() ?? '0',
- className: toAmount ? 'amount-input defined' : 'amount-input',
+ value: activeQuote?.toTokenAmount?.amount.toFixed() ?? '0',
+ className: activeQuote?.toTokenAmount.amount
+ ? '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 cb7b5afb4c77..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 = 0.9989 USDC
+ 1 USDC = 1.00 USDC
@@ -131,13 +131,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.01 ETH
+ 0.001000 ETH
- $0.01
+ $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 = 2465.4630 USDC
+ 1 ETH = 2443.89 USDC
@@ -304,13 +304,13 @@ Fees are based on network traffic and transaction complexity. MetaMask does not
- 0.01 ETH
+ 0.001000 ETH
- $0.01
+ $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 41d8a03d1ac1..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
@@ -33,31 +33,31 @@ exports[`BridgeQuotesModal should render the modal 1`] = `
class="mm-box mm-header-base mm-modal-header mm-box--padding-right-4 mm-box--padding-bottom-4 mm-box--padding-left-4 mm-box--display-flex mm-box--justify-content-space-between"
>
-
- Select a quote
-
-
-
+
+
+ Select a quote
+
+
-
+
- $0.01
-
-
+
+ $3 network fee
+
+
+ 14 USDC receive amount
+
+
+
- 1 minutes
-
+
+ 1 min
+
+
+ Across
+
+
-
- $0.01
-
-
+
+ $3 network fee
+
+
+ 14 USDC receive amount
+
+
+
- 26 minutes
-
+
+ 26 min
+
+
+ Celercircle
+
+
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-quote-card.tsx b/ui/pages/bridge/quotes/bridge-quote-card.tsx
index fc1176c8c3f9..8adf675afbb3 100644
--- a/ui/pages/bridge/quotes/bridge-quote-card.tsx
+++ b/ui/pages/bridge/quotes/bridge-quote-card.tsx
@@ -6,30 +6,32 @@ import {
ButtonVariant,
Text,
} from '../../../components/component-library';
-import {
- getBridgeQuotes,
- getRecommendedQuote,
-} from '../../../ducks/bridge/selectors';
+import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
import { useI18nContext } from '../../../hooks/useI18nContext';
-import { getQuoteDisplayData } from '../utils/quote';
+import {
+ formatFiatAmount,
+ formatTokenAmount,
+ formatEtaInMinutes,
+} from '../utils/quote';
import { useCountdownTimer } from '../../../hooks/bridge/useCountdownTimer';
import MascotBackgroundAnimation from '../../swaps/mascot-background-animation/mascot-background-animation';
+import { getCurrentCurrency } from '../../../selectors';
+import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { QuoteInfoRow } from './quote-info-row';
import { BridgeQuotesModal } from './bridge-quotes-modal';
export const BridgeQuoteCard = () => {
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 currency = useSelector(getCurrentCurrency);
+ const ticker = useSelector(getNativeCurrency);
const secondsUntilNextRefresh = useCountdownTimer();
const [showAllQuotes, setShowAllQuotes] = useState(false);
- if (isLoading && !recommendedQuote) {
+ if (isLoading && !activeQuote) {
return (
@@ -37,7 +39,7 @@ export const BridgeQuoteCard = () => {
);
}
- return etaInMinutes && totalFees && quoteRate ? (
+ return activeQuote ? (
{
-
-
+ {activeQuote.swapRate && (
+
+ )}
+ {activeQuote.totalNetworkFee && (
+
+ )}
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..bbdf9b47fa47
--- /dev/null
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.stories.tsx
@@ -0,0 +1,106 @@
+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';
+import { SortOrder } from '../types';
+
+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 7e78e515af6e..0f4986aa18fc 100644
--- a/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
+++ b/ui/pages/bridge/quotes/bridge-quotes-modal.tsx
@@ -1,11 +1,9 @@
import React from 'react';
-import { useSelector } from 'react-redux';
import { IconName } from '@metamask/snaps-sdk/jsx';
+import { useDispatch, useSelector } from 'react-redux';
+import { startCase } from 'lodash';
import {
- Box,
- Button,
- ButtonVariant,
- Icon,
+ ButtonLink,
IconSize,
Modal,
ModalContent,
@@ -14,51 +12,198 @@ import {
Text,
} from '../../../components/component-library';
import {
+ AlignItems,
+ BackgroundColor,
TextAlign,
+ TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
-import { getBridgeQuotes } from '../../../ducks/bridge/selectors';
-import { getQuoteDisplayData } from '../utils/quote';
+import {
+ formatEtaInMinutes,
+ formatFiatAmount,
+ formatTokenAmount,
+} from '../utils/quote';
import { useI18nContext } from '../../../hooks/useI18nContext';
+import { getCurrentCurrency } from '../../../selectors';
+import { setSelectedQuote, setSortOrder } from '../../../ducks/bridge/actions';
+import { SortOrder } from '../types';
+import {
+ getBridgeQuotes,
+ getBridgeSortOrder,
+} from '../../../ducks/bridge/selectors';
+import { Column, Row } from '../layout';
+import { getNativeCurrency } from '../../../ducks/metamask/metamask';
export const BridgeQuotesModal = ({
onClose,
...modalProps
}: Omit, 'children'>) => {
- const { quotes } = useSelector(getBridgeQuotes);
const t = useI18nContext();
+ const dispatch = useDispatch();
+
+ const { sortedQuotes, activeQuote } = useSelector(getBridgeQuotes);
+ const sortOrder = useSelector(getBridgeSortOrder);
+ const currency = useSelector(getCurrentCurrency);
+ const nativeCurrency = useSelector(getNativeCurrency);
return (
-
-
+
+
+
{t('swapSelectAQuote')}
-
- {[t('bridgeOverallCost'), t('time')].map((label) => {
- return (
-
- );
- })}
-
-
- {quotes.map((quote, index) => {
- const { totalFees, etaInMinutes } = getQuoteDisplayData(quote);
+ {/* HEADERS */}
+
+ {[
+ [SortOrder.COST_ASC, t('bridgeNetCost'), IconName.Arrow2Up],
+ [SortOrder.ETA_ASC, t('time'), IconName.Arrow2Down],
+ ].map(([sortOrderOption, label, icon]) => (
+ dispatch(setSortOrder(sortOrderOption))}
+ 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
+ : TextColor.textAlternative
+ }
+ >
+
+ {label}
+
+
+ ))}
+
+ {/* QUOTE LIST */}
+
+ {sortedQuotes.map((quote, index) => {
+ const {
+ totalNetworkFee,
+ estimatedProcessingTimeInSeconds,
+ toTokenAmount,
+ cost,
+ quote: { destAsset, bridges, requestId },
+ } = quote;
+ const isQuoteActive = requestId === activeQuote?.quote.requestId;
+
return (
-
- {totalFees?.fiat}
- {t('bridgeTimingMinutes', [etaInMinutes])}
-
+ {
+ dispatch(setSelectedQuote(quote));
+ onClose();
+ }}
+ paddingInline={4}
+ paddingTop={3}
+ paddingBottom={3}
+ style={{ position: 'relative', height: 78 }}
+ >
+ {isQuoteActive && (
+
+ )}
+
+
+ {cost.fiat && formatFiatAmount(cost.fiat, currency, 0)}
+
+ {[
+ totalNetworkFee?.fiat
+ ? t('quotedNetworkFee', [
+ formatFiatAmount(totalNetworkFee.fiat, currency, 0),
+ ])
+ : t('quotedNetworkFee', [
+ formatTokenAmount(
+ totalNetworkFee.amount,
+ nativeCurrency,
+ ),
+ ]),
+ t(
+ sortOrder === SortOrder.ETA_ASC
+ ? 'quotedReceivingAmount'
+ : 'quotedReceiveAmount',
+ [
+ formatFiatAmount(toTokenAmount.fiat, currency, 0) ??
+ formatTokenAmount(
+ toTokenAmount.amount,
+ destAsset.symbol,
+ 0,
+ ),
+ ],
+ ),
+ ]
+ [sortOrder === SortOrder.ETA_ASC ? 'reverse' : 'slice']()
+ .map((content) => (
+
+ {content}
+
+ ))}
+
+
+
+ {t('bridgeTimingMinutes', [
+ formatEtaInMinutes(estimatedProcessingTimeInSeconds),
+ ])}
+
+
+ {startCase(bridges[0])}
+
+
+
);
})}
-
+
);
diff --git a/ui/pages/bridge/quotes/index.scss b/ui/pages/bridge/quotes/index.scss
index 6d52c9e1e753..6407309220c2 100644
--- a/ui/pages/bridge/quotes/index.scss
+++ b/ui/pages/bridge/quotes/index.scss
@@ -63,21 +63,7 @@
}
}
-.quotes-modal {
- &__column-header {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
-
- &__quotes {
- display: flex;
- flex-direction: column;
-
- &__row {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- }
+.mm-modal-content__dialog {
+ display: flex;
+ height: 100%;
}
diff --git a/ui/pages/bridge/types.ts b/ui/pages/bridge/types.ts
index a1ee163eca48..61143bb9de68 100644
--- a/ui/pages/bridge/types.ts
+++ b/ui/pages/bridge/types.ts
@@ -1,3 +1,27 @@
+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
+// 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
+ sentAmount: { amount: 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..eec342517f83
--- /dev/null
+++ b/ui/pages/bridge/utils/quote.test.ts
@@ -0,0 +1,309 @@
+import { BigNumber } from 'bignumber.js';
+import { zeroAddress } from 'ethereumjs-util';
+import {
+ calcAdjustedReturn,
+ calcSentAmount,
+ calcSwapRate,
+ calcToAmount,
+ calcTotalGasFee,
+ calcRelayerFee,
+ 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',
+ 2521.73,
+ { amount: '1.009', fiat: '2544.42557' },
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '2543140000',
+ 0.999781,
+ { amount: '2543.14', fiat: '2542.58305234' },
+ ],
+ [
+ 'erc20 with null exchange rates',
+ ERC20_TOKEN,
+ '2543140000',
+ null,
+ { amount: '2543.14', fiat: undefined },
+ ],
+ ])(
+ 'calcToAmount: toToken is %s',
+ (
+ _: string,
+ destAsset: { decimals: number; address: string },
+ destTokenAmount: string,
+ toTokenExchangeRate: number,
+ { amount, fiat }: { amount: string; fiat: string },
+ ) => {
+ const result = calcToAmount(
+ {
+ destAsset,
+ destTokenAmount,
+ } as never,
+ toTokenExchangeRate,
+ );
+ expect(result.amount?.toString()).toStrictEqual(amount);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1009000000000000000',
+ 2515.02,
+ {
+ amount: '1.143217728',
+ fiat: '2875.21545027456',
+ },
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ 0.999781,
+ { amount: '100.512', fiat: '100.489987872' },
+ ],
+ [
+ 'erc20 with null exchange rates',
+ ERC20_TOKEN,
+ '2543140000',
+ null,
+ { amount: '2543.652', fiat: undefined },
+ ],
+ ])(
+ 'calcSentAmount: fromToken is %s',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ fromTokenExchangeRate: number,
+ { amount, fiat }: { amount: string; fiat: string },
+ ) => {
+ const result = calcSentAmount(
+ {
+ srcAsset,
+ srcTokenAmount,
+ feeData: {
+ metabridge: {
+ amount: Math.pow(8 * 10, srcAsset.decimals / 2),
+ },
+ },
+ } as never,
+ fromTokenExchangeRate,
+ );
+ expect(result.amount?.toString()).toStrictEqual(amount);
+ expect(result.fiat?.toString()).toStrictEqual(fiat);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de0b6b3a7640000',
+ { amount: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ undefined,
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { amount: '2.2351800712e-7', fiat: '0.0005626887014840304' },
+ undefined,
+ ],
+ [
+ 'erc20 with approval',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { amount: '4.4703601424e-7', fiat: '0.0011253774029680608' },
+ 1092677,
+ ],
+ [
+ 'erc20 with relayer fee',
+ ERC20_TOKEN,
+ '100000000',
+ '0x0de0b6b3a7640000',
+ { amount: '1.00000022351800712', fiat: '2517.4205626887014840304' },
+ undefined,
+ ],
+ [
+ 'native with relayer fee',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de1b6b3a7640000',
+ { amount: '0.000281698494717776', fiat: '0.70915342457242365792' },
+ undefined,
+ ],
+ ])(
+ 'calcTotalGasFee and calcRelayerFee: fromToken is %s',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ value: string,
+ { amount, fiat }: { amount: string; fiat: string },
+ approvalGasLimit?: number,
+ ) => {
+ const feeData = { metabridge: { amount: 0 } };
+ 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);
+ },
+ );
+
+ // @ts-expect-error This is missing from the Mocha type definitions
+ it.each([
+ [
+ 'native',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de0b6b3a7640000',
+ { amount: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ undefined,
+ ],
+ [
+ 'erc20',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { amount: '0.000002832228395508', fiat: '0.00712990840741974936' },
+ undefined,
+ ],
+ [
+ 'erc20 with approval',
+ ERC20_TOKEN,
+ '100000000',
+ '0x00',
+ { amount: '0.000003055746402628', fiat: '0.00769259710890377976' },
+ 1092677,
+ ],
+ [
+ 'erc20 with relayer fee',
+ ERC20_TOKEN,
+ '100000000',
+ '0x0de0b6b3a7640000',
+ { amount: '1.000002832228395508', fiat: '2517.42712990840741974936' },
+ undefined,
+ ],
+ [
+ 'native with relayer fee',
+ NATIVE_TOKEN,
+ '1000000000000000000',
+ '0x0de1b6b3a7640000',
+ { amount: '0.000284307205106164', fiat: '0.71572064427835937688' },
+ undefined,
+ ],
+ ])(
+ 'calcTotalGasFee and calcRelayerFee: fromToken is %s with l1GasFee',
+ (
+ _: string,
+ srcAsset: { decimals: number; address: string },
+ srcTokenAmount: string,
+ value: string,
+ { amount, fiat }: { amount: string; fiat: string },
+ approvalGasLimit?: number,
+ ) => {
+ const feeData = { metabridge: { amount: 0 } };
+ 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);
+ },
+ );
+
+ // @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..2fff7e9c1b18 100644
--- a/ui/pages/bridge/utils/quote.ts
+++ b/ui/pages/bridge/utils/quote.ts
@@ -1,5 +1,17 @@
+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, L1GasFees } from '../types';
+import {
+ hexToDecimal,
+ sumDecimals,
+} from '../../../../shared/modules/conversion.utils';
+import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
+import { Numeric } from '../../../../shared/modules/Numeric';
+import { EtherDenomination } from '../../../../shared/constants/common';
+import { DEFAULT_PRECISION } from '../../../hooks/useCurrencyDisplay';
+
+export const isNativeAddress = (address?: string) => address === zeroAddress();
export const isValidQuoteRequest = (
partialRequest: Partial,
@@ -33,27 +45,150 @@ 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 {
+ amount: normalizedDestAmount,
+ fiat: exchangeRate
+ ? normalizedDestAmount.mul(exchangeRate.toString())
+ : null,
+ };
+};
- 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}`;
+export const calcSentAmount = (
+ { srcTokenAmount, srcAsset, feeData }: Quote,
+ exchangeRate: number | null,
+) => {
+ const normalizedSentAmount = calcTokenAmount(
+ new BigNumber(srcTokenAmount).plus(feeData.metabridge.amount),
+ srcAsset.decimals,
+ );
+ return {
+ amount: normalizedSentAmount,
+ fiat: exchangeRate
+ ? normalizedSentAmount.mul(exchangeRate.toString())
+ : null,
+ };
+};
+export 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 {
- etaInMinutes,
- totalFees: {
- amount: '0.01 ETH', // TODO implement gas + relayer fee
- fiat: '$0.01',
- },
- quoteRate,
+ amount: relayerFeeInNative,
+ fiat: nativeExchangeRate
+ ? relayerFeeInNative.mul(nativeExchangeRate.toString())
+ : null,
};
};
+
+export const calcTotalGasFee = (
+ bridgeQuote: QuoteResponse & L1GasFees,
+ estimatedBaseFeeInDecGwei: string,
+ maxPriorityFeePerGasInDecGwei: string,
+ nativeExchangeRate?: number,
+) => {
+ const { approval, trade, l1GasFeesInHexWei } = bridgeQuote;
+ const totalGasLimitInDec = sumDecimals(
+ trade.gasLimit?.toString() ?? '0',
+ approval?.gasLimit?.toString() ?? '0',
+ );
+ const feePerGasInDecGwei = sumDecimals(
+ estimatedBaseFeeInDecGwei,
+ maxPriorityFeePerGasInDecGwei,
+ );
+
+ 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(),
+ );
+ const gasFeesInUSD = nativeExchangeRate
+ ? gasFeesInDecEth.times(nativeExchangeRate.toString())
+ : null;
+
+ return {
+ amount: gasFeesInDecEth,
+ fiat: gasFeesInUSD,
+ };
+};
+
+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
+ ? sentAmountInFiat.minus(adjustedReturnInFiat)
+ : null,
+});
+
+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,
+ precision: number = DEFAULT_PRECISION,
+) => {
+ if (!amount) {
+ return undefined;
+ }
+ if (precision === 0) {
+ if (amount.lt(0.01)) {
+ return `<${formatCurrency('0', currency, precision)}`;
+ }
+ if (amount.lt(1)) {
+ return formatCurrency(amount.toString(), currency, 2);
+ }
+ }
+ return formatCurrency(amount.toString(), currency, precision);
+};
From 23ec9475e46a29710c6969cc4a2c9f8d101d3fbd Mon Sep 17 00:00:00 2001
From: David Walsh
Date: Fri, 22 Nov 2024 15:59:51 -0600
Subject: [PATCH 05/40] =?UTF-8?q?chore:=20PortfolioView=E2=84=A2:=20Design?=
=?UTF-8?q?=20Review=20Cleanup:=20Networks,=20sort,=20&=20Menu=20(#28663)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Improves various design review aspects pointed out by @amandaye0h
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28663?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../asset-list-control-bar.tsx | 23 +++++++++++-----
.../asset-list-control-bar/index.scss | 7 +----
.../import-control/import-control.tsx | 1 +
.../asset-list/network-filter/index.scss | 27 -------------------
.../network-filter/network-filter.tsx | 5 ++--
.../assets/asset-list/sort-control/index.scss | 7 ++++-
.../asset-list/sort-control/sort-control.tsx | 16 ++++++++---
7 files changed, 40 insertions(+), 46 deletions(-)
delete mode 100644 ui/components/app/assets/asset-list/network-filter/index.scss
diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx
index dede9004d29e..8e6abb940d1c 100644
--- a/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx
+++ b/ui/components/app/assets/asset-list/asset-list-control-bar/asset-list-control-bar.tsx
@@ -6,7 +6,9 @@ import {
Box,
ButtonBase,
ButtonBaseSize,
+ Icon,
IconName,
+ IconSize,
Popover,
PopoverPosition,
} from '../../../../component-library';
@@ -198,7 +200,8 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
className="asset-list-control-bar__button"
onClick={toggleTokenSortPopover}
size={ButtonBaseSize.Sm}
- endIconName={IconName.SwapVertical}
+ startIconName={IconName.Filter}
+ startIconProps={{ marginInlineEnd: 0 }}
backgroundColor={
isTokenSortPopoverOpen
? BackgroundColor.backgroundPressed
@@ -221,13 +224,13 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
isOpen={isNetworkFilterPopoverOpen}
position={PopoverPosition.BottomStart}
referenceElement={popoverRef.current}
- matchWidth={!isFullScreen}
+ matchWidth={false}
style={{
zIndex: 10,
display: 'flex',
flexDirection: 'column',
padding: 0,
- minWidth: isFullScreen ? '325px' : '',
+ minWidth: isFullScreen ? '250px' : '',
}}
>
@@ -237,13 +240,13 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
isOpen={isTokenSortPopoverOpen}
position={PopoverPosition.BottomEnd}
referenceElement={popoverRef.current}
- matchWidth={!isFullScreen}
+ matchWidth={false}
style={{
zIndex: 10,
display: 'flex',
flexDirection: 'column',
padding: 0,
- minWidth: isFullScreen ? '325px' : '',
+ minWidth: isFullScreen ? '250px' : '',
}}
>
@@ -254,19 +257,25 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
isOpen={isImportTokensPopoverOpen}
position={PopoverPosition.BottomEnd}
referenceElement={popoverRef.current}
- matchWidth={!isFullScreen}
+ matchWidth={false}
style={{
zIndex: 10,
display: 'flex',
flexDirection: 'column',
padding: 0,
- minWidth: isFullScreen ? '325px' : '',
+ minWidth: isFullScreen ? '158px' : '',
}}
>
+
{t('importTokensCamelCase')}
+
{t('refreshList')}
diff --git a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss
index b133586371c3..21cdbe2e83e1 100644
--- a/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss
+++ b/ui/components/app/assets/asset-list/asset-list-control-bar/index.scss
@@ -8,12 +8,7 @@
min-width: auto;
border-radius: 8px;
padding: 0 8px !important;
- gap: 5px;
- text-transform: lowercase;
-
- span::first-letter {
- text-transform: uppercase;
- }
+ gap: 4px;
}
&__buttons {
diff --git a/ui/components/app/assets/asset-list/import-control/import-control.tsx b/ui/components/app/assets/asset-list/import-control/import-control.tsx
index d3a9bfd9ccb7..6ac4a6b1fce1 100644
--- a/ui/components/app/assets/asset-list/import-control/import-control.tsx
+++ b/ui/components/app/assets/asset-list/import-control/import-control.tsx
@@ -33,6 +33,7 @@ const AssetListControlBar = ({
disabled={!shouldShowTokensLinks}
size={ButtonBaseSize.Sm}
startIconName={IconName.MoreVertical}
+ startIconProps={{ marginInlineEnd: 0 }}
backgroundColor={BackgroundColor.backgroundDefault}
color={TextColor.textDefault}
onClick={onClick}
diff --git a/ui/components/app/assets/asset-list/network-filter/index.scss b/ui/components/app/assets/asset-list/network-filter/index.scss
deleted file mode 100644
index 76e61c1025ae..000000000000
--- a/ui/components/app/assets/asset-list/network-filter/index.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-.selectable-list-item-wrapper {
- position: relative;
-}
-
-.selectable-list-item {
- cursor: pointer;
- padding: 16px;
-
- &--selected {
- background: var(--color-primary-muted);
- }
-
- &:not(.selectable-list-item--selected) {
- &:hover,
- &:focus-within {
- background: var(--color-background-default-hover);
- }
- }
-
- &__selected-indicator {
- width: 4px;
- height: calc(100% - 8px);
- position: absolute;
- top: 4px;
- left: 4px;
- }
-}
diff --git a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx
index 4e9aa14eea25..d032712be9c1 100644
--- a/ui/components/app/assets/asset-list/network-filter/network-filter.tsx
+++ b/ui/components/app/assets/asset-list/network-filter/network-filter.tsx
@@ -15,6 +15,7 @@ import { SelectableListItem } from '../sort-control/sort-control';
import { Text } from '../../../../component-library/text/text';
import {
AlignItems,
+ BlockSize,
Display,
JustifyContent,
TextColor,
@@ -108,6 +109,7 @@ const NetworkFilter = ({ handleClose }: SortControlProps) => {
{
color={TextColor.textAlternative}
data-testid="network-filter-all__total"
>
- {/* TODO: Should query cross chain account balance */}
-
{
display={Display.Flex}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.center}
+ width={BlockSize.Full}
>
{
return (
-
{children}
-
+
{isSelected && (
Date: Sun, 24 Nov 2024 11:47:23 -0500
Subject: [PATCH 06/40] test: Adding unit test for setupPhishingCommunication
and setUpCookieHandlerCommunication (#27736)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adding unit test for setupPhishingCommunication and
setUpCookieHandlerCommunication.
## **Description**
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/27736?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/27119
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/scripts/metamask-controller.test.js | 124 ++++++++++++++++++++++++
1 file changed, 124 insertions(+)
diff --git a/app/scripts/metamask-controller.test.js b/app/scripts/metamask-controller.test.js
index 0cd4fba34589..880df69aa00f 100644
--- a/app/scripts/metamask-controller.test.js
+++ b/app/scripts/metamask-controller.test.js
@@ -45,6 +45,7 @@ import {
} from './lib/accounts/BalancesController';
import { BalancesTracker as MultichainBalancesTracker } from './lib/accounts/BalancesTracker';
import { deferredPromise } from './lib/util';
+import { METAMASK_COOKIE_HANDLER } from './constants/stream';
import MetaMaskController, {
ONE_KEY_VIA_TREZOR_MINOR_VERSION,
} from './metamask-controller';
@@ -1273,6 +1274,129 @@ describe('MetaMaskController', () => {
expect(mockKeyring.destroy).toHaveBeenCalledTimes(1);
});
});
+ describe('#setupPhishingCommunication', () => {
+ beforeEach(() => {
+ jest.spyOn(metamaskController, 'safelistPhishingDomain');
+ jest.spyOn(metamaskController, 'backToSafetyPhishingWarning');
+ metamaskController.preferencesController.setUsePhishDetect(true);
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ it('creates a phishing stream with safelistPhishingDomain and backToSafetyPhishingWarning handler', async () => {
+ const safelistPhishingDomainRequest = {
+ name: 'metamask-phishing-safelist',
+ data: {
+ id: 1,
+ method: 'safelistPhishingDomain',
+ params: ['mockHostname'],
+ },
+ };
+ const backToSafetyPhishingWarningRequest = {
+ name: 'metamask-phishing-safelist',
+ data: { id: 2, method: 'backToSafetyPhishingWarning', params: [] },
+ };
+
+ const { promise, resolve } = deferredPromise();
+ const { promise: promiseStream, resolve: resolveStream } =
+ deferredPromise();
+ const streamTest = createThroughStream((chunk, _, cb) => {
+ if (chunk.name !== 'metamask-phishing-safelist') {
+ cb();
+ return;
+ }
+ resolve();
+ cb(null, chunk);
+ });
+
+ metamaskController.setupPhishingCommunication({
+ connectionStream: streamTest,
+ });
+
+ streamTest.write(safelistPhishingDomainRequest, null, () => {
+ expect(
+ metamaskController.safelistPhishingDomain,
+ ).toHaveBeenCalledWith('mockHostname');
+ });
+ streamTest.write(backToSafetyPhishingWarningRequest, null, () => {
+ expect(
+ metamaskController.backToSafetyPhishingWarning,
+ ).toHaveBeenCalled();
+ resolveStream();
+ });
+
+ await promise;
+ streamTest.end();
+ await promiseStream;
+ });
+ });
+
+ describe('#setUpCookieHandlerCommunication', () => {
+ let localMetaMaskController;
+ beforeEach(() => {
+ localMetaMaskController = new MetaMaskController({
+ showUserConfirmation: noop,
+ encryptor: mockEncryptor,
+ initState: {
+ ...cloneDeep(firstTimeState),
+ MetaMetricsController: {
+ metaMetricsId: 'MOCK_METRICS_ID',
+ participateInMetaMetrics: true,
+ dataCollectionForMarketing: true,
+ },
+ },
+ initLangCode: 'en_US',
+ platform: {
+ showTransactionNotification: () => undefined,
+ getVersion: () => 'foo',
+ },
+ browser: browserPolyfillMock,
+ infuraProjectId: 'foo',
+ isFirstMetaMaskControllerSetup: true,
+ });
+ jest.spyOn(localMetaMaskController, 'getCookieFromMarketingPage');
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+ it('creates a cookie handler communication stream with getCookieFromMarketingPage handler', async () => {
+ const attributionRequest = {
+ name: METAMASK_COOKIE_HANDLER,
+ data: {
+ id: 1,
+ method: 'getCookieFromMarketingPage',
+ params: [{ ga_client_id: 'XYZ.ABC' }],
+ },
+ };
+
+ const { promise, resolve } = deferredPromise();
+ const { promise: promiseStream, resolve: resolveStream } =
+ deferredPromise();
+ const streamTest = createThroughStream((chunk, _, cb) => {
+ if (chunk.name !== METAMASK_COOKIE_HANDLER) {
+ cb();
+ return;
+ }
+ resolve();
+ cb(null, chunk);
+ });
+
+ localMetaMaskController.setUpCookieHandlerCommunication({
+ connectionStream: streamTest,
+ });
+
+ streamTest.write(attributionRequest, null, () => {
+ expect(
+ localMetaMaskController.getCookieFromMarketingPage,
+ ).toHaveBeenCalledWith({ ga_client_id: 'XYZ.ABC' });
+ resolveStream();
+ });
+
+ await promise;
+ streamTest.end();
+ await promiseStream;
+ });
+ });
describe('#setupUntrustedCommunicationEip1193', () => {
const mockTxParams = { from: TEST_ADDRESS };
From ebb492665bf4b2d09a102ce20a448952ff601df6 Mon Sep 17 00:00:00 2001
From: Jyoti Puri
Date: Mon, 25 Nov 2024 16:43:30 +0530
Subject: [PATCH 07/40] fix: add alert when selected account is different from
signing account in confirmation (#28562)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Show warning when signing account in confirmation is different from
currently selected account in MM.
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/28015
## **Manual testing steps**
1. Go to test dapp and connect with an account
2. Switch to a different account
3. Submit a confirmation and check warning next to signing account
## **Screenshots/Recordings**
Signature Request:
Contract Interaction:
Send token:
## **Pre-merge author checklist**
- [X] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
app/_locales/en/messages.json | 6 +
.../info/__snapshots__/info.test.tsx.snap | 375 +++++++++++++++
.../__snapshots__/approve.test.tsx.snap | 75 +++
.../approve-details/approve-details.tsx | 2 +
.../approve/revoke-details/revoke-details.tsx | 2 +
.../base-transaction-info.test.tsx.snap | 75 +++
.../__snapshots__/personal-sign.test.tsx.snap | 150 ++++++
.../info/personal-sign/personal-sign.tsx | 14 +-
.../set-approval-for-all-info.test.tsx.snap | 75 +++
.../sign-in-with-row.test.tsx | 30 ++
.../sign-in-with-row/sign-in-with-row.tsx | 34 ++
.../transaction-details.test.tsx.snap | 75 +++
.../transaction-details.tsx | 2 +
.../transaction-flow-section.test.tsx.snap | 166 ++++++-
.../transaction-flow-section.test.tsx | 11 +
.../transaction-flow-section.tsx | 48 +-
.../__snapshots__/typed-sign-v1.test.tsx.snap | 75 +++
.../info/typed-sign-v1/typed-sign-v1.tsx | 2 +
.../__snapshots__/typed-sign.test.tsx.snap | 375 +++++++++++++++
.../confirm/info/typed-sign/typed-sign.tsx | 2 +
.../__snapshots__/confirm.test.tsx.snap | 450 ++++++++++++++++++
.../alerts/useSelectedAccountAlerts.test.ts | 79 +++
.../hooks/alerts/useSelectedAccountAlerts.ts | 41 ++
.../hooks/useConfirmationAlerts.ts | 5 +
24 files changed, 2123 insertions(+), 46 deletions(-)
create mode 100644 ui/pages/confirmations/components/confirm/info/shared/sign-in-with-row/sign-in-with-row.test.tsx
create mode 100644 ui/pages/confirmations/components/confirm/info/shared/sign-in-with-row/sign-in-with-row.tsx
create mode 100644 ui/pages/confirmations/hooks/alerts/useSelectedAccountAlerts.test.ts
create mode 100644 ui/pages/confirmations/hooks/alerts/useSelectedAccountAlerts.ts
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 2d603ddc5156..62c7b4542cf5 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -488,6 +488,9 @@
"alertReasonWrongAccount": {
"message": "Wrong account"
},
+ "alertSelectedAccountWarning": {
+ "message": "This request is for a different account than the one selected in your wallet. To use another account, connect it to the site."
+ },
"alerts": {
"message": "Alerts"
},
@@ -4863,6 +4866,9 @@
"selectType": {
"message": "Select Type"
},
+ "selectedAccountMismatch": {
+ "message": "Different account selected"
+ },
"selectingAllWillAllow": {
"message": "Selecting all will allow this site to view all of your current accounts. Make sure you trust this site."
},
diff --git a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap
index 86779c10cad6..6d48461f1c89 100644
--- a/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/__snapshots__/info.test.tsx.snap
@@ -50,6 +50,81 @@ exports[`Info renders info section for approve request 1`] = `
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
renders component for approve request 1`] = `
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
{showAdvancedDetails && (
<>
diff --git a/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx b/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx
index 49bf5e7724e1..09b2e6a809f3 100644
--- a/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx
+++ b/ui/pages/confirmations/components/confirm/info/approve/revoke-details/revoke-details.tsx
@@ -1,11 +1,13 @@
import React from 'react';
import { ConfirmInfoSection } from '../../../../../../../components/app/confirm/info/row/section';
import { OriginRow } from '../../shared/transaction-details/transaction-details';
+import { SigningInWithRow } from '../../shared/sign-in-with-row/sign-in-with-row';
export const RevokeDetails = () => {
return (
+
);
};
diff --git a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap
index d7054050f710..10506183561d 100644
--- a/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/base-transaction-info/__snapshots__/base-transaction-info.test.tsx.snap
@@ -233,6 +233,81 @@ exports[` renders component for contract interaction requ
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
+
+
{
@@ -51,9 +51,7 @@ const PersonalSignInfo: React.FC = () => {
return null;
}
- const { from } = currentConfirmation.msgParams;
const isSIWE = isSIWESignatureRequest(currentConfirmation);
- const chainId = currentConfirmation.chainId as string;
const messageText = sanitizeString(
hexToText(currentConfirmation.msgParams?.data),
);
@@ -138,15 +136,7 @@ const PersonalSignInfo: React.FC = () => {
>
- {isSIWE && (
-
-
-
- )}
+
{isSIWE ? (
diff --git a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap
index 42efa15a2a5b..5da63fd56680 100644
--- a/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/set-approval-for-all-info/__snapshots__/set-approval-for-all-info.test.tsx.snap
@@ -154,6 +154,81 @@ exports[` renders component for approve request 1`] = `
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
({
+ useAlertMetrics: jest.fn(() => ({
+ trackAlertMetrics: jest.fn(),
+ })),
+ }),
+);
+
+describe('', () => {
+ const middleware = [thunk];
+
+ it('renders component for transaction details', () => {
+ const state = getMockContractInteractionConfirmState();
+ const mockStore = configureMockStore(middleware)(state);
+ const { getByText } = renderWithConfirmContextProvider(
+ ,
+ mockStore,
+ );
+ expect(getByText('Signing in with')).toBeInTheDocument();
+ expect(getByText('0x2e0D7...5d09B')).toBeInTheDocument();
+ });
+});
diff --git a/ui/pages/confirmations/components/confirm/info/shared/sign-in-with-row/sign-in-with-row.tsx b/ui/pages/confirmations/components/confirm/info/shared/sign-in-with-row/sign-in-with-row.tsx
new file mode 100644
index 000000000000..7b20cbc08062
--- /dev/null
+++ b/ui/pages/confirmations/components/confirm/info/shared/sign-in-with-row/sign-in-with-row.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { TransactionMeta } from '@metamask/transaction-controller';
+
+import { ConfirmInfoRowAddress } from '../../../../../../../components/app/confirm/info/row';
+import { ConfirmInfoAlertRow } from '../../../../../../../components/app/confirm/info/row/alert-row/alert-row';
+import { RowAlertKey } from '../../../../../../../components/app/confirm/info/row/constants';
+import { useI18nContext } from '../../../../../../../hooks/useI18nContext';
+import { useConfirmContext } from '../../../../../context/confirm';
+import { SignatureRequestType } from '../../../../../types/confirm';
+
+export const SigningInWithRow = () => {
+ const t = useI18nContext();
+
+ const { currentConfirmation } = useConfirmContext();
+
+ const chainId = currentConfirmation?.chainId as string;
+ const from =
+ (currentConfirmation as TransactionMeta)?.txParams?.from ??
+ (currentConfirmation as SignatureRequestType)?.msgParams?.from;
+
+ if (!from) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/__snapshots__/transaction-details.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/__snapshots__/transaction-details.test.tsx.snap
index f5183e21206c..9e9b9b23eca5 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/__snapshots__/transaction-details.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/__snapshots__/transaction-details.test.tsx.snap
@@ -151,6 +151,81 @@ exports[` renders component for transaction details 1`] =
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
+
`;
diff --git a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
index 89c2da783a4f..79cea5963c45 100644
--- a/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
+++ b/ui/pages/confirmations/components/confirm/info/shared/transaction-details/transaction-details.tsx
@@ -20,6 +20,7 @@ import { ConfirmInfoRowCurrency } from '../../../../../../../components/app/conf
import { PRIMARY } from '../../../../../../../helpers/constants/common';
import { useUserPreferencedCurrency } from '../../../../../../../hooks/useUserPreferencedCurrency';
import { HEX_ZERO } from '../constants';
+import { SigningInWithRow } from '../sign-in-with-row/sign-in-with-row';
export const OriginRow = () => {
const t = useI18nContext();
@@ -156,6 +157,7 @@ export const TransactionDetails = () => {
{showAdvancedDetails && }
+
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
index 23cddb2b59b2..01614eec26cd 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/__snapshots__/transaction-flow-section.test.tsx.snap
@@ -7,23 +7,85 @@ exports[` renders correctly 1`] = `
data-testid="confirmation__transaction-flow"
>
-
-
- 0x2e0D7...5d09B
-
+
+ From
+
+
+
+
+
+
+
+
+ 0x2e0D7...5d09B
+
+
+
renders correctly 1`] = `
style="mask-image: url('./images/icons/arrow-right.svg');"
/>
+
-
-
- 0x6B175...71d0F
-
+
+
+
+ 0x6B175...71d0F
+
+
+
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
index c23d3645abd3..bdc6ed30678a 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.test.tsx
@@ -11,6 +11,17 @@ jest.mock('../hooks/useDecodedTransactionData', () => ({
useDecodedTransactionData: jest.fn(),
}));
+jest.mock(
+ '../../../../../../components/app/alert-system/contexts/alertMetricsContext.tsx',
+ () => ({
+ useAlertMetrics: jest.fn(() => ({
+ trackInlineAlertClicked: jest.fn(),
+ trackAlertRender: jest.fn(),
+ trackAlertActionClicked: jest.fn(),
+ })),
+ }),
+);
+
describe('', () => {
const useDecodedTransactionDataMock = jest.fn().mockImplementation(() => ({
pending: false,
diff --git a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
index f5a9a46acbb2..d868611be4cc 100644
--- a/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
+++ b/ui/pages/confirmations/components/confirm/info/token-transfer/transaction-flow-section.tsx
@@ -1,11 +1,9 @@
-import { NameType } from '@metamask/name-controller';
import {
TransactionMeta,
TransactionType,
} from '@metamask/transaction-controller';
import React from 'react';
import { ConfirmInfoSection } from '../../../../../../components/app/confirm/info/row/section';
-import Name from '../../../../../../components/app/name';
import {
Box,
Icon,
@@ -19,10 +17,18 @@ import {
IconColor,
JustifyContent,
} from '../../../../../../helpers/constants/design-system';
+import {
+ ConfirmInfoRow,
+ ConfirmInfoRowAddress,
+} from '../../../../../../components/app/confirm/info/row';
+import { RowAlertKey } from '../../../../../../components/app/confirm/info/row/constants';
+import { ConfirmInfoAlertRow } from '../../../../../../components/app/confirm/info/row/alert-row/alert-row';
+import { useI18nContext } from '../../../../../../hooks/useI18nContext';
import { useConfirmContext } from '../../../../context/confirm';
import { useDecodedTransactionData } from '../hooks/useDecodedTransactionData';
export const TransactionFlowSection = () => {
+ const t = useI18nContext();
const { currentConfirmation: transactionMeta } =
useConfirmContext();
@@ -50,24 +56,40 @@ export const TransactionFlowSection = () => {
flexDirection={FlexDirection.Row}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.center}
- padding={3}
>
-
+
+
+
+
+
+
{recipientAddress && (
-
+
+
+
+
+
)}
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap
index e80d317f574b..8713c5d303ca 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign-v1/__snapshots__/typed-sign-v1.test.tsx.snap
@@ -47,6 +47,81 @@ exports[`TypedSignInfo correctly renders typed sign data request 1`] = `
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
{
const t = useI18nContext();
@@ -42,6 +43,7 @@ const TypedSignV1Info: React.FC = () => {
url={currentConfirmation.msgParams?.origin ?? ''}
/>
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
{
@@ -81,6 +82,7 @@ const TypedSignInfo: React.FC = () => {
/>
)}
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
+
+
+
+
+
+
+ 0x935E7...05477
+
+
+
+
{
+ it('returns an empty array when there is no current confirmation', () => {
+ const { result } = renderHookWithConfirmContextProvider(
+ () => useSelectedAccountAlerts(),
+ mockState,
+ );
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns an alert for signature if signing account is different from selected account', () => {
+ const { result } = renderHookWithConfirmContextProvider(
+ () => useSelectedAccountAlerts(),
+ getMockPersonalSignConfirmStateForRequest({
+ ...unapprovedPersonalSignMsg,
+ msgParams: {
+ ...unapprovedPersonalSignMsg.msgParams,
+ from: '0x0',
+ },
+ } as SignatureRequestType),
+ );
+ expect(result.current).toEqual(expectedAlert);
+ });
+
+ it('does not returns an alert for signature if signing account is same as selected account', () => {
+ const { result } = renderHookWithConfirmContextProvider(
+ () => useSelectedAccountAlerts(),
+ getMockPersonalSignConfirmStateForRequest(
+ unapprovedPersonalSignMsg as SignatureRequestType,
+ ),
+ );
+ expect(result.current).toEqual([]);
+ });
+
+ it('returns an alert for transaction if signing account is different from selected account', () => {
+ const contractInteraction = genUnapprovedContractInteractionConfirmation({
+ address: '0x0',
+ });
+ const { result } = renderHookWithConfirmContextProvider(
+ () => useSelectedAccountAlerts(),
+ getMockConfirmStateForTransaction(contractInteraction as TransactionMeta),
+ );
+ expect(result.current).toEqual(expectedAlert);
+ });
+
+ it('does not returns an alert for transaction if signing account is same as selected account', () => {
+ const contractInteraction = genUnapprovedContractInteractionConfirmation({
+ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
+ });
+ const { result } = renderHookWithConfirmContextProvider(
+ () => useSelectedAccountAlerts(),
+ getMockConfirmStateForTransaction(contractInteraction as TransactionMeta),
+ );
+ expect(result.current).toEqual([]);
+ });
+});
diff --git a/ui/pages/confirmations/hooks/alerts/useSelectedAccountAlerts.ts b/ui/pages/confirmations/hooks/alerts/useSelectedAccountAlerts.ts
new file mode 100644
index 000000000000..6e4be13b1ae5
--- /dev/null
+++ b/ui/pages/confirmations/hooks/alerts/useSelectedAccountAlerts.ts
@@ -0,0 +1,41 @@
+import { TransactionMeta } from '@metamask/transaction-controller';
+import { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+
+import { Alert } from '../../../../ducks/confirm-alerts/confirm-alerts';
+import { RowAlertKey } from '../../../../components/app/confirm/info/row/constants';
+import { Severity } from '../../../../helpers/constants/design-system';
+import { getSelectedAccount } from '../../../../selectors';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { SignatureRequestType } from '../../types/confirm';
+import { useConfirmContext } from '../../context/confirm';
+
+export const useSelectedAccountAlerts = (): Alert[] => {
+ const t = useI18nContext();
+
+ const { currentConfirmation } = useConfirmContext();
+ const selectedAccount = useSelector(getSelectedAccount);
+
+ const fromAccount =
+ (currentConfirmation as SignatureRequestType)?.msgParams?.from ??
+ (currentConfirmation as TransactionMeta)?.txParams?.from;
+ const confirmationAccountSameAsSelectedAccount =
+ !fromAccount ||
+ fromAccount.toLowerCase() === selectedAccount?.address?.toLowerCase();
+
+ return useMemo
((): Alert[] => {
+ if (confirmationAccountSameAsSelectedAccount) {
+ return [];
+ }
+
+ return [
+ {
+ key: 'selectedAccountWarning',
+ reason: t('selectedAccountMismatch'),
+ field: RowAlertKey.SigningInWith,
+ severity: Severity.Warning,
+ message: t('alertSelectedAccountWarning'),
+ },
+ ];
+ }, [confirmationAccountSameAsSelectedAccount, t]);
+};
diff --git a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts
index c5f77f143cb6..efcb0beacf9e 100644
--- a/ui/pages/confirmations/hooks/useConfirmationAlerts.ts
+++ b/ui/pages/confirmations/hooks/useConfirmationAlerts.ts
@@ -16,6 +16,7 @@ import { useSigningOrSubmittingAlerts } from './alerts/transactions/useSigningOr
///: END:ONLY_INCLUDE_IF
import useConfirmationOriginAlerts from './alerts/useConfirmationOriginAlerts';
import useBlockaidAlerts from './alerts/useBlockaidAlerts';
+import { useSelectedAccountAlerts } from './alerts/useSelectedAccountAlerts';
function useSignatureAlerts(): Alert[] {
const accountMismatchAlerts = useAccountMismatchAlerts();
@@ -40,6 +41,7 @@ function useTransactionAlerts(): Alert[] {
const signingOrSubmittingAlerts = useSigningOrSubmittingAlerts();
///: END:ONLY_INCLUDE_IF
const queuedConfirmationsAlerts = useQueuedConfirmationsAlerts();
+
return useMemo(
() => [
...gasEstimateFailedAlerts,
@@ -77,6 +79,7 @@ export default function useConfirmationAlerts(): Alert[] {
const confirmationOriginAlerts = useConfirmationOriginAlerts();
const signatureAlerts = useSignatureAlerts();
const transactionAlerts = useTransactionAlerts();
+ const selectedAccountAlerts = useSelectedAccountAlerts();
return useMemo(
() => [
@@ -84,12 +87,14 @@ export default function useConfirmationAlerts(): Alert[] {
...confirmationOriginAlerts,
...signatureAlerts,
...transactionAlerts,
+ ...selectedAccountAlerts,
],
[
blockaidAlerts,
confirmationOriginAlerts,
signatureAlerts,
transactionAlerts,
+ selectedAccountAlerts,
],
);
}
From 67b2f5a1f3df8096ed10aa59fc61e3db3122d040 Mon Sep 17 00:00:00 2001
From: Jyoti Puri
Date: Mon, 25 Nov 2024 16:45:08 +0530
Subject: [PATCH 08/40] feat: adding tooltip to signature decoding state
changes (#28430)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
Add tooltip to state change labels.
## **Related issues**
Fixes: https://github.com/MetaMask/MetaMask-planning/issues/3628
## **Manual testing steps**
1. Enable signature decoding locally.
2. Check NFT bidding or listing permit
3. It should show appropriate tooltip
## **Screenshots/Recordings**
## **Pre-merge author checklist**
- [X] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [X] I've completed the PR template to the best of my ability
- [X] I’ve included tests if applicable
- [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [X] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---------
Co-authored-by: MetaMask Bot
---
app/_locales/en/messages.json | 6 ++
.../decoded-simulation.test.tsx | 59 ++++++++++++++++++-
.../decoded-simulation/decoded-simulation.tsx | 44 +++++++++++++-
.../permit-simulation/permit-simulation.tsx | 2 +-
.../value-display/value-display.tsx | 18 ++----
5 files changed, 110 insertions(+), 19 deletions(-)
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 62c7b4542cf5..060ba2a43dca 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -5020,6 +5020,12 @@
"signatureRequestGuidance": {
"message": "Only sign this message if you fully understand the content and trust the requesting site."
},
+ "signature_decoding_bid_nft_tooltip": {
+ "message": "The NFT will be reflected in your wallet, when the bid is accepted."
+ },
+ "signature_decoding_list_nft_tooltip": {
+ "message": "Expect changes only if someone buys your NFTs."
+ },
"signed": {
"message": "Signed"
},
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.test.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.test.tsx
index 690cfb5b5195..86f30472b0e5 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.test.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.test.tsx
@@ -3,12 +3,13 @@ import configureMockStore from 'redux-mock-store';
import {
DecodingData,
DecodingDataChangeType,
+ DecodingDataStateChanges,
} from '@metamask/signature-controller';
import { getMockTypedSignConfirmStateForRequest } from '../../../../../../../../../test/data/confirmations/helper';
import { renderWithConfirmContextProvider } from '../../../../../../../../../test/lib/confirmations/render-helpers';
import { permitSignatureMsg } from '../../../../../../../../../test/data/confirmations/typed_sign';
-import PermitSimulation from './decoded-simulation';
+import PermitSimulation, { getStateChangeToolip } from './decoded-simulation';
const decodingData: DecodingData = {
stateChanges: [
@@ -22,6 +23,42 @@ const decodingData: DecodingData = {
],
};
+const decodingDataListing: DecodingDataStateChanges = [
+ {
+ assetType: 'NATIVE',
+ changeType: DecodingDataChangeType.Receive,
+ address: '',
+ amount: '900000000000000000',
+ contractAddress: '',
+ },
+ {
+ assetType: 'ERC721',
+ changeType: DecodingDataChangeType.Listing,
+ address: '',
+ amount: '',
+ contractAddress: '0xafd4896984CA60d2feF66136e57f958dCe9482d5',
+ tokenID: '2101',
+ },
+];
+
+const decodingDataBidding: DecodingDataStateChanges = [
+ {
+ assetType: 'ERC721',
+ changeType: DecodingDataChangeType.Receive,
+ address: '',
+ amount: '900000000000000000',
+ contractAddress: '',
+ },
+ {
+ assetType: 'Native',
+ changeType: DecodingDataChangeType.Bidding,
+ address: '',
+ amount: '',
+ contractAddress: '0xafd4896984CA60d2feF66136e57f958dCe9482d5',
+ tokenID: '2101',
+ },
+];
+
describe('DecodedSimulation', () => {
it('renders component correctly', async () => {
const state = getMockTypedSignConfirmStateForRequest({
@@ -38,4 +75,24 @@ describe('DecodedSimulation', () => {
expect(container).toMatchSnapshot();
});
+
+ describe('getStateChangeToolip', () => {
+ it('return correct tooltip when permit is for listing NFT', async () => {
+ const tooltip = getStateChangeToolip(
+ decodingDataListing,
+ decodingDataListing?.[0],
+ (str: string) => str,
+ );
+ expect(tooltip).toBe('signature_decoding_list_nft_tooltip');
+ });
+ });
+
+ it('return correct tooltip when permit is for bidding NFT', async () => {
+ const tooltip = getStateChangeToolip(
+ decodingDataBidding,
+ decodingDataBidding?.[0],
+ (str: string) => str,
+ );
+ expect(tooltip).toBe('signature_decoding_bid_nft_tooltip');
+ });
});
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.tsx
index 3798776ca85d..cf774483ee6c 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/decoded-simulation/decoded-simulation.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import {
DecodingDataChangeType,
DecodingDataStateChange,
+ DecodingDataStateChanges,
} from '@metamask/signature-controller';
import { Hex } from '@metamask/utils';
@@ -11,8 +12,35 @@ import { useI18nContext } from '../../../../../../../../hooks/useI18nContext';
import { SignatureRequestType } from '../../../../../../types/confirm';
import { useConfirmContext } from '../../../../../../context/confirm';
import StaticSimulation from '../../../shared/static-simulation/static-simulation';
-import NativeValueDisplay from '../native-value-display/native-value-display';
import TokenValueDisplay from '../value-display/value-display';
+import NativeValueDisplay from '../native-value-display/native-value-display';
+
+export const getStateChangeToolip = (
+ stateChangeList: DecodingDataStateChanges | null,
+ stateChange: DecodingDataStateChange,
+ t: ReturnType,
+): string | undefined => {
+ if (stateChange.changeType === DecodingDataChangeType.Receive) {
+ if (
+ stateChangeList?.some(
+ (change) =>
+ change.changeType === DecodingDataChangeType.Listing &&
+ change.assetType === TokenStandard.ERC721,
+ )
+ ) {
+ return t('signature_decoding_list_nft_tooltip');
+ }
+ if (
+ stateChange.assetType === TokenStandard.ERC721 &&
+ stateChangeList?.some(
+ (change) => change.changeType === DecodingDataChangeType.Bidding,
+ )
+ ) {
+ return t('signature_decoding_bid_nft_tooltip');
+ }
+ }
+ return undefined;
+};
const getStateChangeLabelMap = (
t: ReturnType,
@@ -28,17 +56,23 @@ const getStateChangeLabelMap = (
}[changeType]);
const StateChangeRow = ({
+ stateChangeList,
stateChange,
chainId,
}: {
+ stateChangeList: DecodingDataStateChanges | null;
stateChange: DecodingDataStateChange;
chainId: Hex;
}) => {
const t = useI18nContext();
const { assetType, changeType, amount, contractAddress, tokenID } =
stateChange;
+ const tooltip = getStateChangeToolip(stateChangeList, stateChange, t);
return (
-
+
{(assetType === TokenStandard.ERC20 ||
assetType === TokenStandard.ERC721) && (
= () => {
const stateChangeFragment = (decodingData?.stateChanges ?? []).map(
(change: DecodingDataStateChange) => (
-
+
),
);
diff --git a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
index 0b4d9eed22d4..86055425fa46 100644
--- a/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
+++ b/ui/pages/confirmations/components/confirm/info/typed-sign/permit-simulation/permit-simulation.tsx
@@ -11,7 +11,7 @@ const PermitSimulation: React.FC
diff --git a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
index da6f598dfa5e..3520a1b64a13 100644
--- a/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
+++ b/ui/components/app/name/name-details/__snapshots__/name-details.test.tsx.snap
@@ -753,7 +753,7 @@ exports[`NameDetails renders with recognized name 1`] = `
- iZUMi Bond USD
+ iZUMi Bond U...
diff --git a/ui/components/app/name/name.tsx b/ui/components/app/name/name.tsx
index 75f2a2f79c15..4871cc481f0c 100644
--- a/ui/components/app/name/name.tsx
+++ b/ui/components/app/name/name.tsx
@@ -103,9 +103,10 @@ const Name = memo(
}, [setModalOpen]);
const formattedValue = formatValue(value, type);
+ const MAX_PET_NAME_LENGTH = 12;
const formattedName = shortenString(name || undefined, {
- truncatedCharLimit: 15,
- truncatedStartChars: 15,
+ truncatedCharLimit: MAX_PET_NAME_LENGTH,
+ truncatedStartChars: MAX_PET_NAME_LENGTH,
truncatedEndChars: 0,
skipCharacterInEnd: true,
});
From b7fa3fa7539c43409a34f209d9347e553748ed16 Mon Sep 17 00:00:00 2001
From: Pedro Figueiredo
Date: Mon, 25 Nov 2024 13:25:01 +0000
Subject: [PATCH 12/40] fix: Add default value to custom nonce modal (#28659)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
See original bug ticket.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28659?quickstart=1)
## **Related issues**
Fixes: https://github.com/MetaMask/metamask-extension/issues/28033
## **Manual testing steps**
See original bug ticket.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../__snapshots__/customize-nonce.test.js.snap | 2 +-
.../modals/customize-nonce/customize-nonce.component.js | 9 ++++-----
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap
index 020adaa0c952..4698963a26ec 100644
--- a/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap
+++ b/ui/components/app/modals/customize-nonce/__snapshots__/customize-nonce.test.js.snap
@@ -86,7 +86,7 @@ exports[`Customize Nonce should match snapshot 1`] = `
min="0"
placeholder="1"
type="number"
- value=""
+ value="1"
/>
diff --git a/ui/components/app/modals/customize-nonce/customize-nonce.component.js b/ui/components/app/modals/customize-nonce/customize-nonce.component.js
index 1d87cbd549b1..b6b2f82a096e 100644
--- a/ui/components/app/modals/customize-nonce/customize-nonce.component.js
+++ b/ui/components/app/modals/customize-nonce/customize-nonce.component.js
@@ -27,7 +27,9 @@ const CustomizeNonce = ({
updateCustomNonce,
getNextNonce,
}) => {
- const [customNonce, setCustomNonce] = useState('');
+ const defaultNonce =
+ customNonceValue || (typeof nextNonce === 'number' && nextNonce.toString());
+ const [customNonce, setCustomNonce] = useState(defaultNonce);
const t = useI18nContext();
return (
@@ -107,10 +109,7 @@ const CustomizeNonce = ({
type="number"
data-testid="custom-nonce-input"
min="0"
- placeholder={
- customNonceValue ||
- (typeof nextNonce === 'number' && nextNonce.toString())
- }
+ placeholder={defaultNonce}
onChange={(e) => {
setCustomNonce(e.target.value);
}}
From 592a628b2bd99558d6f86b7f4d80af3701deec41 Mon Sep 17 00:00:00 2001
From: seaona <54408225+seaona@users.noreply.github.com>
Date: Mon, 25 Nov 2024 14:43:53 +0100
Subject: [PATCH 13/40] fix: add missing filter for scheduled job
rerun-from-failed (#28644)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
After [the rerun from failed PR
](https://github.com/MetaMask/metamask-extension/pull/28143) was merged
there was one remaining filter to tweak, related to the scheduled
trigger that will be done in the UI:
- In the Circle ci UI panel, there is no way to tell to trigger a
specific workflow, rather all the config file will be checked, which
means both the test_and_release and the rerun_from_failed workflows
would run. For that, we need to add a filter in the `test_and_release`
flow, so that is not run when we schedule the automatic runs using the
name rerun-from-failed. Unfortunately there is no way to do this from
the UI, so we need this filter in the config file
- I saw, that the rerun-failed workflow was run once the PR was merged,
which shouldn't be the case. I've tweaked that following this example in
circle [ci
docs](https://circleci.com/docs/schedule-pipelines-with-multiple-workflows/#schedule-using-built-in-pipeline-values))
![Screenshot from 2024-11-22
11-06-07](https://github.com/user-attachments/assets/ef82e9e0-4da9-42e6-bb24-6fb97b55034e)
Why this was missed: on the testing for the previous PR, I removed ALL
the filters, to make the rerun-from-failed workflow run and check that
it works (it worked). However, I didn't test leaving the filter of the
`[rerun-from-failed, << pipeline.schedule.name >>]` and checking that it
is not run
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28644?quickstart=1)
## **Related issues**
Fixes:
## **Manual testing steps**
How to test this: instead of removing ALL the filters (like it was done
for testing the previous PR), just remove the filter for develop. This
way, we can see if the job rerun-from-failed is run --> it shouldn't be,
as it's not a scheduled run with that name.
It can be checked
[here](https://app.circleci.com/pipelines/github/MetaMask/metamask-extension?branch=rerun-failed-missing-filter):
see only the test and release is run
![Screenshot from 2024-11-22
11-12-15](https://github.com/user-attachments/assets/d33761b2-beec-4d01-914a-c559815578d5)
For testing the scheduled run, it needs to be done in the UI once this
PR is merged, but if the filter has been proven to work here, it should
then also work there, for when it needs to be run
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.circleci/config.yml | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1b4242570daf..0817f18c36a4 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -103,9 +103,11 @@ workflows:
test_and_release:
when:
not:
- matches:
- pattern: /^l10n_crowdin_action$/
- value: << pipeline.git.branch >>
+ or:
+ - matches:
+ pattern: /^l10n_crowdin_action$/
+ value: << pipeline.git.branch >>
+ - equal: [rerun-from-failed, << pipeline.schedule.name >>]
jobs:
- create_release_pull_request:
<<: *rc_branch_only
@@ -358,8 +360,7 @@ workflows:
rerun-from-failed:
when:
- condition:
- equal: ["<< pipeline.schedule.name >>", "rerun-from-failed"]
+ equal: [rerun-from-failed, << pipeline.schedule.name >>]
jobs:
- prep-deps
- rerun-workflows-from-failed:
From bf1455bec13d6a910f9528a000c026d375e5b13e Mon Sep 17 00:00:00 2001
From: Howard Braham
Date: Mon, 25 Nov 2024 21:03:18 +0700
Subject: [PATCH 14/40] perf: add React.lazy to the Routes (#28172)
## **Description**
> **Note:** I would really appreciate quick reviews here mostly based on
"did I break anything." It's not really worth our time to nit-pick about
the style or the details because every line of this is going to be
changed by me again in the near future. Still to do in later PRs that
are going to completely re-write what I did here:
> - Convert routes.component.js to TypeScript
> - Convert routes.component.js to Hooks
> - Convert routes.component.js to react-router v6
> - Eliminate routes.container.js
>
> All this has to be done in sequential PRs based on top of this one, so
while I wait for reviews I can only work on a non-Routes topic.
This is the first PR to add `React.lazy` to the codebase. The meat of
the changes are in `ui/pages/routes/routes.component.js`.
Every other file changed is just a reaction to those changes. Much of
this is caused by the fact that `React.lazy` only works on default
exports ([see the documentation
here](https://react.dev/reference/react/lazy)), so I had to change a few
named exports to default exports.
I don't think this PR has much of an impact on loading times, but it
sets up further work that should have an impact.
There was one component (`ConfirmTransaction`) that needed some extra
attention to work with `React.lazy`. I had to remove the
`history.replace(mostRecentOverviewPage);`, which I'm pretty sure was an
unnecessary statement, and remove the Unit Test related to it.
**UPDATE:** I changed all of the Integration Tests to convert `getBy...`
to `await findBy...` to wait for the `React.lazy` component to load.
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28172?quickstart=1)
## **Related issues**
Progresses: MetaMask/MetaMask-planning#3302
---
app/scripts/lib/manifestFlags.ts | 9 +
builds.yml | 3 +
jest.integration.config.js | 3 +-
shared/lib/trace.ts | 1 +
.../confirmations/signatures/permit.test.tsx | 36 +-
.../signatures/personalSign.test.tsx | 33 +-
.../transactions/alerts.test.tsx | 14 +-
.../transactions/contract-deployment.test.tsx | 79 +++--
.../contract-interaction.test.tsx | 112 ++++---
.../transactions/erc20-approve.test.tsx | 50 +--
.../transactions/erc721-approve.test.tsx | 46 ++-
.../transactions/increase-allowance.test.tsx | 50 +--
.../set-approval-for-all.test.tsx | 48 ++-
.../notifications-activation.test.tsx | 6 +-
.../notifications-list.test.tsx | 60 ++--
.../notifications-toggle.test.tsx | 12 +-
.../onboarding/wallet-created.test.tsx | 17 +-
ui/components/multichain/pages/index.js | 3 -
.../pages/review-permissions-page/index.js | 2 -
.../review-permissions-page.stories.tsx | 2 +-
.../review-permissions-page.test.tsx | 2 +-
.../review-permissions-page.tsx | 2 +-
ui/helpers/utils/mm-lazy.ts | 86 +++++
.../confirm-transaction.component.js | 4 +-
.../confirm-transaction.test.js | 21 --
.../connect-page/connect-page.tsx | 2 +-
ui/pages/routes/routes.component.js | 315 ++++++++++--------
.../add-contact/add-contact.test.js | 15 +-
28 files changed, 617 insertions(+), 416 deletions(-)
delete mode 100644 ui/components/multichain/pages/index.js
delete mode 100644 ui/components/multichain/pages/review-permissions-page/index.js
create mode 100644 ui/helpers/utils/mm-lazy.ts
diff --git a/app/scripts/lib/manifestFlags.ts b/app/scripts/lib/manifestFlags.ts
index 93925bf63a0c..5804c7391973 100644
--- a/app/scripts/lib/manifestFlags.ts
+++ b/app/scripts/lib/manifestFlags.ts
@@ -11,6 +11,7 @@ export type ManifestFlags = {
};
sentry?: {
tracesSampleRate?: number;
+ lazyLoadSubSampleRate?: number; // multiply by tracesSampleRate to get the actual probability
forceEnable?: boolean;
};
};
@@ -27,6 +28,14 @@ interface WebExtensionManifestWithFlags
* @returns flags if they exist, otherwise an empty object
*/
export function getManifestFlags(): ManifestFlags {
+ // If this is running in a unit test, there's no manifest, so just return an empty object
+ if (
+ process.env.JEST_WORKER_ID === undefined ||
+ !browser.runtime.getManifest
+ ) {
+ return {};
+ }
+
return (
(browser.runtime.getManifest() as WebExtensionManifestWithFlags)._flags ||
{}
diff --git a/builds.yml b/builds.yml
index fe33507c1e4a..75f6f4b462c6 100644
--- a/builds.yml
+++ b/builds.yml
@@ -298,6 +298,9 @@ env:
# Enables the notifications feature within the build:
- NOTIFICATIONS: ''
+ # This will be defined if running a unit test
+ - JEST_WORKER_ID: undefined
+
- METAMASK_RAMP_API_CONTENT_BASE_URL: https://on-ramp-content.api.cx.metamask.io
###
diff --git a/jest.integration.config.js b/jest.integration.config.js
index d7236b832aed..685080330fb3 100644
--- a/jest.integration.config.js
+++ b/jest.integration.config.js
@@ -25,7 +25,8 @@ module.exports = {
setupFilesAfterEnv: ['/test/integration/config/setupAfter.js'],
testMatch: ['/test/integration/**/*.test.(js|ts|tsx)'],
testPathIgnorePatterns: ['/test/integration/config/*'],
- testTimeout: 5500,
+ // This was increased from 5500 to 10000 to when lazy loading was introduced
+ testTimeout: 10000,
// We have to specify the environment we are running in, which is jsdom. The
// default is 'node'. This can be modified *per file* using a comment at the
// head of the file. So it may be worthwhile to switch to 'node' in any
diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts
index 0d58ddcdcfcc..4c05b098f120 100644
--- a/shared/lib/trace.ts
+++ b/shared/lib/trace.ts
@@ -18,6 +18,7 @@ export enum TraceName {
FirstRender = 'First Render',
GetState = 'Get State',
InitialActions = 'Initial Actions',
+ LazyLoadComponent = 'Lazy Load Component',
LoadScripts = 'Load Scripts',
Middleware = 'Middleware',
NestedTest1 = 'Nested Test 1',
diff --git a/test/integration/confirmations/signatures/permit.test.tsx b/test/integration/confirmations/signatures/permit.test.tsx
index 5ff87bf7c533..7af3be743f5f 100644
--- a/test/integration/confirmations/signatures/permit.test.tsx
+++ b/test/integration/confirmations/signatures/permit.test.tsx
@@ -103,25 +103,29 @@ describe('Permit Confirmation', () => {
});
});
- expect(screen.getByTestId('header-account-name')).toHaveTextContent(
+ expect(await screen.findByTestId('header-account-name')).toHaveTextContent(
accountName,
);
- expect(screen.getByTestId('header-network-display-name')).toHaveTextContent(
- 'Sepolia',
- );
+ expect(
+ await screen.findByTestId('header-network-display-name'),
+ ).toHaveTextContent('Sepolia');
- fireEvent.click(screen.getByTestId('header-info__account-details-button'));
+ fireEvent.click(
+ await screen.findByTestId('header-info__account-details-button'),
+ );
expect(
await screen.findByTestId(
'confirmation-account-details-modal__account-name',
),
).toHaveTextContent(accountName);
- expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent(
- '0x0DCD5...3E7bc',
- );
expect(
- screen.getByTestId('confirmation-account-details-modal__account-balance'),
+ await screen.findByTestId('address-copy-button-text'),
+ ).toHaveTextContent('0x0DCD5...3E7bc');
+ expect(
+ await screen.findByTestId(
+ 'confirmation-account-details-modal__account-balance',
+ ),
).toHaveTextContent('1.582717SepoliaETH');
let confirmAccountDetailsModalMetricsEvent;
@@ -154,7 +158,9 @@ describe('Permit Confirmation', () => {
);
fireEvent.click(
- screen.getByTestId('confirmation-account-details-modal__close-button'),
+ await screen.findByTestId(
+ 'confirmation-account-details-modal__close-button',
+ ),
);
await waitFor(() => {
@@ -252,7 +258,7 @@ describe('Permit Confirmation', () => {
});
});
- const simulationSection = screen.getByTestId(
+ const simulationSection = await screen.findByTestId(
'confirmation__simulation_section',
);
expect(simulationSection).toBeInTheDocument();
@@ -262,9 +268,9 @@ describe('Permit Confirmation', () => {
);
expect(simulationSection).toHaveTextContent('Spending cap');
expect(simulationSection).toHaveTextContent('0xCcCCc...ccccC');
- expect(screen.getByTestId('simulation-token-value')).toHaveTextContent(
- '30',
- );
+ expect(
+ await screen.findByTestId('simulation-token-value'),
+ ).toHaveTextContent('30');
const individualFiatDisplay = await screen.findByTestId(
'individual-fiat-display',
@@ -303,6 +309,6 @@ describe('Permit Confirmation', () => {
account.address,
)})`;
- expect(screen.getByText(mismatchAccountText)).toBeInTheDocument();
+ expect(await screen.findByText(mismatchAccountText)).toBeInTheDocument();
});
});
diff --git a/test/integration/confirmations/signatures/personalSign.test.tsx b/test/integration/confirmations/signatures/personalSign.test.tsx
index 5a9c311c9abd..03685f46ab7b 100644
--- a/test/integration/confirmations/signatures/personalSign.test.tsx
+++ b/test/integration/confirmations/signatures/personalSign.test.tsx
@@ -82,17 +82,20 @@ describe('PersonalSign Confirmation', () => {
account.address,
);
- const { getByTestId, queryByTestId } = await integrationTestRender({
- preloadedState: mockedMetaMaskState,
- backgroundConnection: backgroundConnectionMocked,
- });
+ const { findByTestId, getByTestId, queryByTestId } =
+ await integrationTestRender({
+ preloadedState: mockedMetaMaskState,
+ backgroundConnection: backgroundConnectionMocked,
+ });
- expect(getByTestId('header-account-name')).toHaveTextContent(accountName);
- expect(getByTestId('header-network-display-name')).toHaveTextContent(
+ expect(await findByTestId('header-account-name')).toHaveTextContent(
+ accountName,
+ );
+ expect(await findByTestId('header-network-display-name')).toHaveTextContent(
'Sepolia',
);
- fireEvent.click(getByTestId('header-info__account-details-button'));
+ fireEvent.click(await findByTestId('header-info__account-details-button'));
await waitFor(() => {
expect(
@@ -101,13 +104,13 @@ describe('PersonalSign Confirmation', () => {
});
expect(
- getByTestId('confirmation-account-details-modal__account-name'),
+ await findByTestId('confirmation-account-details-modal__account-name'),
).toHaveTextContent(accountName);
- expect(getByTestId('address-copy-button-text')).toHaveTextContent(
+ expect(await findByTestId('address-copy-button-text')).toHaveTextContent(
'0x0DCD5...3E7bc',
);
expect(
- getByTestId('confirmation-account-details-modal__account-balance'),
+ await findByTestId('confirmation-account-details-modal__account-balance'),
).toHaveTextContent('1.582717SepoliaETH');
let confirmAccountDetailsModalMetricsEvent;
@@ -137,7 +140,7 @@ describe('PersonalSign Confirmation', () => {
);
fireEvent.click(
- getByTestId('confirmation-account-details-modal__close-button'),
+ await findByTestId('confirmation-account-details-modal__close-button'),
);
await waitFor(() => {
@@ -165,9 +168,9 @@ describe('PersonalSign Confirmation', () => {
});
});
- expect(screen.getByText('Signature request')).toBeInTheDocument();
+ expect(await screen.findByText('Signature request')).toBeInTheDocument();
expect(
- screen.getByText('Review request details before you confirm.'),
+ await screen.findByText('Review request details before you confirm.'),
).toBeInTheDocument();
});
@@ -186,7 +189,7 @@ describe('PersonalSign Confirmation', () => {
account.address,
);
- const { getByText } = await integrationTestRender({
+ const { findByText } = await integrationTestRender({
preloadedState: mockedMetaMaskState,
backgroundConnection: backgroundConnectionMocked,
});
@@ -197,6 +200,6 @@ describe('PersonalSign Confirmation', () => {
account.address,
)})`;
- expect(getByText(mismatchAccountText)).toBeInTheDocument();
+ expect(await findByText(mismatchAccountText)).toBeInTheDocument();
});
});
diff --git a/test/integration/confirmations/transactions/alerts.test.tsx b/test/integration/confirmations/transactions/alerts.test.tsx
index 74d37c858b0f..1bbf9d1fd2c5 100644
--- a/test/integration/confirmations/transactions/alerts.test.tsx
+++ b/test/integration/confirmations/transactions/alerts.test.tsx
@@ -129,7 +129,7 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
@@ -182,7 +182,7 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
@@ -228,7 +228,7 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
@@ -274,7 +274,7 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
@@ -349,9 +349,9 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- expect(await screen.getByTestId('inline-alert')).toBeInTheDocument();
+ expect(await screen.findByTestId('inline-alert')).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
@@ -390,7 +390,7 @@ describe('Contract Interaction Confirmation Alerts', () => {
});
});
- fireEvent.click(screen.getByTestId('inline-alert'));
+ fireEvent.click(await screen.findByTestId('inline-alert'));
expect(await screen.findByTestId('alert-modal')).toBeInTheDocument();
diff --git a/test/integration/confirmations/transactions/contract-deployment.test.tsx b/test/integration/confirmations/transactions/contract-deployment.test.tsx
index 67862e6b9550..7698ce3ef8b2 100644
--- a/test/integration/confirmations/transactions/contract-deployment.test.tsx
+++ b/test/integration/confirmations/transactions/contract-deployment.test.tsx
@@ -162,25 +162,29 @@ describe('Contract Deployment Confirmation', () => {
});
});
- expect(screen.getByTestId('header-account-name')).toHaveTextContent(
+ expect(await screen.findByTestId('header-account-name')).toHaveTextContent(
accountName,
);
- expect(screen.getByTestId('header-network-display-name')).toHaveTextContent(
- 'Sepolia',
- );
+ expect(
+ await screen.findByTestId('header-network-display-name'),
+ ).toHaveTextContent('Sepolia');
- fireEvent.click(screen.getByTestId('header-info__account-details-button'));
+ fireEvent.click(
+ await screen.findByTestId('header-info__account-details-button'),
+ );
expect(
await screen.findByTestId(
'confirmation-account-details-modal__account-name',
),
).toHaveTextContent(accountName);
- expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent(
- '0x0DCD5...3E7bc',
- );
expect(
- screen.getByTestId('confirmation-account-details-modal__account-balance'),
+ await screen.findByTestId('address-copy-button-text'),
+ ).toHaveTextContent('0x0DCD5...3E7bc');
+ expect(
+ await screen.findByTestId(
+ 'confirmation-account-details-modal__account-balance',
+ ),
).toHaveTextContent('1.582717SepoliaETH');
let confirmAccountDetailsModalMetricsEvent;
@@ -213,7 +217,9 @@ describe('Contract Deployment Confirmation', () => {
);
fireEvent.click(
- screen.getByTestId('confirmation-account-details-modal__close-button'),
+ await screen.findByTestId(
+ 'confirmation-account-details-modal__close-button',
+ ),
);
await waitFor(() => {
@@ -245,10 +251,12 @@ describe('Contract Deployment Confirmation', () => {
});
expect(
- screen.getByText(tEn('confirmTitleDeployContract') as string),
+ await screen.findByText(tEn('confirmTitleDeployContract') as string),
).toBeInTheDocument();
- const simulationSection = screen.getByTestId('simulation-details-layout');
+ const simulationSection = await screen.findByTestId(
+ 'simulation-details-layout',
+ );
expect(simulationSection).toBeInTheDocument();
expect(simulationSection).toHaveTextContent(
tEn('simulationDetailsTitle') as string,
@@ -261,10 +269,10 @@ describe('Contract Deployment Confirmation', () => {
tEn('simulationDetailsIncomingHeading') as string,
);
expect(simulationDetailsRow).toContainElement(
- screen.getByTestId('simulation-details-amount-pill'),
+ await screen.findByTestId('simulation-details-amount-pill'),
);
- const transactionDetailsSection = screen.getByTestId(
+ const transactionDetailsSection = await screen.findByTestId(
'transaction-details-section',
);
expect(transactionDetailsSection).toBeInTheDocument();
@@ -275,25 +283,30 @@ describe('Contract Deployment Confirmation', () => {
tEn('interactingWith') as string,
);
- const gasFeesSection = screen.getByTestId('gas-fee-section');
+ const gasFeesSection = await screen.findByTestId('gas-fee-section');
expect(gasFeesSection).toBeInTheDocument();
- const editGasFeesRow =
- within(gasFeesSection).getByTestId('edit-gas-fees-row');
+ const editGasFeesRow = await within(gasFeesSection).findByTestId(
+ 'edit-gas-fees-row',
+ );
expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string);
- const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field');
+ const firstGasField = await within(editGasFeesRow).findByTestId(
+ 'first-gas-field',
+ );
expect(firstGasField).toHaveTextContent('0.0001 SepoliaETH');
expect(editGasFeesRow).toContainElement(
- screen.getByTestId('edit-gas-fee-icon'),
+ await screen.findByTestId('edit-gas-fee-icon'),
);
- const gasFeeSpeed = within(gasFeesSection).getByTestId(
+ const gasFeeSpeed = await within(gasFeesSection).findByTestId(
'gas-fee-details-speed',
);
expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string);
- const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time');
+ const gasTimingTime = await within(gasFeeSpeed).findByTestId(
+ 'gas-timing-time',
+ );
expect(gasTimingTime).toHaveTextContent('~0 sec');
});
@@ -321,7 +334,9 @@ describe('Contract Deployment Confirmation', () => {
});
});
- fireEvent.click(screen.getByTestId('header-advanced-details-button'));
+ fireEvent.click(
+ await screen.findByTestId('header-advanced-details-button'),
+ );
await waitFor(() => {
expect(
@@ -364,28 +379,32 @@ describe('Contract Deployment Confirmation', () => {
).toHaveBeenCalledWith('getNextNonce', expect.anything());
});
- const gasFeesSection = screen.getByTestId('gas-fee-section');
- const maxFee = screen.getByTestId('gas-fee-details-max-fee');
+ const gasFeesSection = await screen.findByTestId('gas-fee-section');
+ const maxFee = await screen.findByTestId('gas-fee-details-max-fee');
expect(gasFeesSection).toContainElement(maxFee);
expect(maxFee).toHaveTextContent(tEn('maxFee') as string);
expect(maxFee).toHaveTextContent('0.0023 SepoliaETH');
- const nonceSection = screen.getByTestId('advanced-details-nonce-section');
+ const nonceSection = await screen.findByTestId(
+ 'advanced-details-nonce-section',
+ );
expect(nonceSection).toBeInTheDocument();
expect(nonceSection).toHaveTextContent(
tEn('advancedDetailsNonceDesc') as string,
);
expect(nonceSection).toContainElement(
- screen.getByTestId('advanced-details-displayed-nonce'),
+ await screen.findByTestId('advanced-details-displayed-nonce'),
);
expect(
- screen.getByTestId('advanced-details-displayed-nonce'),
+ await screen.findByTestId('advanced-details-displayed-nonce'),
).toHaveTextContent('9');
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -394,7 +413,7 @@ describe('Contract Deployment Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('Deposit');
- const transactionDataParams = screen.getByTestId(
+ const transactionDataParams = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(transactionDataParams);
diff --git a/test/integration/confirmations/transactions/contract-interaction.test.tsx b/test/integration/confirmations/transactions/contract-interaction.test.tsx
index 9a955e1a45fb..5db121bc9fda 100644
--- a/test/integration/confirmations/transactions/contract-interaction.test.tsx
+++ b/test/integration/confirmations/transactions/contract-interaction.test.tsx
@@ -23,6 +23,8 @@ import {
getUnapprovedContractInteractionTransaction,
} from './transactionDataHelpers';
+jest.setTimeout(20_000);
+
jest.mock('../../../../ui/store/background-connection', () => ({
...jest.requireActual('../../../../ui/store/background-connection'),
submitRequestToBackground: jest.fn(),
@@ -180,20 +182,25 @@ describe('Contract Interaction Confirmation', () => {
});
});
- expect(screen.getByTestId('header-account-name')).toHaveTextContent(
+ await screen.findByText(accountName);
+ expect(await screen.findByTestId('header-account-name')).toHaveTextContent(
accountName,
);
- expect(screen.getByTestId('header-network-display-name')).toHaveTextContent(
- 'Sepolia',
- );
+ expect(
+ await screen.findByTestId('header-network-display-name'),
+ ).toHaveTextContent('Sepolia');
- fireEvent.click(screen.getByTestId('header-info__account-details-button'));
+ await act(async () => {
+ fireEvent.click(
+ await screen.findByTestId('header-info__account-details-button'),
+ );
+ });
- expect(
- await screen.findByTestId(
- 'confirmation-account-details-modal__account-name',
- ),
- ).toHaveTextContent(accountName);
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('confirmation-account-details-modal__account-name'),
+ ).toHaveTextContent(accountName);
+ });
expect(screen.getByTestId('address-copy-button-text')).toHaveTextContent(
'0x0DCD5...3E7bc',
);
@@ -214,21 +221,21 @@ describe('Contract Interaction Confirmation', () => {
expect(confirmAccountDetailsModalMetricsEvent?.[0]).toBe(
'trackMetaMetricsEvent',
);
- });
- expect(confirmAccountDetailsModalMetricsEvent?.[1]).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- category: MetaMetricsEventCategory.Confirmations,
- event: MetaMetricsEventName.AccountDetailsOpened,
- properties: {
- action: 'Confirm Screen',
- location: MetaMetricsEventLocation.Transaction,
- transaction_type: TransactionType.contractInteraction,
- },
- }),
- ]),
- );
+ expect(confirmAccountDetailsModalMetricsEvent?.[1]).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ category: MetaMetricsEventCategory.Confirmations,
+ event: MetaMetricsEventName.AccountDetailsOpened,
+ properties: {
+ action: 'Confirm Screen',
+ location: MetaMetricsEventLocation.Transaction,
+ transaction_type: TransactionType.contractInteraction,
+ },
+ }),
+ ]),
+ );
+ });
fireEvent.click(
screen.getByTestId('confirmation-account-details-modal__close-button'),
@@ -263,10 +270,12 @@ describe('Contract Interaction Confirmation', () => {
});
expect(
- screen.getByText(tEn('confirmTitleTransaction') as string),
+ await screen.findByText(tEn('confirmTitleTransaction') as string),
).toBeInTheDocument();
- const simulationSection = screen.getByTestId('simulation-details-layout');
+ const simulationSection = await screen.findByTestId(
+ 'simulation-details-layout',
+ );
expect(simulationSection).toBeInTheDocument();
expect(simulationSection).toHaveTextContent(
tEn('simulationDetailsTitle') as string,
@@ -279,10 +288,10 @@ describe('Contract Interaction Confirmation', () => {
tEn('simulationDetailsIncomingHeading') as string,
);
expect(simulationDetailsRow).toContainElement(
- screen.getByTestId('simulation-details-amount-pill'),
+ await screen.findByTestId('simulation-details-amount-pill'),
);
- const transactionDetailsSection = screen.getByTestId(
+ const transactionDetailsSection = await screen.findByTestId(
'transaction-details-section',
);
expect(transactionDetailsSection).toBeInTheDocument();
@@ -293,25 +302,30 @@ describe('Contract Interaction Confirmation', () => {
tEn('interactingWith') as string,
);
- const gasFeesSection = screen.getByTestId('gas-fee-section');
+ const gasFeesSection = await screen.findByTestId('gas-fee-section');
expect(gasFeesSection).toBeInTheDocument();
- const editGasFeesRow =
- within(gasFeesSection).getByTestId('edit-gas-fees-row');
+ const editGasFeesRow = await within(gasFeesSection).findByTestId(
+ 'edit-gas-fees-row',
+ );
expect(editGasFeesRow).toHaveTextContent(tEn('networkFee') as string);
- const firstGasField = within(editGasFeesRow).getByTestId('first-gas-field');
+ const firstGasField = await within(editGasFeesRow).findByTestId(
+ 'first-gas-field',
+ );
expect(firstGasField).toHaveTextContent('0.0001 SepoliaETH');
expect(editGasFeesRow).toContainElement(
- screen.getByTestId('edit-gas-fee-icon'),
+ await screen.findByTestId('edit-gas-fee-icon'),
);
- const gasFeeSpeed = within(gasFeesSection).getByTestId(
+ const gasFeeSpeed = await within(gasFeesSection).findByTestId(
'gas-fee-details-speed',
);
expect(gasFeeSpeed).toHaveTextContent(tEn('speed') as string);
- const gasTimingTime = within(gasFeeSpeed).getByTestId('gas-timing-time');
+ const gasTimingTime = await within(gasFeeSpeed).findByTestId(
+ 'gas-timing-time',
+ );
expect(gasTimingTime).toHaveTextContent('~0 sec');
});
@@ -339,7 +353,9 @@ describe('Contract Interaction Confirmation', () => {
});
});
- fireEvent.click(screen.getByTestId('header-advanced-details-button'));
+ fireEvent.click(
+ await screen.findByTestId('header-advanced-details-button'),
+ );
await waitFor(() => {
expect(
@@ -395,28 +411,32 @@ describe('Contract Interaction Confirmation', () => {
]);
});
- const gasFeesSection = screen.getByTestId('gas-fee-section');
- const maxFee = screen.getByTestId('gas-fee-details-max-fee');
+ const gasFeesSection = await screen.findByTestId('gas-fee-section');
+ const maxFee = await screen.findByTestId('gas-fee-details-max-fee');
expect(gasFeesSection).toContainElement(maxFee);
expect(maxFee).toHaveTextContent(tEn('maxFee') as string);
expect(maxFee).toHaveTextContent('0.0023 SepoliaETH');
- const nonceSection = screen.getByTestId('advanced-details-nonce-section');
+ const nonceSection = await screen.findByTestId(
+ 'advanced-details-nonce-section',
+ );
expect(nonceSection).toBeInTheDocument();
expect(nonceSection).toHaveTextContent(
tEn('advancedDetailsNonceDesc') as string,
);
expect(nonceSection).toContainElement(
- screen.getByTestId('advanced-details-displayed-nonce'),
+ await screen.findByTestId('advanced-details-displayed-nonce'),
);
expect(
- screen.getByTestId('advanced-details-displayed-nonce'),
+ await screen.findByTestId('advanced-details-displayed-nonce'),
).toHaveTextContent('9');
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -425,7 +445,7 @@ describe('Contract Interaction Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('mintNFTs');
- const transactionDataParams = screen.getByTestId(
+ const transactionDataParams = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(transactionDataParams);
@@ -454,7 +474,7 @@ describe('Contract Interaction Confirmation', () => {
const headingText = tEn('blockaidTitleDeceptive') as string;
const bodyText = tEn('blockaidDescriptionTransferFarming') as string;
- expect(screen.getByText(headingText)).toBeInTheDocument();
- expect(screen.getByText(bodyText)).toBeInTheDocument();
+ expect(await screen.findByText(headingText)).toBeInTheDocument();
+ expect(await screen.findByText(bodyText)).toBeInTheDocument();
});
});
diff --git a/test/integration/confirmations/transactions/erc20-approve.test.tsx b/test/integration/confirmations/transactions/erc20-approve.test.tsx
index a2404ba75b09..c25b2ee3627d 100644
--- a/test/integration/confirmations/transactions/erc20-approve.test.tsx
+++ b/test/integration/confirmations/transactions/erc20-approve.test.tsx
@@ -163,10 +163,10 @@ describe('ERC20 Approve Confirmation', () => {
});
expect(
- screen.getByText(tEn('confirmTitlePermitTokens') as string),
+ await screen.findByText(tEn('confirmTitlePermitTokens') as string),
).toBeInTheDocument();
expect(
- screen.getByText(
+ await screen.findByText(
tEn('confirmTitleDescERC20ApproveTransaction') as string,
),
).toBeInTheDocument();
@@ -183,7 +183,7 @@ describe('ERC20 Approve Confirmation', () => {
});
});
- const simulationSection = screen.getByTestId(
+ const simulationSection = await screen.findByTestId(
'confirmation__simulation_section',
);
expect(simulationSection).toBeInTheDocument();
@@ -192,7 +192,9 @@ describe('ERC20 Approve Confirmation', () => {
tEn('simulationDetailsERC20ApproveDesc') as string,
);
expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string);
- const spendingCapValue = screen.getByTestId('simulation-token-value');
+ const spendingCapValue = await screen.findByTestId(
+ 'simulation-token-value',
+ );
expect(simulationSection).toContainElement(spendingCapValue);
expect(spendingCapValue).toHaveTextContent('1');
expect(simulationSection).toHaveTextContent('0x07614...3ad68');
@@ -211,16 +213,18 @@ describe('ERC20 Approve Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsSpender = screen.getByTestId(
+ const approveDetailsSpender = await screen.findByTestId(
'confirmation__approve-spender',
);
expect(approveDetails).toContainElement(approveDetailsSpender);
expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string);
expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B');
- const spenderTooltip = screen.getByTestId(
+ const spenderTooltip = await screen.findByTestId(
'confirmation__approve-spender-tooltip',
);
expect(approveDetailsSpender).toContainElement(spenderTooltip);
@@ -231,7 +235,7 @@ describe('ERC20 Approve Confirmation', () => {
);
expect(spenderTooltipContent).toBeInTheDocument();
- const approveDetailsRequestFrom = screen.getByTestId(
+ const approveDetailsRequestFrom = await screen.findByTestId(
'transaction-details-origin-row',
);
expect(approveDetails).toContainElement(approveDetailsRequestFrom);
@@ -240,7 +244,7 @@ describe('ERC20 Approve Confirmation', () => {
'http://localhost:8086/',
);
- const approveDetailsRequestFromTooltip = screen.getByTestId(
+ const approveDetailsRequestFromTooltip = await screen.findByTestId(
'transaction-details-origin-row-tooltip',
);
expect(approveDetailsRequestFrom).toContainElement(
@@ -266,7 +270,7 @@ describe('ERC20 Approve Confirmation', () => {
});
});
- const spendingCapSection = screen.getByTestId(
+ const spendingCapSection = await screen.findByTestId(
'confirmation__approve-spending-cap-section',
);
expect(spendingCapSection).toBeInTheDocument();
@@ -275,14 +279,14 @@ describe('ERC20 Approve Confirmation', () => {
tEn('accountBalance') as string,
);
expect(spendingCapSection).toHaveTextContent('0');
- const spendingCapGroup = screen.getByTestId(
+ const spendingCapGroup = await screen.findByTestId(
'confirmation__approve-spending-cap-group',
);
expect(spendingCapSection).toContainElement(spendingCapGroup);
expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string);
expect(spendingCapGroup).toHaveTextContent('1');
- const spendingCapGroupTooltip = screen.getByTestId(
+ const spendingCapGroupTooltip = await screen.findByTestId(
'confirmation__approve-spending-cap-group-tooltip',
);
expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip);
@@ -308,10 +312,12 @@ describe('ERC20 Approve Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsRecipient = screen.getByTestId(
+ const approveDetailsRecipient = await screen.findByTestId(
'transaction-details-recipient-row',
);
expect(approveDetails).toContainElement(approveDetailsRecipient);
@@ -320,7 +326,7 @@ describe('ERC20 Approve Confirmation', () => {
);
expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68');
- const approveDetailsRecipientTooltip = screen.getByTestId(
+ const approveDetailsRecipientTooltip = await screen.findByTestId(
'transaction-details-recipient-row-tooltip',
);
expect(approveDetailsRecipient).toContainElement(
@@ -338,7 +344,7 @@ describe('ERC20 Approve Confirmation', () => {
expect(approveDetails).toContainElement(approveMethodData);
expect(approveMethodData).toHaveTextContent(tEn('methodData') as string);
expect(approveMethodData).toHaveTextContent('Approve');
- const approveMethodDataTooltip = screen.getByTestId(
+ const approveMethodDataTooltip = await screen.findByTestId(
'transaction-details-method-data-row-tooltip',
);
expect(approveMethodData).toContainElement(approveMethodDataTooltip);
@@ -348,15 +354,17 @@ describe('ERC20 Approve Confirmation', () => {
);
expect(approveMethodDataTooltipContent).toBeInTheDocument();
- const approveDetailsNonce = screen.getByTestId(
+ const approveDetailsNonce = await screen.findByTestId(
'advanced-details-nonce-section',
);
expect(approveDetailsNonce).toBeInTheDocument();
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -365,14 +373,14 @@ describe('ERC20 Approve Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('Approve');
- const approveDataParams1 = screen.getByTestId(
+ const approveDataParams1 = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(approveDataParams1);
expect(approveDataParams1).toHaveTextContent('Param #1');
expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B');
- const approveDataParams2 = screen.getByTestId(
+ const approveDataParams2 = await screen.findByTestId(
'advanced-details-data-param-1',
);
expect(dataSection).toContainElement(approveDataParams2);
diff --git a/test/integration/confirmations/transactions/erc721-approve.test.tsx b/test/integration/confirmations/transactions/erc721-approve.test.tsx
index c3948d150b1d..c158717cc9c9 100644
--- a/test/integration/confirmations/transactions/erc721-approve.test.tsx
+++ b/test/integration/confirmations/transactions/erc721-approve.test.tsx
@@ -163,10 +163,12 @@ describe('ERC721 Approve Confirmation', () => {
});
expect(
- screen.getByText(tEn('confirmTitleApproveTransaction') as string),
+ await screen.findByText(tEn('confirmTitleApproveTransaction') as string),
).toBeInTheDocument();
expect(
- screen.getByText(tEn('confirmTitleDescApproveTransaction') as string),
+ await screen.findByText(
+ tEn('confirmTitleDescApproveTransaction') as string,
+ ),
).toBeInTheDocument();
});
@@ -181,7 +183,7 @@ describe('ERC721 Approve Confirmation', () => {
});
});
- const simulationSection = screen.getByTestId(
+ const simulationSection = await screen.findByTestId(
'confirmation__simulation_section',
);
expect(simulationSection).toBeInTheDocument();
@@ -192,7 +194,9 @@ describe('ERC721 Approve Confirmation', () => {
expect(simulationSection).toHaveTextContent(
tEn('simulationApproveHeading') as string,
);
- const spendingCapValue = screen.getByTestId('simulation-token-value');
+ const spendingCapValue = await screen.findByTestId(
+ 'simulation-token-value',
+ );
expect(simulationSection).toContainElement(spendingCapValue);
expect(spendingCapValue).toHaveTextContent('1');
expect(simulationSection).toHaveTextContent('0x07614...3ad68');
@@ -211,16 +215,18 @@ describe('ERC721 Approve Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsSpender = screen.getByTestId(
+ const approveDetailsSpender = await screen.findByTestId(
'confirmation__approve-spender',
);
expect(approveDetails).toContainElement(approveDetailsSpender);
expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string);
expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B');
- const spenderTooltip = screen.getByTestId(
+ const spenderTooltip = await screen.findByTestId(
'confirmation__approve-spender-tooltip',
);
expect(approveDetailsSpender).toContainElement(spenderTooltip);
@@ -230,7 +236,7 @@ describe('ERC721 Approve Confirmation', () => {
);
expect(spenderTooltipContent).toBeInTheDocument();
- const approveDetailsRequestFrom = screen.getByTestId(
+ const approveDetailsRequestFrom = await screen.findByTestId(
'transaction-details-origin-row',
);
expect(approveDetails).toContainElement(approveDetailsRequestFrom);
@@ -241,7 +247,7 @@ describe('ERC721 Approve Confirmation', () => {
'http://localhost:8086/',
);
- const approveDetailsRequestFromTooltip = screen.getByTestId(
+ const approveDetailsRequestFromTooltip = await screen.findByTestId(
'transaction-details-origin-row-tooltip',
);
expect(approveDetailsRequestFrom).toContainElement(
@@ -269,10 +275,12 @@ describe('ERC721 Approve Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsRecipient = screen.getByTestId(
+ const approveDetailsRecipient = await screen.findByTestId(
'transaction-details-recipient-row',
);
expect(approveDetails).toContainElement(approveDetailsRecipient);
@@ -281,7 +289,7 @@ describe('ERC721 Approve Confirmation', () => {
);
expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68');
- const approveDetailsRecipientTooltip = screen.getByTestId(
+ const approveDetailsRecipientTooltip = await screen.findByTestId(
'transaction-details-recipient-row-tooltip',
);
expect(approveDetailsRecipient).toContainElement(
@@ -299,7 +307,7 @@ describe('ERC721 Approve Confirmation', () => {
expect(approveDetails).toContainElement(approveMethodData);
expect(approveMethodData).toHaveTextContent(tEn('methodData') as string);
expect(approveMethodData).toHaveTextContent('Approve');
- const approveMethodDataTooltip = screen.getByTestId(
+ const approveMethodDataTooltip = await screen.findByTestId(
'transaction-details-method-data-row-tooltip',
);
expect(approveMethodData).toContainElement(approveMethodDataTooltip);
@@ -309,15 +317,17 @@ describe('ERC721 Approve Confirmation', () => {
);
expect(approveMethodDataTooltipContent).toBeInTheDocument();
- const approveDetailsNonce = screen.getByTestId(
+ const approveDetailsNonce = await screen.findByTestId(
'advanced-details-nonce-section',
);
expect(approveDetailsNonce).toBeInTheDocument();
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -326,14 +336,14 @@ describe('ERC721 Approve Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('Approve');
- const approveDataParams1 = screen.getByTestId(
+ const approveDataParams1 = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(approveDataParams1);
expect(approveDataParams1).toHaveTextContent('Param #1');
expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B');
- const approveDataParams2 = screen.getByTestId(
+ const approveDataParams2 = await screen.findByTestId(
'advanced-details-data-param-1',
);
expect(dataSection).toContainElement(approveDataParams2);
diff --git a/test/integration/confirmations/transactions/increase-allowance.test.tsx b/test/integration/confirmations/transactions/increase-allowance.test.tsx
index c288a5cc4e6d..810477d3a3a5 100644
--- a/test/integration/confirmations/transactions/increase-allowance.test.tsx
+++ b/test/integration/confirmations/transactions/increase-allowance.test.tsx
@@ -167,10 +167,10 @@ describe('ERC20 increaseAllowance Confirmation', () => {
});
expect(
- screen.getByText(tEn('confirmTitlePermitTokens') as string),
+ await screen.findByText(tEn('confirmTitlePermitTokens') as string),
).toBeInTheDocument();
expect(
- screen.getByText(tEn('confirmTitleDescPermitSignature') as string),
+ await screen.findByText(tEn('confirmTitleDescPermitSignature') as string),
).toBeInTheDocument();
});
@@ -185,7 +185,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
});
});
- const simulationSection = screen.getByTestId(
+ const simulationSection = await screen.findByTestId(
'confirmation__simulation_section',
);
expect(simulationSection).toBeInTheDocument();
@@ -194,7 +194,9 @@ describe('ERC20 increaseAllowance Confirmation', () => {
tEn('simulationDetailsERC20ApproveDesc') as string,
);
expect(simulationSection).toHaveTextContent(tEn('spendingCap') as string);
- const spendingCapValue = screen.getByTestId('simulation-token-value');
+ const spendingCapValue = await screen.findByTestId(
+ 'simulation-token-value',
+ );
expect(simulationSection).toContainElement(spendingCapValue);
expect(spendingCapValue).toHaveTextContent('1');
expect(simulationSection).toHaveTextContent('0x07614...3ad68');
@@ -213,16 +215,18 @@ describe('ERC20 increaseAllowance Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsSpender = screen.getByTestId(
+ const approveDetailsSpender = await screen.findByTestId(
'confirmation__approve-spender',
);
expect(approveDetails).toContainElement(approveDetailsSpender);
expect(approveDetailsSpender).toHaveTextContent(tEn('spender') as string);
expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B');
- const spenderTooltip = screen.getByTestId(
+ const spenderTooltip = await screen.findByTestId(
'confirmation__approve-spender-tooltip',
);
expect(approveDetailsSpender).toContainElement(spenderTooltip);
@@ -233,7 +237,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
);
expect(spenderTooltipContent).toBeInTheDocument();
- const approveDetailsRequestFrom = screen.getByTestId(
+ const approveDetailsRequestFrom = await screen.findByTestId(
'transaction-details-origin-row',
);
expect(approveDetails).toContainElement(approveDetailsRequestFrom);
@@ -242,7 +246,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
'http://localhost:8086/',
);
- const approveDetailsRequestFromTooltip = screen.getByTestId(
+ const approveDetailsRequestFromTooltip = await screen.findByTestId(
'transaction-details-origin-row-tooltip',
);
expect(approveDetailsRequestFrom).toContainElement(
@@ -268,7 +272,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
});
});
- const spendingCapSection = screen.getByTestId(
+ const spendingCapSection = await screen.findByTestId(
'confirmation__approve-spending-cap-section',
);
expect(spendingCapSection).toBeInTheDocument();
@@ -277,14 +281,14 @@ describe('ERC20 increaseAllowance Confirmation', () => {
tEn('accountBalance') as string,
);
expect(spendingCapSection).toHaveTextContent('0');
- const spendingCapGroup = screen.getByTestId(
+ const spendingCapGroup = await screen.findByTestId(
'confirmation__approve-spending-cap-group',
);
expect(spendingCapSection).toContainElement(spendingCapGroup);
expect(spendingCapGroup).toHaveTextContent(tEn('spendingCap') as string);
expect(spendingCapGroup).toHaveTextContent('1');
- const spendingCapGroupTooltip = screen.getByTestId(
+ const spendingCapGroupTooltip = await screen.findByTestId(
'confirmation__approve-spending-cap-group-tooltip',
);
expect(spendingCapGroup).toContainElement(spendingCapGroupTooltip);
@@ -310,10 +314,12 @@ describe('ERC20 increaseAllowance Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsRecipient = screen.getByTestId(
+ const approveDetailsRecipient = await screen.findByTestId(
'transaction-details-recipient-row',
);
expect(approveDetails).toContainElement(approveDetailsRecipient);
@@ -322,7 +328,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
);
expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68');
- const approveDetailsRecipientTooltip = screen.getByTestId(
+ const approveDetailsRecipientTooltip = await screen.findByTestId(
'transaction-details-recipient-row-tooltip',
);
expect(approveDetailsRecipient).toContainElement(
@@ -340,7 +346,7 @@ describe('ERC20 increaseAllowance Confirmation', () => {
expect(approveDetails).toContainElement(approveMethodData);
expect(approveMethodData).toHaveTextContent(tEn('methodData') as string);
expect(approveMethodData).toHaveTextContent('increaseAllowance');
- const approveMethodDataTooltip = screen.getByTestId(
+ const approveMethodDataTooltip = await screen.findByTestId(
'transaction-details-method-data-row-tooltip',
);
expect(approveMethodData).toContainElement(approveMethodDataTooltip);
@@ -350,15 +356,17 @@ describe('ERC20 increaseAllowance Confirmation', () => {
);
expect(approveMethodDataTooltipContent).toBeInTheDocument();
- const approveDetailsNonce = screen.getByTestId(
+ const approveDetailsNonce = await screen.findByTestId(
'advanced-details-nonce-section',
);
expect(approveDetailsNonce).toBeInTheDocument();
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -367,14 +375,14 @@ describe('ERC20 increaseAllowance Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('increaseAllowance');
- const approveDataParams1 = screen.getByTestId(
+ const approveDataParams1 = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(approveDataParams1);
expect(approveDataParams1).toHaveTextContent('Param #1');
expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B');
- const approveDataParams2 = screen.getByTestId(
+ const approveDataParams2 = await screen.findByTestId(
'advanced-details-data-param-1',
);
expect(dataSection).toContainElement(approveDataParams2);
diff --git a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx
index a65688030e90..ebe680983a6c 100644
--- a/test/integration/confirmations/transactions/set-approval-for-all.test.tsx
+++ b/test/integration/confirmations/transactions/set-approval-for-all.test.tsx
@@ -167,10 +167,14 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
});
expect(
- screen.getByText(tEn('setApprovalForAllRedesignedTitle') as string),
+ await screen.findByText(
+ tEn('setApprovalForAllRedesignedTitle') as string,
+ ),
).toBeInTheDocument();
expect(
- screen.getByText(tEn('confirmTitleDescApproveTransaction') as string),
+ await screen.findByText(
+ tEn('confirmTitleDescApproveTransaction') as string,
+ ),
).toBeInTheDocument();
});
@@ -185,7 +189,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
});
});
- const simulationSection = screen.getByTestId(
+ const simulationSection = await screen.findByTestId(
'confirmation__simulation_section',
);
expect(simulationSection).toBeInTheDocument();
@@ -194,7 +198,9 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
tEn('simulationDetailsSetApprovalForAllDesc') as string,
);
expect(simulationSection).toHaveTextContent(tEn('withdrawing') as string);
- const spendingCapValue = screen.getByTestId('simulation-token-value');
+ const spendingCapValue = await screen.findByTestId(
+ 'simulation-token-value',
+ );
expect(simulationSection).toContainElement(spendingCapValue);
expect(spendingCapValue).toHaveTextContent(tEn('all') as string);
expect(simulationSection).toHaveTextContent('0x07614...3ad68');
@@ -213,9 +219,11 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsSpender = screen.getByTestId(
+ const approveDetailsSpender = await screen.findByTestId(
'confirmation__approve-spender',
);
@@ -224,7 +232,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
tEn('permissionFor') as string,
);
expect(approveDetailsSpender).toHaveTextContent('0x2e0D7...5d09B');
- const spenderTooltip = screen.getByTestId(
+ const spenderTooltip = await screen.findByTestId(
'confirmation__approve-spender-tooltip',
);
expect(approveDetailsSpender).toContainElement(spenderTooltip);
@@ -235,7 +243,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
);
expect(spenderTooltipContent).toBeInTheDocument();
- const approveDetailsRequestFrom = screen.getByTestId(
+ const approveDetailsRequestFrom = await screen.findByTestId(
'transaction-details-origin-row',
);
expect(approveDetails).toContainElement(approveDetailsRequestFrom);
@@ -246,7 +254,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
'http://localhost:8086/',
);
- const approveDetailsRequestFromTooltip = screen.getByTestId(
+ const approveDetailsRequestFromTooltip = await screen.findByTestId(
'transaction-details-origin-row-tooltip',
);
expect(approveDetailsRequestFrom).toContainElement(
@@ -274,10 +282,12 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
});
});
- const approveDetails = screen.getByTestId('confirmation__approve-details');
+ const approveDetails = await screen.findByTestId(
+ 'confirmation__approve-details',
+ );
expect(approveDetails).toBeInTheDocument();
- const approveDetailsRecipient = screen.getByTestId(
+ const approveDetailsRecipient = await screen.findByTestId(
'transaction-details-recipient-row',
);
expect(approveDetails).toContainElement(approveDetailsRecipient);
@@ -286,7 +296,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
);
expect(approveDetailsRecipient).toHaveTextContent('0x07614...3ad68');
- const approveDetailsRecipientTooltip = screen.getByTestId(
+ const approveDetailsRecipientTooltip = await screen.findByTestId(
'transaction-details-recipient-row-tooltip',
);
expect(approveDetailsRecipient).toContainElement(
@@ -304,7 +314,7 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
expect(approveDetails).toContainElement(approveMethodData);
expect(approveMethodData).toHaveTextContent(tEn('methodData') as string);
expect(approveMethodData).toHaveTextContent('setApprovalForAll');
- const approveMethodDataTooltip = screen.getByTestId(
+ const approveMethodDataTooltip = await screen.findByTestId(
'transaction-details-method-data-row-tooltip',
);
expect(approveMethodData).toContainElement(approveMethodDataTooltip);
@@ -314,15 +324,17 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
);
expect(approveMethodDataTooltipContent).toBeInTheDocument();
- const approveDetailsNonce = screen.getByTestId(
+ const approveDetailsNonce = await screen.findByTestId(
'advanced-details-nonce-section',
);
expect(approveDetailsNonce).toBeInTheDocument();
- const dataSection = screen.getByTestId('advanced-details-data-section');
+ const dataSection = await screen.findByTestId(
+ 'advanced-details-data-section',
+ );
expect(dataSection).toBeInTheDocument();
- const dataSectionFunction = screen.getByTestId(
+ const dataSectionFunction = await screen.findByTestId(
'advanced-details-data-function',
);
expect(dataSection).toContainElement(dataSectionFunction);
@@ -331,14 +343,14 @@ describe('ERC721 setApprovalForAll Confirmation', () => {
);
expect(dataSectionFunction).toHaveTextContent('setApprovalForAll');
- const approveDataParams1 = screen.getByTestId(
+ const approveDataParams1 = await screen.findByTestId(
'advanced-details-data-param-0',
);
expect(dataSection).toContainElement(approveDataParams1);
expect(approveDataParams1).toHaveTextContent('Param #1');
expect(approveDataParams1).toHaveTextContent('0x2e0D7...5d09B');
- const approveDataParams2 = screen.getByTestId(
+ const approveDataParams2 = await screen.findByTestId(
'advanced-details-data-param-1',
);
expect(dataSection).toContainElement(approveDataParams2);
diff --git a/test/integration/notifications&auth/notifications-activation.test.tsx b/test/integration/notifications&auth/notifications-activation.test.tsx
index e11e58dad320..d52921c386e8 100644
--- a/test/integration/notifications&auth/notifications-activation.test.tsx
+++ b/test/integration/notifications&auth/notifications-activation.test.tsx
@@ -70,7 +70,7 @@ describe('Notifications Activation', () => {
const clickElement = async (testId: string) => {
await act(async () => {
- fireEvent.click(screen.getByTestId(testId));
+ fireEvent.click(await screen.findByTestId(testId));
});
};
@@ -105,7 +105,7 @@ describe('Notifications Activation', () => {
});
await act(async () => {
- fireEvent.click(screen.getByText('Turn on'));
+ fireEvent.click(await screen.findByText('Turn on'));
});
await waitFor(() => {
@@ -148,7 +148,7 @@ describe('Notifications Activation', () => {
await act(async () => {
fireEvent.click(
- within(screen.getByRole('dialog')).getByRole('button', {
+ await within(screen.getByRole('dialog')).findByRole('button', {
name: 'Close',
}),
);
diff --git a/test/integration/notifications&auth/notifications-list.test.tsx b/test/integration/notifications&auth/notifications-list.test.tsx
index 4e17a53db107..e4c1d6f20107 100644
--- a/test/integration/notifications&auth/notifications-list.test.tsx
+++ b/test/integration/notifications&auth/notifications-list.test.tsx
@@ -77,8 +77,8 @@ describe('Notifications List', () => {
});
});
- await waitFor(() => {
- const unreadCount = screen.getByTestId(
+ await waitFor(async () => {
+ const unreadCount = await screen.findByTestId(
'notifications-tag-counter__unread-dot',
);
expect(unreadCount).toBeInTheDocument();
@@ -96,30 +96,36 @@ describe('Notifications List', () => {
});
});
- fireEvent.click(screen.getByTestId('account-options-menu-button'));
+ fireEvent.click(await screen.findByTestId('account-options-menu-button'));
- await waitFor(() => {
- expect(screen.getByTestId('notifications-menu-item')).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('notifications-menu-item'));
+ await waitFor(async () => {
+ expect(
+ await screen.findByTestId('notifications-menu-item'),
+ ).toBeInTheDocument();
+ fireEvent.click(await screen.findByTestId('notifications-menu-item'));
});
- await waitFor(() => {
- const notificationsList = screen.getByTestId('notifications-list');
+ await waitFor(async () => {
+ const notificationsList = await screen.findByTestId('notifications-list');
expect(notificationsList).toBeInTheDocument();
expect(notificationsList.childElementCount).toBe(3);
// Feature notification details
expect(
- within(notificationsList).getByText(featureNotification.data.title),
+ await within(notificationsList).findByText(
+ featureNotification.data.title,
+ ),
).toBeInTheDocument();
expect(
- within(notificationsList).getByText(
+ await within(notificationsList).findByText(
featureNotification.data.shortDescription,
),
).toBeInTheDocument();
// Eth sent notification details
- const sentToElement = within(notificationsList).getByText('Sent to');
+ const sentToElement = await within(notificationsList).findByText(
+ 'Sent to',
+ );
expect(sentToElement).toBeInTheDocument();
const addressElement = sentToElement.nextElementSibling;
@@ -127,12 +133,12 @@ describe('Notifications List', () => {
// Read all button
expect(
- within(notificationsList).getByTestId(
+ await within(notificationsList).findByTestId(
'notifications-list-read-all-button',
),
).toBeInTheDocument();
- const unreadDot = screen.getAllByTestId('unread-dot');
+ const unreadDot = await screen.findAllByTestId('unread-dot');
expect(unreadDot).toHaveLength(2);
});
@@ -178,17 +184,19 @@ describe('Notifications List', () => {
backgroundConnection: backgroundConnectionMocked,
});
- fireEvent.click(screen.getByTestId('account-options-menu-button'));
+ fireEvent.click(await screen.findByTestId('account-options-menu-button'));
- await waitFor(() => {
+ await waitFor(async () => {
expect(
- screen.getByTestId('notifications-menu-item'),
+ await screen.findByTestId('notifications-menu-item'),
).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('notifications-menu-item'));
+ fireEvent.click(await screen.findByTestId('notifications-menu-item'));
});
- await waitFor(() => {
- const notificationsList = screen.getByTestId('notifications-list');
+ await waitFor(async () => {
+ const notificationsList = await screen.findByTestId(
+ 'notifications-list',
+ );
expect(notificationsList).toBeInTheDocument();
expect(notificationsList.childElementCount).toBe(2);
@@ -211,14 +219,18 @@ describe('Notifications List', () => {
});
});
- fireEvent.click(screen.getByTestId('account-options-menu-button'));
+ fireEvent.click(await screen.findByTestId('account-options-menu-button'));
- await waitFor(() => {
- expect(screen.getByTestId('notifications-menu-item')).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('notifications-menu-item'));
+ await waitFor(async () => {
+ expect(
+ await screen.findByTestId('notifications-menu-item'),
+ ).toBeInTheDocument();
+ fireEvent.click(await screen.findByTestId('notifications-menu-item'));
});
- fireEvent.click(screen.getByTestId('notifications-list-read-all-button'));
+ fireEvent.click(
+ await screen.findByTestId('notifications-list-read-all-button'),
+ );
await waitFor(() => {
const markAllAsReadEvent =
diff --git a/test/integration/notifications&auth/notifications-toggle.test.tsx b/test/integration/notifications&auth/notifications-toggle.test.tsx
index 8133e4c4bc3d..fd4c11ec4494 100644
--- a/test/integration/notifications&auth/notifications-toggle.test.tsx
+++ b/test/integration/notifications&auth/notifications-toggle.test.tsx
@@ -48,13 +48,13 @@ describe('Notifications Toggle', () => {
const clickElement = async (testId: string) => {
await act(async () => {
- fireEvent.click(screen.getByTestId(testId));
+ fireEvent.click(await screen.findByTestId(testId));
});
};
const waitForElement = async (testId: string) => {
- await waitFor(() => {
- expect(screen.getByTestId(testId)).toBeInTheDocument();
+ await waitFor(async () => {
+ expect(await screen.findByTestId(testId)).toBeInTheDocument();
});
};
@@ -73,12 +73,12 @@ describe('Notifications Toggle', () => {
await clickElement('notifications-settings-button');
await waitForElement('notifications-settings-allow-notifications');
- const toggleSection = screen.getByTestId(
+ const toggleSection = await screen.findByTestId(
'notifications-settings-allow-notifications',
);
await act(async () => {
- fireEvent.click(within(toggleSection).getByRole('checkbox'));
+ fireEvent.click(await within(toggleSection).findByRole('checkbox'));
});
await waitFor(() => {
@@ -159,7 +159,7 @@ describe('Notifications Toggle', () => {
await clickElement('notifications-settings-button');
await waitForElement('notifications-settings-allow-notifications');
- const allToggles = screen.getAllByTestId('test-toggle');
+ const allToggles = await screen.findAllByTestId('test-toggle');
await act(async () => {
fireEvent.click(allToggles[1]);
diff --git a/test/integration/onboarding/wallet-created.test.tsx b/test/integration/onboarding/wallet-created.test.tsx
index 55be476839fe..c1ddb1f1886a 100644
--- a/test/integration/onboarding/wallet-created.test.tsx
+++ b/test/integration/onboarding/wallet-created.test.tsx
@@ -31,14 +31,15 @@ describe('Wallet Created Events', () => {
});
it('are sent when onboarding user who chooses to opt in metrics', async () => {
- const { getByTestId, getByText } = await integrationTestRender({
- preloadedState: mockMetaMaskState,
- backgroundConnection: backgroundConnectionMocked,
- });
+ const { getByTestId, findByTestId, getByText, findByText } =
+ await integrationTestRender({
+ preloadedState: mockMetaMaskState,
+ backgroundConnection: backgroundConnectionMocked,
+ });
- expect(getByText('Congratulations!')).toBeInTheDocument();
+ expect(await findByText('Congratulations!')).toBeInTheDocument();
- fireEvent.click(getByTestId('onboarding-complete-done'));
+ fireEvent.click(await findByTestId('onboarding-complete-done'));
await waitFor(() => {
expect(getByTestId('onboarding-pin-extension')).toBeInTheDocument();
@@ -69,7 +70,7 @@ describe('Wallet Created Events', () => {
]),
);
- fireEvent.click(getByTestId('pin-extension-next'));
+ fireEvent.click(await findByTestId('pin-extension-next'));
let onboardingPinExtensionMetricsEvent;
@@ -91,7 +92,7 @@ describe('Wallet Created Events', () => {
).toBeInTheDocument();
});
- fireEvent.click(getByTestId('pin-extension-done'));
+ fireEvent.click(await findByTestId('pin-extension-done'));
await waitFor(() => {
const completeOnboardingBackgroundRequest =
diff --git a/ui/components/multichain/pages/index.js b/ui/components/multichain/pages/index.js
deleted file mode 100644
index a19f23039138..000000000000
--- a/ui/components/multichain/pages/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export { Connections } from './connections';
-export { PermissionsPage } from './permissions-page/permissions-page';
-export { ReviewPermissions, SiteCell } from './review-permissions-page';
diff --git a/ui/components/multichain/pages/review-permissions-page/index.js b/ui/components/multichain/pages/review-permissions-page/index.js
deleted file mode 100644
index e2da178368f1..000000000000
--- a/ui/components/multichain/pages/review-permissions-page/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ReviewPermissions } from './review-permissions-page';
-export { SiteCell } from './site-cell/site-cell';
diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx
index b2da4553ce50..a886d26e77e6 100644
--- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.stories.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { ReviewPermissions } from '.';
+import { ReviewPermissions } from './review-permissions-page';
export default {
title: 'Components/Multichain/ReviewPermissions',
diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx
index b644c16b6440..55f7ab9bf332 100644
--- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.test.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { renderWithProvider } from '../../../../../test/jest/rendering';
import mockState from '../../../../../test/data/mock-state.json';
import configureStore from '../../../../store/store';
-import { ReviewPermissions } from '.';
+import { ReviewPermissions } from './review-permissions-page';
const render = (state = {}) => {
const store = configureStore({
diff --git a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
index f65dd7a662cf..95a8ea394000 100644
--- a/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
+++ b/ui/components/multichain/pages/review-permissions-page/review-permissions-page.tsx
@@ -54,7 +54,7 @@ import { PermissionsHeader } from '../../permissions-header/permissions-header';
import { mergeAccounts } from '../../account-list-menu/account-list-menu';
import { MergedInternalAccount } from '../../../../selectors/selectors.types';
import { TEST_CHAINS } from '../../../../../shared/constants/network';
-import { SiteCell } from '.';
+import { SiteCell } from './site-cell/site-cell';
export const ReviewPermissions = () => {
const t = useI18nContext();
diff --git a/ui/helpers/utils/mm-lazy.ts b/ui/helpers/utils/mm-lazy.ts
new file mode 100644
index 000000000000..e31c22dfc99a
--- /dev/null
+++ b/ui/helpers/utils/mm-lazy.ts
@@ -0,0 +1,86 @@
+import React from 'react';
+// eslint-disable-next-line import/no-restricted-paths
+import { getManifestFlags } from '../../../app/scripts/lib/manifestFlags';
+import { endTrace, trace, TraceName } from '../../../shared/lib/trace';
+
+type DynamicImportType = () => Promise<{ default: React.ComponentType }>;
+type ModuleWithDefaultType = {
+ default: React.ComponentType;
+};
+
+// This only has to happen once per app load, so do it outside a function
+const lazyLoadSubSampleRate = getManifestFlags().sentry?.lazyLoadSubSampleRate;
+
+/**
+ * A wrapper around React.lazy that adds two things:
+ * 1. Sentry tracing for how long it takes to load the component (not render, just load)
+ * 2. React.lazy can only deal with default exports, but the wrapper can handle named exports too
+ *
+ * @param fn - an import of the form `() => import('AAA')`
+ */
+export function mmLazy(fn: DynamicImportType) {
+ return React.lazy(async () => {
+ // We can't start the trace here because we don't have the componentName yet, so we just hold the startTime
+ const startTime = Date.now();
+
+ const importedModule = await fn();
+ const { componentName, component } = parseImportedComponent(importedModule);
+
+ // Only trace load time of lazy-loaded components if the manifestFlag is set, and then do it by Math.random probability
+ if (lazyLoadSubSampleRate && Math.random() < lazyLoadSubSampleRate) {
+ trace({
+ name: TraceName.LazyLoadComponent,
+ data: { componentName },
+ startTime,
+ });
+
+ endTrace({ name: TraceName.LazyLoadComponent });
+ }
+
+ return component;
+ });
+}
+
+// There can be a lot of different types here, and we're basically doing type-checking in the code,
+// so I don't think TypeScript safety on `importedModule` is worth it in this function
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function parseImportedComponent(importedModule: any): {
+ componentName: string; // TODO: in many circumstances, the componentName gets minified
+ component: ModuleWithDefaultType;
+} {
+ let componentName: string;
+
+ // If there's no default export
+ if (!importedModule.default) {
+ const keys = Object.keys(importedModule);
+
+ // If there's only one named export
+ if (keys.length === 1) {
+ componentName = keys[0];
+
+ return {
+ componentName,
+ // Force the component to be the default export
+ component: { default: importedModule[componentName] },
+ };
+ }
+
+ // If there are multiple named exports, this isn't good for tree-shaking, so throw an error
+ throw new Error(
+ 'mmLazy: You cannot lazy-load a component when there are multiple exported components in one file',
+ );
+ }
+
+ if (importedModule.default.WrappedComponent) {
+ // If there's a wrapped component, we don't want to see the name reported as `withRouter(Connect(AAA))` we want just `AAA`
+ componentName = importedModule.default.WrappedComponent.name;
+ } else {
+ componentName =
+ importedModule.default.name || importedModule.default.displayName;
+ }
+
+ return {
+ componentName,
+ component: importedModule,
+ };
+}
diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js
index ef08fb6bbba1..f5858e853dd1 100644
--- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js
+++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.component.js
@@ -133,9 +133,7 @@ const ConfirmTransaction = () => {
});
useEffect(() => {
- if (!totalUnapproved && !sendTo) {
- history.replace(mostRecentOverviewPage);
- } else {
+ if (totalUnapproved || sendTo) {
const { txParams: { data } = {}, origin } = transaction;
if (origin !== ORIGIN_METAMASK) {
diff --git a/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js b/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js
index fbd8840de4ec..3c374fd7d0c0 100644
--- a/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js
+++ b/ui/pages/confirmations/confirm-transaction/confirm-transaction.test.js
@@ -270,26 +270,5 @@ describe('Confirmation Transaction Page', () => {
expect(replaceSpy).not.toHaveBeenCalled();
});
});
-
- describe('when no unapproved transactions and no sendTo recipient exist', () => {
- it('should call history.replace(mostRecentOverviewPage)', () => {
- const mockStore = configureMockStore(middleware)({
- ...mockState,
- metamask: {
- ...mockState.metamask,
- transactions: [],
- },
- });
- const replaceSpy = jest.fn();
- jest.spyOn(ReactRouterDOM, 'useHistory').mockImplementation(() => {
- return {
- replace: replaceSpy,
- };
- });
-
- renderWithProvider(, mockStore, '/asdfb');
- expect(replaceSpy).toHaveBeenCalled();
- });
- });
});
});
diff --git a/ui/pages/permissions-connect/connect-page/connect-page.tsx b/ui/pages/permissions-connect/connect-page/connect-page.tsx
index ba9bcc6bf674..99791a8d5333 100644
--- a/ui/pages/permissions-connect/connect-page/connect-page.tsx
+++ b/ui/pages/permissions-connect/connect-page/connect-page.tsx
@@ -22,7 +22,7 @@ import {
Header,
Page,
} from '../../../components/multichain/pages/page';
-import { SiteCell } from '../../../components/multichain/pages/review-permissions-page';
+import { SiteCell } from '../../../components/multichain/pages/review-permissions-page/site-cell/site-cell';
import {
BackgroundColor,
BlockSize,
diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js
index bce88a9f9236..206998cf82ba 100644
--- a/ui/pages/routes/routes.component.js
+++ b/ui/pages/routes/routes.component.js
@@ -1,27 +1,12 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
-import React, { Component } from 'react';
+import React, { Component, Suspense } from 'react';
import { matchPath, Route, Switch } from 'react-router-dom';
import IdleTimer from 'react-idle-timer';
-import Swaps from '../swaps';
-import ConfirmTransaction from '../confirmations/confirm-transaction';
-import Home from '../home';
-import {
- PermissionsPage,
- Connections,
- ReviewPermissions,
-} from '../../components/multichain/pages';
-import Settings from '../settings';
import Authenticated from '../../helpers/higher-order-components/authenticated';
import Initialized from '../../helpers/higher-order-components/initialized';
-import Lock from '../lock';
import PermissionsConnect from '../permissions-connect';
-import RestoreVaultPage from '../keychains/restore-vault';
-import RevealSeedConfirmation from '../keychains/reveal-seed';
-import ConfirmAddSuggestedTokenPage from '../confirm-add-suggested-token';
-import CreateAccountPage from '../create-account/create-account.component';
-import ConfirmAddSuggestedNftPage from '../confirm-add-suggested-nft';
import Loading from '../../components/ui/loading-screen';
import LoadingNetwork from '../../components/app/loading-network-screen';
import { Modal } from '../../components/app/modals';
@@ -34,15 +19,8 @@ import {
ImportNftsModal,
ImportTokensModal,
} from '../../components/multichain';
-import UnlockPage from '../unlock-page';
import Alerts from '../../components/app/alerts';
-import Asset from '../asset';
import OnboardingAppHeader from '../onboarding-flow/onboarding-app-header/onboarding-app-header';
-import Notifications from '../notifications';
-import NotificationsSettings from '../notifications-settings';
-import NotificationDetails from '../notification-details';
-import SnapList from '../snaps/snaps-list';
-import SnapView from '../snaps/snap-view';
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
import InstitutionalEntityDonePage from '../institutional/institutional-entity-done-page';
import InteractiveReplacementTokenNotification from '../../components/institutional/interactive-replacement-token-notification';
@@ -95,8 +73,6 @@ import {
// TODO: Remove restricted import
// eslint-disable-next-line import/no-restricted-paths
import { getEnvironmentType } from '../../../app/scripts/lib/util';
-import ConfirmationPage from '../confirmations/confirmation';
-import OnboardingFlow from '../onboarding-flow/onboarding-flow';
import QRHardwarePopover from '../../components/app/qr-hardware-popover';
import DeprecatedNetworks from '../../components/ui/deprecated-networks/deprecated-networks';
import NewNetworkInfo from '../../components/ui/new-network-info/new-network-info';
@@ -107,13 +83,11 @@ import { BasicConfigurationModal } from '../../components/app/basic-configuratio
import KeyringSnapRemovalResult from '../../components/app/modals/keyring-snap-removal-modal';
///: END:ONLY_INCLUDE_IF
-import { SendPage } from '../../components/multichain/pages/send';
import { DeprecatedNetworkModal } from '../settings/deprecated-network-modal/DeprecatedNetworkModal';
import { MultichainMetaFoxLogo } from '../../components/multichain/app-header/multichain-meta-fox-logo';
import NetworkConfirmationPopover from '../../components/multichain/network-list-menu/network-confirmation-popover/network-confirmation-popover';
-import NftFullImage from '../../components/app/assets/nfts/nft-details/nft-full-image';
-import CrossChainSwap from '../bridge';
import { ToastMaster } from '../../components/app/toast-master/toast-master';
+import { mmLazy } from '../../helpers/utils/mm-lazy';
import { InternalAccountPropType } from '../../selectors/multichain';
import { isCurrentChainCompatibleWithAccount } from '../../../shared/lib/multichain';
import {
@@ -128,6 +102,54 @@ import {
showOnboardingHeader,
} from './utils';
+// Begin Lazy Routes
+const OnboardingFlow = mmLazy(() =>
+ import('../onboarding-flow/onboarding-flow'),
+);
+const Lock = mmLazy(() => import('../lock'));
+const UnlockPage = mmLazy(() => import('../unlock-page'));
+const RestoreVaultPage = mmLazy(() => import('../keychains/restore-vault'));
+const RevealSeedConfirmation = mmLazy(() => import('../keychains/reveal-seed'));
+const Settings = mmLazy(() => import('../settings'));
+const NotificationsSettings = mmLazy(() => import('../notifications-settings'));
+const NotificationDetails = mmLazy(() => import('../notification-details'));
+const Notifications = mmLazy(() => import('../notifications'));
+const SnapList = mmLazy(() => import('../snaps/snaps-list'));
+const SnapView = mmLazy(() => import('../snaps/snap-view'));
+const ConfirmTransaction = mmLazy(() =>
+ import('../confirmations/confirm-transaction'),
+);
+const SendPage = mmLazy(() => import('../../components/multichain/pages/send'));
+const Swaps = mmLazy(() => import('../swaps'));
+const CrossChainSwap = mmLazy(() => import('../bridge'));
+const ConfirmAddSuggestedTokenPage = mmLazy(() =>
+ import('../confirm-add-suggested-token'),
+);
+const ConfirmAddSuggestedNftPage = mmLazy(() =>
+ import('../confirm-add-suggested-nft'),
+);
+const ConfirmationPage = mmLazy(() => import('../confirmations/confirmation'));
+const CreateAccountPage = mmLazy(() =>
+ import('../create-account/create-account.component'),
+);
+const NftFullImage = mmLazy(() =>
+ import('../../components/app/assets/nfts/nft-details/nft-full-image'),
+);
+const Asset = mmLazy(() => import('../asset'));
+const PermissionsPage = mmLazy(() =>
+ import('../../components/multichain/pages/permissions-page/permissions-page'),
+);
+const Connections = mmLazy(() =>
+ import('../../components/multichain/pages/connections'),
+);
+const ReviewPermissions = mmLazy(() =>
+ import(
+ '../../components/multichain/pages/review-permissions-page/review-permissions-page'
+ ),
+);
+const Home = mmLazy(() => import('../home'));
+// End Lazy Routes
+
export default class Routes extends Component {
static propTypes = {
currentCurrency: PropTypes.string,
@@ -271,122 +293,127 @@ export default class Routes extends Component {
const RestoreVaultComponent = forgottenPassword ? Route : Initialized;
const routes = (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- ///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
- }
-
-
-
-
-
-
- {
- ///: END:ONLY_INCLUDE_IF
- }
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {/* since the loading time is less than 200ms, we decided not to show a spinner fallback or anything */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ ///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
+ }
+
+
+
+
+
+
+ {
+ ///: END:ONLY_INCLUDE_IF
+ }
+
+
+
+
+
+
+
+
+
+
+
+
);
if (autoLockTimeLimit > 0) {
diff --git a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js
index e162f84ba40f..1983a0143174 100644
--- a/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js
+++ b/ui/pages/settings/contact-list-tab/add-contact/add-contact.test.js
@@ -178,12 +178,15 @@ describe('AddContact component', () => {
expect(saveButton).toBeDisabled();
});
- it('should display error message when name entered is an existing account name', () => {
+ it('should display error message when name entered is an existing account name', async () => {
const duplicateName = 'Account 1';
const store = configureMockStore(middleware)(state);
- const { getByText } = renderWithProvider(, store);
+ const { getByText, findByText } = renderWithProvider(
+ ,
+ store,
+ );
const nameInput = document.getElementById('nickname');
@@ -191,7 +194,7 @@ describe('AddContact component', () => {
const saveButton = getByText('Save');
- expect(getByText('Name is already in use')).toBeDefined();
+ expect(await findByText('Name is already in use')).toBeDefined();
expect(saveButton).toBeDisabled();
});
@@ -212,10 +215,10 @@ describe('AddContact component', () => {
expect(saveButton).toBeDisabled();
});
- it('should display error when ENS inserts a name that is already in use', () => {
+ it('should display error when ENS inserts a name that is already in use', async () => {
const store = configureMockStore(middleware)(state);
- const { getByTestId, getByText } = renderWithProvider(
+ const { getByTestId, getByText, findByText } = renderWithProvider(
,
store,
);
@@ -231,7 +234,7 @@ describe('AddContact component', () => {
const saveButton = getByText('Save');
- expect(getByText('Name is already in use')).toBeDefined();
+ expect(await findByText('Name is already in use')).toBeDefined();
expect(saveButton).toBeDisabled();
});
});
From 652afc3961b7e038b2560af282bda395425208d5 Mon Sep 17 00:00:00 2001
From: Priya
Date: Mon, 25 Nov 2024 22:06:52 +0700
Subject: [PATCH 15/40] test: blockaid e2e test for contract interaction
(#28156)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## **Description**
[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28156?quickstart=1)
## **Related issues**
Fixes:
[#2802](https://github.com/MetaMask/MetaMask-planning/issues/2802)
## **Manual testing steps**
1. Go to this page...
2.
3.
## **Screenshots/Recordings**
### **Before**
### **After**
## **Pre-merge author checklist**
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.
---
.../mock-cdn/cdn-stale-diff-res-headers.json | 2 +-
test/e2e/mock-cdn/cdn-stale-diff.txt | Bin 170935 -> 965977 bytes
test/e2e/mock-cdn/ppom-version-headers.json | 2 +-
test/e2e/mock-cdn/ppom-version.json | 306 +++++++++---------
.../tests/ppom/mocks/mock-server-json-rpc.ts | 15 +-
...lockaid-alert-contract-interaction.spec.js | 238 ++++++++++++++
6 files changed, 406 insertions(+), 157 deletions(-)
create mode 100644 test/e2e/tests/ppom/ppom-blockaid-alert-contract-interaction.spec.js
diff --git a/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json b/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json
index 0fb0ec0f7d89..786436052a14 100644
--- a/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json
+++ b/test/e2e/mock-cdn/cdn-stale-diff-res-headers.json
@@ -1,4 +1,4 @@
{
"Content-Type": "text/plain",
- "Etag": "W/\"5ae8a43f84ccd89e8ddc79b1dfed0035\""
+ "Etag": "W/\"24396be8aa89668e00ba9f795d7753e6\""
}
diff --git a/test/e2e/mock-cdn/cdn-stale-diff.txt b/test/e2e/mock-cdn/cdn-stale-diff.txt
index 44eb67f85fa4afa1acd35bbdc3457e97dc8c30bc..9d9b4e83e97bc56900c292790947c415c00dd25a 100644
GIT binary patch
literal 965977
zcmV(pK=8kK+7z2-JeGeO#!2X~Ls`kl9to8Z8Bw-GX72AtbAkk&*08
zwv0%_&MG4#kLT_E=Dx1;cmB@vIF9ejc1o;dcs*|zQAJA>5
zeXLvS@DUq7(qck{P|`&>2M
zc+o#Oq|&@>n>{B|0Ui7ESL;g|3BhZq%(^{0MGgL6VtbkWn0fOC2WC6R-n8krc1&
z@E>8E^d-69Op*|ac})pN<-98@*wZ4G59nSChUJIEg)Kd9W~jS-^t^GNKm>Q!Z7Aqy
zZNCB<6oHzlzegaIuJXs%j_x7sw+q`@=hv1{diValF1M?1AzgExf?V!YAZ{6~{x~G4
z;(@2k`u~K_>t2BdU%#fqv~dHfG(wwxmprZq^Q=eh)x>L;@zTAJZouHj2CB9rASCPwO*1>sOMK$hT;d`hGeDABHIzoxt;SMoRQeLj&R@~CTlGV6*
zs7qQjDpg>pViSsXyXtKpdL(cc0%Qln4KpOw1^;jGIpGz7W|pb#xB{dV892jk-GWG(~T5m29x
zcX8QBxD2D62U1KyIc=zxH=EXCEB*-%@16mQ^1PRDy#6?+Z{puy$l>f$lb|Y36qAFZ
zWU0>qXI`Ip&9vY?jnYK5#@8)g)1bY?G83^UwTN&*yA?7|cTGHeEpqpP8X$1IuBt3dt^&~p7Oav9^nC-Flh|zHF
z`-f#bD|6FQ@4JzKf$1*sqq+tUFn;x6k&vKQAULHYB6ETeAw(8@!L^&+HPY^Ks
zN}>+&Qc=s_&Y_KPFTcSl>T)p^%zaNDJN$~N!dlU-zGD^azp%7LPD2pLaU293Ze6Xx
zYc?P&_Vl|GS0aMn?PMbHmv3yt_`i68`=Vosu$ZnF)VSLB0yRTg4Tj)7cEWYRD9{RLF!GcCG$|9Sw|N=089dtWx}y2mM0rKbJE
z;-1{yde*yV;Y|2INYYm0D%d(MQ&}DU6NbU^Q^!K)HS=(n^_9i_aOYH{z4GFoJLR1Y
zT0H|s9>$+9p;|{2Bx-bZ6EfuA95WA;a^ui&S#oZ{EqT24nU6X2l4=ur9&9OOA@Kzm
z4k95Vy}IFt(4A&o3|uD2!kt@xnj-BMk3qbFWvEt#={*LYY5n78q&SZ^
z^LK;!<*VOfBf&8_`N!uqoG9s}A>%vS3M-HC;&aY>m7tB49yQS3IEKmu2Rb>KU?coD
zuhU0Q%km!tGqx{Jrhfelj)^U`bH|Ciaq+%SXL(EoU|LVK_+aq%6I^GsW;h(kHG-BI
z|39z)8h?OUh>Z%H_N{{;EF_=QY>TLd+ocs+{@Rq0-gEak=SBlbv&gn
zUawY&J&q?tPxUsVpJjmlw$umqHYZOwd(Oo9p3Ut9$z}ODtpm=F5qMWDoWnMt0#z{)bA*7*G
z$xlO+j;mrPDEj5Ey@SbZVeJ_5yD|7BcxJ`MwYDGYwKwL1wW`~Z_o76EOn~DL_?D_!
z_ohgxvE2O5@bitl81##8{P!^9+Z&{;1#I07j@LqIexzR4>u2N;Sed`X*d}}*a?f0t
zrJDN{F!0#R+nU9|3)-s3RXs_r=HPdu-K+Q0sn;Qtup$v~vzH3Tn*K6t?yY~qam!1)
zBus>SFbog`i{I0hp@pYrz?`f;j6Ft3BF2IFZ^4vc?B-h2T>uCO^py;qn_yh
zd8QJ__{sTL)_&i?5?k}jWD#{vB;R^)Z-~nAFD%n|q{yZzGa;E$W?_|mr9|1$yt9-rM39-jLG
zyKkpndamaA!A49p%G#GX1tf=OU*Bk6<-ipmowVC|eEWs6tln{}R_g=Je@>z5iSIw-
zD(x+0!_c07P&0Y
z3rV}Pau_$lDK;eZT9!aDaviwr-kaW1ht8LKGA#X9$ncz;FjqUtsuYs9b^YsRwx*y&
z!KO-_PiBiCU7J7tO7ud=<{W%*%sJ>As>Y89g#_e<;%5wKdto7Eru6d)21rIWZbE
zZA&sZm44vL)8eh@!fXWyyNygx|5ss+qR}KnzVM^+&>4R<7c(V1j@^%P1I!KWHQ*3<
zdYLcbJ~epH{P%8Ah#?nNbUzZGcUwwfnaE|2_VN2hDBVBNMX!42JGSl~EE4uK*2CeT
z%ARLI9jB1~ALoq=p-EqHY*jP*;hWD^C~p*f(eq+<|KJ$yh)*UP)}Y(P>yP2{jHQRFFF#@cQ9Z68O6?7B!^e@
z##@r7fWzFVl>HOJapLJSqMy85PS|uJt4OZZWx_w*f^?Pd{FhL~P#Ag2=`ixN-bO80FnNcqJH)OG6XlA{#VK>!;FpxQgj7B9{HlYPNXlM
z*Xb$V<~b8`iVd1MdWwC0QrErIb_t;cwauK#4d$ayLsPKaKk$;#vtLI8(`n69NhOzLAmzFeAbf_M
z5@J;6izi~#=J4v(j%5~W>m1VKS+xwF;~9SKIP#99OZ-Lnac$8ePhJOnXur*&CSI9}
z;}SOof;m5=!{v!irM`%#1=3q3A}WR+0VVO0<(3)m$#H(Vj^$FUE;C+IkXe&iuAhdk
zvnuy?n%@}Y#@=;lcRBH+&TBPn{&rF@)NI+`AqqcE^4=-!KUadR9YXw2k
zZH^NkmbGBD!9Lggsz4fvZ84fw=UFa+$s%QGt1?F&Um4bOX_;idbF$00#YCOh+(NO2`EmxEZ`omSFeVbs
z{19;V4DP0gJn;>#`hAx(wFXHJe)MwGlkB*kHi>dEG>z#g@LCp0|l$>pP`EH~;AL#>|)~gH>dEnIE(h9w?YYON8?kI6p
z#@xZK?uFDns{@Bn-}CI6NaC#k{CZlKerxy%H-s0NGwFZipRzq=
za_$>0dLAh5eH1Nnnt
zr{ZZ*)(l+**&rfGewL($&pYAH#xsYry~jJTYsFlqU#&C-_JWC1Z?Z{cFc(Ebo&Hq3
z12OF`1Mwfp3UOG&+s~QLog727G)+&^X{~XYHtWZ(j=>8AEjis~8s)WxGrjtwZzYT*
zQ1m<|E6duHfrCk$;2Ole-I+T@r{OTPS
z2)!+qr5#vbK_1EcktPRaHtgjIzaBkAB?r1pt|{7!tG@UUc%iX|r-lG5kDM$rO*wzz
zbw8uYcfyG^%$k>slU3Bdz{FIguODMz1Eyx)TWiI)_CfJarfR7ha~CEYq?voREkDC3
zR6uutzLFY=sU@48wfm#;$HXnZOsbOv7*s@TU-Zm6sXQA`I3g9ky}QYYK~sa&92gF35>Yo3^H-fjr
zRsnWAtgfIReRoaw-N6)uO{%5;>WR!i8SQbovFMg89GY-1DZ0+Wk4*)ttRp!WDlqJm
z!+J~0BnI=>gn3>bQ)ociuHV0uWMNeZu}i)0uCz0Q_^Rfox%BBlH1NsUM-6^@f#;h4
zy`x_y=>r$v^@*@I%r`N_v#3LOr||{y&YCZijD@jc!qstx5VobV*K2s3R$-@(wFJJ3zpS0m(i4#>=i>m{4nJN;Cf;_Jwr##`tlYrh-+&rXp
zmST!C9V7Pay(FoBc+hFkS@^-e>jy0U&aFmuRX0Jfw*Ajwc=ScAUnX_do-hhOBEevl03pz_ml!tETV!+7l#Fxs+1
zND7*k#wXTgt;-M=G80{Kd^`z~x#CkDDN)*RcWxhl`a8fG1t0q=+J89H!d%Oa!{gC8
zTG)sa^`0ai8^XGK1Czjd+$S8C?ev>dWavX{n3v(g{Vh#sTLon7Rz6IFsAYnIgaBa=
z{xR=(DZsgq4|;(OLcO#I1u;b6~rg?=yGcQWJ8h`I{pihJHkf
zk7h3G!P711^hov)IQ!rtp6f4ANV|Qf0Rv@eXu>&rQvBd}!duSZeIFb1$CAy2l0G7`
ztL2gV?}i}!p-+CP{q%AfScu4eAJhTxMIb67Uqj|1K7DA;%NX<|#&ve#_#3*RCaC;)
zEo6GDM-GIs&tIC94A6%g#=uImF*lUz}8jAbzwm
zXRquzGwv2;eo8SCb^~)E%hlGKM3G=2puMS=5c&`mGGAJ===Q@aH7%+ag!isk!|9LW
zgEXZFyO3?KzDMA5+82@LdS5n%CybG6r2)hG=loC%kYK&Ra`O>-AMD76XmYyx=ulxO_MlCQdhZ|A;QDL-ZAOyY7`(Z)^<7Htx~XYoNI7si~2nUnI)L1&R2l
z+Vqg4S)eO8N5u+G23EOg-v|ao=r*4o7f1kh2|rhn4qRWw#+>KS$v>_BNFUYITacAK
z1^T3tgOP`M^UzEC+;=$rmMbX@d>#S+md=Xg
zMPi+JQii7EcO@8E_zU@8pK3(yd!l<2xg;N8Z)iWk`|i>ayt&SL#KP`hDMU{b&JjNC
zmVx4pckgqFV|t(?bHrAL@ewtg6EoXH*h*FL)I%{xF?6FCEn>Y#&$PAo;NTjYS{sQ0
zKOWrq-dN=rOAGJDHVJx;mLDMLXT5hJr7;vNaso|$3?(n2H7woZ?lvrf0S3ZHnL@_X
zkd?byP4i?U5*A;AiM~@`qsEhYc4vnI)k#cOvZ?s<3b*1Y_q`LH2Rp)H>HWKp@W1&s
zyv|w}Nq(?}L*p3ITs+Obe-T#6n#B)D4SOINfTni>vrViKyV(-n$d6lQ86!WO?1ElaJss
z^|-%plf_{)&&5xrS$zdWlqApGarh=gyZqK@w9I9(Tu$+4b%VtnM;Vt`?sX8qhUM9x
zZdty*DcGsOoi`}-Dwfxn#c_&NfRo+5aObc%roMa*7;
zZj-r+$Z_a8{g@);6vCAs+w%TCNd`*!J8q?q$D;9fE6(!L%Wz_pr_fBZ5%Hd9V
z9*QXOor>$?h}s@Bl`&g72_DCb7Hf1V`B=@!~ymk10o^qKECGI5tK27iyxm~O
zdX@NB*!Vh5-ZA~un)_!HTxua7yJt&>pwz~jaqnYBJ#MqjbFqGPD**8=1%6Sp9&fPi
zDYB3g$_MP*Ng~(As3Y@mICA!F9)F=24uxoy)&BdLk08UI_y?B6+?e$@&zG3!H-@f_
z*ahEzw#jTzvo8-GO(o9nF#!&y*6v%YP54%{IqrkYIes
zj@Li47rtYJX8zOL-0)45Z0!FKP7Nj1;1d1|V}IZvc|HEfrCa_$Q%ZQyf~zl1>F{*9
zU&<~A7M5@tWyCzayZr0+4a51
zxgD_{r8innht=W@^W%OGvREl>?GVH$9n_csBhx*}zALLvIHOa(V?&@ezyGGVWNA#G
zfLboqgZ&jm76=rKWPe-7?SVnUvM4ixcy^GaXDdb6X)Z#geR<2=U%L}@KKYUiH~8H^
z7t42c#CtpsrZ1-clWg5g$8DEtRS~V$+we(0YGe6%LKYU+jguEPZZ{%Xk@MT>GZONU
zYg;Vuo^PZ@fkLPxAFH7pdKuR=C*^C>;P|Mo%C5`m62#v5yHZ5wokM%ywWS5si#aIQ
zxHywZX3UG>=0C3@os_?z$G?g2b;HORL^3$Lt3S>02I0^-I;@65Lavfa-Nk{ivx$EWW*WdZo?LdP`O8VT5oo1vKh0FYeD%7T
zN}a|52>PF>pfnBX!{R~9KURT5jYtTuGRYc{bpXdAPqdmpSug%MbSR$M%UVFq)LQZJ
zj?H4IJN_G2%cQg3@8{OplX<;E=y-ecv=uwAB#N`s?(%(oRtI(E?;ZZW-J@`j@_&5f
z<32Ewl_IhakzH0sB-^F)6Xf=*fO_l2K++2wh|IDN5{-9wi}@`t5~i?k{7C+lnx{ps
zcN~`z6D~3>-g|@E=F)`jrM%BbsQmcs%7ujjBn&ExuqG(k;e`K0&U1#j^U&=$a!F42
z76)9OS{1ePTc3dxv3JQ?OWr)hk00M}~43uyoMAakMr
zzB8nvs6D8*zkNd0`nATi##ACaxtQ%Dl-ffC8Sn)DH{0+WaWo1dbk##+xRhIMeLAi1
z1NvV4d_-SVya=JchlH|wm+xS!{;hYk#NP>n)GN2f*|Fo=@e1;Txnpny?wHt
zq!h4neP6sCE37@N`cj1(0ewH|WpZjgacuaZx}vL-IbM}=-v3i5HjU0}Ql>xJ-pGOP
zqASHA;Z-Z7Yd?;@c|fKWN+(PGcJ0nG;