diff --git a/explorer/src/components/account/TokenAccountSection.tsx b/explorer/src/components/account/TokenAccountSection.tsx
index d0550d2880..9dca6aea44 100644
--- a/explorer/src/components/account/TokenAccountSection.tsx
+++ b/explorer/src/components/account/TokenAccountSection.tsx
@@ -10,6 +10,8 @@ import { coerce } from "superstruct";
import { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address";
import { UnknownAccountCard } from "./UnknownAccountCard";
+import { useFetchTokenSupply, useTokenSupply } from "providers/mints/supply";
+import { FetchStatus } from "providers/cache";
export function TokenAccountSection({
account,
@@ -44,7 +46,36 @@ function MintAccountCard({
account: Account;
info: MintAccountInfo;
}) {
- const refresh = useFetchAccountInfo();
+ const mintAddress = account.pubkey.toBase58();
+ const fetchInfo = useFetchAccountInfo();
+ const supply = useTokenSupply(mintAddress);
+ const fetchSupply = useFetchTokenSupply();
+ const refreshSupply = () => fetchSupply(account.pubkey);
+ const refresh = () => {
+ fetchInfo(account.pubkey);
+ refreshSupply();
+ };
+
+ let renderSupply;
+ const supplyTotal = supply?.data?.uiAmount;
+ if (supplyTotal === undefined) {
+ if (!supply || supply?.status === FetchStatus.Fetching) {
+ renderSupply = (
+ <>
+
+ Loading
+ >
+ );
+ } else {
+ renderSupply = "Fetch failed";
+ }
+ } else {
+ renderSupply = supplyTotal;
+ }
+
+ React.useEffect(() => {
+ if (!supply) refreshSupply();
+ }, [mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
return (
@@ -52,10 +83,7 @@ function MintAccountCard({
Token Mint Account
-
- {pubkey && }
- {pubkey && }
+ {!pubkey ? (
+
+ ) : (
+
+ )}
);
}
-function InfoSection({ pubkey }: { pubkey: PublicKey }) {
+function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
const fetchAccount = useFetchAccountInfo();
const address = pubkey.toBase58();
const info = useAccountInfo(address);
- const refresh = useFetchAccountInfo();
const { status } = useCluster();
+ const location = useLocation();
// Fetch account on load
React.useEffect(() => {
@@ -61,11 +62,53 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
info.status === FetchStatus.FetchFailed ||
info.data?.lamports === undefined
) {
- return refresh(pubkey)} text="Fetch Failed" />;
+ return fetchAccount(pubkey)} text="Fetch Failed" />;
}
const account = info.data;
const data = account?.details?.data;
+
+ let tabs: Tab[] = [
+ {
+ slug: "history",
+ title: "History",
+ path: "",
+ },
+ ];
+
+ if (data && data?.name === "spl-token") {
+ if (data.parsed.type === "mint") {
+ tabs.push({
+ slug: "holders",
+ title: "Holders",
+ path: "/holders",
+ });
+ }
+ } else {
+ tabs.push({
+ slug: "tokens",
+ title: "Tokens",
+ path: "/tokens",
+ });
+ }
+
+ let moreTab: MoreTabs = "history";
+ if (tab && tabs.filter(({ slug }) => slug === tab).length === 0) {
+ return ;
+ } else if (tab) {
+ moreTab = tab as MoreTabs;
+ }
+
+ return (
+ <>
+ {}
+ {}
+ >
+ );
+}
+
+function InfoSection({ account }: { account: Account }) {
+ const data = account?.details?.data;
if (data && data.name === "stake") {
let stakeAccountType, stakeAccount;
if ("accountType" in data.parsed) {
@@ -90,11 +133,24 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
}
}
-type MoreTabs = "history" | "tokens";
-function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
- const address = pubkey.toBase58();
- const info = useAccountInfo(address);
- if (info?.data === undefined) return null;
+type Tab = {
+ slug: MoreTabs;
+ title: string;
+ path: string;
+};
+
+type MoreTabs = "history" | "tokens" | "holders";
+function MoreSection({
+ account,
+ tab,
+ tabs,
+}: {
+ account: Account;
+ tab: MoreTabs;
+ tabs: Tab[];
+}) {
+ const pubkey = account.pubkey;
+ const address = account.pubkey.toBase58();
return (
<>
@@ -102,24 +158,17 @@ function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
- -
-
- History
-
-
- -
-
- Tokens
-
-
+ {tabs.map(({ title, path }) => (
+ -
+
+ {title}
+
+
+ ))}
@@ -131,6 +180,7 @@ function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
>
)}
{tab === "history" && }
+ {tab === "holders" && }
>
);
}
diff --git a/explorer/src/providers/mints/index.tsx b/explorer/src/providers/mints/index.tsx
new file mode 100644
index 0000000000..04d52557ee
--- /dev/null
+++ b/explorer/src/providers/mints/index.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { SupplyProvider } from "./supply";
+import { LargestAccountsProvider } from "./largest";
+
+type ProviderProps = { children: React.ReactNode };
+export function MintsProvider({ children }: ProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/explorer/src/providers/mints/largest.tsx b/explorer/src/providers/mints/largest.tsx
new file mode 100644
index 0000000000..bfacaf0ba0
--- /dev/null
+++ b/explorer/src/providers/mints/largest.tsx
@@ -0,0 +1,99 @@
+import React from "react";
+import { useCluster } from "providers/cluster";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
+import {
+ PublicKey,
+ Connection,
+ TokenAccountBalancePair,
+} from "@solana/web3.js";
+
+type LargestAccounts = {
+ largest: TokenAccountBalancePair[];
+};
+
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
+
+const StateContext = React.createContext(undefined);
+const DispatchContext = React.createContext(undefined);
+
+type ProviderProps = { children: React.ReactNode };
+export function LargestAccountsProvider({ children }: ProviderProps) {
+ const { url } = useCluster();
+ const [state, dispatch] = Cache.useReducer(url);
+
+ // Clear cache whenever cluster is changed
+ React.useEffect(() => {
+ dispatch({ type: ActionType.Clear, url });
+ }, [dispatch, url]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+async function fetchLargestAccounts(
+ dispatch: Dispatch,
+ pubkey: PublicKey,
+ url: string
+) {
+ dispatch({
+ type: ActionType.Update,
+ key: pubkey.toBase58(),
+ status: Cache.FetchStatus.Fetching,
+ url,
+ });
+
+ let data;
+ let fetchStatus;
+ try {
+ data = {
+ largest: (
+ await new Connection(url, "single").getTokenLargestAccounts(pubkey)
+ ).value,
+ };
+ fetchStatus = FetchStatus.Fetched;
+ } catch (error) {
+ fetchStatus = FetchStatus.FetchFailed;
+ }
+ dispatch({
+ type: ActionType.Update,
+ status: fetchStatus,
+ data,
+ key: pubkey.toBase58(),
+ url,
+ });
+}
+
+export function useFetchTokenLargestAccounts() {
+ const dispatch = React.useContext(DispatchContext);
+ if (!dispatch) {
+ throw new Error(
+ `useFetchTokenLargestAccounts must be used within a MintsProvider`
+ );
+ }
+
+ const { url } = useCluster();
+ return (pubkey: PublicKey) => {
+ fetchLargestAccounts(dispatch, pubkey, url);
+ };
+}
+
+export function useTokenLargestTokens(
+ address: string
+): Cache.CacheEntry | undefined {
+ const context = React.useContext(StateContext);
+
+ if (!context) {
+ throw new Error(
+ `useTokenLargestTokens must be used within a MintsProvider`
+ );
+ }
+
+ return context.entries[address];
+}
diff --git a/explorer/src/providers/mints/supply.tsx b/explorer/src/providers/mints/supply.tsx
new file mode 100644
index 0000000000..bd51070c50
--- /dev/null
+++ b/explorer/src/providers/mints/supply.tsx
@@ -0,0 +1,80 @@
+import React from "react";
+import { useCluster } from "providers/cluster";
+import * as Cache from "providers/cache";
+import { ActionType, FetchStatus } from "providers/cache";
+import { TokenAmount, PublicKey, Connection } from "@solana/web3.js";
+
+type State = Cache.State;
+type Dispatch = Cache.Dispatch;
+
+const StateContext = React.createContext(undefined);
+const DispatchContext = React.createContext(undefined);
+
+type ProviderProps = { children: React.ReactNode };
+export function SupplyProvider({ children }: ProviderProps) {
+ const { url } = useCluster();
+ const [state, dispatch] = Cache.useReducer(url);
+
+ // Clear cache whenever cluster is changed
+ React.useEffect(() => {
+ dispatch({ type: ActionType.Clear, url });
+ }, [dispatch, url]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+async function fetchSupply(dispatch: Dispatch, pubkey: PublicKey, url: string) {
+ dispatch({
+ type: ActionType.Update,
+ key: pubkey.toBase58(),
+ status: Cache.FetchStatus.Fetching,
+ url,
+ });
+
+ let data;
+ let fetchStatus;
+ try {
+ data = (await new Connection(url, "single").getTokenSupply(pubkey)).value;
+
+ fetchStatus = FetchStatus.Fetched;
+ } catch (error) {
+ fetchStatus = FetchStatus.FetchFailed;
+ }
+ dispatch({
+ type: ActionType.Update,
+ status: fetchStatus,
+ data,
+ key: pubkey.toBase58(),
+ url,
+ });
+}
+
+export function useFetchTokenSupply() {
+ const dispatch = React.useContext(DispatchContext);
+ if (!dispatch) {
+ throw new Error(`useFetchTokenSupply must be used within a MintsProvider`);
+ }
+
+ const { url } = useCluster();
+ return (pubkey: PublicKey) => {
+ fetchSupply(dispatch, pubkey, url);
+ };
+}
+
+export function useTokenSupply(
+ address: string
+): Cache.CacheEntry | undefined {
+ const context = React.useContext(StateContext);
+
+ if (!context) {
+ throw new Error(`useTokenSupply must be used within a MintsProvider`);
+ }
+
+ return context.entries[address];
+}