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 ( + <> +
+
+
+
+

Blockhashes

+
+
+
+ +
+ + + + + + + + + + {blockhashes.length > 0 && + blockhashes.map((entry: RecentBlockhashesEntry, index) => { + return renderAccountRow(entry, index); + })} + +
RecencyBlockhashFee Calculator
+
+ +
+
+ {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 Hashes

+
+
+
+ +
+ + + + + + + + + {slotHashes.length > 0 && + slotHashes.map((entry: SlotHashEntry, index) => { + return renderAccountRow(entry, index); + })} + +
SlotBlockhash
+
+ +
+
+ {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 ( + <> +
+
+
+
+

Stake History

+
+
+
+ +
+ + + + + + + + + + + {stakeHistory.length > 0 && + stakeHistory.map((entry: StakeHistoryEntry, index) => { + return renderAccountRow(entry, index); + })} + +
EpochEffective (SOL)Activating (SOL)Deactivating (SOL)
+
+ +
+
+ {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 ( + <> +
+
+
+
+

Vote History

+
+
+
+ +
+ + + + + + + + + {voteAccount.info.votes.length > 0 && + voteAccount.info.votes + .reverse() + .map((vote: Vote, index) => renderAccountRow(vote, index))} + +
SlotConfirmation Count
+
+ +
+
+ {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, +});