explorer: Add epoch details page (#20335)
This commit is contained in:
parent
8db9586599
commit
5952b65932
|
@ -13,6 +13,7 @@ import { ClusterStatsPage } from "pages/ClusterStatsPage";
|
||||||
import { SupplyPage } from "pages/SupplyPage";
|
import { SupplyPage } from "pages/SupplyPage";
|
||||||
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
||||||
import { BlockDetailsPage } from "pages/BlockDetailsPage";
|
import { BlockDetailsPage } from "pages/BlockDetailsPage";
|
||||||
|
import { EpochDetailsPage } from "pages/EpochDetailsPage";
|
||||||
|
|
||||||
const ADDRESS_ALIASES = ["account", "accounts", "addresses"];
|
const ADDRESS_ALIASES = ["account", "accounts", "addresses"];
|
||||||
const TX_ALIASES = ["txs", "txn", "txns", "transaction", "transactions"];
|
const TX_ALIASES = ["txs", "txn", "txns", "transaction", "transactions"];
|
||||||
|
@ -52,6 +53,11 @@ function App() {
|
||||||
<TransactionDetailsPage signature={match.params.signature} />
|
<TransactionDetailsPage signature={match.params.signature} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={"/epoch/:id"}
|
||||||
|
render={({ match }) => <EpochDetailsPage epoch={match.params.id} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={["/block/:id", "/block/:id/:tab"]}
|
path={["/block/:id", "/block/:id/:tab"]}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function SearchBar() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { tokenRegistry } = useTokenRegistry();
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
const { cluster } = useCluster();
|
const { cluster, epochInfo } = useCluster();
|
||||||
|
|
||||||
const onChange = (
|
const onChange = (
|
||||||
{ pathname }: ValueType<any, false>,
|
{ pathname }: ValueType<any, false>,
|
||||||
|
@ -44,7 +44,12 @@ export function SearchBar() {
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<Select
|
<Select
|
||||||
ref={(ref) => (selectRef.current = ref)}
|
ref={(ref) => (selectRef.current = ref)}
|
||||||
options={buildOptions(search, cluster, tokenRegistry)}
|
options={buildOptions(
|
||||||
|
search,
|
||||||
|
cluster,
|
||||||
|
tokenRegistry,
|
||||||
|
epochInfo?.epoch
|
||||||
|
)}
|
||||||
noOptionsMessage={() => "No Results"}
|
noOptionsMessage={() => "No Results"}
|
||||||
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||||
value={resetValue}
|
value={resetValue}
|
||||||
|
@ -195,7 +200,8 @@ function buildTokenOptions(
|
||||||
function buildOptions(
|
function buildOptions(
|
||||||
rawSearch: string,
|
rawSearch: string,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
tokenRegistry: TokenInfoMap
|
tokenRegistry: TokenInfoMap,
|
||||||
|
currentEpoch?: number
|
||||||
) {
|
) {
|
||||||
const search = rawSearch.trim();
|
const search = rawSearch.trim();
|
||||||
if (search.length === 0) return [];
|
if (search.length === 0) return [];
|
||||||
|
@ -238,6 +244,19 @@ function buildOptions(
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (currentEpoch !== undefined && Number(search) <= currentEpoch + 1) {
|
||||||
|
options.push({
|
||||||
|
label: "Epoch",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: `Epoch #${search}`,
|
||||||
|
value: [search],
|
||||||
|
pathname: `/epoch/${search}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer nice suggestions over raw suggestions
|
// Prefer nice suggestions over raw suggestions
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Slot } from "components/common/Slot";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { useAccountInfo } from "providers/accounts";
|
import { useAccountInfo } from "providers/accounts";
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
const MAX_EPOCH = new BN(2).pow(new BN(64)).sub(new BN(1));
|
const MAX_EPOCH = new BN(2).pow(new BN(64)).sub(new BN(1));
|
||||||
|
|
||||||
|
@ -52,7 +53,9 @@ export function RewardsCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={reward.epoch}>
|
<tr key={reward.epoch}>
|
||||||
<td>{reward.epoch}</td>
|
<td>
|
||||||
|
<Epoch epoch={reward.epoch} link />
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Slot slot={reward.effectiveSlot} link />
|
<Slot slot={reward.effectiveSlot} link />
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from "validators/accounts/stake";
|
} from "validators/accounts/stake";
|
||||||
import BN from "bn.js";
|
import BN from "bn.js";
|
||||||
import { StakeActivationData } from "@solana/web3.js";
|
import { StakeActivationData } from "@solana/web3.js";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
const MAX_EPOCH = new BN(2).pow(new BN(64)).sub(new BN(1));
|
const MAX_EPOCH = new BN(2).pow(new BN(64)).sub(new BN(1));
|
||||||
|
|
||||||
|
@ -162,12 +163,12 @@ function DelegationCard({
|
||||||
const delegation = stakeAccount?.stake?.delegation;
|
const delegation = stakeAccount?.stake?.delegation;
|
||||||
if (delegation) {
|
if (delegation) {
|
||||||
voterPubkey = delegation.voter;
|
voterPubkey = delegation.voter;
|
||||||
activationEpoch = delegation.activationEpoch.eq(MAX_EPOCH)
|
if (!delegation.activationEpoch.eq(MAX_EPOCH)) {
|
||||||
? "-"
|
activationEpoch = delegation.activationEpoch.toNumber();
|
||||||
: delegation.activationEpoch.toString();
|
}
|
||||||
deactivationEpoch = delegation.deactivationEpoch.eq(MAX_EPOCH)
|
if (!delegation.deactivationEpoch.eq(MAX_EPOCH)) {
|
||||||
? "-"
|
deactivationEpoch = delegation.deactivationEpoch.toNumber();
|
||||||
: delegation.deactivationEpoch.toString();
|
}
|
||||||
}
|
}
|
||||||
const { stake } = stakeAccount;
|
const { stake } = stakeAccount;
|
||||||
return (
|
return (
|
||||||
|
@ -223,12 +224,23 @@ function DelegationCard({
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Activation Epoch</td>
|
<td>Activation Epoch</td>
|
||||||
<td className="text-lg-right">{activationEpoch}</td>
|
<td className="text-lg-right">
|
||||||
|
{activationEpoch !== undefined ? (
|
||||||
|
<Epoch epoch={activationEpoch} link />
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Deactivation Epoch</td>
|
<td>Deactivation Epoch</td>
|
||||||
<td className="text-lg-right">{deactivationEpoch}</td>
|
<td className="text-lg-right">
|
||||||
|
{deactivationEpoch !== undefined ? (
|
||||||
|
<Epoch epoch={deactivationEpoch} link />
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SolBalance } from "utils";
|
import { SolBalance } from "utils";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
import {
|
import {
|
||||||
SysvarAccount,
|
SysvarAccount,
|
||||||
StakeHistoryInfo,
|
StakeHistoryInfo,
|
||||||
|
@ -55,7 +56,9 @@ export function StakeHistoryCard({
|
||||||
const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
|
const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
|
||||||
return (
|
return (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className="w-1 text-monospace">{entry.epoch}</td>
|
<td className="w-1 text-monospace">
|
||||||
|
<Epoch epoch={entry.epoch} link />
|
||||||
|
</td>
|
||||||
<td className="text-monospace">
|
<td className="text-monospace">
|
||||||
<SolBalance lamports={entry.stakeHistory.effective} />
|
<SolBalance lamports={entry.stakeHistory.effective} />
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from "components/common/Account";
|
} from "components/common/Account";
|
||||||
import { displayTimestamp } from "utils/date";
|
import { displayTimestamp } from "utils/date";
|
||||||
import { Slot } from "components/common/Slot";
|
import { Slot } from "components/common/Slot";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
export function SysvarAccountSection({
|
export function SysvarAccountSection({
|
||||||
account,
|
account,
|
||||||
|
@ -318,13 +319,15 @@ function SysvarAccountClockCard({
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Epoch</td>
|
<td>Epoch</td>
|
||||||
<td className="text-lg-right">{sysvarAccount.info.epoch}</td>
|
<td className="text-lg-right">
|
||||||
|
<Epoch epoch={sysvarAccount.info.epoch} link />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Leader Schedule Epoch</td>
|
<td>Leader Schedule Epoch</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
{sysvarAccount.info.leaderScheduleEpoch}
|
<Epoch epoch={sysvarAccount.info.leaderScheduleEpoch} link />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,6 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
|
||||||
}, [block]);
|
}, [block]);
|
||||||
|
|
||||||
const filteredTransactions = React.useMemo(() => {
|
const filteredTransactions = React.useMemo(() => {
|
||||||
// console.log("Filter: ", filter);
|
|
||||||
// console.log("invocations", transactions);
|
|
||||||
return transactions.filter(({ invocations }) => {
|
return transactions.filter(({ invocations }) => {
|
||||||
if (filter === ALL_TRANSACTIONS) {
|
if (filter === ALL_TRANSACTIONS) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { clusterPath } from "utils/url";
|
||||||
import { BlockProgramsCard } from "./BlockProgramsCard";
|
import { BlockProgramsCard } from "./BlockProgramsCard";
|
||||||
import { BlockAccountsCard } from "./BlockAccountsCard";
|
import { BlockAccountsCard } from "./BlockAccountsCard";
|
||||||
import { displayTimestamp, displayTimestampUtc } from "utils/date";
|
import { displayTimestamp, displayTimestampUtc } from "utils/date";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
export function BlockOverviewCard({
|
export function BlockOverviewCard({
|
||||||
slot,
|
slot,
|
||||||
|
@ -23,7 +24,7 @@ export function BlockOverviewCard({
|
||||||
}) {
|
}) {
|
||||||
const confirmedBlock = useBlock(slot);
|
const confirmedBlock = useBlock(slot);
|
||||||
const fetchBlock = useFetchBlock();
|
const fetchBlock = useFetchBlock();
|
||||||
const { status } = useCluster();
|
const { epochSchedule, status } = useCluster();
|
||||||
const refresh = () => fetchBlock(slot);
|
const refresh = () => fetchBlock(slot);
|
||||||
|
|
||||||
// Fetch block on load
|
// Fetch block on load
|
||||||
|
@ -44,6 +45,7 @@ export function BlockOverviewCard({
|
||||||
|
|
||||||
const block = confirmedBlock.data.block;
|
const block = confirmedBlock.data.block;
|
||||||
const committedTxs = block.transactions.filter((tx) => tx.meta?.err === null);
|
const committedTxs = block.transactions.filter((tx) => tx.meta?.err === null);
|
||||||
|
const epoch = epochSchedule?.getEpoch(slot);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -66,7 +68,7 @@ export function BlockOverviewCard({
|
||||||
<span>{block.blockhash}</span>
|
<span>{block.blockhash}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{block.blockTime && (
|
{block.blockTime ? (
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Timestamp (Local)</td>
|
<td>Timestamp (Local)</td>
|
||||||
|
@ -85,6 +87,11 @@ export function BlockOverviewCard({
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Timestamp</td>
|
||||||
|
<td className="text-lg-right">Unavailable</td>
|
||||||
|
</tr>
|
||||||
)}
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Parent Slot</td>
|
<td className="w-100">Parent Slot</td>
|
||||||
|
@ -92,6 +99,14 @@ export function BlockOverviewCard({
|
||||||
<Slot slot={block.parentSlot} link />
|
<Slot slot={block.parentSlot} link />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{epoch !== undefined && (
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Epoch</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Epoch epoch={epoch} link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Parent Blockhash</td>
|
<td className="w-100">Parent Blockhash</td>
|
||||||
<td className="text-lg-right text-monospace">
|
<td className="text-lg-right text-monospace">
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function Copyable({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = "";
|
let message: string | undefined;
|
||||||
let textColor = "";
|
let textColor = "";
|
||||||
if (state === "copied") {
|
if (state === "copied") {
|
||||||
message = "Copied";
|
message = "Copied";
|
||||||
|
@ -56,7 +56,7 @@ export function Copyable({
|
||||||
<>
|
<>
|
||||||
<span className="font-size-tiny mr-2">
|
<span className="font-size-tiny mr-2">
|
||||||
<span className={textColor}>
|
<span className={textColor}>
|
||||||
<span className="mr-2">{message}</span>
|
{message !== undefined && <span className="mr-2">{message}</span>}
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { clusterPath } from "utils/url";
|
||||||
|
import { Copyable } from "./Copyable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
epoch: number;
|
||||||
|
link?: boolean;
|
||||||
|
};
|
||||||
|
export function Epoch({ epoch, link }: Props) {
|
||||||
|
return (
|
||||||
|
<span className="text-monospace">
|
||||||
|
{link ? (
|
||||||
|
<Copyable text={epoch.toString()}>
|
||||||
|
<Link to={clusterPath(`/epoch/${epoch}`)}>
|
||||||
|
{epoch.toLocaleString("en-US")}
|
||||||
|
</Link>
|
||||||
|
</Copyable>
|
||||||
|
) : (
|
||||||
|
epoch.toLocaleString("en-US")
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,15 +8,17 @@ type Props = {
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
};
|
};
|
||||||
export function Slot({ slot, link }: Props) {
|
export function Slot({ slot, link }: Props) {
|
||||||
return link ? (
|
return (
|
||||||
<Copyable text={slot.toString()}>
|
<span className="text-monospace">
|
||||||
<span className="text-monospace">
|
{link ? (
|
||||||
<Link to={clusterPath(`/block/${slot}`)}>
|
<Copyable text={slot.toString()}>
|
||||||
{slot.toLocaleString("en-US")}
|
<Link to={clusterPath(`/block/${slot}`)}>
|
||||||
</Link>
|
{slot.toLocaleString("en-US")}
|
||||||
</span>
|
</Link>
|
||||||
</Copyable>
|
</Copyable>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-monospace">{slot.toLocaleString("en-US")}</span>
|
slot.toLocaleString("en-US")
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { InstructionCard } from "../InstructionCard";
|
||||||
import { Address } from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
import { InitializeInfo } from "./types";
|
import { InitializeInfo } from "./types";
|
||||||
import { displayTimestampUtc } from "utils/date";
|
import { displayTimestampUtc } from "utils/date";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
export function InitializeDetailsCard(props: {
|
export function InitializeDetailsCard(props: {
|
||||||
ix: ParsedInstruction;
|
ix: ParsedInstruction;
|
||||||
|
@ -60,7 +61,9 @@ export function InitializeDetailsCard(props: {
|
||||||
{info.lockup.epoch > 0 && (
|
{info.lockup.epoch > 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Lockup Expiry Epoch</td>
|
<td>Lockup Expiry Epoch</td>
|
||||||
<td className="text-lg-right">{info.lockup.epoch}</td>
|
<td className="text-lg-right">
|
||||||
|
<Epoch epoch={info.lockup.epoch} link />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { SupplyProvider } from "./providers/supply";
|
||||||
import { TransactionsProvider } from "./providers/transactions";
|
import { TransactionsProvider } from "./providers/transactions";
|
||||||
import { AccountsProvider } from "./providers/accounts";
|
import { AccountsProvider } from "./providers/accounts";
|
||||||
import { BlockProvider } from "./providers/block";
|
import { BlockProvider } from "./providers/block";
|
||||||
|
import { EpochProvider } from "./providers/epoch";
|
||||||
import { StatsProvider } from "providers/stats";
|
import { StatsProvider } from "providers/stats";
|
||||||
import { MintsProvider } from "providers/mints";
|
import { MintsProvider } from "providers/mints";
|
||||||
|
|
||||||
|
@ -27,11 +28,13 @@ ReactDOM.render(
|
||||||
<RichListProvider>
|
<RichListProvider>
|
||||||
<AccountsProvider>
|
<AccountsProvider>
|
||||||
<BlockProvider>
|
<BlockProvider>
|
||||||
<MintsProvider>
|
<EpochProvider>
|
||||||
<TransactionsProvider>
|
<MintsProvider>
|
||||||
<App />
|
<TransactionsProvider>
|
||||||
</TransactionsProvider>
|
<App />
|
||||||
</MintsProvider>
|
</TransactionsProvider>
|
||||||
|
</MintsProvider>
|
||||||
|
</EpochProvider>
|
||||||
</BlockProvider>
|
</BlockProvider>
|
||||||
</AccountsProvider>
|
</AccountsProvider>
|
||||||
</RichListProvider>
|
</RichListProvider>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { ErrorCard } from "components/common/ErrorCard";
|
||||||
import { LoadingCard } from "components/common/LoadingCard";
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
import { useVoteAccounts } from "providers/accounts/vote-accounts";
|
import { useVoteAccounts } from "providers/accounts/vote-accounts";
|
||||||
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
|
||||||
const CLUSTER_STATS_TIMEOUT = 5000;
|
const CLUSTER_STATS_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
@ -225,7 +226,6 @@ function StatsCardBody() {
|
||||||
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
||||||
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
|
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
|
||||||
const { slotIndex, slotsInEpoch } = epochInfo;
|
const { slotIndex, slotsInEpoch } = epochInfo;
|
||||||
const currentEpoch = epochInfo.epoch.toString();
|
|
||||||
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
||||||
const epochTimeRemaining = slotsToHumanString(
|
const epochTimeRemaining = slotsToHumanString(
|
||||||
slotsInEpoch - slotIndex,
|
slotsInEpoch - slotIndex,
|
||||||
|
@ -267,7 +267,9 @@ function StatsCardBody() {
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Epoch</td>
|
<td className="w-100">Epoch</td>
|
||||||
<td className="text-lg-right text-monospace">{currentEpoch}</td>
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Epoch epoch={epochInfo.epoch} link />
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Epoch progress</td>
|
<td className="w-100">Epoch progress</td>
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { ClusterStatus, useCluster } from "providers/cluster";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
|
import { Epoch } from "components/common/Epoch";
|
||||||
|
import { Slot } from "components/common/Slot";
|
||||||
|
import { useEpoch, useFetchEpoch } from "providers/epoch";
|
||||||
|
import { displayTimestampUtc } from "utils/date";
|
||||||
|
|
||||||
|
type Props = { epoch: string };
|
||||||
|
export function EpochDetailsPage({ epoch }: Props) {
|
||||||
|
let output;
|
||||||
|
if (isNaN(Number(epoch))) {
|
||||||
|
output = <ErrorCard text={`Epoch ${epoch} is not valid`} />;
|
||||||
|
} else {
|
||||||
|
output = <EpochOverviewCard epoch={Number(epoch)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-n3">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body">
|
||||||
|
<h6 className="header-pretitle">Details</h6>
|
||||||
|
<h2 className="header-title">Epoch</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{output}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverviewProps = { epoch: number };
|
||||||
|
function EpochOverviewCard({ epoch }: OverviewProps) {
|
||||||
|
const { status, epochSchedule, epochInfo } = useCluster();
|
||||||
|
|
||||||
|
const epochState = useEpoch(epoch);
|
||||||
|
const fetchEpoch = useFetchEpoch();
|
||||||
|
|
||||||
|
// Fetch extra epoch info on load
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!epochSchedule || !epochInfo) return;
|
||||||
|
const currentEpoch = epochInfo.epoch;
|
||||||
|
if (
|
||||||
|
epoch <= currentEpoch &&
|
||||||
|
!epochState &&
|
||||||
|
status === ClusterStatus.Connected
|
||||||
|
)
|
||||||
|
fetchEpoch(epoch, currentEpoch, epochSchedule);
|
||||||
|
}, [epoch, epochState, epochInfo, epochSchedule, status, fetchEpoch]);
|
||||||
|
|
||||||
|
if (!epochSchedule || !epochInfo) {
|
||||||
|
return <LoadingCard message="Connecting to cluster" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEpoch = epochInfo.epoch;
|
||||||
|
if (epoch > currentEpoch) {
|
||||||
|
return <ErrorCard text={`Epoch ${epoch} hasn't started yet`} />;
|
||||||
|
} else if (!epochState?.data) {
|
||||||
|
return <LoadingCard message="Loading epoch" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstSlot = epochSchedule.getFirstSlotInEpoch(epoch);
|
||||||
|
const lastSlot = epochSchedule.getLastSlotInEpoch(epoch);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||||
|
Overview
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<TableCardBody>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Epoch</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Epoch epoch={epoch} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{epoch > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Previous Epoch</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Epoch epoch={epoch - 1} link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Next Epoch</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
{currentEpoch > epoch ? (
|
||||||
|
<Epoch epoch={epoch + 1} link />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">Epoch in progress</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">First Slot</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Slot slot={firstSlot} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Last Slot</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Slot slot={lastSlot} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{epochState.data.firstTimestamp && (
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">First Block Timestamp</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<span className="text-monospace">
|
||||||
|
{displayTimestampUtc(
|
||||||
|
epochState.data.firstTimestamp * 1000,
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">First Block</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Slot slot={epochState.data.firstBlock} link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Last Block</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
{epochState.data.lastBlock !== undefined ? (
|
||||||
|
<Slot slot={epochState.data.lastBlock} link />
|
||||||
|
) : (
|
||||||
|
<span className="text-muted">Epoch in progress</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{epochState.data.lastTimestamp && (
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Last Block Timestamp</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<span className="text-monospace">
|
||||||
|
{displayTimestampUtc(
|
||||||
|
epochState.data.lastTimestamp * 1000,
|
||||||
|
true
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</TableCardBody>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,10 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { clusterApiUrl, Connection } from "@solana/web3.js";
|
import {
|
||||||
|
clusterApiUrl,
|
||||||
|
Connection,
|
||||||
|
EpochInfo,
|
||||||
|
EpochSchedule,
|
||||||
|
} from "@solana/web3.js";
|
||||||
import { useQuery } from "../utils/url";
|
import { useQuery } from "../utils/url";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
|
@ -74,6 +79,8 @@ interface State {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
customUrl: string;
|
customUrl: string;
|
||||||
firstAvailableBlock?: number;
|
firstAvailableBlock?: number;
|
||||||
|
epochSchedule?: EpochSchedule;
|
||||||
|
epochInfo?: EpochInfo;
|
||||||
status: ClusterStatus;
|
status: ClusterStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +89,8 @@ interface Action {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
customUrl: string;
|
customUrl: string;
|
||||||
firstAvailableBlock?: number;
|
firstAvailableBlock?: number;
|
||||||
|
epochSchedule?: EpochSchedule;
|
||||||
|
epochInfo?: EpochInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dispatch = (action: Action) => void;
|
type Dispatch = (action: Action) => void;
|
||||||
|
@ -191,11 +200,15 @@ async function updateCluster(
|
||||||
try {
|
try {
|
||||||
const connection = new Connection(clusterUrl(cluster, customUrl));
|
const connection = new Connection(clusterUrl(cluster, customUrl));
|
||||||
const firstAvailableBlock = await connection.getFirstAvailableBlock();
|
const firstAvailableBlock = await connection.getFirstAvailableBlock();
|
||||||
|
const epochSchedule = await connection.getEpochSchedule();
|
||||||
|
const epochInfo = await connection.getEpochInfo();
|
||||||
dispatch({
|
dispatch({
|
||||||
status: ClusterStatus.Connected,
|
status: ClusterStatus.Connected,
|
||||||
cluster,
|
cluster,
|
||||||
customUrl,
|
customUrl,
|
||||||
firstAvailableBlock,
|
firstAvailableBlock,
|
||||||
|
epochSchedule,
|
||||||
|
epochInfo,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cluster !== Cluster.Custom) {
|
if (cluster !== Cluster.Custom) {
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import * as Cache from "providers/cache";
|
||||||
|
import { Connection, EpochSchedule } from "@solana/web3.js";
|
||||||
|
import { useCluster, Cluster } from "./cluster";
|
||||||
|
|
||||||
|
export enum FetchStatus {
|
||||||
|
Fetching,
|
||||||
|
FetchFailed,
|
||||||
|
Fetched,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
Update,
|
||||||
|
Clear,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Epoch = {
|
||||||
|
firstBlock: number;
|
||||||
|
firstTimestamp: number | null;
|
||||||
|
lastBlock?: number;
|
||||||
|
lastTimestamp: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = Cache.State<Epoch>;
|
||||||
|
type Dispatch = Cache.Dispatch<Epoch>;
|
||||||
|
|
||||||
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
|
type EpochProviderProps = { children: React.ReactNode };
|
||||||
|
|
||||||
|
export function EpochProvider({ children }: EpochProviderProps) {
|
||||||
|
const { url } = useCluster();
|
||||||
|
const [state, dispatch] = Cache.useReducer<Epoch>(url);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
dispatch({ type: ActionType.Clear, url });
|
||||||
|
}, [dispatch, url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={state}>
|
||||||
|
<DispatchContext.Provider value={dispatch}>
|
||||||
|
{children}
|
||||||
|
</DispatchContext.Provider>
|
||||||
|
</StateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEpoch(key: number): Cache.CacheEntry<Epoch> | undefined {
|
||||||
|
const context = React.useContext(StateContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useEpoch must be used within a EpochProvider`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.entries[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchEpoch(
|
||||||
|
dispatch: Dispatch,
|
||||||
|
url: string,
|
||||||
|
cluster: Cluster,
|
||||||
|
epochSchedule: EpochSchedule,
|
||||||
|
currentEpoch: number,
|
||||||
|
epoch: number
|
||||||
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
status: FetchStatus.Fetching,
|
||||||
|
key: epoch,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
|
||||||
|
let status: FetchStatus;
|
||||||
|
let data: Epoch | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const connection = new Connection(url, "confirmed");
|
||||||
|
const firstSlot = epochSchedule.getFirstSlotInEpoch(epoch);
|
||||||
|
const lastSlot = epochSchedule.getLastSlotInEpoch(epoch);
|
||||||
|
const [firstBlock, lastBlock] = await Promise.all([
|
||||||
|
(async () => {
|
||||||
|
const firstBlocks = await connection.getBlocks(
|
||||||
|
firstSlot,
|
||||||
|
firstSlot + 100
|
||||||
|
);
|
||||||
|
return firstBlocks.shift();
|
||||||
|
})(),
|
||||||
|
(async () => {
|
||||||
|
const lastBlocks = await connection.getBlocks(
|
||||||
|
Math.max(0, lastSlot - 100),
|
||||||
|
lastSlot
|
||||||
|
);
|
||||||
|
return lastBlocks.pop();
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (firstBlock === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to find confirmed block at start of epoch ${epoch}`
|
||||||
|
);
|
||||||
|
} else if (epoch < currentEpoch && lastBlock === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`failed to find confirmed block at end of epoch ${epoch}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstTimestamp, lastTimestamp] = await Promise.all([
|
||||||
|
connection.getBlockTime(firstBlock),
|
||||||
|
lastBlock ? connection.getBlockTime(lastBlock) : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
data = {
|
||||||
|
firstBlock,
|
||||||
|
lastBlock,
|
||||||
|
firstTimestamp,
|
||||||
|
lastTimestamp,
|
||||||
|
};
|
||||||
|
status = FetchStatus.Fetched;
|
||||||
|
} catch (err) {
|
||||||
|
status = FetchStatus.FetchFailed;
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
Sentry.captureException(err, { tags: { url } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ActionType.Update,
|
||||||
|
url,
|
||||||
|
key: epoch,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFetchEpoch() {
|
||||||
|
const dispatch = React.useContext(DispatchContext);
|
||||||
|
if (!dispatch) {
|
||||||
|
throw new Error(`useFetchEpoch must be used within a EpochProvider`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cluster, url } = useCluster();
|
||||||
|
return React.useCallback(
|
||||||
|
(key: number, currentEpoch: number, epochSchedule: EpochSchedule) =>
|
||||||
|
fetchEpoch(dispatch, url, cluster, epochSchedule, currentEpoch, key),
|
||||||
|
[dispatch, cluster, url]
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue