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 };