diff --git a/src/components/Swap/Input.tsx b/src/components/Swap/Input.tsx index 53fb6b441..838b9a59b 100644 --- a/src/components/Swap/Input.tsx +++ b/src/components/Swap/Input.tsx @@ -85,7 +85,7 @@ interface FieldWrapperProps { field: Field maxAmount?: string approved?: boolean - impact?: PriceImpact + fiatValueChange?: PriceImpact subheader: string } @@ -93,7 +93,7 @@ export function FieldWrapper({ field, maxAmount, approved, - impact, + fiatValueChange, className, subheader, }: FieldWrapperProps & { className?: string }) { @@ -164,7 +164,7 @@ export function FieldWrapper({ {usdc && `${formatCurrencyAmount(usdc, NumberType.FiatTokenQuantity)}`} - + {balance && ( diff --git a/src/components/Swap/Output.tsx b/src/components/Swap/Output.tsx index 26ffd9079..3a52aea9f 100644 --- a/src/components/Swap/Output.tsx +++ b/src/components/Swap/Output.tsx @@ -28,8 +28,7 @@ const OutputWrapper = styled(FieldWrapper)<{ hasColor?: boolean | null; isWide: ` export default function Output() { - const { impact } = useSwapInfo() - + const { fiatValueChange } = useSwapInfo() const [currency] = useSwapCurrency(Field.OUTPUT) const overrideColor = useAtomValue(colorAtom) const dynamicColor = useCurrencyColor(currency) @@ -43,7 +42,7 @@ export default function Output() { diff --git a/src/components/Swap/Settings/MaxSlippageSelect.tsx b/src/components/Swap/Settings/MaxSlippageSelect.tsx index 4e781e393..930a97b47 100644 --- a/src/components/Swap/Settings/MaxSlippageSelect.tsx +++ b/src/components/Swap/Settings/MaxSlippageSelect.tsx @@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro' import Expando, { IconPrefix } from 'components/Expando' import Popover from 'components/Popover' import { useTooltip } from 'components/Tooltip' +import { useSwapInfo } from 'hooks/swap' import { getSlippageWarning, toPercent } from 'hooks/useSlippage' import { Expando as ExpandoIcon } from 'icons' import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'icons' @@ -144,6 +145,8 @@ export default function MaxSlippageSelect() { [setSlippage] ) + const { slippage: allowedSlippage } = useSwapInfo() + const [open, setOpen] = useState(false) return ( @@ -190,7 +193,7 @@ export default function MaxSlippageSelect() { size={Math.max(maxSlippageInput.length, 4)} value={maxSlippageInput} onChange={(input) => processInput(input)} - placeholder={'0.10'} + placeholder={allowedSlippage.allowed.toFixed(2)} ref={input} data-testid="input-slippage" maxLength={10} diff --git a/src/components/Swap/Summary/Details.tsx b/src/components/Swap/Summary/Details.tsx index 725a1a9b8..f653aa009 100644 --- a/src/components/Swap/Summary/Details.tsx +++ b/src/components/Swap/Summary/Details.tsx @@ -156,7 +156,7 @@ export default function Details({ trade, slippage, gasUseEstimateUSD, inputUSDC, details.push([t`Price impact`, impact?.percent ? impact?.toString() : '-', impact.warning]) } - const { estimateMessage, descriptor, value } = getEstimateMessage(trade, slippage, impact) + const { estimateMessage, descriptor, value } = getEstimateMessage(trade, slippage) details.push([descriptor, value]) return { details, estimateMessage } diff --git a/src/components/Swap/Summary/Estimate.tsx b/src/components/Swap/Summary/Estimate.tsx index f86cd41ee..75726dc6a 100644 --- a/src/components/Swap/Summary/Estimate.tsx +++ b/src/components/Swap/Summary/Estimate.tsx @@ -1,7 +1,6 @@ import { t, Trans } from '@lingui/macro' import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format' -import { PriceImpact } from 'hooks/usePriceImpact' -import { Slippage } from 'hooks/useSlippage' +import { formatSlippage, Slippage } from 'hooks/useSlippage' import { ReactNode, useMemo } from 'react' import { InterfaceTrade } from 'state/routing/types' import styled from 'styled-components/macro' @@ -20,22 +19,25 @@ interface EstimateProps { } export default function SwapInputOutputEstimate({ trade, slippage }: EstimateProps) { - const { estimateMessage } = useMemo( - () => getEstimateMessage(trade, slippage, undefined /* priceImpact */), - [slippage, trade] - ) + const { estimateMessage } = useMemo(() => getEstimateMessage(trade, slippage), [slippage, trade]) return {estimateMessage} } export function getEstimateMessage( - trade: InterfaceTrade, - slippage: Slippage, - priceImpact: PriceImpact | undefined + trade: InterfaceTrade | undefined, + slippage: Slippage ): { estimateMessage: string descriptor: ReactNode value: string } { + if (!trade) { + return { + estimateMessage: '', + descriptor: '', + value: '-', + } + } const { inputAmount, outputAmount } = trade const inputCurrency = inputAmount.currency const outputCurrency = outputAmount.currency @@ -49,10 +51,10 @@ export function getEstimateMessage( descriptor: ( Minimum output after slippage - {priceImpact && ( + {slippage && ( {' '} - ({priceImpact?.toString()}) + ({formatSlippage(slippage)}) )} @@ -68,10 +70,10 @@ export function getEstimateMessage( descriptor: ( Maximum input after slippage - {priceImpact && ( + {slippage && ( {' '} - ({priceImpact?.toString()}) + ({formatSlippage(slippage)}) )} diff --git a/src/components/Swap/TokenInput.tsx b/src/components/Swap/TokenInput.tsx index 3df53ebd5..306e8a6e9 100644 --- a/src/components/Swap/TokenInput.tsx +++ b/src/components/Swap/TokenInput.tsx @@ -18,10 +18,8 @@ const TokenInputRow = styled(Row)` const ValueInput = styled(DecimalInput)` color: ${({ theme }) => theme.primary}; - height: 1.5rem; - margin: -0.25rem 0; - ${loadingTransitionCss}; + ${loadingTransitionCss} ` const TokenInputColumn = styled(Column)` diff --git a/src/components/Swap/Toolbar/index.tsx b/src/components/Swap/Toolbar/index.tsx index 6bbcd81eb..d6f4e845e 100644 --- a/src/components/Swap/Toolbar/index.tsx +++ b/src/components/Swap/Toolbar/index.tsx @@ -10,10 +10,9 @@ import { memo, ReactNode, useCallback, useContext, useMemo } from 'react' import { TradeState } from 'state/routing/types' import { Field } from 'state/swap' import styled from 'styled-components/macro' -import { ThemedText } from 'theme' import Row from '../../Row' -import SwapInputOutputEstimate from '../Summary/Estimate' +import SwapInputOutputEstimate, { getEstimateMessage } from '../Summary/Estimate' import SwapActionButton from '../SwapActionButton' import * as Caption from './Caption' import { Context as ToolbarContext, Provider as ToolbarContextProvider } from './ToolbarContext' @@ -111,6 +110,7 @@ function CaptionRow() { const tradeSummaryRows: SummaryRowProps[] = useMemo(() => { const currencySymbol = trade?.outputAmount?.currency.symbol ?? '' + const { descriptor, value } = getEstimateMessage(trade, slippage) const rows: SummaryRowProps[] = [ { name: t`Network fee`, @@ -128,16 +128,9 @@ function CaptionRow() { : undefined, }, { - name: ( - - Minimum output after slippage - - {' '} - ({impact?.toString()}) - - - ), - value: trade ? `${formatCurrencyAmount(trade?.minimumAmountOut(slippage.allowed))} ${currencySymbol}` : '-', + // min/max output/input after slippage + name:
{descriptor}
, + value, }, { name: t`Expected output`, diff --git a/src/components/Swap/index.tsx b/src/components/Swap/index.tsx index c78b7f1e1..c51d25a97 100644 --- a/src/components/Swap/index.tsx +++ b/src/components/Swap/index.tsx @@ -48,21 +48,21 @@ export default function Swap(props: SwapProps) { return ( <> -
Swap}> - - -
-
- - + +
Swap}> + + +
+
+ {useBrandedFooter() && } - - -
+
+
+ {displayTx && ( setDisplayTxHash()} /> diff --git a/src/hooks/swap/useSwapInfo.tsx b/src/hooks/swap/useSwapInfo.tsx index a370d49d4..a72ff80e4 100644 --- a/src/hooks/swap/useSwapInfo.tsx +++ b/src/hooks/swap/useSwapInfo.tsx @@ -6,7 +6,7 @@ import { useRouterTrade } from 'hooks/routing/useRouterTrade' import { useCurrencyBalances } from 'hooks/useCurrencyBalance' import useOnSupportedNetwork from 'hooks/useOnSupportedNetwork' import usePermit2Allowance, { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' -import { PriceImpact, usePriceImpact } from 'hooks/usePriceImpact' +import { PriceImpact, useFiatValueChange, usePriceImpact } from 'hooks/usePriceImpact' import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'hooks/useSlippage' import { usePermit2 as usePermit2Enabled } from 'hooks/useSyncFlags' import useUSDCPrice, { useUSDCValue } from 'hooks/useUSDCPrice' @@ -50,6 +50,7 @@ interface SwapInfo { allowance: Allowance slippage: Slippage impact?: PriceImpact + fiatValueChange?: PriceImpact } /** Returns the best computed swap (trade/wrap). */ @@ -105,6 +106,7 @@ function useComputeSwapInfo(): SwapInfo { // Wait until the trade is valid to avoid displaying incorrect intermediate values. const slippage = useSlippage(trade) const impact = usePriceImpact(trade.trade) + const fiatValueChange = useFiatValueChange(trade.trade) const permit2Enabled = usePermit2Enabled() const maximumAmountIn = useMemo(() => { @@ -137,6 +139,7 @@ function useComputeSwapInfo(): SwapInfo { allowance, slippage, impact, + fiatValueChange, } }, [ allowance, @@ -148,6 +151,7 @@ function useComputeSwapInfo(): SwapInfo { currencyIn, currencyOut, error, + fiatValueChange, impact, slippage, trade, diff --git a/src/hooks/useAllCurrencyCombinations.ts b/src/hooks/useAllCurrencyCombinations.ts deleted file mode 100644 index 1a92680be..000000000 --- a/src/hooks/useAllCurrencyCombinations.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Currency, Token } from '@uniswap/sdk-core' -import { ADDITIONAL_BASES, BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from 'constants/routing' -import { useMemo } from 'react' - -export function useAllCurrencyCombinations(currencyA?: Currency, currencyB?: Currency): [Token, Token][] { - const chainId = currencyA?.chainId - - const [tokenA, tokenB] = chainId ? [currencyA?.wrapped, currencyB?.wrapped] : [undefined, undefined] - - const bases: Token[] = useMemo(() => { - if (!chainId || chainId !== tokenB?.chainId) return [] - - const common = BASES_TO_CHECK_TRADES_AGAINST[chainId] ?? [] - const additionalA = tokenA ? ADDITIONAL_BASES[chainId]?.[tokenA.address] ?? [] : [] - const additionalB = tokenB ? ADDITIONAL_BASES[chainId]?.[tokenB.address] ?? [] : [] - - return [...common, ...additionalA, ...additionalB] - }, [chainId, tokenA, tokenB]) - - const basePairs: [Token, Token][] = useMemo( - () => - bases - .flatMap((base): [Token, Token][] => bases.map((otherBase) => [base, otherBase])) - // though redundant with the first filter below, that expression runs more often, so this is probably worthwhile - .filter(([t0, t1]) => !t0.equals(t1)), - [bases] - ) - - return useMemo( - () => - tokenA && tokenB - ? [ - // the direct pair - [tokenA, tokenB] as [Token, Token], - // token A against all bases - ...bases.map((base): [Token, Token] => [tokenA, base]), - // token B against all bases - ...bases.map((base): [Token, Token] => [tokenB, base]), - // each base against all bases - ...basePairs, - ] - // filter out invalid pairs comprised of the same asset (e.g. WETH<>WETH) - .filter(([t0, t1]) => !t0.equals(t1)) - // filter out duplicate pairs - .filter(([t0, t1], i, otherPairs) => { - // find the first index in the array at which there are the same 2 tokens as the current - const firstIndexInOtherPairs = otherPairs.findIndex(([t0Other, t1Other]) => { - return (t0.equals(t0Other) && t1.equals(t1Other)) || (t0.equals(t1Other) && t1.equals(t0Other)) - }) - // only accept the first occurrence of the same 2 tokens - return firstIndexInOtherPairs === i - }) - // optionally filter out some pairs for tokens with custom bases defined - .filter(([tokenA, tokenB]) => { - if (!chainId) return true - const customBases = CUSTOM_BASES[chainId] - - const customBasesA: Token[] | undefined = customBases?.[tokenA.address] - const customBasesB: Token[] | undefined = customBases?.[tokenB.address] - - if (!customBasesA && !customBasesB) return true - - if (customBasesA && !customBasesA.find((base) => tokenB.equals(base))) return false - if (customBasesB && !customBasesB.find((base) => tokenA.equals(base))) return false - - return true - }) - : [], - [tokenA, tokenB, bases, basePairs, chainId] - ) -} diff --git a/src/hooks/usePriceImpact.ts b/src/hooks/usePriceImpact.ts index 555ad8603..6ce3b9656 100644 --- a/src/hooks/usePriceImpact.ts +++ b/src/hooks/usePriceImpact.ts @@ -1,8 +1,11 @@ import { Percent } from '@uniswap/sdk-core' import { useMemo } from 'react' import { InterfaceTrade } from 'state/routing/types' +import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact' import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices' +import { useUSDCValue } from './useUSDCPrice' + export interface PriceImpact { percent: Percent warning?: 'warning' | 'error' @@ -22,6 +25,21 @@ export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined }, [trade]) } +export function useFiatValueChange(trade?: InterfaceTrade) { + const [inputUSDCValue, outputUSDCValue] = [useUSDCValue(trade?.inputAmount), useUSDCValue(trade?.outputAmount)] + return useMemo(() => { + const fiatPriceImpact = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue) + if (!fiatPriceImpact) { + return undefined + } + return { + percent: fiatPriceImpact, + warning: getPriceImpactWarning(fiatPriceImpact), + toString: () => toHumanReadablePercent(fiatPriceImpact), + } + }, [inputUSDCValue, outputUSDCValue]) +} + export function toHumanReadablePercent(priceImpact: Percent): string { const sign = priceImpact.lessThan(0) ? '+' : '' const exactFloat = (Number(priceImpact.numerator) / Number(priceImpact.denominator)) * 100 diff --git a/src/hooks/useSlippage.ts b/src/hooks/useSlippage.ts index 4293c5a30..8228f11cd 100644 --- a/src/hooks/useSlippage.ts +++ b/src/hooks/useSlippage.ts @@ -5,6 +5,8 @@ import { useMemo } from 'react' import { InterfaceTrade } from 'state/routing/types' import { slippageAtom } from 'state/swap/settings' +import { toHumanReadablePercent } from './usePriceImpact' + export function toPercent(maxSlippage: string | undefined): Percent | undefined { if (!maxSlippage) return undefined if (Number.isNaN(maxSlippage)) return undefined @@ -18,7 +20,10 @@ export interface Slippage { warning?: 'warning' | 'error' } -export const DEFAULT_SLIPPAGE = { auto: true, allowed: DEFAULT_AUTO_SLIPPAGE } +export const DEFAULT_SLIPPAGE = { + auto: true, + allowed: DEFAULT_AUTO_SLIPPAGE, +} /** Returns the allowed slippage, and whether it is auto-slippage. */ export default function useSlippage(trade: { @@ -47,3 +52,7 @@ export function getSlippageWarning(slippage?: Percent): 'warning' | 'error' | un if (slippage?.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning' return } + +export function formatSlippage(slippage: Slippage): string { + return toHumanReadablePercent(slippage.allowed) +} diff --git a/src/utils/computeFiatValuePriceImpact.test.ts b/src/utils/computeFiatValuePriceImpact.test.ts new file mode 100644 index 000000000..fd80929e3 --- /dev/null +++ b/src/utils/computeFiatValuePriceImpact.test.ts @@ -0,0 +1,37 @@ +import { CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { DAI, USDC_MAINNET } from 'constants/tokens' + +import { computeFiatValuePriceImpact } from './computeFiatValuePriceImpact' + +describe('computeFiatValuePriceImpact', () => { + it('should return undefined', () => { + expect(computeFiatValuePriceImpact(null, null)).toBeUndefined() + }) + + it('should return 0: same currency, same value', () => { + expect( + computeFiatValuePriceImpact( + CurrencyAmount.fromRawAmount(USDC_MAINNET, 100), + CurrencyAmount.fromRawAmount(USDC_MAINNET, 100) + ) + ).toEqual(new Percent(0, 100)) + }) + + it('should return 0.5: same currency, different values', () => { + expect( + computeFiatValuePriceImpact( + CurrencyAmount.fromRawAmount(USDC_MAINNET, 100), + CurrencyAmount.fromRawAmount(USDC_MAINNET, 150) + )?.toFixed(2) + ).toEqual(new Percent(-50, 100).toFixed(2)) + }) + + it('should return undefined: different currencies', () => { + expect( + computeFiatValuePriceImpact( + CurrencyAmount.fromRawAmount(USDC_MAINNET, 100), + CurrencyAmount.fromRawAmount(DAI, 150) + )?.toFixed(2) + ).toEqual(undefined) + }) +}) diff --git a/src/utils/computeFiatValuePriceImpact.tsx b/src/utils/computeFiatValuePriceImpact.tsx new file mode 100644 index 000000000..6138d13c0 --- /dev/null +++ b/src/utils/computeFiatValuePriceImpact.tsx @@ -0,0 +1,14 @@ +import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' +import { ONE_HUNDRED_PERCENT } from 'constants/misc' +import JSBI from 'jsbi' + +export function computeFiatValuePriceImpact( + fiatValueInput: CurrencyAmount | undefined | null, + fiatValueOutput: CurrencyAmount | 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) +}