- {occurances === 1
+ {occurrences === 1
? t('swapTokenVerificationOnlyOneSource')
: t('swapTokenVerificationAddedManually')}
@@ -503,7 +505,7 @@ export default function BuildQuote({
className="build-quote__bold"
key="token-verification-bold-text"
>
- {t('swapTokenVerificationSources', [occurances])}
+ {t('swapTokenVerificationSources', [occurrences])}
{blockExplorerTokenLink && (
<>
@@ -563,7 +565,7 @@ export default function BuildQuote({
!selectedToToken?.address ||
Number(maxSlippage) < 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
- (toTokenIsNotDefault && occurances < 2 && !verificationClicked)
+ (toTokenIsNotDefault && occurrences < 2 && !verificationClicked)
}
hideCancel
showTermsOfService
diff --git a/ui/pages/swaps/fee-card/fee-card.js b/ui/pages/swaps/fee-card/fee-card.js
index 68ee4faec..67b448a5d 100644
--- a/ui/pages/swaps/fee-card/fee-card.js
+++ b/ui/pages/swaps/fee-card/fee-card.js
@@ -6,6 +6,7 @@ import {
MAINNET_CHAIN_ID,
BSC_CHAIN_ID,
LOCALHOST_CHAIN_ID,
+ POLYGON_CHAIN_ID,
} from '../../../../shared/constants/network';
export default function FeeCard({
@@ -38,6 +39,8 @@ export default function FeeCard({
return t('networkNameEthereum');
case BSC_CHAIN_ID:
return t('networkNameBSC');
+ case POLYGON_CHAIN_ID:
+ return t('networkNamePolygon');
case LOCALHOST_CHAIN_ID:
return t('networkNameTestnet');
default:
diff --git a/ui/pages/swaps/index.js b/ui/pages/swaps/index.js
index 05a0d6d42..d2a30b904 100644
--- a/ui/pages/swaps/index.js
+++ b/ui/pages/swaps/index.js
@@ -29,10 +29,11 @@ import {
getAggregatorMetadata,
getBackgroundSwapRouteState,
getSwapsErrorKey,
- getSwapsFeatureLiveness,
+ getSwapsFeatureIsLive,
prepareToLeaveSwaps,
fetchAndSetSwapsGasPriceInfo,
fetchSwapsLiveness,
+ getUseNewSwapsApi,
} from '../../ducks/swaps/swaps';
import {
AWAITING_SIGNATURES_ROUTE,
@@ -103,9 +104,10 @@ export default function Swap() {
const aggregatorMetadata = useSelector(getAggregatorMetadata);
const fetchingQuotes = useSelector(getFetchingQuotes);
let swapsErrorKey = useSelector(getSwapsErrorKey);
- const swapsEnabled = useSelector(getSwapsFeatureLiveness);
+ const swapsEnabled = useSelector(getSwapsFeatureIsLive);
const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain);
+ const useNewSwapsApi = useSelector(getUseNewSwapsApi);
const {
balance: ethBalance,
@@ -165,27 +167,28 @@ export default function Swap() {
};
}, []);
+ // eslint-disable-next-line
useEffect(() => {
- fetchTokens(chainId)
- .then((tokens) => {
- dispatch(setSwapsTokens(tokens));
- })
- .catch((error) => console.error(error));
-
- fetchTopAssets(chainId).then((topAssets) => {
- dispatch(setTopAssets(topAssets));
- });
-
- fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => {
- dispatch(setAggregatorMetadata(newAggregatorMetadata));
- });
-
- dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
-
- return () => {
- dispatch(prepareToLeaveSwaps());
- };
- }, [dispatch, chainId]);
+ if (isFeatureFlagLoaded) {
+ fetchTokens(chainId, useNewSwapsApi)
+ .then((tokens) => {
+ dispatch(setSwapsTokens(tokens));
+ })
+ .catch((error) => console.error(error));
+ fetchTopAssets(chainId, useNewSwapsApi).then((topAssets) => {
+ dispatch(setTopAssets(topAssets));
+ });
+ fetchAggregatorMetadata(chainId, useNewSwapsApi).then(
+ (newAggregatorMetadata) => {
+ dispatch(setAggregatorMetadata(newAggregatorMetadata));
+ },
+ );
+ dispatch(fetchAndSetSwapsGasPriceInfo(chainId));
+ return () => {
+ dispatch(prepareToLeaveSwaps());
+ };
+ }
+ }, [dispatch, chainId, isFeatureFlagLoaded, useNewSwapsApi]);
const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType);
diff --git a/ui/pages/swaps/index.test.js b/ui/pages/swaps/index.test.js
index a3162ae2a..ed2323e40 100644
--- a/ui/pages/swaps/index.test.js
+++ b/ui/pages/swaps/index.test.js
@@ -24,7 +24,7 @@ setBackgroundConnection({
});
describe('Swap', () => {
- let tokensNock;
+ let featureFlagsNock;
beforeEach(() => {
nock(CONSTANTS.METASWAP_BASE_URL)
@@ -43,9 +43,13 @@ describe('Swap', () => {
.get('/gasPrices')
.reply(200, MOCKS.GAS_PRICES_GET_RESPONSE);
- tokensNock = nock(CONSTANTS.METASWAP_BASE_URL)
+ nock(CONSTANTS.METASWAP_BASE_URL)
.get('/tokens')
.reply(200, MOCKS.TOKENS_GET_RESPONSE);
+
+ featureFlagsNock = nock(CONSTANTS.METASWAP_API_V2_BASE_URL)
+ .get('/featureFlags')
+ .reply(200, MOCKS.createFeatureFlagsResponse());
});
afterAll(() => {
@@ -55,7 +59,7 @@ describe('Swap', () => {
it('renders the component with initial props', async () => {
const store = configureMockStore(middleware)(createSwapsMockStore());
const { container, getByText } = renderWithProvider(, store);
- await waitFor(() => expect(tokensNock.isDone()).toBe(true));
+ await waitFor(() => expect(featureFlagsNock.isDone()).toBe(true));
expect(getByText('Swap')).toBeInTheDocument();
expect(getByText('Cancel')).toBeInTheDocument();
expect(container).toMatchSnapshot();
diff --git a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js
index 26fbc0124..ed1b9e257 100644
--- a/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js
+++ b/ui/pages/swaps/searchable-item-list/list-item-search/list-item-search.component.js
@@ -9,6 +9,7 @@ import { usePrevious } from '../../../../hooks/usePrevious';
import { isValidHexAddress } from '../../../../../shared/modules/hexstring-utils';
import { fetchToken } from '../../swaps.util';
import { getCurrentChainId } from '../../../../selectors/selectors';
+import { getUseNewSwapsApi } from '../../../../ducks/swaps/swaps';
const renderAdornment = () => (
@@ -28,6 +29,7 @@ export default function ListItemSearch({
const fuseRef = useRef();
const [searchQuery, setSearchQuery] = useState('');
const chainId = useSelector(getCurrentChainId);
+ const useNewSwapsApi = useSelector(getUseNewSwapsApi);
/**
* Search a custom token for import based on a contract address.
@@ -36,7 +38,7 @@ export default function ListItemSearch({
const handleSearchTokenForImport = async (contractAddress) => {
setSearchQuery(contractAddress);
try {
- const token = await fetchToken(contractAddress, chainId);
+ const token = await fetchToken(contractAddress, chainId, useNewSwapsApi);
if (token) {
token.primaryLabel = token.symbol;
token.secondaryLabel = token.name;
diff --git a/ui/pages/swaps/swaps.util.js b/ui/pages/swaps/swaps.util.js
index 58ef33f63..068c3f870 100644
--- a/ui/pages/swaps/swaps.util.js
+++ b/ui/pages/swaps/swaps.util.js
@@ -6,6 +6,9 @@ import {
METASWAP_CHAINID_API_HOST_MAP,
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
ETH_WETH_CONTRACT_ADDRESS,
+ ETHEREUM,
+ POLYGON,
+ BSC,
} from '../../../shared/constants/swaps';
import {
isSwapsDefaultTokenAddress,
@@ -15,6 +18,9 @@ import {
ETH_SYMBOL,
WETH_SYMBOL,
MAINNET_CHAIN_ID,
+ BSC_CHAIN_ID,
+ POLYGON_CHAIN_ID,
+ LOCALHOST_CHAIN_ID,
} from '../../../shared/constants/network';
import { SECOND } from '../../../shared/constants/time';
import {
@@ -42,24 +48,50 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH =
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) {
case 'trade':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`;
+ return `${baseUrl}/trades?`;
case 'tokens':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`;
+ return `${baseUrl}/tokens`;
case 'token':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/token`;
+ return `${baseUrl}/token`;
case 'topAssets':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`;
- case 'featureFlag':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/featureFlag`;
+ return `${baseUrl}/topAssets`;
case 'aggregatorMetadata':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/aggregatorMetadata`;
+ return `${baseUrl}/aggregatorMetadata`;
case 'gasPrices':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/gasPrices`;
+ return `${baseUrl}/gasPrices`;
case 'refreshTime':
- return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/quoteRefreshRate`;
+ return `${baseUrl}/quoteRefreshRate`;
default:
throw new Error('getBaseApi requires an api call type');
}
@@ -233,7 +265,7 @@ export async function fetchTradesInfo(
fromAddress,
exchangeList,
},
- { chainId },
+ { chainId, useNewSwapsApi },
) {
const urlParams = {
destinationToken,
@@ -249,7 +281,11 @@ export async function fetchTradesInfo(
}
const queryString = new URLSearchParams(urlParams).toString();
- const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`;
+ const tradeURL = `${getBaseApi(
+ 'trade',
+ chainId,
+ useNewSwapsApi,
+ )}${queryString}`;
const tradesResponse = await fetchWithCache(
tradeURL,
{ method: 'GET' },
@@ -293,8 +329,8 @@ export async function fetchTradesInfo(
return newQuotes;
}
-export async function fetchToken(contractAddress, chainId) {
- const tokenUrl = getBaseApi('token', chainId);
+export async function fetchToken(contractAddress, chainId, useNewSwapsApi) {
+ const tokenUrl = getBaseApi('token', chainId, useNewSwapsApi);
const token = await fetchWithCache(
`${tokenUrl}?address=${contractAddress}`,
{ method: 'GET' },
@@ -303,8 +339,8 @@ export async function fetchToken(contractAddress, chainId) {
return token;
}
-export async function fetchTokens(chainId) {
- const tokensUrl = getBaseApi('tokens', chainId);
+export async function fetchTokens(chainId, useNewSwapsApi) {
+ const tokensUrl = getBaseApi('tokens', chainId, useNewSwapsApi);
const tokens = await fetchWithCache(
tokensUrl,
{ method: 'GET' },
@@ -325,8 +361,12 @@ export async function fetchTokens(chainId) {
return filteredTokens;
}
-export async function fetchAggregatorMetadata(chainId) {
- const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata', chainId);
+export async function fetchAggregatorMetadata(chainId, useNewSwapsApi) {
+ const aggregatorMetadataUrl = getBaseApi(
+ 'aggregatorMetadata',
+ chainId,
+ useNewSwapsApi,
+ );
const aggregators = await fetchWithCache(
aggregatorMetadataUrl,
{ method: 'GET' },
@@ -347,8 +387,8 @@ export async function fetchAggregatorMetadata(chainId) {
return filteredAggregators;
}
-export async function fetchTopAssets(chainId) {
- const topAssetsUrl = getBaseApi('topAssets', chainId);
+export async function fetchTopAssets(chainId, useNewSwapsApi) {
+ const topAssetsUrl = getBaseApi('topAssets', chainId, useNewSwapsApi);
const response = await fetchWithCache(
topAssetsUrl,
{ method: 'GET' },
@@ -363,18 +403,18 @@ export async function fetchTopAssets(chainId) {
return topAssetsMap;
}
-export async function fetchSwapsFeatureLiveness(chainId) {
- const status = await fetchWithCache(
- getBaseApi('featureFlag', chainId),
+export async function fetchSwapsFeatureFlags() {
+ const response = await fetchWithCache(
+ `${SWAPS_API_V2_BASE_URL}/featureFlags`,
{ method: 'GET' },
{ cacheRefreshTime: 600000 },
);
- return status?.active;
+ return response;
}
-export async function fetchSwapsQuoteRefreshTime(chainId) {
+export async function fetchSwapsQuoteRefreshTime(chainId, useNewSwapsApi) {
const response = await fetchWithCache(
- getBaseApi('refreshTime', chainId),
+ getBaseApi('refreshTime', chainId, useNewSwapsApi),
{ method: 'GET' },
{ cacheRefreshTime: 600000 },
);
@@ -409,8 +449,8 @@ export async function fetchTokenBalance(address, userAddress) {
return usersToken;
}
-export async function fetchSwapsGasPrices(chainId) {
- const gasPricesUrl = getBaseApi('gasPrices', chainId);
+export async function fetchSwapsGasPrices(chainId, useNewSwapsApi) {
+ const gasPricesUrl = getBaseApi('gasPrices', chainId, useNewSwapsApi);
const response = await fetchWithCache(
gasPricesUrl,
{ method: 'GET' },
@@ -730,3 +770,56 @@ export const isContractAddressValid = (
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,
+ };
+};
diff --git a/ui/pages/swaps/swaps.util.test.js b/ui/pages/swaps/swaps.util.test.js
index 936864f0f..81220cbd4 100644
--- a/ui/pages/swaps/swaps.util.test.js
+++ b/ui/pages/swaps/swaps.util.test.js
@@ -1,14 +1,20 @@
import nock from 'nock';
+import { MOCKS } from '../../../test/jest';
import {
ETH_SYMBOL,
WETH_SYMBOL,
MAINNET_CHAIN_ID,
BSC_CHAIN_ID,
+ POLYGON_CHAIN_ID,
LOCALHOST_CHAIN_ID,
+ RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import {
SWAPS_CHAINID_CONTRACT_ADDRESS_MAP,
ETH_WETH_CONTRACT_ADDRESS,
+ ETHEREUM,
+ POLYGON,
+ BSC,
} from '../../../shared/constants/swaps';
import {
TOKENS,
@@ -17,13 +23,14 @@ import {
AGGREGATOR_METADATA,
TOP_ASSETS,
} from './swaps-util-test-constants';
-
import {
fetchTradesInfo,
fetchTokens,
fetchAggregatorMetadata,
fetchTopAssets,
isContractAddressValid,
+ getNetworkNameByChainId,
+ getSwapsLivenessForNetwork,
} from './swaps.util';
jest.mock('../../helpers/utils/storage-helpers.js', () => ({
@@ -372,4 +379,75 @@ describe('Swaps Util', () => {
).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);
+ });
+ });
});
diff --git a/ui/store/actions.js b/ui/store/actions.js
index 8945de10b..0e3b9fe0d 100644
--- a/ui/store/actions.js
+++ b/ui/store/actions.js
@@ -2109,9 +2109,9 @@ export function setPendingTokens(pendingTokens) {
// Swaps
-export function setSwapsLiveness(swapsFeatureIsLive) {
+export function setSwapsLiveness(swapsLiveness) {
return async (dispatch) => {
- await promisifiedBackground.setSwapsLiveness(swapsFeatureIsLive);
+ await promisifiedBackground.setSwapsLiveness(swapsLiveness);
await forceUpdateMetamaskState(dispatch);
};
}