diff --git a/explorer/src/components/account/BlockhashesCard.tsx b/explorer/src/components/account/BlockhashesCard.tsx
new file mode 100644
index 0000000000..bbf5298b1c
--- /dev/null
+++ b/explorer/src/components/account/BlockhashesCard.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import {
+ RecentBlockhashesInfo,
+ RecentBlockhashesEntry,
+} from "validators/accounts/sysvar";
+
+export function BlockhashesCard({
+ blockhashes,
+}: {
+ blockhashes: RecentBlockhashesInfo;
+}) {
+ return (
+ <>
+
+
+
+
+
+
+
+ Recency |
+ Blockhash |
+ Fee Calculator |
+
+
+
+ {blockhashes.length > 0 &&
+ blockhashes.map((entry: RecentBlockhashesEntry, index) => {
+ return renderAccountRow(entry, index);
+ })}
+
+
+
+
+
+
+ {blockhashes.length > 0 ? "" : "No blockhashes found"}
+
+
+
+ >
+ );
+}
+
+const renderAccountRow = (entry: RecentBlockhashesEntry, index: number) => {
+ return (
+
+ {index + 1} |
+ {entry.blockhash} |
+
+ {entry.feeCalculator.lamportsPerSignature} lamports per signature
+ |
+
+ );
+};
diff --git a/explorer/src/components/account/ConfigAccountSection.tsx b/explorer/src/components/account/ConfigAccountSection.tsx
new file mode 100644
index 0000000000..86f7e7a587
--- /dev/null
+++ b/explorer/src/components/account/ConfigAccountSection.tsx
@@ -0,0 +1,158 @@
+import React from "react";
+import { Account, useFetchAccountInfo } from "providers/accounts";
+import { TableCardBody } from "components/common/TableCardBody";
+import {
+ ConfigAccount,
+ StakeConfigInfoAccount,
+ ValidatorInfoAccount,
+} from "validators/accounts/config";
+import {
+ AccountAddressRow,
+ AccountBalanceRow,
+ AccountHeader,
+} from "components/common/Account";
+import { PublicKey } from "@solana/web3.js";
+import { Address } from "components/common/Address";
+
+const MAX_SLASH_PENALTY = Math.pow(2, 8);
+
+export function ConfigAccountSection({
+ account,
+ configAccount,
+}: {
+ account: Account;
+ configAccount: ConfigAccount;
+}) {
+ switch (configAccount.type) {
+ case "stakeConfig":
+ return (
+
+ );
+ case "validatorInfo":
+ return (
+
+ );
+ }
+}
+
+function StakeConfigCard({
+ account,
+ configAccount,
+}: {
+ account: Account;
+ configAccount: StakeConfigInfoAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+
+ const warmupCooldownFormatted = new Intl.NumberFormat("en-US", {
+ style: "percent",
+ maximumFractionDigits: 2,
+ }).format(configAccount.info.warmupCooldownRate);
+
+ const slashPenaltyFormatted = new Intl.NumberFormat("en-US", {
+ style: "percent",
+ maximumFractionDigits: 2,
+ }).format(configAccount.info.slashPenalty / MAX_SLASH_PENALTY);
+
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Warmup / Cooldown Rate |
+ {warmupCooldownFormatted} |
+
+
+
+ Slash Penalty |
+ {slashPenaltyFormatted} |
+
+
+
+ );
+}
+
+function ValidatorInfoCard({
+ account,
+ configAccount,
+}: {
+ account: Account;
+ configAccount: ValidatorInfoAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+ {configAccount.info.configData.name && (
+
+ Name |
+
+ {configAccount.info.configData.name}
+ |
+
+ )}
+
+ {configAccount.info.configData.keybaseUsername && (
+
+ Keybase Username |
+
+ {configAccount.info.configData.keybaseUsername}
+ |
+
+ )}
+
+ {configAccount.info.configData.website && (
+
+ Website |
+
+
+ {configAccount.info.configData.website}
+
+ |
+
+ )}
+
+ {configAccount.info.configData.details && (
+
+ Details |
+
+ {configAccount.info.configData.details}
+ |
+
+ )}
+
+ {configAccount.info.keys && configAccount.info.keys.length > 1 && (
+
+ Signer |
+
+
+ |
+
+ )}
+
+
+ );
+}
diff --git a/explorer/src/components/account/NonceAccountSection.tsx b/explorer/src/components/account/NonceAccountSection.tsx
new file mode 100644
index 0000000000..a0f21ab3f8
--- /dev/null
+++ b/explorer/src/components/account/NonceAccountSection.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { Account, useFetchAccountInfo } from "providers/accounts";
+import { TableCardBody } from "components/common/TableCardBody";
+import { Address } from "components/common/Address";
+import { NonceAccount } from "validators/accounts/nonce";
+import {
+ AccountHeader,
+ AccountAddressRow,
+ AccountBalanceRow,
+} from "components/common/Account";
+
+export function NonceAccountSection({
+ account,
+ nonceAccount,
+}: {
+ account: Account;
+ nonceAccount: NonceAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Authority |
+
+
+ |
+
+
+
+ Blockhash |
+
+ {nonceAccount.info.blockhash}
+ |
+
+
+
+ Fee |
+
+ {nonceAccount.info.feeCalculator.lamportsPerSignature} lamports per
+ signature
+ |
+
+
+
+ );
+}
diff --git a/explorer/src/components/account/SlotHashesCard.tsx b/explorer/src/components/account/SlotHashesCard.tsx
new file mode 100644
index 0000000000..4ae804f621
--- /dev/null
+++ b/explorer/src/components/account/SlotHashesCard.tsx
@@ -0,0 +1,59 @@
+import React from "react";
+import {
+ SysvarAccount,
+ SlotHashesInfo,
+ SlotHashEntry,
+} from "validators/accounts/sysvar";
+
+export function SlotHashesCard({
+ sysvarAccount,
+}: {
+ sysvarAccount: SysvarAccount;
+}) {
+ const slotHashes = sysvarAccount.info as SlotHashesInfo;
+ return (
+
+
+
+
+
+
+
+ Slot |
+ Blockhash |
+
+
+
+ {slotHashes.length > 0 &&
+ slotHashes.map((entry: SlotHashEntry, index) => {
+ return renderAccountRow(entry, index);
+ })}
+
+
+
+
+
+
+ {slotHashes.length > 0 ? "" : "No hashes found"}
+
+
+
+ );
+}
+
+const renderAccountRow = (entry: SlotHashEntry, index: number) => {
+ return (
+
+
+ {entry.slot.toLocaleString("en-US")}
+ |
+ {entry.hash} |
+
+ );
+};
diff --git a/explorer/src/components/account/StakeHistoryCard.tsx b/explorer/src/components/account/StakeHistoryCard.tsx
new file mode 100644
index 0000000000..dea1bdd1a6
--- /dev/null
+++ b/explorer/src/components/account/StakeHistoryCard.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+import { lamportsToSolString } from "utils";
+import {
+ SysvarAccount,
+ StakeHistoryInfo,
+ StakeHistoryEntry,
+} from "validators/accounts/sysvar";
+
+export function StakeHistoryCard({
+ sysvarAccount,
+}: {
+ sysvarAccount: SysvarAccount;
+}) {
+ const stakeHistory = sysvarAccount.info as StakeHistoryInfo;
+ return (
+ <>
+
+
+
+
+
+
+
+ Epoch |
+ Effective (SOL) |
+ Activating (SOL) |
+ Deactivating (SOL) |
+
+
+
+ {stakeHistory.length > 0 &&
+ stakeHistory.map((entry: StakeHistoryEntry, index) => {
+ return renderAccountRow(entry, index);
+ })}
+
+
+
+
+
+
+ {stakeHistory.length > 0 ? "" : "No stake history found"}
+
+
+
+ >
+ );
+}
+
+const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
+ return (
+
+ {entry.epoch} |
+
+ {lamportsToSolString(entry.stakeHistory.effective)}
+ |
+
+ {lamportsToSolString(entry.stakeHistory.activating)}
+ |
+
+ {lamportsToSolString(entry.stakeHistory.deactivating)}
+ |
+
+ );
+};
diff --git a/explorer/src/components/account/SysvarAccountSection.tsx b/explorer/src/components/account/SysvarAccountSection.tsx
new file mode 100644
index 0000000000..4aae3971eb
--- /dev/null
+++ b/explorer/src/components/account/SysvarAccountSection.tsx
@@ -0,0 +1,416 @@
+import React from "react";
+import { Account, useFetchAccountInfo } from "providers/accounts";
+import {
+ SysvarAccount,
+ SysvarClockAccount,
+ SysvarEpochScheduleAccount,
+ SysvarFeesAccount,
+ SysvarRecentBlockhashesAccount,
+ SysvarRentAccount,
+ SysvarRewardsAccount,
+ SysvarSlotHashesAccount,
+ SysvarSlotHistoryAccount,
+ SysvarStakeHistoryAccount,
+} from "validators/accounts/sysvar";
+import { TableCardBody } from "components/common/TableCardBody";
+import {
+ AccountHeader,
+ AccountAddressRow,
+ AccountBalanceRow,
+} from "components/common/Account";
+import { displayTimestamp } from "utils/date";
+
+export function SysvarAccountSection({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarAccount;
+}) {
+ switch (sysvarAccount.type) {
+ case "clock":
+ return (
+
+ );
+ case "rent":
+ return (
+
+ );
+ case "rewards":
+ return (
+
+ );
+ case "epochSchedule":
+ return (
+
+ );
+ case "fees":
+ return (
+
+ );
+ case "recentBlockhashes":
+ return (
+
+ );
+ case "slotHashes":
+ return (
+
+ );
+ case "slotHistory":
+ return (
+
+ );
+ case "stakeHistory":
+ return (
+
+ );
+ }
+}
+
+function SysvarAccountRecentBlockhashesCard({
+ account,
+}: {
+ account: Account;
+ sysvarAccount: SysvarRecentBlockhashesAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ );
+}
+
+function SysvarAccountSlotHashes({
+ account,
+}: {
+ account: Account;
+ sysvarAccount: SysvarSlotHashesAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ );
+}
+
+function SysvarAccountSlotHistory({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarSlotHistoryAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ const history = Array.from(
+ {
+ length: 100,
+ },
+ (v, k) => sysvarAccount.info.nextSlot - k
+ );
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+
+ Slot History{" "}
+ (previous 100 slots)
+ |
+
+ {history.map((val) => (
+
+ {val}
+
+ ))}
+ |
+
+
+
+ );
+}
+
+function SysvarAccountStakeHistory({
+ account,
+}: {
+ account: Account;
+ sysvarAccount: SysvarStakeHistoryAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ );
+}
+
+function SysvarAccountFeesCard({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarFeesAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Lamports Per Signature |
+
+ {sysvarAccount.info.feeCalculator.lamportsPerSignature}
+ |
+
+
+
+ );
+}
+
+function SysvarAccountEpochScheduleCard({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarEpochScheduleAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Slots Per Epoch |
+ {sysvarAccount.info.slotsPerEpoch} |
+
+
+
+ Leader Schedule Slot Offset |
+
+ {sysvarAccount.info.leaderScheduleSlotOffset}
+ |
+
+
+
+ Epoch Warmup Enabled |
+
+ {sysvarAccount.info.warmup ? "true" : "false"}
+ |
+
+
+
+ First Normal Epoch |
+
+ {sysvarAccount.info.firstNormalEpoch}
+ |
+
+
+
+ First Normal Slot |
+
+ {sysvarAccount.info.firstNormalSlot}
+ |
+
+
+
+ );
+}
+
+function SysvarAccountClockCard({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarClockAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Timestamp |
+
+ {displayTimestamp(sysvarAccount.info.unixTimestamp * 1000)}
+ |
+
+
+
+ Epoch |
+ {sysvarAccount.info.epoch} |
+
+
+
+ Leader Schedule Epoch |
+
+ {sysvarAccount.info.leaderScheduleEpoch}
+ |
+
+
+
+ Slot |
+ {sysvarAccount.info.slot} |
+
+
+
+ );
+}
+
+function SysvarAccountRentCard({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarRentAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Burn Percent |
+
+ {sysvarAccount.info.burnPercent + "%"}
+ |
+
+
+
+ Exemption Threshold |
+
+ {sysvarAccount.info.exemptionThreshold} years
+ |
+
+
+
+ Lamports Per Byte Year |
+
+ {sysvarAccount.info.lamportsPerByteYear}
+ |
+
+
+
+ );
+}
+
+function SysvarAccountRewardsCard({
+ account,
+ sysvarAccount,
+}: {
+ account: Account;
+ sysvarAccount: SysvarRewardsAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+
+ const validatorPointValueFormatted = new Intl.NumberFormat("en-US", {
+ maximumSignificantDigits: 20,
+ }).format(sysvarAccount.info.validatorPointValue);
+
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+ Validator Point Value |
+
+ {validatorPointValueFormatted} lamports
+ |
+
+
+
+ );
+}
diff --git a/explorer/src/components/account/VoteAccountSection.tsx b/explorer/src/components/account/VoteAccountSection.tsx
new file mode 100644
index 0000000000..6fb190e024
--- /dev/null
+++ b/explorer/src/components/account/VoteAccountSection.tsx
@@ -0,0 +1,83 @@
+import React from "react";
+import { Account, useFetchAccountInfo } from "providers/accounts";
+import { TableCardBody } from "components/common/TableCardBody";
+import { Address } from "components/common/Address";
+import { VoteAccount } from "validators/accounts/vote";
+import { displayTimestamp } from "utils/date";
+import {
+ AccountHeader,
+ AccountAddressRow,
+ AccountBalanceRow,
+} from "components/common/Account";
+
+export function VoteAccountSection({
+ account,
+ voteAccount,
+}: {
+ account: Account;
+ voteAccount: VoteAccount;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
refresh(account.pubkey)}
+ />
+
+
+
+
+
+
+
+ Authorized Voter
+ {voteAccount.info.authorizedVoters.length > 1 ? "s" : ""}
+ |
+
+ {voteAccount.info.authorizedVoters.map((voter) => {
+ return (
+
+ );
+ })}
+ |
+
+
+
+ Authorized Withdrawer |
+
+
+ |
+
+
+
+ Last Timestamp |
+
+ {displayTimestamp(voteAccount.info.lastTimestamp.timestamp * 1000)}
+ |
+
+
+
+ Commission |
+ {voteAccount.info.commission + "%"} |
+
+
+
+ Root Slot |
+ {voteAccount.info.rootSlot} |
+
+
+
+ );
+}
diff --git a/explorer/src/components/account/VotesCard.tsx b/explorer/src/components/account/VotesCard.tsx
new file mode 100644
index 0000000000..2d5feb0278
--- /dev/null
+++ b/explorer/src/components/account/VotesCard.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { VoteAccount, Vote } from "validators/accounts/vote";
+
+export function VotesCard({ voteAccount }: { voteAccount: VoteAccount }) {
+ return (
+ <>
+
+
+
+
+
+
+
+ Slot |
+ Confirmation Count |
+
+
+
+ {voteAccount.info.votes.length > 0 &&
+ voteAccount.info.votes
+ .reverse()
+ .map((vote: Vote, index) => renderAccountRow(vote, index))}
+
+
+
+
+
+
+ {voteAccount.info.votes.length > 0 ? "" : "No votes found"}
+
+
+
+ >
+ );
+}
+
+const renderAccountRow = (vote: Vote, index: number) => {
+ return (
+
+
+ {vote.slot.toLocaleString("en-US")}
+ |
+ {vote.confirmationCount} |
+
+ );
+};
diff --git a/explorer/src/components/common/Account.tsx b/explorer/src/components/common/Account.tsx
new file mode 100644
index 0000000000..f2eb5280fc
--- /dev/null
+++ b/explorer/src/components/common/Account.tsx
@@ -0,0 +1,63 @@
+import React from "react";
+import { Address } from "./Address";
+import { Account } from "providers/accounts";
+import { lamportsToSolString } from "utils";
+
+type AccountHeaderProps = {
+ title: string;
+ refresh: Function;
+};
+
+type AccountProps = {
+ account: Account;
+};
+
+export function AccountHeader({ title, refresh }: AccountHeaderProps) {
+ return (
+
+
{title}
+
+
+ );
+}
+
+export function AccountAddressRow({ account }: AccountProps) {
+ return (
+
+ Address |
+
+
+ |
+
+ );
+}
+
+export function AccountBalanceRow({ account }: AccountProps) {
+ const { lamports } = account;
+ return (
+
+ Balance (SOL) |
+
+ {lamportsToSolString(lamports)}
+ |
+
+ );
+}
+
+export function AccountOwnerRow({ account }: AccountProps) {
+ if (account.details) {
+ return (
+
+ Owner |
+
+
+ |
+
+ );
+ }
+
+ return <>>;
+}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index a759731a9a..2573e3be7e 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -5,6 +5,7 @@ import {
useFetchAccountInfo,
useAccountInfo,
Account,
+ ProgramData,
} from "providers/accounts";
import { StakeAccountSection } from "components/account/StakeAccountSection";
import { TokenAccountSection } from "components/account/TokenAccountSection";
@@ -19,6 +20,50 @@ import { TransactionHistoryCard } from "components/account/TransactionHistoryCar
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
import { TokenRegistry } from "tokenRegistry";
+import { VoteAccountSection } from "components/account/VoteAccountSection";
+import { NonceAccountSection } from "components/account/NonceAccountSection";
+import { VotesCard } from "components/account/VotesCard";
+import { SysvarAccountSection } from "components/account/SysvarAccountSection";
+import { SlotHashesCard } from "components/account/SlotHashesCard";
+import { StakeHistoryCard } from "components/account/StakeHistoryCard";
+import { BlockhashesCard } from "components/account/BlockhashesCard";
+import { ConfigAccountSection } from "components/account/ConfigAccountSection";
+
+const TABS_LOOKUP: { [id: string]: Tab } = {
+ "spl-token:mint": {
+ slug: "largest",
+ title: "Distribution",
+ path: "/largest",
+ },
+ vote: {
+ slug: "vote-history",
+ title: "Vote History",
+ path: "/vote-history",
+ },
+ "sysvar:recentBlockhashes": {
+ slug: "blockhashes",
+ title: "Blockhashes",
+ path: "/blockhashes",
+ },
+ "sysvar:slotHashes": {
+ slug: "slot-hashes",
+ title: "Slot Hashes",
+ path: "/slot-hashes",
+ },
+ "sysvar:stakeHistory": {
+ slug: "stake-history",
+ title: "Stake History",
+ path: "/stake-history",
+ },
+};
+
+const TOKEN_TABS_HIDDEN = [
+ "spl-token:mint",
+ "config",
+ "vote",
+ "sysvar",
+ "config",
+];
type Props = { address: string; tab?: string };
export function AccountDetailsPage({ address, tab }: Props) {
@@ -101,30 +146,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
const account = info.data;
const data = account?.details?.data;
-
- let tabs: Tab[] = [
- {
- slug: "history",
- title: "History",
- path: "",
- },
- ];
-
- if (data && data?.program === "spl-token") {
- if (data.parsed.type === "mint") {
- tabs.push({
- slug: "largest",
- title: "Distribution",
- path: "/largest",
- });
- }
- } else {
- tabs.push({
- slug: "tokens",
- title: "Tokens",
- path: "/tokens",
- });
- }
+ const tabs = getTabs(data);
let moreTab: MoreTabs = "history";
if (tab && tabs.filter(({ slug }) => slug === tab).length === 0) {
@@ -164,6 +186,18 @@ function InfoSection({ account }: { account: Account }) {
);
} else if (data && data.program === "spl-token") {
return ;
+ } else if (data && data.program === "nonce") {
+ return ;
+ } else if (data && data.program === "vote") {
+ return ;
+ } else if (data && data.program === "sysvar") {
+ return (
+
+ );
+ } else if (data && data.program === "config") {
+ return (
+
+ );
} else {
return ;
}
@@ -175,7 +209,15 @@ type Tab = {
path: string;
};
-type MoreTabs = "history" | "tokens" | "largest";
+type MoreTabs =
+ | "history"
+ | "tokens"
+ | "largest"
+ | "vote-history"
+ | "slot-hashes"
+ | "stake-history"
+ | "blockhashes";
+
function MoreSection({
account,
tab,
@@ -187,7 +229,7 @@ function MoreSection({
}) {
const pubkey = account.pubkey;
const address = account.pubkey.toBase58();
-
+ const data = account?.details?.data;
return (
<>
@@ -217,6 +259,63 @@ function MoreSection({
)}
{tab === "history" && }
{tab === "largest" && }
+ {tab === "vote-history" && data?.program === "vote" && (
+
+ )}
+ {tab === "slot-hashes" &&
+ data?.program === "sysvar" &&
+ data.parsed.type === "slotHashes" && (
+
+ )}
+ {tab === "stake-history" &&
+ data?.program === "sysvar" &&
+ data.parsed.type === "stakeHistory" && (
+
+ )}
+ {tab === "blockhashes" &&
+ data?.program === "sysvar" &&
+ data.parsed.type === "recentBlockhashes" && (
+
+ )}
>
);
}
+
+function getTabs(data?: ProgramData): Tab[] {
+ const tabs: Tab[] = [
+ {
+ slug: "history",
+ title: "History",
+ path: "",
+ },
+ ];
+
+ let programTypeKey = "";
+ if (data && "type" in data.parsed) {
+ programTypeKey = `${data.program}:${data.parsed.type}`;
+ }
+
+ if (data && data.program in TABS_LOOKUP) {
+ tabs.push(TABS_LOOKUP[data.program]);
+ }
+
+ if (data && programTypeKey in TABS_LOOKUP) {
+ tabs.push(TABS_LOOKUP[programTypeKey]);
+ }
+
+ if (
+ !data ||
+ !(
+ TOKEN_TABS_HIDDEN.includes(data.program) ||
+ TOKEN_TABS_HIDDEN.includes(programTypeKey)
+ )
+ ) {
+ tabs.push({
+ slug: "tokens",
+ title: "Tokens",
+ path: "/tokens",
+ });
+ }
+
+ return tabs;
+}
diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx
index fb924053aa..b8430207ad 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -20,6 +20,10 @@ import {
import * as Cache from "providers/cache";
import { ActionType, FetchStatus } from "providers/cache";
import { reportError } from "utils/sentry";
+import { VoteAccount } from "validators/accounts/vote";
+import { NonceAccount } from "validators/accounts/nonce";
+import { SysvarAccount } from "validators/accounts/sysvar";
+import { ConfigAccount } from "validators/accounts/config";
export { useAccountHistory } from "./history";
export type StakeProgramData = {
@@ -33,7 +37,33 @@ export type TokenProgramData = {
parsed: TokenAccount;
};
-export type ProgramData = StakeProgramData | TokenProgramData;
+export type VoteProgramData = {
+ program: "vote";
+ parsed: VoteAccount;
+};
+
+export type NonceProgramData = {
+ program: "nonce";
+ parsed: NonceAccount;
+};
+
+export type SysvarProgramData = {
+ program: "sysvar";
+ parsed: SysvarAccount;
+};
+
+export type ConfigProgramData = {
+ program: "config";
+ parsed: ConfigAccount;
+};
+
+export type ProgramData =
+ | StakeProgramData
+ | TokenProgramData
+ | VoteProgramData
+ | NonceProgramData
+ | SysvarProgramData
+ | ConfigProgramData;
export interface Details {
executable: boolean;
@@ -134,20 +164,54 @@ async function fetchAccountInfo(
reportError(err, { url, address: pubkey.toBase58() });
// TODO store error state in Account info
}
+ } else if (
+ "parsed" in result.data &&
+ result.owner.equals(TOKEN_PROGRAM_ID)
+ ) {
+ try {
+ const info = coerce(result.data.parsed, ParsedInfo);
+ const parsed = coerce(info, TokenAccount);
+ data = {
+ program: "spl-token",
+ parsed,
+ };
+ } catch (err) {
+ reportError(err, { url, address: pubkey.toBase58() });
+ // TODO store error state in Account info
+ }
} else if ("parsed" in result.data) {
- if (result.owner.equals(TOKEN_PROGRAM_ID)) {
- try {
- const info = coerce(result.data.parsed, ParsedInfo);
- const parsed = coerce(info, TokenAccount);
-
- data = {
- program: "spl-token",
- parsed,
- };
- } catch (err) {
- reportError(err, { url, address: pubkey.toBase58() });
- // TODO store error state in Account info
+ try {
+ const info = coerce(result.data.parsed, ParsedInfo);
+ switch (result.data.program) {
+ case "vote":
+ data = {
+ program: result.data.program,
+ parsed: coerce(info, VoteAccount),
+ };
+ break;
+ case "nonce":
+ data = {
+ program: result.data.program,
+ parsed: coerce(info, NonceAccount),
+ };
+ break;
+ case "sysvar":
+ data = {
+ program: result.data.program,
+ parsed: coerce(info, SysvarAccount),
+ };
+ break;
+ case "config":
+ data = {
+ program: result.data.program,
+ parsed: coerce(info, ConfigAccount),
+ };
+ break;
+ default:
+ data = undefined;
}
+ } catch (error) {
+ reportError(error, { url, address: pubkey.toBase58() });
}
}
diff --git a/explorer/src/validators/accounts/config.ts b/explorer/src/validators/accounts/config.ts
new file mode 100644
index 0000000000..4cadcadbf5
--- /dev/null
+++ b/explorer/src/validators/accounts/config.ts
@@ -0,0 +1,55 @@
+import {
+ StructType,
+ pick,
+ array,
+ boolean,
+ object,
+ number,
+ string,
+ record,
+ union,
+ literal,
+} from "superstruct";
+
+export type StakeConfigInfo = StructType;
+export const StakeConfigInfo = pick({
+ warmupCooldownRate: number(),
+ slashPenalty: number(),
+});
+
+export type ConfigKey = StructType;
+export const ConfigKey = pick({
+ pubkey: string(),
+ signer: boolean(),
+});
+
+export type ValidatorInfoConfigData = StructType<
+ typeof ValidatorInfoConfigData
+>;
+export const ValidatorInfoConfigData = record(string(), string());
+
+export type ValidatorInfoConfigInfo = StructType<
+ typeof ValidatorInfoConfigInfo
+>;
+export const ValidatorInfoConfigInfo = pick({
+ keys: array(ConfigKey),
+ configData: ValidatorInfoConfigData,
+});
+
+export type ValidatorInfoAccount = StructType;
+export const ValidatorInfoAccount = object({
+ type: literal("validatorInfo"),
+ info: ValidatorInfoConfigInfo,
+});
+
+export type StakeConfigInfoAccount = StructType;
+export const StakeConfigInfoAccount = object({
+ type: literal("stakeConfig"),
+ info: StakeConfigInfo,
+});
+
+export type ConfigAccount = StructType;
+export const ConfigAccount = union([
+ StakeConfigInfoAccount,
+ ValidatorInfoAccount,
+]);
diff --git a/explorer/src/validators/accounts/nonce.ts b/explorer/src/validators/accounts/nonce.ts
new file mode 100644
index 0000000000..4d5d3a03df
--- /dev/null
+++ b/explorer/src/validators/accounts/nonce.ts
@@ -0,0 +1,20 @@
+import { StructType, object, string, enums, pick } from "superstruct";
+import { Pubkey } from "validators/pubkey";
+
+export type NonceAccountType = StructType;
+export const NonceAccountType = enums(["uninitialized", "initialized"]);
+
+export type NonceAccountInfo = StructType;
+export const NonceAccountInfo = pick({
+ authority: Pubkey,
+ blockhash: string(),
+ feeCalculator: pick({
+ lamportsPerSignature: string(),
+ }),
+});
+
+export type NonceAccount = StructType;
+export const NonceAccount = object({
+ type: NonceAccountType,
+ info: NonceAccountInfo,
+});
diff --git a/explorer/src/validators/accounts/sysvar.ts b/explorer/src/validators/accounts/sysvar.ts
new file mode 100644
index 0000000000..9259ac29d1
--- /dev/null
+++ b/explorer/src/validators/accounts/sysvar.ts
@@ -0,0 +1,180 @@
+import {
+ StructType,
+ enums,
+ array,
+ number,
+ object,
+ boolean,
+ string,
+ pick,
+ literal,
+ union,
+} from "superstruct";
+
+export type SysvarAccountType = StructType;
+export const SysvarAccountType = enums([
+ "clock",
+ "epochSchedule",
+ "fees",
+ "recentBlockhashes",
+ "rent",
+ "rewards",
+ "slotHashes",
+ "slotHistory",
+ "stakeHistory",
+]);
+
+export type ClockAccountInfo = StructType;
+export const ClockAccountInfo = pick({
+ slot: number(),
+ epoch: number(),
+ leaderScheduleEpoch: number(),
+ unixTimestamp: number(),
+});
+
+export type SysvarClockAccount = StructType;
+export const SysvarClockAccount = object({
+ type: literal("clock"),
+ info: ClockAccountInfo,
+});
+
+export type EpochScheduleInfo = StructType;
+export const EpochScheduleInfo = pick({
+ slotsPerEpoch: number(),
+ leaderScheduleSlotOffset: number(),
+ warmup: boolean(),
+ firstNormalEpoch: number(),
+ firstNormalSlot: number(),
+});
+
+export type SysvarEpochScheduleAccount = StructType<
+ typeof SysvarEpochScheduleAccount
+>;
+export const SysvarEpochScheduleAccount = object({
+ type: literal("epochSchedule"),
+ info: EpochScheduleInfo,
+});
+
+export type FeesInfo = StructType;
+export const FeesInfo = pick({
+ feeCalculator: pick({
+ lamportsPerSignature: string(),
+ }),
+});
+
+export type SysvarFeesAccount = StructType;
+export const SysvarFeesAccount = object({
+ type: literal("fees"),
+ info: FeesInfo,
+});
+
+export type RecentBlockhashesEntry = StructType;
+export const RecentBlockhashesEntry = pick({
+ blockhash: string(),
+ feeCalculator: pick({
+ lamportsPerSignature: string(),
+ }),
+});
+
+export type RecentBlockhashesInfo = StructType;
+export const RecentBlockhashesInfo = array(RecentBlockhashesEntry);
+
+export type SysvarRecentBlockhashesAccount = StructType<
+ typeof SysvarRecentBlockhashesAccount
+>;
+export const SysvarRecentBlockhashesAccount = object({
+ type: literal("recentBlockhashes"),
+ info: RecentBlockhashesInfo,
+});
+
+export type RentInfo = StructType;
+export const RentInfo = pick({
+ lamportsPerByteYear: string(),
+ exemptionThreshold: number(),
+ burnPercent: number(),
+});
+
+export type SysvarRentAccount = StructType;
+export const SysvarRentAccount = object({
+ type: literal("rent"),
+ info: RentInfo,
+});
+
+export type RewardsInfo = StructType;
+export const RewardsInfo = pick({
+ validatorPointValue: number(),
+});
+
+export type SysvarRewardsAccount = StructType;
+export const SysvarRewardsAccount = object({
+ type: literal("rewards"),
+ info: RewardsInfo,
+});
+
+export type SlotHashEntry = StructType;
+export const SlotHashEntry = pick({
+ slot: number(),
+ hash: string(),
+});
+
+export type SlotHashesInfo = StructType;
+export const SlotHashesInfo = array(SlotHashEntry);
+
+export type SysvarSlotHashesAccount = StructType<
+ typeof SysvarSlotHashesAccount
+>;
+export const SysvarSlotHashesAccount = object({
+ type: literal("slotHashes"),
+ info: SlotHashesInfo,
+});
+
+export type SlotHistoryInfo = StructType;
+export const SlotHistoryInfo = pick({
+ nextSlot: number(),
+ bits: string(),
+});
+
+export type SysvarSlotHistoryAccount = StructType<
+ typeof SysvarSlotHistoryAccount
+>;
+export const SysvarSlotHistoryAccount = object({
+ type: literal("slotHistory"),
+ info: SlotHistoryInfo,
+});
+
+export type StakeHistoryEntryItem = StructType;
+export const StakeHistoryEntryItem = pick({
+ effective: number(),
+ activating: number(),
+ deactivating: number(),
+});
+
+export type StakeHistoryEntry = StructType;
+export const StakeHistoryEntry = pick({
+ epoch: number(),
+ stakeHistory: StakeHistoryEntryItem,
+});
+
+export type StakeHistoryInfo = StructType;
+export const StakeHistoryInfo = array(StakeHistoryEntry);
+
+export type SysvarStakeHistoryAccount = StructType<
+ typeof SysvarStakeHistoryAccount
+>;
+export const SysvarStakeHistoryAccount = object({
+ type: literal("stakeHistory"),
+ info: StakeHistoryInfo,
+});
+
+export type SysvarAccount = StructType;
+export const SysvarAccount = union([
+ SysvarClockAccount,
+ SysvarEpochScheduleAccount,
+ SysvarFeesAccount,
+ SysvarRecentBlockhashesAccount,
+ SysvarRentAccount,
+ SysvarRewardsAccount,
+ SysvarSlotHashesAccount,
+ SysvarSlotHistoryAccount,
+ SysvarStakeHistoryAccount,
+]);
diff --git a/explorer/src/validators/accounts/vote.ts b/explorer/src/validators/accounts/vote.ts
new file mode 100644
index 0000000000..a445e60b7e
--- /dev/null
+++ b/explorer/src/validators/accounts/vote.ts
@@ -0,0 +1,62 @@
+import {
+ StructType,
+ enums,
+ pick,
+ number,
+ array,
+ object,
+ nullable,
+ string,
+} from "superstruct";
+import { Pubkey } from "validators/pubkey";
+
+export type VoteAccountType = StructType;
+export const VoteAccountType = enums(["vote"]);
+
+export type AuthorizedVoter = StructType;
+export const AuthorizedVoter = pick({
+ authorizedVoter: Pubkey,
+ epoch: number(),
+});
+
+export type PriorVoter = StructType;
+export const PriorVoter = pick({
+ authorizedPubkey: Pubkey,
+ epochOfLastAuthorizedSwitch: number(),
+ targetEpoch: number(),
+});
+
+export type EpochCredits = StructType;
+export const EpochCredits = pick({
+ epoch: number(),
+ credits: string(),
+ previousCredits: string(),
+});
+
+export type Vote = StructType;
+export const Vote = object({
+ slot: number(),
+ confirmationCount: number(),
+});
+
+export type VoteAccountInfo = StructType;
+export const VoteAccountInfo = pick({
+ authorizedVoters: array(AuthorizedVoter),
+ authorizedWithdrawer: Pubkey,
+ commission: number(),
+ epochCredits: array(EpochCredits),
+ lastTimestamp: object({
+ slot: number(),
+ timestamp: number(),
+ }),
+ nodePubkey: Pubkey,
+ priorVoters: array(PriorVoter),
+ rootSlot: nullable(number()),
+ votes: array(Vote),
+});
+
+export type VoteAccount = StructType;
+export const VoteAccount = pick({
+ type: VoteAccountType,
+ info: VoteAccountInfo,
+});