src/pages/transactions/[txid].page.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import {
  GetServerSidePropsContext,
  GetServerSidePropsResult,
  InferGetServerSidePropsType,
} from "next";
import {
  Transaction,
  TransactionVin,
  TransactionVout,
} from "@defichain/whale-api-client/dist/api/transactions";
import { getWhaleApiClient } from "@contexts/WhaleContext";
import { Container } from "@components/commons/Container";
import BigNumber from "bignumber.js";
import { SmartBuffer } from "smart-buffer";
import {
  AccountToUtxos,
  CAccountToUtxos,
  DfTx,
  OP_DEFI_TX,
  toOPCodes,
} from "@defichain/jellyfish-transaction";
import { Head } from "@components/commons/Head";
import { useRouter } from "next/router";
import { useNetwork } from "@contexts/NetworkContext";
import { useMemo } from "react";
import { checkIfEvmTx } from "../../utils/commons/evmtx/checkIfEvmTx";
import {
  TransactionHeading,
  TransactionNotFoundHeading,
} from "./_components/TransactionHeadings";
import { TransactionVinVout } from "./_components/TransactionVinVout";
import { TransactionSummaryTable } from "./_components/TransactionSummaryTable";
import { TransactionDfTx } from "./_components/TransactionDfTx";
import { isAlphanumeric } from "../../utils/commons/StringValidator";
import { RawTransaction } from "./_components/RawTransaction";
import { RawAccountHistory } from "./_components/RawAccountHistory";
import { VmmapResult, VmmapTypes } from "./enum/VmmapTypes";
import { getMetaScanTxUrl } from "../../utils/commons/getNetworkParams";

interface TransactionPageProps {
  txid: string;
  transaction?: Transaction;
  vins?: TransactionVin[];
  vouts?: TransactionVout[];
  mappedEvmTxId?: string | null;
}

export default function TransactionPage(
  props: InferGetServerSidePropsType<typeof getServerSideProps>,
): JSX.Element {
  const router = useRouter();
  const network = useNetwork().connection;

  const mappedEvmTxId = props.mappedEvmTxId;
  const metachainTxUrl = useMemo(() => {
    if (mappedEvmTxId) {
      return getMetaScanTxUrl(network, mappedEvmTxId);
    }
    return undefined;
  }, [mappedEvmTxId, network]);

  const transactionPending =
    props.transaction === undefined ||
    props.vins === undefined ||
    props.vouts === undefined;
  if (router.query.rawtx !== undefined && transactionPending) {
    return (
      <Container className="pt-12 pb-20">
        <RawTransaction rawTx={router.query.rawtx as string} />
      </Container>
    );
  }

  if (
    props.transaction === undefined ||
    props.vins === undefined ||
    props.vouts === undefined
  ) {
    return (
      <Container className="pt-12 pb-20">
        <TransactionNotFoundHeading txid={props.txid} />
      </Container>
    );
  }

  const dftx: DfTx<any> | undefined = getDfTx(props.vouts);
  const isDeFiTransaction = dftx !== undefined;
  const fee = getTransactionFee(props.transaction, props.vins, dftx);
  const feeRate = fee.multipliedBy(100000000).dividedBy(props.transaction.size);
  const isEvmTx = checkIfEvmTx({
    transaction: props.transaction,
    vins: props.vins,
    vouts: props.vouts,
    dftxType: dftx?.type,
  });

  return (
    <>
      <Head title={`Transaction #${props.transaction.txid}`} />

      <Container className="pt-12 pb-20">
        <div className="lg:flex flex-col lg:flex-row items-end lg:justify-between lg:gap-14 w-full">
          <TransactionHeading
            transaction={props.transaction}
            metachainTxUrl={metachainTxUrl}
          />
        </div>
        <TransactionSummaryTable
          transaction={props.transaction}
          vins={props.vins}
          vouts={props.vouts}
          fee={fee}
          feeRate={feeRate}
          isDeFiTransaction={isDeFiTransaction}
          isEvmTx={isEvmTx}
        />
        <TransactionVinVout
          transaction={props.transaction}
          vins={props.vins}
          vouts={props.vouts}
          fee={fee}
          dftxName={dftx?.name}
          isEvmTx={isEvmTx}
        />
        <TransactionDfTx dftx={dftx} transaction={props.transaction} />
        <RawAccountHistory transaction={props.transaction} />
      </Container>
    </>
  );
}

function getTransactionFee(
  transaction: Transaction,
  vins: TransactionVin[],
  dftx?: DfTx<any>,
): BigNumber {
  if (dftx === undefined || dftx.type !== CAccountToUtxos.OP_CODE) {
    return new BigNumber(
      getTotalVinsValue(vins).minus(transaction.totalVoutValue),
    );
  }

  // AccountToUtxos
  const accountToUtxos = dftx as DfTx<AccountToUtxos>;
  const sumOfInputs = getTotalVinsValue(vins);
  const sumOfAccountInputs = accountToUtxos.data.balances
    .map((balance) => balance.amount)
    .reduce((a, b) => a.plus(b));
  return new BigNumber(
    sumOfInputs.plus(sumOfAccountInputs).minus(transaction.totalVoutValue),
  );
}

function getTotalVinsValue(vins: TransactionVin[]): BigNumber {
  let value: BigNumber = new BigNumber(0);
  vins.forEach((vin) => {
    if (vin.vout !== undefined) {
      value = new BigNumber(vin.vout.value).plus(value);
    }
  });
  return value;
}

function getDfTx(vouts: TransactionVout[]): DfTx<any> | undefined {
  const hex = vouts[0].script.hex;
  const buffer = SmartBuffer.fromBuffer(Buffer.from(hex, "hex"));
  const stack = toOPCodes(buffer);
  if (stack.length !== 2 || stack[1].type !== "OP_DEFI_TX") {
    return undefined;
  }
  return (stack[1] as OP_DEFI_TX).tx;
}

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

  if (!isAlphanumeric(txid)) {
    return { notFound: true };
  }

  let transaction: Transaction | undefined;

  try {
    transaction = await api.transactions.get(txid);
  } catch (e) {
    return {
      props: {
        txid: txid,
      },
    };
  }

  // Will improve with newer iteration of whale api
  async function getVins(): Promise<TransactionVin[]> {
    const vins: TransactionVin[] = [];
    let vinsResponse = await api.transactions.getVins(txid, 100);
    vins.push(...vinsResponse);
    while (vinsResponse.hasNext) {
      vinsResponse = await api.transactions.getVins(
        txid,
        100,
        vinsResponse.nextToken,
      );
      vins.push(...vinsResponse);
    }
    return vins;
  }

  async function getVouts(): Promise<TransactionVout[]> {
    const vouts: TransactionVout[] = [];
    let voutsResponse = await api.transactions.getVouts(txid, 100);
    vouts.push(...voutsResponse);
    while (voutsResponse.hasNext) {
      voutsResponse = await api.transactions.getVouts(
        txid,
        100,
        voutsResponse.nextToken,
      );
      vouts.push(...voutsResponse);
    }
    return vouts;
  }

  async function getEvmTxDetails() {
    try {
      const vmmap: VmmapResult = await api.rpc.call(
        "vmmap",
        [txid, VmmapTypes.TxHashDVMToEVM],
        "lossless",
      );
      return vmmap.output;
    } catch (e) {
      return null;
    }
  }

  try {
    return {
      props: {
        txid: txid,
        transaction: transaction,
        vins: await getVins(),
        vouts: await getVouts(),
        mappedEvmTxId: await getEvmTxDetails(),
      },
    };
  } catch (e) {
    return {
      props: {
        txid: txid,
      },
    };
  }
}