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 <ianlapham@gmail.com>
This commit is contained in:
parent
ffe2bd315e
commit
ffe334ccbf
@ -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<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({
|
||||
trade,
|
||||
originalTrade,
|
||||
|
@ -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;
|
||||
|
@ -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 ? (
|
||||
<Modal color="dialog">
|
||||
<SummaryDialog onConfirm={() => void 0} />
|
||||
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Modal>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default <Fixture />
|
||||
export default (
|
||||
<>
|
||||
<SwapInfoUpdater />
|
||||
<Fixture />
|
||||
</>
|
||||
)
|
||||
|
@ -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<Currency, Currency, TradeType>
|
||||
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]) => (
|
||||
|
@ -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: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
|
||||
|
||||
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: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
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()
|
||||
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const price = trade.executionPrice
|
||||
|
||||
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<HTMLDivElement | null>(null)
|
||||
@ -119,26 +126,28 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
|
||||
<Rule />
|
||||
<DetailsColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<Details input={inputCurrency} output={outputCurrency} />
|
||||
<Details trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Column>
|
||||
</DetailsColumn>
|
||||
<Estimate color="secondary">
|
||||
<Trans>Output is estimated.</Trans> {/* //@TODO(ianlapham): update with actual recieved values */}
|
||||
{/* {swap?.minimumReceived && (
|
||||
<Trans>Output is estimated.</Trans>
|
||||
{independentField === Field.INPUT && (
|
||||
<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>
|
||||
)}
|
||||
{swap?.maximumSent && (
|
||||
{independentField === Field.OUTPUT && (
|
||||
<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>
|
||||
)} */}
|
||||
)}
|
||||
</Estimate>
|
||||
<ActionButton
|
||||
onClick={onConfirm}
|
||||
onUpdate={() => confirmPrice(price)}
|
||||
updated={price === confirmedPrice ? undefined : updated}
|
||||
onUpdate={() => setConfirmedTrade(trade)}
|
||||
updated={doesTradeDiffer ? priceUpdate : undefined}
|
||||
>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
|
@ -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<typeof trade.trade | undefined>(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 (
|
||||
<>
|
||||
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={getApproval} {...actionProps}>
|
||||
<ActionButton
|
||||
color="interactive"
|
||||
onClick={() => setActiveTrade(trade.trade)}
|
||||
onUpdate={getApproval}
|
||||
{...actionProps}
|
||||
>
|
||||
<Trans>Review swap</Trans>
|
||||
</ActionButton>
|
||||
{mode >= Mode.SUMMARY && (
|
||||
<Dialog color="dialog" onClose={() => setMode(Mode.SWAP)}>
|
||||
<SummaryDialog onConfirm={onConfirm} />
|
||||
{activeTrade && (
|
||||
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
|
||||
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
|
||||
</Dialog>
|
||||
)}
|
||||
{mode >= Mode.STATUS && (
|
||||
{false && (
|
||||
<Dialog color="dialog">
|
||||
<StatusDialog onClose={() => setMode(Mode.SWAP)} />
|
||||
<StatusDialog onClose={() => void 0} />
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
|
19
src/utils/tradeMeaningFullyDiffer.ts
Normal file
19
src/utils/tradeMeaningFullyDiffer.ts
Normal file
@ -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<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)
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user