explorer: Add support for all parsed accounts (#11842)

* introduce vote and nonce validators

* introduce config, nonce, sysvar, vote validators / types

* change ConfigProgram to ConfigProgramData

* introduce vote account section and nonce account section, clean up superstructs

* nonce section

* round out vote account and nonce account

* refactor account components, add votes tab

* update program data name to program

* introduce slot hashes, stake history

* introduce blockhashes card and config account

* run fix format

* remove comment

* introduce config section and typings

* refactor tabs if blocks

* change superstructs to pick in some cases

* remove account owners, rename vote history, some nit fixes

* general cleanup and improvements

* add recency column

* add balance row to parsed accounts

* union account types under sysvar and config for improved typing. modify row headers for consistency.

* remove random spaces

* use proper type checking and clean up a cast
This commit is contained in:
Josh 2020-10-10 01:03:45 -07:00 committed by GitHub
parent f1bbe1cd84
commit 86ca85d72b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1536 additions and 39 deletions

View File

@ -0,0 +1,61 @@
import React from "react";
import {
RecentBlockhashesInfo,
RecentBlockhashesEntry,
} from "validators/accounts/sysvar";
export function BlockhashesCard({
blockhashes,
}: {
blockhashes: RecentBlockhashesInfo;
}) {
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Blockhashes</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Recency</th>
<th className="w-1 text-muted">Blockhash</th>
<th className="text-muted">Fee Calculator</th>
</tr>
</thead>
<tbody className="list">
{blockhashes.length > 0 &&
blockhashes.map((entry: RecentBlockhashesEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{blockhashes.length > 0 ? "" : "No blockhashes found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (entry: RecentBlockhashesEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1">{index + 1}</td>
<td className="w-1 text-monospace">{entry.blockhash}</td>
<td className="">
{entry.feeCalculator.lamportsPerSignature} lamports per signature
</td>
</tr>
);
};

View File

@ -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 (
<StakeConfigCard account={account} configAccount={configAccount} />
);
case "validatorInfo":
return (
<ValidatorInfoCard account={account} configAccount={configAccount} />
);
}
}
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 (
<div className="card">
<AccountHeader
title="Stake Config"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Warmup / Cooldown Rate</td>
<td className="text-lg-right">{warmupCooldownFormatted}</td>
</tr>
<tr>
<td>Slash Penalty</td>
<td className="text-lg-right">{slashPenaltyFormatted}</td>
</tr>
</TableCardBody>
</div>
);
}
function ValidatorInfoCard({
account,
configAccount,
}: {
account: Account;
configAccount: ValidatorInfoAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Validator Info"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
{configAccount.info.configData.name && (
<tr>
<td>Name</td>
<td className="text-lg-right">
{configAccount.info.configData.name}
</td>
</tr>
)}
{configAccount.info.configData.keybaseUsername && (
<tr>
<td>Keybase Username</td>
<td className="text-lg-right">
{configAccount.info.configData.keybaseUsername}
</td>
</tr>
)}
{configAccount.info.configData.website && (
<tr>
<td>Website</td>
<td className="text-lg-right">
<a
href={configAccount.info.configData.website}
target="_blank"
rel="noopener noreferrer"
>
{configAccount.info.configData.website}
</a>
</td>
</tr>
)}
{configAccount.info.configData.details && (
<tr>
<td>Details</td>
<td className="text-lg-right">
{configAccount.info.configData.details}
</td>
</tr>
)}
{configAccount.info.keys && configAccount.info.keys.length > 1 && (
<tr>
<td>Signer</td>
<td className="text-lg-right">
<Address
pubkey={new PublicKey(configAccount.info.keys[1].pubkey)}
link
alignRight
/>
</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -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 (
<div className="card">
<AccountHeader
title="Nonce Account"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Authority</td>
<td className="text-lg-right">
<Address pubkey={nonceAccount.info.authority} alignRight raw link />
</td>
</tr>
<tr>
<td>Blockhash</td>
<td className="text-lg-right">
<code>{nonceAccount.info.blockhash}</code>
</td>
</tr>
<tr>
<td>Fee</td>
<td className="text-lg-right">
{nonceAccount.info.feeCalculator.lamportsPerSignature} lamports per
signature
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -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 (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Slot Hashes</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Slot</th>
<th className="text-muted">Blockhash</th>
</tr>
</thead>
<tbody className="list">
{slotHashes.length > 0 &&
slotHashes.map((entry: SlotHashEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{slotHashes.length > 0 ? "" : "No hashes found"}
</div>
</div>
</div>
);
}
const renderAccountRow = (entry: SlotHashEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1 text-monospace">
{entry.slot.toLocaleString("en-US")}
</td>
<td className="text-monospace">{entry.hash}</td>
</tr>
);
};

View File

@ -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 (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Stake History</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Epoch</th>
<th className="text-muted">Effective (SOL)</th>
<th className="text-muted">Activating (SOL)</th>
<th className="text-muted">Deactivating (SOL)</th>
</tr>
</thead>
<tbody className="list">
{stakeHistory.length > 0 &&
stakeHistory.map((entry: StakeHistoryEntry, index) => {
return renderAccountRow(entry, index);
})}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{stakeHistory.length > 0 ? "" : "No stake history found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
return (
<tr key={index}>
<td className="w-1 text-monospace">{entry.epoch}</td>
<td className="text-monospace">
{lamportsToSolString(entry.stakeHistory.effective)}
</td>
<td className="text-monospace">
{lamportsToSolString(entry.stakeHistory.activating)}
</td>
<td className="text-monospace">
{lamportsToSolString(entry.stakeHistory.deactivating)}
</td>
</tr>
);
};

View File

@ -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 (
<SysvarAccountClockCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "rent":
return (
<SysvarAccountRentCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "rewards":
return (
<SysvarAccountRewardsCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "epochSchedule":
return (
<SysvarAccountEpochScheduleCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "fees":
return (
<SysvarAccountFeesCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "recentBlockhashes":
return (
<SysvarAccountRecentBlockhashesCard
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "slotHashes":
return (
<SysvarAccountSlotHashes
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "slotHistory":
return (
<SysvarAccountSlotHistory
account={account}
sysvarAccount={sysvarAccount}
/>
);
case "stakeHistory":
return (
<SysvarAccountStakeHistory
account={account}
sysvarAccount={sysvarAccount}
/>
);
}
}
function SysvarAccountRecentBlockhashesCard({
account,
}: {
account: Account;
sysvarAccount: SysvarRecentBlockhashesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Recent Blockhashes"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountSlotHashes({
account,
}: {
account: Account;
sysvarAccount: SysvarSlotHashesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Slot Hashes"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountSlotHistory({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarSlotHistoryAccount;
}) {
const refresh = useFetchAccountInfo();
const history = Array.from(
{
length: 100,
},
(v, k) => sysvarAccount.info.nextSlot - k
);
return (
<div className="card">
<AccountHeader
title="Sysvar Slot History"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td className="align-top">
Slot History{" "}
<span className="text-muted">(previous 100 slots)</span>
</td>
<td className="text-lg-right text-monospace">
{history.map((val) => (
<p key={val} className="mb-0">
{val}
</p>
))}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountStakeHistory({
account,
}: {
account: Account;
sysvarAccount: SysvarStakeHistoryAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Stake History"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
</TableCardBody>
</div>
);
}
function SysvarAccountFeesCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarFeesAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Fees"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Lamports Per Signature</td>
<td className="text-lg-right">
{sysvarAccount.info.feeCalculator.lamportsPerSignature}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountEpochScheduleCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarEpochScheduleAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Epoch Schedule"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Slots Per Epoch</td>
<td className="text-lg-right">{sysvarAccount.info.slotsPerEpoch}</td>
</tr>
<tr>
<td>Leader Schedule Slot Offset</td>
<td className="text-lg-right">
{sysvarAccount.info.leaderScheduleSlotOffset}
</td>
</tr>
<tr>
<td>Epoch Warmup Enabled</td>
<td className="text-lg-right">
<code>{sysvarAccount.info.warmup ? "true" : "false"}</code>
</td>
</tr>
<tr>
<td>First Normal Epoch</td>
<td className="text-lg-right">
{sysvarAccount.info.firstNormalEpoch}
</td>
</tr>
<tr>
<td>First Normal Slot</td>
<td className="text-lg-right">
{sysvarAccount.info.firstNormalSlot}
</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountClockCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarClockAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Clock"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Timestamp</td>
<td className="text-lg-right">
{displayTimestamp(sysvarAccount.info.unixTimestamp * 1000)}
</td>
</tr>
<tr>
<td>Epoch</td>
<td className="text-lg-right">{sysvarAccount.info.epoch}</td>
</tr>
<tr>
<td>Leader Schedule Epoch</td>
<td className="text-lg-right">
{sysvarAccount.info.leaderScheduleEpoch}
</td>
</tr>
<tr>
<td>Slot</td>
<td className="text-lg-right">{sysvarAccount.info.slot}</td>
</tr>
</TableCardBody>
</div>
);
}
function SysvarAccountRentCard({
account,
sysvarAccount,
}: {
account: Account;
sysvarAccount: SysvarRentAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<AccountHeader
title="Sysvar Rent"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Burn Percent</td>
<td className="text-lg-right">
{sysvarAccount.info.burnPercent + "%"}
</td>
</tr>
<tr>
<td>Exemption Threshold</td>
<td className="text-lg-right">
{sysvarAccount.info.exemptionThreshold} years
</td>
</tr>
<tr>
<td>Lamports Per Byte Year</td>
<td className="text-lg-right">
{sysvarAccount.info.lamportsPerByteYear}
</td>
</tr>
</TableCardBody>
</div>
);
}
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 (
<div className="card">
<AccountHeader
title="Sysvar Rewards"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>Validator Point Value</td>
<td className="text-lg-right text-monospace">
{validatorPointValueFormatted} lamports
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -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 (
<div className="card">
<AccountHeader
title="Vote Account"
refresh={() => refresh(account.pubkey)}
/>
<TableCardBody>
<AccountAddressRow account={account} />
<AccountBalanceRow account={account} />
<tr>
<td>
Authorized Voter
{voteAccount.info.authorizedVoters.length > 1 ? "s" : ""}
</td>
<td className="text-lg-right">
{voteAccount.info.authorizedVoters.map((voter) => {
return (
<Address
pubkey={voter.authorizedVoter}
key={voter.authorizedVoter.toString()}
alignRight
raw
link
/>
);
})}
</td>
</tr>
<tr>
<td>Authorized Withdrawer</td>
<td className="text-lg-right">
<Address
pubkey={voteAccount.info.authorizedWithdrawer}
alignRight
raw
link
/>
</td>
</tr>
<tr>
<td>Last Timestamp</td>
<td className="text-lg-right">
{displayTimestamp(voteAccount.info.lastTimestamp.timestamp * 1000)}
</td>
</tr>
<tr>
<td>Commission</td>
<td className="text-lg-right">{voteAccount.info.commission + "%"}</td>
</tr>
<tr>
<td>Root Slot</td>
<td className="text-lg-right">{voteAccount.info.rootSlot}</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -0,0 +1,52 @@
import React from "react";
import { VoteAccount, Vote } from "validators/accounts/vote";
export function VotesCard({ voteAccount }: { voteAccount: VoteAccount }) {
return (
<>
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h3 className="card-header-title">Vote History</h3>
</div>
</div>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="w-1 text-muted">Slot</th>
<th className="text-muted">Confirmation Count</th>
</tr>
</thead>
<tbody className="list">
{voteAccount.info.votes.length > 0 &&
voteAccount.info.votes
.reverse()
.map((vote: Vote, index) => renderAccountRow(vote, index))}
</tbody>
</table>
</div>
<div className="card-footer">
<div className="text-muted text-center">
{voteAccount.info.votes.length > 0 ? "" : "No votes found"}
</div>
</div>
</div>
</>
);
}
const renderAccountRow = (vote: Vote, index: number) => {
return (
<tr key={index}>
<td className="w-1 text-monospace">
{vote.slot.toLocaleString("en-US")}
</td>
<td className="text-monospace">{vote.confirmationCount}</td>
</tr>
);
};

View File

@ -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 (
<div className="card-header align-items-center">
<h3 className="card-header-title">{title}</h3>
<button className="btn btn-white btn-sm" onClick={() => refresh()}>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
);
}
export function AccountAddressRow({ account }: AccountProps) {
return (
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
);
}
export function AccountBalanceRow({ account }: AccountProps) {
const { lamports } = account;
return (
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">
{lamportsToSolString(lamports)}
</td>
</tr>
);
}
export function AccountOwnerRow({ account }: AccountProps) {
if (account.details) {
return (
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={account.details.owner} alignRight link />
</td>
</tr>
);
}
return <></>;
}

View File

@ -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 <TokenAccountSection account={account} tokenAccount={data.parsed} />;
} else if (data && data.program === "nonce") {
return <NonceAccountSection account={account} nonceAccount={data.parsed} />;
} else if (data && data.program === "vote") {
return <VoteAccountSection account={account} voteAccount={data.parsed} />;
} else if (data && data.program === "sysvar") {
return (
<SysvarAccountSection account={account} sysvarAccount={data.parsed} />
);
} else if (data && data.program === "config") {
return (
<ConfigAccountSection account={account} configAccount={data.parsed} />
);
} else {
return <UnknownAccountCard account={account} />;
}
@ -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 (
<>
<div className="container">
@ -217,6 +259,63 @@ function MoreSection({
)}
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
{tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />}
{tab === "vote-history" && data?.program === "vote" && (
<VotesCard voteAccount={data.parsed} />
)}
{tab === "slot-hashes" &&
data?.program === "sysvar" &&
data.parsed.type === "slotHashes" && (
<SlotHashesCard sysvarAccount={data.parsed} />
)}
{tab === "stake-history" &&
data?.program === "sysvar" &&
data.parsed.type === "stakeHistory" && (
<StakeHistoryCard sysvarAccount={data.parsed} />
)}
{tab === "blockhashes" &&
data?.program === "sysvar" &&
data.parsed.type === "recentBlockhashes" && (
<BlockhashesCard blockhashes={data.parsed.info} />
)}
</>
);
}
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;
}

View File

@ -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,12 +164,13 @@ async function fetchAccountInfo(
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)) {
} 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,
@ -148,6 +179,39 @@ async function fetchAccountInfo(
reportError(err, { url, address: pubkey.toBase58() });
// TODO store error state in Account info
}
} else if ("parsed" in result.data) {
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() });
}
}

View File

@ -0,0 +1,55 @@
import {
StructType,
pick,
array,
boolean,
object,
number,
string,
record,
union,
literal,
} from "superstruct";
export type StakeConfigInfo = StructType<typeof StakeConfigInfo>;
export const StakeConfigInfo = pick({
warmupCooldownRate: number(),
slashPenalty: number(),
});
export type ConfigKey = StructType<typeof ConfigKey>;
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<typeof ValidatorInfoAccount>;
export const ValidatorInfoAccount = object({
type: literal("validatorInfo"),
info: ValidatorInfoConfigInfo,
});
export type StakeConfigInfoAccount = StructType<typeof StakeConfigInfoAccount>;
export const StakeConfigInfoAccount = object({
type: literal("stakeConfig"),
info: StakeConfigInfo,
});
export type ConfigAccount = StructType<typeof ConfigAccount>;
export const ConfigAccount = union([
StakeConfigInfoAccount,
ValidatorInfoAccount,
]);

View File

@ -0,0 +1,20 @@
import { StructType, object, string, enums, pick } from "superstruct";
import { Pubkey } from "validators/pubkey";
export type NonceAccountType = StructType<typeof NonceAccountType>;
export const NonceAccountType = enums(["uninitialized", "initialized"]);
export type NonceAccountInfo = StructType<typeof NonceAccountInfo>;
export const NonceAccountInfo = pick({
authority: Pubkey,
blockhash: string(),
feeCalculator: pick({
lamportsPerSignature: string(),
}),
});
export type NonceAccount = StructType<typeof NonceAccount>;
export const NonceAccount = object({
type: NonceAccountType,
info: NonceAccountInfo,
});

View File

@ -0,0 +1,180 @@
import {
StructType,
enums,
array,
number,
object,
boolean,
string,
pick,
literal,
union,
} from "superstruct";
export type SysvarAccountType = StructType<typeof SysvarAccountType>;
export const SysvarAccountType = enums([
"clock",
"epochSchedule",
"fees",
"recentBlockhashes",
"rent",
"rewards",
"slotHashes",
"slotHistory",
"stakeHistory",
]);
export type ClockAccountInfo = StructType<typeof ClockAccountInfo>;
export const ClockAccountInfo = pick({
slot: number(),
epoch: number(),
leaderScheduleEpoch: number(),
unixTimestamp: number(),
});
export type SysvarClockAccount = StructType<typeof SysvarClockAccount>;
export const SysvarClockAccount = object({
type: literal("clock"),
info: ClockAccountInfo,
});
export type EpochScheduleInfo = StructType<typeof EpochScheduleInfo>;
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<typeof FeesInfo>;
export const FeesInfo = pick({
feeCalculator: pick({
lamportsPerSignature: string(),
}),
});
export type SysvarFeesAccount = StructType<typeof SysvarFeesAccount>;
export const SysvarFeesAccount = object({
type: literal("fees"),
info: FeesInfo,
});
export type RecentBlockhashesEntry = StructType<typeof RecentBlockhashesEntry>;
export const RecentBlockhashesEntry = pick({
blockhash: string(),
feeCalculator: pick({
lamportsPerSignature: string(),
}),
});
export type RecentBlockhashesInfo = StructType<typeof RecentBlockhashesInfo>;
export const RecentBlockhashesInfo = array(RecentBlockhashesEntry);
export type SysvarRecentBlockhashesAccount = StructType<
typeof SysvarRecentBlockhashesAccount
>;
export const SysvarRecentBlockhashesAccount = object({
type: literal("recentBlockhashes"),
info: RecentBlockhashesInfo,
});
export type RentInfo = StructType<typeof RentInfo>;
export const RentInfo = pick({
lamportsPerByteYear: string(),
exemptionThreshold: number(),
burnPercent: number(),
});
export type SysvarRentAccount = StructType<typeof SysvarRentAccount>;
export const SysvarRentAccount = object({
type: literal("rent"),
info: RentInfo,
});
export type RewardsInfo = StructType<typeof RewardsInfo>;
export const RewardsInfo = pick({
validatorPointValue: number(),
});
export type SysvarRewardsAccount = StructType<typeof SysvarRewardsAccount>;
export const SysvarRewardsAccount = object({
type: literal("rewards"),
info: RewardsInfo,
});
export type SlotHashEntry = StructType<typeof SlotHashEntry>;
export const SlotHashEntry = pick({
slot: number(),
hash: string(),
});
export type SlotHashesInfo = StructType<typeof SlotHashesInfo>;
export const SlotHashesInfo = array(SlotHashEntry);
export type SysvarSlotHashesAccount = StructType<
typeof SysvarSlotHashesAccount
>;
export const SysvarSlotHashesAccount = object({
type: literal("slotHashes"),
info: SlotHashesInfo,
});
export type SlotHistoryInfo = StructType<typeof SlotHistoryInfo>;
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<typeof StakeHistoryEntryItem>;
export const StakeHistoryEntryItem = pick({
effective: number(),
activating: number(),
deactivating: number(),
});
export type StakeHistoryEntry = StructType<typeof StakeHistoryEntry>;
export const StakeHistoryEntry = pick({
epoch: number(),
stakeHistory: StakeHistoryEntryItem,
});
export type StakeHistoryInfo = StructType<typeof StakeHistoryInfo>;
export const StakeHistoryInfo = array(StakeHistoryEntry);
export type SysvarStakeHistoryAccount = StructType<
typeof SysvarStakeHistoryAccount
>;
export const SysvarStakeHistoryAccount = object({
type: literal("stakeHistory"),
info: StakeHistoryInfo,
});
export type SysvarAccount = StructType<typeof SysvarAccount>;
export const SysvarAccount = union([
SysvarClockAccount,
SysvarEpochScheduleAccount,
SysvarFeesAccount,
SysvarRecentBlockhashesAccount,
SysvarRentAccount,
SysvarRewardsAccount,
SysvarSlotHashesAccount,
SysvarSlotHistoryAccount,
SysvarStakeHistoryAccount,
]);

View File

@ -0,0 +1,62 @@
import {
StructType,
enums,
pick,
number,
array,
object,
nullable,
string,
} from "superstruct";
import { Pubkey } from "validators/pubkey";
export type VoteAccountType = StructType<typeof VoteAccountType>;
export const VoteAccountType = enums(["vote"]);
export type AuthorizedVoter = StructType<typeof AuthorizedVoter>;
export const AuthorizedVoter = pick({
authorizedVoter: Pubkey,
epoch: number(),
});
export type PriorVoter = StructType<typeof PriorVoter>;
export const PriorVoter = pick({
authorizedPubkey: Pubkey,
epochOfLastAuthorizedSwitch: number(),
targetEpoch: number(),
});
export type EpochCredits = StructType<typeof EpochCredits>;
export const EpochCredits = pick({
epoch: number(),
credits: string(),
previousCredits: string(),
});
export type Vote = StructType<typeof Vote>;
export const Vote = object({
slot: number(),
confirmationCount: number(),
});
export type VoteAccountInfo = StructType<typeof VoteAccountInfo>;
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<typeof VoteAccount>;
export const VoteAccount = pick({
type: VoteAccountType,
info: VoteAccountInfo,
});