/ src / components / transactions / Withdraw / WithdrawAndSwitchActions.tsx
WithdrawAndSwitchActions.tsx
  1  import { ERC20Service, gasLimitRecommendations, ProtocolAction } from '@aave/contract-helpers';
  2  import { SignatureLike } from '@ethersproject/bytes';
  3  import { Trans } from '@lingui/macro';
  4  import { BoxProps } from '@mui/material';
  5  import { useQueryClient } from '@tanstack/react-query';
  6  import { parseUnits } from 'ethers/lib/utils';
  7  import { useCallback, useEffect, useMemo, useState } from 'react';
  8  import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
  9  import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
 10  import { calculateSignedAmount, SwapTransactionParams } from 'src/hooks/paraswap/common';
 11  import { useModalContext } from 'src/hooks/useModal';
 12  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 13  import { useRootStore } from 'src/store/root';
 14  import { ApprovalMethod } from 'src/store/walletSlice';
 15  import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
 16  import { queryKeysFactory } from 'src/ui-config/queries';
 17  import { GENERAL } from 'src/utils/mixPanelEvents';
 18  import { useShallow } from 'zustand/shallow';
 19  
 20  import { TxActionsWrapper } from '../TxActionsWrapper';
 21  import { APPROVAL_GAS_LIMIT } from '../utils';
 22  
 23  interface WithdrawAndSwitchProps extends BoxProps {
 24    amountToSwap: string;
 25    amountToReceive: string;
 26    poolReserve: ComputedReserveData;
 27    targetReserve: ComputedReserveData;
 28    isWrongNetwork: boolean;
 29    blocked: boolean;
 30    isMaxSelected: boolean;
 31    loading?: boolean;
 32    buildTxFn: () => Promise<SwapTransactionParams>;
 33  }
 34  
 35  export interface WithdrawAndSwitchActionProps
 36    extends Pick<
 37      WithdrawAndSwitchProps,
 38      'amountToSwap' | 'amountToReceive' | 'poolReserve' | 'targetReserve' | 'isMaxSelected'
 39    > {
 40    augustus: string;
 41    signatureParams?: SignedParams;
 42    txCalldata: string;
 43  }
 44  
 45  interface SignedParams {
 46    signature: SignatureLike;
 47    deadline: string;
 48    amount: string;
 49  }
 50  
 51  export const WithdrawAndSwitchActions = ({
 52    amountToSwap,
 53    amountToReceive,
 54    isWrongNetwork,
 55    sx,
 56    poolReserve,
 57    targetReserve,
 58    isMaxSelected,
 59    loading,
 60    blocked,
 61    buildTxFn,
 62  }: WithdrawAndSwitchProps) => {
 63    const [
 64      withdrawAndSwitch,
 65      currentMarketData,
 66      jsonRpcProvider,
 67      account,
 68      generateApproval,
 69      estimateGasLimit,
 70      walletApprovalMethodPreference,
 71      generateSignatureRequest,
 72      addTransaction,
 73      trackEvent,
 74    ] = useRootStore(
 75      useShallow((state) => [
 76        state.withdrawAndSwitch,
 77        state.currentMarketData,
 78        state.jsonRpcProvider,
 79        state.account,
 80        state.generateApproval,
 81        state.estimateGasLimit,
 82        state.walletApprovalMethodPreference,
 83        state.generateSignatureRequest,
 84        state.addTransaction,
 85        state.trackEvent,
 86      ])
 87    );
 88    const {
 89      approvalTxState,
 90      mainTxState,
 91      loadingTxns,
 92      setMainTxState,
 93      setTxError,
 94      setGasLimit,
 95      setLoadingTxns,
 96      setApprovalTxState,
 97    } = useModalContext();
 98  
 99    const { sendTx, signTxData } = useWeb3Context();
100    const queryClient = useQueryClient();
101  
102    const [approvedAmount, setApprovedAmount] = useState<number | undefined>(undefined);
103    const [signatureParams, setSignatureParams] = useState<SignedParams | undefined>();
104  
105    const requiresApproval = useMemo(() => {
106      if (
107        approvedAmount === undefined ||
108        approvedAmount === -1 ||
109        amountToSwap === '0' ||
110        isWrongNetwork
111      )
112        return false;
113      else return approvedAmount <= Number(amountToSwap);
114    }, [approvedAmount, amountToSwap, isWrongNetwork]);
115  
116    const useSignature = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
117  
118    const action = async () => {
119      try {
120        setMainTxState({ ...mainTxState, loading: true });
121        const route = await buildTxFn();
122        const tx = withdrawAndSwitch({
123          poolReserve,
124          targetReserve,
125          isMaxSelected,
126          amountToSwap: parseUnits(amountToSwap, poolReserve.decimals).toString(),
127          amountToReceive: parseUnits(amountToReceive, targetReserve.decimals).toString(),
128          augustus: route.augustus,
129          txCalldata: route.swapCallData,
130          signatureParams,
131        });
132        const txDataWithGasEstimation = await estimateGasLimit(tx);
133        const response = await sendTx(txDataWithGasEstimation);
134        await response.wait(1);
135        queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
136        queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho });
137        setMainTxState({
138          txHash: response.hash,
139          loading: false,
140          success: true,
141        });
142        addTransaction(response.hash, {
143          action: ProtocolAction.withdrawAndSwitch,
144          txState: 'success',
145          asset: poolReserve.underlyingAsset,
146          amount: parseUnits(route.inputAmount, poolReserve.decimals).toString(),
147          assetName: poolReserve.name,
148          outAsset: targetReserve.underlyingAsset,
149          outAssetName: targetReserve.name,
150          outAmount: parseUnits(route.outputAmount, targetReserve.decimals).toString(),
151        });
152      } catch (error) {
153        const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
154        setTxError(parsedError);
155        setMainTxState({
156          txHash: undefined,
157          loading: false,
158        });
159        trackEvent(GENERAL.TRANSACTION_ERROR, {
160          transactiontype: ProtocolAction.withdrawAndSwitch,
161          asset: poolReserve.underlyingAsset,
162          assetName: poolReserve.name,
163          error,
164        });
165      }
166    };
167  
168    const approval = async () => {
169      const amountToApprove = calculateSignedAmount(amountToSwap, poolReserve.decimals);
170      const approvalData = {
171        user: account,
172        token: poolReserve.aTokenAddress,
173        spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER || '',
174        amount: amountToApprove,
175      };
176      try {
177        if (useSignature) {
178          const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
179          const signatureRequest = await generateSignatureRequest({
180            ...approvalData,
181            deadline,
182          });
183          setApprovalTxState({ ...approvalTxState, loading: true });
184          const response = await signTxData(signatureRequest);
185          setSignatureParams({ signature: response, deadline, amount: amountToApprove });
186          setApprovalTxState({
187            txHash: MOCK_SIGNED_HASH,
188            loading: false,
189            success: true,
190          });
191        } else {
192          const tx = generateApproval(approvalData);
193          const txWithGasEstimation = await estimateGasLimit(tx);
194          setApprovalTxState({ ...approvalTxState, loading: true });
195          const response = await sendTx(txWithGasEstimation);
196          await response.wait(1);
197          setApprovalTxState({
198            txHash: response.hash,
199            loading: false,
200            success: true,
201          });
202          addTransaction(response.hash, {
203            action: ProtocolAction.withdrawAndSwitch,
204            txState: 'success',
205            asset: poolReserve.aTokenAddress,
206            amount: parseUnits(amountToApprove, poolReserve.decimals).toString(),
207            assetName: `a${poolReserve.symbol}`,
208            spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER,
209          });
210          setTxError(undefined);
211          fetchApprovedAmount(poolReserve.aTokenAddress);
212        }
213      } catch (error) {
214        const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
215        setTxError(parsedError);
216        if (!approvalTxState.success) {
217          setApprovalTxState({
218            txHash: undefined,
219            loading: false,
220          });
221        }
222      }
223    };
224  
225    const fetchApprovedAmount = useCallback(
226      async (aTokenAddress: string) => {
227        setLoadingTxns(true);
228        const rpc = jsonRpcProvider();
229        const erc20Service = new ERC20Service(rpc);
230        const approvedTargetAmount = await erc20Service.approvedAmount({
231          user: account,
232          token: aTokenAddress,
233          spender: currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER || '',
234        });
235        setApprovedAmount(approvedTargetAmount);
236        setLoadingTxns(false);
237      },
238      [jsonRpcProvider, account, currentMarketData.addresses.WITHDRAW_SWITCH_ADAPTER, setLoadingTxns]
239    );
240  
241    useEffect(() => {
242      fetchApprovedAmount(poolReserve.aTokenAddress);
243    }, [fetchApprovedAmount, poolReserve.aTokenAddress]);
244  
245    useEffect(() => {
246      let switchGasLimit = 0;
247      switchGasLimit = Number(gasLimitRecommendations[ProtocolAction.withdrawAndSwitch].recommended);
248      if (requiresApproval && !approvalTxState.success) {
249        switchGasLimit += Number(APPROVAL_GAS_LIMIT);
250      }
251      setGasLimit(switchGasLimit.toString());
252    }, [requiresApproval, approvalTxState, setGasLimit]);
253  
254    return (
255      <TxActionsWrapper
256        mainTxState={mainTxState}
257        approvalTxState={approvalTxState}
258        isWrongNetwork={isWrongNetwork}
259        preparingTransactions={loadingTxns}
260        handleAction={action}
261        requiresAmount
262        amount={amountToSwap}
263        handleApproval={() => approval()}
264        requiresApproval={requiresApproval}
265        actionText={<Trans>Withdraw and Switch</Trans>}
266        actionInProgressText={<Trans>Withdrawing and Switching</Trans>}
267        sx={sx}
268        errorParams={{
269          loading: false,
270          disabled: blocked || !approvalTxState?.success,
271          content: <Trans>Withdraw and Switch</Trans>,
272          handleClick: action,
273        }}
274        fetchingData={loading}
275        blocked={blocked}
276        tryPermit={true}
277      />
278    );
279  };