From 480512d14fe4891a508cddbcb7e027d9181ae728 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Thu, 18 Mar 2021 07:50:06 -0230 Subject: [PATCH] Swaps support for local testnet (#10658) * Swaps support for local testnet * Create util method for comparison of token addresses/symbols to default swaps token * Get chainId from txMeta in _trackSwapsMetrics of transaction controller * Add comment to document purpose of getTransactionGroupRecipientAddressFilter * Use isSwapsDefaultTokenSymbol in place of repeated defaultTokenSymbol comparisons in build-quote.js --- app/scripts/controllers/swaps.js | 71 ++++++---- app/scripts/controllers/swaps.test.js | 13 +- app/scripts/controllers/transactions/index.js | 1 + app/scripts/metamask-controller.js | 3 + shared/constants/swaps.js | 52 +++++-- shared/modules/swaps.utils.js | 33 +++++ .../transaction-list.component.js | 37 ++++- .../app/wallet-overview/eth-overview.js | 14 +- .../app/wallet-overview/token-overview.js | 11 +- ui/app/ducks/swaps/swaps.js | 28 ++-- ui/app/hooks/useCurrentAsset.js | 17 ++- ui/app/hooks/useSwappedTokenValue.js | 26 +++- ui/app/hooks/useTokensToSearch.js | 31 ++-- .../hooks/useTransactionDisplayData.test.js | 4 + .../swaps/awaiting-swap/awaiting-swap.js | 29 ++-- ui/app/pages/swaps/build-quote/build-quote.js | 53 ++++--- ui/app/pages/swaps/index.js | 18 +-- ui/app/pages/swaps/intro-popup/intro-popup.js | 12 +- ui/app/pages/swaps/swaps.util.js | 132 ++++++++++-------- ui/app/pages/swaps/swaps.util.test.js | 40 +++--- ui/app/pages/swaps/view-quote/view-quote.js | 42 +++--- ui/app/selectors/selectors.js | 26 +++- 22 files changed, 466 insertions(+), 227 deletions(-) create mode 100644 shared/modules/swaps.utils.js diff --git a/app/scripts/controllers/swaps.js b/app/scripts/controllers/swaps.js index 0011984b3..fcb2fce00 100644 --- a/app/scripts/controllers/swaps.js +++ b/app/scripts/controllers/swaps.js @@ -8,12 +8,13 @@ import { calcTokenAmount } from '../../../ui/app/helpers/utils/token-util'; import { calcGasTotal } from '../../../ui/app/pages/send/send.utils'; import { conversionUtil } from '../../../ui/app/helpers/utils/conversion-util'; import { - ETH_SWAPS_TOKEN_OBJECT, DEFAULT_ERC20_APPROVE_GAS, QUOTES_EXPIRED_ERROR, QUOTES_NOT_AVAILABLE_ERROR, SWAPS_FETCH_ORDER_CONFLICT, } from '../../../shared/constants/swaps'; +import { isSwapsDefaultTokenAddress } from '../../../shared/modules/swaps.utils'; + import { fetchTradesInfo as defaultFetchTradesInfo, fetchSwapsFeatureLiveness as defaultFetchSwapsFeatureLiveness, @@ -85,6 +86,7 @@ export default class SwapsController { fetchTradesInfo = defaultFetchTradesInfo, fetchSwapsFeatureLiveness = defaultFetchSwapsFeatureLiveness, fetchSwapsQuoteRefreshTime = defaultFetchSwapsQuoteRefreshTime, + getCurrentChainId, }) { this.store = new ObservableStore({ swapsState: { ...initialState.swapsState }, @@ -93,6 +95,7 @@ export default class SwapsController { this._fetchTradesInfo = fetchTradesInfo; this._fetchSwapsFeatureLiveness = fetchSwapsFeatureLiveness; this._fetchSwapsQuoteRefreshTime = fetchSwapsQuoteRefreshTime; + this._getCurrentChainId = getCurrentChainId; this.getBufferedGasLimit = getBufferedGasLimit; this.tokenRatesStore = tokenRatesStore; @@ -116,10 +119,11 @@ export default class SwapsController { // Sets the refresh rate for quote updates from the MetaSwap API async _setSwapsQuoteRefreshTime() { + const chainId = this._getCurrentChainId(); // Default to fallback time unless API returns valid response let swapsQuoteRefreshTime = FALLBACK_QUOTE_REFRESH_TIME; try { - swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(); + swapsQuoteRefreshTime = await this._fetchSwapsQuoteRefreshTime(chainId); } catch (e) { console.error('Request for swaps quote refresh time failed: ', e); } @@ -158,6 +162,8 @@ export default class SwapsController { fetchParamsMetaData = {}, isPolledRequest, ) { + const { chainId } = fetchParamsMetaData; + if (!fetchParams) { return null; } @@ -177,7 +183,7 @@ export default class SwapsController { this.indexOfNewestCallInFlight = indexOfCurrentCall; let [newQuotes] = await Promise.all([ - this._fetchTradesInfo(fetchParams), + this._fetchTradesInfo(fetchParams, fetchParamsMetaData), this._setSwapsQuoteRefreshTime(), ]); @@ -191,7 +197,7 @@ export default class SwapsController { let approvalRequired = false; if ( - fetchParams.sourceToken !== ETH_SWAPS_TOKEN_OBJECT.address && + !isSwapsDefaultTokenAddress(fetchParams.sourceToken, chainId) && Object.values(newQuotes).length ) { const allowance = await this._getERC20Allowance( @@ -490,6 +496,7 @@ export default class SwapsController { const { swapsState: { customGasPrice }, } = this.store.getState(); + const chainId = this._getCurrentChainId(); const numQuotes = Object.keys(quotes).length; if (!numQuotes) { @@ -533,8 +540,8 @@ export default class SwapsController { // trade.value is a sum of different values depending on the transaction. // It always includes any external fees charged by the quote source. In - // addition, if the source asset is ETH, trade.value includes the amount - // of swapped ETH. + // addition, if the source asset is the selected chain's default token, trade.value + // includes the amount of that token. const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16).plus( trade.value, 16, @@ -549,21 +556,21 @@ export default class SwapsController { }); // The total fee is aggregator/exchange fees plus gas fees. - // If the swap is from ETH, subtract the sourceAmount from the total cost. - // Otherwise, the total fee is simply trade.value plus gas fees. - const ethFee = - sourceToken === ETH_SWAPS_TOKEN_OBJECT.address - ? conversionUtil( - totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei - { - fromCurrency: 'ETH', - fromDenomination: 'WEI', - toDenomination: 'ETH', - fromNumericBase: 'BN', - numberOfDecimals: 6, - }, - ) - : totalEthCost; + // If the swap is from the selected chain's default token, subtract + // the sourceAmount from the total cost. Otherwise, the total fee + // is simply trade.value plus gas fees. + const ethFee = isSwapsDefaultTokenAddress(sourceToken, chainId) + ? conversionUtil( + totalWeiCost.minus(sourceAmount, 10), // sourceAmount is in wei + { + fromCurrency: 'ETH', + fromDenomination: 'WEI', + toDenomination: 'ETH', + fromNumericBase: 'BN', + numberOfDecimals: 6, + }, + ) + : totalEthCost; const decimalAdjustedDestinationAmount = calcTokenAmount( destinationAmount, @@ -588,10 +595,12 @@ export default class SwapsController { 10, ); - const conversionRateForCalculations = - destinationToken === ETH_SWAPS_TOKEN_OBJECT.address - ? 1 - : tokenConversionRate; + const conversionRateForCalculations = isSwapsDefaultTokenAddress( + destinationToken, + chainId, + ) + ? 1 + : tokenConversionRate; const overallValueOfQuoteForSorting = conversionRateForCalculations === undefined @@ -618,8 +627,10 @@ export default class SwapsController { }); const isBest = - newQuotes[topAggId].destinationToken === ETH_SWAPS_TOKEN_OBJECT.address || - Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]); + isSwapsDefaultTokenAddress( + newQuotes[topAggId].destinationToken, + chainId, + ) || Boolean(tokenConversionRates[newQuotes[topAggId]?.destinationToken]); let savings = null; @@ -726,13 +737,17 @@ export default class SwapsController { async _fetchAndSetSwapsLiveness() { const { swapsState } = this.store.getState(); const { swapsFeatureIsLive: oldSwapsFeatureIsLive } = swapsState; + const chainId = this._getCurrentChainId(); + let swapsFeatureIsLive = false; let successfullyFetched = false; let numAttempts = 0; const fetchAndIncrementNumAttempts = async () => { try { - swapsFeatureIsLive = Boolean(await this._fetchSwapsFeatureLiveness()); + swapsFeatureIsLive = Boolean( + await this._fetchSwapsFeatureLiveness(chainId), + ); successfullyFetched = true; } catch (err) { log.error(err); diff --git a/app/scripts/controllers/swaps.test.js b/app/scripts/controllers/swaps.test.js index 8fd07a18b..12169c83c 100644 --- a/app/scripts/controllers/swaps.test.js +++ b/app/scripts/controllers/swaps.test.js @@ -8,6 +8,7 @@ import { ObservableStore } from '@metamask/obs-store'; import { ROPSTEN_NETWORK_ID, MAINNET_NETWORK_ID, + MAINNET_CHAIN_ID, } from '../../../shared/constants/network'; import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; import { createTestProviderTools } from '../../../test/stub/provider'; @@ -75,6 +76,7 @@ const MOCK_FETCH_METADATA = { symbol: 'FOO', decimals: 18, }, + chainId: MAINNET_CHAIN_ID, }; const MOCK_TOKEN_RATES_STORE = new ObservableStore({ @@ -131,6 +133,8 @@ const sandbox = sinon.createSandbox(); const fetchTradesInfoStub = sandbox.stub(); const fetchSwapsFeatureLivenessStub = sandbox.stub(); const fetchSwapsQuoteRefreshTimeStub = sandbox.stub(); +const getCurrentChainIdStub = sandbox.stub(); +getCurrentChainIdStub.returns(MAINNET_CHAIN_ID); describe('SwapsController', function () { let provider; @@ -145,6 +149,7 @@ describe('SwapsController', function () { fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, fetchSwapsQuoteRefreshTime: fetchSwapsQuoteRefreshTimeStub, + getCurrentChainId: getCurrentChainIdStub, }); }; @@ -194,6 +199,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -218,6 +224,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -242,6 +249,7 @@ describe('SwapsController', function () { tokenRatesStore: MOCK_TOKEN_RATES_STORE, fetchTradesInfo: fetchTradesInfoStub, fetchSwapsFeatureLiveness: fetchSwapsFeatureLivenessStub, + getCurrentChainId: getCurrentChainIdStub, }); const currentEthersInstance = swapsController.ethersProvider; const onNetworkDidChange = networkController.on.getCall(0).args[1]; @@ -686,7 +694,10 @@ describe('SwapsController', function () { }); assert.strictEqual( - fetchTradesInfoStub.calledOnceWithExactly(MOCK_FETCH_PARAMS), + fetchTradesInfoStub.calledOnceWithExactly( + MOCK_FETCH_PARAMS, + MOCK_FETCH_METADATA, + ), true, ); }); diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index fe88b897e..e61d747bd 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -958,6 +958,7 @@ export default class TransactionController extends EventEmitter { txMeta.txParams.from, txMeta.destinationTokenDecimals, approvalTxMeta, + txMeta.chainId, ); const quoteVsExecutionRatio = `${new BigNumber(tokensReceived, 10) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9eaaf17f7..d34c5c86f 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -383,6 +383,9 @@ export default class MetamaskController extends EventEmitter { this.networkController, ), tokenRatesStore: this.tokenRatesController.store, + getCurrentChainId: this.networkController.getCurrentChainId.bind( + this.networkController, + ), }); // ensure accountTracker updates balances after network change diff --git a/shared/constants/swaps.js b/shared/constants/swaps.js index 7ae20c5ae..17898d256 100644 --- a/shared/constants/swaps.js +++ b/shared/constants/swaps.js @@ -1,3 +1,12 @@ +import { MAINNET_CHAIN_ID } from './network'; + +export const QUOTES_EXPIRED_ERROR = 'quotes-expired'; +export const SWAP_FAILED_ERROR = 'swap-failed-error'; +export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes'; +export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable'; +export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance'; +export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict'; + // An address that the metaswap-api recognizes as ETH, in place of the token address that ERC-20 tokens have const ETH_SWAPS_TOKEN_ADDRESS = '0x0000000000000000000000000000000000000000'; @@ -9,17 +18,42 @@ export const ETH_SWAPS_TOKEN_OBJECT = { iconUrl: 'images/black-eth-logo.svg', }; -export const QUOTES_EXPIRED_ERROR = 'quotes-expired'; -export const SWAP_FAILED_ERROR = 'swap-failed-error'; -export const ERROR_FETCHING_QUOTES = 'error-fetching-quotes'; -export const QUOTES_NOT_AVAILABLE_ERROR = 'quotes-not-avilable'; -export const OFFLINE_FOR_MAINTENANCE = 'offline-for-maintenance'; -export const SWAPS_FETCH_ORDER_CONFLICT = 'swaps-fetch-order-conflict'; +const TEST_ETH_SWAPS_TOKEN_OBJECT = { + symbol: 'TESTETH', + name: 'Test Ether', + address: ETH_SWAPS_TOKEN_ADDRESS, + decimals: 18, + iconUrl: 'images/black-eth-logo.svg', +}; // A gas value for ERC20 approve calls that should be sufficient for all ERC20 approve implementations export const DEFAULT_ERC20_APPROVE_GAS = '0x1d4c0'; -export const SWAPS_CONTRACT_ADDRESS = - '0x881d40237659c251811cec9c364ef91dc08d300c'; +const MAINNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; -export const METASWAP_API_HOST = 'https://api.metaswap.codefi.network'; +const TESTNET_CONTRACT_ADDRESS = '0x881d40237659c251811cec9c364ef91dc08d300c'; + +const METASWAP_ETH_API_HOST = 'https://api.metaswap.codefi.network'; + +const SWAPS_TESTNET_CHAIN_ID = '0x539'; +const SWAPS_TESTNET_HOST = 'https://metaswap-api.airswap-dev.codefi.network'; + +export const ALLOWED_SWAPS_CHAIN_IDS = { + [MAINNET_CHAIN_ID]: true, + [SWAPS_TESTNET_CHAIN_ID]: true, +}; + +export const METASWAP_CHAINID_API_HOST_MAP = { + [MAINNET_CHAIN_ID]: METASWAP_ETH_API_HOST, + [SWAPS_TESTNET_CHAIN_ID]: SWAPS_TESTNET_HOST, +}; + +export const SWAPS_CHAINID_CONTRACT_ADDRESS_MAP = { + [MAINNET_CHAIN_ID]: MAINNET_CONTRACT_ADDRESS, + [SWAPS_TESTNET_CHAIN_ID]: TESTNET_CONTRACT_ADDRESS, +}; + +export const SWAPS_CHAINID_DEFAULT_TOKEN_MAP = { + [MAINNET_CHAIN_ID]: ETH_SWAPS_TOKEN_OBJECT, + [SWAPS_TESTNET_CHAIN_ID]: TEST_ETH_SWAPS_TOKEN_OBJECT, +}; diff --git a/shared/modules/swaps.utils.js b/shared/modules/swaps.utils.js new file mode 100644 index 000000000..799f17d1d --- /dev/null +++ b/shared/modules/swaps.utils.js @@ -0,0 +1,33 @@ +import { SWAPS_CHAINID_DEFAULT_TOKEN_MAP } from '../constants/swaps'; + +/** + * Checks whether the provided address is strictly equal to the address for + * the default swaps token of the provided chain. + * + * @param {string} address - The string to compare to the default token address + * @param {string} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the address is the provided chain's default token address + */ +export function isSwapsDefaultTokenAddress(address, chainId) { + if (!address || !chainId) { + return false; + } + + return address === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.address; +} + +/** + * Checks whether the provided symbol is strictly equal to the symbol for + * the default swaps token of the provided chain. + * + * @param {string} symbol - The string to compare to the default token symbol + * @param {string} chainId - The hex encoded chain ID of the default swaps token to check + * @returns {boolean} Whether the symbl is the provided chain's default token symbol + */ +export function isSwapsDefaultTokenSymbol(symbol, chainId) { + if (!symbol || !chainId) { + return false; + } + + return symbol === SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]?.symbol; +} diff --git a/ui/app/components/app/transaction-list/transaction-list.component.js b/ui/app/components/app/transaction-list/transaction-list.component.js index 305fbed98..389a76025 100644 --- a/ui/app/components/app/transaction-list/transaction-list.component.js +++ b/ui/app/components/app/transaction-list/transaction-list.component.js @@ -5,20 +5,31 @@ import { nonceSortedCompletedTransactionsSelector, nonceSortedPendingTransactionsSelector, } from '../../../selectors/transactions'; +import { getCurrentChainId } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; import TransactionListItem from '../transaction-list-item'; import Button from '../../ui/button'; import { TOKEN_CATEGORY_HASH } from '../../../helpers/constants/transactions'; -import { SWAPS_CONTRACT_ADDRESS } from '../../../../../shared/constants/swaps'; +import { SWAPS_CHAINID_CONTRACT_ADDRESS_MAP } from '../../../../../shared/constants/swaps'; import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction'; const PAGE_INCREMENT = 10; -const getTransactionGroupRecipientAddressFilter = (recipientAddress) => { +// When we are on a token page, we only want to show transactions that involve that token. +// In the case of token transfers or approvals, these will be transactions sent to the +// token contract. In the case of swaps, these will be transactions sent to the swaps contract +// and which have the token address in the transaction data. +// +// getTransactionGroupRecipientAddressFilter is used to determine whether a transaction matches +// either of those criteria +const getTransactionGroupRecipientAddressFilter = ( + recipientAddress, + chainId, +) => { return ({ initialTransaction: { txParams } }) => { return ( txParams?.to === recipientAddress || - (txParams?.to === SWAPS_CONTRACT_ADDRESS && + (txParams?.to === SWAPS_CHAINID_CONTRACT_ADDRESS_MAP[chainId] && txParams.data.match(recipientAddress.slice(2))) ); }; @@ -39,12 +50,13 @@ const getFilteredTransactionGroups = ( transactionGroups, hideTokenTransactions, tokenAddress, + chainId, ) => { if (hideTokenTransactions) { return transactionGroups.filter(tokenTransactionFilter); } else if (tokenAddress) { return transactionGroups.filter( - getTransactionGroupRecipientAddressFilter(tokenAddress), + getTransactionGroupRecipientAddressFilter(tokenAddress, chainId), ); } return transactionGroups; @@ -63,6 +75,7 @@ export default function TransactionList({ const unfilteredCompletedTransactions = useSelector( nonceSortedCompletedTransactionsSelector, ); + const chainId = useSelector(getCurrentChainId); const pendingTransactions = useMemo( () => @@ -70,8 +83,14 @@ export default function TransactionList({ unfilteredPendingTransactions, hideTokenTransactions, tokenAddress, + chainId, ), - [hideTokenTransactions, tokenAddress, unfilteredPendingTransactions], + [ + hideTokenTransactions, + tokenAddress, + unfilteredPendingTransactions, + chainId, + ], ); const completedTransactions = useMemo( () => @@ -79,8 +98,14 @@ export default function TransactionList({ unfilteredCompletedTransactions, hideTokenTransactions, tokenAddress, + chainId, ), - [hideTokenTransactions, tokenAddress, unfilteredCompletedTransactions], + [ + hideTokenTransactions, + tokenAddress, + unfilteredCompletedTransactions, + chainId, + ], ); const viewMore = useCallback( diff --git a/ui/app/components/app/wallet-overview/eth-overview.js b/ui/app/components/app/wallet-overview/eth-overview.js index 38ada5c24..c4ef76115 100644 --- a/ui/app/components/app/wallet-overview/eth-overview.js +++ b/ui/app/components/app/wallet-overview/eth-overview.js @@ -25,7 +25,8 @@ import { getIsMainnet, getIsTestnet, getCurrentKeyring, - getSwapsEthToken, + getSwapsDefaultToken, + getIsSwapsChain, } from '../../../selectors/selectors'; import SwapIcon from '../../ui/icon/swap-icon.component'; import BuyIcon from '../../ui/icon/overview-buy-icon.component'; @@ -63,13 +64,14 @@ const EthOverview = ({ className }) => { const { balance } = selectedAccount; const isMainnetChain = useSelector(getIsMainnet); const isTestnetChain = useSelector(getIsTestnet); + const isSwapsChain = useSelector(getIsSwapsChain); const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Main View', active_currency: 'ETH' }, category: 'swaps', }); const swapsEnabled = useSelector(getSwapsFeatureLiveness); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); return ( { {swapsEnabled ? ( { - if (isMainnetChain) { + if (isSwapsChain) { enteredSwapsEvent(); - dispatch(setSwapsFromToken(swapsEthToken)); + dispatch(setSwapsFromToken(defaultSwapsToken)); if (usingHardwareWallet) { global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE); } else { @@ -154,7 +156,7 @@ const EthOverview = ({ className }) => { {contents} diff --git a/ui/app/components/app/wallet-overview/token-overview.js b/ui/app/components/app/wallet-overview/token-overview.js index e705461d9..953de1fd8 100644 --- a/ui/app/components/app/wallet-overview/token-overview.js +++ b/ui/app/components/app/wallet-overview/token-overview.js @@ -25,9 +25,8 @@ import { import { getAssetImages, getCurrentKeyring, - getCurrentChainId, + getIsSwapsChain, } from '../../../selectors/selectors'; -import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network'; import SwapIcon from '../../ui/icon/swap-icon.component'; import SendIcon from '../../ui/icon/overview-send-icon.component'; @@ -58,7 +57,7 @@ const TokenOverview = ({ className, token }) => { balanceToRender, token.symbol, ); - const chainId = useSelector(getCurrentChainId); + const isSwapsChain = useSelector(getIsSwapsChain); const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Token View', active_currency: token.symbol }, @@ -100,10 +99,10 @@ const TokenOverview = ({ className, token }) => { {swapsEnabled ? ( { - if (chainId === MAINNET_CHAIN_ID) { + if (isSwapsChain) { enteredSwapsEvent(); dispatch( setSwapsFromToken({ @@ -125,7 +124,7 @@ const TokenOverview = ({ className, token }) => { {contents} diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 40e97428f..ae3198629 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -49,7 +49,8 @@ import { getSelectedAccount, getTokenExchangeRates, getUSDConversionRate, - getSwapsEthToken, + getSwapsDefaultToken, + getCurrentChainId, } from '../../selectors'; import { ERROR_FETCHING_QUOTES, @@ -376,9 +377,11 @@ export const fetchQuotesAndSetQuoteState = ( metaMetricsEvent, ) => { return async (dispatch, getState) => { + const state = getState(); + const chainId = getCurrentChainId(state); let swapsFeatureIsLive = false; try { - swapsFeatureIsLive = await fetchSwapsFeatureLiveness(); + swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); } catch (error) { log.error('Failed to fetch Swaps liveness, defaulting to false.', error); } @@ -389,13 +392,14 @@ export const fetchQuotesAndSetQuoteState = ( return; } - const state = getState(); const fetchParams = getFetchParams(state); const selectedAccount = getSelectedAccount(state); const balanceError = getBalanceError(state); + const swapsDefaultToken = getSwapsDefaultToken(state); const fetchParamsFromToken = - fetchParams?.metaData?.sourceTokenInfo?.symbol === 'ETH' - ? getSwapsEthToken(state) + fetchParams?.metaData?.sourceTokenInfo?.symbol === + swapsDefaultToken.symbol + ? swapsDefaultToken : fetchParams?.metaData?.sourceTokenInfo; const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {}; const selectedToToken = @@ -420,7 +424,10 @@ export const fetchQuotesAndSetQuoteState = ( const contractExchangeRates = getTokenExchangeRates(state); let destinationTokenAddedForSwap = false; - if (toTokenSymbol !== 'ETH' && !contractExchangeRates[toTokenAddress]) { + if ( + toTokenSymbol !== swapsDefaultToken.symbol && + !contractExchangeRates[toTokenAddress] + ) { destinationTokenAddedForSwap = true; await dispatch( addToken( @@ -433,7 +440,7 @@ export const fetchQuotesAndSetQuoteState = ( ); } if ( - fromTokenSymbol !== 'ETH' && + fromTokenSymbol !== swapsDefaultToken.symbol && !contractExchangeRates[fromTokenAddress] && fromTokenBalance && new BigNumber(fromTokenBalance, 16).gt(0) @@ -494,6 +501,7 @@ export const fetchQuotesAndSetQuoteState = ( sourceTokenInfo, destinationTokenInfo, accountBalance: selectedAccount.balance, + chainId, }, ), ); @@ -563,9 +571,12 @@ export const fetchQuotesAndSetQuoteState = ( export const signAndSendTransactions = (history, metaMetricsEvent) => { return async (dispatch, getState) => { + const state = getState(); + const chainId = getCurrentChainId(state); + let swapsFeatureIsLive = false; try { - swapsFeatureIsLive = await fetchSwapsFeatureLiveness(); + swapsFeatureIsLive = await fetchSwapsFeatureLiveness(chainId); } catch (error) { log.error('Failed to fetch Swaps liveness, defaulting to false.', error); } @@ -576,7 +587,6 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => { return; } - const state = getState(); const customSwapsGas = getCustomSwapsGas(state); const fetchParams = getFetchParams(state); const { metaData, value: swapTokenValue, slippage } = fetchParams; diff --git a/ui/app/hooks/useCurrentAsset.js b/ui/app/hooks/useCurrentAsset.js index 382250e5f..832576a0c 100644 --- a/ui/app/hooks/useCurrentAsset.js +++ b/ui/app/hooks/useCurrentAsset.js @@ -1,13 +1,18 @@ import { useSelector } from 'react-redux'; import { useRouteMatch } from 'react-router-dom'; import { getTokens } from '../ducks/metamask/metamask'; +import { getCurrentChainId } from '../selectors'; import { ASSET_ROUTE } from '../helpers/constants/routes'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + ETH_SWAPS_TOKEN_OBJECT, +} from '../../../shared/constants/swaps'; /** * Returns a token object for the asset that is currently being viewed. - * Will return the ETH_SWAPS_TOKEN_OBJECT when the user is viewing either - * the primary, unfiltered, activity list or the ETH asset page. + * Will return the default token object for the current chain when the + * user is viewing either the primary, unfiltered, activity list or the + * default token asset page. * @returns {import('./useTokenDisplayValue').Token} */ export function useCurrentAsset() { @@ -22,6 +27,10 @@ export function useCurrentAsset() { const knownTokens = useSelector(getTokens); const token = tokenAddress && knownTokens.find(({ address }) => address === tokenAddress); + const chainId = useSelector(getCurrentChainId); - return token ?? ETH_SWAPS_TOKEN_OBJECT; + return ( + token ?? + (SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId] || ETH_SWAPS_TOKEN_OBJECT) + ); } diff --git a/ui/app/hooks/useSwappedTokenValue.js b/ui/app/hooks/useSwappedTokenValue.js index 788104bbb..6eff3726f 100644 --- a/ui/app/hooks/useSwappedTokenValue.js +++ b/ui/app/hooks/useSwappedTokenValue.js @@ -1,6 +1,11 @@ +import { useSelector } from 'react-redux'; import { TRANSACTION_TYPES } from '../../../shared/constants/transaction'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../shared/modules/swaps.utils'; import { getSwapsTokensReceivedFromTxMeta } from '../pages/swaps/swaps.util'; +import { getCurrentChainId } from '../selectors'; import { useTokenFiatAmount } from './useTokenFiatAmount'; /** @@ -14,10 +19,11 @@ import { useTokenFiatAmount } from './useTokenFiatAmount'; /** * A Swap transaction group's primaryTransaction contains details of the swap, * including the source (from) and destination (to) token type (ETH, DAI, etc..) - * When viewing a non ETH asset page, we need to determine if that asset is the - * token that was received (destination) from the swap. In that circumstance we - * would want to show the primaryCurrency in the activity list that is most relevant - * for that token (- 1000 DAI, for example, when swapping DAI for ETH). + * When viewing an asset page that is not for the current chain's default token, we + * need to determine if that asset is the token that was received (destination) from + * the swap. In that circumstance we would want to show the primaryCurrency in the + * activity list that is most relevant for that token (- 1000 DAI, for example, when + * swapping DAI for ETH). * @param {import('../selectors').transactionGroup} transactionGroup - Group of transactions by nonce * @param {import('./useTokenDisplayValue').Token} currentAsset - The current asset the user is looking at * @returns {SwappedTokenValue} @@ -27,11 +33,15 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { const { primaryTransaction, initialTransaction } = transactionGroup; const { type } = initialTransaction; const { from: senderAddress } = initialTransaction.txParams || {}; + const chainId = useSelector(getCurrentChainId); const isViewingReceivedTokenFromSwap = currentAsset?.symbol === primaryTransaction.destinationTokenSymbol || - (currentAsset.address === ETH_SWAPS_TOKEN_OBJECT.address && - primaryTransaction.destinationTokenSymbol === 'ETH'); + (isSwapsDefaultTokenAddress(currentAsset.address, chainId) && + isSwapsDefaultTokenSymbol( + primaryTransaction.destinationTokenSymbol, + chainId, + )); const swapTokenValue = type === TRANSACTION_TYPES.SWAP && isViewingReceivedTokenFromSwap @@ -41,6 +51,8 @@ export function useSwappedTokenValue(transactionGroup, currentAsset) { address, senderAddress, decimals, + null, + chainId, ) : type === TRANSACTION_TYPES.SWAP && primaryTransaction.swapTokenValue; diff --git a/ui/app/hooks/useTokensToSearch.js b/ui/app/hooks/useTokensToSearch.js index b1af93cdf..4542882f0 100644 --- a/ui/app/hooks/useTokensToSearch.js +++ b/ui/app/hooks/useTokensToSearch.js @@ -9,9 +9,11 @@ import { getTokenExchangeRates, getConversionRate, getCurrentCurrency, - getSwapsEthToken, + getSwapsDefaultToken, + getCurrentChainId, } from '../selectors'; import { getSwapsTokens } from '../ducks/swaps/swaps'; +import { isSwapsDefaultTokenSymbol } from '../../../shared/modules/swaps.utils'; import { useEqualityCheck } from './useEqualityCheck'; const tokenList = shuffle( @@ -28,12 +30,15 @@ export function getRenderableTokenData( contractExchangeRates, conversionRate, currentCurrency, + chainId, ) { const { symbol, name, address, iconUrl, string, balance, decimals } = token; const formattedFiat = getTokenFiatAmount( - symbol === 'ETH' ? 1 : contractExchangeRates[address], + isSwapsDefaultTokenSymbol(symbol, chainId) + ? 1 + : contractExchangeRates[address], conversionRate, currentCurrency, string, @@ -42,7 +47,9 @@ export function getRenderableTokenData( ) || ''; const rawFiat = getTokenFiatAmount( - symbol === 'ETH' ? 1 : contractExchangeRates[address], + isSwapsDefaultTokenSymbol(symbol, chainId) + ? 1 + : contractExchangeRates[address], conversionRate, currentCurrency, string, @@ -70,30 +77,32 @@ export function getRenderableTokenData( } export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { + const chainId = useSelector(getCurrentChainId); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const conversionRate = useSelector(getConversionRate); const currentCurrency = useSelector(getCurrentCurrency); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); const memoizedTopTokens = useEqualityCheck(topTokens); const memoizedUsersToken = useEqualityCheck(usersTokens); - const ethToken = getRenderableTokenData( - swapsEthToken, + const defaultToken = getRenderableTokenData( + defaultSwapsToken, tokenConversionRates, conversionRate, currentCurrency, + chainId, ); - const memoizedEthToken = useEqualityCheck(ethToken); + const memoizedDefaultToken = useEqualityCheck(defaultToken); const swapsTokens = useSelector(getSwapsTokens) || []; const tokensToSearch = swapsTokens.length ? swapsTokens : [ - memoizedEthToken, + memoizedDefaultToken, ...tokenList.filter( - (token) => token.symbol !== memoizedEthToken.symbol, + (token) => token.symbol !== memoizedDefaultToken.symbol, ), ]; @@ -116,9 +125,10 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { tokenConversionRates, conversionRate, currentCurrency, + chainId, ); if ( - renderableDataToken.symbol === 'ETH' || + isSwapsDefaultTokenSymbol(renderableDataToken.symbol, chainId) || (usersTokensAddressMap[token.address] && Number(renderableDataToken.balance ?? 0) !== 0) ) { @@ -150,5 +160,6 @@ export function useTokensToSearch({ usersTokens = [], topTokens = {} }) { conversionRate, currentCurrency, memoizedTopTokens, + chainId, ]); } diff --git a/ui/app/hooks/useTransactionDisplayData.test.js b/ui/app/hooks/useTransactionDisplayData.test.js index 531b5a649..76dca1bcd 100644 --- a/ui/app/hooks/useTransactionDisplayData.test.js +++ b/ui/app/hooks/useTransactionDisplayData.test.js @@ -10,11 +10,13 @@ import { getShouldShowFiat, getNativeCurrency, getCurrentCurrency, + getCurrentChainId, } from '../selectors'; import { getTokens } from '../ducks/metamask/metamask'; import { getMessage } from '../helpers/utils/i18n-helper'; import messages from '../../../app/_locales/en/messages.json'; import { ASSET_ROUTE, DEFAULT_ROUTE } from '../helpers/constants/routes'; +import { MAINNET_CHAIN_ID } from '../../../shared/constants/network'; import { TRANSACTION_TYPES, TRANSACTION_GROUP_CATEGORIES, @@ -164,6 +166,8 @@ describe('useTransactionDisplayData', function () { return 'ETH'; } else if (selector === getCurrentCurrency) { return 'ETH'; + } else if (selector === getCurrentChainId) { + return MAINNET_CHAIN_ID; } return null; }); diff --git a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js index e218cbb9a..7f39c12e8 100644 --- a/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js +++ b/ui/app/pages/swaps/awaiting-swap/awaiting-swap.js @@ -6,12 +6,14 @@ import { useHistory } from 'react-router-dom'; import { I18nContext } from '../../../contexts/i18n'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { MetaMetricsContext } from '../../../contexts/metametrics.new'; + import { getCurrentChainId, getCurrentCurrency, getRpcPrefsForCurrentProvider, getUSDConversionRate, } from '../../../selectors'; + import { getUsedQuote, getFetchParams, @@ -23,7 +25,6 @@ import { prepareToLeaveSwaps, } from '../../../ducks/swaps/swaps'; import Mascot from '../../../components/ui/mascot'; -import PulseLoader from '../../../components/ui/pulse-loader'; import { QUOTES_EXPIRED_ERROR, SWAP_FAILED_ERROR, @@ -31,6 +32,9 @@ import { QUOTES_NOT_AVAILABLE_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../../../shared/constants/swaps'; +import { isSwapsDefaultTokenSymbol } from '../../../../../shared/modules/swaps.utils'; +import PulseLoader from '../../../components/ui/pulse-loader'; + import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { getRenderableNetworkFeesForQuote } from '../swaps.util'; @@ -73,16 +77,17 @@ export default function AwaitingSwap({ let feeinUnformattedFiat; if (usedQuote && swapsGasPrice) { - const renderableNetworkFees = getRenderableNetworkFeesForQuote( - usedQuote.gasEstimateWithRefund || usedQuote.averageGas, - approveTxParams?.gas || '0x0', - swapsGasPrice, + const renderableNetworkFees = getRenderableNetworkFeesForQuote({ + tradeGas: usedQuote.gasEstimateWithRefund || usedQuote.averageGas, + approveGas: approveTxParams?.gas || '0x0', + gasPrice: swapsGasPrice, currentCurrency, - usdConversionRate, - usedQuote?.trade?.value, - sourceTokenInfo?.symbol, - usedQuote.sourceAmount, - ); + conversionRate: usdConversionRate, + tradeValue: usedQuote?.trade?.value, + sourceSymbol: sourceTokenInfo?.symbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + }); feeinUnformattedFiat = renderableNetworkFees.rawNetworkFees; } @@ -228,7 +233,9 @@ export default function AwaitingSwap({ ); } else if (errorKey) { await dispatch(navigateBackToBuildQuote(history)); - } else if (destinationTokenInfo?.symbol === 'ETH') { + } else if ( + isSwapsDefaultTokenSymbol(destinationTokenInfo?.symbol, chainId) + ) { history.push(DEFAULT_ROUTE); } else { history.push(`${ASSET_ROUTE}/${destinationTokenInfo?.address}`); diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index 7648efe47..d106fa18d 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -29,10 +29,11 @@ import { getFetchParams, } from '../../../ducks/swaps/swaps'; import { - getSwapsEthToken, + getSwapsDefaultToken, getTokenExchangeRates, getConversionRate, getCurrentCurrency, + getCurrentChainId, } from '../../../selectors'; import { getValueFromWeiHex, @@ -44,7 +45,10 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker'; import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'; import { useEthFiatAmount } from '../../../hooks/useEthFiatAmount'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../../../shared/constants/swaps'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../../../shared/modules/swaps.utils'; import { resetSwapsPostFetchState, removeToken } from '../../../store/actions'; import { fetchTokenPrice, fetchTokenBalance } from '../swaps.util'; @@ -84,21 +88,29 @@ export default function BuildQuote({ const topAssets = useSelector(getTopAssets); const fromToken = useSelector(getFromToken); const toToken = useSelector(getToToken) || destinationTokenInfo; - const swapsEthToken = useSelector(getSwapsEthToken); - const fetchParamsFromToken = - sourceTokenInfo?.symbol === 'ETH' ? swapsEthToken : sourceTokenInfo; + const defaultSwapsToken = useSelector(getSwapsDefaultToken); + const chainId = useSelector(getCurrentChainId); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const conversionRate = useSelector(getConversionRate); const currentCurrency = useSelector(getCurrentCurrency); + const fetchParamsFromToken = isSwapsDefaultTokenSymbol( + sourceTokenInfo?.symbol, + chainId, + ) + ? defaultSwapsToken + : sourceTokenInfo; + const { loading, tokensWithBalances } = useTokenTracker(tokens); // If the fromToken was set in a call to `onFromSelect` (see below), and that from token has a balance // but is not in tokensWithBalances or tokens, then we want to add it to the usersTokens array so that // the balance of the token can appear in the from token selection dropdown const fromTokenArray = - fromToken?.symbol !== 'ETH' && fromToken?.balance ? [fromToken] : []; + !isSwapsDefaultTokenSymbol(fromToken?.symbol, chainId) && fromToken?.balance + ? [fromToken] + : []; const usersTokens = uniqBy( [...tokensWithBalances, ...tokens, ...fromTokenArray], 'address', @@ -110,6 +122,7 @@ export default function BuildQuote({ tokenConversionRates, conversionRate, currentCurrency, + chainId, ); const tokensToSearch = useTokensToSearch({ @@ -119,9 +132,9 @@ export default function BuildQuote({ const selectedToToken = tokensToSearch.find(({ address }) => address === toToken?.address) || toToken; - const toTokenIsNotEth = + const toTokenIsNotDefault = selectedToToken?.address && - selectedToToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address; + !isSwapsDefaultTokenAddress(selectedToToken?.address, chainId); const occurances = Number(selectedToToken?.occurances || 0); const { address: fromTokenAddress, @@ -151,8 +164,9 @@ export default function BuildQuote({ { showFiat: true }, true, ); - const swapFromFiatValue = - fromTokenSymbol === 'ETH' ? swapFromEthFiatValue : swapFromTokenFiatValue; + const swapFromFiatValue = isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) + ? swapFromEthFiatValue + : swapFromTokenFiatValue; const onFromSelect = (token) => { if ( @@ -227,15 +241,17 @@ export default function BuildQuote({ ); useEffect(() => { - const notEth = - tokensWithBalancesFromToken?.address !== ETH_SWAPS_TOKEN_OBJECT.address; + const notDefault = !isSwapsDefaultTokenAddress( + tokensWithBalancesFromToken?.address, + chainId, + ); const addressesAreTheSame = tokensWithBalancesFromToken?.address === previousTokensWithBalancesFromToken?.address; const balanceHasChanged = tokensWithBalancesFromToken?.balance !== previousTokensWithBalancesFromToken?.balance; - if (notEth && addressesAreTheSame && balanceHasChanged) { + if (notDefault && addressesAreTheSame && balanceHasChanged) { dispatch( setSwapsFromToken({ ...fromToken, @@ -249,12 +265,13 @@ export default function BuildQuote({ tokensWithBalancesFromToken, previousTokensWithBalancesFromToken, fromToken, + chainId, ]); // If the eth balance changes while on build quote, we update the selected from token useEffect(() => { if ( - fromToken?.address === ETH_SWAPS_TOKEN_OBJECT.address && + isSwapsDefaultTokenAddress(fromToken?.address, chainId) && fromToken?.balance !== hexToDecimal(ethBalance) ) { dispatch( @@ -269,7 +286,7 @@ export default function BuildQuote({ }), ); } - }, [dispatch, fromToken, ethBalance]); + }, [dispatch, fromToken, ethBalance, chainId]); useEffect(() => { if (prevFromTokenBalance !== fromTokenBalance) { @@ -286,7 +303,7 @@ export default function BuildQuote({
{t('swapSwapFrom')}
- {fromTokenSymbol !== 'ETH' && ( + {!isSwapsDefaultTokenSymbol(fromTokenSymbol, chainId) && (
@@ -384,7 +401,7 @@ export default function BuildQuote({ defaultToAll />
- {toTokenIsNotEth && + {toTokenIsNotDefault && (occurances < 2 ? ( MAX_ALLOWED_SLIPPAGE || - (toTokenIsNotEth && occurances < 2 && !verificationClicked) + (toTokenIsNotDefault && occurances < 2 && !verificationClicked) } hideCancel showTermsOfService diff --git a/ui/app/pages/swaps/index.js b/ui/app/pages/swaps/index.js index 75752d24c..04f4505b3 100644 --- a/ui/app/pages/swaps/index.js +++ b/ui/app/pages/swaps/index.js @@ -12,6 +12,7 @@ import { I18nContext } from '../../contexts/i18n'; import { getSelectedAccount, getCurrentChainId, + getIsSwapsChain, } from '../../selectors/selectors'; import { getQuotes, @@ -45,7 +46,6 @@ import { SWAP_FAILED_ERROR, OFFLINE_FOR_MAINTENANCE, } from '../../../../shared/constants/swaps'; -import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { resetBackgroundSwapsState, @@ -96,6 +96,8 @@ export default function Swap() { const fetchingQuotes = useSelector(getFetchingQuotes); let swapsErrorKey = useSelector(getSwapsErrorKey); const swapsEnabled = useSelector(getSwapsFeatureLiveness); + const chainId = useSelector(getCurrentChainId); + const isSwapsChain = useSelector(getIsSwapsChain); const { balance: ethBalance, @@ -116,6 +118,7 @@ export default function Swap() { selectedAccountAddress, destinationTokenInfo?.decimals, approveTxData, + chainId, ); const tradeConfirmed = tradeTxData?.status === TRANSACTION_STATUSES.CONFIRMED; const approveError = @@ -155,26 +158,26 @@ export default function Swap() { }, []); useEffect(() => { - fetchTokens() + fetchTokens(chainId) .then((tokens) => { dispatch(setSwapsTokens(tokens)); }) .catch((error) => console.error(error)); - fetchTopAssets().then((topAssets) => { + fetchTopAssets(chainId).then((topAssets) => { dispatch(setTopAssets(topAssets)); }); - fetchAggregatorMetadata().then((newAggregatorMetadata) => { + fetchAggregatorMetadata(chainId).then((newAggregatorMetadata) => { dispatch(setAggregatorMetadata(newAggregatorMetadata)); }); - dispatch(fetchAndSetSwapsGasPriceInfo()); + dispatch(fetchAndSetSwapsGasPriceInfo(chainId)); return () => { dispatch(prepareToLeaveSwaps()); }; - }, [dispatch]); + }, [dispatch, chainId]); const exitedSwapsEvent = useNewMetricEvent({ event: 'Exited Swaps', @@ -224,8 +227,7 @@ export default function Swap() { return () => window.removeEventListener('beforeunload', fn); }, [dispatch, isLoadingQuotesRoute]); - const chainId = useSelector(getCurrentChainId); - if (chainId !== MAINNET_CHAIN_ID) { + if (!isSwapsChain) { return ; } diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.js b/ui/app/pages/swaps/intro-popup/intro-popup.js index 359cd0d34..658c84bb9 100644 --- a/ui/app/pages/swaps/intro-popup/intro-popup.js +++ b/ui/app/pages/swaps/intro-popup/intro-popup.js @@ -6,7 +6,7 @@ import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import { I18nContext } from '../../../contexts/i18n'; import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; -import { getSwapsEthToken } from '../../../selectors'; +import { getSwapsDefaultToken } from '../../../selectors'; import Button from '../../../components/ui/button'; import Popover from '../../../components/ui/popover'; @@ -14,9 +14,14 @@ export default function IntroPopup({ onClose }) { const dispatch = useDispatch(useDispatch); const history = useHistory(); const t = useContext(I18nContext); + + const swapsDefaultToken = useSelector(getSwapsDefaultToken); const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', - properties: { source: 'Intro popup', active_currency: 'ETH' }, + properties: { + source: 'Intro popup', + active_currency: swapsDefaultToken.symbol, + }, category: 'swaps', }); const blogPostVisitedEvent = useNewMetricEvent({ @@ -31,7 +36,6 @@ export default function IntroPopup({ onClose }) { event: 'Product Overview Dismissed', category: 'swaps', }); - const swapsEthToken = useSelector(getSwapsEthToken); return (
@@ -51,7 +55,7 @@ export default function IntroPopup({ onClose }) { onClick={() => { onClose(); enteredSwapsEvent(); - dispatch(setSwapsFromToken(swapsEthToken)); + dispatch(setSwapsFromToken(swapsDefaultToken)); history.push(BUILD_QUOTE_ROUTE); }} > diff --git a/ui/app/pages/swaps/swaps.util.js b/ui/app/pages/swaps/swaps.util.js index 85c8a5cdf..8e2c89bac 100644 --- a/ui/app/pages/swaps/swaps.util.js +++ b/ui/app/pages/swaps/swaps.util.js @@ -3,9 +3,15 @@ import BigNumber from 'bignumber.js'; import abi from 'human-standard-token-abi'; import { isValidAddress } from 'ethereumjs-util'; import { - ETH_SWAPS_TOKEN_OBJECT, - METASWAP_API_HOST, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + METASWAP_CHAINID_API_HOST_MAP, } from '../../../../shared/constants/swaps'; +import { + isSwapsDefaultTokenAddress, + isSwapsDefaultTokenSymbol, +} from '../../../../shared/modules/swaps.utils'; + +import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { calcTokenValue, calcTokenAmount, @@ -30,22 +36,22 @@ const TOKEN_TRANSFER_LOG_TOPIC_HASH = const CACHE_REFRESH_ONE_HOUR = 3600000; -const getBaseApi = function (type) { +const getBaseApi = function (type, chainId = MAINNET_CHAIN_ID) { switch (type) { case 'trade': - return `${METASWAP_API_HOST}/trades?`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/trades?`; case 'tokens': - return `${METASWAP_API_HOST}/tokens`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/tokens`; case 'topAssets': - return `${METASWAP_API_HOST}/topAssets`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/topAssets`; case 'featureFlag': - return `${METASWAP_API_HOST}/featureFlag`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/featureFlag`; case 'aggregatorMetadata': - return `${METASWAP_API_HOST}/aggregatorMetadata`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/aggregatorMetadata`; case 'gasPrices': - return `${METASWAP_API_HOST}/gasPrices`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/gasPrices`; case 'refreshTime': - return `${METASWAP_API_HOST}/quoteRefreshRate`; + return `${METASWAP_CHAINID_API_HOST_MAP[chainId]}/quoteRefreshRate`; default: throw new Error('getBaseApi requires an api call type'); } @@ -205,15 +211,18 @@ function validateData(validators, object, urlUsed) { }); } -export async function fetchTradesInfo({ - slippage, - sourceToken, - sourceDecimals, - destinationToken, - value, - fromAddress, - exchangeList, -}) { +export async function fetchTradesInfo( + { + slippage, + sourceToken, + sourceDecimals, + destinationToken, + value, + fromAddress, + exchangeList, + }, + { chainId }, +) { const urlParams = { destinationToken, sourceToken, @@ -228,7 +237,7 @@ export async function fetchTradesInfo({ } const queryString = new URLSearchParams(urlParams).toString(); - const tradeURL = `${getBaseApi('trade')}${queryString}`; + const tradeURL = `${getBaseApi('trade', chainId)}${queryString}`; const tradesResponse = await fetchWithCache( tradeURL, { method: 'GET' }, @@ -272,21 +281,21 @@ export async function fetchTradesInfo({ return newQuotes; } -export async function fetchTokens() { - const tokenUrl = getBaseApi('tokens'); +export async function fetchTokens(chainId) { + const tokenUrl = getBaseApi('tokens', chainId); const tokens = await fetchWithCache( tokenUrl, { method: 'GET' }, { cacheRefreshTime: CACHE_REFRESH_ONE_HOUR }, ); const filteredTokens = [ - ETH_SWAPS_TOKEN_OBJECT, + SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId], ...tokens.filter((token) => { return ( validateData(TOKEN_VALIDATORS, token, tokenUrl) && !( - token.symbol === ETH_SWAPS_TOKEN_OBJECT.symbol || - token.address === ETH_SWAPS_TOKEN_OBJECT.address + isSwapsDefaultTokenSymbol(token.symbol, chainId) || + isSwapsDefaultTokenAddress(token.address, chainId) ) ); }), @@ -294,8 +303,8 @@ export async function fetchTokens() { return filteredTokens; } -export async function fetchAggregatorMetadata() { - const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata'); +export async function fetchAggregatorMetadata(chainId) { + const aggregatorMetadataUrl = getBaseApi('aggregatorMetadata', chainId); const aggregators = await fetchWithCache( aggregatorMetadataUrl, { method: 'GET' }, @@ -316,8 +325,8 @@ export async function fetchAggregatorMetadata() { return filteredAggregators; } -export async function fetchTopAssets() { - const topAssetsUrl = getBaseApi('topAssets'); +export async function fetchTopAssets(chainId) { + const topAssetsUrl = getBaseApi('topAssets', chainId); const response = await fetchWithCache( topAssetsUrl, { method: 'GET' }, @@ -332,18 +341,18 @@ export async function fetchTopAssets() { return topAssetsMap; } -export async function fetchSwapsFeatureLiveness() { +export async function fetchSwapsFeatureLiveness(chainId) { const status = await fetchWithCache( - getBaseApi('featureFlag'), + getBaseApi('featureFlag', chainId), { method: 'GET' }, { cacheRefreshTime: 600000 }, ); return status?.active; } -export async function fetchSwapsQuoteRefreshTime() { +export async function fetchSwapsQuoteRefreshTime(chainId) { const response = await fetchWithCache( - getBaseApi('refreshTime'), + getBaseApi('refreshTime', chainId), { method: 'GET' }, { cacheRefreshTime: 600000 }, ); @@ -378,8 +387,8 @@ export async function fetchTokenBalance(address, userAddress) { return usersToken; } -export async function fetchSwapsGasPrices() { - const gasPricesUrl = getBaseApi('gasPrices'); +export async function fetchSwapsGasPrices(chainId) { + const gasPricesUrl = getBaseApi('gasPrices', chainId); const response = await fetchWithCache( gasPricesUrl, { method: 'GET' }, @@ -408,7 +417,7 @@ export async function fetchSwapsGasPrices() { }; } -export function getRenderableNetworkFeesForQuote( +export function getRenderableNetworkFeesForQuote({ tradeGas, approveGas, gasPrice, @@ -417,14 +426,18 @@ export function getRenderableNetworkFeesForQuote( tradeValue, sourceSymbol, sourceAmount, -) { + chainId, +}) { const totalGasLimitForCalculation = new BigNumber(tradeGas || '0x0', 16) .plus(approveGas || '0x0', 16) .toString(16); const gasTotalInWeiHex = calcGasTotal(totalGasLimitForCalculation, gasPrice); const nonGasFee = new BigNumber(tradeValue, 16) - .minus(sourceSymbol === 'ETH' ? sourceAmount : 0, 10) + .minus( + isSwapsDefaultTokenSymbol(sourceSymbol, chainId) ? sourceAmount : 0, + 10, + ) .toString(16); const totalWeiCost = new BigNumber(gasTotalInWeiHex, 16) @@ -447,7 +460,7 @@ export function getRenderableNetworkFeesForQuote( rawNetworkFees, rawEthFee: ethFee, feeInFiat: formattedNetworkFee, - feeInEth: `${ethFee} ETH`, + feeInEth: `${ethFee} ${SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId].symbol}`, nonGasFee, }; } @@ -459,6 +472,7 @@ export function quotesToRenderableData( currentCurrency, approveGas, tokenConversionRates, + chainId, ) { return Object.values(quotes).map((quote) => { const { @@ -488,16 +502,17 @@ export function quotesToRenderableData( rawNetworkFees, rawEthFee, feeInEth, - } = getRenderableNetworkFeesForQuote( - gasEstimateWithRefund || decimalToHex(averageGas || 800000), + } = getRenderableNetworkFeesForQuote({ + tradeGas: gasEstimateWithRefund || decimalToHex(averageGas || 800000), approveGas, gasPrice, currentCurrency, conversionRate, - trade.value, - sourceTokenInfo.symbol, + tradeValue: trade.value, + sourceSymbol: sourceTokenInfo.symbol, sourceAmount, - ); + chainId, + }); const slippageMultiplier = new BigNumber(100 - slippage).div(100); const minimumAmountReceived = new BigNumber(destinationValue) @@ -506,18 +521,20 @@ export function quotesToRenderableData( const tokenConversionRate = tokenConversionRates[destinationTokenInfo.address]; - const ethValueOfTrade = - destinationTokenInfo.symbol === 'ETH' - ? calcTokenAmount( - destinationAmount, - destinationTokenInfo.decimals, - ).minus(rawEthFee, 10) - : new BigNumber(tokenConversionRate || 0, 10) - .times( - calcTokenAmount(destinationAmount, destinationTokenInfo.decimals), - 10, - ) - .minus(rawEthFee, 10); + const ethValueOfTrade = isSwapsDefaultTokenSymbol( + destinationTokenInfo.symbol, + chainId, + ) + ? calcTokenAmount(destinationAmount, destinationTokenInfo.decimals).minus( + rawEthFee, + 10, + ) + : new BigNumber(tokenConversionRate || 0, 10) + .times( + calcTokenAmount(destinationAmount, destinationTokenInfo.decimals), + 10, + ) + .minus(rawEthFee, 10); let liquiditySourceKey; let renderedSlippage = slippage; @@ -566,9 +583,10 @@ export function getSwapsTokensReceivedFromTxMeta( accountAddress, tokenDecimals, approvalTxMeta, + chainId, ) { const txReceipt = txMeta?.txReceipt; - if (tokenSymbol === 'ETH') { + if (isSwapsDefaultTokenSymbol(tokenSymbol, chainId)) { if ( !txReceipt || !txMeta || diff --git a/ui/app/pages/swaps/swaps.util.test.js b/ui/app/pages/swaps/swaps.util.test.js index c68cff29b..df305e675 100644 --- a/ui/app/pages/swaps/swaps.util.test.js +++ b/ui/app/pages/swaps/swaps.util.test.js @@ -1,5 +1,6 @@ import { strict as assert } from 'assert'; import proxyquire from 'proxyquire'; +import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network'; import { TRADES_BASE_PROD_URL, TOKENS_BASE_PROD_URL, @@ -89,42 +90,45 @@ describe('Swaps Util', function () { }, }; it('should fetch trade info on prod', async function () { - const result = await fetchTradesInfo({ - TOKENS, - slippage: '3', - sourceToken: TOKENS[0].address, - destinationToken: TOKENS[1].address, - value: '2000000000000000000', - fromAddress: '0xmockAddress', - sourceSymbol: TOKENS[0].symbol, - sourceDecimals: TOKENS[0].decimals, - sourceTokenInfo: { ...TOKENS[0] }, - destinationTokenInfo: { ...TOKENS[1] }, - }); + const result = await fetchTradesInfo( + { + TOKENS, + slippage: '3', + sourceToken: TOKENS[0].address, + destinationToken: TOKENS[1].address, + value: '2000000000000000000', + fromAddress: '0xmockAddress', + sourceSymbol: TOKENS[0].symbol, + sourceDecimals: TOKENS[0].decimals, + sourceTokenInfo: { ...TOKENS[0] }, + destinationTokenInfo: { ...TOKENS[1] }, + }, + { chainId: MAINNET_CHAIN_ID }, + ); assert.deepStrictEqual(result, expectedResult2); }); }); describe('fetchTokens', function () { it('should fetch tokens', async function () { - const result = await fetchTokens(true); + const result = await fetchTokens(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT); }); it('should fetch tokens on prod', async function () { - const result = await fetchTokens(false); + const result = await fetchTokens(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, EXPECTED_TOKENS_RESULT); }); }); describe('fetchAggregatorMetadata', function () { it('should fetch aggregator metadata', async function () { - const result = await fetchAggregatorMetadata(true); + const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, AGGREGATOR_METADATA); }); it('should fetch aggregator metadata on prod', async function () { - const result = await fetchAggregatorMetadata(false); + const result = await fetchAggregatorMetadata(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, AGGREGATOR_METADATA); }); }); @@ -148,12 +152,12 @@ describe('Swaps Util', function () { }, }; it('should fetch top assets', async function () { - const result = await fetchTopAssets(true); + const result = await fetchTopAssets(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, expectedResult); }); it('should fetch top assets on prod', async function () { - const result = await fetchTopAssets(false); + const result = await fetchTopAssets(MAINNET_CHAIN_ID); assert.deepStrictEqual(result, expectedResult); }); }); diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index f16af36a0..02cf4b464 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -36,7 +36,8 @@ import { getSelectedAccount, getCurrentCurrency, getTokenExchangeRates, - getSwapsEthToken, + getSwapsDefaultToken, + getCurrentChainId, } from '../../../selectors'; import { toPrecisionWithoutTrailingZeros } from '../../../helpers/utils/util'; import { getTokens } from '../../../ducks/metamask/metamask'; @@ -125,7 +126,8 @@ export default function ViewQuote() { const usedQuote = selectedQuote || topQuote; const tradeValue = usedQuote?.trade?.value ?? '0x0'; const swapsQuoteRefreshTime = useSelector(getSwapsQuoteRefreshTime); - const swapsEthToken = useSelector(getSwapsEthToken); + const defaultSwapsToken = useSelector(getSwapsDefaultToken); + const chainId = useSelector(getCurrentChainId); const { isBestQuote } = usedQuote; @@ -151,8 +153,8 @@ export default function ViewQuote() { const { tokensWithBalances } = useTokenTracker(swapsTokens, true); const balanceToken = - fetchParamsSourceToken === swapsEthToken.address - ? swapsEthToken + fetchParamsSourceToken === defaultSwapsToken.address + ? defaultSwapsToken : tokensWithBalances.find( ({ address }) => address === fetchParamsSourceToken, ); @@ -183,6 +185,7 @@ export default function ViewQuote() { currentCurrency, approveGas, memoizedTokenConversionRates, + chainId, ); }, [ quotes, @@ -191,6 +194,7 @@ export default function ViewQuote() { currentCurrency, approveGas, memoizedTokenConversionRates, + chainId, ]); const renderableDataForUsedQuote = renderablePopoverData.find( @@ -209,31 +213,33 @@ export default function ViewQuote() { sourceTokenIconUrl, } = renderableDataForUsedQuote; - const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote( - usedGasLimit, + const { feeInFiat, feeInEth } = getRenderableNetworkFeesForQuote({ + tradeGas: usedGasLimit, approveGas, gasPrice, currentCurrency, conversionRate, tradeValue, - sourceTokenSymbol, - usedQuote.sourceAmount, - ); + sourceSymbol: sourceTokenSymbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + }); const { feeInFiat: maxFeeInFiat, feeInEth: maxFeeInEth, nonGasFee, - } = getRenderableNetworkFeesForQuote( - maxGasLimit, + } = getRenderableNetworkFeesForQuote({ + tradeGas: maxGasLimit, approveGas, gasPrice, currentCurrency, conversionRate, tradeValue, - sourceTokenSymbol, - usedQuote.sourceAmount, - ); + sourceSymbol: sourceTokenSymbol, + sourceAmount: usedQuote.sourceAmount, + chainId, + }); const tokenCost = new BigNumber(usedQuote.sourceAmount); const ethCost = new BigNumber(usedQuote.trade.value || 0, 10).plus( @@ -481,9 +487,9 @@ export default function ViewQuote() { {tokenBalanceNeeded || ethBalanceNeeded} , - tokenBalanceNeeded && !(sourceTokenSymbol === 'ETH') + tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) ? sourceTokenSymbol - : 'ETH', + : defaultSwapsToken.symbol, ]); // Price difference warning @@ -643,7 +649,7 @@ export default function ViewQuote() { setSelectQuotePopoverShown(true); }} tokenConversionRate={ - destinationTokenSymbol === 'ETH' + destinationTokenSymbol === defaultSwapsToken.symbol ? 1 : memoizedTokenConversionRates[destinationToken.address] } @@ -655,7 +661,7 @@ export default function ViewQuote() { setSubmitClicked(true); if (!balanceError) { dispatch(signAndSendTransactions(history, metaMetricsEvent)); - } else if (destinationToken.symbol === 'ETH') { + } else if (destinationToken.symbol === defaultSwapsToken.symbol) { history.push(DEFAULT_ROUTE); } else { history.push(`${ASSET_ROUTE}/${destinationToken.address}`); diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index f0146194f..550c45983 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -15,7 +15,10 @@ import { getValueFromWeiHex, hexToDecimal, } from '../helpers/utils/conversions.util'; -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../shared/constants/swaps'; +import { + SWAPS_CHAINID_DEFAULT_TOKEN_MAP, + ALLOWED_SWAPS_CHAIN_IDS, +} from '../../../shared/constants/swaps'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -431,22 +434,26 @@ export function getWeb3ShimUsageStateForOrigin(state, origin) { * minimal token units (according to its decimals). * `string` is the token balance in a readable format, ready for rendering. * - * Swaps treats ETH as a token, and we use the ETH_SWAPS_TOKEN_OBJECT constant - * to set the standard properties for the token. The getSwapsEthToken selector - * extends that object with `balance` and `balance` values of the same type as - * in regular ERC-20 token objects, per the above description. + * Swaps treats the selected chain's currency as a token, and we use the token constants + * in the SWAPS_CHAINID_DEFAULT_TOKEN_MAP to set the standard properties for + * the token. The getSwapsDefaultToken selector extends that object with + * `balance` and `string` values of the same type as in regular ERC-20 token + * objects, per the above description. * * @param {object} state - the redux state object * @returns {SwapsEthToken} The token object representation of the currently * selected account's ETH balance, as expected by the Swaps API. */ -export function getSwapsEthToken(state) { +export function getSwapsDefaultToken(state) { const selectedAccount = getSelectedAccount(state); const { balance } = selectedAccount; + const chainId = getCurrentChainId(state); + + const defaultTokenObject = SWAPS_CHAINID_DEFAULT_TOKEN_MAP[chainId]; return { - ...ETH_SWAPS_TOKEN_OBJECT, + ...defaultTokenObject, balance: hexToDecimal(balance), string: getValueFromWeiHex({ value: balance, @@ -455,3 +462,8 @@ export function getSwapsEthToken(state) { }), }; } + +export function getIsSwapsChain(state) { + const chainId = getCurrentChainId(state); + return ALLOWED_SWAPS_CHAIN_IDS[chainId]; +}