diff --git a/ui/app/components/app/wallet-overview/eth-overview.js b/ui/app/components/app/wallet-overview/eth-overview.js index 401a96fd8..ce24a74e4 100644 --- a/ui/app/components/app/wallet-overview/eth-overview.js +++ b/ui/app/components/app/wallet-overview/eth-overview.js @@ -8,17 +8,16 @@ import Identicon from '../../ui/identicon' import { I18nContext } from '../../../contexts/i18n' import { SEND_ROUTE, BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes' import { useMetricEvent, useNewMetricEvent } from '../../../hooks/useMetricEvent' +import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken' import Tooltip from '../../ui/tooltip' import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' import { showModal } from '../../../store/actions' import { isBalanceCached, getSelectedAccount, getShouldShowFiat, getCurrentNetworkId, getCurrentKeyring } from '../../../selectors/selectors' -import { getValueFromWeiHex } from '../../../helpers/utils/conversions.util' import SwapIcon from '../../ui/icon/swap-icon.component' import BuyIcon from '../../ui/icon/overview-buy-icon.component' import SendIcon from '../../ui/icon/overview-send-icon.component' import { getSwapsFeatureLiveness, setSwapsFromToken } from '../../../ducks/swaps/swaps' -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps' import IconButton from '../../ui/icon-button' import { MAINNET_NETWORK_ID } from '../../../../../app/scripts/controllers/network/enums' import WalletOverview from './wallet-overview' @@ -50,6 +49,7 @@ const EthOverview = ({ className }) => { const networkId = useSelector(getCurrentNetworkId) const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Main View', active_currency: 'ETH' }, category: 'swaps' }) const swapsEnabled = useSelector(getSwapsFeatureLiveness) + const swapsEthToken = useSwapsEthToken() return ( { onClick={() => { if (networkId === MAINNET_NETWORK_ID) { enteredSwapsEvent() - dispatch(setSwapsFromToken({ - ...ETH_SWAPS_TOKEN_OBJECT, - balance, - string: getValueFromWeiHex({ value: balance, numberOfDecimals: 4, toDenomination: 'ETH' }), - })) + dispatch(setSwapsFromToken(swapsEthToken)) if (usingHardwareWallet) { global.platform.openExtensionInBrowser(BUILD_QUOTE_ROUTE) } else { diff --git a/ui/app/ducks/swaps/swaps.js b/ui/app/ducks/swaps/swaps.js index 171256720..9fa019079 100644 --- a/ui/app/ducks/swaps/swaps.js +++ b/ui/app/ducks/swaps/swaps.js @@ -23,7 +23,7 @@ import { import { AWAITING_SWAP_ROUTE, BUILD_QUOTE_ROUTE, LOADING_QUOTES_ROUTE, SWAPS_ERROR_ROUTE, SWAPS_MAINTENANCE_ROUTE } from '../../helpers/constants/routes' import { fetchSwapsFeatureLiveness } from '../../pages/swaps/swaps.util' import { calcGasTotal } from '../../pages/send/send.utils' -import { decimalToHex, getValueFromWeiHex, hexMax, decGWEIToHexWEI } from '../../helpers/utils/conversions.util' +import { decimalToHex, getValueFromWeiHex, hexMax, decGWEIToHexWEI, hexToDecimal } from '../../helpers/utils/conversions.util' import { calcTokenAmount } from '../../helpers/utils/token-util' import { getFastPriceEstimateInHexWEI, @@ -274,7 +274,7 @@ export const fetchQuotesAndSetQuoteState = (history, inputValue, maxSlippage, me { ...ETH_SWAPS_TOKEN_OBJECT, string: getValueFromWeiHex({ value: selectedAccount.balance, numberOfDecimals: 4, toDenomination: 'ETH' }), - balance: selectedAccount.balance, + balance: hexToDecimal(selectedAccount.balance), } : fetchParams?.metaData?.sourceTokenInfo const selectedFromToken = getFromToken(state) || fetchParamsFromToken || {} diff --git a/ui/app/hooks/useSwapsEthToken.js b/ui/app/hooks/useSwapsEthToken.js new file mode 100644 index 000000000..398668cf5 --- /dev/null +++ b/ui/app/hooks/useSwapsEthToken.js @@ -0,0 +1,50 @@ +import { useSelector } from 'react-redux' +import { getSelectedAccount } from '../selectors' +import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps' +import { getValueFromWeiHex, hexToDecimal } from '../helpers/utils/conversions.util' + +/** + * @typedef {Object} SwapsEthToken + * @property {string} symbol - The symbol for ETH, namely "ETH" + * @property {string} name - The name of the ETH currency, "Ether" + * @property {string} address - A substitute address for the metaswap-api to + * recognize the ETH token + * @property {string} decimals - The number of ETH decimals, i.e. 18 + * @property {string} balance - The user's ETH balance in decimal wei, with a + * precision of 4 decimal places + * @property {string} string - The user's ETH balance in decimal ETH + */ + +/** + * Swaps related code uses token objects for various purposes. These objects + * always have the following properties: `symbol`, `name`, `address`, and + * `decimals`. + * + * When available for the current account, the objects can have `balance` and + * `string` properties. + * `balance` is the users token balance in decimal values, denominated in the + * 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 useSwapsEthToken hook + * extends that object with `balance` and `balance` values of the same type as + * in regular ERC-20 token objects, per the above description. + * + * @returns {SwapsEthToken} The token object representation of the currently + * selected account's ETH balance, as expected by the Swaps API. + */ +export function useSwapsEthToken () { + const selectedAccount = useSelector(getSelectedAccount) + const { balance } = selectedAccount + + return { + ...ETH_SWAPS_TOKEN_OBJECT, + balance: hexToDecimal(balance), + string: getValueFromWeiHex({ + value: balance, + numberOfDecimals: 4, + toDenomination: 'ETH', + }), + } +} diff --git a/ui/app/hooks/useTokensToSearch.js b/ui/app/hooks/useTokensToSearch.js index 98387de49..04a0e7bf1 100644 --- a/ui/app/hooks/useTokensToSearch.js +++ b/ui/app/hooks/useTokensToSearch.js @@ -3,12 +3,11 @@ import { useSelector } from 'react-redux' import contractMap from 'eth-contract-metadata' import BigNumber from 'bignumber.js' import { isEqual, shuffle } from 'lodash' -import { getValueFromWeiHex } from '../helpers/utils/conversions.util' import { checksumAddress } from '../helpers/utils/util' import { getTokenFiatAmount } from '../helpers/utils/token-util' import { getTokenExchangeRates, getConversionRate, getCurrentCurrency } from '../selectors' import { getSwapsTokens } from '../ducks/swaps/swaps' -import { ETH_SWAPS_TOKEN_OBJECT } from '../helpers/constants/swaps' +import { useSwapsEthToken } from './useSwapsEthToken' import { useEqualityCheck } from './useEqualityCheck' const tokenList = shuffle(Object.entries(contractMap) @@ -50,7 +49,13 @@ export function getRenderableTokenData (token, contractExchangeRates, conversion } } -export function useTokensToSearch ({ providedTokens, rawEthBalance, usersTokens = [], topTokens = {}, onlyEth, singleToken }) { +export function useTokensToSearch ({ + providedTokens, + usersTokens = [], + topTokens = {}, + onlyEth, + singleToken, +}) { const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual) const conversionRate = useSelector(getConversionRate) const currentCurrency = useSelector(getCurrentCurrency) @@ -58,9 +63,9 @@ export function useTokensToSearch ({ providedTokens, rawEthBalance, usersTokens const memoizedTopTokens = useEqualityCheck(topTokens) const memoizedUsersToken = useEqualityCheck(usersTokens) - const decEthBalance = getValueFromWeiHex({ value: rawEthBalance, numberOfDecimals: 4, toDenomination: 'ETH' }) + const swapsEthToken = useSwapsEthToken() const [ethToken] = useState(() => getRenderableTokenData( - { ...ETH_SWAPS_TOKEN_OBJECT, balance: rawEthBalance, string: decEthBalance }, + swapsEthToken, tokenConversionRates, conversionRate, currentCurrency, diff --git a/ui/app/pages/swaps/build-quote/build-quote.js b/ui/app/pages/swaps/build-quote/build-quote.js index b5869bc18..aa1f7b748 100644 --- a/ui/app/pages/swaps/build-quote/build-quote.js +++ b/ui/app/pages/swaps/build-quote/build-quote.js @@ -7,6 +7,7 @@ import { useHistory } from 'react-router-dom' import { MetaMetricsContext } from '../../../contexts/metametrics.new' import { useTokensToSearch } from '../../../hooks/useTokensToSearch' import { useEqualityCheck } from '../../../hooks/useEqualityCheck' +import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken' import { I18nContext } from '../../../contexts/i18n' import DropdownInputPair from '../dropdown-input-pair' import DropdownSearchList from '../dropdown-search-list' @@ -60,8 +61,9 @@ export default function BuildQuote ({ const topAssets = useSelector(getTopAssets) const fromToken = useSelector(getFromToken) const toToken = useSelector(getToToken) || destinationTokenInfo + const swapsEthToken = useSwapsEthToken() const fetchParamsFromToken = sourceTokenInfo?.symbol === 'ETH' - ? { ...ETH_SWAPS_TOKEN_OBJECT, string: getValueFromWeiHex({ value: ethBalance, numberOfDecimals: 4, toDenomination: 'ETH' }), balance: ethBalance } + ? swapsEthToken : sourceTokenInfo const { loading, tokensWithBalances } = useTokenTracker(tokens) @@ -77,14 +79,12 @@ export default function BuildQuote ({ const selectedFromToken = useTokensToSearch({ providedTokens: fromToken || fetchParamsFromToken ? [fromToken || fetchParamsFromToken] : [], - rawEthBalance: ethBalance, usersTokens: memoizedUsersTokens, onlyEth: (fromToken || fetchParamsFromToken)?.symbol === 'ETH', singleToken: true, })[0] const tokensToSearch = useTokensToSearch({ - rawEthBalance: ethBalance, usersTokens: memoizedUsersTokens, topTokens: topAssets, }) diff --git a/ui/app/pages/swaps/intro-popup/intro-popup.js b/ui/app/pages/swaps/intro-popup/intro-popup.js index 9c9a258d6..82c2c855a 100644 --- a/ui/app/pages/swaps/intro-popup/intro-popup.js +++ b/ui/app/pages/swaps/intro-popup/intro-popup.js @@ -1,27 +1,24 @@ import React, { useContext } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' import PropTypes from 'prop-types' -import { getValueFromWeiHex } from '../../../helpers/utils/conversions.util' import { setSwapsFromToken } from '../../../ducks/swaps/swaps' -import { ETH_SWAPS_TOKEN_OBJECT } from '../../../helpers/constants/swaps' import { I18nContext } from '../../../contexts/i18n' import { BUILD_QUOTE_ROUTE } from '../../../helpers/constants/routes' import { useNewMetricEvent } from '../../../hooks/useMetricEvent' +import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken' import Button from '../../../components/ui/button' import Popover from '../../../components/ui/popover' -import { getSelectedAccount } from '../../../selectors/selectors' export default function IntroPopup ({ onClose }) { const dispatch = useDispatch(useDispatch) const history = useHistory() const t = useContext(I18nContext) - const selectedAccount = useSelector(getSelectedAccount) - const { balance } = selectedAccount const enteredSwapsEvent = useNewMetricEvent({ event: 'Swaps Opened', properties: { source: 'Intro popup', active_currency: 'ETH' }, category: 'swaps' }) const blogPostVisitedEvent = useNewMetricEvent({ event: 'Blog Post Visited ', category: 'swaps' }) const contractAuditVisitedEvent = useNewMetricEvent({ event: 'Contract Audit Visited', category: 'swaps' }) const productOverviewDismissedEvent = useNewMetricEvent({ event: 'Product Overview Dismissed', category: 'swaps' }) + const swapsEthToken = useSwapsEthToken() return (
@@ -41,11 +38,7 @@ export default function IntroPopup ({ onClose }) { onClick={() => { onClose() enteredSwapsEvent() - dispatch(setSwapsFromToken({ - ...ETH_SWAPS_TOKEN_OBJECT, - balance, - string: getValueFromWeiHex({ value: balance, numberOfDecimals: 4, toDenomination: 'ETH' }), - })) + dispatch(setSwapsFromToken(swapsEthToken)) history.push(BUILD_QUOTE_ROUTE) }} > diff --git a/ui/app/pages/swaps/view-quote/view-quote.js b/ui/app/pages/swaps/view-quote/view-quote.js index 00eac2cf9..04db7a9c2 100644 --- a/ui/app/pages/swaps/view-quote/view-quote.js +++ b/ui/app/pages/swaps/view-quote/view-quote.js @@ -8,6 +8,7 @@ import { I18nContext } from '../../../contexts/i18n' import SelectQuotePopover from '../select-quote-popover' import { useEqualityCheck } from '../../../hooks/useEqualityCheck' import { useNewMetricEvent } from '../../../hooks/useMetricEvent' +import { useSwapsEthToken } from '../../../hooks/useSwapsEthToken' import { MetaMetricsContext } from '../../../contexts/metametrics.new' import FeeCard from '../fee-card' import { setCustomGasLimit } from '../../../ducks/gas/gas.duck' @@ -68,10 +69,7 @@ import { getCustomTxParamsData } from '../../confirm-approve/confirm-approve.uti import ActionableMessage from '../actionable-message' import { quotesToRenderableData, getRenderableGasFeesForQuote } from '../swaps.util' import { useTokenTracker } from '../../../hooks/useTokenTracker' -import { - ETH_SWAPS_TOKEN_OBJECT, - QUOTES_EXPIRED_ERROR, -} from '../../../helpers/constants/swaps' +import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps' import CountdownTimer from '../countdown-timer' import SwapsFooter from '../swaps-footer' @@ -143,8 +141,9 @@ export default function ViewQuote () { const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice) const { tokensWithBalances } = useTokenTracker(swapsTokens) - const balanceToken = fetchParamsSourceToken === ETH_SWAPS_TOKEN_OBJECT.address - ? { ...ETH_SWAPS_TOKEN_OBJECT, balance: ethBalance } + const swapsEthToken = useSwapsEthToken() + const balanceToken = fetchParamsSourceToken === swapsEthToken.address + ? swapsEthToken : tokensWithBalances.find(({ address }) => address === fetchParamsSourceToken) const selectedFromToken = balanceToken || usedQuote.sourceTokenInfo