/ src / components / transactions / StakeCooldown / StakeCooldownModalContent.tsx
StakeCooldownModalContent.tsx
  1  import { ChainId, Stake } from '@aave/contract-helpers';
  2  import { valueToBigNumber } from '@aave/math-utils';
  3  import { ArrowDownIcon, CalendarIcon } from '@heroicons/react/outline';
  4  import { ArrowNarrowRightIcon } from '@heroicons/react/solid';
  5  import { Trans } from '@lingui/macro';
  6  import { Box, Checkbox, FormControlLabel, SvgIcon, Typography } from '@mui/material';
  7  import dayjs from 'dayjs';
  8  import { formatEther, parseUnits } from 'ethers/lib/utils';
  9  import React, { useState } from 'react';
 10  import { FormattedNumber } from 'src/components/primitives/FormattedNumber';
 11  import { TokenIcon } from 'src/components/primitives/TokenIcon';
 12  import { Warning } from 'src/components/primitives/Warning';
 13  import { useGeneralStakeUiData } from 'src/hooks/stake/useGeneralStakeUiData';
 14  import { useUserStakeUiData } from 'src/hooks/stake/useUserStakeUiData';
 15  import { useModalContext } from 'src/hooks/useModal';
 16  import { useWeb3Context } from 'src/libs/hooks/useWeb3Context';
 17  import { useRootStore } from 'src/store/root';
 18  import { stakeConfig } from 'src/ui-config/stakeConfig';
 19  import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
 20  import { GENERAL } from 'src/utils/mixPanelEvents';
 21  
 22  import { formattedTime, timeText } from '../../../helpers/timeHelper';
 23  import { Link } from '../../primitives/Link';
 24  import { TxErrorView } from '../FlowCommons/Error';
 25  import { GasEstimationError } from '../FlowCommons/GasEstimationError';
 26  import { TxSuccessView } from '../FlowCommons/Success';
 27  import { TxModalTitle } from '../FlowCommons/TxModalTitle';
 28  import { GasStation } from '../GasStation/GasStation';
 29  import { ChangeNetworkWarning } from '../Warnings/ChangeNetworkWarning';
 30  import { StakeCooldownActions } from './StakeCooldownActions';
 31  
 32  export type StakeCooldownProps = {
 33    stakeAssetName: Stake;
 34    icon: string;
 35  };
 36  
 37  export enum ErrorType {
 38    NOT_ENOUGH_BALANCE,
 39    ALREADY_ON_COOLDOWN,
 40  }
 41  
 42  type CalendarEvent = {
 43    title: string;
 44    start: string;
 45    end: string;
 46    description: string;
 47  };
 48  
 49  export const StakeCooldownModalContent = ({ stakeAssetName, icon }: StakeCooldownProps) => {
 50    const { chainId: connectedChainId, readOnlyModeAddress } = useWeb3Context();
 51    const { gasLimit, mainTxState: txState, txError } = useModalContext();
 52    const trackEvent = useRootStore((store) => store.trackEvent);
 53    const currentMarketData = useRootStore((store) => store.currentMarketData);
 54    const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
 55    const currentChainId = useRootStore((store) => store.currentChainId);
 56  
 57    const { data: stakeUserResult } = useUserStakeUiData(currentMarketData, stakeAssetName);
 58    const { data: stakeGeneralResult } = useGeneralStakeUiData(currentMarketData, stakeAssetName);
 59  
 60    // states
 61    const [cooldownCheck, setCooldownCheck] = useState(false);
 62  
 63    const stakeData = stakeGeneralResult?.[0];
 64    const stakeUserData = stakeUserResult?.[0];
 65  
 66    // Cooldown logic
 67    const stakeCooldownSeconds = stakeData?.stakeCooldownSeconds || 0;
 68    const stakeUnstakeWindow = stakeData?.stakeUnstakeWindow || 0;
 69  
 70    const cooldownPercent = valueToBigNumber(stakeCooldownSeconds)
 71      .dividedBy(stakeCooldownSeconds + stakeUnstakeWindow)
 72      .multipliedBy(100)
 73      .toNumber();
 74    const unstakeWindowPercent = valueToBigNumber(stakeUnstakeWindow)
 75      .dividedBy(stakeCooldownSeconds + stakeUnstakeWindow)
 76      .multipliedBy(100)
 77      .toNumber();
 78  
 79    const cooldownLineWidth = cooldownPercent < 15 ? 15 : cooldownPercent > 85 ? 85 : cooldownPercent;
 80    const unstakeWindowLineWidth =
 81      unstakeWindowPercent < 15 ? 15 : unstakeWindowPercent > 85 ? 85 : unstakeWindowPercent;
 82  
 83    const stakedAmount = stakeUserData?.stakeTokenRedeemableAmount;
 84  
 85    // error handler
 86    let blockingError: ErrorType | undefined = undefined;
 87    if (stakedAmount === '0') {
 88      blockingError = ErrorType.NOT_ENOUGH_BALANCE;
 89    }
 90  
 91    const handleBlocked = () => {
 92      switch (blockingError) {
 93        case ErrorType.NOT_ENOUGH_BALANCE:
 94          return <Trans>Nothing staked</Trans>;
 95        default:
 96          return null;
 97      }
 98    };
 99  
100    // is Network mismatched
101    const stakingChain =
102      currentNetworkConfig.isFork && currentNetworkConfig.underlyingChainId === stakeConfig.chainId
103        ? currentChainId
104        : stakeConfig.chainId;
105    const isWrongNetwork = connectedChainId !== stakingChain;
106  
107    const networkConfig = getNetworkConfig(stakingChain);
108  
109    if (txError && txError.blocking) {
110      return <TxErrorView txError={txError} />;
111    }
112    if (txState.success) return <TxSuccessView action={<Trans>Stake cooldown activated</Trans>} />;
113  
114    const timeMessage = (time: number) => {
115      return `${formattedTime(time)} ${timeText(time)}`;
116    };
117  
118    const handleOnCoolDownCheckBox = () => {
119      trackEvent(GENERAL.ACCEPT_RISK, {
120        asset: stakeAssetName,
121        modal: 'Cooldown',
122      });
123      setCooldownCheck(!cooldownCheck);
124    };
125    const amountToCooldown = formatEther(stakeUserData?.stakeTokenRedeemableAmount || 0);
126  
127    const dateMessage = (time: number) => {
128      const now = dayjs();
129  
130      const futureDate = now.add(time, 'second');
131  
132      return futureDate.format('DD.MM.YY');
133    };
134  
135    const googleDate = (timeInSeconds: number) => {
136      const date = dayjs().add(timeInSeconds, 'second');
137      return date.format('YYYYMMDDTHHmmss') + 'Z'; // UTC time
138    };
139  
140    const createGoogleCalendarUrl = (event: CalendarEvent) => {
141      const startTime = encodeURIComponent(event.start);
142      const endTime = encodeURIComponent(event.end);
143      const text = encodeURIComponent(event.title);
144      const details = encodeURIComponent(event.description);
145  
146      return `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${text}&dates=${startTime}/${endTime}&details=${details}`;
147    };
148  
149    const event = {
150      title: 'Unstaking window for Aave',
151      start: googleDate(stakeCooldownSeconds),
152      end: googleDate(stakeCooldownSeconds + stakeUnstakeWindow),
153      description: 'Unstaking window for Aave staking activated',
154    };
155  
156    const googleCalendarUrl = createGoogleCalendarUrl(event);
157  
158    return (
159      <>
160        <TxModalTitle title="Cooldown to unstake" />
161        {isWrongNetwork && !readOnlyModeAddress && (
162          <ChangeNetworkWarning networkName={networkConfig.name} chainId={stakingChain} />
163        )}
164        <Typography variant="description" sx={{ mb: 6 }}>
165          <Trans>
166            The cooldown period is {timeMessage(stakeCooldownSeconds)}. After{' '}
167            {timeMessage(stakeCooldownSeconds)} of cooldown, you will enter unstake window of{' '}
168            {timeMessage(stakeUnstakeWindow)}. You will continue receiving rewards during cooldown and
169            unstake window.
170          </Trans>{' '}
171          <Link
172            onClick={() =>
173              trackEvent(GENERAL.EXTERNAL_LINK, {
174                assetName: 'ABPT',
175                link: 'Cooldown Learn More',
176              })
177            }
178            variant="description"
179            href="https://docs.aave.com/faq/migration-and-staking"
180            sx={{ textDecoration: 'underline' }}
181          >
182            <Trans>Learn more</Trans>
183          </Link>
184          .
185        </Typography>
186  
187        <Box
188          sx={{
189            display: 'flex',
190            flexDirection: 'row',
191            width: '100%',
192            justifyContent: 'space-between',
193            pt: '6px',
194            pb: '30px',
195          }}
196        >
197          <Typography variant="description" color="text.primary">
198            <Trans>Amount to unstake</Trans>
199          </Typography>
200          <Box sx={{ display: 'flex', alignItems: 'center' }}>
201            <TokenIcon symbol={icon} sx={{ mr: 1, width: 14, height: 14 }} />
202            <FormattedNumber value={amountToCooldown} variant="secondary14" color="text.primary" />
203          </Box>
204        </Box>
205  
206        <Box
207          sx={{
208            display: 'flex',
209            flexDirection: 'row',
210            width: '100%',
211            justifyContent: 'space-between',
212            pt: '6px',
213            pb: '30px',
214          }}
215        >
216          <Typography variant="description" color="text.primary">
217            <Trans>Unstake window</Trans>
218          </Typography>
219          <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
220            <Box sx={{ display: 'flex', alignItems: 'center' }}>
221              <Typography variant="secondary14" component="span">
222                {dateMessage(stakeCooldownSeconds)}
223              </Typography>
224              <SvgIcon sx={{ fontSize: '13px', mx: 1 }}>
225                <ArrowNarrowRightIcon />
226              </SvgIcon>
227              <Typography variant="secondary14" component="span">
228                {dateMessage(stakeCooldownSeconds + stakeUnstakeWindow)}
229              </Typography>
230            </Box>
231            <Link
232              href={googleCalendarUrl}
233              target="_blank"
234              rel="noopener noreferrer"
235              sx={{ display: 'flex', alignItems: 'center', mt: 1 }}
236            >
237              <Trans>Remind me</Trans>
238              <SvgIcon sx={{ fontSize: '16px', ml: 1 }}>
239                <CalendarIcon />
240              </SvgIcon>
241            </Link>
242          </Box>
243        </Box>
244  
245        <Box mb={6}>
246          <Box
247            sx={{
248              width: `${unstakeWindowLineWidth}%`,
249              display: 'flex',
250              alignItems: 'center',
251              justifyContent: 'center',
252              textAlign: 'center',
253              flexDirection: 'column',
254              ml: 'auto',
255            }}
256          >
257            <Typography variant="helperText" mb={1}>
258              <Trans>You unstake here</Trans>
259            </Typography>
260            <SvgIcon sx={{ fontSize: '13px' }}>
261              <ArrowDownIcon />
262            </SvgIcon>
263          </Box>
264  
265          <Box sx={{ display: 'flex', alignItems: 'center', my: 3 }}>
266            <Box
267              sx={{
268                height: '2px',
269                width: `${cooldownLineWidth}%`,
270                bgcolor: 'error.main',
271                position: 'relative',
272                '&:after': {
273                  content: "''",
274                  position: 'absolute',
275                  left: 0,
276                  top: '50%',
277                  transform: 'translateY(-50%)',
278                  bgcolor: 'error.main',
279                  width: '2px',
280                  height: '8px',
281                  borderRadius: '2px',
282                },
283              }}
284            />
285            <Box
286              sx={{
287                height: '2px',
288                width: `${unstakeWindowLineWidth}%`,
289                bgcolor: 'success.main',
290                position: 'relative',
291                '&:after, &:before': {
292                  content: "''",
293                  position: 'absolute',
294                  top: '50%',
295                  transform: 'translateY(-50%)',
296                  bgcolor: 'success.main',
297                  width: '2px',
298                  height: '8px',
299                  borderRadius: '2px',
300                },
301                '&:before': {
302                  left: 0,
303                },
304                '&:after': {
305                  right: 0,
306                },
307              }}
308            />
309          </Box>
310          <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
311            <Box>
312              <Typography variant="helperText" mb={1}>
313                <Trans>Cooldown period</Trans>
314              </Typography>
315              <Typography variant="subheader2" color="error.main">
316                <Trans>{timeMessage(stakeCooldownSeconds)}</Trans>
317              </Typography>
318            </Box>
319            <Box sx={{ textAlign: 'right' }}>
320              <Typography variant="helperText" mb={1}>
321                <Trans>Unstake window</Trans>
322              </Typography>
323              <Typography variant="subheader2" color="success.main">
324                <Trans>{timeMessage(stakeUnstakeWindow)}</Trans>
325              </Typography>
326            </Box>
327          </Box>
328        </Box>
329  
330        {blockingError !== undefined && (
331          <Typography variant="helperText" color="red">
332            {handleBlocked()}
333          </Typography>
334        )}
335  
336        <Warning severity="error">
337          <Typography variant="caption">
338            <Trans>
339              If you DO NOT unstake within {timeMessage(stakeUnstakeWindow)} of unstake window, you
340              will need to activate cooldown process again.
341            </Trans>
342          </Typography>
343        </Warning>
344  
345        <GasStation chainId={ChainId.mainnet} gasLimit={parseUnits(gasLimit || '0', 'wei')} />
346  
347        <FormControlLabel
348          sx={{ mt: 12 }}
349          control={
350            <Checkbox
351              checked={cooldownCheck}
352              onClick={handleOnCoolDownCheckBox}
353              inputProps={{ 'aria-label': 'controlled' }}
354              data-cy={`cooldownAcceptCheckbox`}
355            />
356          }
357          label={
358            <Trans>
359              I understand how cooldown ({timeMessage(stakeCooldownSeconds)}) and unstaking (
360              {timeMessage(stakeUnstakeWindow)}) work
361            </Trans>
362          }
363        />
364  
365        {txError && <GasEstimationError txError={txError} />}
366  
367        <StakeCooldownActions
368          sx={{ mt: '48px' }}
369          isWrongNetwork={isWrongNetwork}
370          blocked={blockingError !== undefined || !cooldownCheck}
371          selectedToken={stakeAssetName}
372          amountToCooldown={amountToCooldown}
373        />
374      </>
375    );
376  };