From 5a1ef8fb7d031137cd9fe30fdeb5fa71aae0e775 Mon Sep 17 00:00:00 2001 From: Jordan Frankfurt Date: Wed, 2 Mar 2022 13:36:35 -0500 Subject: [PATCH] feat(widgets): support wrapping native assets (#3301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/lib/components/ActionButton.tsx | 7 +- src/lib/components/Swap/Input.tsx | 4 +- src/lib/components/Swap/Output.tsx | 4 +- .../components/Swap/Status/StatusDialog.tsx | 26 +++- src/lib/components/Swap/SwapButton.tsx | 67 +++++++-- src/lib/components/Swap/Toolbar/Caption.tsx | 19 ++- src/lib/components/Swap/Toolbar/index.tsx | 19 ++- src/lib/components/Swap/WrapErrorText.tsx | 22 +++ src/lib/components/Swap/index.tsx | 12 +- .../useClientSideSmartOrderRouterTrade.ts | 7 +- src/lib/hooks/swap/useSwapInfo.tsx | 12 +- src/lib/hooks/swap/useWrapCallback.tsx | 137 ++++++++++++++++++ src/lib/state/transactions.ts | 10 +- 13 files changed, 306 insertions(+), 40 deletions(-) create mode 100644 src/lib/components/Swap/WrapErrorText.tsx create mode 100644 src/lib/hooks/swap/useWrapCallback.tsx diff --git a/src/lib/components/ActionButton.tsx b/src/lib/components/ActionButton.tsx index a9f26f1eb5..6846c298b4 100644 --- a/src/lib/components/ActionButton.tsx +++ b/src/lib/components/ActionButton.tsx @@ -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, keyof BaseProps> + export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) { const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled]) return ( diff --git a/src/lib/components/Swap/Input.tsx b/src/lib/components/Swap/Input.tsx index bf9488deb1..d38afc788b 100644 --- a/src/lib/components/Swap/Input.tsx +++ b/src/lib/components/Swap/Input.tsx @@ -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) diff --git a/src/lib/components/Swap/Output.tsx b/src/lib/components/Swap/Output.tsx index 27c1535c1a..dd7120e2dd 100644 --- a/src/lib/components/Swap/Output.tsx +++ b/src/lib/components/Swap/Output.tsx @@ -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) diff --git a/src/lib/components/Swap/Status/StatusDialog.tsx b/src/lib/components/Swap/Status/StatusDialog.tsx index 4ddb6c86cb..cdfbd8579a 100644 --- a/src/lib/components/Swap/Status/StatusDialog.tsx +++ b/src/lib/components/Swap/Status/StatusDialog.tsx @@ -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' @@ -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 = ( @@ -26,7 +26,9 @@ const TransactionRow = styled(Row)` flex-direction: row-reverse; ` -function ElapsedTime({ tx }: { tx: Transaction }) { +type PendingTransaction = Transaction + +function ElapsedTime({ tx }: { tx: PendingTransaction }) { const [elapsedMs, setElapsedMs] = useState(0) useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`) @@ -54,7 +56,7 @@ function ElapsedTime({ tx }: { tx: Transaction }) { } interface TransactionStatusProps { - tx: Transaction + tx: PendingTransaction onClose: () => void } @@ -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 ? Transaction submitted : Transaction pending - }, [tx.receipt?.status]) + if (tx.info.type === TransactionType.SWAP) { + return tx.receipt?.status ? Swap confirmed : Swap pending + } else if (tx.info.type === TransactionType.WRAP) { + if (tx.info.unwrapped) { + return tx.receipt?.status ? Unwrap confirmed : Unwrap pending + } + return tx.receipt?.status ? Wrap confirmed : Wrap pending + } + return tx.receipt?.status ? Transaction confirmed : Transaction pending + }, [tx.info, tx.receipt?.status]) return ( {heading} - + {tx.info.type === TransactionType.SWAP ? ( + + ) : null} diff --git a/src/lib/components/Swap/SwapButton.tsx b/src/lib/components/Swap/SwapButton.tsx index b039caaa48..8f2886f908 100644 --- a/src/lib/components/Swap/SwapButton.tsx +++ b/src/lib/components/Swap/SwapButton.tsx @@ -2,6 +2,7 @@ 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, @@ -9,6 +10,7 @@ import useSwapApproval, { 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' @@ -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() @@ -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 | 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) @@ -107,7 +114,7 @@ export default function SwapButton({ disabled }: SwapButtonProps) { children: Approve, }, } - } else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) { + } else if (hasSufficientInputForTrade || (wrapType !== WrapType.NOT_APPLICABLE && !wrapError)) { return {} } } @@ -120,8 +127,11 @@ export default function SwapButton({ disabled }: SwapButtonProps) { chainId, disabled, inputCurrency, - inputCurrencyAmount, inputCurrencyBalance, + inputTradeCurrencyAmount, + wrapError, + wrapLoading, + wrapType, ]) const deadline = useTransactionDeadline() @@ -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) => { @@ -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 + } + switch (wrapType) { + case WrapType.UNWRAP: + return Unwrap + case WrapType.WRAP: + return Wrap + case WrapType.NOT_APPLICABLE: + default: + return Review swap + } + }, [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 ( <> setActiveTrade(trade.trade)} + onClick={handleActionButtonClick} {...actionProps} > - Review swap + {activeTrade && ( - setActiveTrade(undefined)}> + )} diff --git a/src/lib/components/Swap/Toolbar/Caption.tsx b/src/lib/components/Swap/Toolbar/Caption.tsx index 2dee635f84..6e93601e7a 100644 --- a/src/lib/components/Swap/Toolbar/Caption.tsx +++ b/src/lib/components/Swap/Toolbar/Caption.tsx @@ -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' @@ -28,22 +29,38 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) { export function ConnectWallet() { return Connect wallet to swap} /> } + export function UnsupportedNetwork() { return Unsupported network - switch to another to trade.} /> } + export function InsufficientBalance({ currency }: { currency: Currency }) { return Insufficient {currency?.symbol} balance} /> } + export function InsufficientLiquidity() { return Insufficient liquidity in the pool for your trade} /> } + export function Empty() { return Enter an amount} /> } + export function LoadingTrade() { return Fetching best price…} /> } +export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) { + const WrapText = useCallback(() => { + if (wrapType === WrapType.WRAP) { + return loading ? Wrapping native currency. : Wrap native currency. + } + return loading ? Unwrapping native currency. : Unwrap native currency. + }, [loading, wrapType]) + + return } /> +} + export function Trade({ trade }: { trade: InterfaceTrade }) { const [flip, setFlip] = useState(true) const { inputAmount, outputAmount, executionPrice } = trade diff --git a/src/lib/components/Swap/Toolbar/index.tsx b/src/lib/components/Swap/Toolbar/index.tsx index 0df5b84abd..4bb964052f 100644 --- a/src/lib/components/Swap/Toolbar/index.tsx +++ b/src/lib/components/Swap/Toolbar/index.tsx @@ -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' @@ -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 @@ -36,6 +37,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { } if (inputCurrency && outputCurrency && isAmountPopulated) { + if (wrapType !== WrapType.NOT_APPLICABLE) { + return + } if (isRouteLoading) { return } @@ -51,7 +55,18 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) { } return - }, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade]) + }, [ + balance, + chainId, + disabled, + inputCurrency, + isAmountPopulated, + isRouteLoading, + outputCurrency, + trade, + wrapLoading, + wrapType, + ]) return ( <> diff --git a/src/lib/components/Swap/WrapErrorText.tsx b/src/lib/components/Swap/WrapErrorText.tsx new file mode 100644 index 0000000000..07e38825b1 --- /dev/null +++ b/src/lib/components/Swap/WrapErrorText.tsx @@ -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 Enter {native?.symbol} amount + case WrapError.ENTER_WRAPPED_AMOUNT: + return Enter {wrapped?.symbol} amount + case WrapError.INSUFFICIENT_NATIVE_BALANCE: + return Insufficient {native?.symbol} balance + case WrapError.INSUFFICIENT_WRAPPED_BALANCE: + return Insufficient {wrapped?.symbol} balance + case WrapError.NO_ERROR: + default: + return null + } +} diff --git a/src/lib/components/Swap/index.tsx b/src/lib/components/Swap/index.tsx index f179647837..2fb15b43af 100644 --- a/src/lib/components/Swap/index.tsx +++ b/src/lib/components/Swap/index.tsx @@ -10,7 +10,7 @@ import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useHasFocus from 'lib/hooks/useHasFocus' import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList' 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 Dialog from '../Dialog' @@ -28,12 +28,18 @@ import Toolbar from './Toolbar' export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE' -function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction | undefined { +function getTransactionFromMap( + txs: { [hash: string]: Transaction }, + hash?: string +): Transaction | undefined { if (hash) { const tx = txs[hash] if (tx?.info?.type === TransactionType.SWAP) { return tx as Transaction } + if (tx?.info?.type === TransactionType.WRAP) { + return tx as Transaction + } } return } @@ -59,7 +65,7 @@ export default function Swap(props: SwapProps) { const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom) const pendingTxs = usePendingTransactions() - const displayTx = getSwapTx(pendingTxs, displayTxHash) + const displayTx = getTransactionFromMap(pendingTxs, displayTxHash) const tokenList = useTokenList() const isSwapSupported = useMemo( diff --git a/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts index 61e05128fb..aea5406936 100644 --- a/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts +++ b/src/lib/hooks/routing/useClientSideSmartOrderRouterTrade.ts @@ -7,6 +7,7 @@ import { useEffect, useMemo, useState } from 'react' import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types' import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils' +import useWrapCallback, { WrapType } from '../swap/useWrapCallback' import useActiveWeb3React from '../useActiveWeb3React' import { getClientSideQuote } from './clientSideSmartOrderRouter' import { useRoutingAPIArguments } from './useRoutingAPIArguments' @@ -74,9 +75,13 @@ export default function useClientSideSmartOrderRouterTrade({ error: undefined }) const config = useMemo(() => getConfig(chainId), [chainId]) + const { type: wrapType } = useWrapCallback() // When arguments update, make a new call to SOR for updated quote useEffect(() => { + if (wrapType !== WrapType.NOT_APPLICABLE) { + return + } setLoading(true) if (isDebouncing) return @@ -101,7 +106,7 @@ export default function useClientSideSmartOrderRouterTrade computeRoutes(currencyIn, currencyOut, tradeType, quoteResult), diff --git a/src/lib/hooks/swap/useSwapInfo.tsx b/src/lib/hooks/swap/useSwapInfo.tsx index add5e85921..289101daf7 100644 --- a/src/lib/hooks/swap/useSwapInfo.tsx +++ b/src/lib/hooks/swap/useSwapInfo.tsx @@ -17,7 +17,7 @@ import { useBestTrade } from './useBestTrade' interface SwapInfo { currencies: { [field in Field]?: Currency } currencyBalances: { [field in Field]?: CurrencyAmount } - currencyAmounts: { [field in Field]?: CurrencyAmount } + tradeCurrencyAmounts: { [field in Field]?: CurrencyAmount } trade: { trade?: InterfaceTrade state: TradeState @@ -52,7 +52,7 @@ function useComputeSwapInfo(): SwapInfo { useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency]) ) - const isExactIn: boolean = independentField === Field.INPUT + const isExactIn = independentField === Field.INPUT const parsedAmount = useMemo( () => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined), [inputCurrency, isExactIn, outputCurrency, amount] @@ -81,7 +81,7 @@ function useComputeSwapInfo(): SwapInfo { [relevantTokenBalances] ) - const currencyAmounts = useMemo( + const tradeCurrencyAmounts = useMemo( () => ({ [Field.INPUT]: trade.trade?.inputAmount, [Field.OUTPUT]: trade.trade?.outputAmount, @@ -129,21 +129,21 @@ function useComputeSwapInfo(): SwapInfo { () => ({ currencies, currencyBalances, - currencyAmounts, inputError, trade, + tradeCurrencyAmounts, allowedSlippage, feeOptions, }), - [currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions] + [currencies, currencyBalances, inputError, trade, tradeCurrencyAmounts, allowedSlippage, feeOptions] ) } const swapInfoAtom = atom({ currencies: {}, currencyBalances: {}, - currencyAmounts: {}, trade: { state: TradeState.INVALID }, + tradeCurrencyAmounts: {}, allowedSlippage: new Percent(0), feeOptions: undefined, }) diff --git a/src/lib/hooks/swap/useWrapCallback.tsx b/src/lib/hooks/swap/useWrapCallback.tsx new file mode 100644 index 0000000000..d5e5784430 --- /dev/null +++ b/src/lib/hooks/swap/useWrapCallback.tsx @@ -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 + 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({ + 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] + ) +} diff --git a/src/lib/state/transactions.ts b/src/lib/state/transactions.ts index cc3a1089b6..9a0fdb9eab 100644 --- a/src/lib/state/transactions.ts +++ b/src/lib/state/transactions.ts @@ -5,6 +5,7 @@ import { atomWithImmer } from 'jotai/immer' export enum TransactionType { APPROVAL, SWAP, + WRAP, } interface BaseTransactionInfo { @@ -37,7 +38,14 @@ export interface OutputSwapTransactionInfo extends SwapTransactionInfo { 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 { addedTime: number