/ src / helpers / useParaSwapTransactionHandler.tsx
useParaSwapTransactionHandler.tsx
  1  import { EthereumTransactionTypeExtended, ProtocolAction } from '@aave/contract-helpers';
  2  import { SignatureLike } from '@ethersproject/bytes';
  3  import { TransactionResponse } from '@ethersproject/providers';
  4  import { useQueryClient } from '@tanstack/react-query';
  5  import { DependencyList, useEffect, useRef, useState } from 'react';
  6  import { SIGNATURE_AMOUNT_MARGIN } from 'src/hooks/paraswap/common';
  7  import { useModalContext } from 'src/hooks/useModal';
  8  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
  9  import { useRootStore } from 'src/store/root';
 10  import { ApprovalMethod } from 'src/store/walletSlice';
 11  import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
 12  import { queryKeysFactory } from 'src/ui-config/queries';
 13  import { useShallow } from 'zustand/shallow';
 14  
 15  import { MOCK_SIGNED_HASH } from './useTransactionHandler';
 16  
 17  interface UseParaSwapTransactionHandlerProps {
 18    /**
 19     * This function is called when the user clicks the action button in the modal and should return the transaction for the swap or repay.
 20     * The paraswap API should be called in the implementation to get the required transaction parameters.
 21     */
 22    handleGetTxns: (
 23      signature?: SignatureLike,
 24      deadline?: string
 25    ) => Promise<EthereumTransactionTypeExtended[]>;
 26    /**
 27     * This function is only called once on initial load, and should return a transaction for the swap or repay,
 28     * but the paraswap API should not be called in the implementation. This is to determine if an approval is needed and
 29     * to get the gas limit for the swap or repay.
 30     */
 31    handleGetApprovalTxns: () => Promise<EthereumTransactionTypeExtended[]>;
 32    /**
 33     * The gas limit recommendation to use for the swap or repay. This is used in the case where there is no approval needed.
 34     */
 35    gasLimitRecommendation: string;
 36    /**
 37     * If true, handleGetApprovalTxns will not be called. Can be used if the route information is still loading.
 38     */
 39    skip?: boolean;
 40    spender: string;
 41    deps?: DependencyList;
 42    protocolAction?: ProtocolAction;
 43  }
 44  
 45  interface ApprovalProps {
 46    amount?: string;
 47    underlyingAsset?: string;
 48  }
 49  
 50  export const useParaSwapTransactionHandler = ({
 51    handleGetTxns,
 52    handleGetApprovalTxns,
 53    gasLimitRecommendation,
 54    skip,
 55    spender,
 56    protocolAction,
 57    deps = [],
 58  }: UseParaSwapTransactionHandlerProps) => {
 59    const {
 60      approvalTxState,
 61      setApprovalTxState,
 62      mainTxState,
 63      setMainTxState,
 64      setGasLimit,
 65      loadingTxns,
 66      setLoadingTxns,
 67      setTxError,
 68    } = useModalContext();
 69    const { sendTx, getTxError, signTxData } = useWeb3Context();
 70    const [walletApprovalMethodPreference, generateSignatureRequest, addTransaction] = useRootStore(
 71      useShallow((state) => [
 72        state.walletApprovalMethodPreference,
 73        state.generateSignatureRequest,
 74        state.addTransaction,
 75      ])
 76    );
 77  
 78    const [approvalTx, setApprovalTx] = useState<EthereumTransactionTypeExtended | undefined>();
 79    const [actionTx, setActionTx] = useState<EthereumTransactionTypeExtended | undefined>();
 80    const [signature, setSignature] = useState<SignatureLike | undefined>();
 81    const [signatureDeadline, setSignatureDeadline] = useState<string | undefined>();
 82    interface Dependency {
 83      asset: string;
 84      amount: string;
 85    }
 86    const [previousDeps, setPreviousDeps] = useState<Dependency>({
 87      asset: deps[0] as string,
 88      amount: deps[1] as string,
 89    });
 90    const [usePermit, setUsePermit] = useState(false);
 91    const mounted = useRef(false);
 92    const queryClient = useQueryClient();
 93  
 94    useEffect(() => {
 95      mounted.current = true; // Will set it to true on mount ...
 96      return () => {
 97        mounted.current = false;
 98      }; // ... and to false on unmount
 99    }, []);
100    /**
101     * Executes the transactions and handles loading & error states.
102     * @param fn
103     * @returns
104     */
105    // eslint-disable-next-line
106    const processTx = async ({
107      tx,
108      errorCallback,
109      successCallback,
110    }: {
111      tx: () => Promise<TransactionResponse>;
112      // eslint-disable-next-line @typescript-eslint/no-explicit-any
113      errorCallback?: (error: any, hash?: string) => void;
114      successCallback?: (param: TransactionResponse) => void;
115      action: TxAction;
116    }) => {
117      try {
118        const txnResult = await tx();
119        try {
120          await txnResult.wait(1);
121          mounted.current && successCallback && successCallback(txnResult);
122          addTransaction(txnResult.hash, {
123            txState: 'success',
124            action: protocolAction || ProtocolAction.default,
125          });
126          queryClient.invalidateQueries({ queryKey: queryKeysFactory.pool });
127        } catch (e) {
128          // TODO: what to do with this error?
129          try {
130            // TODO: what to do with this error?
131            const error = await getTxError(txnResult.hash);
132            mounted.current && errorCallback && errorCallback(new Error(error), txnResult.hash);
133            return;
134          } catch (e) {
135            mounted.current && errorCallback && errorCallback(e, txnResult.hash);
136            return;
137          } finally {
138            addTransaction(txnResult.hash, {
139              txState: 'failed',
140              action: protocolAction || ProtocolAction.default,
141            });
142          }
143        }
144  
145        return;
146      } catch (e) {
147        errorCallback && errorCallback(e);
148      }
149    };
150  
151    const approval = async ({ amount, underlyingAsset }: ApprovalProps) => {
152      if (usePermit && amount && underlyingAsset) {
153        setApprovalTxState({ ...approvalTxState, loading: true });
154        try {
155          // deadline is an hour after signature
156          const deadline = Math.floor(Date.now() / 1000 + 3600).toString();
157          const unsingedPayload = await generateSignatureRequest({
158            token: underlyingAsset,
159            amount: amount,
160            deadline,
161            spender,
162          });
163          try {
164            const signature = await signTxData(unsingedPayload);
165            if (!mounted.current) return;
166            setSignature(signature);
167            setSignatureDeadline(deadline);
168            setApprovalTxState({
169              txHash: MOCK_SIGNED_HASH,
170              loading: false,
171              success: true,
172            });
173            setTxError(undefined);
174          } catch (error) {
175            if (!mounted.current) return;
176            const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
177            setTxError(parsedError);
178  
179            setApprovalTxState({
180              txHash: undefined,
181              loading: false,
182            });
183          }
184        } catch (error) {
185          if (!mounted.current) return;
186          const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
187          setTxError(parsedError);
188          setApprovalTxState({
189            txHash: undefined,
190            loading: false,
191          });
192        }
193      } else if (approvalTx) {
194        try {
195          setApprovalTxState({ ...approvalTxState, loading: true });
196          const params = await approvalTx.tx();
197          delete params.gasPrice;
198          await processTx({
199            tx: () => sendTx(params),
200            successCallback: (txnResponse: TransactionResponse) => {
201              setApprovalTxState({
202                txHash: txnResponse.hash,
203                loading: false,
204                success: true,
205              });
206              setTxError(undefined);
207            },
208            errorCallback: (error, hash) => {
209              const parsedError = getErrorTextFromError(error, TxAction.APPROVAL, false);
210              setTxError(parsedError);
211              setApprovalTxState({
212                txHash: hash,
213                loading: false,
214              });
215            },
216            action: TxAction.APPROVAL,
217          });
218        } catch (error) {
219          if (!mounted.current) return;
220          const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
221          setTxError(parsedError);
222          setApprovalTxState({
223            txHash: undefined,
224            loading: false,
225          });
226        }
227      }
228    };
229  
230    const action = async () => {
231      setMainTxState({ ...mainTxState, loading: true });
232      setTxError(undefined);
233      await handleGetTxns(signature, signatureDeadline)
234        .then(async (data) => {
235          // Find actionTx (repay with collateral or swap)
236          const actionTx = data.find((tx) => ['DLP_ACTION'].includes(tx.txType));
237          if (actionTx) {
238            try {
239              const params = await actionTx.tx();
240              delete params.gasPrice;
241              return processTx({
242                tx: () => sendTx(params),
243                successCallback: (txnResponse: TransactionResponse) => {
244                  setMainTxState({
245                    txHash: txnResponse.hash,
246                    loading: false,
247                    success: true,
248                  });
249                  setTxError(undefined);
250                },
251                errorCallback: (error, hash) => {
252                  const parsedError = getErrorTextFromError(error, TxAction.MAIN_ACTION);
253                  setTxError(parsedError);
254                  setMainTxState({
255                    txHash: hash,
256                    loading: false,
257                  });
258                },
259                action: TxAction.MAIN_ACTION,
260              });
261            } catch (error) {
262              const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
263              setTxError(parsedError);
264              setMainTxState({
265                ...mainTxState,
266                loading: false,
267              });
268            }
269          }
270        })
271        .catch((error) => {
272          const parsedError = getErrorTextFromError(error, TxAction.GAS_ESTIMATION, false);
273          setTxError(parsedError);
274          setMainTxState({
275            ...mainTxState,
276            loading: false,
277          });
278        });
279    };
280  
281    // Populates the approval transaction and sets the default gas estimation.
282    useEffect(() => {
283      if (!skip) {
284        setLoadingTxns(true);
285        handleGetApprovalTxns()
286          .then(async (data) => {
287            const approval = data.find((tx) => tx.txType === 'ERC20_APPROVAL');
288            const preferPermit = walletApprovalMethodPreference === ApprovalMethod.PERMIT;
289            // reset error and approval state if new signature request is required
290            if (
291              deps[0] !== previousDeps.asset ||
292              Number(deps[1]) >
293                Number(previousDeps.amount) +
294                  Number(previousDeps.amount) * (SIGNATURE_AMOUNT_MARGIN / 2)
295            ) {
296              setApprovalTxState({ success: false });
297              setTxError(undefined);
298            }
299            // clear error but use existing signature if amount changes
300            if (Number(deps[1]) < Number(previousDeps.amount)) {
301              setTxError(undefined);
302            }
303            setPreviousDeps({ asset: deps[0] as string, amount: deps[1] as string });
304            if (approval && preferPermit) {
305              setUsePermit(true);
306              setMainTxState({
307                txHash: undefined,
308              });
309              setLoadingTxns(false);
310            } else {
311              setUsePermit(false);
312              setApprovalTx(approval);
313            }
314          })
315          .finally(() => {
316            setMainTxState({
317              txHash: undefined,
318            });
319            setGasLimit(gasLimitRecommendation);
320            setLoadingTxns(false);
321          });
322      } else {
323        setApprovalTx(undefined);
324        setActionTx(undefined);
325      }
326    }, [skip, ...deps, walletApprovalMethodPreference]);
327  
328    return {
329      approval,
330      action,
331      loadingTxns,
332      requiresApproval: !!approvalTx || usePermit,
333      approvalTxState,
334      mainTxState,
335      actionTx,
336      approvalTx,
337    };
338  };