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