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