diff --git a/cypress/e2e/token-details.test.ts b/cypress/e2e/token-details.test.ts index a1c3d4b8a1..622d155af9 100644 --- a/cypress/e2e/token-details.test.ts +++ b/cypress/e2e/token-details.test.ts @@ -49,6 +49,10 @@ describe('Token details', () => { // Shiba predator token, low trading volume and also has warning modal cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974') + // Should have missing price chart when price unavailable (expected for this token) + if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) { + cy.get('[data-cy="missing-chart"]').should('exist') + } // Stats should have: TVL, 24H Volume, 52W low, 52W high cy.get(getTestSelector('token-details-stats')).should('exist') cy.get(getTestSelector('token-details-stats')).within(() => { diff --git a/src/components/CurrencyInputPanel/FiatValue.tsx b/src/components/CurrencyInputPanel/FiatValue.tsx index bcf3f15de2..a4e8df1c2e 100644 --- a/src/components/CurrencyInputPanel/FiatValue.tsx +++ b/src/components/CurrencyInputPanel/FiatValue.tsx @@ -1,11 +1,11 @@ import { Trans } from '@lingui/macro' // eslint-disable-next-line no-restricted-imports import { t } from '@lingui/macro' -import { formatCurrencyAmount, formatPriceImpact, NumberType } from '@uniswap/conedison/format' -import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { formatNumber, formatPriceImpact, NumberType } from '@uniswap/conedison/format' +import { Percent } from '@uniswap/sdk-core' import { LoadingBubble } from 'components/Tokens/loading' import { MouseoverTooltip } from 'components/Tooltip' -import { useEffect, useMemo, useState } from 'react' +import { useMemo } from 'react' import styled, { useTheme } from 'styled-components/macro' import { ThemedText } from '../../theme' @@ -20,11 +20,9 @@ const FiatLoadingBubble = styled(LoadingBubble)` export function FiatValue({ fiatValue, priceImpact, - isLoading = false, }: { - fiatValue: CurrencyAmount | null | undefined + fiatValue?: { data?: number; isLoading: boolean } priceImpact?: Percent - isLoading?: boolean }) { const theme = useTheme() const priceImpactColor = useMemo(() => { @@ -35,27 +33,14 @@ export function FiatValue({ if (severity < 3) return theme.deprecated_yellow1 return theme.accentFailure }, [priceImpact, theme.accentSuccess, theme.accentFailure, theme.textTertiary, theme.deprecated_yellow1]) - const [showLoadingPlaceholder, setShowLoadingPlaceholder] = useState(false) - useEffect(() => { - const stale = false - let timeoutId = 0 - if (isLoading && !fiatValue) { - timeoutId = setTimeout(() => { - if (!stale) setShowLoadingPlaceholder(true) - }, 200) as unknown as number - } else { - setShowLoadingPlaceholder(false) - } - return () => clearTimeout(timeoutId) - }, [isLoading, fiatValue]) return ( - {showLoadingPlaceholder ? ( + {fiatValue?.isLoading ? ( ) : (
- {fiatValue ? <>{formatCurrencyAmount(fiatValue, NumberType.FiatTokenPrice)} : undefined} + {fiatValue?.data ? formatNumber(fiatValue.data, NumberType.FiatTokenPrice) : undefined} {priceImpact && ( {' '} diff --git a/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx b/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx index e41eabe7c3..7c04d40e3c 100644 --- a/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx +++ b/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { useWeb3React } from '@web3-react/core' import { AutoColumn } from 'components/Column' @@ -195,7 +195,7 @@ interface SwapCurrencyInputPanelProps { pair?: Pair | null hideInput?: boolean otherCurrency?: Currency | null - fiatValue: CurrencyAmount | null + fiatValue: { data?: number; isLoading: boolean } priceImpact?: Percent id: string showCommonBases?: boolean diff --git a/src/components/CurrencyInputPanel/index.tsx b/src/components/CurrencyInputPanel/index.tsx index 0d2d5208ad..9eb7515044 100644 --- a/src/components/CurrencyInputPanel/index.tsx +++ b/src/components/CurrencyInputPanel/index.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { useWeb3React } from '@web3-react/core' import { AutoColumn } from 'components/Column' @@ -182,7 +182,7 @@ interface CurrencyInputPanelProps { pair?: Pair | null hideInput?: boolean otherCurrency?: Currency | null - fiatValue?: CurrencyAmount | null + fiatValue?: { data?: number; isLoading: boolean } priceImpact?: Percent id: string showCommonBases?: boolean diff --git a/src/components/NavBar/SuggestionRow.tsx b/src/components/NavBar/SuggestionRow.tsx index 156b070795..54aee087af 100644 --- a/src/components/NavBar/SuggestionRow.tsx +++ b/src/components/NavBar/SuggestionRow.tsx @@ -156,7 +156,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, } }, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath]) - const arrow = getDeltaArrow(token.project?.markets?.[0]?.pricePercentChange?.value, 18) + const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18) return ( - {!!token.project?.markets?.[0]?.price?.value && ( + {!!token.market?.price?.value && ( <> - {formatUSDPrice(token.project.markets[0].price.value)} + {formatUSDPrice(token.market.price.value)} {arrow} - - {Math.abs(token.project.markets[0].pricePercentChange?.value ?? 0).toFixed(2)}% + + {Math.abs(token.market?.pricePercentChange?.value ?? 0).toFixed(2)}% diff --git a/src/components/Tokens/TokenDetails/ChartSection.tsx b/src/components/Tokens/TokenDetails/ChartSection.tsx index 7f9e88940d..06dbcbead9 100644 --- a/src/components/Tokens/TokenDetails/ChartSection.tsx +++ b/src/components/Tokens/TokenDetails/ChartSection.tsx @@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector' function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined { // Appends the current price to the end of the priceHistory array const priceHistory = useMemo(() => { - const market = tokenPriceData.token?.project?.markets?.[0] + const market = tokenPriceData.token?.market const priceHistory = market?.priceHistory?.filter(isPricePoint) const currentPrice = market?.price?.value if (Array.isArray(priceHistory) && currentPrice !== undefined) { diff --git a/src/components/Tokens/TokenDetails/PriceChart.tsx b/src/components/Tokens/TokenDetails/PriceChart.tsx index e343a6c44c..cf2a65c35d 100644 --- a/src/components/Tokens/TokenDetails/PriceChart.tsx +++ b/src/components/Tokens/TokenDetails/PriceChart.tsx @@ -138,6 +138,15 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod } ) const chartAvailable = !!prices && prices.length > 0 + const missingPricesMessage = !chartAvailable ? ( + prices?.length === 0 ? ( + <> + Missing price data due to recently low trading volume on Uniswap v3 + + ) : ( + Missing chart data + ) + ) : null // first price point on the x-axis of the current time period's chart const startingPrice = originalPrices?.[0] ?? DATA_EMPTY @@ -278,18 +287,12 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod } ) : ( <> Price Unavailable - - Missing price data - + {missingPricesMessage} )} {!chartAvailable ? ( - Price history unavailable} - /> + ) : (
- {formatUSDPrice(token.project?.markets?.[0]?.price?.value)} + {formatUSDPrice(token.market?.price?.value)} {smallArrow} {formattedDelta} @@ -509,7 +509,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef ) diff --git a/src/components/Widget/transactions.ts b/src/components/Widget/transactions.ts index 2f5b21e3d4..69962862e1 100644 --- a/src/components/Widget/transactions.ts +++ b/src/components/Widget/transactions.ts @@ -108,7 +108,7 @@ export function useSyncWidgetTransactions() { ...formatSwapSignedAnalyticsEventProperties({ trade, // TODO: add once Widgets adds fiat values to callback - fiatValues: { amountIn: null, amountOut: null }, + fiatValues: { amountIn: undefined, amountOut: undefined }, txHash: transaction.receipt?.transactionHash ?? '', }), ...trace, diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index 4728274641..8096c4859f 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro' import { Trace } from '@uniswap/analytics' import { InterfaceModalName } from '@uniswap/analytics-events' import { Trade } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { ReactNode, useCallback, useMemo, useState } from 'react' import { InterfaceTrade } from 'state/routing/types' import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' @@ -42,8 +42,8 @@ export default function ConfirmSwapModal({ swapErrorMessage: ReactNode | undefined onDismiss: () => void swapQuoteReceivedDate: Date | undefined - fiatValueInput: CurrencyAmount | null - fiatValueOutput: CurrencyAmount | null + fiatValueInput: { data?: number; isLoading: boolean } + fiatValueOutput: { data?: number; isLoading: boolean } }) { // shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed // and an event triggered by modal closing should be logged. diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index 994c8b0bb7..44cc726807 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -1,7 +1,7 @@ import { Trans } from '@lingui/macro' import { TraceEvent } from '@uniswap/analytics' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' -import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import useTransactionDeadline from 'hooks/useTransactionDeadline' import { formatPercentInBasisPointsNumber, @@ -31,8 +31,8 @@ interface AnalyticsEventProps { isAutoRouterApi: boolean swapQuoteReceivedDate: Date | undefined routes: RoutingDiagramEntry[] - fiatValueInput?: CurrencyAmount | null - fiatValueOutput?: CurrencyAmount | null + fiatValueInput?: number + fiatValueOutput?: number } const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => { @@ -83,8 +83,8 @@ const formatAnalyticsEventProperties = ({ token_out_symbol: trade.outputAmount.currency.symbol, token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), - token_in_amount_usd: fiatValueInput ? parseFloat(fiatValueInput.toFixed(2)) : undefined, - token_out_amount_usd: fiatValueOutput ? parseFloat(fiatValueOutput.toFixed(2)) : undefined, + token_in_amount_usd: fiatValueInput, + token_out_amount_usd: fiatValueOutput, price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)), allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), is_auto_router_api: isAutoRouterApi, @@ -118,8 +118,8 @@ export default function SwapModalFooter({ swapErrorMessage: ReactNode | undefined disabledConfirm: boolean swapQuoteReceivedDate: Date | undefined - fiatValueInput: CurrencyAmount | null - fiatValueOutput: CurrencyAmount | null + fiatValueInput: { data?: number; isLoading: boolean } + fiatValueOutput: { data?: number; isLoading: boolean } }) { const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto' @@ -142,8 +142,8 @@ export default function SwapModalFooter({ isAutoRouterApi: !clientSideRouter, swapQuoteReceivedDate, routes, - fiatValueInput, - fiatValueOutput, + fiatValueInput: fiatValueInput.data, + fiatValueOutput: fiatValueOutput.data, })} > () - const fiatValueInput = useStablecoinValue(trade.inputAmount) - const fiatValueOutput = useStablecoinValue(trade.outputAmount) + const fiatValueInput = useUSDPrice(trade.inputAmount) + const fiatValueOutput = useUSDPrice(trade.outputAmount) useEffect(() => { if (!trade.executionPrice.equalTo(lastExecutionPrice)) { @@ -145,7 +145,7 @@ export default function SwapModalHeader({ diff --git a/src/components/swap/TradePrice.tsx b/src/components/swap/TradePrice.tsx index 642af7beff..e3d6ae9fc6 100644 --- a/src/components/swap/TradePrice.tsx +++ b/src/components/swap/TradePrice.tsx @@ -1,10 +1,12 @@ import { Trans } from '@lingui/macro' +import { formatNumber, NumberType } from '@uniswap/conedison/format' import { Currency, Price } from '@uniswap/sdk-core' -import useStablecoinPrice from 'hooks/useStablecoinPrice' +import { useUSDPrice } from 'hooks/useUSDPrice' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useCallback, useState } from 'react' import styled from 'styled-components/macro' import { ThemedText } from 'theme' -import { formatDollar, formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers' +import { formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers' interface TradePriceProps { price: Price @@ -30,7 +32,8 @@ const StyledPriceContainer = styled.button` export default function TradePrice({ price }: TradePriceProps) { const [showInverted, setShowInverted] = useState(false) - const usdcPrice = useStablecoinPrice(showInverted ? price.baseCurrency : price.quoteCurrency) + const { baseCurrency, quoteCurrency } = price + const { data: usdPrice } = useUSDPrice(tryParseCurrencyAmount('1', showInverted ? baseCurrency : quoteCurrency)) let formattedPrice: string try { @@ -56,9 +59,9 @@ export default function TradePrice({ price }: TradePriceProps) { title={text} > {text}{' '} - {usdcPrice && ( + {usdPrice && ( - ({formatDollar({ num: priceToPreciseFloat(usdcPrice), isPrice: true })}) + ({formatNumber(usdPrice, NumberType.FiatTokenPrice)}) )} diff --git a/src/graphql/data/RecentlySearched.ts b/src/graphql/data/RecentlySearched.ts index cc9ea93fe4..b892ba72c9 100644 --- a/src/graphql/data/RecentlySearched.ts +++ b/src/graphql/data/RecentlySearched.ts @@ -34,6 +34,15 @@ gql` symbol market(currency: USD) { id + price { + id + value + currency + } + pricePercentChange(duration: DAY) { + id + value + } volume24H: volume(duration: DAY) { id value @@ -44,17 +53,6 @@ gql` id logoUrl safetyLevel - markets(currencies: [USD]) { - id - price { - id - value - } - pricePercentChange(duration: DAY) { - id - value - } - } } } } diff --git a/src/graphql/data/SearchTokens.ts b/src/graphql/data/SearchTokens.ts index e98267bfa7..e041d2e6bd 100644 --- a/src/graphql/data/SearchTokens.ts +++ b/src/graphql/data/SearchTokens.ts @@ -17,6 +17,15 @@ gql` symbol market(currency: USD) { id + price { + id + value + currency + } + pricePercentChange(duration: DAY) { + id + value + } volume24H: volume(duration: DAY) { id value @@ -27,17 +36,6 @@ gql` id logoUrl safetyLevel - markets(currencies: [USD]) { - id - price { - id - value - } - pricePercentChange(duration: DAY) { - id - value - } - } } } } diff --git a/src/graphql/data/Token.ts b/src/graphql/data/Token.ts index 475ae0058e..bbf35ad7f4 100644 --- a/src/graphql/data/Token.ts +++ b/src/graphql/data/Token.ts @@ -30,11 +30,24 @@ gql` value currency } + price { + id + value + currency + } volume24H: volume(duration: DAY) { id value currency } + priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) { + id + value + } + priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) { + id + value + } } project { id @@ -47,22 +60,6 @@ gql` chain address } - markets(currencies: [USD]) { - id - price { - id - value - currency - } - priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) { - id - value - } - priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) { - id - value - } - } } } } diff --git a/src/graphql/data/TokenPrice.ts b/src/graphql/data/TokenPrice.ts index 8c229e52db..ef9390870a 100644 --- a/src/graphql/data/TokenPrice.ts +++ b/src/graphql/data/TokenPrice.ts @@ -6,19 +6,16 @@ gql` id address chain - project { + market(currency: USD) { id - markets(currencies: [USD]) { + price { id - price { - id - value - } - priceHistory(duration: $duration) { - id - timestamp - value - } + value + } + priceHistory(duration: $duration) { + id + timestamp + value } } } diff --git a/src/graphql/data/TokenSpotPrice.ts b/src/graphql/data/TokenSpotPrice.ts new file mode 100644 index 0000000000..fbc728ed59 --- /dev/null +++ b/src/graphql/data/TokenSpotPrice.ts @@ -0,0 +1,23 @@ +import gql from 'graphql-tag' + +gql` + query TokenSpotPrice($chain: Chain!, $address: String) { + token(chain: $chain, address: $address) { + id + address + chain + name + symbol + project { + id + markets(currencies: [USD]) { + id + price { + id + value + } + } + } + } + } +` diff --git a/src/graphql/data/TopTokens.ts b/src/graphql/data/TopTokens.ts index 3a1a1532f8..2e413c5cf4 100644 --- a/src/graphql/data/TopTokens.ts +++ b/src/graphql/data/TopTokens.ts @@ -36,12 +36,22 @@ gql` standard market(currency: USD) { id - volume(duration: $duration) { + totalValueLocked { id value currency } - totalValueLocked { + price { + id + value + currency + } + pricePercentChange(duration: $duration) { + id + currency + value + } + volume(duration: $duration) { id value currency @@ -50,18 +60,6 @@ gql` project { id logoUrl - markets(currencies: [USD]) { - id - price { - id - value - } - pricePercentChange(duration: $duration) { - id - currency - value - } - } } } } @@ -74,14 +72,12 @@ gql` id address chain - project { - markets(currencies: [USD]) { + market(currency: USD) { + id + priceHistory(duration: $duration) { id - priceHistory(duration: $duration) { - id - timestamp - value - } + timestamp + value } } } @@ -97,15 +93,11 @@ function useSortedTokens(tokens: TopTokens100Query['topTokens']) { let tokenArray = Array.from(tokens) switch (sortMethod) { case TokenSortMethod.PRICE: - tokenArray = tokenArray.sort( - (a, b) => (b?.project?.markets?.[0]?.price?.value ?? 0) - (a?.project?.markets?.[0]?.price?.value ?? 0) - ) + tokenArray = tokenArray.sort((a, b) => (b?.market?.price?.value ?? 0) - (a?.market?.price?.value ?? 0)) break case TokenSortMethod.PERCENT_CHANGE: tokenArray = tokenArray.sort( - (a, b) => - (b?.project?.markets?.[0]?.pricePercentChange?.value ?? 0) - - (a?.project?.markets?.[0]?.pricePercentChange?.value ?? 0) + (a, b) => (b?.market?.pricePercentChange?.value ?? 0) - (a?.market?.pricePercentChange?.value ?? 0) ) break case TokenSortMethod.TOTAL_VALUE_LOCKED: @@ -169,8 +161,7 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue { const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken)) const map: SparklineMap = {} unwrappedTokens?.forEach( - (current) => - current?.address && (map[current.address] = current?.project?.markets?.[0]?.priceHistory?.filter(isPricePoint)) + (current) => current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint)) ) return map }, [chainId, sparklineQuery?.topTokens]) diff --git a/src/graphql/data/TrendingTokens.ts b/src/graphql/data/TrendingTokens.ts index bc9de607c1..4fec21c813 100644 --- a/src/graphql/data/TrendingTokens.ts +++ b/src/graphql/data/TrendingTokens.ts @@ -16,6 +16,15 @@ gql` symbol market(currency: USD) { id + price { + id + value + currency + } + pricePercentChange(duration: DAY) { + id + value + } volume24H: volume(duration: DAY) { id value @@ -26,18 +35,6 @@ gql` id logoUrl safetyLevel - markets(currencies: [USD]) { - id - price { - id - value - } - pricePercentChange(duration: DAY) { - id - currency - value - } - } } } } diff --git a/src/graphql/data/util.tsx b/src/graphql/data/util.tsx index 4648fdddd6..52bd845885 100644 --- a/src/graphql/data/util.tsx +++ b/src/graphql/data/util.tsx @@ -73,6 +73,18 @@ export function chainIdToBackendName(chainId: number | undefined) { : CHAIN_ID_TO_BACKEND_NAME[SupportedChainId.MAINNET] } +const GQL_CHAINS: number[] = [ + SupportedChainId.MAINNET, + SupportedChainId.OPTIMISM, + SupportedChainId.POLYGON, + SupportedChainId.ARBITRUM_ONE, + SupportedChainId.CELO, +] + +export function isGqlSupportedChain(chainId: number | undefined): chainId is SupportedChainId { + return !!chainId && GQL_CHAINS.includes(chainId) +} + const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = { ethereum: Chain.Ethereum, polygon: Chain.Polygon, diff --git a/src/hooks/useSwapCallback.tsx b/src/hooks/useSwapCallback.tsx index 322b8c9725..75da85c15b 100644 --- a/src/hooks/useSwapCallback.tsx +++ b/src/hooks/useSwapCallback.tsx @@ -1,5 +1,5 @@ import { Trade } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { PermitSignature } from 'hooks/usePermitAllowance' import { useMemo } from 'react' @@ -13,7 +13,7 @@ import { useUniversalRouterSwapCallback } from './useUniversalRouter' // and the user has approved the slippage adjusted input amount for the trade export function useSwapCallback( trade: Trade | undefined, // trade to execute, required - fiatValues: { amountIn: CurrencyAmount | null; amountOut: CurrencyAmount | null }, // usd values for amount in and out, logged for analytics + fiatValues: { amountIn: number | undefined; amountOut: number | undefined }, // usd values for amount in and out, logged for analytics allowedSlippage: Percent, // in bips permitSignature: PermitSignature | undefined ): { callback: null | (() => Promise) } { diff --git a/src/hooks/useUSDPrice.ts b/src/hooks/useUSDPrice.ts new file mode 100644 index 0000000000..a2c875499f --- /dev/null +++ b/src/hooks/useUSDPrice.ts @@ -0,0 +1,81 @@ +import { NetworkStatus } from '@apollo/client' +import { Currency, CurrencyAmount, Price, SupportedChainId, TradeType } from '@uniswap/sdk-core' +import { nativeOnChain } from 'constants/tokens' +import { Chain, useTokenSpotPriceQuery } from 'graphql/data/__generated__/types-and-hooks' +import { chainIdToBackendName, isGqlSupportedChain, PollingInterval } from 'graphql/data/util' +import { RouterPreference } from 'state/routing/slice' +import { TradeState } from 'state/routing/types' +import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' +import { getNativeTokenDBAddress } from 'utils/nativeTokens' + +import useStablecoinPrice from './useStablecoinPrice' + +// ETH amounts used when calculating spot price for a given currency. +// The amount is large enough to filter low liquidity pairs. +const ETH_AMOUNT_OUT: { [chainId: number]: CurrencyAmount } = { + [SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(nativeOnChain(SupportedChainId.MAINNET), 100e18), + [SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(nativeOnChain(SupportedChainId.ARBITRUM_ONE), 10e18), + [SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(nativeOnChain(SupportedChainId.OPTIMISM), 10e18), + [SupportedChainId.POLYGON]: CurrencyAmount.fromRawAmount(nativeOnChain(SupportedChainId.POLYGON), 10_000e18), + [SupportedChainId.CELO]: CurrencyAmount.fromRawAmount(nativeOnChain(SupportedChainId.CELO), 10e18), +} + +function useETHValue(currencyAmount?: CurrencyAmount): { + data: CurrencyAmount | undefined + isLoading: boolean +} { + const chainId = currencyAmount?.currency?.chainId + const amountOut = isGqlSupportedChain(chainId) ? ETH_AMOUNT_OUT[chainId] : undefined + const { trade, state } = useRoutingAPITrade( + TradeType.EXACT_OUTPUT, + amountOut, + currencyAmount?.currency, + RouterPreference.PRICE + ) + + // Get ETH value of ETH or WETH + if (chainId && currencyAmount && currencyAmount.currency.wrapped.equals(nativeOnChain(chainId).wrapped)) { + return { + data: new Price(currencyAmount.currency, currencyAmount.currency, '1', '1').quote(currencyAmount), + isLoading: false, + } + } + + if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) { + return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING } + } + + const { numerator, denominator } = trade.routes[0].midPrice + const price = new Price(currencyAmount?.currency, nativeOnChain(chainId), denominator, numerator) + return { data: price.quote(currencyAmount), isLoading: false } +} + +export function useUSDPrice(currencyAmount?: CurrencyAmount): { + data: number | undefined + isLoading: boolean +} { + const chain = currencyAmount?.currency.chainId ? chainIdToBackendName(currencyAmount?.currency.chainId) : undefined + const currency = currencyAmount?.currency + const { data: ethValue, isLoading: isEthValueLoading } = useETHValue(currencyAmount) + + const { data, networkStatus } = useTokenSpotPriceQuery({ + variables: { chain: chain ?? Chain.Ethereum, address: getNativeTokenDBAddress(chain ?? Chain.Ethereum) }, + skip: !chain || !isGqlSupportedChain(currency?.chainId), + pollInterval: PollingInterval.Normal, + notifyOnNetworkStatusChange: true, + }) + + // Use USDC price for chains not supported by backend yet + const stablecoinPrice = useStablecoinPrice(!isGqlSupportedChain(currency?.chainId) ? currency : undefined) + if (!isGqlSupportedChain(currency?.chainId) && currencyAmount && stablecoinPrice) { + return { data: parseFloat(stablecoinPrice.quote(currencyAmount).toSignificant()), isLoading: false } + } + + const isFirstLoad = networkStatus === NetworkStatus.loading + + // Otherwise, get the price of the token in ETH, and then multiple by the price of ETH + const ethUSDPrice = data?.token?.project?.markets?.[0]?.price?.value + if (!ethUSDPrice || !ethValue) return { data: undefined, isLoading: isEthValueLoading || isFirstLoad } + + return { data: parseFloat(ethValue.toExact()) * ethUSDPrice, isLoading: false } +} diff --git a/src/hooks/useUniversalRouter.ts b/src/hooks/useUniversalRouter.ts index c5a8fe4522..1c3b3b22a7 100644 --- a/src/hooks/useUniversalRouter.ts +++ b/src/hooks/useUniversalRouter.ts @@ -4,7 +4,7 @@ import { t } from '@lingui/macro' import { sendAnalyticsEvent } from '@uniswap/analytics' import { SwapEventName } from '@uniswap/analytics-events' import { Trade } from '@uniswap/router-sdk' -import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { FeeOptions, toHex } from '@uniswap/v3-sdk' import { useWeb3React } from '@web3-react/core' @@ -27,7 +27,7 @@ interface SwapOptions { export function useUniversalRouterSwapCallback( trade: Trade | undefined, - fiatValues: { amountIn: CurrencyAmount | null; amountOut: CurrencyAmount | null }, + fiatValues: { amountIn: number | undefined; amountOut: number | undefined }, options: SwapOptions ) { const { account, chainId, provider } = useWeb3React() diff --git a/src/lib/utils/analytics.ts b/src/lib/utils/analytics.ts index 10b09e93e3..f27a6cb76c 100644 --- a/src/lib/utils/analytics.ts +++ b/src/lib/utils/analytics.ts @@ -40,7 +40,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({ txHash, }: { trade: InterfaceTrade | Trade - fiatValues: { amountIn: CurrencyAmount | null; amountOut: CurrencyAmount | null } + fiatValues: { amountIn: number | undefined; amountOut: number | undefined } txHash: string }) => ({ transaction_hash: txHash, @@ -50,8 +50,8 @@ export const formatSwapSignedAnalyticsEventProperties = ({ token_out_symbol: trade.outputAmount.currency.symbol, token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), - token_in_amount_usd: fiatValues.amountIn ? parseFloat(fiatValues.amountIn.toFixed(2)) : undefined, - token_out_amount_usd: fiatValues.amountOut ? parseFloat(fiatValues.amountOut.toFixed(2)) : undefined, + token_in_amount_usd: fiatValues.amountIn, + token_out_amount_usd: fiatValues.amountOut, price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)), chain_id: trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index b588647c06..9e84fe5704 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -9,7 +9,7 @@ import { useWeb3React } from '@web3-react/core' import { sendEvent } from 'components/analytics' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' import usePrevious from 'hooks/usePrevious' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { AlertTriangle } from 'react-feather' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { Text } from 'rebass' @@ -532,6 +532,22 @@ export default function AddLiquidity() { ) + const usdcValueCurrencyA = usdcValues[Field.CURRENCY_A] + const usdcValueCurrencyB = usdcValues[Field.CURRENCY_B] + const currencyAFiat = useMemo( + () => ({ + data: usdcValueCurrencyA ? parseFloat(usdcValueCurrencyA.toSignificant()) : undefined, + isLoading: false, + }), + [usdcValueCurrencyA] + ) + const currencyBFiat = useMemo( + () => ({ + data: usdcValueCurrencyB ? parseFloat(usdcValueCurrencyB.toSignificant()) : undefined, + isLoading: false, + }), + [usdcValueCurrencyB] + ) return ( <> @@ -682,7 +698,7 @@ export default function AddLiquidity() { showMaxButton={!atMaxAmounts[Field.CURRENCY_A]} currency={currencies[Field.CURRENCY_A] ?? null} id="add-liquidity-input-tokena" - fiatValue={usdcValues[Field.CURRENCY_A]} + fiatValue={currencyAFiat} showCommonBases locked={depositADisabled} /> @@ -694,7 +710,7 @@ export default function AddLiquidity() { onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '') }} showMaxButton={!atMaxAmounts[Field.CURRENCY_B]} - fiatValue={usdcValues[Field.CURRENCY_B]} + fiatValue={currencyBFiat} currency={currencies[Field.CURRENCY_B] ?? null} id="add-liquidity-input-tokenb" showCommonBases diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 7556c162fe..6bc8fca6e1 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -25,6 +25,7 @@ import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget' import useENSAddress from 'hooks/useENSAddress' import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance' import { useSwapCallback } from 'hooks/useSwapCallback' +import { useUSDPrice } from 'hooks/useUSDPrice' import JSBI from 'jsbi' import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -54,7 +55,6 @@ import { SwitchLocaleLink } from '../../components/SwitchLocaleLink' import { TOKEN_SHORTHANDS } from '../../constants/tokens' import { useAllTokens, useCurrency } from '../../hooks/Tokens' import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported' -import { useStablecoinValue } from '../../hooks/useStablecoinPrice' import useWrapCallback, { WrapErrorText, WrapType } from '../../hooks/useWrapCallback' import { Field } from '../../state/swap/actions' import { @@ -230,19 +230,21 @@ export default function Swap({ className }: { className?: string }) { }, [independentField, parsedAmount, showWrap, trade] ) - const fiatValueInput = useStablecoinValue(parsedAmounts[Field.INPUT]) - const fiatValueOutput = useStablecoinValue(parsedAmounts[Field.OUTPUT]) + const fiatValueInput = useUSDPrice(parsedAmounts[Field.INPUT]) + const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT]) const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo( () => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.SYNCING === tradeState], [trade, tradeState] ) - const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount) - const fiatValueTradeOutput = useStablecoinValue(trade?.outputAmount) + const fiatValueTradeInput = useUSDPrice(trade?.inputAmount) + const fiatValueTradeOutput = useUSDPrice(trade?.outputAmount) const stablecoinPriceImpact = useMemo( () => - routeIsSyncing || !trade ? undefined : computeFiatValuePriceImpact(fiatValueTradeInput, fiatValueTradeOutput), + routeIsSyncing || !trade + ? undefined + : computeFiatValuePriceImpact(fiatValueTradeInput.data, fiatValueTradeOutput.data), [fiatValueTradeInput, fiatValueTradeOutput, routeIsSyncing, trade] ) @@ -334,7 +336,7 @@ export default function Swap({ className }: { className?: string }) { ) const showMaxButton = Boolean(maxInputAmount?.greaterThan(0) && !parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount)) const swapFiatValues = useMemo(() => { - return { amountIn: fiatValueTradeInput, amountOut: fiatValueTradeOutput } + return { amountIn: fiatValueTradeInput.data, amountOut: fiatValueTradeOutput.data } }, [fiatValueTradeInput, fiatValueTradeOutput]) // the callback to execute the swap diff --git a/src/state/routing/useRoutingAPITrade.ts b/src/state/routing/useRoutingAPITrade.ts index 76e3308497..35205ad026 100644 --- a/src/state/routing/useRoutingAPITrade.ts +++ b/src/state/routing/useRoutingAPITrade.ts @@ -62,7 +62,7 @@ export function useRoutingAPITrade( const isSyncing = currentData !== data return useMemo(() => { - if (!currencyIn || !currencyOut) { + if (!currencyIn || !currencyOut || currencyIn.equals(currencyOut)) { return { state: TradeState.INVALID, trade: undefined, diff --git a/src/utils/computeFiatValuePriceImpact.tsx b/src/utils/computeFiatValuePriceImpact.tsx index ab831c5df7..f5abaf9fc4 100644 --- a/src/utils/computeFiatValuePriceImpact.tsx +++ b/src/utils/computeFiatValuePriceImpact.tsx @@ -1,15 +1,14 @@ -import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' -import JSBI from 'jsbi' - -import { ONE_HUNDRED_PERCENT } from '../constants/misc' +import { Percent } from '@uniswap/sdk-core' +const PRECISION = 10000 export function computeFiatValuePriceImpact( - fiatValueInput: CurrencyAmount | undefined | null, - fiatValueOutput: CurrencyAmount | undefined | null + fiatValueInput: number | undefined | null, + fiatValueOutput: number | undefined | null ): Percent | undefined { if (!fiatValueOutput || !fiatValueInput) return undefined - if (!fiatValueInput.currency.equals(fiatValueOutput.currency)) return undefined - if (JSBI.equal(fiatValueInput.quotient, JSBI.BigInt(0))) return undefined - const pct = ONE_HUNDRED_PERCENT.subtract(fiatValueOutput.divide(fiatValueInput)) - return new Percent(pct.numerator, pct.denominator) + if (fiatValueInput === 0) return undefined + + const ratio = 1 - fiatValueOutput / fiatValueInput + const numerator = Math.floor(ratio * PRECISION) + return new Percent(numerator, PRECISION) }