antoncoding/monarch

View on GitHub
app/rewards/components/RewardContent.tsx

Summary

Maintainability
A
0 mins
Test Coverage
'use client';

import { useMemo } from 'react';
import { Table, TableHeader, TableBody, TableColumn, TableRow, TableCell } from '@nextui-org/table';
import { ExternalLinkIcon } from '@radix-ui/react-icons';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { toast } from 'react-toastify';
import { Address } from 'viem';
import { useAccount, useSwitchChain } from 'wagmi';
import Header from '@/components/layout/header/Header';
import EmptyScreen from '@/components/Status/EmptyScreen';
import LoadingScreen from '@/components/Status/LoadingScreen';
import useMarkets from '@/hooks/useMarkets';
import useUserRewards from '@/hooks/useRewards';

import { useTransactionWithToast } from '@/hooks/useTransactionWithToast';
import { formatReadable, formatBalance } from '@/utils/balance';
import { getMarketURL } from '@/utils/external';
import { findToken } from '@/utils/tokens';

export default function Rewards() {
  const { account } = useParams<{ account: string }>();

  const { loading, data: markets } = useMarkets();
  const { rewards, distributions, loading: loadingRewards } = useUserRewards(account);

  const { chainId } = useAccount();

  const { sendTransaction } = useTransactionWithToast({
    toastId: 'claim',
    pendingText: 'Claiming Reward...',
    successText: 'Reward Claimed!',
    errorText: 'Failed to claim rewards',
    chainId,
    pendingDescription: `Claiming rewards`,
    successDescription: `Successfully claimed rewards`,
  });

  // all rewards returned as "rewards", not necessarily in distributions (might not be claimable)
  const allRewardTokens = useMemo(
    () =>
      rewards.reduce(
        (
          entries: {
            token: string;
            claimed: bigint;
            claimable: bigint;
            pending: bigint;
            chainId: number;
          }[],
          reward,
        ) => {
          const idx = entries.findIndex((e) => e.token === reward.program.asset.address);
          if (idx === -1) {
            return [
              ...entries,
              {
                token: reward.program.asset.address,
                claimed: BigInt(reward.for_supply?.claimed ?? '0'),
                claimable: BigInt(reward.for_supply?.claimable_now ?? '0'),
                pending: BigInt(reward.for_supply?.claimable_next ?? '0'),
                chainId: reward.program.chain_id,
              },
            ];
          } else {
            // update existing entry
            entries[idx].claimed += BigInt(reward.for_supply?.claimed ?? '0');
            entries[idx].claimable += BigInt(reward.for_supply?.claimable_now ?? '0');
            entries[idx].pending += BigInt(reward.for_supply?.claimable_next ?? '0');
            return entries;
          }
        },
        [],
      ),
    [rewards],
  );

  const marketsWithRewards = useMemo(
    () =>
      markets.filter((market) =>
        rewards.some((reward) => reward.program.market_id === market.uniqueKey),
      ),
    [markets, rewards],
  );

  const { switchChain } = useSwitchChain();

  return (
    <div className="flex flex-col justify-between font-zen">
      <Header />
      <div className="container mt-4 gap-8" style={{ padding: '0 5%' }}>
        {allRewardTokens.map((tokenReward) => {
          const matchedToken = findToken(tokenReward.token, tokenReward.chainId);
          const distribution = distributions.find(
            (d) => d.asset.address.toLowerCase() === tokenReward.token.toLowerCase(),
          );
          if (!matchedToken) return null;
          return (
            <div className="flex flex-col gap-2 p-2" key={`div-${tokenReward.token}`}>
              <div key={`table-${tokenReward.token}`}>
                {/* title and claim button */}
                <div className="flex items-center justify-between" key={`dis-${tokenReward.token}`}>
                  <div className="flex items-center justify-center gap-2 p-2">
                    <h1 className="py-2 font-zen text-xl"> {matchedToken.symbol} Rewards </h1>
                    {matchedToken.img && (
                      <Image src={matchedToken.img} alt="icon" width="20" height="20" />
                    )}
                  </div>

                  <button
                    type="button"
                    className="flex justify-center gap-2 rounded-sm bg-secondary p-2 font-zen text-sm opacity-80 transition-all duration-200 ease-in-out hover:opacity-100"
                    disabled={tokenReward.claimable === BigInt(0) || distribution === undefined}
                    onClick={() => {
                      if (!account) {
                        toast.error('Connect wallet');
                        return;
                      }
                      if (!distribution) {
                        toast.error('No claim data');
                        return;
                      }
                      if (chainId !== distribution.distributor.chain_id) {
                        switchChain({ chainId: tokenReward.chainId });
                        toast('Click on claim again after switching network');
                        return;
                      }
                      sendTransaction({
                        account: account as Address,
                        to: distribution.distributor.address as Address,
                        data: distribution.tx_data as `0x${string}`,
                        chainId: distribution.distributor.chain_id,
                      });
                    }}
                  >
                    Claim{' '}
                    {matchedToken.img && (
                      <Image src={matchedToken.img} alt="icon" width="20" height="20" />
                    )}
                  </button>
                </div>

                <div className="my-4 flex gap-4">
                  {/* box 1, claimable */}
                  <div className="flex flex-col gap-2 rounded-sm bg-secondary p-4 px-8">
                    <p className="text-sm"> Total Claimable </p>
                    <div className="flex items-center justify-center gap-2">
                      <p className="text-base">
                        {' '}
                        {formatReadable(
                          formatBalance(tokenReward.claimable, matchedToken.decimals),
                        )}{' '}
                      </p>
                      {matchedToken.img && (
                        <Image src={matchedToken.img} alt="icon" width="15" height="15" />
                      )}
                    </div>
                  </div>

                  <div className="flex flex-col gap-2 rounded-sm bg-secondary p-4 px-8">
                    <p className="text-sm"> Total Pending </p>
                    <div className="flex items-center justify-center gap-2">
                      <p className="text-base">
                        {' '}
                        {formatReadable(
                          formatBalance(tokenReward.pending, matchedToken.decimals),
                        )}{' '}
                      </p>
                      {matchedToken.img && (
                        <Image src={matchedToken.img} alt="icon" width="15" height="15" />
                      )}
                    </div>
                  </div>

                  <div className="flex flex-col gap-2 rounded-sm bg-secondary p-4 px-8">
                    <p className="text-sm"> Total Claimed </p>
                    <div className="flex items-center justify-center gap-2">
                      <p className="text-base">
                        {' '}
                        {formatReadable(
                          formatBalance(tokenReward.claimed, matchedToken.decimals),
                        )}{' '}
                      </p>
                      {matchedToken.img && (
                        <Image src={matchedToken.img} alt="icon" width="15" height="15" />
                      )}
                    </div>
                  </div>
                </div>

                <div className="mb-6 mt-2 bg-secondary">
                  <Table
                    classNames={{
                      th: 'bg-secondary',
                      wrapper: 'rounded-none shadow-none bg-secondary',
                    }}
                  >
                    <TableHeader className="table-header">
                      <TableColumn> Market ID </TableColumn>
                      <TableColumn> Loan Asset </TableColumn>
                      <TableColumn> Collateral </TableColumn>
                      <TableColumn> LLTV </TableColumn>
                      <TableColumn> Claimable Reward </TableColumn>
                      <TableColumn> Pending Reward </TableColumn>
                    </TableHeader>
                    <TableBody>
                      {marketsWithRewards
                        .filter((m) =>
                          rewards.find(
                            (r) =>
                              r.program.market_id.toLowerCase() === m.uniqueKey.toLowerCase() &&
                              r.program.asset.address.toLowerCase() ===
                                tokenReward.token.toLowerCase(),
                          ),
                        )
                        .map((market, index) => {
                          const collatImg = findToken(
                            market.collateralAsset.address,
                            market.morphoBlue.chain.id,
                          )?.img;
                          const loanImg = findToken(
                            market.loanAsset.address,
                            market.morphoBlue.chain.id,
                          )?.img;

                          const tokenRewardsForMarket = rewards.filter((reward) => {
                            return (
                              reward.program.market_id === market.uniqueKey &&
                              reward.program.asset.address.toLowerCase() ===
                                tokenReward.token.toLowerCase()
                            );
                          });

                          const hasRewards = tokenRewardsForMarket.length !== 0;

                          const claimable = tokenRewardsForMarket.reduce((a: bigint, b) => {
                            return a + BigInt(b.for_supply?.claimable_now ?? '0');
                          }, BigInt(0));
                          const pending = tokenRewardsForMarket.reduce((a: bigint, b) => {
                            return a + BigInt(b.for_supply?.claimable_next ?? '0');
                          }, BigInt(0));

                          return (
                            <TableRow key={index.toFixed()}>
                              {/* id */}
                              <TableCell>
                                <div className="flex justify-center">
                                  <a
                                    className="group flex items-center gap-1 no-underline hover:underline"
                                    href={getMarketURL(
                                      market.uniqueKey,
                                      market.morphoBlue.chain.id,
                                    )}
                                    target="_blank"
                                  >
                                    <p>{market.uniqueKey.slice(2, 8)} </p>
                                    <p className="opacity-0 group-hover:opacity-100">
                                      <ExternalLinkIcon />
                                    </p>
                                  </a>
                                </div>
                              </TableCell>

                              {/* supply */}
                              <TableCell>
                                <div>
                                  <div className="flex items-center justify-center gap-1">
                                    <p> {market.loanAsset.symbol} </p>
                                    {loanImg ? (
                                      <Image src={loanImg} alt="icon" width="18" height="18" />
                                    ) : null}
                                  </div>
                                </div>
                              </TableCell>

                              {/* collateral */}
                              <TableCell>
                                <div className="flex items-center justify-center gap-1">
                                  <div> {market.collateralAsset.symbol} </div>
                                  {collatImg ? (
                                    <Image src={collatImg} alt="icon" width="18" height="18" />
                                  ) : null}
                                  <p> {} </p>
                                </div>
                              </TableCell>

                              <TableCell>
                                <div className="flex items-center justify-center gap-1">
                                  <p> {formatBalance(market.lltv, 16)} % </p>
                                </div>
                              </TableCell>

                              <TableCell>
                                <div className="flex items-center justify-center gap-1">
                                  {hasRewards && (
                                    <p>
                                      {' '}
                                      {formatReadable(
                                        formatBalance(claimable, matchedToken.decimals),
                                      )}{' '}
                                    </p>
                                  )}
                                  {hasRewards && matchedToken.img && (
                                    <Image
                                      src={matchedToken.img}
                                      alt="icon"
                                      width="18"
                                      height="18"
                                    />
                                  )}
                                  {!hasRewards && <p> - </p>}
                                </div>
                              </TableCell>

                              <TableCell>
                                <div className="flex items-center justify-center gap-1">
                                  {hasRewards && (
                                    <p>
                                      {' '}
                                      {formatReadable(
                                        formatBalance(pending, matchedToken.decimals),
                                      )}{' '}
                                    </p>
                                  )}
                                  {hasRewards && matchedToken.img && (
                                    <Image
                                      src={matchedToken.img}
                                      alt="icon"
                                      width="18"
                                      height="18"
                                    />
                                  )}
                                  {!hasRewards && <p> - </p>}
                                </div>
                              </TableCell>
                            </TableRow>
                          );
                        })}
                    </TableBody>
                  </Table>
                </div>
              </div>
            </div>
          );
        })}

        {loading || loadingRewards ? (
          <LoadingScreen message="Loading Rewards..." />
        ) : markets.length === 0 ? (
          <EmptyScreen message="No rewards" />
        ) : (
          <div> </div>
        )}
      </div>
    </div>
  );
}