/ src / components / transactions / ClaimRewards / ClaimRewardsModalContent.tsx
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  };