/ src / components / transactions / DebtSwitch / DebtSwitchActions.tsx
DebtSwitchActions.tsx
  1  import {
  2    ApproveDelegationType,
  3    gasLimitRecommendations,
  4    ProtocolAction,
  5  } from '@aave/contract-helpers';
  6  import { SignatureLike } from '@ethersproject/bytes';
  7  import { Trans } from '@lingui/macro';
  8  import { BoxProps } from '@mui/material';
  9  import { useQueryClient } from '@tanstack/react-query';
 10  import { parseUnits } from 'ethers/lib/utils';
 11  import { useCallback, useEffect, useState } from 'react';
 12  import { MOCK_SIGNED_HASH } from 'src/helpers/useTransactionHandler';
 13  import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider';
 14  import { calculateSignedAmount, SwapTransactionParams } from 'src/hooks/paraswap/common';
 15  import { useModalContext } from 'src/hooks/useModal';
 16  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 17  import { useRootStore } from 'src/store/root';
 18  import { ApprovalMethod } from 'src/store/walletSlice';
 19  import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
 20  import { queryKeysFactory } from 'src/ui-config/queries';
 21  import { useShallow } from 'zustand/shallow';
 22  
 23  import { TxActionsWrapper } from '../TxActionsWrapper';
 24  import { APPROVE_DELEGATION_GAS_LIMIT, checkRequiresApproval } from '../utils';
 25  
 26  interface DebtSwitchBaseProps extends BoxProps {
 27    amountToSwap: string;
 28    amountToReceive: string;
 29    poolReserve: ComputedReserveData;
 30    targetReserve: ComputedReserveData;
 31    isWrongNetwork: boolean;
 32    customGasPrice?: string;
 33    symbol?: string;
 34    blocked?: boolean;
 35    isMaxSelected: boolean;
 36    loading?: boolean;
 37    signatureParams?: SignedParams;
 38  }
 39  
 40  export interface DebtSwitchActionProps extends DebtSwitchBaseProps {
 41    augustus: string;
 42    txCalldata: string;
 43  }
 44  
 45  interface SignedParams {
 46    signature: SignatureLike;
 47    deadline: string;
 48    amount: string;
 49  }
 50  
 51  export const DebtSwitchActions = ({
 52    amountToSwap,
 53    amountToReceive,
 54    isWrongNetwork,
 55    sx,
 56    poolReserve,
 57    targetReserve,
 58    isMaxSelected,
 59    loading,
 60    blocked,
 61    buildTxFn,
 62  }: DebtSwitchBaseProps & { buildTxFn: () => Promise<SwapTransactionParams> }) => {
 63    const [
 64      getCreditDelegationApprovedAmount,
 65      currentMarketData,
 66      generateApproveDelegation,
 67      estimateGasLimit,
 68      addTransaction,
 69      debtSwitch,
 70      walletApprovalMethodPreference,
 71      generateCreditDelegationSignatureRequest,
 72    ] = useRootStore(
 73      useShallow((state) => [
 74        state.getCreditDelegationApprovedAmount,
 75        state.currentMarketData,
 76        state.generateApproveDelegation,
 77        state.estimateGasLimit,
 78        state.addTransaction,
 79        state.debtSwitch,
 80        state.walletApprovalMethodPreference,
 81        state.generateCreditDelegationSignatureRequest,
 82      ])
 83    );
 84    const {
 85      approvalTxState,
 86      mainTxState,
 87      loadingTxns,
 88      setMainTxState,
 89      setTxError,
 90      setGasLimit,
 91      setLoadingTxns,
 92      setApprovalTxState,
 93    } = useModalContext();
 94    const { sendTx, signTxData } = useWeb3Context();
 95    const queryClient = useQueryClient();
 96    const [requiresApproval, setRequiresApproval] = useState<boolean>(false);
 97    const [approvedAmount, setApprovedAmount] = useState<ApproveDelegationType | undefined>();
 98    const [useSignature, setUseSignature] = useState(false);
 99    const [signatureParams, setSignatureParams] = useState<SignedParams | undefined>();
100  
101    const approvalWithSignatureAvailable = currentMarketData.v3;
102  
103    useEffect(() => {
104      const preferSignature = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
105      setUseSignature(preferSignature);
106    }, [walletApprovalMethodPreference]);
107  
108    const approval = async () => {
109      try {
110        if (requiresApproval && approvedAmount) {
111          const approveDelegationAmount = calculateSignedAmount(
112            amountToReceive,
113            targetReserve.decimals,
114            0.25
115          );
116          if (useSignature && approvalWithSignatureAvailable) {
117            const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
118            const signatureRequest = await generateCreditDelegationSignatureRequest({
119              underlyingAsset: targetReserve.variableDebtTokenAddress,
120              deadline,
121              amount: approveDelegationAmount,
122              spender: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
123            });
124            const response = await signTxData(signatureRequest);
125            setSignatureParams({ signature: response, deadline, amount: approveDelegationAmount });
126            setApprovalTxState({
127              txHash: MOCK_SIGNED_HASH,
128              loading: false,
129              success: true,
130            });
131          } else {
132            let approveDelegationTxData = generateApproveDelegation({
133              debtTokenAddress: targetReserve.variableDebtTokenAddress,
134              delegatee: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
135              amount: approveDelegationAmount,
136            });
137            setApprovalTxState({ ...approvalTxState, loading: true });
138            approveDelegationTxData = await estimateGasLimit(approveDelegationTxData);
139            const response = await sendTx(approveDelegationTxData);
140            await response.wait(1);
141            setApprovalTxState({
142              txHash: response.hash,
143              loading: false,
144              success: true,
145            });
146            addTransaction(response.hash, {
147              action: ProtocolAction.approval,
148              txState: 'success',
149              asset: targetReserve.variableDebtTokenAddress,
150              amount: approveDelegationAmount,
151              assetName: 'varDebt' + targetReserve.name,
152              spender: currentMarketData.addresses.DEBT_SWITCH_ADAPTER,
153            });
154            setTxError(undefined);
155            fetchApprovedAmount(true);
156          }
157        }
158      } catch (error) {
159        const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
160        setTxError(parsedError);
161        if (!approvalTxState.success) {
162          setApprovalTxState({
163            txHash: undefined,
164            loading: false,
165          });
166        }
167      }
168    };
169    const action = async () => {
170      try {
171        setMainTxState({ ...mainTxState, loading: true });
172        const route = await buildTxFn();
173        let debtSwitchTxData = debtSwitch({
174          poolReserve,
175          targetReserve,
176          amountToReceive: parseUnits(amountToReceive, targetReserve.decimals).toString(),
177          amountToSwap: parseUnits(amountToSwap, poolReserve.decimals).toString(),
178          isMaxSelected,
179          txCalldata: route.swapCallData,
180          augustus: route.augustus,
181          signatureParams,
182          isWrongNetwork,
183        });
184        debtSwitchTxData = await estimateGasLimit(debtSwitchTxData);
185        const response = await sendTx(debtSwitchTxData);
186        await response.wait(1);
187        setMainTxState({
188          txHash: response.hash,
189          loading: false,
190          success: true,
191        });
192        addTransaction(response.hash, {
193          action: 'debtSwitch',
194          txState: 'success',
195          previousState: `${route.outputAmount} variable ${poolReserve.symbol}`,
196          newState: `${route.inputAmount} variable ${targetReserve.symbol}`,
197        });
198  
199        queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
200        queryClient.invalidateQueries({ queryKey: queryKeysFactory.gho });
201      } catch (error) {
202        const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
203        setTxError(parsedError);
204        setMainTxState({
205          txHash: undefined,
206          loading: false,
207        });
208      }
209    };
210  
211    // callback to fetch approved credit delegation amount and determine execution path on dependency updates
212    const fetchApprovedAmount = useCallback(
213      async (forceApprovalCheck?: boolean) => {
214        // Check approved amount on-chain on first load or if an action triggers a re-check such as an approveDelegation being confirmed
215        let approval = approvedAmount;
216        if (approval === undefined || forceApprovalCheck) {
217          setLoadingTxns(true);
218          approval = await getCreditDelegationApprovedAmount({
219            debtTokenAddress: targetReserve.variableDebtTokenAddress,
220            delegatee: currentMarketData.addresses.DEBT_SWITCH_ADAPTER ?? '',
221          });
222          setApprovedAmount(approval);
223        } else {
224          setRequiresApproval(false);
225          setApprovalTxState({});
226        }
227  
228        if (approval) {
229          const fetchedRequiresApproval = checkRequiresApproval({
230            approvedAmount: approval.amount,
231            amount: amountToReceive,
232            signedAmount: '0',
233          });
234          setRequiresApproval(fetchedRequiresApproval);
235          if (fetchedRequiresApproval) setApprovalTxState({});
236        }
237  
238        setLoadingTxns(false);
239      },
240      [
241        approvedAmount,
242        setLoadingTxns,
243        getCreditDelegationApprovedAmount,
244        targetReserve.variableDebtTokenAddress,
245        currentMarketData.addresses.DEBT_SWITCH_ADAPTER,
246        setApprovalTxState,
247        amountToReceive,
248      ]
249    );
250  
251    // Run on first load and when the target reserve changes
252    useEffect(() => {
253      if (amountToSwap === '0') return;
254  
255      if (!approvedAmount) {
256        fetchApprovedAmount();
257      } else if (approvedAmount.debtTokenAddress !== targetReserve.variableDebtTokenAddress) {
258        fetchApprovedAmount(true);
259      }
260    }, [amountToSwap, approvedAmount, fetchApprovedAmount, targetReserve.variableDebtTokenAddress]);
261  
262    // Update gas estimation
263    useEffect(() => {
264      let switchGasLimit = 0;
265      switchGasLimit = Number(gasLimitRecommendations[ProtocolAction.borrow].recommended);
266      if (requiresApproval && !approvalTxState.success) {
267        switchGasLimit += Number(APPROVE_DELEGATION_GAS_LIMIT);
268      }
269      setGasLimit(switchGasLimit.toString());
270    }, [requiresApproval, approvalTxState, setGasLimit]);
271  
272    return (
273      <TxActionsWrapper
274        mainTxState={mainTxState}
275        approvalTxState={approvalTxState}
276        isWrongNetwork={isWrongNetwork}
277        preparingTransactions={loadingTxns}
278        handleAction={action}
279        requiresAmount
280        amount={amountToSwap}
281        handleApproval={() => approval()}
282        requiresApproval={requiresApproval}
283        actionText={<Trans>Switch</Trans>}
284        actionInProgressText={<Trans>Switching</Trans>}
285        sx={sx}
286        fetchingData={loading}
287        errorParams={{
288          loading: false,
289          disabled: blocked || !approvalTxState?.success,
290          content: <Trans>Switch</Trans>,
291          handleClick: action,
292        }}
293        blocked={blocked}
294        tryPermit={approvalWithSignatureAvailable}
295      />
296    );
297  };