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 { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
||||
import { BlockDetailsPage } from "pages/BlockDetailsPage";
|
||||
import { EpochDetailsPage } from "pages/EpochDetailsPage";
|
||||
|
||||
const ADDRESS_ALIASES = ["account", "accounts", "addresses"];
|
||||
const TX_ALIASES = ["txs", "txn", "txns", "transaction", "transactions"];
|
||||
|
@ -52,6 +53,11 @@ function App() {
|
|||
<TransactionDetailsPage signature={match.params.signature} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={"/epoch/:id"}
|
||||
render={({ match }) => <EpochDetailsPage epoch={match.params.id} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={["/block/:id", "/block/:id/:tab"]}
|
||||
|
|
|
@ -21,7 +21,7 @@ export function SearchBar() {
|
|||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { tokenRegistry } = useTokenRegistry();
|
||||
const { cluster } = useCluster();
|
||||
const { cluster, epochInfo } = useCluster();
|
||||
|
||||
const onChange = (
|
||||
{ pathname }: ValueType<any, false>,
|
||||
|
@ -44,7 +44,12 @@ export function SearchBar() {
|
|||
<div className="col">
|
||||
<Select
|
||||
ref={(ref) => (selectRef.current = ref)}
|
||||
options={buildOptions(search, cluster, tokenRegistry)}
|
||||
options={buildOptions(
|
||||
search,
|
||||
cluster,
|
||||
tokenRegistry,
|
||||
epochInfo?.epoch
|
||||
)}
|
||||
noOptionsMessage={() => "No Results"}
|
||||
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||
value={resetValue}
|
||||
|
@ -195,7 +200,8 @@ function buildTokenOptions(
|
|||
function buildOptions(
|
||||
rawSearch: string,
|
||||
cluster: Cluster,
|
||||
tokenRegistry: TokenInfoMap
|
||||
tokenRegistry: TokenInfoMap,
|
||||
currentEpoch?: number
|
||||
) {
|
||||
const search = rawSearch.trim();
|
||||
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
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Slot } from "components/common/Slot";
|
|||
import { lamportsToSolString } from "utils";
|
||||
import { useAccountInfo } from "providers/accounts";
|
||||
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));
|
||||
|
||||
|
@ -52,7 +53,9 @@ export function RewardsCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
|
||||
return (
|
||||
<tr key={reward.epoch}>
|
||||
<td>{reward.epoch}</td>
|
||||
<td>
|
||||
<Epoch epoch={reward.epoch} link />
|
||||
</td>
|
||||
<td>
|
||||
<Slot slot={reward.effectiveSlot} link />
|
||||
</td>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from "validators/accounts/stake";
|
||||
import BN from "bn.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));
|
||||
|
||||
|
@ -162,12 +163,12 @@ function DelegationCard({
|
|||
const delegation = stakeAccount?.stake?.delegation;
|
||||
if (delegation) {
|
||||
voterPubkey = delegation.voter;
|
||||
activationEpoch = delegation.activationEpoch.eq(MAX_EPOCH)
|
||||
? "-"
|
||||
: delegation.activationEpoch.toString();
|
||||
deactivationEpoch = delegation.deactivationEpoch.eq(MAX_EPOCH)
|
||||
? "-"
|
||||
: delegation.deactivationEpoch.toString();
|
||||
if (!delegation.activationEpoch.eq(MAX_EPOCH)) {
|
||||
activationEpoch = delegation.activationEpoch.toNumber();
|
||||
}
|
||||
if (!delegation.deactivationEpoch.eq(MAX_EPOCH)) {
|
||||
deactivationEpoch = delegation.deactivationEpoch.toNumber();
|
||||
}
|
||||
}
|
||||
const { stake } = stakeAccount;
|
||||
return (
|
||||
|
@ -223,12 +224,23 @@ function DelegationCard({
|
|||
|
||||
<tr>
|
||||
<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>
|
||||
<td>Deactivation Epoch</td>
|
||||
<td className="text-lg-right">{deactivationEpoch}</td>
|
||||
<td className="text-lg-right">
|
||||
{deactivationEpoch !== undefined ? (
|
||||
<Epoch epoch={deactivationEpoch} link />
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { SolBalance } from "utils";
|
||||
import { Epoch } from "components/common/Epoch";
|
||||
import {
|
||||
SysvarAccount,
|
||||
StakeHistoryInfo,
|
||||
|
@ -55,7 +56,9 @@ export function StakeHistoryCard({
|
|||
const renderAccountRow = (entry: StakeHistoryEntry, index: number) => {
|
||||
return (
|
||||
<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">
|
||||
<SolBalance lamports={entry.stakeHistory.effective} />
|
||||
</td>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "components/common/Account";
|
||||
import { displayTimestamp } from "utils/date";
|
||||
import { Slot } from "components/common/Slot";
|
||||
import { Epoch } from "components/common/Epoch";
|
||||
|
||||
export function SysvarAccountSection({
|
||||
account,
|
||||
|
@ -318,13 +319,15 @@ function SysvarAccountClockCard({
|
|||
|
||||
<tr>
|
||||
<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>
|
||||
<td>Leader Schedule Epoch</td>
|
||||
<td className="text-lg-right">
|
||||
{sysvarAccount.info.leaderScheduleEpoch}
|
||||
<Epoch epoch={sysvarAccount.info.leaderScheduleEpoch} link />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -79,8 +79,6 @@ export function BlockHistoryCard({ block }: { block: BlockResponse }) {
|
|||
}, [block]);
|
||||
|
||||
const filteredTransactions = React.useMemo(() => {
|
||||
// console.log("Filter: ", filter);
|
||||
// console.log("invocations", transactions);
|
||||
return transactions.filter(({ invocations }) => {
|
||||
if (filter === ALL_TRANSACTIONS) {
|
||||
return true;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { clusterPath } from "utils/url";
|
|||
import { BlockProgramsCard } from "./BlockProgramsCard";
|
||||
import { BlockAccountsCard } from "./BlockAccountsCard";
|
||||
import { displayTimestamp, displayTimestampUtc } from "utils/date";
|
||||
import { Epoch } from "components/common/Epoch";
|
||||
|
||||
export function BlockOverviewCard({
|
||||
slot,
|
||||
|
@ -23,7 +24,7 @@ export function BlockOverviewCard({
|
|||
}) {
|
||||
const confirmedBlock = useBlock(slot);
|
||||
const fetchBlock = useFetchBlock();
|
||||
const { status } = useCluster();
|
||||
const { epochSchedule, status } = useCluster();
|
||||
const refresh = () => fetchBlock(slot);
|
||||
|
||||
// Fetch block on load
|
||||
|
@ -44,6 +45,7 @@ export function BlockOverviewCard({
|
|||
|
||||
const block = confirmedBlock.data.block;
|
||||
const committedTxs = block.transactions.filter((tx) => tx.meta?.err === null);
|
||||
const epoch = epochSchedule?.getEpoch(slot);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -66,7 +68,7 @@ export function BlockOverviewCard({
|
|||
<span>{block.blockhash}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{block.blockTime && (
|
||||
{block.blockTime ? (
|
||||
<>
|
||||
<tr>
|
||||
<td>Timestamp (Local)</td>
|
||||
|
@ -85,6 +87,11 @@ export function BlockOverviewCard({
|
|||
</td>
|
||||
</tr>
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td className="w-100">Timestamp</td>
|
||||
<td className="text-lg-right">Unavailable</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className="w-100">Parent Slot</td>
|
||||
|
@ -92,6 +99,14 @@ export function BlockOverviewCard({
|
|||
<Slot slot={block.parentSlot} link />
|
||||
</td>
|
||||
</tr>
|
||||
{epoch !== undefined && (
|
||||
<tr>
|
||||
<td className="w-100">Epoch</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<Epoch epoch={epoch} link />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className="w-100">Parent Blockhash</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
|
|
|
@ -41,7 +41,7 @@ export function Copyable({
|
|||
return null;
|
||||
}
|
||||
|
||||
let message = "";
|
||||
let message: string | undefined;
|
||||
let textColor = "";
|
||||
if (state === "copied") {
|
||||
message = "Copied";
|
||||
|
@ -56,7 +56,7 @@ export function Copyable({
|
|||
<>
|
||||
<span className="font-size-tiny mr-2">
|
||||
<span className={textColor}>
|
||||
<span className="mr-2">{message}</span>
|
||||
{message !== undefined && <span className="mr-2">{message}</span>}
|
||||
<CopyIcon />
|
||||
</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;
|
||||
};
|
||||
export function Slot({ slot, link }: Props) {
|
||||
return link ? (
|
||||
<Copyable text={slot.toString()}>
|
||||
return (
|
||||
<span className="text-monospace">
|
||||
{link ? (
|
||||
<Copyable text={slot.toString()}>
|
||||
<Link to={clusterPath(`/block/${slot}`)}>
|
||||
{slot.toLocaleString("en-US")}
|
||||
</Link>
|
||||
</span>
|
||||
</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 { InitializeInfo } from "./types";
|
||||
import { displayTimestampUtc } from "utils/date";
|
||||
import { Epoch } from "components/common/Epoch";
|
||||
|
||||
export function InitializeDetailsCard(props: {
|
||||
ix: ParsedInstruction;
|
||||
|
@ -60,7 +61,9 @@ export function InitializeDetailsCard(props: {
|
|||
{info.lockup.epoch > 0 && (
|
||||
<tr>
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { SupplyProvider } from "./providers/supply";
|
|||
import { TransactionsProvider } from "./providers/transactions";
|
||||
import { AccountsProvider } from "./providers/accounts";
|
||||
import { BlockProvider } from "./providers/block";
|
||||
import { EpochProvider } from "./providers/epoch";
|
||||
import { StatsProvider } from "providers/stats";
|
||||
import { MintsProvider } from "providers/mints";
|
||||
|
||||
|
@ -27,11 +28,13 @@ ReactDOM.render(
|
|||
<RichListProvider>
|
||||
<AccountsProvider>
|
||||
<BlockProvider>
|
||||
<EpochProvider>
|
||||
<MintsProvider>
|
||||
<TransactionsProvider>
|
||||
<App />
|
||||
</TransactionsProvider>
|
||||
</MintsProvider>
|
||||
</EpochProvider>
|
||||
</BlockProvider>
|
||||
</AccountsProvider>
|
||||
</RichListProvider>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ErrorCard } from "components/common/ErrorCard";
|
|||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { useVoteAccounts } from "providers/accounts/vote-accounts";
|
||||
import { CoingeckoStatus, useCoinGecko } from "utils/coingecko";
|
||||
import { Epoch } from "components/common/Epoch";
|
||||
|
||||
const CLUSTER_STATS_TIMEOUT = 5000;
|
||||
|
||||
|
@ -225,7 +226,6 @@ function StatsCardBody() {
|
|||
const hourlySlotTime = Math.round(1000 * avgSlotTime_1h);
|
||||
const averageSlotTime = Math.round(1000 * avgSlotTime_1min);
|
||||
const { slotIndex, slotsInEpoch } = epochInfo;
|
||||
const currentEpoch = epochInfo.epoch.toString();
|
||||
const epochProgress = ((100 * slotIndex) / slotsInEpoch).toFixed(1) + "%";
|
||||
const epochTimeRemaining = slotsToHumanString(
|
||||
slotsInEpoch - slotIndex,
|
||||
|
@ -267,7 +267,9 @@ function StatsCardBody() {
|
|||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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 { clusterApiUrl, Connection } from "@solana/web3.js";
|
||||
import {
|
||||
clusterApiUrl,
|
||||
Connection,
|
||||
EpochInfo,
|
||||
EpochSchedule,
|
||||
} from "@solana/web3.js";
|
||||
import { useQuery } from "../utils/url";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { reportError } from "utils/sentry";
|
||||
|
@ -74,6 +79,8 @@ interface State {
|
|||
cluster: Cluster;
|
||||
customUrl: string;
|
||||
firstAvailableBlock?: number;
|
||||
epochSchedule?: EpochSchedule;
|
||||
epochInfo?: EpochInfo;
|
||||
status: ClusterStatus;
|
||||
}
|
||||
|
||||
|
@ -82,6 +89,8 @@ interface Action {
|
|||
cluster: Cluster;
|
||||
customUrl: string;
|
||||
firstAvailableBlock?: number;
|
||||
epochSchedule?: EpochSchedule;
|
||||
epochInfo?: EpochInfo;
|
||||
}
|
||||
|
||||
type Dispatch = (action: Action) => void;
|
||||
|
@ -191,11 +200,15 @@ async function updateCluster(
|
|||
try {
|
||||
const connection = new Connection(clusterUrl(cluster, customUrl));
|
||||
const firstAvailableBlock = await connection.getFirstAvailableBlock();
|
||||
const epochSchedule = await connection.getEpochSchedule();
|
||||
const epochInfo = await connection.getEpochInfo();
|
||||
dispatch({
|
||||
status: ClusterStatus.Connected,
|
||||
cluster,
|
||||
customUrl,
|
||||
firstAvailableBlock,
|
||||
epochSchedule,
|
||||
epochInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
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