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:
parent
f1bbe1cd84
commit
86ca85d72b
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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 <></>;
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import {
|
||||||
useFetchAccountInfo,
|
useFetchAccountInfo,
|
||||||
useAccountInfo,
|
useAccountInfo,
|
||||||
Account,
|
Account,
|
||||||
|
ProgramData,
|
||||||
} from "providers/accounts";
|
} from "providers/accounts";
|
||||||
import { StakeAccountSection } from "components/account/StakeAccountSection";
|
import { StakeAccountSection } from "components/account/StakeAccountSection";
|
||||||
import { TokenAccountSection } from "components/account/TokenAccountSection";
|
import { TokenAccountSection } from "components/account/TokenAccountSection";
|
||||||
|
@ -19,6 +20,50 @@ import { TransactionHistoryCard } from "components/account/TransactionHistoryCar
|
||||||
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
|
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
|
||||||
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
|
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
|
||||||
import { TokenRegistry } from "tokenRegistry";
|
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 };
|
type Props = { address: string; tab?: string };
|
||||||
export function AccountDetailsPage({ address, tab }: Props) {
|
export function AccountDetailsPage({ address, tab }: Props) {
|
||||||
|
@ -101,30 +146,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
|
||||||
|
|
||||||
const account = info.data;
|
const account = info.data;
|
||||||
const data = account?.details?.data;
|
const data = account?.details?.data;
|
||||||
|
const tabs = getTabs(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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let moreTab: MoreTabs = "history";
|
let moreTab: MoreTabs = "history";
|
||||||
if (tab && tabs.filter(({ slug }) => slug === tab).length === 0) {
|
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") {
|
} else if (data && data.program === "spl-token") {
|
||||||
return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
|
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 {
|
} else {
|
||||||
return <UnknownAccountCard account={account} />;
|
return <UnknownAccountCard account={account} />;
|
||||||
}
|
}
|
||||||
|
@ -175,7 +209,15 @@ type Tab = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MoreTabs = "history" | "tokens" | "largest";
|
type MoreTabs =
|
||||||
|
| "history"
|
||||||
|
| "tokens"
|
||||||
|
| "largest"
|
||||||
|
| "vote-history"
|
||||||
|
| "slot-hashes"
|
||||||
|
| "stake-history"
|
||||||
|
| "blockhashes";
|
||||||
|
|
||||||
function MoreSection({
|
function MoreSection({
|
||||||
account,
|
account,
|
||||||
tab,
|
tab,
|
||||||
|
@ -187,7 +229,7 @@ function MoreSection({
|
||||||
}) {
|
}) {
|
||||||
const pubkey = account.pubkey;
|
const pubkey = account.pubkey;
|
||||||
const address = account.pubkey.toBase58();
|
const address = account.pubkey.toBase58();
|
||||||
|
const data = account?.details?.data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
@ -217,6 +259,63 @@ function MoreSection({
|
||||||
)}
|
)}
|
||||||
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
||||||
{tab === "largest" && <TokenLargestAccountsCard 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;
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ import {
|
||||||
import * as Cache from "providers/cache";
|
import * as Cache from "providers/cache";
|
||||||
import { ActionType, FetchStatus } from "providers/cache";
|
import { ActionType, FetchStatus } from "providers/cache";
|
||||||
import { reportError } from "utils/sentry";
|
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 { useAccountHistory } from "./history";
|
||||||
|
|
||||||
export type StakeProgramData = {
|
export type StakeProgramData = {
|
||||||
|
@ -33,7 +37,33 @@ export type TokenProgramData = {
|
||||||
parsed: TokenAccount;
|
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 {
|
export interface Details {
|
||||||
executable: boolean;
|
executable: boolean;
|
||||||
|
@ -134,20 +164,54 @@ async function fetchAccountInfo(
|
||||||
reportError(err, { url, address: pubkey.toBase58() });
|
reportError(err, { url, address: pubkey.toBase58() });
|
||||||
// TODO store error state in Account info
|
// 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) {
|
} else if ("parsed" in result.data) {
|
||||||
if (result.owner.equals(TOKEN_PROGRAM_ID)) {
|
try {
|
||||||
try {
|
const info = coerce(result.data.parsed, ParsedInfo);
|
||||||
const info = coerce(result.data.parsed, ParsedInfo);
|
switch (result.data.program) {
|
||||||
const parsed = coerce(info, TokenAccount);
|
case "vote":
|
||||||
|
data = {
|
||||||
data = {
|
program: result.data.program,
|
||||||
program: "spl-token",
|
parsed: coerce(info, VoteAccount),
|
||||||
parsed,
|
};
|
||||||
};
|
break;
|
||||||
} catch (err) {
|
case "nonce":
|
||||||
reportError(err, { url, address: pubkey.toBase58() });
|
data = {
|
||||||
// TODO store error state in Account info
|
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() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
]);
|
|
@ -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,
|
||||||
|
});
|
|
@ -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,
|
||||||
|
]);
|
|
@ -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,
|
||||||
|
});
|
Loading…
Reference in New Issue