Commit ffe334cc authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

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: default avatarianlapham <ianlapham@gmail.com>
parent ffe2bd31
...@@ -3,6 +3,7 @@ import { Trade } from '@uniswap/router-sdk' ...@@ -3,6 +3,7 @@ import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ReactNode, useCallback, useMemo } from 'react' import { ReactNode, useCallback, useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import TransactionConfirmationModal, { import TransactionConfirmationModal, {
ConfirmationModalContent, ConfirmationModalContent,
...@@ -11,23 +12,6 @@ import TransactionConfirmationModal, { ...@@ -11,23 +12,6 @@ import TransactionConfirmationModal, {
import SwapModalFooter from './SwapModalFooter' import SwapModalFooter from './SwapModalFooter'
import SwapModalHeader from './SwapModalHeader' 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<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
): 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({ export default function ConfirmSwapModal({
trade, trade,
originalTrade, originalTrade,
......
...@@ -16,7 +16,7 @@ const StyledPriceContainer = styled.button` ...@@ -16,7 +16,7 @@ const StyledPriceContainer = styled.button`
background-color: transparent; background-color: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
align-items: center align-items: center;
justify-content: flex-start; justify-content: flex-start;
padding: 0; padding: 0;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
......
...@@ -2,8 +2,10 @@ import { tokens } from '@uniswap/default-token-list' ...@@ -2,8 +2,10 @@ import { tokens } from '@uniswap/default-token-list'
import { SupportedChainId } from 'constants/chains' import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils' 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 { Field, swapAtom } from 'lib/state/swap'
import { useEffect, useState } from 'react' import { useEffect } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
...@@ -18,9 +20,12 @@ const UNI = (function () { ...@@ -18,9 +20,12 @@ const UNI = (function () {
})() })()
function Fixture() { function Fixture() {
const [initialized, setInitialized] = useState(false)
const setState = useUpdateAtom(swapAtom) const setState = useUpdateAtom(swapAtom)
// eslint-disable-next-line react-hooks/exhaustive-deps const {
allowedSlippage,
trade: { trade },
} = useSwapInfo()
useEffect(() => { useEffect(() => {
setState({ setState({
independentField: Field.INPUT, independentField: Field.INPUT,
...@@ -28,14 +33,18 @@ function Fixture() { ...@@ -28,14 +33,18 @@ function Fixture() {
[Field.INPUT]: ETH, [Field.INPUT]: ETH,
[Field.OUTPUT]: UNI, [Field.OUTPUT]: UNI,
}) })
setInitialized(true) }, [setState])
})
return initialized ? ( return trade ? (
<Modal color="dialog"> <Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} /> <SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
</Modal> </Modal>
) : null ) : null
} }
export default <Fixture /> export default (
<>
<SwapInfoUpdater />
<Fixture />
</>
)
import { t } from '@lingui/macro' 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 { useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { settingsAtom } from 'lib/state/settings'
import { integratorFeeAtom } from 'lib/state/swap' import { integratorFeeAtom } from 'lib/state/swap'
import { ThemedText } from 'lib/theme' import { ThemedText } from 'lib/theme'
import { useMemo } from 'react' import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId' import { currencyId } from 'utils/currencyId'
import { computeRealizedLPFeePercent } from 'utils/prices'
import Row from '../../Row' import Row from '../../Row'
...@@ -27,31 +27,42 @@ function Detail({ label, value }: DetailProps) { ...@@ -27,31 +27,42 @@ function Detail({ label, value }: DetailProps) {
} }
interface DetailsProps { interface DetailsProps {
input: Currency trade: Trade<Currency, Currency, TradeType>
output: Currency allowedSlippage: Percent
} }
export default function Details({ input, output }: DetailsProps) { export default function Details({ trade, allowedSlippage }: DetailsProps) {
const integrator = window.location.hostname 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 [integratorFee] = useAtom(integratorFeeAtom)
const priceImpact = useMemo(() => {
const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
return trade.priceImpact.subtract(realizedLpFeePercent)
}, [trade])
const details = useMemo((): [string, string][] => { 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 [ return [
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`], // [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(input)}`], [t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(inputCurrency)}`],
// [t`Price impact`, `${swap.priceImpact}%`], [t`Price impact`, `${priceImpact.toFixed(2)}%`],
// [t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`], trade.tradeType === TradeType.EXACT_INPUT
// [t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`], ? [t`Maximum sent`, `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${inputCurrency.symbol}`]
[t`Slippage tolerance`, `${maxSlippage}%`], : [],
trade.tradeType === TradeType.EXACT_OUTPUT
? [t`Minimum received`, `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${outputCurrency.symbol}`]
: [],
[t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`],
].filter(isDetail) ].filter(isDetail)
function isDetail(detail: unknown[]): detail is [string, string] { function isDetail(detail: unknown[]): detail is [string, string] {
return Boolean(detail[1]) return Boolean(detail[1])
} }
}, [input, integrator, integratorFee, maxSlippage]) }, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade])
return ( return (
<> <>
{details.map(([label, detail]) => ( {details.map(([label, detail]) => (
......
import { Trans } from '@lingui/macro' 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 { IconButton } from 'lib/components/Button'
import { useSwapInfo } from 'lib/hooks/swap'
import useScrollbar from 'lib/hooks/useScrollbar' import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando, Info } from 'lib/icons' 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 styled, { ThemedText } from 'lib/theme'
import { useState } from 'react' import { useMemo, useState } from 'react'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton from '../../ActionButton' import ActionButton from '../../ActionButton'
import Column from '../../Column' import Column from '../../Column'
...@@ -17,8 +20,6 @@ import Summary from './Summary' ...@@ -17,8 +20,6 @@ import Summary from './Summary'
export default Summary export default Summary
const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
const SummaryColumn = styled(Column)`` const SummaryColumn = styled(Column)``
const ExpandoColumn = styled(Column)`` const ExpandoColumn = styled(Column)``
const DetailsColumn = styled(Column)`` const DetailsColumn = styled(Column)``
...@@ -70,21 +71,27 @@ const Body = styled(Column)<{ open: boolean }>` ...@@ -70,21 +71,27 @@ const Body = styled(Column)<{ open: boolean }>`
} }
` `
const priceUpdate = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
interface SummaryDialogProps { interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
onConfirm: () => void onConfirm: () => void
} }
export function SummaryDialog({ onConfirm }: SummaryDialogProps) { export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
const { const { inputAmount, outputAmount } = trade
trade: { trade }, const inputCurrency = inputAmount.currency
currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount }, const outputCurrency = outputAmount.currency
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency }, const price = trade.executionPrice
} = useSwapInfo()
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 [open, setOpen] = useState(true)
const [details, setDetails] = useState<HTMLDivElement | null>(null) const [details, setDetails] = useState<HTMLDivElement | null>(null)
...@@ -119,26 +126,28 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) { ...@@ -119,26 +126,28 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<Rule /> <Rule />
<DetailsColumn> <DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}> <Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details input={inputCurrency} output={outputCurrency} /> <Details trade={trade} allowedSlippage={allowedSlippage} />
</Column> </Column>
</DetailsColumn> </DetailsColumn>
<Estimate color="secondary"> <Estimate color="secondary">
<Trans>Output is estimated.</Trans> {/* //@TODO(ianlapham): update with actual recieved values */} <Trans>Output is estimated.</Trans>
{/* {swap?.minimumReceived && ( {independentField === Field.INPUT && (
<Trans> <Trans>
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.
</Trans> </Trans>
)} )}
{swap?.maximumSent && ( {independentField === Field.OUTPUT && (
<Trans> <Trans>
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.
</Trans> </Trans>
)} */} )}
</Estimate> </Estimate>
<ActionButton <ActionButton
onClick={onConfirm} onClick={onConfirm}
onUpdate={() => confirmPrice(price)} onUpdate={() => setConfirmedTrade(trade)}
updated={price === confirmedPrice ? undefined : updated} updated={doesTradeDiffer ? priceUpdate : undefined}
> >
<Trans>Confirm swap</Trans> <Trans>Confirm swap</Trans>
</ActionButton> </ActionButton>
......
...@@ -2,35 +2,36 @@ import { Trans } from '@lingui/macro' ...@@ -2,35 +2,36 @@ import { Trans } from '@lingui/macro'
import { useSwapInfo } from 'lib/hooks/swap' import { useSwapInfo } from 'lib/hooks/swap'
import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval' import useSwapApproval, { ApprovalState, useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { Field } from 'lib/state/swap' import { Field } from 'lib/state/swap'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import ActionButton from '../ActionButton' import ActionButton from '../ActionButton'
import Dialog from '../Dialog' import Dialog from '../Dialog'
import { StatusDialog } from './Status' import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary' import { SummaryDialog } from './Summary'
enum Mode {
SWAP,
SUMMARY,
STATUS,
}
interface SwapButtonProps { interface SwapButtonProps {
disabled?: boolean disabled?: boolean
} }
export default function SwapButton({ disabled }: SwapButtonProps) { export default function SwapButton({ disabled }: SwapButtonProps) {
const [mode, setMode] = useState(Mode.SWAP)
const { const {
trade, trade,
allowedSlippage, allowedSlippage,
currencyBalances: { [Field.INPUT]: inputCurrencyBalance }, currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount }, currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>(undefined)
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// TODO(zzmp): Track pending approval // TODO(zzmp): Track pending approval
const useIsPendingApproval = () => false const useIsPendingApproval = () => false
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) const optimizedTrade = useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval)
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval) const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
// TODO(zzmp): Pass optimized trade to SummaryDialog
const actionProps = useMemo(() => { const actionProps = useMemo(() => {
if (disabled) return { disabled: true } if (disabled) return { disabled: true }
...@@ -50,24 +51,30 @@ export default function SwapButton({ disabled }: SwapButtonProps) { ...@@ -50,24 +51,30 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
} }
return { disabled: true } return { disabled: true }
}, [disabled, approval, inputCurrencyAmount, inputCurrencyBalance]) }, [approval, disabled, inputCurrencyAmount, inputCurrencyBalance])
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {
// TODO: Send the tx to the connected wallet. // TODO(zzmp): Transact the trade.
setMode(Mode.STATUS)
}, []) }, [])
return ( return (
<> <>
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={getApproval} {...actionProps}> <ActionButton
color="interactive"
onClick={() => setActiveTrade(trade.trade)}
onUpdate={getApproval}
{...actionProps}
>
<Trans>Review swap</Trans> <Trans>Review swap</Trans>
</ActionButton> </ActionButton>
{mode >= Mode.SUMMARY && ( {activeTrade && (
<Dialog color="dialog" onClose={() => setMode(Mode.SWAP)}> <Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
<SummaryDialog onConfirm={onConfirm} /> <SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog> </Dialog>
)} )}
{mode >= Mode.STATUS && ( {false && (
<Dialog color="dialog"> <Dialog color="dialog">
<StatusDialog onClose={() => setMode(Mode.SWAP)} /> <StatusDialog onClose={() => void 0} />
</Dialog> </Dialog>
)} )}
</> </>
......
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<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
): 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)
)
}
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