Skip to content

Commit

Permalink
feat(widgets): support wrapping native assets (Uniswap#3301)
Browse files Browse the repository at this point in the history
* feat(widgets): support wrapping native assets

* integrate wrap with swapInfo, start a useWrapCallback hook

* add loading state

* add pending state to (un)wrap transactions

* final cleanup

* janky merge conflict fix--disregard! this will change

* fixed

* 💢

* pr feedback

* z's pr feedback

* pr feedback

* zzmp pr feedback

* zzmp pr feedback
  • Loading branch information
JFrankfurt authored Mar 2, 2022
1 parent 2863971 commit 5a1ef8f
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 40 deletions.
7 changes: 3 additions & 4 deletions src/lib/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,13 @@ export interface Action {
children: ReactNode
}

export interface ActionButtonProps {
export interface BaseProps {
color?: Color
disabled?: boolean
action?: Action
onClick: () => void
children: ReactNode
}

export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>

export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/Swap/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export interface InputProps {
export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui()
const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)

Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/Swap/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const { i18n } = useLingui()

const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo()

const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
Expand Down
26 changes: 19 additions & 7 deletions src/lib/components/Swap/Status/StatusDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink'
import SwapSummary from 'lib/components/Swap/Summary'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import ms from 'ms.macro'
import { useCallback, useMemo, useState } from 'react'
Expand All @@ -12,7 +13,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
import Row from '../../Row'
import Summary from '../Summary'

const errorMessage = (
<Trans>
Expand All @@ -26,7 +26,9 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`

function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>

function ElapsedTime({ tx }: { tx: PendingTransaction }) {
const [elapsedMs, setElapsedMs] = useState(0)

useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
Expand Down Expand Up @@ -54,7 +56,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
}

interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
tx: PendingTransaction
onClose: () => void
}

Expand All @@ -63,14 +65,24 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status])
const heading = useMemo(() => {
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
if (tx.info.type === TransactionType.SWAP) {
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
} else if (tx.info.type === TransactionType.WRAP) {
if (tx.info.unwrapped) {
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
}, [tx.info, tx.receipt?.status])

return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
{tx.info.type === TransactionType.SWAP ? (
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
) : null}
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
Expand Down
67 changes: 56 additions & 11 deletions src/lib/components/Swap/SwapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils'
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import useSwapApproval, {
ApprovalState,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction } from 'lib/hooks/transactions'
import { usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
Expand Down Expand Up @@ -40,12 +42,12 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
const { tokenColorExtraction } = useTheme()

const {
trade,
allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions,
trade,
tradeCurrencyAmounts: { [Field.INPUT]: inputTradeCurrencyAmount, [Field.OUTPUT]: outputTradeCurrencyAmount },
} = useSwapInfo()

const tradeType = useSwapTradeType()
Expand Down Expand Up @@ -81,8 +83,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
})
}, [addTransaction, getApproval])

const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()

const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disabled || wrapLoading) return { disabled: true }
if (!disabled && chainId) {
const hasSufficientInputForTrade =
inputTradeCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputTradeCurrencyAmount)
if (approval === ApprovalState.NOT_APPROVED) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
Expand All @@ -107,7 +114,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
children: <Trans>Approve</Trans>,
},
}
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
} else if (hasSufficientInputForTrade || (wrapType !== WrapType.NOT_APPLICABLE && !wrapError)) {
return {}
}
}
Expand All @@ -120,8 +127,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
chainId,
disabled,
inputCurrency,
inputCurrencyAmount,
inputCurrencyBalance,
inputTradeCurrencyAmount,
wrapError,
wrapLoading,
wrapType,
])

const deadline = useTransactionDeadline()
Expand All @@ -144,13 +154,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount)
invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType,
inputCurrencyAmount,
outputCurrencyAmount,
inputCurrencyAmount: inputTradeCurrencyAmount,
outputCurrencyAmount: outputTradeCurrencyAmount,
})
})
.catch((error) => {
Expand All @@ -160,19 +170,54 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])

const ButtonText = useCallback(() => {
if (wrapError !== WrapError.NO_ERROR) {
return <WrapErrorText wrapError={wrapError} />
}
switch (wrapType) {
case WrapType.UNWRAP:
return <Trans>Unwrap</Trans>
case WrapType.WRAP:
return <Trans>Wrap</Trans>
case WrapType.NOT_APPLICABLE:
default:
return <Trans>Review swap</Trans>
}
}, [wrapError, wrapType])

const handleDialogClose = useCallback(() => {
setActiveTrade(undefined)
}, [])

const handleActionButtonClick = useCallback(async () => {
if (wrapType === WrapType.NOT_APPLICABLE) {
setActiveTrade(trade.trade)
} else {
const transaction = await wrapCallback()
addTransaction({
response: transaction,
type: TransactionType.WRAP,
unwrapped: wrapType === WrapType.UNWRAP,
currencyAmountRaw: transaction.value?.toString() ?? '0',
chainId,
})
setDisplayTxHash(transaction.hash)
}
}, [addTransaction, chainId, setDisplayTxHash, trade.trade, wrapCallback, wrapType])

return (
<>
<ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)}
onClick={handleActionButtonClick}
{...actionProps}
>
<Trans>Review swap</Trans>
<ButtonText />
</ActionButton>
{activeTrade && (
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
<Dialog color="dialog" onClose={handleDialogClose}>
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog>
)}
Expand Down
19 changes: 18 additions & 1 deletion src/lib/components/Swap/Toolbar/Caption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice'
import Tooltip from 'lib/components/Tooltip'
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { ReactNode, useMemo, useState } from 'react'
import { ReactNode, useCallback, useMemo, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'

import { TextButton } from '../../Button'
Expand All @@ -28,22 +29,38 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
}

export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
}

export function InsufficientBalance({ currency }: { currency: Currency }) {
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
}

export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}

export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}

export function LoadingTrade() {
return <Caption icon={Spinner} caption={<Trans>Fetching best price…</Trans>} />
}

export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) {
const WrapText = useCallback(() => {
if (wrapType === WrapType.WRAP) {
return loading ? <Trans>Wrapping native currency.</Trans> : <Trans>Wrap native currency.</Trans>
}
return loading ? <Trans>Unwrapping native currency.</Trans> : <Trans>Unwrap native currency.</Trans>
}, [loading, wrapType])

return <Caption icon={Info} caption={<WrapText />} />
}

export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade
Expand Down
19 changes: 17 additions & 2 deletions src/lib/components/Swap/Toolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { largeIconCss } from 'lib/icons'
import { Field } from 'lib/state/swap'
Expand All @@ -25,7 +26,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
} = useSwapInfo()
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()

const { type: wrapType, loading: wrapLoading } = useWrapCallback()
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
Expand All @@ -36,6 +37,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}

if (inputCurrency && outputCurrency && isAmountPopulated) {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
}
if (isRouteLoading) {
return <Caption.LoadingTrade />
}
Expand All @@ -51,7 +55,18 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}

return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
}, [
balance,
chainId,
disabled,
inputCurrency,
isAmountPopulated,
isRouteLoading,
outputCurrency,
trade,
wrapLoading,
wrapType,
])

return (
<>
Expand Down
22 changes: 22 additions & 0 deletions src/lib/components/Swap/WrapErrorText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Trans } from '@lingui/macro'
import { WrapError } from 'lib/hooks/swap/useWrapCallback'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'

export function WrapErrorText({ wrapError }: { wrapError: WrapError }) {
const native = useNativeCurrency()
const wrapped = native?.wrapped

switch (wrapError) {
case WrapError.ENTER_NATIVE_AMOUNT:
return <Trans>Enter {native?.symbol} amount</Trans>
case WrapError.ENTER_WRAPPED_AMOUNT:
return <Trans>Enter {wrapped?.symbol} amount</Trans>
case WrapError.INSUFFICIENT_NATIVE_BALANCE:
return <Trans>Insufficient {native?.symbol} balance</Trans>
case WrapError.INSUFFICIENT_WRAPPED_BALANCE:
return <Trans>Insufficient {wrapped?.symbol} balance</Trans>
case WrapError.NO_ERROR:
default:
return null
}
}
Loading

0 comments on commit 5a1ef8f

Please sign in to comment.