Organize explorer file structure (#11464)
This commit is contained in:
@ -1,18 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch, Route, Redirect } from "react-router-dom";
|
import { Switch, Route, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import AccountDetails from "./components/AccountDetails";
|
import { ClusterModal } from "components/ClusterModal";
|
||||||
import TransactionDetails from "./components/TransactionDetails";
|
import { TX_ALIASES } from "providers/transactions";
|
||||||
import ClusterModal from "./components/ClusterModal";
|
import { MessageBanner } from "components/MessageBanner";
|
||||||
import { TX_ALIASES } from "./providers/transactions";
|
import { Navbar } from "components/Navbar";
|
||||||
import TopAccountsCard from "components/TopAccountsCard";
|
|
||||||
import SupplyCard from "components/SupplyCard";
|
|
||||||
import StatsCard from "components/StatsCard";
|
|
||||||
import MessageBanner from "components/MessageBanner";
|
|
||||||
import Navbar from "components/Navbar";
|
|
||||||
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
import { ClusterStatusBanner } from "components/ClusterStatusButton";
|
||||||
import { SearchBar } from "components/SearchBar";
|
import { SearchBar } from "components/SearchBar";
|
||||||
|
|
||||||
|
import { AccountDetailsPage } from "pages/AccountDetailsPage";
|
||||||
|
import { ClusterStatsPage } from "pages/ClusterStatsPage";
|
||||||
|
import { SupplyPage } from "pages/SupplyPage";
|
||||||
|
import { TransactionDetailsPage } from "pages/TransactionDetailsPage";
|
||||||
|
|
||||||
const ACCOUNT_ALIASES = ["account", "accounts", "addresses"];
|
const ACCOUNT_ALIASES = ["account", "accounts", "addresses"];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -26,10 +26,7 @@ function App() {
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
|
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
|
||||||
<div className="container mt-4">
|
<SupplyPage />
|
||||||
<SupplyCard />
|
|
||||||
<TopAccountsCard />
|
|
||||||
</div>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
@ -37,7 +34,7 @@ function App() {
|
|||||||
(tx) => `/${tx}/:signature`
|
(tx) => `/${tx}/:signature`
|
||||||
)}
|
)}
|
||||||
render={({ match }) => (
|
render={({ match }) => (
|
||||||
<TransactionDetails signature={match.params.signature} />
|
<TransactionDetailsPage signature={match.params.signature} />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
@ -58,16 +55,14 @@ function App() {
|
|||||||
exact
|
exact
|
||||||
path={["/address/:address", "/address/:address/:tab"]}
|
path={["/address/:address", "/address/:address/:tab"]}
|
||||||
render={({ match }) => (
|
render={({ match }) => (
|
||||||
<AccountDetails
|
<AccountDetailsPage
|
||||||
address={match.params.address}
|
address={match.params.address}
|
||||||
tab={match.params.tab}
|
tab={match.params.tab}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<div className="container mt-4">
|
<ClusterStatsPage />
|
||||||
<StatsCard />
|
|
||||||
</div>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
render={({ location }) => (
|
render={({ location }) => (
|
||||||
|
@ -1,411 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
|
||||||
import {
|
|
||||||
FetchStatus,
|
|
||||||
useFetchAccountInfo,
|
|
||||||
useAccountInfo,
|
|
||||||
useAccountHistory,
|
|
||||||
Account,
|
|
||||||
} from "providers/accounts";
|
|
||||||
import { lamportsToSolString } from "utils";
|
|
||||||
import { StakeAccountCards } from "components/account/StakeAccountCards";
|
|
||||||
import ErrorCard from "components/common/ErrorCard";
|
|
||||||
import LoadingCard from "components/common/LoadingCard";
|
|
||||||
import TableCardBody from "components/common/TableCardBody";
|
|
||||||
import { useFetchAccountHistory } from "providers/accounts/history";
|
|
||||||
import {
|
|
||||||
useFetchAccountOwnedTokens,
|
|
||||||
useAccountOwnedTokens,
|
|
||||||
TokenAccountData,
|
|
||||||
} from "providers/accounts/tokens";
|
|
||||||
import { useCluster, ClusterStatus } from "providers/cluster";
|
|
||||||
import Address from "./common/Address";
|
|
||||||
import Signature from "./common/Signature";
|
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { clusterPath } from "utils/url";
|
|
||||||
|
|
||||||
type Props = { address: string; tab?: string };
|
|
||||||
export default function AccountDetails({ address, tab }: Props) {
|
|
||||||
let pubkey: PublicKey | undefined;
|
|
||||||
try {
|
|
||||||
pubkey = new PublicKey(address);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
// TODO handle bad addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
let moreTab: MoreTabs = "history";
|
|
||||||
if (tab === "history" || tab === "tokens") {
|
|
||||||
moreTab = tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mt-n3">
|
|
||||||
<div className="header">
|
|
||||||
<div className="header-body">
|
|
||||||
<h6 className="header-pretitle">Details</h6>
|
|
||||||
<h4 className="header-title">Account</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{pubkey && <AccountCards pubkey={pubkey} />}
|
|
||||||
{pubkey && <MoreSection pubkey={pubkey} tab={moreTab} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type MoreTabs = "history" | "tokens";
|
|
||||||
function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
|
|
||||||
const address = pubkey.toBase58();
|
|
||||||
const info = useAccountInfo(address);
|
|
||||||
if (!info || info.lamports === undefined) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="container">
|
|
||||||
<div className="header">
|
|
||||||
<div className="header-body pt-0">
|
|
||||||
<ul className="nav nav-tabs nav-overflow header-tabs">
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
className="nav-link"
|
|
||||||
to={clusterPath(`/address/${address}`)}
|
|
||||||
exact
|
|
||||||
>
|
|
||||||
History
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item">
|
|
||||||
<NavLink
|
|
||||||
className="nav-link"
|
|
||||||
to={clusterPath(`/address/${address}/tokens`)}
|
|
||||||
exact
|
|
||||||
>
|
|
||||||
Tokens
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{tab === "tokens" && <TokensCard pubkey={pubkey} />}
|
|
||||||
{tab === "history" && <HistoryCard pubkey={pubkey} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
|
||||||
const fetchAccount = useFetchAccountInfo();
|
|
||||||
const address = pubkey.toBase58();
|
|
||||||
const info = useAccountInfo(address);
|
|
||||||
const refresh = useFetchAccountInfo();
|
|
||||||
const { status } = useCluster();
|
|
||||||
|
|
||||||
// Fetch account on load
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!info && status === ClusterStatus.Connected) fetchAccount(pubkey);
|
|
||||||
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
if (!info || info.status === FetchStatus.Fetching) {
|
|
||||||
return <LoadingCard />;
|
|
||||||
} else if (
|
|
||||||
info.status === FetchStatus.FetchFailed ||
|
|
||||||
info.lamports === undefined
|
|
||||||
) {
|
|
||||||
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const owner = info.details?.owner;
|
|
||||||
const data = info.details?.data;
|
|
||||||
if (data && owner && owner.equals(StakeProgram.programId)) {
|
|
||||||
return <StakeAccountCards account={info} stakeAccount={data} />;
|
|
||||||
} else {
|
|
||||||
return <UnknownAccountCard account={info} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnknownAccountCard({ account }: { account: Account }) {
|
|
||||||
const { details, lamports } = account;
|
|
||||||
if (lamports === undefined) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header align-items-center">
|
|
||||||
<h3 className="card-header-title">Overview</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TableCardBody>
|
|
||||||
<tr>
|
|
||||||
<td>Address</td>
|
|
||||||
<td className="text-lg-right">
|
|
||||||
<Address pubkey={account.pubkey} alignRight />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Balance (SOL)</td>
|
|
||||||
<td className="text-lg-right text-uppercase">
|
|
||||||
{lamportsToSolString(lamports)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
{details && (
|
|
||||||
<tr>
|
|
||||||
<td>Data (Bytes)</td>
|
|
||||||
<td className="text-lg-right">{details.space}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{details && (
|
|
||||||
<tr>
|
|
||||||
<td>Owner</td>
|
|
||||||
<td className="text-lg-right">
|
|
||||||
<Address pubkey={details.owner} alignRight link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{details && (
|
|
||||||
<tr>
|
|
||||||
<td>Executable</td>
|
|
||||||
<td className="text-lg-right">
|
|
||||||
{details.executable ? "Yes" : "No"}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</TableCardBody>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TokensCard({ pubkey }: { pubkey: PublicKey }) {
|
|
||||||
const address = pubkey.toBase58();
|
|
||||||
const ownedTokens = useAccountOwnedTokens(address);
|
|
||||||
const fetchAccountTokens = useFetchAccountOwnedTokens();
|
|
||||||
const refresh = () => fetchAccountTokens(pubkey);
|
|
||||||
|
|
||||||
// Fetch owned tokens
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!ownedTokens) refresh();
|
|
||||||
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
if (ownedTokens === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, tokens } = ownedTokens;
|
|
||||||
const fetching = status === FetchStatus.Fetching;
|
|
||||||
if (fetching && (tokens === undefined || tokens.length === 0)) {
|
|
||||||
return <LoadingCard message="Loading owned tokens" />;
|
|
||||||
} else if (tokens === undefined) {
|
|
||||||
return <ErrorCard retry={refresh} text="Failed to fetch owned tokens" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
return (
|
|
||||||
<ErrorCard
|
|
||||||
retry={refresh}
|
|
||||||
retryText="Try Again"
|
|
||||||
text={"No owned tokens found"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mappedTokens = new Map<string, TokenAccountData>();
|
|
||||||
for (const token of tokens) {
|
|
||||||
const mintAddress = token.mint.toBase58();
|
|
||||||
const tokenInfo = mappedTokens.get(mintAddress);
|
|
||||||
if (tokenInfo) {
|
|
||||||
tokenInfo.amount += token.amount;
|
|
||||||
} else {
|
|
||||||
mappedTokens.set(mintAddress, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailsList: React.ReactNode[] = [];
|
|
||||||
mappedTokens.forEach((tokenInfo, mintAddress) => {
|
|
||||||
const balance = tokenInfo.amount;
|
|
||||||
detailsList.push(
|
|
||||||
<tr key={mintAddress}>
|
|
||||||
<td>
|
|
||||||
<Address pubkey={new PublicKey(mintAddress)} link />
|
|
||||||
</td>
|
|
||||||
<td>{balance}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header align-items-center">
|
|
||||||
<h3 className="card-header-title">Owned Tokens</h3>
|
|
||||||
<button
|
|
||||||
className="btn btn-white btn-sm"
|
|
||||||
disabled={fetching}
|
|
||||||
onClick={refresh}
|
|
||||||
>
|
|
||||||
{fetching ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="fe fe-refresh-cw mr-2"></span>
|
|
||||||
Refresh
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
|
||||||
<table className="table table-sm table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-muted">Token Address</th>
|
|
||||||
<th className="text-muted">Balance</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="list">{detailsList}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|
||||||
const address = pubkey.toBase58();
|
|
||||||
const info = useAccountInfo(address);
|
|
||||||
const history = useAccountHistory(address);
|
|
||||||
const fetchAccountHistory = useFetchAccountHistory();
|
|
||||||
const refresh = () => fetchAccountHistory(pubkey, true);
|
|
||||||
const loadMore = () => fetchAccountHistory(pubkey);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!history) refresh();
|
|
||||||
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
if (!info || !history || info.lamports === undefined) {
|
|
||||||
return null;
|
|
||||||
} else if (history.fetched === undefined) {
|
|
||||||
if (history.status === FetchStatus.Fetching) {
|
|
||||||
return <LoadingCard message="Loading history" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (history.fetched.length === 0) {
|
|
||||||
if (history.status === FetchStatus.Fetching) {
|
|
||||||
return <LoadingCard message="Loading history" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ErrorCard
|
|
||||||
retry={loadMore}
|
|
||||||
retryText="Try again"
|
|
||||||
text="No transaction history found"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailsList: React.ReactNode[] = [];
|
|
||||||
const transactions = history.fetched;
|
|
||||||
|
|
||||||
for (var i = 0; i < transactions.length; i++) {
|
|
||||||
const slot = transactions[i].slot;
|
|
||||||
const slotTransactions = [transactions[i]];
|
|
||||||
while (i + 1 < transactions.length) {
|
|
||||||
const nextSlot = transactions[i + 1].slot;
|
|
||||||
if (nextSlot !== slot) break;
|
|
||||||
slotTransactions.push(transactions[++i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
slotTransactions.forEach(({ signature, err }) => {
|
|
||||||
let statusText;
|
|
||||||
let statusClass;
|
|
||||||
if (err) {
|
|
||||||
statusClass = "warning";
|
|
||||||
statusText = "Failed";
|
|
||||||
} else {
|
|
||||||
statusClass = "success";
|
|
||||||
statusText = "Success";
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsList.push(
|
|
||||||
<tr key={signature}>
|
|
||||||
<td className="w-1">{slot}</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-${statusClass}`}>
|
|
||||||
{statusText}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Signature signature={signature} link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetching = history.status === FetchStatus.Fetching;
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header align-items-center">
|
|
||||||
<h3 className="card-header-title">Transaction History</h3>
|
|
||||||
<button
|
|
||||||
className="btn btn-white btn-sm"
|
|
||||||
disabled={fetching}
|
|
||||||
onClick={refresh}
|
|
||||||
>
|
|
||||||
{fetching ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="fe fe-refresh-cw mr-2"></span>
|
|
||||||
Refresh
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
|
||||||
<table className="table table-sm table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-muted w-1">Slot</th>
|
|
||||||
<th className="text-muted">Result</th>
|
|
||||||
<th className="text-muted">Transaction Signature</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="list">{detailsList}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card-footer">
|
|
||||||
{history.foundOldest ? (
|
|
||||||
<div className="text-muted text-center">Fetched full history</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary w-100"
|
|
||||||
onClick={loadMore}
|
|
||||||
disabled={fetching}
|
|
||||||
>
|
|
||||||
{fetching ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Load More"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -12,12 +12,12 @@ import {
|
|||||||
Cluster,
|
Cluster,
|
||||||
useClusterModal,
|
useClusterModal,
|
||||||
useUpdateCustomUrl,
|
useUpdateCustomUrl,
|
||||||
} from "../providers/cluster";
|
} from "providers/cluster";
|
||||||
import { assertUnreachable } from "../utils";
|
import { assertUnreachable } from "../utils";
|
||||||
import Overlay from "./Overlay";
|
import { Overlay } from "./common/Overlay";
|
||||||
import { useQuery } from "utils/url";
|
import { useQuery } from "utils/url";
|
||||||
|
|
||||||
function ClusterModal() {
|
export function ClusterModal() {
|
||||||
const [show, setShow] = useClusterModal();
|
const [show, setShow] = useClusterModal();
|
||||||
const onClose = () => setShow(false);
|
const onClose = () => setShow(false);
|
||||||
return (
|
return (
|
||||||
@ -164,5 +164,3 @@ function ClusterToggle() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClusterModal;
|
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
ClusterStatus,
|
ClusterStatus,
|
||||||
Cluster,
|
Cluster,
|
||||||
useClusterModal,
|
useClusterModal,
|
||||||
} from "../providers/cluster";
|
} from "providers/cluster";
|
||||||
|
|
||||||
export function ClusterStatusBanner() {
|
export function ClusterStatusBanner() {
|
||||||
const [, setShow] = useClusterModal();
|
const [, setShow] = useClusterModal();
|
||||||
|
@ -25,7 +25,7 @@ const announcements = new Map<Cluster, Announcement>();
|
|||||||
// "Mainnet Beta upgrade in progress. Transactions disabled until epoch 62",
|
// "Mainnet Beta upgrade in progress. Transactions disabled until epoch 62",
|
||||||
// });
|
// });
|
||||||
|
|
||||||
export default function Banner() {
|
export function MessageBanner() {
|
||||||
const cluster = useCluster().cluster;
|
const cluster = useCluster().cluster;
|
||||||
const announcement = announcements.get(cluster);
|
const announcement = announcements.get(cluster);
|
||||||
if (!announcement) return null;
|
if (!announcement) return null;
|
||||||
|
@ -4,7 +4,7 @@ import { clusterPath } from "utils/url";
|
|||||||
import { Link, NavLink } from "react-router-dom";
|
import { Link, NavLink } from "react-router-dom";
|
||||||
import { ClusterStatusButton } from "components/ClusterStatusButton";
|
import { ClusterStatusButton } from "components/ClusterStatusButton";
|
||||||
|
|
||||||
export default function Navbar() {
|
export function Navbar() {
|
||||||
// TODO: use `collapsing` to animate collapsible navbar
|
// TODO: use `collapsing` to animate collapsible navbar
|
||||||
const [collapse, setCollapse] = React.useState(false);
|
const [collapse, setCollapse] = React.useState(false);
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSupply, useFetchSupply, Status } from "providers/supply";
|
import { useSupply, useFetchSupply, Status } from "providers/supply";
|
||||||
import LoadingCard from "./common/LoadingCard";
|
import { LoadingCard } from "./common/LoadingCard";
|
||||||
import ErrorCard from "./common/ErrorCard";
|
import { ErrorCard } from "./common/ErrorCard";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import TableCardBody from "./common/TableCardBody";
|
import { TableCardBody } from "./common/TableCardBody";
|
||||||
|
|
||||||
export default function SupplyCard() {
|
export function SupplyCard() {
|
||||||
const supply = useSupply();
|
const supply = useSupply();
|
||||||
const fetchSupply = useFetchSupply();
|
const fetchSupply = useFetchSupply();
|
||||||
|
|
||||||
|
@ -3,16 +3,16 @@ import { Link } from "react-router-dom";
|
|||||||
import { Location } from "history";
|
import { Location } from "history";
|
||||||
import { AccountBalancePair } from "@solana/web3.js";
|
import { AccountBalancePair } from "@solana/web3.js";
|
||||||
import { useRichList, useFetchRichList, Status } from "providers/richList";
|
import { useRichList, useFetchRichList, Status } from "providers/richList";
|
||||||
import LoadingCard from "./common/LoadingCard";
|
import { LoadingCard } from "./common/LoadingCard";
|
||||||
import ErrorCard from "./common/ErrorCard";
|
import { ErrorCard } from "./common/ErrorCard";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { useQuery } from "utils/url";
|
import { useQuery } from "utils/url";
|
||||||
import { useSupply } from "providers/supply";
|
import { useSupply } from "providers/supply";
|
||||||
import Address from "./common/Address";
|
import { Address } from "./common/Address";
|
||||||
|
|
||||||
type Filter = "circulating" | "nonCirculating" | "all" | null;
|
type Filter = "circulating" | "nonCirculating" | "all" | null;
|
||||||
|
|
||||||
export default function TopAccountsCard() {
|
export function TopAccountsCard() {
|
||||||
const supply = useSupply();
|
const supply = useSupply();
|
||||||
const richList = useRichList();
|
const richList = useRichList();
|
||||||
const fetchRichList = useFetchRichList();
|
const fetchRichList = useFetchRichList();
|
||||||
|
106
explorer/src/components/account/OwnedTokensCard.tsx
Normal file
106
explorer/src/components/account/OwnedTokensCard.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { FetchStatus } from "providers/accounts";
|
||||||
|
import {
|
||||||
|
useFetchAccountOwnedTokens,
|
||||||
|
useAccountOwnedTokens,
|
||||||
|
TokenAccountData,
|
||||||
|
} from "providers/accounts/tokens";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
|
export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const ownedTokens = useAccountOwnedTokens(address);
|
||||||
|
const fetchAccountTokens = useFetchAccountOwnedTokens();
|
||||||
|
const refresh = () => fetchAccountTokens(pubkey);
|
||||||
|
|
||||||
|
// Fetch owned tokens
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!ownedTokens) refresh();
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (ownedTokens === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, tokens } = ownedTokens;
|
||||||
|
const fetching = status === FetchStatus.Fetching;
|
||||||
|
if (fetching && (tokens === undefined || tokens.length === 0)) {
|
||||||
|
return <LoadingCard message="Loading owned tokens" />;
|
||||||
|
} else if (tokens === undefined) {
|
||||||
|
return <ErrorCard retry={refresh} text="Failed to fetch owned tokens" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
return (
|
||||||
|
<ErrorCard
|
||||||
|
retry={refresh}
|
||||||
|
retryText="Try Again"
|
||||||
|
text={"No owned tokens found"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedTokens = new Map<string, TokenAccountData>();
|
||||||
|
for (const token of tokens) {
|
||||||
|
const mintAddress = token.mint.toBase58();
|
||||||
|
const tokenInfo = mappedTokens.get(mintAddress);
|
||||||
|
if (tokenInfo) {
|
||||||
|
tokenInfo.amount += token.amount;
|
||||||
|
} else {
|
||||||
|
mappedTokens.set(mintAddress, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
mappedTokens.forEach((tokenInfo, mintAddress) => {
|
||||||
|
const balance = tokenInfo.amount;
|
||||||
|
detailsList.push(
|
||||||
|
<tr key={mintAddress}>
|
||||||
|
<td>
|
||||||
|
<Address pubkey={new PublicKey(mintAddress)} link />
|
||||||
|
</td>
|
||||||
|
<td>{balance}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Owned Tokens</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
disabled={fetching}
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
{fetching ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Token Address</th>
|
||||||
|
<th className="text-muted">Balance</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{detailsList}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { StakeAccount, Meta } from "solana-sdk-wasm";
|
import { StakeAccount, Meta } from "solana-sdk-wasm";
|
||||||
import TableCardBody from "components/common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { displayTimestamp } from "utils/date";
|
import { displayTimestamp } from "utils/date";
|
||||||
import { Account, useFetchAccountInfo } from "providers/accounts";
|
import { Account, useFetchAccountInfo } from "providers/accounts";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function StakeAccountCards({
|
export function StakeAccountSection({
|
||||||
account,
|
account,
|
||||||
stakeAccount,
|
stakeAccount,
|
||||||
}: {
|
}: {
|
150
explorer/src/components/account/TransactionHistoryCard.tsx
Normal file
150
explorer/src/components/account/TransactionHistoryCard.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
FetchStatus,
|
||||||
|
useAccountInfo,
|
||||||
|
useAccountHistory,
|
||||||
|
} from "providers/accounts";
|
||||||
|
import { useFetchAccountHistory } from "providers/accounts/history";
|
||||||
|
import { Signature } from "components/common/Signature";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
|
||||||
|
export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
const history = useAccountHistory(address);
|
||||||
|
const fetchAccountHistory = useFetchAccountHistory();
|
||||||
|
const refresh = () => fetchAccountHistory(pubkey, true);
|
||||||
|
const loadMore = () => fetchAccountHistory(pubkey);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!history) refresh();
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (!info || !history || info.lamports === undefined) {
|
||||||
|
return null;
|
||||||
|
} else if (history.fetched === undefined) {
|
||||||
|
if (history.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard message="Loading history" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history.fetched.length === 0) {
|
||||||
|
if (history.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard message="Loading history" />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ErrorCard
|
||||||
|
retry={loadMore}
|
||||||
|
retryText="Try again"
|
||||||
|
text="No transaction history found"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
const transactions = history.fetched;
|
||||||
|
|
||||||
|
for (var i = 0; i < transactions.length; i++) {
|
||||||
|
const slot = transactions[i].slot;
|
||||||
|
const slotTransactions = [transactions[i]];
|
||||||
|
while (i + 1 < transactions.length) {
|
||||||
|
const nextSlot = transactions[i + 1].slot;
|
||||||
|
if (nextSlot !== slot) break;
|
||||||
|
slotTransactions.push(transactions[++i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
slotTransactions.forEach(({ signature, err }) => {
|
||||||
|
let statusText;
|
||||||
|
let statusClass;
|
||||||
|
if (err) {
|
||||||
|
statusClass = "warning";
|
||||||
|
statusText = "Failed";
|
||||||
|
} else {
|
||||||
|
statusClass = "success";
|
||||||
|
statusText = "Success";
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsList.push(
|
||||||
|
<tr key={signature}>
|
||||||
|
<td className="w-1">{slot}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Signature signature={signature} link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetching = history.status === FetchStatus.Fetching;
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Transaction History</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
disabled={fetching}
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
{fetching ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted w-1">Slot</th>
|
||||||
|
<th className="text-muted">Result</th>
|
||||||
|
<th className="text-muted">Transaction Signature</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{detailsList}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-footer">
|
||||||
|
{history.foundOldest ? (
|
||||||
|
<div className="text-muted text-center">Fetched full history</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
onClick={loadMore}
|
||||||
|
disabled={fetching}
|
||||||
|
>
|
||||||
|
{fetching ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Load More"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
58
explorer/src/components/account/UnknownAccountCard.tsx
Normal file
58
explorer/src/components/account/UnknownAccountCard.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Account } from "providers/accounts";
|
||||||
|
import { lamportsToSolString } from "utils";
|
||||||
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
|
export function UnknownAccountCard({ account }: { account: Account }) {
|
||||||
|
const { details, lamports } = account;
|
||||||
|
if (lamports === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Overview</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableCardBody>
|
||||||
|
<tr>
|
||||||
|
<td>Address</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<Address pubkey={account.pubkey} alignRight />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Balance (SOL)</td>
|
||||||
|
<td className="text-lg-right text-uppercase">
|
||||||
|
{lamportsToSolString(lamports)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Data (Bytes)</td>
|
||||||
|
<td className="text-lg-right">{details.space}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Owner</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
<Address pubkey={details.owner} alignRight link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{details && (
|
||||||
|
<tr>
|
||||||
|
<td>Executable</td>
|
||||||
|
<td className="text-lg-right">
|
||||||
|
{details.executable ? "Yes" : "No"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</TableCardBody>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -12,7 +12,7 @@ type Props = {
|
|||||||
link?: boolean;
|
link?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Address({ pubkey, alignRight, link }: Props) {
|
export function Address({ pubkey, alignRight, link }: Props) {
|
||||||
const [state, setState] = useState<CopyState>("copy");
|
const [state, setState] = useState<CopyState>("copy");
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ function Popover({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Copyable({ bottom, right, text, children }: CopyableProps) {
|
export function Copyable({ bottom, right, text, children }: CopyableProps) {
|
||||||
const [state, setState] = useState<State>("hide");
|
const [state, setState] = useState<State>("hide");
|
||||||
|
|
||||||
const copyToClipboard = () => navigator.clipboard.writeText(text);
|
const copyToClipboard = () => navigator.clipboard.writeText(text);
|
||||||
@ -54,5 +54,3 @@ function Copyable({ bottom, right, text, children }: CopyableProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Copyable;
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function ErrorCard({
|
export function ErrorCard({
|
||||||
retry,
|
retry,
|
||||||
retryText,
|
retryText,
|
||||||
text,
|
text,
|
||||||
|
@ -33,7 +33,7 @@ function Popover({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoTooltip({ bottom, right, text, children }: Props) {
|
export function InfoTooltip({ bottom, right, text, children }: Props) {
|
||||||
const [state, setState] = useState<State>("hide");
|
const [state, setState] = useState<State>("hide");
|
||||||
|
|
||||||
const justify = right ? "end" : "start";
|
const justify = right ? "end" : "start";
|
||||||
@ -51,5 +51,3 @@ function InfoTooltip({ bottom, right, text, children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InfoTooltip;
|
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function LoadingCard({ message }: { message?: string }) {
|
export function LoadingCard({ message }: { message?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body text-center">
|
<div className="card-body text-center">
|
||||||
|
@ -4,6 +4,6 @@ type OverlayProps = {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Overlay({ show }: OverlayProps) {
|
export function Overlay({ show }: OverlayProps) {
|
||||||
return <div className={`modal-backdrop fade${show ? " show" : ""}`}></div>;
|
return <div className={`modal-backdrop fade${show ? " show" : ""}`}></div>;
|
||||||
}
|
}
|
@ -10,7 +10,7 @@ type Props = {
|
|||||||
link?: boolean;
|
link?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Signature({ signature, alignRight, link }: Props) {
|
export function Signature({ signature, alignRight, link }: Props) {
|
||||||
const [state, setState] = useState<CopyState>("copy");
|
const [state, setState] = useState<CopyState>("copy");
|
||||||
|
|
||||||
const copyToClipboard = () => navigator.clipboard.writeText(signature);
|
const copyToClipboard = () => navigator.clipboard.writeText(signature);
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function TableCardBody({
|
export function TableCardBody({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="table-responsive mb-0">
|
<div className="table-responsive mb-0">
|
||||||
<table className="table table-sm table-nowrap card-table">
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import bs58 from "bs58";
|
import bs58 from "bs58";
|
||||||
import { TransactionInstruction } from "@solana/web3.js";
|
import { TransactionInstruction } from "@solana/web3.js";
|
||||||
import Copyable from "components/Copyable";
|
import { Copyable } from "components/common/Copyable";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
function displayData(data: string) {
|
function displayData(data: string) {
|
||||||
if (data.length > 50) {
|
if (data.length > 50) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ParsedInstruction } from "@solana/web3.js";
|
import { ParsedInstruction } from "@solana/web3.js";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) {
|
export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) {
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function AuthorizeDetailsCard(props: {
|
export function AuthorizeDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function DeactivateDetailsCard(props: {
|
export function DeactivateDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function DelegateDetailsCard(props: {
|
export function DelegateDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function InitializeDetailsCard(props: {
|
export function InitializeDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function SplitDetailsCard(props: {
|
export function SplitDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function WithdrawDetailsCard(props: {
|
export function WithdrawDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function AllocateDetailsCard(props: {
|
export function AllocateDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
SystemInstruction,
|
SystemInstruction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import Copyable from "components/Copyable";
|
import { Copyable } from "components/common/Copyable";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function AllocateWithSeedDetailsCard(props: {
|
export function AllocateWithSeedDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function AssignDetailsCard(props: {
|
export function AssignDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -6,9 +6,9 @@ import {
|
|||||||
SystemInstruction,
|
SystemInstruction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import Copyable from "components/Copyable";
|
import { Copyable } from "components/common/Copyable";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function AssignWithSeedDetailsCard(props: {
|
export function AssignWithSeedDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function CreateDetailsCard(props: {
|
export function CreateDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,9 +7,9 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import Copyable from "components/Copyable";
|
import { Copyable } from "components/common/Copyable";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function CreateWithSeedDetailsCard(props: {
|
export function CreateWithSeedDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function NonceAdvanceDetailsCard(props: {
|
export function NonceAdvanceDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function NonceAuthorizeDetailsCard(props: {
|
export function NonceAuthorizeDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function NonceInitializeDetailsCard(props: {
|
export function NonceInitializeDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function NonceWithdrawDetailsCard(props: {
|
export function NonceWithdrawDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
export function TransferDetailsCard(props: {
|
export function TransferDetailsCard(props: {
|
||||||
ix: TransactionInstruction;
|
ix: TransactionInstruction;
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||||
import { InstructionCard } from "../InstructionCard";
|
import { InstructionCard } from "../InstructionCard";
|
||||||
import Address from "components/common/Address";
|
import { Address } from "components/common/Address";
|
||||||
import { ParsedInstructionInfo, IX_STRUCTS } from "./types";
|
import { ParsedInstructionInfo, IX_STRUCTS } from "./types";
|
||||||
|
|
||||||
const IX_TITLES = {
|
const IX_TITLES = {
|
||||||
|
115
explorer/src/pages/AccountDetailsPage.tsx
Normal file
115
explorer/src/pages/AccountDetailsPage.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
FetchStatus,
|
||||||
|
useFetchAccountInfo,
|
||||||
|
useAccountInfo,
|
||||||
|
} from "providers/accounts";
|
||||||
|
import { StakeAccountSection } from "components/account/StakeAccountSection";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { useCluster, ClusterStatus } from "providers/cluster";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { clusterPath } from "utils/url";
|
||||||
|
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
|
||||||
|
import { OwnedTokensCard } from "components/account/OwnedTokensCard";
|
||||||
|
import { TransactionHistoryCard } from "components/account/TransactionHistoryCard";
|
||||||
|
|
||||||
|
type Props = { address: string; tab?: string };
|
||||||
|
export function AccountDetailsPage({ address, tab }: Props) {
|
||||||
|
let pubkey: PublicKey | undefined;
|
||||||
|
try {
|
||||||
|
pubkey = new PublicKey(address);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
// TODO handle bad addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
let moreTab: MoreTabs = "history";
|
||||||
|
if (tab === "history" || tab === "tokens") {
|
||||||
|
moreTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-n3">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body">
|
||||||
|
<h6 className="header-pretitle">Details</h6>
|
||||||
|
<h4 className="header-title">Account</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{pubkey && <InfoSection pubkey={pubkey} />}
|
||||||
|
{pubkey && <MoreSection pubkey={pubkey} tab={moreTab} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoSection({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const fetchAccount = useFetchAccountInfo();
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
const refresh = useFetchAccountInfo();
|
||||||
|
const { status } = useCluster();
|
||||||
|
|
||||||
|
// Fetch account on load
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!info && status === ClusterStatus.Connected) fetchAccount(pubkey);
|
||||||
|
}, [address, status]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (!info || info.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard />;
|
||||||
|
} else if (
|
||||||
|
info.status === FetchStatus.FetchFailed ||
|
||||||
|
info.lamports === undefined
|
||||||
|
) {
|
||||||
|
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = info.details?.owner;
|
||||||
|
const data = info.details?.data;
|
||||||
|
if (data && owner && owner.equals(StakeProgram.programId)) {
|
||||||
|
return <StakeAccountSection account={info} stakeAccount={data} />;
|
||||||
|
} else {
|
||||||
|
return <UnknownAccountCard account={info} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoreTabs = "history" | "tokens";
|
||||||
|
function MoreSection({ pubkey, tab }: { pubkey: PublicKey; tab: MoreTabs }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const info = useAccountInfo(address);
|
||||||
|
if (!info || info.lamports === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body pt-0">
|
||||||
|
<ul className="nav nav-tabs nav-overflow header-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
History
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/address/${address}/tokens`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
Tokens
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tab === "tokens" && <OwnedTokensCard pubkey={pubkey} />}
|
||||||
|
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import CountUp from "react-countup";
|
import CountUp from "react-countup";
|
||||||
|
|
||||||
import TableCardBody from "./common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import {
|
import {
|
||||||
useDashboardInfo,
|
useDashboardInfo,
|
||||||
usePerformanceInfo,
|
usePerformanceInfo,
|
||||||
@ -12,17 +12,19 @@ import {
|
|||||||
import { slotsToHumanString } from "utils";
|
import { slotsToHumanString } from "utils";
|
||||||
import { useCluster, Cluster } from "providers/cluster";
|
import { useCluster, Cluster } from "providers/cluster";
|
||||||
|
|
||||||
export default function StatsCard() {
|
export function ClusterStatsPage() {
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="container mt-4">
|
||||||
<div className="card-header">
|
<div className="card">
|
||||||
<div className="row align-items-center">
|
<div className="card-header">
|
||||||
<div className="col">
|
<div className="row align-items-center">
|
||||||
<h4 className="card-header-title">Live Cluster Stats</h4>
|
<div className="col">
|
||||||
|
<h4 className="card-header-title">Live Cluster Stats</h4>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<StatsCardBody />
|
||||||
</div>
|
</div>
|
||||||
<StatsCardBody />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
12
explorer/src/pages/SupplyPage.tsx
Normal file
12
explorer/src/pages/SupplyPage.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TopAccountsCard } from "components/TopAccountsCard";
|
||||||
|
import { SupplyCard } from "components/SupplyCard";
|
||||||
|
|
||||||
|
export function SupplyPage() {
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<SupplyCard />
|
||||||
|
<TopAccountsCard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,7 +4,7 @@ import {
|
|||||||
useTransactionStatus,
|
useTransactionStatus,
|
||||||
useTransactionDetails,
|
useTransactionDetails,
|
||||||
FetchStatus,
|
FetchStatus,
|
||||||
} from "../providers/transactions";
|
} from "providers/transactions";
|
||||||
import { useFetchTransactionDetails } from "providers/transactions/details";
|
import { useFetchTransactionDetails } from "providers/transactions/details";
|
||||||
import { useCluster, ClusterStatus } from "providers/cluster";
|
import { useCluster, ClusterStatus } from "providers/cluster";
|
||||||
import {
|
import {
|
||||||
@ -14,22 +14,22 @@ import {
|
|||||||
SystemInstruction,
|
SystemInstruction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import { UnknownDetailsCard } from "./instruction/UnknownDetailsCard";
|
import { UnknownDetailsCard } from "components/instruction/UnknownDetailsCard";
|
||||||
import { SystemDetailsCard } from "./instruction/system/SystemDetailsCard";
|
import { SystemDetailsCard } from "components/instruction/system/SystemDetailsCard";
|
||||||
import { StakeDetailsCard } from "./instruction/stake/StakeDetailsCard";
|
import { StakeDetailsCard } from "components/instruction/stake/StakeDetailsCard";
|
||||||
import ErrorCard from "./common/ErrorCard";
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
import LoadingCard from "./common/LoadingCard";
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
import TableCardBody from "./common/TableCardBody";
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
import { displayTimestamp } from "utils/date";
|
import { displayTimestamp } from "utils/date";
|
||||||
import InfoTooltip from "components/InfoTooltip";
|
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||||
import { isCached } from "providers/transactions/cached";
|
import { isCached } from "providers/transactions/cached";
|
||||||
import Address from "./common/Address";
|
import { Address } from "components/common/Address";
|
||||||
import Signature from "./common/Signature";
|
import { Signature } from "components/common/Signature";
|
||||||
import { intoTransactionInstruction } from "utils/tx";
|
import { intoTransactionInstruction } from "utils/tx";
|
||||||
import { TokenDetailsCard } from "./instruction/token/TokenDetailsCard";
|
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
|
||||||
|
|
||||||
type Props = { signature: TransactionSignature };
|
type Props = { signature: TransactionSignature };
|
||||||
export default function TransactionDetails({ signature }: Props) {
|
export function TransactionDetailsPage({ signature }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="container mt-n3">
|
<div className="container mt-n3">
|
||||||
<div className="header">
|
<div className="header">
|
Reference in New Issue
Block a user