ClaimRewardsModalContent.tsx
1 import { ChainId } from '@aave/contract-helpers'; 2 import { normalize, UserIncentiveData } from '@aave/math-utils'; 3 import { Trans } from '@lingui/macro'; 4 import { Box, Typography } from '@mui/material'; 5 import { useEffect, useState } from 'react'; 6 import { FormattedNumber } from 'src/components/primitives/FormattedNumber'; 7 import { Row } from 'src/components/primitives/Row'; 8 import { TokenIcon } from 'src/components/primitives/TokenIcon'; 9 import { Reward } from 'src/helpers/types'; 10 import { 11 ComputedReserveData, 12 ExtendedFormattedUser, 13 } from 'src/hooks/app-data-provider/useAppDataProvider'; 14 import { useModalContext } from 'src/hooks/useModal'; 15 import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; 16 import { useRootStore } from 'src/store/root'; 17 import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig'; 18 import { useShallow } from 'zustand/shallow'; 19 20 import { TxErrorView } from '../FlowCommons/Error'; 21 import { GasEstimationError } from '../FlowCommons/GasEstimationError'; 22 import { TxSuccessView } from '../FlowCommons/Success'; 23 import { 24 DetailsNumberLine, 25 DetailsNumberLineWithSub, 26 TxModalDetails, 27 } from '../FlowCommons/TxModalDetails'; 28 import { TxModalTitle } from '../FlowCommons/TxModalTitle'; 29 import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning'; 30 import { ClaimRewardsActions } from './ClaimRewardsActions'; 31 import { RewardsSelect } from './RewardsSelect'; 32 33 export enum ErrorType { 34 NOT_ENOUGH_BALANCE, 35 } 36 37 interface ClaimRewardsModalContentProps { 38 user: ExtendedFormattedUser; 39 reserves: ComputedReserveData[]; 40 } 41 42 export const ClaimRewardsModalContent = ({ user, reserves }: ClaimRewardsModalContentProps) => { 43 const { gasLimit, mainTxState: claimRewardsTxState, txError } = useModalContext(); 44 const [currentChainId, currentMarketData] = useRootStore( 45 useShallow((store) => [store.currentChainId, store.currentMarketData]) 46 ); 47 const { chainId: connectedChainId, readOnlyModeAddress } = useWeb3Context(); 48 const [claimableUsd, setClaimableUsd] = useState('0'); 49 const [selectedRewardSymbol, setSelectedRewardSymbol] = useState<string>('all'); 50 const [rewards, setRewards] = useState<Reward[]>([]); 51 const [allReward, setAllReward] = useState<Reward>(); 52 53 const networkConfig = getNetworkConfig(currentChainId); 54 55 // get all rewards 56 useEffect(() => { 57 const userIncentives: Reward[] = []; 58 let totalClaimableUsd = Number(claimableUsd); 59 const allAssets: string[] = []; 60 Object.keys(user.calculatedUserIncentives).forEach((rewardTokenAddress) => { 61 const incentive: UserIncentiveData = user.calculatedUserIncentives[rewardTokenAddress]; 62 const rewardBalance = normalize(incentive.claimableRewards, incentive.rewardTokenDecimals); 63 64 let tokenPrice = 0; 65 // getting price from reserves for the native rewards for v2 markets 66 if (!currentMarketData.v3 && Number(rewardBalance) > 0) { 67 if (currentMarketData.chainId === ChainId.mainnet) { 68 const aave = reserves.find((reserve) => reserve.symbol === 'AAVE'); 69 tokenPrice = aave ? Number(aave.priceInUSD) : 0; 70 } else { 71 reserves.forEach((reserve) => { 72 if (reserve.isWrappedBaseAsset) { 73 tokenPrice = Number(reserve.priceInUSD); 74 } 75 }); 76 } 77 } else { 78 tokenPrice = Number(incentive.rewardPriceFeed); 79 } 80 81 const rewardBalanceUsd = Number(rewardBalance) * tokenPrice; 82 83 if (rewardBalanceUsd > 0) { 84 incentive.assets.forEach((asset) => { 85 if (allAssets.indexOf(asset) === -1) { 86 allAssets.push(asset); 87 } 88 }); 89 90 userIncentives.push({ 91 assets: incentive.assets, 92 incentiveControllerAddress: incentive.incentiveControllerAddress, 93 symbol: incentive.rewardTokenSymbol, 94 balance: rewardBalance, 95 balanceUsd: rewardBalanceUsd.toString(), 96 rewardTokenAddress, 97 }); 98 99 totalClaimableUsd = totalClaimableUsd + Number(rewardBalanceUsd); 100 } 101 }); 102 103 if (userIncentives.length === 1) { 104 setSelectedRewardSymbol(userIncentives[0].symbol); 105 } else if (userIncentives.length > 1 && !selectedReward) { 106 const allRewards = { 107 assets: allAssets, 108 incentiveControllerAddress: userIncentives[0].incentiveControllerAddress, 109 symbol: 'all', 110 balance: '0', 111 balanceUsd: totalClaimableUsd.toString(), 112 rewardTokenAddress: '', 113 }; 114 setSelectedRewardSymbol('all'); 115 setAllReward(allRewards); 116 } 117 118 setRewards(userIncentives); 119 setClaimableUsd(totalClaimableUsd.toString()); 120 }, []); 121 122 // error handling 123 let blockingError: ErrorType | undefined = undefined; 124 if (claimableUsd === '0') { 125 blockingError = ErrorType.NOT_ENOUGH_BALANCE; 126 } 127 128 // error handling render 129 const handleBlocked = () => { 130 switch (blockingError) { 131 case ErrorType.NOT_ENOUGH_BALANCE: 132 return <Trans>Your reward balance is 0</Trans>; 133 default: 134 return null; 135 } 136 }; 137 138 // is Network mismatched 139 const isWrongNetwork = currentChainId !== connectedChainId; 140 const selectedReward = 141 selectedRewardSymbol === 'all' 142 ? allReward 143 : rewards.find((r) => r.symbol === selectedRewardSymbol); 144 145 if (txError && txError.blocking) { 146 return <TxErrorView txError={txError} />; 147 } 148 if (claimRewardsTxState.success) 149 return <TxSuccessView action={<Trans>Claimed</Trans>} amount={selectedReward?.balanceUsd} />; 150 151 return ( 152 <> 153 <TxModalTitle title="Claim rewards" /> 154 {isWrongNetwork && !readOnlyModeAddress && ( 155 <ChangeNetworkWarning networkName={networkConfig.name} chainId={currentChainId} /> 156 )} 157 158 {blockingError !== undefined && ( 159 <Typography variant="helperText" color="error.main"> 160 {handleBlocked()} 161 </Typography> 162 )} 163 164 {rewards.length > 1 && ( 165 <RewardsSelect 166 rewards={rewards} 167 selectedReward={selectedRewardSymbol} 168 setSelectedReward={setSelectedRewardSymbol} 169 /> 170 )} 171 172 {selectedReward && ( 173 <TxModalDetails gasLimit={gasLimit}> 174 {selectedRewardSymbol === 'all' && ( 175 <> 176 <Row 177 caption={<Trans>Balance</Trans>} 178 captionVariant="description" 179 align="flex-start" 180 mb={selectedReward.symbol !== 'all' ? 0 : 4} 181 > 182 <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}> 183 {rewards.map((reward) => ( 184 <Box 185 key={`claim-${reward.symbol}`} 186 sx={{ 187 display: 'flex', 188 flexDirection: 'column', 189 alignItems: 'flex-end', 190 mb: 4, 191 }} 192 > 193 <Box sx={{ display: 'flex', alignItems: 'center' }}> 194 <TokenIcon symbol={reward.symbol} sx={{ mr: 1, fontSize: '16px' }} /> 195 <FormattedNumber value={Number(reward.balance)} variant="secondary14" /> 196 <Typography ml={1} variant="secondary14"> 197 {reward.symbol} 198 </Typography> 199 </Box> 200 <FormattedNumber 201 value={Number(reward.balanceUsd)} 202 variant="helperText" 203 compact 204 symbol="USD" 205 color="text.secondary" 206 /> 207 </Box> 208 ))} 209 </Box> 210 </Row> 211 <DetailsNumberLine description={<Trans>Total worth</Trans>} value={claimableUsd} /> 212 </> 213 )} 214 {selectedRewardSymbol !== 'all' && ( 215 <DetailsNumberLineWithSub 216 symbol={<TokenIcon symbol={selectedReward.symbol} />} 217 futureValue={selectedReward.balance} 218 futureValueUSD={selectedReward.balanceUsd} 219 description={<Trans>{selectedReward.symbol} Balance</Trans>} 220 /> 221 )} 222 </TxModalDetails> 223 )} 224 225 {txError && <GasEstimationError txError={txError} />} 226 227 <ClaimRewardsActions 228 isWrongNetwork={isWrongNetwork} 229 selectedReward={selectedReward ?? ({} as Reward)} 230 blocked={blockingError !== undefined} 231 /> 232 </> 233 ); 234 };