From ffe334ccbffedf5033d6fbe7d5cb33f1cae2edf4 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 25 Jan 2022 10:48:52 -0800 Subject: [PATCH] feat: update summary view with real values (#3179) * refactor: isolate approval callback hooks * fix: use approval callback from trade * chore: pass optimized trade to summary * start review screen UI updates * chore: pass optimized trade to summary * fix: pass Trade to summary * remove uneeded value type * remove uneeded styling * code cleanup * code styling, update props * fix fixture bug, code style updates * bug fix in details array * update logic in details Co-authored-by: ianlapham --- src/components/swap/ConfirmSwapModal.tsx | 18 +------ src/components/swap/TradePrice.tsx | 2 +- src/lib/components/Swap/Summary.fixture.tsx | 25 +++++++--- src/lib/components/Swap/Summary/Details.tsx | 41 +++++++++------ src/lib/components/Swap/Summary/index.tsx | 55 ++++++++++++--------- src/lib/components/Swap/SwapButton.tsx | 43 +++++++++------- src/utils/tradeMeaningFullyDiffer.ts | 19 +++++++ 7 files changed, 121 insertions(+), 82 deletions(-) create mode 100644 src/utils/tradeMeaningFullyDiffer.ts diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index f95f2f2ba3..0ce10bf7d6 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -3,6 +3,7 @@ import { Trade } from '@uniswap/router-sdk' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { ReactNode, useCallback, useMemo } from 'react' import { InterfaceTrade } from 'state/routing/types' +import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' import TransactionConfirmationModal, { ConfirmationModalContent, @@ -11,23 +12,6 @@ import TransactionConfirmationModal, { import SwapModalFooter from './SwapModalFooter' import SwapModalHeader from './SwapModalHeader' -/** - * Returns true if the trade requires a confirmation of details before we can submit it - * @param args either a pair of V2 trades or a pair of V3 trades - */ -function tradeMeaningfullyDiffers( - ...args: [Trade, Trade] -): boolean { - const [tradeA, tradeB] = args - return ( - tradeA.tradeType !== tradeB.tradeType || - !tradeA.inputAmount.currency.equals(tradeB.inputAmount.currency) || - !tradeA.inputAmount.equalTo(tradeB.inputAmount) || - !tradeA.outputAmount.currency.equals(tradeB.outputAmount.currency) || - !tradeA.outputAmount.equalTo(tradeB.outputAmount) - ) -} - export default function ConfirmSwapModal({ trade, originalTrade, diff --git a/src/components/swap/TradePrice.tsx b/src/components/swap/TradePrice.tsx index 15f3382436..18eda1d1d5 100644 --- a/src/components/swap/TradePrice.tsx +++ b/src/components/swap/TradePrice.tsx @@ -16,7 +16,7 @@ const StyledPriceContainer = styled.button` background-color: transparent; border: none; cursor: pointer; - align-items: center + align-items: center; justify-content: flex-start; padding: 0; grid-template-columns: 1fr auto; diff --git a/src/lib/components/Swap/Summary.fixture.tsx b/src/lib/components/Swap/Summary.fixture.tsx index 019a960c64..08eabb0879 100644 --- a/src/lib/components/Swap/Summary.fixture.tsx +++ b/src/lib/components/Swap/Summary.fixture.tsx @@ -2,8 +2,10 @@ import { tokens } from '@uniswap/default-token-list' import { SupportedChainId } from 'constants/chains' import { nativeOnChain } from 'constants/tokens' import { useUpdateAtom } from 'jotai/utils' +import { useSwapInfo } from 'lib/hooks/swap' +import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo' import { Field, swapAtom } from 'lib/state/swap' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import invariant from 'tiny-invariant' @@ -18,9 +20,12 @@ const UNI = (function () { })() function Fixture() { - const [initialized, setInitialized] = useState(false) const setState = useUpdateAtom(swapAtom) - // eslint-disable-next-line react-hooks/exhaustive-deps + const { + allowedSlippage, + trade: { trade }, + } = useSwapInfo() + useEffect(() => { setState({ independentField: Field.INPUT, @@ -28,14 +33,18 @@ function Fixture() { [Field.INPUT]: ETH, [Field.OUTPUT]: UNI, }) - setInitialized(true) - }) + }, [setState]) - return initialized ? ( + return trade ? ( - void 0} /> + void 0} trade={trade} allowedSlippage={allowedSlippage} /> ) : null } -export default +export default ( + <> + + + +) diff --git a/src/lib/components/Swap/Summary/Details.tsx b/src/lib/components/Swap/Summary/Details.tsx index 20c66a6c3c..8a44e90856 100644 --- a/src/lib/components/Swap/Summary/Details.tsx +++ b/src/lib/components/Swap/Summary/Details.tsx @@ -1,12 +1,12 @@ import { t } from '@lingui/macro' -import { Currency } from '@uniswap/sdk-core' +import { Trade } from '@uniswap/router-sdk' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { useAtom } from 'jotai' -import { useAtomValue } from 'jotai/utils' -import { settingsAtom } from 'lib/state/settings' import { integratorFeeAtom } from 'lib/state/swap' import { ThemedText } from 'lib/theme' import { useMemo } from 'react' import { currencyId } from 'utils/currencyId' +import { computeRealizedLPFeePercent } from 'utils/prices' import Row from '../../Row' @@ -27,31 +27,42 @@ function Detail({ label, value }: DetailProps) { } interface DetailsProps { - input: Currency - output: Currency + trade: Trade + allowedSlippage: Percent } -export default function Details({ input, output }: DetailsProps) { - const integrator = window.location.hostname +export default function Details({ trade, allowedSlippage }: DetailsProps) { + const { inputAmount, outputAmount } = trade + const inputCurrency = inputAmount.currency + const outputCurrency = outputAmount.currency - const { maxSlippage } = useAtomValue(settingsAtom) + const integrator = window.location.hostname const [integratorFee] = useAtom(integratorFeeAtom) + const priceImpact = useMemo(() => { + const realizedLpFeePercent = computeRealizedLPFeePercent(trade) + return trade.priceImpact.subtract(realizedLpFeePercent) + }, [trade]) + const details = useMemo((): [string, string][] => { - // @TODO(ianlapham) = update details to pull derived value from useDerivedSwapInfo + // @TODO(ianlapham): Check that provider fee is even a valid list item return [ // [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`], - [t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(input)}`], - // [t`Price impact`, `${swap.priceImpact}%`], - // [t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`], - // [t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`], - [t`Slippage tolerance`, `${maxSlippage}%`], + [t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(inputCurrency)}`], + [t`Price impact`, `${priceImpact.toFixed(2)}%`], + trade.tradeType === TradeType.EXACT_INPUT + ? [t`Maximum sent`, `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${inputCurrency.symbol}`] + : [], + trade.tradeType === TradeType.EXACT_OUTPUT + ? [t`Minimum received`, `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${outputCurrency.symbol}`] + : [], + [t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`], ].filter(isDetail) function isDetail(detail: unknown[]): detail is [string, string] { return Boolean(detail[1]) } - }, [input, integrator, integratorFee, maxSlippage]) + }, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade]) return ( <> {details.map(([label, detail]) => ( diff --git a/src/lib/components/Swap/Summary/index.tsx b/src/lib/components/Swap/Summary/index.tsx index fe12b509a5..32c026452f 100644 --- a/src/lib/components/Swap/Summary/index.tsx +++ b/src/lib/components/Swap/Summary/index.tsx @@ -1,11 +1,14 @@ import { Trans } from '@lingui/macro' +import { Trade } from '@uniswap/router-sdk' +import { Currency, Percent, TradeType } from '@uniswap/sdk-core' +import { useAtomValue } from 'jotai/utils' import { IconButton } from 'lib/components/Button' -import { useSwapInfo } from 'lib/hooks/swap' import useScrollbar from 'lib/hooks/useScrollbar' import { Expando, Info } from 'lib/icons' -import { Field } from 'lib/state/swap' +import { Field, independentFieldAtom } from 'lib/state/swap' import styled, { ThemedText } from 'lib/theme' -import { useState } from 'react' +import { useMemo, useState } from 'react' +import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer' import ActionButton from '../../ActionButton' import Column from '../../Column' @@ -17,8 +20,6 @@ import Summary from './Summary' export default Summary -const updated = { message: Price updated, action: Accept } - const SummaryColumn = styled(Column)`` const ExpandoColumn = styled(Column)`` const DetailsColumn = styled(Column)`` @@ -70,21 +71,27 @@ const Body = styled(Column)<{ open: boolean }>` } ` +const priceUpdate = { message: Price updated, action: Accept } + interface SummaryDialogProps { + trade: Trade + allowedSlippage: Percent onConfirm: () => void } -export function SummaryDialog({ onConfirm }: SummaryDialogProps) { - const { - trade: { trade }, - currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount }, - currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency }, - } = useSwapInfo() - - const price = trade?.executionPrice +export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) { + const { inputAmount, outputAmount } = trade + const inputCurrency = inputAmount.currency + const outputCurrency = outputAmount.currency + const price = trade.executionPrice - const [confirmedPrice, confirmPrice] = useState(price) + const independentField = useAtomValue(independentFieldAtom) + const [confirmedTrade, setConfirmedTrade] = useState(trade) + const doesTradeDiffer = useMemo( + () => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)), + [confirmedTrade, trade] + ) const [open, setOpen] = useState(true) const [details, setDetails] = useState(null) @@ -119,26 +126,28 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { -
+
- Output is estimated. {/* //@TODO(ianlapham): update with actual recieved values */} - {/* {swap?.minimumReceived && ( + Output is estimated. + {independentField === Field.INPUT && ( - You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert. + You will send at most {trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {inputCurrency.symbol}{' '} + or the transaction will revert. )} - {swap?.maximumSent && ( + {independentField === Field.OUTPUT && ( - You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert. + You will receive at least {trade.minimumAmountOut(allowedSlippage).toSignificant(6)}{' '} + {outputCurrency.symbol} or the transaction will revert. - )} */} + )} confirmPrice(price)} - updated={price === confirmedPrice ? undefined : updated} + onUpdate={() => setConfirmedTrade(trade)} + updated={doesTradeDiffer ? priceUpdate : undefined} > Confirm swap diff --git a/src/lib/components/Swap/SwapButton.tsx b/src/lib/components/Swap/SwapButton.tsx index 38668b09b0..c2a6639bb1 100644 --- a/src/lib/components/Swap/SwapButton.tsx +++ b/src/lib/components/Swap/SwapButton.tsx @@ -2,35 +2,36 @@ import { Trans } from '@lingui/macro' import { useSwapInfo } from 'lib/hooks/swap' import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval' import { Field } from 'lib/state/swap' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import ActionButton from '../ActionButton' import Dialog from '../Dialog' import { StatusDialog } from './Status' import { SummaryDialog } from './Summary' -enum Mode { - SWAP, - SUMMARY, - STATUS, -} - interface SwapButtonProps { disabled?: boolean } + export default function SwapButton({ disabled }: SwapButtonProps) { - const [mode, setMode] = useState(Mode.SWAP) const { trade, allowedSlippage, currencyBalances: { [Field.INPUT]: inputCurrencyBalance }, currencyAmounts: { [Field.INPUT]: inputCurrencyAmount }, } = useSwapInfo() + + const [activeTrade, setActiveTrade] = useState(undefined) + useEffect(() => { + setActiveTrade((activeTrade) => activeTrade && trade.trade) + }, [trade]) + // TODO(zzmp): Track pending approval const useIsPendingApproval = () => false + + // TODO(zzmp): Return an optimized trade directly from useSwapInfo. const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval) - // TODO(zzmp): Pass optimized trade to SummaryDialog const actionProps = useMemo(() => { if (disabled) return { disabled: true } @@ -50,24 +51,30 @@ export default function SwapButton({ disabled }: SwapButtonProps) { } return { disabled: true } - }, [disabled, approval, inputCurrencyAmount, inputCurrencyBalance]) + }, [approval, disabled, inputCurrencyAmount, inputCurrencyBalance]) + const onConfirm = useCallback(() => { - // TODO: Send the tx to the connected wallet. - setMode(Mode.STATUS) + // TODO(zzmp): Transact the trade. }, []) + return ( <> - setMode(Mode.SUMMARY)} onUpdate={getApproval} {...actionProps}> + setActiveTrade(trade.trade)} + onUpdate={getApproval} + {...actionProps} + > Review swap - {mode >= Mode.SUMMARY && ( - setMode(Mode.SWAP)}> - + {activeTrade && ( + setActiveTrade(undefined)}> + )} - {mode >= Mode.STATUS && ( + {false && ( - setMode(Mode.SWAP)} /> + void 0} /> )} diff --git a/src/utils/tradeMeaningFullyDiffer.ts b/src/utils/tradeMeaningFullyDiffer.ts new file mode 100644 index 0000000000..485960e77a --- /dev/null +++ b/src/utils/tradeMeaningFullyDiffer.ts @@ -0,0 +1,19 @@ +import { Trade } from '@uniswap/router-sdk' +import { Currency, TradeType } from '@uniswap/sdk-core' + +/** + * Returns true if the trade requires a confirmation of details before we can submit it + * @param args either a pair of V2 trades or a pair of V3 trades + */ +export function tradeMeaningfullyDiffers( + ...args: [Trade, Trade] +): boolean { + const [tradeA, tradeB] = args + return ( + tradeA.tradeType !== tradeB.tradeType || + !tradeA.inputAmount.currency.equals(tradeB.inputAmount.currency) || + !tradeA.inputAmount.equalTo(tradeB.inputAmount) || + !tradeA.outputAmount.currency.equals(tradeB.outputAmount.currency) || + !tradeA.outputAmount.equalTo(tradeB.outputAmount) + ) +}