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:
@ -64,5 +64,6 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { AccountDetailsPage } from "pages/AccountDetailsPage";
|
|||||||
import { ClusterStatsPage } from "pages/ClusterStatsPage";
|
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";
|
||||||
|
|
||||||
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"];
|
||||||
@ -43,6 +44,11 @@ function App() {
|
|||||||
<TransactionDetailsPage signature={match.params.signature} />
|
<TransactionDetailsPage signature={match.params.signature} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={"/block/:id"}
|
||||||
|
render={({ match }) => <BlockDetailsPage slot={match.params.id} />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={[
|
path={[
|
||||||
|
@ -40,7 +40,7 @@ export function SearchBar() {
|
|||||||
ref={(ref) => (selectRef.current = ref)}
|
ref={(ref) => (selectRef.current = ref)}
|
||||||
options={buildOptions(search, cluster)}
|
options={buildOptions(search, cluster)}
|
||||||
noOptionsMessage={() => "No Results"}
|
noOptionsMessage={() => "No Results"}
|
||||||
placeholder="Search for accounts, transactions, programs, and tokens"
|
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||||
value={resetValue}
|
value={resetValue}
|
||||||
inputValue={search}
|
inputValue={search}
|
||||||
blurInputOnSelect
|
blurInputOnSelect
|
||||||
@ -189,6 +189,19 @@ function buildOptions(search: string, cluster: Cluster) {
|
|||||||
options.push(tokenOptions);
|
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
|
// Prefer nice suggestions over raw suggestions
|
||||||
if (options.length > 0) return options;
|
if (options.length > 0) return options;
|
||||||
|
|
||||||
|
124
explorer/src/components/account/BlockHistoryCard.tsx
Normal file
124
explorer/src/components/account/BlockHistoryCard.tsx
Normal file
@ -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 = {
|
type Props = {
|
||||||
slot: number;
|
slot: number;
|
||||||
|
link?: boolean;
|
||||||
};
|
};
|
||||||
export function Slot({ slot }: Props) {
|
export function Slot({ slot, link }: Props) {
|
||||||
return <span className="text-monospace">{slot.toLocaleString("en-US")}</span>;
|
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 { 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 { StatsProvider } from "providers/stats";
|
import { StatsProvider } from "providers/stats";
|
||||||
import { MintsProvider } from "providers/mints";
|
import { MintsProvider } from "providers/mints";
|
||||||
|
|
||||||
@ -26,11 +27,13 @@ ReactDOM.render(
|
|||||||
<SupplyProvider>
|
<SupplyProvider>
|
||||||
<RichListProvider>
|
<RichListProvider>
|
||||||
<AccountsProvider>
|
<AccountsProvider>
|
||||||
|
<BlockProvider>
|
||||||
<MintsProvider>
|
<MintsProvider>
|
||||||
<TransactionsProvider>
|
<TransactionsProvider>
|
||||||
<App />
|
<App />
|
||||||
</TransactionsProvider>
|
</TransactionsProvider>
|
||||||
</MintsProvider>
|
</MintsProvider>
|
||||||
|
</BlockProvider>
|
||||||
</AccountsProvider>
|
</AccountsProvider>
|
||||||
</RichListProvider>
|
</RichListProvider>
|
||||||
</SupplyProvider>
|
</SupplyProvider>
|
||||||
|
26
explorer/src/pages/BlockDetailsPage.tsx
Normal file
26
explorer/src/pages/BlockDetailsPage.tsx
Normal file
@ -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 { ErrorCard } from "components/common/ErrorCard";
|
||||||
import { LoadingCard } from "components/common/LoadingCard";
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
import { TableCardBody } from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { Slot } from "components/common/Slot";
|
|
||||||
import { displayTimestamp } from "utils/date";
|
import { displayTimestamp } from "utils/date";
|
||||||
import { InfoTooltip } from "components/common/InfoTooltip";
|
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||||
import { Address } from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
@ -29,6 +28,7 @@ import { intoTransactionInstruction, isSerumInstruction } from "utils/tx";
|
|||||||
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
||||||
import { FetchStatus } from "providers/cache";
|
import { FetchStatus } from "providers/cache";
|
||||||
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
import { SerumDetailsCard } from "components/instruction/SerumDetailsCard";
|
||||||
|
import { Slot } from "components/common/Slot";
|
||||||
|
|
||||||
const AUTO_REFRESH_INTERVAL = 2000;
|
const AUTO_REFRESH_INTERVAL = 2000;
|
||||||
const ZERO_CONFIRMATION_BAILOUT = 5;
|
const ZERO_CONFIRMATION_BAILOUT = 5;
|
||||||
@ -249,7 +249,7 @@ function StatusCard({
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Block</td>
|
<td>Block</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">
|
||||||
<Slot slot={info.slot} />
|
<Slot slot={info.slot} link />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@ -264,9 +264,7 @@ function StatusCard({
|
|||||||
</InfoTooltip>
|
</InfoTooltip>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-lg-right">
|
<td className="text-lg-right">{blockhash}</td>
|
||||||
<code>{blockhash}</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
114
explorer/src/providers/block.tsx
Normal file
114
explorer/src/providers/block.tsx
Normal file
@ -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> = {
|
export type Update<T> = {
|
||||||
type: ActionType.Update;
|
type: ActionType.Update;
|
||||||
url: string;
|
url: string;
|
||||||
key: string;
|
key: string | number;
|
||||||
status: FetchStatus;
|
status: FetchStatus;
|
||||||
data?: T;
|
data?: T;
|
||||||
};
|
};
|
||||||
|
@ -186,7 +186,11 @@ async function updateCluster(
|
|||||||
if (cluster !== Cluster.Custom) {
|
if (cluster !== Cluster.Custom) {
|
||||||
reportError(error, { clusterUrl: clusterUrl(cluster, customUrl) });
|
reportError(error, { clusterUrl: clusterUrl(cluster, customUrl) });
|
||||||
}
|
}
|
||||||
dispatch({ status: ClusterStatus.Failure, cluster, customUrl });
|
dispatch({
|
||||||
|
status: ClusterStatus.Failure,
|
||||||
|
cluster,
|
||||||
|
customUrl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user