Commit bdcf761d authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

chore: refactor swap button for maintainability (#3579)

* chore: mv SwapButton to dir

* chore: mv approval data to its own hook

* chore: mv approval actions to approvals hook

* chore: simplify SwapButton logic

* fix: pass through approval amount

* fix: mv error handling to consumer
parent e876267d
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap' import { useSwapInfo } from 'lib/hooks/swap'
import { import { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback' import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback' import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions' import { useAddTransaction } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React' import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock' import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline' import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons'
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap' import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions' import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme' import { useTheme } from 'lib/theme'
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton, { ActionButtonProps } from '../ActionButton' import ActionButton, { ActionButtonProps } from '../../ActionButton'
import Dialog from '../Dialog' import Dialog from '../../Dialog'
import EtherscanLink from '../EtherscanLink' import { SummaryDialog } from '../Summary'
import { SummaryDialog } from './Summary' import useApprovalData, { useIsPendingApproval } from './useApprovalData'
interface SwapButtonProps { interface SwapButtonProps {
disabled?: boolean disabled?: boolean
} }
function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default memo(function SwapButton({ disabled }: SwapButtonProps) { export default memo(function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const { tokenColorExtraction } = useTheme()
const { const {
[Field.INPUT]: { [Field.INPUT]: {
currency: inputCurrency, currency: inputCurrency,
...@@ -47,118 +32,21 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -47,118 +32,21 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
balance: inputCurrencyBalance, balance: inputCurrencyBalance,
usdc: inputUSDC, usdc: inputUSDC,
}, },
[Field.OUTPUT]: { amount: outputCurrencyAmount, usdc: outputUSDC }, [Field.OUTPUT]: { usdc: outputUSDC },
trade, trade,
slippage, slippage,
impact, impact,
} = useSwapInfo() } = useSwapInfo()
const feeOptions = useAtomValue(feeOptionsAtom) const feeOptions = useAtomValue(feeOptionsAtom)
const tradeType = useSwapTradeType()
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// clear active trade on chain change
useEffect(() => {
setActiveTrade(undefined)
}, [chainId])
// TODO(zzmp): Return an optimized trade directly from useSwapInfo. // TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade = const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending. // Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
optimizedTrade,
slippage.allowed,
useIsPendingApproval,
approvalCurrencyAmount
)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const onApprove = useCallback(async () => {
const transaction = await handleApproveOrPermit()
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
}, [addTransaction, handleApproveOrPermit])
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
const disableSwap = useMemo(
() =>
disabled ||
(wrapType === WrapType.NONE && !optimizedTrade) ||
!chainId ||
!(inputCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disableSwap) return { disabled: true }
if (
wrapType === WrapType.NONE &&
(approvalState === ApproveOrPermitState.REQUIRES_APPROVAL ||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE)
) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? (
<Trans>Allow {currency.symbol} first</Trans>
) : (
<Trans>Approve {currency.symbol} first</Trans>
),
onClick: onApprove,
children:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
},
}
}
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
children: <Trans>Approve</Trans>,
},
}
}
if (approvalState === ApproveOrPermitState.PENDING_SIGNATURE) {
return {
disabled: true,
action: {
message: <Trans>Allowance pending</Trans>,
icon: Spinner,
children: <Trans>Allow</Trans>,
},
}
}
return {}
}, [approvalCurrencyAmount?.currency, approvalHash, approvalState, disableSwap, inputCurrency, onApprove, wrapType])
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
// the callback to execute the swap const { type: wrapType, callback: wrapCallback } = useWrapCallback()
const { approvalData, signatureData } = useApprovalData(optimizedTrade, slippage, inputCurrencyAmount)
const { callback: swapCallback } = useSwapCallback({ const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade, trade: optimizedTrade,
allowedSlippage: slippage.allowed, allowedSlippage: slippage.allowed,
...@@ -168,47 +56,78 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -168,47 +56,78 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
feeOptions, feeOptions,
}) })
//@TODO(ianlapham): add a loading state, process errors const [open, setOpen] = useState(false)
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom) // Close the review modal if there is no available trade.
useEffect(() => setOpen((open) => (trade.trade ? open : false)), [trade.trade])
// Close the review modal on chain change.
useEffect(() => setOpen(false), [chainId])
const addTransaction = useAddTransaction()
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const setOldestValidBlock = useSetOldestValidBlock() const setOldestValidBlock = useSetOldestValidBlock()
const onConfirm = useCallback(() => {
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType,
inputCurrencyAmount,
outputCurrencyAmount,
})
// Set the block containing the response to the oldest valid block to ensure that the const onWrap = useCallback(async () => {
// completed trade's impact is reflected in future fetched trades. try {
response.wait(1).then((receipt) => { const transaction = await wrapCallback?.()
setOldestValidBlock(receipt.blockNumber) if (!transaction) return
}) addTransaction({
response: transaction,
type: TransactionType.WRAP,
unwrapped: wrapType === WrapType.UNWRAP,
currencyAmountRaw: transaction.value?.toString() ?? '0',
chainId,
}) })
.catch((error) => { setDisplayTxHash(transaction.hash)
//@TODO(ianlapham): add error handling } catch (e) {
console.log(error) // TODO(zzmp): Surface errors from wrap.
console.log(e)
}
}, [addTransaction, chainId, setDisplayTxHash, wrapCallback, wrapType])
const onSwap = useCallback(async () => {
try {
const transaction = await swapCallback?.()
if (!transaction) return
invariant(trade.trade)
addTransaction({
response: transaction,
type: TransactionType.SWAP,
tradeType: trade.trade.tradeType,
inputCurrencyAmount: trade.trade.inputAmount,
outputCurrencyAmount: trade.trade.outputAmount,
}) })
.finally(() => { setDisplayTxHash(transaction.hash)
setActiveTrade(undefined) setOpen(false)
// Set the block containing the response to the oldest valid block to ensure that the
// completed trade's impact is reflected in future fetched trades.
transaction.wait(1).then((receipt) => {
setOldestValidBlock(receipt.blockNumber)
}) })
}, [ } catch (e) {
addTransaction, // TODO(zzmp): Surface errors from swap.
inputCurrencyAmount, console.log(e)
outputCurrencyAmount, }
setDisplayTxHash, }, [addTransaction, setDisplayTxHash, setOldestValidBlock, swapCallback, trade.trade])
setOldestValidBlock,
swapCallback,
tradeType,
])
const ButtonText = useCallback(() => { const disableSwap = useMemo(
() =>
disabled ||
!chainId ||
(wrapType === WrapType.NONE && !optimizedTrade) ||
!(inputCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disableSwap) {
return { disabled: true }
} else if (wrapType === WrapType.NONE) {
return approvalData || { onClick: () => setOpen(true) }
} else {
return { onClick: onWrap }
}
}, [approvalData, disableSwap, onWrap, wrapType])
const Label = useCallback(() => {
switch (wrapType) { switch (wrapType) {
case WrapType.UNWRAP: case WrapType.UNWRAP:
return <Trans>Unwrap {inputCurrency?.symbol}</Trans> return <Trans>Unwrap {inputCurrency?.symbol}</Trans>
...@@ -219,47 +138,23 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) { ...@@ -219,47 +138,23 @@ export default memo(function SwapButton({ disabled }: SwapButtonProps) {
return <Trans>Review swap</Trans> return <Trans>Review swap</Trans>
} }
}, [inputCurrency?.symbol, wrapType]) }, [inputCurrency?.symbol, wrapType])
const onClose = useCallback(() => setOpen(false), [])
const handleDialogClose = useCallback(() => { const { tokenColorExtraction } = useTheme()
setActiveTrade(undefined)
}, [])
const handleActionButtonClick = useCallback(async () => {
if (wrapType === WrapType.NONE) {
setActiveTrade(trade.trade)
} else {
const transaction = await wrapCallback()
if (!transaction) return
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'} {...actionProps}>
color={tokenColorExtraction ? 'interactive' : 'accent'} <Label />
onClick={handleActionButtonClick}
{...actionProps}
>
<ButtonText />
</ActionButton> </ActionButton>
{activeTrade && ( {open && trade.trade && (
<Dialog color="dialog" onClose={handleDialogClose}> <Dialog color="dialog" onClose={onClose}>
<SummaryDialog <SummaryDialog
trade={activeTrade} trade={trade.trade}
slippage={slippage} slippage={slippage}
inputUSDC={inputUSDC} inputUSDC={inputUSDC}
outputUSDC={outputUSDC} outputUSDC={outputUSDC}
impact={impact} impact={impact}
onConfirm={onConfirm} onConfirm={onSwap}
/> />
</Dialog> </Dialog>
)} )}
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { ActionButtonProps } from 'lib/components/ActionButton'
import EtherscanLink from 'lib/components/EtherscanLink'
import {
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import { Slippage } from 'lib/hooks/useSlippage'
import { Spinner } from 'lib/icons'
import { TransactionType } from 'lib/state/transactions'
import { useCallback, useMemo } from 'react'
import { ExplorerDataType } from 'utils/getExplorerLink'
export function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default function useApprovalData(
trade: ReturnType<typeof useSwapApprovalOptimizedTrade>,
slippage: Slippage,
currencyAmount?: CurrencyAmount<Currency>
) {
const currency = currencyAmount?.currency
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
trade,
slippage.allowed,
useIsPendingApproval,
currencyAmount
)
const addTransaction = useAddTransaction()
const onApprove = useCallback(async () => {
const transaction = await handleApproveOrPermit()
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
}, [addTransaction, handleApproveOrPermit])
const approvalHash = usePendingApproval(currency?.isToken ? currency : undefined, useSwapRouterAddress(trade))
const approvalData = useMemo((): Partial<ActionButtonProps> | undefined => {
if (!trade || !currency) return
if (approvalState === ApproveOrPermitState.REQUIRES_APPROVAL) {
return {
action: {
message: <Trans>Approve {currency.symbol} first</Trans>,
onClick: onApprove,
children: <Trans>Approve</Trans>,
},
}
} else if (approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE) {
return {
action: {
message: <Trans>Allow {currency.symbol} first</Trans>,
onClick: onApprove,
children: <Trans>Allow</Trans>,
},
}
}
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
children: <Trans>Approve</Trans>,
},
}
}
if (approvalState === ApproveOrPermitState.PENDING_SIGNATURE) {
return {
disabled: true,
action: {
message: <Trans>Allowance pending</Trans>,
icon: Spinner,
children: <Trans>Allow</Trans>,
},
}
}
return
}, [approvalHash, approvalState, currency, onApprove, trade])
return { approvalData, signatureData: signatureData ?? undefined }
}
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils' import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { pickAtom } from 'lib/state/atoms' import { pickAtom } from 'lib/state/atoms'
...@@ -63,16 +63,6 @@ export function useIsSwapFieldIndependent(field: Field): boolean { ...@@ -63,16 +63,6 @@ export function useIsSwapFieldIndependent(field: Field): boolean {
return independentField === field return independentField === field
} }
export function useSwapTradeType(): TradeType {
const independentField = useAtomValue(independentFieldAtom)
switch (independentField) {
case Field.INPUT:
return TradeType.EXACT_INPUT
case Field.OUTPUT:
return TradeType.EXACT_OUTPUT
}
}
const amountAtom = pickAtom(swapAtom, 'amount') const amountAtom = pickAtom(swapAtom, 'amount')
// check if any amount has been entered by user // check if any amount has been entered by user
......
...@@ -20,8 +20,8 @@ export enum SwapCallbackState { ...@@ -20,8 +20,8 @@ export enum SwapCallbackState {
interface UseSwapCallbackReturns { interface UseSwapCallbackReturns {
state: SwapCallbackState state: SwapCallbackState
callback: null | (() => Promise<TransactionResponse>) callback?: () => Promise<TransactionResponse>
error: ReactNode | null error?: ReactNode
} }
interface UseSwapCallbackArgs { interface UseSwapCallbackArgs {
trade: AnyTrade | undefined // trade to execute, required trade: AnyTrade | undefined // trade to execute, required
...@@ -59,24 +59,19 @@ export function useSwapCallback({ ...@@ -59,24 +59,19 @@ export function useSwapCallback({
return useMemo(() => { return useMemo(() => {
if (!trade || !library || !account || !chainId || !callback) { if (!trade || !library || !account || !chainId || !callback) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> } return { state: SwapCallbackState.INVALID, error: <Trans>Missing dependencies</Trans> }
} }
if (!recipient) { if (!recipient) {
if (recipientAddressOrName !== null) { if (recipientAddressOrName !== null) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> } return { state: SwapCallbackState.INVALID, error: <Trans>Invalid recipient</Trans> }
} else { } else {
return { state: SwapCallbackState.LOADING, callback: null, error: null } return { state: SwapCallbackState.LOADING }
} }
} }
return { return {
state: SwapCallbackState.VALID, state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<TransactionResponse> { callback: async () => callback(),
return callback().then((response) => {
return response
})
},
error: null,
} }
}, [trade, library, account, chainId, callback, recipient, recipientAddressOrName]) }, [trade, library, account, chainId, callback, recipient, recipientAddressOrName])
} }
...@@ -3,7 +3,7 @@ import { useWETHContract } from 'hooks/useContract' ...@@ -3,7 +3,7 @@ import { useWETHContract } from 'hooks/useContract'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { Field, swapAtom } from 'lib/state/swap' import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useCallback, useMemo } from 'react' import { useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens' import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
import useActiveWeb3React from '../useActiveWeb3React' import useActiveWeb3React from '../useActiveWeb3React'
...@@ -15,7 +15,7 @@ export enum WrapType { ...@@ -15,7 +15,7 @@ export enum WrapType {
UNWRAP, UNWRAP,
} }
interface UseWrapCallbackReturns { interface UseWrapCallbackReturns {
callback: () => Promise<ContractTransaction | undefined> callback?: () => Promise<ContractTransaction>
type: WrapType type: WrapType
} }
...@@ -42,29 +42,21 @@ export default function useWrapCallback(): UseWrapCallbackReturns { ...@@ -42,29 +42,21 @@ export default function useWrapCallback(): UseWrapCallbackReturns {
) )
const balanceIn = useCurrencyBalance(account, inputCurrency) const balanceIn = useCurrencyBalance(account, inputCurrency)
const callback = useCallback(async () => { const callback = useMemo(() => {
if (wrapType === WrapType.NONE) { if (
return Promise.reject('Wrapping not applicable to this asset.') wrapType === WrapType.NONE ||
} !parsedAmountIn ||
if (!parsedAmountIn) { !balanceIn ||
return Promise.reject('Must provide an input amount to wrap.') balanceIn.lessThan(parsedAmountIn) ||
} !wrappedNativeCurrencyContract
if (!balanceIn || balanceIn.lessThan(parsedAmountIn)) { ) {
return Promise.reject('Insufficient balance to wrap desired amount.') return
}
if (!wrappedNativeCurrencyContract) {
return Promise.reject('Wrap contract not found.')
} }
try { return async () =>
return await (wrapType === WrapType.WRAP wrapType === WrapType.WRAP
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` }) ? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)) : wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
} catch (e) {
// TODO(zzmp): add error handling
console.error(e)
return
}
}, [wrapType, parsedAmountIn, balanceIn, wrappedNativeCurrencyContract]) }, [wrapType, parsedAmountIn, balanceIn, wrappedNativeCurrencyContract])
return useMemo(() => ({ callback, type: wrapType }), [callback, wrapType]) return useMemo(() => ({ callback, type: wrapType }), [callback, wrapType])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment