Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check allowance before approve #166

Merged
merged 6 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
]
53 changes: 44 additions & 9 deletions src/features/swap/SwapConfirm.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -207,7 +240,7 @@ export function SwapConfirmCard({ formValues }: Props) {
close={() => setIsModalOpen(false)}
width="max-w-[432px]"
>
<MentoLogoLoader />
<MentoLogoLoader needsApproval={needsApproval} />
</Modal>
</FloatingBox>
)
Expand Down Expand Up @@ -271,7 +304,7 @@ const ChevronRight = (props: SVGProps<SVGSVGElement>) => (
</svg>
)

const MentoLogoLoader = () => {
const MentoLogoLoader = ({ needsApproval }: { needsApproval: boolean }) => {
const { connector } = useAccount()

return (
Expand All @@ -286,12 +319,14 @@ const MentoLogoLoader = () => {
</div>

<div className="my-6">
<div className=" text-sm text-center text-[#636768] dark:text-[#AAB3B6]">
Sending two transactions: Approve and Swap
<div className="text-sm text-center text-[#636768] dark:text-[#AAB3B6]">
{needsApproval
? 'Sending two transactions: Approve and Swap'
: 'Sending swap transaction'}
</div>
<div className="mt-3 text-sm text-center text-[#636768] dark:text-[#AAB3B6]">
{`Sign with ${connector?.name || 'wallet'} to proceed`}
</div>
<div className="mt-3 text-sm text-center text-[#636768] dark:text-[#AAB3B6]">{`Sign with ${
connector?.name || 'wallet'
} to proceed`}</div>
</div>
</>
)
Expand Down
43 changes: 43 additions & 0 deletions src/features/swap/useAllowance.tsx
Original file line number Diff line number Diff line change
@@ -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<string> {
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,
}
}
4 changes: 3 additions & 1 deletion src/features/swap/useSwapTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down