Skip to content

Commit

Permalink
♻️ approve: refactor approval flow
Browse files Browse the repository at this point in the history
  • Loading branch information
jgalat committed Aug 25, 2023
1 parent e2acc76 commit 787469c
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 65 deletions.
2 changes: 1 addition & 1 deletion components/operations/Repay/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion components/operations/RepayAtMaturity/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
3 changes: 1 addition & 2 deletions components/operations/Withdraw/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();

Expand Down
3 changes: 1 addition & 2 deletions components/operations/WithdrawAtMaturity/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
Expand Down
187 changes: 134 additions & 53 deletions hooks/useApprove.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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<boolean> => {
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<boolean> => {
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;
3 changes: 1 addition & 2 deletions hooks/useBorrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
Expand Down
3 changes: 1 addition & 2 deletions hooks/useBorrowAtMaturity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion hooks/useDeposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
2 changes: 1 addition & 1 deletion hooks/useDepositAtMaturity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

1 comment on commit 787469c

@vercel
Copy link

@vercel vercel bot commented on 787469c Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

app – ./

app.exact.ly
exactly.app
app.exactly.app
exactly-development.vercel.app
app-git-main.exactly.app

Please sign in to comment.