Explorer Block Page (#12854)
* Solarweave Implementation * Fixed formatting * Revisions and QA * Added block links to transaction page * Create Blockpage * QA and Revisions * Finalized QA & Revisions * QA & Revisions
This commit is contained in:
parent
5919e67c2a
commit
49e11e1f9c
|
@ -64,5 +64,6 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { AccountDetailsPage } from "pages/AccountDetailsPage";
|
|||
import { ClusterStatsPage } from "pages/ClusterStatsPage";
|
||||
import { SupplyPage } from "pages/SupplyPage";
|
||||
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
||||
import { BlockDetailsPage } from "pages/BlockDetailsPage";
|
||||
|
||||
const ADDRESS_ALIASES = ["account", "accounts", "addresses"];
|
||||
const TX_ALIASES = ["txs", "txn", "txns", "transaction", "transactions"];
|
||||
|
@ -43,6 +44,11 @@ function App() {
|
|||
<TransactionDetailsPage signature={match.params.signature} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={"/block/:id"}
|
||||
render={({ match }) => <BlockDetailsPage slot={match.params.id} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={[
|
||||
|
|
|
@ -40,7 +40,7 @@ export function SearchBar() {
|
|||
ref={(ref) => (selectRef.current = ref)}
|
||||
options={buildOptions(search, cluster)}
|
||||
noOptionsMessage={() => "No Results"}
|
||||
placeholder="Search for accounts, transactions, programs, and tokens"
|
||||
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||
value={resetValue}
|
||||
inputValue={search}
|
||||
blurInputOnSelect
|
||||
|
@ -189,6 +189,19 @@ function buildOptions(search: string, cluster: Cluster) {
|
|||
options.push(tokenOptions);
|
||||
}
|
||||
|
||||
if (!isNaN(Number(search))) {
|
||||
options.push({
|
||||
label: "Block",
|
||||
options: [
|
||||
{
|
||||
label: `Slot #${search}`,
|
||||
value: search,
|
||||
pathname: `/block/${search}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Prefer nice suggestions over raw suggestions
|
||||
if (options.length > 0) return options;
|
||||
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import bs58 from "bs58";
|
||||
import React from "react";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { useBlock, useFetchBlock, FetchStatus } from "providers/block";
|
||||
import { Signature } from "components/common/Signature";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { Slot } from "components/common/Slot";
|
||||
|
||||
export function BlockHistoryCard({ slot }: { slot: number }) {
|
||||
const confirmedBlock = useBlock(slot);
|
||||
const fetchBlock = useFetchBlock();
|
||||
const refresh = () => fetchBlock(slot);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!confirmedBlock) refresh();
|
||||
}, [confirmedBlock, slot]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (!confirmedBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (confirmedBlock.data === undefined) {
|
||||
if (confirmedBlock.status === FetchStatus.Fetching) {
|
||||
return <LoadingCard message="Loading block" />;
|
||||
}
|
||||
|
||||
return <ErrorCard retry={refresh} text="Failed to fetch block" />;
|
||||
}
|
||||
|
||||
if (confirmedBlock.status === FetchStatus.FetchFailed) {
|
||||
return <ErrorCard retry={refresh} text="Failed to fetch block" />;
|
||||
}
|
||||
|
||||
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">Slot</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<Slot slot={Number(slot)} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Parent Slot</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<Slot slot={confirmedBlock.data.parentSlot} link />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Blockhash</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<span>{confirmedBlock.data.blockhash}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="w-100">Previous Blockhash</td>
|
||||
<td className="text-lg-right text-monospace">
|
||||
<span>{confirmedBlock.data.previousBlockhash}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Block Transactions</h3>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-muted">Result</th>
|
||||
<th className="text-muted">Transaction Signature</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{confirmedBlock.data.transactions.map((tx, i) => {
|
||||
let statusText;
|
||||
let statusClass;
|
||||
let signature: React.ReactNode;
|
||||
if (tx.meta?.err || !tx.transaction.signature) {
|
||||
statusClass = "warning";
|
||||
statusText = "Failed";
|
||||
} else {
|
||||
statusClass = "success";
|
||||
statusText = "Success";
|
||||
}
|
||||
|
||||
if (tx.transaction.signature) {
|
||||
signature = (
|
||||
<Signature
|
||||
signature={bs58.encode(tx.transaction.signature)}
|
||||
link
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<span className={`badge badge-soft-${statusClass}`}>
|
||||
{statusText}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>{signature}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,40 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type CopyState = "copy" | "copied";
|
||||
type Props = {
|
||||
slot: number;
|
||||
link?: boolean;
|
||||
};
|
||||
export function Slot({ slot }: Props) {
|
||||
return <span className="text-monospace">{slot.toLocaleString("en-US")}</span>;
|
||||
export function Slot({ slot, link }: Props) {
|
||||
const [state, setState] = useState<CopyState>("copy");
|
||||
|
||||
const copyToClipboard = () => navigator.clipboard.writeText(slot.toString());
|
||||
const handleClick = () =>
|
||||
copyToClipboard().then(() => {
|
||||
setState("copied");
|
||||
setTimeout(() => setState("copy"), 1000);
|
||||
});
|
||||
|
||||
const copyIcon =
|
||||
state === "copy" ? (
|
||||
<span className="fe fe-copy" onClick={handleClick}></span>
|
||||
) : (
|
||||
<span className="fe fe-check-circle"></span>
|
||||
);
|
||||
|
||||
const copyButton = (
|
||||
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
|
||||
);
|
||||
|
||||
return link ? (
|
||||
<span className="text-monospace">
|
||||
{copyButton}
|
||||
<Link className="" to={`/block/${slot}`}>
|
||||
{slot.toLocaleString("en-US")}
|
||||
</Link>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-monospace">{slot.toLocaleString("en-US")}</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { RichListProvider } from "./providers/richList";
|
|||
import { SupplyProvider } from "./providers/supply";
|
||||
import { TransactionsProvider } from "./providers/transactions";
|
||||
import { AccountsProvider } from "./providers/accounts";
|
||||
import { BlockProvider } from "./providers/block";
|
||||
import { StatsProvider } from "providers/stats";
|
||||
import { MintsProvider } from "providers/mints";
|
||||
|
||||
|
@ -26,11 +27,13 @@ ReactDOM.render(
|
|||
<SupplyProvider>
|
||||
<RichListProvider>
|
||||
<AccountsProvider>
|
||||
<MintsProvider>
|
||||
<TransactionsProvider>
|
||||
<App />
|
||||
</TransactionsProvider>
|
||||
</MintsProvider>
|
||||
<BlockProvider>
|
||||
<MintsProvider>
|
||||
<TransactionsProvider>
|
||||
<App />
|
||||
</TransactionsProvider>
|
||||
</MintsProvider>
|
||||
</BlockProvider>
|
||||
</AccountsProvider>
|
||||
</RichListProvider>
|
||||
</SupplyProvider>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
|
||||
import { BlockHistoryCard } from "components/account/BlockHistoryCard";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
|
||||
type Props = { slot: string };
|
||||
|
||||
export function BlockDetailsPage({ slot }: Props) {
|
||||
let output = <ErrorCard text={`Block ${slot} is not valid`} />;
|
||||
|
||||
if (!isNaN(Number(slot))) {
|
||||
output = <BlockHistoryCard slot={Number(slot)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mt-n3">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<h6 className="header-pretitle">Details</h6>
|
||||
<h2 className="header-title">Block</h2>
|
||||
</div>
|
||||
</div>
|
||||
{output}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -20,7 +20,6 @@ import { StakeDetailsCard } from "components/instruction/stake/StakeDetailsCard"
|
|||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { Slot } from "components/common/Slot";
|
||||
import { displayTimestamp } from "utils/date";
|
||||
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||
import { Address } from "components/common/Address";
|
||||
|
@ -29,6 +28,7 @@ import { intoTransactionInstruction, isSerumInstruction } from "utils/tx";
|
|||
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
||||
import { FetchStatus } from "providers/cache";
|
||||
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
||||
import { Slot } from "components/common/Slot";
|
||||
|
||||
const AUTO_REFRESH_INTERVAL = 2000;
|
||||
const ZERO_CONFIRMATION_BAILOUT = 5;
|
||||
|
@ -249,7 +249,7 @@ function StatusCard({
|
|||
<tr>
|
||||
<td>Block</td>
|
||||
<td className="text-lg-right">
|
||||
<Slot slot={info.slot} />
|
||||
<Slot slot={info.slot} link />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -264,9 +264,7 @@ function StatusCard({
|
|||
</InfoTooltip>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-lg-right">
|
||||
<code>{blockhash}</code>
|
||||
</td>
|
||||
<td className="text-lg-right">{blockhash}</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
import React from "react";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import * as Cache from "providers/cache";
|
||||
import { Connection, ConfirmedBlock } from "@solana/web3.js";
|
||||
import { useCluster, Cluster } from "./cluster";
|
||||
|
||||
export enum FetchStatus {
|
||||
Fetching,
|
||||
FetchFailed,
|
||||
Fetched,
|
||||
}
|
||||
|
||||
export enum ActionType {
|
||||
Update,
|
||||
Clear,
|
||||
}
|
||||
|
||||
type State = Cache.State<ConfirmedBlock>;
|
||||
type Dispatch = Cache.Dispatch<ConfirmedBlock>;
|
||||
|
||||
const StateContext = React.createContext<State | undefined>(undefined);
|
||||
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
type BlockProviderProps = { children: React.ReactNode };
|
||||
|
||||
export function BlockProvider({ children }: BlockProviderProps) {
|
||||
const { url } = useCluster();
|
||||
const [state, dispatch] = Cache.useReducer<ConfirmedBlock>(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 useBlock(
|
||||
key: number
|
||||
): Cache.CacheEntry<ConfirmedBlock> | undefined {
|
||||
const context = React.useContext(StateContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(`useBlock must be used within a BlockProvider`);
|
||||
}
|
||||
|
||||
return context.entries[key];
|
||||
}
|
||||
|
||||
export async function fetchBlock(
|
||||
dispatch: Dispatch,
|
||||
url: string,
|
||||
cluster: Cluster,
|
||||
key: number
|
||||
) {
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
status: FetchStatus.Fetching,
|
||||
key,
|
||||
url,
|
||||
});
|
||||
let status = FetchStatus.Fetching;
|
||||
let data: ConfirmedBlock = {
|
||||
blockhash: "",
|
||||
previousBlockhash: "",
|
||||
parentSlot: 0,
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const connection = new Connection(url, "max");
|
||||
data = await connection.getConfirmedBlock(Number(key));
|
||||
status = FetchStatus.Fetched;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (cluster !== Cluster.Custom) {
|
||||
Sentry.captureException(error, { tags: { url } });
|
||||
}
|
||||
status = FetchStatus.FetchFailed;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ActionType.Update,
|
||||
url,
|
||||
key,
|
||||
status,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function useFetchBlock() {
|
||||
const { cluster, url } = useCluster();
|
||||
const state = React.useContext(StateContext);
|
||||
const dispatch = React.useContext(DispatchContext);
|
||||
|
||||
if (!state || !dispatch) {
|
||||
throw new Error(`useFetchBlock must be used within a BlockProvider`);
|
||||
}
|
||||
|
||||
return React.useCallback(
|
||||
(key: number) => {
|
||||
const entry = state.entries[key];
|
||||
if (!entry) {
|
||||
fetchBlock(dispatch, url, cluster, key);
|
||||
}
|
||||
},
|
||||
[state, dispatch, cluster, url]
|
||||
);
|
||||
}
|
|
@ -26,7 +26,7 @@ export enum ActionType {
|
|||
export type Update<T> = {
|
||||
type: ActionType.Update;
|
||||
url: string;
|
||||
key: string;
|
||||
key: string | number;
|
||||
status: FetchStatus;
|
||||
data?: T;
|
||||
};
|
||||
|
|
|
@ -186,7 +186,11 @@ async function updateCluster(
|
|||
if (cluster !== Cluster.Custom) {
|
||||
reportError(error, { clusterUrl: clusterUrl(cluster, customUrl) });
|
||||
}
|
||||
dispatch({ status: ClusterStatus.Failure, cluster, customUrl });
|
||||
dispatch({
|
||||
status: ClusterStatus.Failure,
|
||||
cluster,
|
||||
customUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue