src/pages/tokens/[id].page.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { AdaptiveList } from "@components/commons/AdaptiveList";
import { Breadcrumb } from "@components/commons/Breadcrumb";
import { getAssetIcon, getTokenIcon } from "@components/icons/assets/tokens";
import { getWhaleApiClient, useWhaleApiClient } from "@contexts/WhaleContext";
import { TokenData } from "@defichain/whale-api-client/dist/api/tokens";
import {
  GetServerSidePropsContext,
  GetServerSidePropsResult,
  InferGetServerSidePropsType,
} from "next";
import { IoAlertCircleOutline, IoCheckmarkCircle } from "react-icons/io5";
import { Container } from "@components/commons/Container";
import { AddressLinkExternal } from "@components/commons/link/AddressLink";
import { TxIdLink } from "@components/commons/link/TxIdLink";
import React, { useEffect, useState } from "react";
import BigNumber from "bignumber.js";
import { NumericFormat } from "react-number-format";
import { Head } from "@components/commons/Head";
import { WhaleApiClient } from "@defichain/whale-api-client";
import {
  TOKEN_BACKED,
  TOKEN_BACKED_ADDRESS,
} from "constants/TokenBackedAddress";
import { getTokenName } from "../../utils/commons/token/getTokenName";
import { isAlphanumeric, isNumeric } from "../../utils/commons/StringValidator";
import { getAllTokens } from "./shared/getAllTokens";

interface TokenAssetPageProps {
  token: TokenData;
}

export default function TokenIdPage(
  props: InferGetServerSidePropsType<typeof getServerSideProps>,
): JSX.Element {
  const api = useWhaleApiClient();
  const [burnedAmount, setBurnedAmount] = useState<BigNumber | undefined>();
  const [netSupply, setNetSupply] = useState<BigNumber | undefined>();

  useEffect(() => {
    api.address
      .listToken("8defichainBurnAddressXXXXXXXdRQkSm", 200)
      .then((data) => {
        const burntToken = data.find(
          (token) => token.symbol === props.token.symbol,
        );

        if (
          props.token.isDAT &&
          !props.token.isLPS &&
          props.token.symbol !== "DFI"
        ) {
          if (burntToken !== undefined) {
            setBurnedAmount(new BigNumber(burntToken.amount));
            setNetSupply(
              new BigNumber(props.token.minted).minus(burntToken.amount),
            );
          } else {
            setBurnedAmount(new BigNumber(0));
            setNetSupply(new BigNumber(props.token.minted));
          }
        }
      })
      .catch(() => {
        if (
          props.token.isDAT &&
          !props.token.isLPS &&
          props.token.symbol !== "DFI"
        ) {
          setBurnedAmount(undefined);
          setNetSupply(new BigNumber(props.token.minted));
        }
      });
  }, []);

  return (
    <>
      <Head title={`${props.token.displaySymbol}`} />
      <Container className="pt-4 pb-20">
        <TokenPageHeading token={props.token} />
        <div className="flex flex-col space-y-6 mt-6 items-start lg:flex-row lg:space-x-8 lg:space-y-0">
          <ListLeft
            token={props.token}
            burnedAmount={burnedAmount}
            netSupply={netSupply}
          />
          <ListRight token={props.token} />
        </div>
      </Container>
    </>
  );
}

function TokenPageHeading({ token }: { token: TokenData }): JSX.Element {
  const name = getTokenName(token);

  return (
    <div>
      <Breadcrumb
        items={[
          {
            path: "/tokens",
            name: "Tokens",
          },
          {
            path: `/tokens/${
              token.isDAT || token.isLPS
                ? token.displaySymbol
                : token.displaySymbol.concat("-", token.id)
            }`,
            name: `${name}`,
            canonical: true,
            isCurrentPath: true,
          },
        ]}
      />

      {(() => {
        const Icon = token.isDAT
          ? getAssetIcon(token.symbol)
          : getTokenIcon(token.symbol);

        return (
          <div className="flex flex-row flex-wrap items-center mt-8">
            <Icon className="h-10 w-10 mr-4" />
            <h1
              data-testid="PageHeading"
              className="text-2xl font-semibold dark:text-dark-gray-900"
            >
              {name}
            </h1>
          </div>
        );
      })()}
    </div>
  );
}

function ListRight({ token }: { token: TokenData }): JSX.Element {
  return (
    <AdaptiveList>
      <AdaptiveList.Row name="Decimal">{token.decimal} Places</AdaptiveList.Row>
      <AdaptiveList.Row name="Limit">{token.limit}</AdaptiveList.Row>
      <AdaptiveList.Row name="LPS">
        {token.isLPS ? "Yes" : "No"}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Tradable">
        {(() => {
          if (token.tradeable) {
            return (
              <div className="flex flex-wrap items-center">
                <div>Yes</div>
                <IoCheckmarkCircle className="h-4 w-4 text-green-500 ml-1" />
              </div>
            );
          }
          return (
            <div className="flex flex-wrap items-center">
              <div>No</div>
              <IoAlertCircleOutline className="h-4 w-4 text-gray-500 ml-1" />
            </div>
          );
        })()}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Finalized">
        {token.finalized ? "Yes" : "No"}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Destruction Height">
        {token.destruction.height}
      </AdaptiveList.Row>
      <AdaptiveList.Row
        name="Destruction TX"
        className="flex space-x-10 items-center"
      >
        <div className="break-all">{token.destruction.tx}</div>
      </AdaptiveList.Row>
      <BackingAddress tokenSymbol={token.symbol} />
    </AdaptiveList>
  );
}

function ListLeft({
  token,
  burnedAmount,
  netSupply,
}: {
  token: TokenData;
  burnedAmount?: BigNumber;
  netSupply?: BigNumber;
}): JSX.Element {
  return (
    <AdaptiveList>
      <AdaptiveList.Row name="Category">
        {(() => {
          if (token.isLPS) {
            return "LPS";
          }

          if (token.isDAT) {
            return "DAT";
          }

          return "DCT";
        })()}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Symbol">{token.displaySymbol}</AdaptiveList.Row>
      <AdaptiveList.Row name="Mintable">
        {(() => {
          if (token.mintable) {
            return (
              <div className="flex flex-wrap items-center">
                <div>Yes</div>
                <IoCheckmarkCircle className="h-4 w-4 text-green-500 ml-1" />
              </div>
            );
          }
          return (
            <div className="flex flex-wrap items-center">
              <div>No</div>
              <IoAlertCircleOutline className="h-4 w-4 text-gray-500 ml-1" />
            </div>
          );
        })()}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Minted">
        <NumericFormat
          displayType="text"
          thousandSeparator
          value={new BigNumber(token.minted).toFixed(8)}
          decimalScale={8}
        />
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Burned">
        {burnedAmount === undefined ? (
          "N/A"
        ) : (
          <NumericFormat
            displayType="text"
            thousandSeparator
            value={burnedAmount.toFixed(8)}
            decimalScale={8}
          />
        )}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Net Supply">
        {netSupply === undefined ? (
          "N/A"
        ) : (
          <NumericFormat
            displayType="text"
            thousandSeparator
            value={netSupply.toFixed(8)}
            decimalScale={8}
          />
        )}
      </AdaptiveList.Row>
      <AdaptiveList.Row name="Creation Height">
        <NumericFormat
          displayType="text"
          thousandSeparator
          value={token.creation.height}
          decimalScale={8}
        />
      </AdaptiveList.Row>
      <AdaptiveList.Row
        name="Creation Tx"
        className="flex space-x-10 items-center"
      >
        <TxIdLink txid={token.creation.tx} className="break-all" />
      </AdaptiveList.Row>
    </AdaptiveList>
  );
}

