Commit 184a1419 authored by Connor McEwen's avatar Connor McEwen Committed by GitHub

feat: fetch stablecoin price with SOR, PI warning (#4217)

* feat: fetch stablecoins price with SOR, PI warning

* calculate realized price impact

* remove unrelated changes

* dupe import

* pr feedback

* use the same calculation function for PI

* use proper var
parent 6cb6faa9
......@@ -10,7 +10,7 @@ import { InterfaceTrade } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro'
import { Separator, ThemedText } from '../../theme'
import { computeRealizedLPFeePercent } from '../../utils/prices'
import { computeRealizedPriceImpact } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
......@@ -45,13 +45,6 @@ function TextWithLoadingPlaceholder({
)
}
export function getPriceImpactPercent(
lpFeePercent: Percent,
trade: InterfaceTrade<Currency, Currency, TradeType>
): Percent {
return trade.priceImpact.subtract(lpFeePercent)
}
export function AdvancedSwapDetails({
trade,
allowedSlippage,
......@@ -63,11 +56,10 @@ export function AdvancedSwapDetails({
const nativeCurrency = useNativeCurrency()
const { expectedOutputAmount, priceImpact } = useMemo(() => {
if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined }
const expectedOutputAmount = trade.outputAmount
const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
const priceImpact = getPriceImpactPercent(realizedLpFeePercent, trade)
return { expectedOutputAmount, priceImpact }
return {
expectedOutputAmount: trade?.outputAmount,
priceImpact: trade ? computeRealizedPriceImpact(trade) : undefined,
}
}, [trade])
return !trade ? null : (
......
......@@ -3,13 +3,15 @@ import { Percent } from '@uniswap/sdk-core'
import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds'
export const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
/**
* Formatted version of price impact text with warning colors
*/
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact ? `${priceImpact.multiply(-1).toFixed(2)}%` : '-'}
{priceImpact ? formatPriceImpact(priceImpact) : '-'}
</ErrorText>
)
}
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { OutlineCard } from 'components/Card'
import { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components/macro'
import { opacify } from 'theme/utils'
import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import { formatPriceImpact } from './FormattedPriceImpact'
const StyledCard = styled(OutlineCard)`
padding: 12px;
border: 1px solid ${({ theme }) => opacify(24, theme.deprecated_error)};
`
interface PriceImpactWarningProps {
priceImpact: Percent
}
export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) {
const theme = useContext(ThemeContext)
return (
<StyledCard>
<AutoColumn gap="8px">
<MouseoverTooltip
text={
<Trans>
A swap of this size may have a high price impact, given the current liquidity in the pool. There may be a
large difference between the amount of your input token and what you will receive in the output token
</Trans>
}
>
<RowBetween>
<RowFixed>
<ThemedText.DeprecatedSubHeader color={theme.deprecated_error}>
<Trans>Price impact warning</Trans>
</ThemedText.DeprecatedSubHeader>
</RowFixed>
<ThemedText.DeprecatedLabel textAlign="right" fontSize={14} color={theme.deprecated_error}>
{formatPriceImpact(priceImpact)}
</ThemedText.DeprecatedLabel>
</RowBetween>
</MouseoverTooltip>
</AutoColumn>
</StyledCard>
)
}
import { Trans } from '@lingui/macro'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ElementName, EventName } from 'components/AmplitudeAnalytics/constants'
import { Event } from 'components/AmplitudeAnalytics/constants'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import {
formatPercentInBasisPointsNumber,
......@@ -16,11 +15,10 @@ import { ReactNode } from 'react'
import { Text } from 'rebass'
import { InterfaceTrade } from 'state/routing/types'
import { useClientSideRouter, useUserSlippageTolerance } from 'state/user/hooks'
import { computeRealizedLPFeePercent } from 'utils/prices'
import { computeRealizedPriceImpact } from 'utils/prices'
import { ButtonError } from '../Button'
import { AutoRow } from '../Row'
import { getPriceImpactPercent } from './AdvancedSwapDetails'
import { SwapCallbackError } from './styleds'
interface AnalyticsEventProps {
......@@ -32,7 +30,6 @@ interface AnalyticsEventProps {
isAutoRouterApi: boolean
tokenInAmountUsd: string | undefined
tokenOutAmountUsd: string | undefined
lpFeePercent: Percent
swapQuoteReceivedDate: Date | undefined
}
......@@ -45,7 +42,6 @@ const formatAnalyticsEventProperties = ({
isAutoRouterApi,
tokenInAmountUsd,
tokenOutAmountUsd,
lpFeePercent,
swapQuoteReceivedDate,
}: AnalyticsEventProps) => ({
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
......@@ -59,7 +55,7 @@ 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),
price_impact_basis_points: formatPercentInBasisPointsNumber(getPriceImpactPercent(lpFeePercent, trade)),
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
is_auto_router_api: isAutoRouterApi,
is_auto_slippage: isAutoSlippage,
......@@ -94,7 +90,6 @@ export default function SwapModalFooter({
const [clientSideRouter] = useClientSideRouter()
const tokenInAmountUsd = useStablecoinValue(trade.inputAmount)?.toFixed(2)
const tokenOutAmountUsd = useStablecoinValue(trade.outputAmount)?.toFixed(2)
const lpFeePercent = computeRealizedLPFeePercent(trade)
return (
<>
......@@ -112,7 +107,6 @@ export default function SwapModalFooter({
isAutoRouterApi: !clientSideRouter,
tokenInAmountUsd,
tokenOutAmountUsd,
lpFeePercent,
swapQuoteReceivedDate,
})}
>
......
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Pair, Trade } from '@uniswap/v2-sdk'
import { useMemo } from 'react'
import { isTradeBetter } from 'utils/isTradeBetter'
import { BETTER_TRADE_LESS_HOPS_THRESHOLD } from '../constants/misc'
import { useAllCurrencyCombinations } from './useAllCurrencyCombinations'
import { PairState, useV2Pairs } from './useV2Pairs'
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const allCurrencyCombinations = useAllCurrencyCombinations(currencyA, currencyB)
const allPairs = useV2Pairs(allCurrencyCombinations)
return useMemo(
() =>
Object.values(
allPairs
// filter out invalid pairs
.filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1]))
.map(([, pair]) => pair)
),
[allPairs]
)
}
const MAX_HOPS = 3
/**
* Returns the best v2 trade for a desired swap
* @param tradeType whether the swap is an exact in/out
* @param amountSpecified the exact amount to swap in/out
* @param otherCurrency the desired output/payment currency
*/
export function useBestV2Trade(
tradeType: TradeType.EXACT_INPUT | TradeType.EXACT_OUTPUT,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency,
{ maxHops = MAX_HOPS } = {}
): Trade<Currency, Currency, TradeType.EXACT_INPUT | TradeType.EXACT_OUTPUT> | null {
const [currencyIn, currencyOut] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency],
[tradeType, amountSpecified, otherCurrency]
)
const allowedPairs = useAllCommonPairs(currencyIn, currencyOut)
return useMemo(() => {
if (amountSpecified && currencyIn && currencyOut && allowedPairs.length > 0) {
if (maxHops === 1) {
const options = { maxHops: 1, maxNumResults: 1 }
if (tradeType === TradeType.EXACT_INPUT) {
const amountIn = amountSpecified
return Trade.bestTradeExactIn(allowedPairs, amountIn, currencyOut, options)[0] ?? null
} else {
const amountOut = amountSpecified
return Trade.bestTradeExactOut(allowedPairs, currencyIn, amountOut, options)[0] ?? null
}
}
// search through trades with varying hops, find best trade out of them
let bestTradeSoFar: Trade<Currency, Currency, TradeType.EXACT_INPUT | TradeType.EXACT_OUTPUT> | null = null
for (let i = 1; i <= maxHops; i++) {
const options = { maxHops: i, maxNumResults: 1 }
let currentTrade: Trade<Currency, Currency, TradeType.EXACT_INPUT | TradeType.EXACT_OUTPUT> | null
if (tradeType === TradeType.EXACT_INPUT) {
const amountIn = amountSpecified
currentTrade = Trade.bestTradeExactIn(allowedPairs, amountIn, currencyOut, options)[0] ?? null
} else {
const amountOut = amountSpecified
currentTrade = Trade.bestTradeExactOut(allowedPairs, currencyIn, amountOut, options)[0] ?? null
}
// if current trade is best yet, save it
if (isTradeBetter(bestTradeSoFar, currentTrade, BETTER_TRADE_LESS_HOPS_THRESHOLD)) {
bestTradeSoFar = currentTrade
}
}
return bestTradeSoFar
}
return null
}, [tradeType, amountSpecified, currencyIn, currencyOut, allowedPairs, maxHops])
}
......@@ -2,11 +2,10 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-
import { useWeb3React } from '@web3-react/core'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo, useRef } from 'react'
import { RouterPreference, useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { SupportedChainId } from '../constants/chains'
import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
import { useBestV2Trade } from './useBestV2Trade'
import { useClientSideV3Trade } from './useClientSideV3Trade'
// Stablecoin amounts used when calculating spot price for a given currency.
// The amount is large enough to filter low liquidity pairs.
......@@ -28,11 +27,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
const stablecoin = amountOut?.currency
// TODO(#2808): remove dependency on useBestV2Trade
const v2USDCTrade = useBestV2Trade(TradeType.EXACT_OUTPUT, amountOut, currency, {
maxHops: 2,
})
const v3USDCTrade = useClientSideV3Trade(TradeType.EXACT_OUTPUT, amountOut, currency)
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.CLIENT)
const price = useMemo(() => {
if (!currency || !stablecoin) {
return undefined
......@@ -43,17 +38,13 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
return new Price(stablecoin, stablecoin, '1', '1')
}
// use v2 price if available, v3 as fallback
if (v2USDCTrade) {
const { numerator, denominator } = v2USDCTrade.route.midPrice
return new Price(currency, stablecoin, denominator, numerator)
} else if (v3USDCTrade.trade) {
const { numerator, denominator } = v3USDCTrade.trade.routes[0].midPrice
if (trade) {
const { numerator, denominator } = trade.routes[0].midPrice
return new Price(currency, stablecoin, denominator, numerator)
}
return undefined
}, [currency, stablecoin, v2USDCTrade, v3USDCTrade.trade])
}, [currency, stablecoin, trade])
const lastPrice = useRef(price)
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
......
import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
......@@ -16,7 +16,7 @@ import {
} from 'components/AmplitudeAnalytics/utils'
import { sendEvent } from 'components/analytics'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import { getPriceImpactPercent } from 'components/swap/AdvancedSwapDetails'
import PriceImpactWarning from 'components/swap/PriceImpactWarning'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { MouseoverTooltip } from 'components/Tooltip'
......@@ -68,8 +68,7 @@ import { useExpertModeManager } from '../../state/user/hooks'
import { LinkStyledButton, ThemedText } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeRealizedLPFeePercent } from '../../utils/prices'
import { warningSeverity } from '../../utils/prices'
import { computeRealizedPriceImpact, warningSeverity } from '../../utils/prices'
import { supportedChainId } from '../../utils/supportedChainId'
import AppBody from '../AppBody'
......@@ -86,19 +85,27 @@ export function getIsValidSwapQuote(
return !!swapInputError && !!trade && (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING)
}
function largerPercentValue(a?: Percent, b?: Percent) {
if (a && b) {
return a.greaterThan(b) ? a : b
} else if (a) {
return a
} else if (b) {
return b
}
return undefined
}
const formatAnalyticsEventProperties = (
trade: InterfaceTrade<Currency, Currency, TradeType>,
fetchingSwapQuoteStartTime: Date | undefined
) => {
const lpFeePercent = trade ? computeRealizedLPFeePercent(trade) : undefined
return {
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
price_impact_basis_points: lpFeePercent
? formatPercentInBasisPointsNumber(getPriceImpactPercent(lpFeePercent, trade))
: undefined,
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
......@@ -205,7 +212,7 @@ export default function Swap() {
const outputValue = showWrap ? parsedAmount : trade?.outputAmount
const fiatValueInput = useStablecoinValue(inputValue)
const fiatValueOutput = useStablecoinValue(outputValue)
const priceImpact = useMemo(
const stablecoinPriceImpact = useMemo(
() => (routeIsSyncing ? undefined : computeFiatValuePriceImpact(fiatValueInput, fiatValueOutput)),
[fiatValueInput, fiatValueOutput, routeIsSyncing]
)
......@@ -334,7 +341,7 @@ export default function Swap() {
if (!swapCallback) {
return
}
if (priceImpact && !confirmPriceImpactWithoutFee(priceImpact)) {
if (stablecoinPriceImpact && !confirmPriceImpactWithoutFee(stablecoinPriceImpact)) {
return
}
setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined })
......@@ -373,7 +380,7 @@ export default function Swap() {
})
}, [
swapCallback,
priceImpact,
stablecoinPriceImpact,
tradeToConfirm,
showConfirm,
recipient,
......@@ -389,16 +396,11 @@ export default function Swap() {
const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>()
// warnings on the greater of fiat value price impact and execution price impact
const priceImpactSeverity = useMemo(() => {
const executionPriceImpact = trade?.priceImpact
return warningSeverity(
executionPriceImpact && priceImpact
? executionPriceImpact.greaterThan(priceImpact)
? executionPriceImpact
: priceImpact
: executionPriceImpact ?? priceImpact
)
}, [priceImpact, trade])
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
const marketPriceImpact = trade?.priceImpact ? computeRealizedPriceImpact(trade) : undefined
const largerPriceImpact = largerPercentValue(marketPriceImpact, stablecoinPriceImpact)
return { priceImpactSeverity: warningSeverity(largerPriceImpact), largerPriceImpact }
}, [stablecoinPriceImpact, trade])
const isArgentWallet = useIsArgentWallet()
......@@ -448,6 +450,7 @@ export default function Swap() {
const swapIsUnsupported = useIsSwapUnsupported(currencies[Field.INPUT], currencies[Field.OUTPUT])
const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode
const showPriceImpactWarning = largerPriceImpact && priceImpactSeverity > 3
// Handle time based logging events and event properties.
useEffect(() => {
......@@ -562,7 +565,7 @@ export default function Swap() {
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
priceImpact={priceImpact}
priceImpact={stablecoinPriceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
......@@ -596,6 +599,7 @@ export default function Swap() {
allowedSlippage={allowedSlippage}
/>
)}
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
......
......@@ -13,6 +13,11 @@ import { useClientSideRouter } from 'state/user/hooks'
import { GetQuoteResult, InterfaceTrade, TradeState } from './types'
import { computeRoutes, transformRoutesToTrade } from './utils'
export enum RouterPreference {
CLIENT = 'client',
API = 'api',
}
/**
* Returns the best trade by invoking the routing api or the smart order router on the client
* @param tradeType whether the swap is an exact in/out
......@@ -22,7 +27,8 @@ import { computeRoutes, transformRoutesToTrade } from './utils'
export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
otherCurrency?: Currency,
routerPreference?: RouterPreference
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
......@@ -35,7 +41,10 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
[amountSpecified, otherCurrency, tradeType]
)
const [clientSideRouter] = useClientSideRouter()
const [clientSideRouterStoredPreference] = useClientSideRouter()
const clientSideRouter = routerPreference
? routerPreference === RouterPreference.CLIENT
: clientSideRouterStoredPreference
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment