Organize explorer file structure (#11464)

This commit is contained in:
Justin Starry 2020-08-08 14:45:57 +08:00 committed by GitHub
parent 5a7e99f283
commit c7eba80836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 525 additions and 508 deletions

View File

@ -1,18 +1,18 @@
import React from "react";
import { Switch, Route, Redirect } from "react-router-dom";
import AccountDetails from "./components/AccountDetails";
import TransactionDetails from "./components/TransactionDetails";
import ClusterModal from "./components/ClusterModal";
import { TX_ALIASES } from "./providers/transactions";
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 { ClusterModal } from "components/ClusterModal";
import { TX_ALIASES } from "providers/transactions";
import { MessageBanner } from "components/MessageBanner";
import { Navbar } from "components/Navbar";
import { ClusterStatusBanner } from "components/ClusterStatusButton";
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"];
function App() {
@ -26,10 +26,7 @@ function App() {
<SearchBar />
<Switch>
<Route exact path={["/supply", "/accounts", "accounts/top"]}>
<div className="container mt-4">
<SupplyCard />
<TopAccountsCard />
</div>
<SupplyPage />
</Route>
<Route
exact
@ -37,7 +34,7 @@ function App() {
(tx) => `/${tx}/:signature`
)}
render={({ match }) => (
<TransactionDetails signature={match.params.signature} />
<TransactionDetailsPage signature={match.params.signature} />
)}
/>
<Route
@ -58,16 +55,14 @@ function App() {
exact
path={["/address/:address", "/address/:address/:tab"]}
render={({ match }) => (
<AccountDetails
<AccountDetailsPage
address={match.params.address}
tab={match.params.tab}
/>
)}
/>
<Route exact path="/">
<div className="container mt-4">
<StatsCard />
</div>
<ClusterStatsPage />
</Route>
<Route
render={({ location }) => (

View File

@ -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>
);
}

View File

@ -12,12 +12,12 @@ import {
Cluster,
useClusterModal,
useUpdateCustomUrl,
} from "../providers/cluster";
} from "providers/cluster";
import { assertUnreachable } from "../utils";
import Overlay from "./Overlay";
import { Overlay } from "./common/Overlay";
import { useQuery } from "utils/url";
function ClusterModal() {
export function ClusterModal() {
const [show, setShow] = useClusterModal();
const onClose = () => setShow(false);
return (
@ -164,5 +164,3 @@ function ClusterToggle() {
</div>
);
}
export default ClusterModal;

View File

@ -4,7 +4,7 @@ import {
ClusterStatus,
Cluster,
useClusterModal,
} from "../providers/cluster";
} from "providers/cluster";
export function ClusterStatusBanner() {
const [, setShow] = useClusterModal();

View File

@ -25,7 +25,7 @@ const announcements = new Map<Cluster, Announcement>();
// "Mainnet Beta upgrade in progress. Transactions disabled until epoch 62",
// });
export default function Banner() {
export function MessageBanner() {
const cluster = useCluster().cluster;
const announcement = announcements.get(cluster);
if (!announcement) return null;

View File

@ -4,7 +4,7 @@ import { clusterPath } from "utils/url";
import { Link, NavLink } from "react-router-dom";
import { ClusterStatusButton } from "components/ClusterStatusButton";
export default function Navbar() {
export function Navbar() {
// TODO: use `collapsing` to animate collapsible navbar
const [collapse, setCollapse] = React.useState(false);

View File

@ -1,11 +1,11 @@
import React from "react";
import { useSupply, useFetchSupply, Status } from "providers/supply";
import LoadingCard from "./common/LoadingCard";
import ErrorCard from "./common/ErrorCard";
import { LoadingCard } from "./common/LoadingCard";
import { ErrorCard } from "./common/ErrorCard";
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 fetchSupply = useFetchSupply();

View File

@ -3,16 +3,16 @@ import { Link } from "react-router-dom";
import { Location } from "history";
import { AccountBalancePair } from "@solana/web3.js";
import { useRichList, useFetchRichList, Status } from "providers/richList";
import LoadingCard from "./common/LoadingCard";
import ErrorCard from "./common/ErrorCard";
import { LoadingCard } from "./common/LoadingCard";
import { ErrorCard } from "./common/ErrorCard";
import { lamportsToSolString } from "utils";
import { useQuery } from "utils/url";
import { useSupply } from "providers/supply";
import Address from "./common/Address";
import { Address } from "./common/Address";
type Filter = "circulating" | "nonCirculating" | "all" | null;
export default function TopAccountsCard() {
export function TopAccountsCard() {
const supply = useSupply();
const richList = useRichList();
const fetchRichList = useFetchRichList();

View 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>
);
}

View File

@ -1,12 +1,12 @@
import React from "react";
import { StakeAccount, Meta } from "solana-sdk-wasm";
import TableCardBody from "components/common/TableCardBody";
import { TableCardBody } from "components/common/TableCardBody";
import { lamportsToSolString } from "utils";
import { displayTimestamp } from "utils/date";
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,
stakeAccount,
}: {

View 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>
);
}

View 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>
);
}

View File

@ -12,7 +12,7 @@ type Props = {
link?: boolean;
};
export default function Address({ pubkey, alignRight, link }: Props) {
export function Address({ pubkey, alignRight, link }: Props) {
const [state, setState] = useState<CopyState>("copy");
const address = pubkey.toBase58();

View File

@ -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 copyToClipboard = () => navigator.clipboard.writeText(text);
@ -54,5 +54,3 @@ function Copyable({ bottom, right, text, children }: CopyableProps) {
</div>
);
}
export default Copyable;

View File

@ -1,6 +1,6 @@
import React from "react";
export default function ErrorCard({
export function ErrorCard({
retry,
retryText,
text,

View File

@ -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 justify = right ? "end" : "start";
@ -51,5 +51,3 @@ function InfoTooltip({ bottom, right, text, children }: Props) {
</div>
);
}
export default InfoTooltip;

View File

@ -1,6 +1,6 @@
import React from "react";
export default function LoadingCard({ message }: { message?: string }) {
export function LoadingCard({ message }: { message?: string }) {
return (
<div className="card">
<div className="card-body text-center">

View File

@ -4,6 +4,6 @@ type OverlayProps = {
show: boolean;
};
export default function Overlay({ show }: OverlayProps) {
export function Overlay({ show }: OverlayProps) {
return <div className={`modal-backdrop fade${show ? " show" : ""}`}></div>;
}

View File

@ -10,7 +10,7 @@ type Props = {
link?: boolean;
};
export default function Signature({ signature, alignRight, link }: Props) {
export function Signature({ signature, alignRight, link }: Props) {
const [state, setState] = useState<CopyState>("copy");
const copyToClipboard = () => navigator.clipboard.writeText(signature);

View File

@ -1,10 +1,6 @@
import React from "react";
export default function TableCardBody({
children,
}: {
children: React.ReactNode;
}) {
export function TableCardBody({ children }: { children: React.ReactNode }) {
return (
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">

View File

@ -1,8 +1,8 @@
import React from "react";
import bs58 from "bs58";
import { TransactionInstruction } from "@solana/web3.js";
import Copyable from "components/Copyable";
import Address from "components/common/Address";
import { Copyable } from "components/common/Copyable";
import { Address } from "components/common/Address";
function displayData(data: string) {
if (data.length > 50) {

View File

@ -1,6 +1,6 @@
import React from "react";
import { ParsedInstruction } from "@solana/web3.js";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function RawParsedDetails({ ix }: { ix: ParsedInstruction }) {
return (

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function AuthorizeDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function DeactivateDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function DelegateDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function InitializeDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function SplitDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function WithdrawDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function AllocateDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -6,9 +6,9 @@ import {
SystemInstruction,
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import Copyable from "components/Copyable";
import { Copyable } from "components/common/Copyable";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function AllocateWithSeedDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function AssignDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -6,9 +6,9 @@ import {
SystemInstruction,
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import Copyable from "components/Copyable";
import { Copyable } from "components/common/Copyable";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function AssignWithSeedDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function CreateDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,9 +7,9 @@ import {
} from "@solana/web3.js";
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import Copyable from "components/Copyable";
import { Copyable } from "components/common/Copyable";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function CreateWithSeedDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function NonceAdvanceDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function NonceAuthorizeDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -7,7 +7,7 @@ import {
} from "@solana/web3.js";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function NonceInitializeDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function NonceWithdrawDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -8,7 +8,7 @@ import {
import { lamportsToSolString } from "utils";
import { InstructionCard } from "../InstructionCard";
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
export function TransferDetailsCard(props: {
ix: TransactionInstruction;

View File

@ -9,7 +9,7 @@ import {
import { UnknownDetailsCard } from "../UnknownDetailsCard";
import { InstructionCard } from "../InstructionCard";
import Address from "components/common/Address";
import { Address } from "components/common/Address";
import { ParsedInstructionInfo, IX_STRUCTS } from "./types";
const IX_TITLES = {

View 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} />}
</>
);
}

View File

@ -1,7 +1,7 @@
import React from "react";
import CountUp from "react-countup";
import TableCardBody from "./common/TableCardBody";
import { TableCardBody } from "components/common/TableCardBody";
import {
useDashboardInfo,
usePerformanceInfo,
@ -12,17 +12,19 @@ import {
import { slotsToHumanString } from "utils";
import { useCluster, Cluster } from "providers/cluster";
export default function StatsCard() {
export function ClusterStatsPage() {
return (
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Live Cluster Stats</h4>
<div className="container mt-4">
<div className="card">
<div className="card-header">
<div className="row align-items-center">
<div className="col">
<h4 className="card-header-title">Live Cluster Stats</h4>
</div>
</div>
</div>
<StatsCardBody />
</div>
<StatsCardBody />
</div>
);
}

View 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>
);
}

View File

@ -4,7 +4,7 @@ import {
useTransactionStatus,
useTransactionDetails,
FetchStatus,
} from "../providers/transactions";
} from "providers/transactions";
import { useFetchTransactionDetails } from "providers/transactions/details";
import { useCluster, ClusterStatus } from "providers/cluster";
import {
@ -14,22 +14,22 @@ import {
SystemInstruction,
} from "@solana/web3.js";
import { lamportsToSolString } from "utils";
import { UnknownDetailsCard } from "./instruction/UnknownDetailsCard";
import { SystemDetailsCard } from "./instruction/system/SystemDetailsCard";
import { StakeDetailsCard } from "./instruction/stake/StakeDetailsCard";
import ErrorCard from "./common/ErrorCard";
import LoadingCard from "./common/LoadingCard";
import TableCardBody from "./common/TableCardBody";
import { UnknownDetailsCard } from "components/instruction/UnknownDetailsCard";
import { SystemDetailsCard } from "components/instruction/system/SystemDetailsCard";
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 { displayTimestamp } from "utils/date";
import InfoTooltip from "components/InfoTooltip";
import { InfoTooltip } from "components/common/InfoTooltip";
import { isCached } from "providers/transactions/cached";
import Address from "./common/Address";
import Signature from "./common/Signature";
import { Address } from "components/common/Address";
import { Signature } from "components/common/Signature";
import { intoTransactionInstruction } from "utils/tx";
import { TokenDetailsCard } from "./instruction/token/TokenDetailsCard";
import { TokenDetailsCard } from "components/instruction/token/TokenDetailsCard";
type Props = { signature: TransactionSignature };
export default function TransactionDetails({ signature }: Props) {
export function TransactionDetailsPage({ signature }: Props) {
return (
<div className="container mt-n3">
<div className="header">