function BackingAddress({ tokenSymbol }: { tokenSymbol: string }): JSX.Element {
  if (!TOKEN_BACKED.map((token) => token.symbol).includes(tokenSymbol)) {
    return <></>;
  }

  return (
    <AdaptiveList.Row name="Backing Address" className="break-all">
      {(() => {
        switch (tokenSymbol) {
          case "BCH":
            if (TOKEN_BACKED_ADDRESS.BCH.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.BCH.cake.link}
                  text={TOKEN_BACKED_ADDRESS.BCH.cake.address}
                  testId="BackingAddress.BCH"
                />
              );
            }
            break;
          case "LTC":
            if (TOKEN_BACKED_ADDRESS.LTC.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.LTC.cake.link}
                  text={TOKEN_BACKED_ADDRESS.LTC.cake.address}
                  testId="BackingAddress.LTC"
                />
              );
            }
            break;
          case "DOGE":
            if (TOKEN_BACKED_ADDRESS.DOGE.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.DOGE.cake.link}
                  text={TOKEN_BACKED_ADDRESS.DOGE.cake.address}
                  testId="BackingAddress.DOGE"
                />
              );
            }
            break;
          case "BTC":
            if (TOKEN_BACKED_ADDRESS.BTC.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.BTC.cake.link}
                  text={TOKEN_BACKED_ADDRESS.BTC.cake.address}
                  testId="BackingAddress.BTC"
                />
              );
            }
            break;
          case "MATIC":
            if (TOKEN_BACKED_ADDRESS.MATIC.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.MATIC.cake.link}
                  text={TOKEN_BACKED_ADDRESS.MATIC.cake.address}
                  testId="BackingAddress.MATIC"
                />
              );
            }
            break;
          case "SOL":
            if (TOKEN_BACKED_ADDRESS.SOL.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.SOL.cake.link}
                  text={TOKEN_BACKED_ADDRESS.SOL.cake.address}
                  testId="BackingAddress.SOL"
                />
              );
            }
            break;
          case "DOT":
            if (TOKEN_BACKED_ADDRESS.DOT.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.DOT.cake.link}
                  text={TOKEN_BACKED_ADDRESS.DOT.cake.address}
                  testId="BackingAddress.DOT"
                />
              );
            }
            break;
          case "SUI":
            if (TOKEN_BACKED_ADDRESS.SUI.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.SUI.cake.link}
                  text={TOKEN_BACKED_ADDRESS.SUI.cake.address}
                  testId="BackingAddress.SUI"
                />
              );
            }
            break;
          case "ETH":
          case "USDC":
          case "USDT":
          case "EUROC":
            if (TOKEN_BACKED_ADDRESS.ETH.cake) {
              return (
                <AddressLinkExternal
                  url={TOKEN_BACKED_ADDRESS.ETH.cake.link}
                  text={TOKEN_BACKED_ADDRESS.ETH.cake.address}
                  testId="BackingAddress.ETH"
                />
              );
            }
            break;
        }
      })()}
    </AdaptiveList.Row>
  );
}

async function getTokenByParam(
  param: string,
  api: WhaleApiClient,
): Promise<TokenData | undefined> {
  const tokenList: TokenData[] = await getAllTokens(api);

  return tokenList.find((t) => {
    if (t.isDAT || t.isLPS) {
      return t.displaySymbol.toLowerCase() === param.toLowerCase();
    }
    const i = param.lastIndexOf("-");
    const displaySymbol = param.substring(0, i);
    const id = param.substring(i + 1);
    return (
      t.displaySymbol.toLowerCase() === displaySymbol.toLowerCase() &&
      id === t.id
    );
  });
}

export async function getServerSideProps(
  context: GetServerSidePropsContext,
): Promise<GetServerSidePropsResult<TokenAssetPageProps>> {
  const api = getWhaleApiClient(context);
  const param = context.params?.id?.toString().trim() as string;

  if (!isAlphanumeric(param, "-")) {
    return { notFound: true };
  }

  let token: TokenData | undefined;
  if (isNumeric(param)) {
    try {
      token = await api.tokens.get(param);
    } catch (e) {
      return { notFound: true };
    }
  } else {
    token = await getTokenByParam(param, api);
  }

  if (token === undefined) {
    return { notFound: true };
  }

  return {
    props: {
      token,
    },
  };
}