feat(widgets): support wrapping native assets (#3301)

* 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
This commit is contained in:
Jordan Frankfurt 2022-03-02 13:36:35 -05:00 committed by GitHub
parent 2863971640
commit 5a1ef8fb7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 306 additions and 40 deletions

@ -61,14 +61,13 @@ export interface Action {
children: ReactNode children: ReactNode
} }
export interface ActionButtonProps { export interface BaseProps {
color?: Color color?: Color
disabled?: boolean
action?: Action 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) { export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled]) const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return ( return (

@ -48,9 +48,9 @@ export interface InputProps {
export default function Input({ disabled, focused }: InputProps) { export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui() const { i18n } = useLingui()
const { const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance }, currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount }, trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
const inputUSDC = useUSDCValue(swapInputCurrencyAmount) const inputUSDC = useUSDCValue(swapInputCurrencyAmount)

@ -41,9 +41,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const { i18n } = useLingui() const { i18n } = useLingui()
const { const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance }, currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount }, trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT) const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)

@ -1,9 +1,10 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog' import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink' import EtherscanLink from 'lib/components/EtherscanLink'
import SwapSummary from 'lib/components/Swap/Summary'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons' 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 styled, { ThemedText } from 'lib/theme'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
@ -12,7 +13,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../../ActionButton' import ActionButton from '../../ActionButton'
import Column from '../../Column' import Column from '../../Column'
import Row from '../../Row' import Row from '../../Row'
import Summary from '../Summary'
const errorMessage = ( const errorMessage = (
<Trans> <Trans>
@ -26,7 +26,9 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse; 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) const [elapsedMs, setElapsedMs] = useState(0)
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`) useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
@ -54,7 +56,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
} }
interface TransactionStatusProps { interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo> tx: PendingTransaction
onClose: () => void onClose: () => void
} }
@ -63,14 +65,24 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status ? CheckCircle : Spinner return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status]) }, [tx.receipt?.status])
const heading = useMemo(() => { const heading = useMemo(() => {
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans> if (tx.info.type === TransactionType.SWAP) {
}, [tx.receipt?.status]) 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 ( return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}> <Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}> <StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1> <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> </StatusHeader>
<TransactionRow flex> <TransactionRow flex>
<ThemedText.ButtonSmall> <ThemedText.ButtonSmall>

@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core' import { Token } from '@uniswap/sdk-core'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit' import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap' import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import useSwapApproval, { import useSwapApproval, {
ApprovalState, ApprovalState,
@ -9,6 +10,7 @@ import useSwapApproval, {
useSwapRouterAddress, useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval' } from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback' import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction } from 'lib/hooks/transactions' import { useAddTransaction } from 'lib/hooks/transactions'
import { usePendingApproval } from 'lib/hooks/transactions' import { usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
@ -40,12 +42,12 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
const { tokenColorExtraction } = useTheme() const { tokenColorExtraction } = useTheme()
const { const {
trade,
allowedSlippage, allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency }, currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance }, currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions, feeOptions,
trade,
tradeCurrencyAmounts: { [Field.INPUT]: inputTradeCurrencyAmount, [Field.OUTPUT]: outputTradeCurrencyAmount },
} = useSwapInfo() } = useSwapInfo()
const tradeType = useSwapTradeType() const tradeType = useSwapTradeType()
@ -81,8 +83,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
}) })
}, [addTransaction, getApproval]) }, [addTransaction, getApproval])
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => { const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disabled || wrapLoading) return { disabled: true }
if (!disabled && chainId) { if (!disabled && chainId) {
const hasSufficientInputForTrade =
inputTradeCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputTradeCurrencyAmount)
if (approval === ApprovalState.NOT_APPROVED) { if (approval === ApprovalState.NOT_APPROVED) {
const currency = inputCurrency || approvalCurrencyAmount?.currency const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency) invariant(currency)
@ -107,7 +114,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
children: <Trans>Approve</Trans>, children: <Trans>Approve</Trans>,
}, },
} }
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) { } else if (hasSufficientInputForTrade || (wrapType !== WrapType.NOT_APPLICABLE && !wrapError)) {
return {} return {}
} }
} }
@ -120,8 +127,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
chainId, chainId,
disabled, disabled,
inputCurrency, inputCurrency,
inputCurrencyAmount,
inputCurrencyBalance, inputCurrencyBalance,
inputTradeCurrencyAmount,
wrapError,
wrapLoading,
wrapType,
]) ])
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
@ -144,13 +154,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
swapCallback?.() swapCallback?.()
.then((response) => { .then((response) => {
setDisplayTxHash(response.hash) setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount) invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
addTransaction({ addTransaction({
response, response,
type: TransactionType.SWAP, type: TransactionType.SWAP,
tradeType, tradeType,
inputCurrencyAmount, inputCurrencyAmount: inputTradeCurrencyAmount,
outputCurrencyAmount, outputCurrencyAmount: outputTradeCurrencyAmount,
}) })
}) })
.catch((error) => { .catch((error) => {
@ -160,19 +170,54 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
.finally(() => { .finally(() => {
setActiveTrade(undefined) 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 ( return (
<> <>
<ActionButton <ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'} color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)} onClick={handleActionButtonClick}
{...actionProps} {...actionProps}
> >
<Trans>Review swap</Trans> <ButtonText />
</ActionButton> </ActionButton>
{activeTrade && ( {activeTrade && (
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}> <Dialog color="dialog" onClose={handleDialogClose}>
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} /> <SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog> </Dialog>
)} )}

@ -2,9 +2,10 @@ import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core' import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice' import useUSDCPrice from 'hooks/useUSDCPrice'
import Tooltip from 'lib/components/Tooltip' import Tooltip from 'lib/components/Tooltip'
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons' import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
import { ThemedText } from 'lib/theme' 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 { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../../Button' import { TextButton } from '../../Button'
@ -28,22 +29,38 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
export function ConnectWallet() { export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} /> return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
} }
export function UnsupportedNetwork() { export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} /> return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
} }
export function InsufficientBalance({ currency }: { currency: Currency }) { export function InsufficientBalance({ currency }: { currency: Currency }) {
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} /> return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
} }
export function InsufficientLiquidity() { export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} /> return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
} }
export function Empty() { export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} /> return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
} }
export function LoadingTrade() { export function LoadingTrade() {
return <Caption icon={Spinner} caption={<Trans>Fetching best price</Trans>} /> 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> }) { export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true) const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade const { inputAmount, outputAmount, executionPrice } = trade

@ -1,5 +1,6 @@
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains' import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap' import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { largeIconCss } from 'lib/icons' import { largeIconCss } from 'lib/icons'
import { Field } from 'lib/state/swap' import { Field } from 'lib/state/swap'
@ -25,7 +26,7 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
} = useSwapInfo() } = useSwapInfo()
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated() const isAmountPopulated = useIsAmountPopulated()
const { type: wrapType, loading: wrapLoading } = useWrapCallback()
const caption = useMemo(() => { const caption = useMemo(() => {
if (disabled) { if (disabled) {
return <Caption.ConnectWallet /> return <Caption.ConnectWallet />
@ -36,6 +37,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
} }
if (inputCurrency && outputCurrency && isAmountPopulated) { if (inputCurrency && outputCurrency && isAmountPopulated) {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
}
if (isRouteLoading) { if (isRouteLoading) {
return <Caption.LoadingTrade /> return <Caption.LoadingTrade />
} }
@ -51,7 +55,18 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
} }
return <Caption.Empty /> return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade]) }, [
balance,
chainId,
disabled,
inputCurrency,
isAmountPopulated,
isRouteLoading,
outputCurrency,
trade,
wrapLoading,
wrapType,
])
return ( return (
<> <>

@ -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
}
}

@ -10,7 +10,7 @@ import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useHasFocus from 'lib/hooks/useHasFocus' import useHasFocus from 'lib/hooks/useHasFocus'
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList' import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap' import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions' import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import Dialog from '../Dialog' import Dialog from '../Dialog'
@ -28,12 +28,18 @@ import Toolbar from './Toolbar'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE' export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined { function getTransactionFromMap(
txs: { [hash: string]: Transaction },
hash?: string
): Transaction<SwapTransactionInfo | WrapTransactionInfo> | undefined {
if (hash) { if (hash) {
const tx = txs[hash] const tx = txs[hash]
if (tx?.info?.type === TransactionType.SWAP) { if (tx?.info?.type === TransactionType.SWAP) {
return tx as Transaction<SwapTransactionInfo> return tx as Transaction<SwapTransactionInfo>
} }
if (tx?.info?.type === TransactionType.WRAP) {
return tx as Transaction<WrapTransactionInfo>
}
} }
return return
} }
@ -59,7 +65,7 @@ export default function Swap(props: SwapProps) {
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom) const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
const pendingTxs = usePendingTransactions() const pendingTxs = usePendingTransactions()
const displayTx = getSwapTx(pendingTxs, displayTxHash) const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
const tokenList = useTokenList() const tokenList = useTokenList()
const isSwapSupported = useMemo( const isSwapSupported = useMemo(

@ -7,6 +7,7 @@ import { useEffect, useMemo, useState } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React' import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter' import { getClientSideQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments' import { useRoutingAPIArguments } from './useRoutingAPIArguments'
@ -74,9 +75,13 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
error?: unknown error?: unknown
}>({ error: undefined }) }>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId]) const config = useMemo(() => getConfig(chainId), [chainId])
const { type: wrapType } = useWrapCallback()
// When arguments update, make a new call to SOR for updated quote // When arguments update, make a new call to SOR for updated quote
useEffect(() => { useEffect(() => {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return
}
setLoading(true) setLoading(true)
if (isDebouncing) return if (isDebouncing) return
@ -101,7 +106,7 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
} }
} }
} }
}, [queryArgs, params, config, isDebouncing]) }, [queryArgs, params, config, isDebouncing, wrapType])
const route = useMemo( const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), () => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),

@ -17,7 +17,7 @@ import { useBestTrade } from './useBestTrade'
interface SwapInfo { interface SwapInfo {
currencies: { [field in Field]?: Currency } currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> } currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
currencyAmounts: { [field in Field]?: CurrencyAmount<Currency> } tradeCurrencyAmounts: { [field in Field]?: CurrencyAmount<Currency> }
trade: { trade: {
trade?: InterfaceTrade<Currency, Currency, TradeType> trade?: InterfaceTrade<Currency, Currency, TradeType>
state: TradeState state: TradeState
@ -52,7 +52,7 @@ function useComputeSwapInfo(): SwapInfo {
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency]) useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
) )
const isExactIn: boolean = independentField === Field.INPUT const isExactIn = independentField === Field.INPUT
const parsedAmount = useMemo( const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), () => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount] [inputCurrency, isExactIn, outputCurrency, amount]
@ -81,7 +81,7 @@ function useComputeSwapInfo(): SwapInfo {
[relevantTokenBalances] [relevantTokenBalances]
) )
const currencyAmounts = useMemo( const tradeCurrencyAmounts = useMemo(
() => ({ () => ({
[Field.INPUT]: trade.trade?.inputAmount, [Field.INPUT]: trade.trade?.inputAmount,
[Field.OUTPUT]: trade.trade?.outputAmount, [Field.OUTPUT]: trade.trade?.outputAmount,
@ -129,21 +129,21 @@ function useComputeSwapInfo(): SwapInfo {
() => ({ () => ({
currencies, currencies,
currencyBalances, currencyBalances,
currencyAmounts,
inputError, inputError,
trade, trade,
tradeCurrencyAmounts,
allowedSlippage, allowedSlippage,
feeOptions, feeOptions,
}), }),
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions] [currencies, currencyBalances, inputError, trade, tradeCurrencyAmounts, allowedSlippage, feeOptions]
) )
} }
const swapInfoAtom = atom<SwapInfo>({ const swapInfoAtom = atom<SwapInfo>({
currencies: {}, currencies: {},
currencyBalances: {}, currencyBalances: {},
currencyAmounts: {},
trade: { state: TradeState.INVALID }, trade: { state: TradeState.INVALID },
tradeCurrencyAmounts: {},
allowedSlippage: new Percent(0), allowedSlippage: new Percent(0),
feeOptions: undefined, feeOptions: undefined,
}) })

@ -0,0 +1,137 @@
import { ContractTransaction } from '@ethersproject/contracts'
import { useWETHContract } from 'hooks/useContract'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useCallback, useEffect, useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
import useActiveWeb3React from '../useActiveWeb3React'
import { useCurrencyBalances } from '../useCurrencyBalance'
export enum WrapType {
NOT_APPLICABLE,
WRAP,
UNWRAP,
}
interface UseWrapCallbackReturns {
callback: () => Promise<ContractTransaction>
error: WrapError
loading: boolean
type: WrapType
}
export enum WrapError {
NO_ERROR = 0, // must be equal to 0 so all other errors are truthy
ENTER_NATIVE_AMOUNT,
ENTER_WRAPPED_AMOUNT,
INSUFFICIENT_NATIVE_BALANCE,
INSUFFICIENT_WRAPPED_BALANCE,
}
interface WrapState {
loading: boolean
error: WrapError
}
const wrapState = atom<WrapState>({
loading: false,
error: WrapError.NO_ERROR,
})
export default function useWrapCallback(): UseWrapCallbackReturns {
const { account, chainId } = useActiveWeb3React()
const [{ loading, error }, setWrapState] = useAtom(wrapState)
const wrappedNativeCurrencyContract = useWETHContract()
const {
amount,
independentField,
[Field.INPUT]: inputCurrency,
[Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom)
const wrapType = useMemo(() => {
if (!inputCurrency || !outputCurrency || !chainId) {
return WrapType.NOT_APPLICABLE
}
const wrappedNativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]
if (inputCurrency.isNative && wrappedNativeCurrency.equals(outputCurrency)) {
return WrapType.WRAP
}
if (wrappedNativeCurrency.equals(inputCurrency) && outputCurrency.isNative) {
return WrapType.UNWRAP
}
return WrapType.NOT_APPLICABLE
}, [chainId, inputCurrency, outputCurrency])
const isExactIn = independentField === Field.INPUT
const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount]
)
const parsedAmountIn = isExactIn ? parsedAmount : undefined
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const hasInputAmount = Boolean(parsedAmount?.greaterThan('0'))
const sufficientBalance = parsedAmountIn && !currencyBalances[Field.INPUT]?.lessThan(parsedAmountIn)
useEffect(() => {
if (sufficientBalance) {
setWrapState((state) => ({ ...state, error: WrapError.NO_ERROR }))
} else if (wrapType === WrapType.WRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_NATIVE_BALANCE : WrapError.ENTER_NATIVE_AMOUNT,
}))
} else if (wrapType === WrapType.UNWRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_WRAPPED_BALANCE : WrapError.ENTER_WRAPPED_AMOUNT,
}))
}
}, [hasInputAmount, setWrapState, sufficientBalance, wrapType])
const callback = useCallback(async () => {
if (!parsedAmountIn) {
return Promise.reject('Must provide an input amount to wrap.')
}
if (wrapType === WrapType.NOT_APPLICABLE) {
return Promise.reject('Wrapping not applicable to this asset.')
}
if (!sufficientBalance) {
return Promise.reject('Insufficient balance to wrap desired amount.')
}
if (!wrappedNativeCurrencyContract) {
return Promise.reject('Wrap contract not found.')
}
setWrapState((state) => ({ ...state, loading: true }))
const result = await (wrapType === WrapType.WRAP
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`))
// resolve loading state after one confirmation
result.wait(1).finally(() => setWrapState((state) => ({ ...state, loading: false })))
return Promise.resolve(result)
}, [wrappedNativeCurrencyContract, sufficientBalance, parsedAmountIn, wrapType, setWrapState])
return useMemo(
() => ({
callback,
error,
loading,
type: wrapType,
}),
[callback, error, loading, wrapType]
)
}

@ -5,6 +5,7 @@ import { atomWithImmer } from 'jotai/immer'
export enum TransactionType { export enum TransactionType {
APPROVAL, APPROVAL,
SWAP, SWAP,
WRAP,
} }
interface BaseTransactionInfo { interface BaseTransactionInfo {
@ -37,7 +38,14 @@ export interface OutputSwapTransactionInfo extends SwapTransactionInfo {
maximumInputCurrencyAmount: string maximumInputCurrencyAmount: string
} }
export type TransactionInfo = ApprovalTransactionInfo | SwapTransactionInfo export interface WrapTransactionInfo extends BaseTransactionInfo {
type: TransactionType.WRAP
unwrapped: boolean
currencyAmountRaw: string
chainId?: number
}
export type TransactionInfo = ApprovalTransactionInfo | SwapTransactionInfo | WrapTransactionInfo
export interface Transaction<T extends TransactionInfo = TransactionInfo> { export interface Transaction<T extends TransactionInfo = TransactionInfo> {
addedTime: number addedTime: number