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:
parent
2863971640
commit
5a1ef8fb7d
@ -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 (
|
||||||
<>
|
<>
|
||||||
|
22
src/lib/components/Swap/WrapErrorText.tsx
Normal file
22
src/lib/components/Swap/WrapErrorText.tsx
Normal file
@ -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,
|
||||||
})
|
})
|
||||||
|
137
src/lib/hooks/swap/useWrapCallback.tsx
Normal file
137
src/lib/hooks/swap/useWrapCallback.tsx
Normal file
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user