diff --git a/src/config/consts.ts b/src/config/consts.ts index 5c46d1d..a395c60 100644 --- a/src/config/consts.ts +++ b/src/config/consts.ts @@ -19,3 +19,7 @@ export const MAX_GAS_LIMIT = '10000000' // 10 million export const MIN_ROUNDED_VALUE = 0.0001 export const DISPLAY_DECIMALS = 4 export const MAX_EXCHANGE_SPREAD = 0.1 // 10% + +export const ERC20_ABI = [ + 'function allowance(address owner, address spender) view returns (uint256)', +] diff --git a/src/features/swap/SwapConfirm.tsx b/src/features/swap/SwapConfirm.tsx index e64ad83..e2aa73f 100644 --- a/src/features/swap/SwapConfirm.tsx +++ b/src/features/swap/SwapConfirm.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import Lottie from 'lottie-react' import { SVGProps, useEffect, useState } from 'react' import mentoLoaderBlue from 'src/animations/Mentoloader_blue.json' @@ -8,6 +9,7 @@ import { TokenId, Tokens } from 'src/config/tokens' import { useAppDispatch, useAppSelector } from 'src/features/store/hooks' import { setConfirmView, setFormValues } from 'src/features/swap/swapSlice' import { SwapFormValues } from 'src/features/swap/types' +import { useAllowance } from 'src/features/swap/useAllowance' import { useApproveTransaction } from 'src/features/swap/useApproveTransaction' import { useSwapQuote } from 'src/features/swap/useSwapQuote' import { useSwapTransaction } from 'src/features/swap/useSwapTransaction' @@ -90,6 +92,21 @@ export function SwapConfirmCard({ formValues }: Props) { ) const [isApproveConfirmed, setApproveConfirmed] = useState(false) + const { allowance, isLoading: isAllowanceLoading } = useAllowance(chainId, fromTokenId, address) + const needsApproval = !isAllowanceLoading && new BigNumber(allowance).lte(approveAmount) + const skipApprove = !isAllowanceLoading && !needsApproval + + logger.info(`Allowance loading: ${isAllowanceLoading}`) + logger.info(`Needs approval: ${needsApproval}`) + + useEffect(() => { + if (skipApprove) { + // Enables swap transaction preparation when approval isn't needed + // See useSwapTransaction hook for more details + setApproveConfirmed(true) + } + }, [skipApprove]) + const { sendSwapTx, isSwapTxLoading, isSwapTxSuccess } = useSwapTransaction( chainId, fromTokenId, @@ -104,13 +121,29 @@ export function SwapConfirmCard({ formValues }: Props) { const onSubmit = async () => { if (!rate || !amountWei || !address || !isConnected) return + setIsModalOpen(true) + + if (skipApprove && sendSwapTx) { + try { + logger.info('Skipping approve, sending swap tx directly') + const swapResult = await sendSwapTx() + const swapReceipt = await swapResult.wait(1) + logger.info(`Tx receipt received for swap: ${swapReceipt?.transactionHash}`) + toastToYourSuccess('Swap Complete!', swapReceipt?.transactionHash, chainId) + dispatch(setFormValues(null)) + } catch (error) { + logger.error('Failed to execute swap', error) + } finally { + setIsModalOpen(false) + } + return + } + if (!sendApproveTx || isApproveTxSuccess || isApproveTxLoading) { logger.debug('Approve already started or finished, ignoring submit') return } - setIsModalOpen(true) - try { logger.info('Sending approve tx') const approveResult = await sendApproveTx() @@ -207,7 +240,7 @@ export function SwapConfirmCard({ formValues }: Props) { close={() => setIsModalOpen(false)} width="max-w-[432px]" > - + ) @@ -271,7 +304,7 @@ const ChevronRight = (props: SVGProps) => ( ) -const MentoLogoLoader = () => { +const MentoLogoLoader = ({ needsApproval }: { needsApproval: boolean }) => { const { connector } = useAccount() return ( @@ -286,12 +319,14 @@ const MentoLogoLoader = () => {
-
- Sending two transactions: Approve and Swap +
+ {needsApproval + ? 'Sending two transactions: Approve and Swap' + : 'Sending swap transaction'} +
+
+ {`Sign with ${connector?.name || 'wallet'} to proceed`}
-
{`Sign with ${ - connector?.name || 'wallet' - } to proceed`}
) diff --git a/src/features/swap/useAllowance.tsx b/src/features/swap/useAllowance.tsx new file mode 100644 index 0000000..467bab6 --- /dev/null +++ b/src/features/swap/useAllowance.tsx @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query' +import { Contract } from 'ethers' +import { ERC20_ABI } from 'src/config/consts' +import { BrokerAddresses } from 'src/config/exchanges' +import { TokenId, getTokenAddress } from 'src/config/tokens' +import { getProvider } from 'src/features/providers' +import { logger } from 'src/utils/logger' + +async function fetchAllowance( + tokenAddr: string, + accountAddress: string, + chainId: number +): Promise { + logger.info(`Fetching allowance for token ${tokenAddr} on chain ${chainId}`) + const provider = getProvider(chainId) + const contract = new Contract(tokenAddr, ERC20_ABI, provider) + const brokerAddress = BrokerAddresses[chainId as keyof typeof BrokerAddresses] + + const allowance = await contract.allowance(accountAddress, brokerAddress) + logger.info(`Allowance: ${allowance.toString()}`) + return allowance.toString() +} + +export function useAllowance(chainId: number, tokenId: TokenId, accountAddress?: string) { + const { data: allowance, isLoading } = useQuery( + ['tokenAllowance', chainId, tokenId, accountAddress], + async () => { + if (!accountAddress) return '0' + const tokenAddr = getTokenAddress(tokenId, chainId) + return fetchAllowance(tokenAddr, accountAddress, chainId) + }, + { + retry: false, + enabled: Boolean(accountAddress && chainId && tokenId), + staleTime: 5000, // Consider allowance stale after 5 seconds + } + ) + + return { + allowance: allowance || '0', + isLoading, + } +} diff --git a/src/features/swap/useSwapTransaction.ts b/src/features/swap/useSwapTransaction.ts index 6680883..3532161 100644 --- a/src/features/swap/useSwapTransaction.ts +++ b/src/features/swap/useSwapTransaction.ts @@ -36,8 +36,10 @@ export function useSwapTransaction( !isApproveConfirmed || new BigNumber(amountInWei).lte(0) || new BigNumber(thresholdAmountInWei).lte(0) - ) + ) { + logger.debug('Skipping swap transaction') return null + } const sdk = await getMentoSdk(chainId) const fromTokenAddr = getTokenAddress(fromToken, chainId) const toTokenAddr = getTokenAddress(toToken, chainId)