diff --git a/components/operations/Repay/index.tsx b/components/operations/Repay/index.tsx index 65c47000c..738df516d 100644 --- a/components/operations/Repay/index.tsx +++ b/components/operations/Repay/index.tsx @@ -76,7 +76,7 @@ function Repay() { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('repay', assetContract, marketAccount?.market); + } = useApprove({ operation: 'repay', contract: assetContract, spender: marketAccount?.market }); const onMax = useCallback(() => { setQty(finalAmount); diff --git a/components/operations/RepayAtMaturity/index.tsx b/components/operations/RepayAtMaturity/index.tsx index 7e375b033..7e48bc1d2 100644 --- a/components/operations/RepayAtMaturity/index.tsx +++ b/components/operations/RepayAtMaturity/index.tsx @@ -156,7 +156,7 @@ const RepayAtMaturity: FC = () => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('repayAtMaturity', assetContract, marketAccount?.market); + } = useApprove({ operation: 'repayAtMaturity', contract: assetContract, spender: marketAccount?.market }); const estimate = useEstimateGas(); diff --git a/components/operations/Withdraw/index.tsx b/components/operations/Withdraw/index.tsx index bc070369e..3f9304d9c 100644 --- a/components/operations/Withdraw/index.tsx +++ b/components/operations/Withdraw/index.tsx @@ -24,7 +24,6 @@ import { useTranslation } from 'react-i18next'; import useTranslateOperation from 'hooks/useTranslateOperation'; import useEstimateGas from 'hooks/useEstimateGas'; import { formatUnits, parseUnits } from 'viem'; -import { ERC20 } from 'types/contracts'; import { waitForTransaction } from '@wagmi/core'; import { gasLimit } from 'utils/gas'; @@ -72,7 +71,7 @@ const Withdraw: FC = () => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('withdraw', marketContract as ERC20 | undefined, ETHRouterContract?.address); + } = useApprove({ operation: 'withdraw', contract: marketContract, spender: ETHRouterContract?.address }); const estimate = useEstimateGas(); diff --git a/components/operations/WithdrawAtMaturity/index.tsx b/components/operations/WithdrawAtMaturity/index.tsx index 7abce9b4b..510b8023a 100644 --- a/components/operations/WithdrawAtMaturity/index.tsx +++ b/components/operations/WithdrawAtMaturity/index.tsx @@ -30,7 +30,6 @@ import useTranslateOperation from 'hooks/useTranslateOperation'; import { WEI_PER_ETHER } from 'utils/const'; import useEstimateGas from 'hooks/useEstimateGas'; import { formatUnits, parseUnits, zeroAddress } from 'viem'; -import { ERC20 } from 'types/contracts'; import { waitForTransaction } from '@wagmi/core'; import { gasLimit } from 'utils/gas'; @@ -96,7 +95,7 @@ const WithdrawAtMaturity: FC = () => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('withdrawAtMaturity', marketContract as ERC20 | undefined, ETHRouterContract?.address); + } = useApprove({ operation: 'withdrawAtMaturity', contract: marketContract, spender: ETHRouterContract?.address }); const previewWithdrawAtMaturity = useCallback(async () => { if (!marketAccount || !date || !previewerContract) return; diff --git a/hooks/useApprove.ts b/hooks/useApprove.ts index 0cdb0e035..4846b6c1f 100644 --- a/hooks/useApprove.ts +++ b/hooks/useApprove.ts @@ -1,25 +1,32 @@ import { useCallback, useState } from 'react'; -import { parseUnits } from 'viem'; -import { ErrorCode } from '@ethersproject/logger'; -import { ERC20 } from 'types/contracts'; +import { parseUnits, type Address, type EstimateGasParameters, type Hex } from 'viem'; +import { ERC20, Market } from 'types/contracts'; import { useWeb3 } from './useWeb3'; import { useOperationContext } from 'contexts/OperationContext'; import useAccountData from './useAccountData'; import handleOperationError from 'utils/handleOperationError'; -import { Address, useNetwork } from 'wagmi'; +import { waitForTransaction } from '@wagmi/core'; import { useTranslation } from 'react-i18next'; + import { MAX_UINT256 } from 'utils/const'; import useEstimateGas from './useEstimateGas'; import useAnalytics from './useAnalytics'; -import { waitForTransaction } from '@wagmi/core'; import { gasLimit } from 'utils/gas'; -import type { Operation } from 'types/Operation'; -export default (operation: Operation, contract?: ERC20, spender?: Address) => { +function useApprove({ + operation, + contract, + spender, +}: + | { operation: 'deposit' | 'depositAtMaturity' | 'repay' | 'repayAtMaturity'; contract?: ERC20; spender?: Address } + | { + operation: 'withdraw' | 'withdrawAtMaturity' | 'borrow' | 'borrowAtMaturity'; + contract?: Market; + spender?: Address; + }) { const { t } = useTranslation(); - const { walletAddress, chain: displayNetwork, opts } = useWeb3(); - const { chain } = useNetwork(); - const { symbol, setErrorData, setLoadingButton } = useOperationContext(); + const { walletAddress, opts } = useWeb3(); + const { qty, symbol, setErrorData, setLoadingButton } = useOperationContext(); const [isLoading, setIsLoading] = useState(false); const { transaction } = useAnalytics(); @@ -30,79 +37,153 @@ export default (operation: Operation, contract?: ERC20, spender?: Address) => { const estimateGas = useCallback(async () => { if (!contract || !spender || !walletAddress || !opts) return; - const { request } = await contract.simulate.approve([spender, MAX_UINT256], opts); - return estimate(request); - }, [contract, spender, walletAddress, estimate, opts]); - - const needsApproval = useCallback( - async (qty: string): Promise => { - switch (operation) { - case 'deposit': - case 'depositAtMaturity': - case 'repay': - case 'repayAtMaturity': - if (symbol === 'WETH') return false; - break; - case 'withdraw': - case 'withdrawAtMaturity': - case 'borrow': - case 'borrowAtMaturity': - if (symbol !== 'WETH') return false; - break; + let params: EstimateGasParameters; + switch (operation) { + case 'deposit': + case 'depositAtMaturity': + case 'repay': + case 'repayAtMaturity': { + const { request } = await contract.simulate.approve([spender, MAX_UINT256], opts); + params = request; + break; + } + case 'withdraw': + case 'withdrawAtMaturity': + case 'borrow': + case 'borrowAtMaturity': { + const { request } = await contract.simulate.approve([spender, MAX_UINT256], opts); + params = request; + break; } + } - if (!walletAddress || !marketAccount || !contract || !spender) return true; + if (!params) return; - if (chain?.id !== displayNetwork.id) return true; + return estimate(params); + }, [contract, spender, walletAddress, opts, operation, estimate]); + const needsApproval = useCallback( + async (amount: string): Promise => { try { + if (!walletAddress || !marketAccount || !contract || !spender) return true; + + const quantity = parseUnits(amount, marketAccount.decimals); + + switch (operation) { + case 'deposit': + case 'depositAtMaturity': + case 'repay': + case 'repayAtMaturity': + if (symbol === 'WETH') return false; + break; + case 'borrow': + case 'borrowAtMaturity': + case 'withdraw': + case 'withdrawAtMaturity': { + if (symbol !== 'WETH') return false; + const shares = await contract.read.previewWithdraw([quantity], opts); + const allowance = await contract.read.allowance([walletAddress, spender], opts); + return allowance < shares; + } + } + const allowance = await contract.read.allowance([walletAddress, spender], opts); - return allowance === 0n || allowance < parseUnits(qty, marketAccount.decimals); + return allowance < quantity; } catch { return true; } }, - [operation, walletAddress, marketAccount, contract, spender, chain?.id, displayNetwork.id, symbol, opts], + [operation, walletAddress, marketAccount, contract, spender, symbol, opts], ); const approve = useCallback(async () => { - if (!contract || !spender || !walletAddress || !opts) return; + if (!contract || !spender || !walletAddress || !marketAccount || !qty || !opts) return; try { + let quantity = 0n; + switch (operation) { + case 'deposit': + case 'depositAtMaturity': + quantity = parseUnits(qty, marketAccount.decimals); + break; + case 'repay': + case 'repayAtMaturity': + quantity = (parseUnits(qty, marketAccount.decimals) * 1005n) / 1000n; + break; + case 'borrow': + case 'borrowAtMaturity': + case 'withdraw': + case 'withdrawAtMaturity': + quantity = + ((await contract.read.previewWithdraw([parseUnits(qty, marketAccount.decimals)], opts)) * 1005n) / 1000n; + break; + } + setIsLoading(true); transaction.addToCart('approve'); setLoadingButton({ label: t('Sign the transaction on your wallet') }); - const args = [spender, MAX_UINT256] as const; - const gasEstimation = await contract.estimateGas.approve(args, opts); - const hash = await contract.write.approve(args, { - ...opts, - gasLimit: gasLimit(gasEstimation), - }); + const args = [spender, quantity] as const; + + let hash: Hex; + switch (operation) { + case 'deposit': + case 'depositAtMaturity': + case 'repay': + case 'repayAtMaturity': { + const gas = await contract.estimateGas.approve(args, opts); + hash = await contract.write.approve(args, { + ...opts, + gasLimit: gasLimit(gas), + }); + break; + } + case 'withdraw': + case 'withdrawAtMaturity': + case 'borrow': + case 'borrowAtMaturity': { + const gas = await contract.estimateGas.approve(args, opts); + hash = await contract.write.approve(args, { + ...opts, + gasLimit: gasLimit(gas), + }); + break; + } + } + + if (!hash) return; transaction.beginCheckout('approve'); setLoadingButton({ withCircularProgress: true, label: t('Approving {{symbol}}', { symbol }) }); - const { status } = await waitForTransaction({ hash }); - if (status) transaction.purchase('approve'); + if (status === 'reverted') throw new Error('Transaction reverted'); + + transaction.purchase('approve'); } catch (error) { transaction.removeFromCart('approve'); - const isDenied = [ErrorCode.ACTION_REJECTED, ErrorCode.TRANSACTION_REPLACED].includes( - (error as { code: ErrorCode }).code, - ); - - if (!isDenied) handleOperationError(error); - setErrorData({ - status: true, - message: isDenied ? t('Transaction rejected') : t('Approve failed, please try again'), - }); + setErrorData({ status: true, message: handleOperationError(error) }); } finally { setIsLoading(false); setLoadingButton({}); } - }, [contract, spender, walletAddress, opts, transaction, setLoadingButton, t, symbol, setErrorData]); + }, [ + contract, + spender, + walletAddress, + marketAccount, + opts, + transaction, + setLoadingButton, + t, + operation, + qty, + symbol, + setErrorData, + ]); return { approve, needsApproval, estimateGas, isLoading }; -}; +} + +export default useApprove; diff --git a/hooks/useBorrow.ts b/hooks/useBorrow.ts index 1ed22cbf3..8c6c2d3cb 100644 --- a/hooks/useBorrow.ts +++ b/hooks/useBorrow.ts @@ -13,7 +13,6 @@ import useHealthFactor from './useHealthFactor'; import useAnalytics from './useAnalytics'; import { WEI_PER_ETHER } from 'utils/const'; import useEstimateGas from './useEstimateGas'; -import { ERC20 } from 'types/contracts'; import { parseUnits, formatUnits } from 'viem'; import { gasLimit } from 'utils/gas'; @@ -55,7 +54,7 @@ export default (): Borrow => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('borrow', marketContract as ERC20 | undefined, ETHRouterContract?.address); + } = useApprove({ operation: 'borrow', contract: marketContract, spender: ETHRouterContract?.address }); const borrowLimit: bigint = useMemo( () => (marketAccount ? getBeforeBorrowLimit(marketAccount, 'borrow') : 0n), diff --git a/hooks/useBorrowAtMaturity.ts b/hooks/useBorrowAtMaturity.ts index ea430508b..79557f1ae 100644 --- a/hooks/useBorrowAtMaturity.ts +++ b/hooks/useBorrowAtMaturity.ts @@ -13,7 +13,6 @@ import useHealthFactor from './useHealthFactor'; import useAnalytics from './useAnalytics'; import { WEI_PER_ETHER } from 'utils/const'; import useEstimateGas from './useEstimateGas'; -import { ERC20 } from 'types/contracts'; import { formatUnits, parseUnits } from 'viem'; import { waitForTransaction } from '@wagmi/core'; import dayjs from 'dayjs'; @@ -78,7 +77,7 @@ export default (): BorrowAtMaturity => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('borrowAtMaturity', marketContract as ERC20 | undefined, ETHRouterContract?.address); + } = useApprove({ operation: 'borrowAtMaturity', contract: marketContract, spender: ETHRouterContract?.address }); const poolLiquidity = usePoolLiquidity(symbol); diff --git a/hooks/useDeposit.ts b/hooks/useDeposit.ts index a5c0a0ac1..a8b8625d4 100644 --- a/hooks/useDeposit.ts +++ b/hooks/useDeposit.ts @@ -54,7 +54,7 @@ export default (): Deposit => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('deposit', assetContract, marketAccount?.market); + } = useApprove({ operation: 'deposit', contract: assetContract, spender: marketAccount?.market }); const estimate = useEstimateGas(); diff --git a/hooks/useDepositAtMaturity.ts b/hooks/useDepositAtMaturity.ts index a5383cf98..42fdec4b5 100644 --- a/hooks/useDepositAtMaturity.ts +++ b/hooks/useDepositAtMaturity.ts @@ -72,7 +72,7 @@ export default (): DepositAtMaturity => { estimateGas: approveEstimateGas, isLoading: approveIsLoading, needsApproval, - } = useApprove('depositAtMaturity', assetContract, marketAccount?.market); + } = useApprove({ operation: 'depositAtMaturity', contract: assetContract, spender: marketAccount?.market }); const estimate = useEstimateGas();