solana/explorer/src/components/instruction/token/TokenDetailsCard.tsx

147 lines
4.1 KiB
TypeScript

import React from "react";
import * as Sentry from "@sentry/react";
import { coerce } from "superstruct";
import {
SignatureResult,
ParsedTransaction,
PublicKey,
ParsedInstruction,
} from "@solana/web3.js";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import { InstructionCard } from "../InstructionCard";
import { Address } from "components/common/Address";
import { IX_STRUCTS, TokenInstructionType, IX_TITLES } from "./types";
import { ParsedInfo } from "validators";
import {
useTokenAccountInfo,
useMintAccountInfo,
useFetchAccountInfo,
} from "providers/accounts";
import { normalizeTokenAmount } from "utils";
type DetailsProps = {
tx: ParsedTransaction;
ix: ParsedInstruction;
result: SignatureResult;
index: number;
};
export function TokenDetailsCard(props: DetailsProps) {
try {
const parsed = coerce(props.ix.parsed, ParsedInfo);
const { type: rawType, info } = parsed;
const type = coerce(rawType, TokenInstructionType);
const title = `Token: ${IX_TITLES[type]}`;
const coerced = coerce(info, IX_STRUCTS[type] as any);
return <TokenInstruction title={title} info={coerced} {...props} />;
} catch (err) {
Sentry.captureException(err, {
tags: {
signature: props.tx.signatures[0],
},
});
return <UnknownDetailsCard {...props} />;
}
}
type InfoProps = {
ix: ParsedInstruction;
info: any;
result: SignatureResult;
index: number;
title: string;
};
function TokenInstruction(props: InfoProps) {
const { mintAddress: infoMintAddress, tokenAddress } = React.useMemo(() => {
let mintAddress: string | undefined;
let tokenAddress: string | undefined;
// No sense fetching accounts if we don't need to convert an amount
if (!("amount" in props.info)) return {};
if ("mint" in props.info && props.info.mint instanceof PublicKey) {
mintAddress = props.info.mint.toBase58();
} else if (
"account" in props.info &&
props.info.account instanceof PublicKey
) {
tokenAddress = props.info.account.toBase58();
} else if (
"source" in props.info &&
props.info.source instanceof PublicKey
) {
tokenAddress = props.info.source.toBase58();
}
return {
mintAddress,
tokenAddress,
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const tokenInfo = useTokenAccountInfo(tokenAddress);
const mintAddress = infoMintAddress || tokenInfo?.mint.toBase58();
const mintInfo = useMintAccountInfo(mintAddress);
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (tokenAddress && !tokenInfo) {
fetchAccountInfo(new PublicKey(tokenAddress));
}
}, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (mintAddress && !mintInfo) {
fetchAccountInfo(new PublicKey(mintAddress));
}
}, [fetchAccountInfo, mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
const decimals = mintInfo?.decimals;
const attributes = [];
for (let key in props.info) {
const value = props.info[key];
if (value === undefined) continue;
let tag;
let labelSuffix = "";
if (value instanceof PublicKey) {
tag = <Address pubkey={value} alignRight link />;
} else if (key === "amount") {
let amount;
if (decimals === undefined) {
labelSuffix = " (raw)";
amount = new Intl.NumberFormat("en-US").format(value);
} else {
amount = new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(normalizeTokenAmount(value, decimals));
}
tag = <>{amount}</>;
} else {
tag = <>{value}</>;
}
let label = key.charAt(0).toUpperCase() + key.slice(1) + labelSuffix;
attributes.push(
<tr key={key}>
<td>{label}</td>
<td className="text-lg-right">{tag}</td>
</tr>
);
}
return (
<InstructionCard
ix={props.ix}
index={props.index}
result={props.result}
title={props.title}
>
{attributes}
</InstructionCard>
);
}