Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: sort and display all bridge quotes #27731

Merged
merged 25 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
24dbe30
chore: fetch dest token exchange rates and save to bridge state
micaelae Oct 24, 2024
832dc61
chore: fetch src token exchange rate
micaelae Nov 5, 2024
359a5b2
chore: calculate quote metadata in selectors
micaelae Oct 25, 2024
7fc8ec0
chore: add L1 fees for Base and Optimism
micaelae Nov 14, 2024
612c33b
chore: display quote metadata
micaelae Oct 25, 2024
25b7602
chore: update bridge quote card
micaelae Oct 23, 2024
102a229
chore: format token amounts
micaelae Oct 25, 2024
93d41cf
chore: sort quotes on header click
micaelae Oct 25, 2024
4760e44
chore: reusable layout wrappers for Bridge page
micaelae Oct 31, 2024
e715137
chore: set fromToken when navigating from asset
micaelae Nov 6, 2024
04faf4d
chore: show all quotes in a modal
micaelae Oct 23, 2024
7c24aec
chore: enable selecting alternative quote
micaelae Oct 24, 2024
211c0ca
chore: style quotes modal
micaelae Nov 13, 2024
6ba7918
fix: lint errors
micaelae Nov 13, 2024
a77ab55
fix: unit tests
micaelae Nov 13, 2024
bff17b7
fix: stringify exchange rates before multiplying
micaelae Nov 19, 2024
067af93
chore: move header icon to end
micaelae Nov 20, 2024
5b9d431
fix: scroll in bridge-quotes-modal
micaelae Nov 20, 2024
90a4262
chore: rename raw to amount
micaelae Nov 20, 2024
6377068
chore: add comment to clarify fiat currency
micaelae Nov 20, 2024
35a38b3
chore: rename quote identifier function
micaelae Nov 20, 2024
aca3fe2
chore: use template string for route path
micaelae Nov 20, 2024
19d916a
chore: include gasFees in metadata
micaelae Nov 21, 2024
3efe549
fix: remove toAmount rounding
micaelae Nov 22, 2024
47cdde0
refactor: share util method for fetching exchange rates
micaelae Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

183 changes: 174 additions & 9 deletions app/scripts/controllers/bridge/bridge-controller.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -35,13 +44,15 @@ jest.mock('@ethersproject/providers', () => {
Web3Provider: jest.fn(),
};
});
const getLayer1GasFeeMock = jest.fn();

describe('BridgeController', function () {
let bridgeController: BridgeController;

beforeAll(function () {
bridgeController = new BridgeController({
messenger: messengerMock,
getLayer1GasFee: getLayer1GasFeeMock,
});
});

Expand Down Expand Up @@ -278,15 +289,18 @@ describe('BridgeController', function () {
.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
resolve([1, 2, 3] as never);
resolve(mockBridgeQuotesNativeErc20Eth as never);
}, 5000);
});
});

fetchBridgeQuotesSpy.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
resolve([5, 6, 7] as never);
resolve([
...mockBridgeQuotesNativeErc20Eth,
...mockBridgeQuotesNativeErc20Eth,
] as never);
}, 10000);
});
});
Expand Down Expand Up @@ -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,
}),
);
Expand All @@ -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,
}),
Expand All @@ -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,
}),
Expand All @@ -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 () {
Expand All @@ -426,15 +447,18 @@ describe('BridgeController', function () {
.mockImplementationOnce(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
resolve([1, 2, 3] as never);
resolve(mockBridgeQuotesNativeErc20Eth as never);
}, 5000);
});
});

fetchBridgeQuotesSpy.mockImplementation(async () => {
return await new Promise((resolve) => {
return setTimeout(() => {
resolve([5, 6, 7] as never);
resolve([
...mockBridgeQuotesNativeErc20Eth,
...mockBridgeQuotesNativeErc20Eth,
] as never);
}, 10000);
});
});
Expand Down Expand Up @@ -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,
}),
Expand All @@ -519,14 +543,15 @@ describe('BridgeController', function () {
expect(bridgeController.state.bridgeState).toEqual(
expect.objectContaining({
quoteRequest: { ...quoteRequest, insufficientBal: true },
quotes: [1, 2, 3],
quotes: mockBridgeQuotesNativeErc20Eth,
quotesLoadingStatus: 1,
quotesRefreshCount: 1,
}),
);
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 () {
Expand Down Expand Up @@ -574,11 +599,151 @@ describe('BridgeController', function () {
address: '0x123',
provider: jest.fn(),
} as never);

const allowance = await bridgeController.getBridgeERC20Allowance(
'0x1f9840a85d5af5bf1d1762f925bdaddc4201f984',
'0xa',
);
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,
);
},
);
});
Loading
Loading