1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Swaps: Add conditional routing to new APIs based on a feature flag (#11470)

This commit is contained in:
Daniel 2021-07-09 17:24:00 +02:00 committed by GitHub
parent f51a8451b8
commit d438439661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 448 additions and 141 deletions

View File

@ -1266,6 +1266,9 @@
"networkNameEthereum": { "networkNameEthereum": {
"message": "Ethereum" "message": "Ethereum"
}, },
"networkNamePolygon": {
"message": "Polygon"
},
"networkNameTestnet": { "networkNameTestnet": {
"message": "Testnet" "message": "Testnet"
}, },

View File

@ -19,7 +19,6 @@ import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'
import { import {
fetchTradesInfo as defaultFetchTradesInfo, fetchTradesInfo as defaultFetchTradesInfo,
fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime, fetchSwapsQuoteRefreshTime as defaultFetchSwapsQuoteRefreshTime,
} from '../../../ui/pages/swaps/swaps.util'; } from '../../../ui/pages/swaps/swaps.util';
import { MINUTE, SECOND } from '../../../shared/constants/time'; import { MINUTE, SECOND } from '../../../shared/constants/time';
@ -73,6 +72,7 @@ const initialState = {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: true, swapsFeatureIsLive: true,
useNewSwapsApi: false,
swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME, swapsQuoteRefreshTime: FALLBACK_QUOTE_REFRESH_TIME,
}, },
}; };
@ -85,7 +85,6 @@ export default class SwapsController {
getProviderConfig, getProviderConfig,
tokenRatesStore, tokenRatesStore,
fetchTradesInfo = defaultFetchTradesInfo, fetchTradesInfo = defaultFetchTradesInfo,
fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness,
fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime,
getCurrentChainId, getCurrentChainId,
}) { }) {
@ -94,7 +93,6 @@ export default class SwapsController {
}); });
this._fetchTradesInfo = fetchTradesInfo; this._fetchTradesInfo = fetchTradesInfo;
this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness;
this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime;
this._getCurrentChainId = getCurrentChainId; this._getCurrentChainId = getCurrentChainId;
@ -119,15 +117,19 @@ export default class SwapsController {
// Sets the refresh rate for quote updates from the MetaSwap API // Sets the refresh rate for quote updates from the MetaSwap API
async _setSwapsQuoteRefreshTime() { async _setSwapsQuoteRefreshTime() {
const chainId = this._getCurrentChainId(); const chainId = this._getCurrentChainId();
const { swapsState } = this.store.getState();
// Default to fallback time unless API returns valid response // Default to fallback time unless API returns valid response
let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME;
try { try {
swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(chainId); swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(
chainId,
swapsState.useNewSwapsApi,
);
} catch (e) { } catch (e) {
console.error('Request for swaps quote refresh time failed: ', e); console.error('Request for swaps quote refresh time failed: ', e);
} }
const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, swapsQuoteRefreshTime }, swapsState: { ...swapsState, swapsQuoteRefreshTime },
}); });
@ -162,6 +164,9 @@ export default class SwapsController {
isPolledRequest, isPolledRequest,
) { ) {
const { chainId } = fetchParamsMetaData; const { chainId } = fetchParamsMetaData;
const {
swapsState: { useNewSwapsApi },
} = this.store.getState();
if (!fetchParams) { if (!fetchParams) {
return null; return null;
@ -182,7 +187,10 @@ export default class SwapsController {
this.indexOfNewestCallInFlight = indexOfCurrentCall; this.indexOfNewestCallInFlight = indexOfCurrentCall;
let [newQuotes] = await Promise.all([ let [newQuotes] = await Promise.all([
this._fetchTradesInfo(fetchParams, fetchParamsMetaData), this._fetchTradesInfo(fetchParams, {
...fetchParamsMetaData,
useNewSwapsApi,
}),
this._setSwapsQuoteRefreshTime(), this._setSwapsQuoteRefreshTime(),
]); ]);
@ -449,22 +457,23 @@ export default class SwapsController {
this.store.updateState({ swapsState: { ...swapsState, routeState } }); this.store.updateState({ swapsState: { ...swapsState, routeState } });
} }
setSwapsLiveness(swapsFeatureIsLive) { setSwapsLiveness(swapsLiveness) {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
const { swapsFeatureIsLive, useNewSwapsApi } = swapsLiveness;
this.store.updateState({ this.store.updateState({
swapsState: { ...swapsState, swapsFeatureIsLive }, swapsState: { ...swapsState, swapsFeatureIsLive, useNewSwapsApi },
}); });
} }
resetPostFetchState() { resetPostFetchState() {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { swapsState: {
...initialState.swapsState, ...initialState.swapsState,
tokens: swapsState.tokens, tokens: swapsState.tokens,
fetchParams: swapsState.fetchParams, fetchParams: swapsState.fetchParams,
swapsFeatureIsLive: swapsState.swapsFeatureIsLive, swapsFeatureIsLive: swapsState.swapsFeatureIsLive,
useNewSwapsApi: swapsState.useNewSwapsApi,
swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime, swapsQuoteRefreshTime: swapsState.swapsQuoteRefreshTime,
}, },
}); });
@ -473,7 +482,6 @@ export default class SwapsController {
resetSwapsState() { resetSwapsState() {
const { swapsState } = this.store.getState(); const { swapsState } = this.store.getState();
this.store.updateState({ this.store.updateState({
swapsState: { swapsState: {
...initialState.swapsState, ...initialState.swapsState,

View File

@ -128,13 +128,13 @@ const EMPTY_INIT_STATE = {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: true, swapsFeatureIsLive: true,
useNewSwapsApi: false,
swapsQuoteRefreshTime: 60000, swapsQuoteRefreshTime: 60000,
}, },
}; };
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
const fetchTradesInfoStub = sandbox.stub(); const fetchTradesInfoStub = sandbox.stub();
const fetchSwapsFeatureLivenessStub = sandbox.stub();
const fetchSwapsQuoteRefreshTimeStub = sandbox.stub(); const fetchSwapsQuoteRefreshTimeStub = sandbox.stub();
const getCurrentChainIdStub = sandbox.stub(); const getCurrentChainIdStub = sandbox.stub();
getCurrentChainIdStub.returns(MAINNET_CHAIN_ID); getCurrentChainIdStub.returns(MAINNET_CHAIN_ID);
@ -150,7 +150,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
@ -201,7 +200,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -226,7 +224,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -251,7 +248,6 @@ describe('SwapsController', function () {
getProviderConfig: MOCK_GET_PROVIDER_CONFIG, getProviderConfig: MOCK_GET_PROVIDER_CONFIG,
tokenRatesStore: MOCK_TOKEN_RATES_STORE, tokenRatesStore: MOCK_TOKEN_RATES_STORE,
fetchTradesInfo: fetchTradesInfoStub, fetchTradesInfo: fetchTradesInfoStub,
fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub,
getCurrentChainId: getCurrentChainIdStub, getCurrentChainId: getCurrentChainIdStub,
}); });
const currentEthersInstance = swapsController.ethersProvider; const currentEthersInstance = swapsController.ethersProvider;
@ -658,6 +654,7 @@ describe('SwapsController', function () {
const quotes = await swapsController.fetchAndSetQuotes(undefined); const quotes = await swapsController.fetchAndSetQuotes(undefined);
assert.strictEqual(quotes, null); assert.strictEqual(quotes, null);
}); });
it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () { it('calls fetchTradesInfo with the given fetchParams and returns the correct quotes', async function () {
fetchTradesInfoStub.resolves(getMockQuotes()); fetchTradesInfoStub.resolves(getMockQuotes());
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime());
@ -695,15 +692,15 @@ describe('SwapsController', function () {
metaMaskFeeInEth: '0.5050505050505050505', metaMaskFeeInEth: '0.5050505050505050505',
ethValueOfTokens: '50', ethValueOfTokens: '50',
}); });
assert.strictEqual( assert.strictEqual(
fetchTradesInfoStub.calledOnceWithExactly( fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS, {
MOCK_FETCH_PARAMS, ...MOCK_FETCH_METADATA,
MOCK_FETCH_METADATA, useNewSwapsApi: false,
), }),
true, true,
); );
}); });
it('performs the allowance check', async function () { it('performs the allowance check', async function () {
fetchTradesInfoStub.resolves(getMockQuotes()); fetchTradesInfoStub.resolves(getMockQuotes());
fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime()); fetchSwapsQuoteRefreshTimeStub.resolves(getMockQuoteRefreshTime());
@ -878,12 +875,14 @@ describe('SwapsController', function () {
const tokens = 'test'; const tokens = 'test';
const fetchParams = 'test'; const fetchParams = 'test';
const swapsFeatureIsLive = false; const swapsFeatureIsLive = false;
const useNewSwapsApi = false;
const swapsQuoteRefreshTime = 0; const swapsQuoteRefreshTime = 0;
swapsController.store.updateState({ swapsController.store.updateState({
swapsState: { swapsState: {
tokens, tokens,
fetchParams, fetchParams,
swapsFeatureIsLive, swapsFeatureIsLive,
useNewSwapsApi,
swapsQuoteRefreshTime, swapsQuoteRefreshTime,
}, },
}); });

View File

@ -5,10 +5,10 @@ module.exports = {
coveragePathIgnorePatterns: ['.stories.js', '.snap'], coveragePathIgnorePatterns: ['.stories.js', '.snap'],
coverageThreshold: { coverageThreshold: {
global: { global: {
branches: 45.45, branches: 45.24,
functions: 55.29, functions: 51.94,
lines: 60.22, lines: 58.36,
statements: 60.43, statements: 58.6,
}, },
}, },
setupFiles: ['./test/setup.js', './test/env.js'], setupFiles: ['./test/setup.js', './test/env.js'],

View File

@ -21,6 +21,7 @@ export const LOCALHOST_CHAIN_ID = '0x539';
export const BSC_CHAIN_ID = '0x38'; export const BSC_CHAIN_ID = '0x38';
export const OPTIMISM_CHAIN_ID = '0xa'; export const OPTIMISM_CHAIN_ID = '0xa';
export const OPTIMISM_TESTNET_CHAIN_ID = '0x45'; export const OPTIMISM_TESTNET_CHAIN_ID = '0x45';
export const POLYGON_CHAIN_ID = '0x89';
/** /**
* The largest possible chain ID we can handle. * The largest possible chain ID we can handle.

View File

@ -93,3 +93,7 @@ export const SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP = {
[BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL, [BSC_CHAIN_ID]: BSC_DEFAULT_BLOCK_EXPLORER_URL,
[MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL, [MAINNET_CHAIN_ID]: MAINNET_DEFAULT_BLOCK_EXPLORER_URL,
}; };
export const ETHEREUM = 'ethereum';
export const POLYGON = 'polygon';
export const BSC = 'bsc';

View File

@ -8,9 +8,21 @@
"mockMetaMetricsResponse": true "mockMetaMetricsResponse": true
}, },
"swaps": { "swaps": {
"featureFlag": { "featureFlags": {
"status": { "bsc": {
"active": true "mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"ethereum": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": true
},
"polygon": {
"mobile_active": false,
"extension_active": true,
"fallback_to_v1": false
} }
} }
} }

View File

@ -48,9 +48,9 @@ async function setupFetchMocking(driver) {
return { json: async () => clone(mockResponses.gasPricesBasic) }; return { json: async () => clone(mockResponses.gasPricesBasic) };
} else if (url.match(/chromeextensionmm/u)) { } else if (url.match(/chromeextensionmm/u)) {
return { json: async () => clone(mockResponses.metametrics) }; return { json: async () => clone(mockResponses.metametrics) };
} else if (url.match(/^https:\/\/(api\.metaswap|.*airswap-dev)/u)) { } else if (url.match(/^https:\/\/(api2\.metaswap\.codefi\.network)/u)) {
if (url.match(/featureFlag$/u)) { if (url.match(/featureFlags$/u)) {
return { json: async () => clone(mockResponses.swaps.featureFlag) }; return { json: async () => clone(mockResponses.swaps.featureFlags) };
} }
} }
return window.origFetch(...args); return window.origFetch(...args);

View File

@ -1 +1,2 @@
export const METASWAP_BASE_URL = 'https://api.metaswap.codefi.network'; export const METASWAP_BASE_URL = 'https://api.metaswap.codefi.network';
export const METASWAP_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network';

View File

@ -220,6 +220,7 @@ export const createSwapsMockStore = () => {
topAggId: null, topAggId: null,
routeState: '', routeState: '',
swapsFeatureIsLive: false, swapsFeatureIsLive: false,
useNewSwapsApi: false,
}, },
}, },
appState: { appState: {

View File

@ -59,3 +59,23 @@ export const TOKENS_GET_RESPONSE = [
address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', address: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
}, },
]; ];
export const createFeatureFlagsResponse = () => {
return {
bsc: {
mobile_active: false,
extension_active: true,
fallback_to_v1: true,
},
ethereum: {
mobile_active: false,
extension_active: true,
fallback_to_v1: true,
},
polygon: {
mobile_active: false,
extension_active: true,
fallback_to_v1: false,
},
};
};

View File

@ -34,9 +34,10 @@ import {
SWAPS_MAINTENANCE_ROUTE, SWAPS_MAINTENANCE_ROUTE,
} from '../../helpers/constants/routes'; } from '../../helpers/constants/routes';
import { import {
fetchSwapsFeatureLiveness, fetchSwapsFeatureFlags,
fetchSwapsGasPrices, fetchSwapsGasPrices,
isContractAddressValid, isContractAddressValid,
getSwapsLivenessForNetwork,
} from '../../pages/swaps/swaps.util'; } from '../../pages/swaps/swaps.util';
import { calcGasTotal } from '../../pages/send/send.utils'; import { calcGasTotal } from '../../pages/send/send.utils';
import { import {
@ -223,9 +224,12 @@ export function shouldShowCustomPriceTooLowWarning(state) {
const getSwapsState = (state) => state.metamask.swapsState; const getSwapsState = (state) => state.metamask.swapsState;
export const getSwapsFeatureLiveness = (state) => export const getSwapsFeatureIsLive = (state) =>
state.metamask.swapsState.swapsFeatureIsLive; state.metamask.swapsState.swapsFeatureIsLive;
export const getUseNewSwapsApi = (state) =>
state.metamask.swapsState.useNewSwapsApi;
export const getSwapsQuoteRefreshTime = (state) => export const getSwapsQuoteRefreshTime = (state) =>
state.metamask.swapsState.swapsQuoteRefreshTime; state.metamask.swapsState.swapsQuoteRefreshTime;
@ -373,16 +377,21 @@ export const fetchAndSetSwapsGasPriceInfo = () => {
export const fetchSwapsLiveness = () => { export const fetchSwapsLiveness = () => {
return async (dispatch, getState) => { return async (dispatch, getState) => {
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness( const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
getCurrentChainId(getState()), getCurrentChainId(getState()),
); );
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
return swapsFeatureIsLive; return swapsLivenessForNetwork;
}; };
}; };
@ -395,15 +404,22 @@ export const fetchQuotesAndSetQuoteState = (
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
chainId,
);
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
if (!swapsFeatureIsLive) { if (!swapsLivenessForNetwork.swapsFeatureIsLive) {
await history.push(SWAPS_MAINTENANCE_ROUTE); await history.push(SWAPS_MAINTENANCE_ROUTE);
return; return;
} }
@ -600,15 +616,22 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
const hardwareWalletUsed = isHardwareWallet(state); const hardwareWalletUsed = isHardwareWallet(state);
let swapsFeatureIsLive = false; let swapsLivenessForNetwork = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
try { try {
swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); const swapsFeatureFlags = await fetchSwapsFeatureFlags();
swapsLivenessForNetwork = getSwapsLivenessForNetwork(
swapsFeatureFlags,
chainId,
);
} catch (error) { } catch (error) {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsFeatureIsLive)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
if (!swapsFeatureIsLive) { if (!swapsLivenessForNetwork.swapsFeatureIsLive) {
await history.push(SWAPS_MAINTENANCE_ROUTE); await history.push(SWAPS_MAINTENANCE_ROUTE);
return; return;
} }
@ -808,12 +831,13 @@ export function fetchMetaSwapsGasPriceEstimates() {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
const chainId = getCurrentChainId(state); const chainId = getCurrentChainId(state);
const useNewSwapsApi = getUseNewSwapsApi(state);
dispatch(swapGasPriceEstimatesFetchStarted()); dispatch(swapGasPriceEstimatesFetchStarted());
let priceEstimates; let priceEstimates;
try { try {
priceEstimates = await fetchSwapsGasPrices(chainId); priceEstimates = await fetchSwapsGasPrices(chainId, useNewSwapsApi);
} catch (e) { } catch (e) {
log.warn('Fetching swaps gas prices failed:', e); log.warn('Fetching swaps gas prices failed:', e);

View File

@ -1,5 +1,6 @@
import nock from 'nock'; import nock from 'nock';
import { MOCKS } from '../../../test/jest';
import { setSwapsLiveness } from '../../store/actions'; import { setSwapsLiveness } from '../../store/actions';
import { setStorageItem } from '../../helpers/utils/storage-helpers'; import { setStorageItem } from '../../helpers/utils/storage-helpers';
import * as swaps from './swaps'; import * as swaps from './swaps';
@ -25,7 +26,7 @@ describe('Ducks - Swaps', () => {
describe('fetchSwapsLiveness', () => { describe('fetchSwapsLiveness', () => {
const cleanFeatureFlagApiCache = () => { const cleanFeatureFlagApiCache = () => {
setStorageItem( setStorageItem(
'cachedFetch:https://api.metaswap.codefi.network/featureFlag', 'cachedFetch:https://api2.metaswap.codefi.network/featureFlags',
null, null,
); );
}; };
@ -34,12 +35,12 @@ describe('Ducks - Swaps', () => {
cleanFeatureFlagApiCache(); cleanFeatureFlagApiCache();
}); });
const mockFeatureFlagApiResponse = ({ const mockFeatureFlagsApiResponse = ({
active = false, featureFlagsResponse,
replyWithError = false, replyWithError = false,
} = {}) => { } = {}) => {
const apiNock = nock('https://api.metaswap.codefi.network').get( const apiNock = nock('https://api2.metaswap.codefi.network').get(
'/featureFlag', '/featureFlags',
); );
if (replyWithError) { if (replyWithError) {
return apiNock.replyWithError({ return apiNock.replyWithError({
@ -47,9 +48,7 @@ describe('Ducks - Swaps', () => {
code: 'serverSideError', code: 'serverSideError',
}); });
} }
return apiNock.reply(200, { return apiNock.reply(200, featureFlagsResponse);
active,
});
}; };
const createGetState = () => { const createGetState = () => {
@ -58,61 +57,111 @@ describe('Ducks - Swaps', () => {
}); });
}; };
it('returns true if the Swaps feature is enabled', async () => { it('checks that Swaps for ETH are enabled and can use new API', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); const expectedSwapsLiveness = {
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(true); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(true); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('returns false if the Swaps feature is disabled', async () => { it('checks that Swaps for ETH are disabled for API v2 and enabled for API v1', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: false }); const expectedSwapsLiveness = {
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
featureFlagsResponse.ethereum.extension_active = false;
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(false); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(false); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('returns false if the /featureFlag API call throws an error', async () => { it('checks that Swaps for ETH are disabled for API v1 and v2', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
featureFlagsResponse.ethereum.extension_active = false;
featureFlagsResponse.ethereum.fallback_to_v1 = false;
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch,
createGetState(),
);
expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
});
it('checks that Swaps for ETH are disabled if the /featureFlags API call throws an error', async () => {
const mockDispatch = jest.fn();
const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
const featureFlagApiNock = mockFeatureFlagsApiResponse({
replyWithError: true, replyWithError: true,
}); });
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(setSwapsLiveness).toHaveBeenCalledWith(false); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(false); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
it('only calls the API once and returns true from cache for the second call', async () => { it('only calls the API once and returns response from cache for the second call', async () => {
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const featureFlagApiNock = mockFeatureFlagApiResponse({ active: true }); const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
const featureFlagsResponse = MOCKS.createFeatureFlagsResponse();
const featureFlagApiNock = mockFeatureFlagsApiResponse({
featureFlagsResponse,
});
await swaps.fetchSwapsLiveness()(mockDispatch, createGetState()); await swaps.fetchSwapsLiveness()(mockDispatch, createGetState());
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
const featureFlagApiNock2 = mockFeatureFlagApiResponse({ active: true }); const featureFlagApiNock2 = mockFeatureFlagsApiResponse({
const isSwapsFeatureEnabled = await swaps.fetchSwapsLiveness()( featureFlagsResponse,
});
const swapsLiveness = await swaps.fetchSwapsLiveness()(
mockDispatch, mockDispatch,
createGetState(), createGetState(),
); );
expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead. expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead.
expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(true); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(isSwapsFeatureEnabled).toBe(true); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
}); });
}); });

View File

@ -31,7 +31,7 @@ import {
} from '../../store/actions'; } from '../../store/actions';
import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app'; import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app';
import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask';
import { getSwapsFeatureLiveness } from '../../ducks/swaps/swaps'; import { getSwapsFeatureIsLive } from '../../ducks/swaps/swaps';
import { getEnvironmentType } from '../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../app/scripts/lib/util';
import { import {
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
@ -60,7 +60,7 @@ const mapStateToProps = (state) => {
const accountBalance = getCurrentEthBalance(state); const accountBalance = getCurrentEthBalance(state);
const { forgottenPassword, threeBoxLastUpdated } = appState; const { forgottenPassword, threeBoxLastUpdated } = appState;
const totalUnapprovedCount = getTotalUnapprovedCount(state); const totalUnapprovedCount = getTotalUnapprovedCount(state);
const swapsEnabled = getSwapsFeatureLiveness(state); const swapsEnabled = getSwapsFeatureIsLive(state);
const pendingConfirmations = getUnapprovedTemplatedConfirmations(state); const pendingConfirmations = getUnapprovedTemplatedConfirmations(state);
const envType = getEnvironmentType(); const envType = getEnvironmentType();

View File

@ -141,7 +141,9 @@ export default function BuildQuote({
const toTokenIsNotDefault = const toTokenIsNotDefault =
selectedToToken?.address && selectedToToken?.address &&
!isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId);
const occurances = Number(selectedToToken?.occurances || 0); const occurrences = Number(
selectedToToken?.occurances || selectedToToken?.occurrences || 0,
);
const { const {
address: fromTokenAddress, address: fromTokenAddress,
symbol: fromTokenSymbol, symbol: fromTokenSymbol,
@ -354,11 +356,11 @@ export default function BuildQuote({
let tokenVerificationDescription = ''; let tokenVerificationDescription = '';
if (blockExplorerTokenLink) { if (blockExplorerTokenLink) {
if (occurances === 1) { if (occurrences === 1) {
tokenVerificationDescription = t('verifyThisTokenOn', [ tokenVerificationDescription = t('verifyThisTokenOn', [
<BlockExplorerLink key="block-explorer-link" />, <BlockExplorerLink key="block-explorer-link" />,
]); ]);
} else if (occurances === 0) { } else if (occurrences === 0) {
tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [ tokenVerificationDescription = t('verifyThisUnconfirmedTokenOn', [
<BlockExplorerLink key="block-explorer-link" />, <BlockExplorerLink key="block-explorer-link" />,
]); ]);
@ -470,13 +472,13 @@ export default function BuildQuote({
/> />
</div> </div>
{toTokenIsNotDefault && {toTokenIsNotDefault &&
(occurances < 2 ? ( (occurrences < 2 ? (
<ActionableMessage <ActionableMessage
type={occurances === 1 ? 'warning' : 'danger'} type={occurrences === 1 ? 'warning' : 'danger'}
message={ message={
<div className="build-quote__token-verification-warning-message"> <div className="build-quote__token-verification-warning-message">
<div className="build-quote__bold"> <div className="build-quote__bold">
{occurances === 1 {occurrences === 1
? t('swapTokenVerificationOnlyOneSource') ? t('swapTokenVerificationOnlyOneSource')
: t('swapTokenVerificationAddedManually')} : t('swapTokenVerificationAddedManually')}
</div> </div>
@ -503,7 +505,7 @@ export default function BuildQuote({
className="build-quote__bold" className="build-quote__bold"
key="token-verification-bold-text" key="token-verification-bold-text"
> >
{t('swapTokenVerificationSources', [occurances])} {t('swapTokenVerificationSources', [occurrences])}
</span> </span>
{blockExplorerTokenLink && ( {blockExplorerTokenLink && (
<> <>
@ -563,7 +565,7 @@ export default function BuildQuote({
!selectedToToken?.address || !selectedToToken?.address ||
Number(maxSlippage) < 0 || Number(maxSlippage) < 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
(toTokenIsNotDefault && occurances < 2 && !verificationClicked) (toTokenIsNotDefault && occurrences < 2 && !verificationClicked)
} }
hideCancel hideCancel
showTermsOfService showTermsOfService

View File

@ -6,6 +6,7 @@ import {
MAINNET_CHAIN_ID, MAINNET_CHAIN_ID,
BSC_CHAIN_ID, BSC_CHAIN_ID,
LOCALHOST_CHAIN_ID, LOCALHOST_CHAIN_ID,
POLYGON_CHAIN_ID,
} from '../../../../shared/constants/network'; } from '../../../../shared/constants/network';
export default function FeeCard({ export default function FeeCard({
@ -38,6 +39,8 @@ export default function FeeCard({
return t('networkNameEthereum'); return t('networkNameEthereum');
case BSC_CHAIN_ID: case BSC_CHAIN_ID:
return t('networkNameBSC'); return t('networkNameBSC');
case POLYGON_CHAIN_ID:
return t('networkNamePolygon');
case LOCALHOST_CHAIN_ID: case LOCALHOST_CHAIN_ID:
return t('networkNameTestnet'); return t('networkNameTestnet');
default: default:

View File

@ -29,10 +29,11 @@ import {
getAggregatorMetadata, getAggregatorMetadata,
getBackgroundSwapRouteState, getBackgroundSwapRouteState,
getSwapsErrorKey, getSwapsErrorKey,
getSwapsFeatureLiveness, getSwapsFeatureIsLive,
prepareToLeaveSwaps, prepareToLeaveSwaps,
fetchAndSetSwapsGasPriceInfo, fetchAndSetSwapsGasPriceInfo,
fetchSwapsLiveness, fetchSwapsLiveness,
getUseNewSwapsApi,
} from '../../ducks/swaps/swaps'; } from '../../ducks/swaps/swaps';
import { import {
AWAITING_SIGNATURES_ROUTE, AWAITING_SIGNATURES_ROUTE,
@ -103,9 +104,10 @@ export default function Swap() {
const aggregatorMetadata = useSelector(getAggregatorMetadata); const aggregatorMetadata = useSelector(getAggregatorMetadata);
const fetchingQuotes = useSelector(getFetchingQuotes); const fetchingQuotes = useSelector(getFetchingQuotes);
let swapsErrorKey = useSelector(getSwapsErrorKey); let swapsErrorKey = useSelector(getSwapsErrorKey);
const swapsEnabled = useSelector(getSwapsFeatureLiveness); const swapsEnabled = useSelector(getSwapsFeatureIsLive);
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain); const isSwapsChain = useSelector(getIsSwapsChain);
const useNewSwapsApi = useSelector(getUseNewSwapsApi);
const { const {
balance: ethBalance, balance: ethBalance,
@ -165,27 +167,28 @@ export default function Swap() {
}; };
}, []); }, []);
// eslint-disable-next-line
useEffect(() => { useEffect(() => {
fetchTokens(chainId) if (isFeatureFlagLoaded) {
.then((tokens) => { fetchTokens(chainId, useNewSwapsApi)
dispatch(setSwapsTokens(tokens)); .then((tokens) => {
}) dispatch(setSwapsTokens(tokens));
.catch((error) => console.error(error)); })
.catch((error) => console.error(error));
fetchTopAssets(chainId).then((topAssets) => { fetchTopAssets(chainId, useNewSwapsApi).then((topAssets) => {
dispatch(setTopAssets(topAssets)); dispatch(setTopAssets(topAssets));
}); });
fetchAggregatorMetadata(chainId, useNewSwapsApi).then(
fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => { (newAggregatorMetadata) => {
dispatch(setAggregatorMetadata(newAggregatorMetadata)); dispatch(setAggregatorMetadata(newAggregatorMetadata));
}); },
);
dispatch(fetchAndSetSwapsGasPriceInfo(chainId)); dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
return () => {
return () => { dispatch(prepareToLeaveSwaps());
dispatch(prepareToLeaveSwaps()); };
}; }
}, [dispatch, chainId]); }, [dispatch, chainId, isFeatureFlagLoaded, useNewSwapsApi]);
const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType); const hardwareWalletType = useSelector(getHardwareWalletType);

View File

@ -24,7 +24,7 @@ setBackgroundConnection({
}); });
describe('Swap', () => { describe('Swap', () => {
let tokensNock; let featureFlagsNock;
beforeEach(() => { beforeEach(() => {
nock(CONSTANTS.METASWAP_BASE_URL) nock(CONSTANTS.METASWAP_BASE_URL)
@ -43,9 +43,13 @@ describe('Swap', () => {
.get('/gasPrices') .get('/gasPrices')
.reply(200, MOCKS.GAS_PRICES_GET_RESPONSE); .reply(200, MOCKS.GAS_PRICES_GET_RESPONSE);
tokensNock = nock(CONSTANTS.METASWAP_BASE_URL) nock(CONSTANTS.METASWAP_BASE_URL)
.get('/tokens') .get('/tokens')
.reply(200, MOCKS.TOKENS_GET_RESPONSE); .reply(200, MOCKS.TOKENS_GET_RESPONSE);
featureFlagsNock = nock(CONSTANTS.METASWAP_API_V2_BASE_URL)
.get('/featureFlags')
.reply(200, MOCKS.createFeatureFlagsResponse());
}); });
afterAll(() => { afterAll(() => {
@ -55,7 +59,7 @@ describe('Swap', () => {
it('renders the component with initial props', async () => { it('renders the component with initial props', async () => {
const store = configureMockStore(middleware)(createSwapsMockStore()); const store = configureMockStore(middleware)(createSwapsMockStore());
const { container, getByText } = renderWithProvider(<Swap />, store); const { container, getByText } = renderWithProvider(<Swap />, store);
await waitFor(() => expect(tokensNock.isDone()).toBe(true)); await waitFor(() => expect(featureFlagsNock.isDone()).toBe(true));
expect(getByText('Swap')).toBeInTheDocument(); expect(getByText('Swap')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument(); expect(getByText('Cancel')).toBeInTheDocument();
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();

View File

@ -9,6 +9,7 @@ import { usePrevious } from '../../../../hooks/usePrevious';
import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils'; import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils';
import { fetchToken } from '../../swaps.util'; import { fetchToken } from '../../swaps.util';
import { getCurrentChainId } from '../../../../selectors/selectors'; import { getCurrentChainId } from '../../../../selectors/selectors';
import { getUseNewSwapsApi } from '../../../../ducks/swaps/swaps';
const renderAdornment = () => ( const renderAdornment = () => (
<InputAdornment position="start" style={{ marginRight: '12px' }}> <InputAdornment position="start" style={{ marginRight: '12px' }}>
@ -28,6 +29,7 @@ export default function ListItemSearch({
const fuseRef = useRef(); const fuseRef = useRef();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const useNewSwapsApi = useSelector(getUseNewSwapsApi);
/** /**
* Search a custom token for import based on a contract address. * Search a custom token for import based on a contract address.
@ -36,7 +38,7 @@ export default function ListItemSearch({
const handleSearchTokenForImport = async (contractAddress) => { const handleSearchTokenForImport = async (contractAddress) => {
setSearchQuery(contractAddress); setSearchQuery(contractAddress);
try { try {
const token = await fetchToken(contractAddress, chainId); const token = await fetchToken(contractAddress, chainId, useNewSwapsApi);
if (token) { if (token) {
token.primaryLabel = token.symbol; token.primaryLabel = token.symbol;
token.secondaryLabel = token.name; token.secondaryLabel = token.name;

View File

@ -6,6 +6,9 @@ import {
METASWAP_CHAINID_API_HOST_MAP, METASWAP_CHAINID_API_HOST_MAP,
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
ETH_WETH_CONTRACT_ADDRESS, ETH_WETH_CONTRACT_ADDRESS,
ETHEREUM,
POLYGON,
BSC,
} from '../../../shared/constants/swaps'; } from '../../../shared/constants/swaps';
import { import {
isSwapsDefaultTokenAddress, isSwapsDefaultTokenAddress,
@ -15,6 +18,9 @@ import {
ETH_SYMBOL, ETH_SYMBOL,
WETH_SYMBOL, WETH_SYMBOL,
MAINNET_CHAIN_ID, MAINNET_CHAIN_ID,
BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
LOCALHOST_CHAIN_ID,
} from '../../../shared/constants/network'; } from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time'; import { SECOND } from '../../../shared/constants/time';
import { import {
@ -42,24 +48,50 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH =
const CACHE_REFRESH_FIVE_MINUTES = 300000; const CACHE_REFRESH_FIVE_MINUTES = 300000;
const getBaseApi = function (type, chainId = MAINNET_CHAIN_ID) { const SWAPS_API_V2_BASE_URL = 'https://api2.metaswap.codefi.network';
const GAS_API_BASE_URL = 'https://gas-api.metaswap.codefi.network';
/**
* @param {string} type Type of an API call, e.g. "tokens"
* @param {string} chainId
* @returns string
*/
const getBaseUrlForNewSwapsApi = (type, chainId) => {
const noNetworkSpecificTypes = ['refreshTime']; // These types don't need network info in the URL.
if (noNetworkSpecificTypes.includes(type)) {
return SWAPS_API_V2_BASE_URL;
}
const chainIdDecimal = chainId && parseInt(chainId, 16);
const gasApiTypes = ['gasPrices'];
if (gasApiTypes.includes(type)) {
return `${GAS_API_BASE_URL}/networks/${chainIdDecimal}`; // Gas calculations are in its own repo.
}
return `${SWAPS_API_V2_BASE_URL}/networks/${chainIdDecimal}`;
};
const getBaseApi = function (
type,
chainId = MAINNET_CHAIN_ID,
useNewSwapsApi = false,
) {
const baseUrl = useNewSwapsApi
? getBaseUrlForNewSwapsApi(type, chainId)
: METASWAP_CHAINID_API_HOST_MAP[chainId];
switch (type) { switch (type) {
case 'trade': case 'trade':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`; return `${baseUrl}/trades?`;
case 'tokens': case 'tokens':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`; return `${baseUrl}/tokens`;
case 'token': case 'token':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/token`; return `${baseUrl}/token`;
case 'topAssets': case 'topAssets':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`; return `${baseUrl}/topAssets`;
case 'featureFlag':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/featureFlag`;
case 'aggregatorMetadata': case 'aggregatorMetadata':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/aggregatorMetadata`; return `${baseUrl}/aggregatorMetadata`;
case 'gasPrices': case 'gasPrices':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/gasPrices`; return `${baseUrl}/gasPrices`;
case 'refreshTime': case 'refreshTime':
return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/quoteRefreshRate`; return `${baseUrl}/quoteRefreshRate`;
default: default:
throw new Error('getBaseApi requires an api call type'); throw new Error('getBaseApi requires an api call type');
} }
@ -233,7 +265,7 @@ export async function fetchTradesInfo(
fromAddress, fromAddress,
exchangeList, exchangeList,
}, },
{ chainId }, { chainId, useNewSwapsApi },
) { ) {
const urlParams = { const urlParams = {
destinationToken, destinationToken,
@ -249,7 +281,11 @@ export async function fetchTradesInfo(
} }
const queryString = new URLSearchParams(urlParams).toString(); const queryString = new URLSearchParams(urlParams).toString();
const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`; const tradeURL = `${getBaseApi(
'trade',
chainId,
useNewSwapsApi,
)}${queryString}`;
const tradesResponse = await fetchWithCache( const tradesResponse = await fetchWithCache(
tradeURL, tradeURL,
{ method: 'GET' }, { method: 'GET' },
@ -293,8 +329,8 @@ export async function fetchTradesInfo(
return newQuotes; return newQuotes;
} }
export async function fetchToken(contractAddress, chainId) { export async function fetchToken(contractAddress, chainId, useNewSwapsApi) {
const tokenUrl = getBaseApi('token', chainId); const tokenUrl = getBaseApi('token', chainId, useNewSwapsApi);
const token = await fetchWithCache( const token = await fetchWithCache(
`${tokenUrl}?address=${contractAddress}`, `${tokenUrl}?address=${contractAddress}`,
{ method: 'GET' }, { method: 'GET' },
@ -303,8 +339,8 @@ export async function fetchToken(contractAddress, chainId) {
return token; return token;
} }
export async function fetchTokens(chainId) { export async function fetchTokens(chainId, useNewSwapsApi) {
const tokensUrl = getBaseApi('tokens', chainId); const tokensUrl = getBaseApi('tokens', chainId, useNewSwapsApi);
const tokens = await fetchWithCache( const tokens = await fetchWithCache(
tokensUrl, tokensUrl,
{ method: 'GET' }, { method: 'GET' },
@ -325,8 +361,12 @@ export async function fetchTokens(chainId) {
return filteredTokens; return filteredTokens;
} }
export async function fetchAggregatorMetadata(chainId) { export async function fetchAggregatorMetadata(chainId, useNewSwapsApi) {
const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata', chainId); const aggregatorMetadataUrl = getBaseApi(
'aggregatorMetadata',
chainId,
useNewSwapsApi,
);
const aggregators = await fetchWithCache( const aggregators = await fetchWithCache(
aggregatorMetadataUrl, aggregatorMetadataUrl,
{ method: 'GET' }, { method: 'GET' },
@ -347,8 +387,8 @@ export async function fetchAggregatorMetadata(chainId) {
return filteredAggregators; return filteredAggregators;
} }
export async function fetchTopAssets(chainId) { export async function fetchTopAssets(chainId, useNewSwapsApi) {
const topAssetsUrl = getBaseApi('topAssets', chainId); const topAssetsUrl = getBaseApi('topAssets', chainId, useNewSwapsApi);
const response = await fetchWithCache( const response = await fetchWithCache(
topAssetsUrl, topAssetsUrl,
{ method: 'GET' }, { method: 'GET' },
@ -363,18 +403,18 @@ export async function fetchTopAssets(chainId) {
return topAssetsMap; return topAssetsMap;
} }
export async function fetchSwapsFeatureLiveness(chainId) { export async function fetchSwapsFeatureFlags() {
const status = await fetchWithCache( const response = await fetchWithCache(
getBaseApi('featureFlag', chainId), `${SWAPS_API_V2_BASE_URL}/featureFlags`,
{ method: 'GET' }, { method: 'GET' },
{ cacheRefreshTime: 600000 }, { cacheRefreshTime: 600000 },
); );
return status?.active; return response;
} }
export async function fetchSwapsQuoteRefreshTime(chainId) { export async function fetchSwapsQuoteRefreshTime(chainId, useNewSwapsApi) {
const response = await fetchWithCache( const response = await fetchWithCache(
getBaseApi('refreshTime', chainId), getBaseApi('refreshTime', chainId, useNewSwapsApi),
{ method: 'GET' }, { method: 'GET' },
{ cacheRefreshTime: 600000 }, { cacheRefreshTime: 600000 },
); );
@ -409,8 +449,8 @@ export async function fetchTokenBalance(address, userAddress) {
return usersToken; return usersToken;
} }
export async function fetchSwapsGasPrices(chainId) { export async function fetchSwapsGasPrices(chainId, useNewSwapsApi) {
const gasPricesUrl = getBaseApi('gasPrices', chainId); const gasPricesUrl = getBaseApi('gasPrices', chainId, useNewSwapsApi);
const response = await fetchWithCache( const response = await fetchWithCache(
gasPricesUrl, gasPricesUrl,
{ method: 'GET' }, { method: 'GET' },
@ -730,3 +770,56 @@ export const isContractAddressValid = (
contractAddressForChainId.toUpperCase() === contractAddress.toUpperCase() contractAddressForChainId.toUpperCase() === contractAddress.toUpperCase()
); );
}; };
/**
* @param {string} chainId
* @returns string e.g. ethereum, bsc or polygon
*/
export const getNetworkNameByChainId = (chainId) => {
switch (chainId) {
case MAINNET_CHAIN_ID:
return ETHEREUM;
case BSC_CHAIN_ID:
return BSC;
case POLYGON_CHAIN_ID:
return POLYGON;
default:
return '';
}
};
/**
* It returns info about if Swaps are enabled and if we should use our new APIs for it.
* @param {object} swapsFeatureFlags
* @param {string} chainId
* @returns object with 2 items: "swapsFeatureIsLive" and "useNewSwapsApi"
*/
export const getSwapsLivenessForNetwork = (swapsFeatureFlags = {}, chainId) => {
const networkName = getNetworkNameByChainId(chainId);
// Use old APIs for testnet.
if (chainId === LOCALHOST_CHAIN_ID) {
return {
swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
}
// If a network name is not found in the list of feature flags, disable Swaps.
if (!swapsFeatureFlags[networkName]) {
return {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
}
const isNetworkEnabledForNewApi =
swapsFeatureFlags[networkName].extension_active;
if (isNetworkEnabledForNewApi) {
return {
swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
}
return {
swapsFeatureIsLive: swapsFeatureFlags[networkName].fallback_to_v1,
useNewSwapsApi: false,
};
};

View File

@ -1,14 +1,20 @@
import nock from 'nock'; import nock from 'nock';
import { MOCKS } from '../../../test/jest';
import { import {
ETH_SYMBOL, ETH_SYMBOL,
WETH_SYMBOL, WETH_SYMBOL,
MAINNET_CHAIN_ID, MAINNET_CHAIN_ID,
BSC_CHAIN_ID, BSC_CHAIN_ID,
POLYGON_CHAIN_ID,
LOCALHOST_CHAIN_ID, LOCALHOST_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network'; } from '../../../shared/constants/network';
import { import {
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP, SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
ETH_WETH_CONTRACT_ADDRESS, ETH_WETH_CONTRACT_ADDRESS,
ETHEREUM,
POLYGON,
BSC,
} from '../../../shared/constants/swaps'; } from '../../../shared/constants/swaps';
import { import {
TOKENS, TOKENS,
@ -17,13 +23,14 @@ import {
AGGREGATOR_METADATA, AGGREGATOR_METADATA,
TOP_ASSETS, TOP_ASSETS,
} from './swaps-util-test-constants'; } from './swaps-util-test-constants';
import { import {
fetchTradesInfo, fetchTradesInfo,
fetchTokens, fetchTokens,
fetchAggregatorMetadata, fetchAggregatorMetadata,
fetchTopAssets, fetchTopAssets,
isContractAddressValid, isContractAddressValid,
getNetworkNameByChainId,
getSwapsLivenessForNetwork,
} from './swaps.util'; } from './swaps.util';
jest.mock('../../helpers/utils/storage-helpers.js', () => ({ jest.mock('../../helpers/utils/storage-helpers.js', () => ({
@ -372,4 +379,75 @@ describe('Swaps Util', () => {
).toBe(false); ).toBe(false);
}); });
}); });
describe('getNetworkNameByChainId', () => {
it('returns "ethereum" for mainnet chain ID', () => {
expect(getNetworkNameByChainId(MAINNET_CHAIN_ID)).toBe(ETHEREUM);
});
it('returns "bsc" for mainnet chain ID', () => {
expect(getNetworkNameByChainId(BSC_CHAIN_ID)).toBe(BSC);
});
it('returns "polygon" for mainnet chain ID', () => {
expect(getNetworkNameByChainId(POLYGON_CHAIN_ID)).toBe(POLYGON);
});
it('returns an empty string for an unsupported network', () => {
expect(getNetworkNameByChainId(RINKEBY_CHAIN_ID)).toBe('');
});
});
describe('getSwapsLivenessForNetwork', () => {
it('returns info that Swaps are enabled and cannot use API v2 for localhost chain ID', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
expect(
getSwapsLivenessForNetwork(
MOCKS.createFeatureFlagsResponse(),
LOCALHOST_CHAIN_ID,
),
).toMatchObject(expectedSwapsLiveness);
});
it('returns info that Swaps are disabled and cannot use API v2 if network name is not found', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: false,
useNewSwapsApi: false,
};
expect(
getSwapsLivenessForNetwork(
MOCKS.createFeatureFlagsResponse(),
RINKEBY_CHAIN_ID,
),
).toMatchObject(expectedSwapsLiveness);
});
it('returns info that Swaps are enabled and can use API v2 for mainnet chain ID', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: true,
};
expect(
getSwapsLivenessForNetwork(
MOCKS.createFeatureFlagsResponse(),
MAINNET_CHAIN_ID,
),
).toMatchObject(expectedSwapsLiveness);
});
it('returns info that Swaps are enabled but can only use API v1 for mainnet chain ID', () => {
const expectedSwapsLiveness = {
swapsFeatureIsLive: true,
useNewSwapsApi: false,
};
const swapsFeatureFlags = MOCKS.createFeatureFlagsResponse();
swapsFeatureFlags[ETHEREUM].extension_active = false;
expect(
getSwapsLivenessForNetwork(swapsFeatureFlags, MAINNET_CHAIN_ID),
).toMatchObject(expectedSwapsLiveness);
});
});
}); });

View File

@ -2121,9 +2121,9 @@ export function setPendingTokens(pendingTokens) {
// Swaps // Swaps
export function setSwapsLiveness(swapsFeatureIsLive) { export function setSwapsLiveness(swapsLiveness) {
return async (dispatch) => { return async (dispatch) => {
await promisifiedBackground.setSwapsLiveness(swapsFeatureIsLive); await promisifiedBackground.setSwapsLiveness(swapsLiveness);
await forceUpdateMetamaskState(dispatch); await forceUpdateMetamaskState(dispatch);
}; };
} }