Move top accounts to supply page and add filter
This commit is contained in:
parent
16b0ee0a21
commit
6c45729694
|
@ -34,13 +34,16 @@ function App() {
|
||||||
<Route exact path="/supply">
|
<Route exact path="/supply">
|
||||||
<TabbedPage tab="Supply">
|
<TabbedPage tab="Supply">
|
||||||
<SupplyCard />
|
<SupplyCard />
|
||||||
</TabbedPage>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/accounts/top">
|
|
||||||
<TabbedPage tab="Accounts">
|
|
||||||
<TopAccountsCard />
|
<TopAccountsCard />
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/accounts/top"
|
||||||
|
render={({ location }) => (
|
||||||
|
<Redirect to={{ ...location, pathname: "/supply" }} />
|
||||||
|
)}
|
||||||
|
></Route>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={TX_ALIASES.flatMap(tx => [tx, tx + "s"]).map(
|
path={TX_ALIASES.flatMap(tx => [tx, tx + "s"]).map(
|
||||||
|
|
|
@ -39,6 +39,11 @@ export default function SupplyCard() {
|
||||||
<td className="w-100">Circulating Supply (SOL)</td>
|
<td className="w-100">Circulating Supply (SOL)</td>
|
||||||
<td>{lamportsToSolString(supply.circulating)}</td>
|
<td>{lamportsToSolString(supply.circulating)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Non-Circulating Supply (SOL)</td>
|
||||||
|
<td>{lamportsToSolString(supply.nonCirculating)}</td>
|
||||||
|
</tr>
|
||||||
</TableCardBody>
|
</TableCardBody>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +54,7 @@ const renderHeader = () => {
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<h4 className="card-header-title">Supply Stats</h4>
|
<h4 className="card-header-title">Overview</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Location } from "history";
|
||||||
import { AccountBalancePair } from "@solana/web3.js";
|
import { AccountBalancePair } from "@solana/web3.js";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
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 { useSupply } from "providers/supply";
|
||||||
|
|
||||||
|
type Filter = "circulating" | "nonCirculating" | null;
|
||||||
|
|
||||||
export default function TopAccountsCard() {
|
export default function TopAccountsCard() {
|
||||||
|
const supply = useSupply();
|
||||||
const richList = useRichList();
|
const richList = useRichList();
|
||||||
const fetchRichList = useFetchRichList();
|
const fetchRichList = useFetchRichList();
|
||||||
|
const [showDropdown, setDropdown] = React.useState(false);
|
||||||
|
const filter = useQueryFilter();
|
||||||
|
|
||||||
// Fetch on load
|
// Fetch on load
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (richList === Status.Idle) fetchRichList();
|
if (richList === Status.Idle && typeof supply === "object") fetchRichList();
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [supply]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (typeof supply !== "object") return null;
|
||||||
|
|
||||||
if (richList === Status.Disconnected) {
|
if (richList === Status.Disconnected) {
|
||||||
return <ErrorCard text="Not connected to the cluster" />;
|
return <ErrorCard text="Not connected to the cluster" />;
|
||||||
|
@ -27,11 +37,51 @@ export default function TopAccountsCard() {
|
||||||
return <ErrorCard text={richList} retry={fetchRichList} />;
|
return <ErrorCard text={richList} retry={fetchRichList} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accounts, totalSupply: supply } = richList;
|
let supplyCount: number;
|
||||||
|
let accounts, header;
|
||||||
|
switch (filter) {
|
||||||
|
case "circulating": {
|
||||||
|
accounts = richList.circulating;
|
||||||
|
supplyCount = supply.circulating;
|
||||||
|
header = "Circulating";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "nonCirculating": {
|
||||||
|
accounts = richList.nonCirculating;
|
||||||
|
supplyCount = supply.nonCirculating;
|
||||||
|
header = "Non-Circulating";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
accounts = richList.total;
|
||||||
|
supplyCount = supply.total;
|
||||||
|
header = "Total";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{showDropdown && (
|
||||||
|
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
{renderHeader()}
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h4 className="card-header-title">Largest Accounts</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-auto">
|
||||||
|
<FilterDropdown
|
||||||
|
filter={filter}
|
||||||
|
toggle={() => setDropdown(show => !show)}
|
||||||
|
show={showDropdown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
|
@ -40,28 +90,93 @@ export default function TopAccountsCard() {
|
||||||
<th className="text-muted">Rank</th>
|
<th className="text-muted">Rank</th>
|
||||||
<th className="text-muted">Address</th>
|
<th className="text-muted">Address</th>
|
||||||
<th className="text-muted">Balance (SOL)</th>
|
<th className="text-muted">Balance (SOL)</th>
|
||||||
<th className="text-muted">% of Total Supply</th>
|
<th className="text-muted">% of {header} Supply</th>
|
||||||
<th className="text-muted">Details</th>
|
<th className="text-muted">Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="list">
|
<tbody className="list">
|
||||||
{accounts.map((account, index) =>
|
{accounts.map((account, index) =>
|
||||||
renderAccountRow(account, index, supply)
|
renderAccountRow(account, index, supplyCount)
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderHeader = () => {
|
const useQueryFilter = (): Filter => {
|
||||||
|
const query = useQuery();
|
||||||
|
const filter = query.get("filter");
|
||||||
|
if (filter === "circulating" || filter === "nonCirculating") {
|
||||||
|
return filter;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTitle = (filter: Filter): string => {
|
||||||
|
switch (filter) {
|
||||||
|
case "circulating": {
|
||||||
|
return "Circulating";
|
||||||
|
}
|
||||||
|
case "nonCirculating": {
|
||||||
|
return "Non-Circulating";
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return "All";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownProps = {
|
||||||
|
filter: Filter;
|
||||||
|
toggle: () => void;
|
||||||
|
show: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterDropdown = ({ filter, toggle, show }: DropdownProps) => {
|
||||||
|
const buildLocation = (location: Location, filter: Filter) => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
if (filter === null) {
|
||||||
|
params.delete("filter");
|
||||||
|
} else {
|
||||||
|
params.set("filter", filter);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
search: params.toString()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILTERS: Filter[] = [null, "circulating", "nonCirculating"];
|
||||||
return (
|
return (
|
||||||
<div className="card-header">
|
<div className="dropdown">
|
||||||
<div className="row align-items-center">
|
<button
|
||||||
<div className="col">
|
className="btn btn-white btn-sm dropdown-toggle"
|
||||||
<h4 className="card-header-title">Largest Accounts</h4>
|
type="button"
|
||||||
</div>
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{filterTitle(filter)}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={`dropdown-menu-right dropdown-menu${show ? " show" : ""}`}
|
||||||
|
>
|
||||||
|
{FILTERS.map(filterOption => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={filterOption || "null"}
|
||||||
|
to={location => buildLocation(location, filterOption)}
|
||||||
|
className={`dropdown-item${
|
||||||
|
filterOption === filter ? " active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{filterTitle(filterOption)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,13 +9,13 @@ export enum Status {
|
||||||
Connecting
|
Connecting
|
||||||
}
|
}
|
||||||
|
|
||||||
type RichList = {
|
type RichLists = {
|
||||||
accounts: AccountBalancePair[];
|
total: AccountBalancePair[];
|
||||||
totalSupply: number;
|
circulating: AccountBalancePair[];
|
||||||
circulatingSupply: number;
|
nonCirculating: AccountBalancePair[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = RichList | Status | string;
|
type State = RichLists | Status | string;
|
||||||
|
|
||||||
type Dispatch = React.Dispatch<React.SetStateAction<State>>;
|
type Dispatch = React.Dispatch<React.SetStateAction<State>>;
|
||||||
const StateContext = React.createContext<State | undefined>(undefined);
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
|
@ -28,9 +28,16 @@ export function RichListProvider({ children }: Props) {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (state !== Status.Idle) {
|
if (state !== Status.Idle) {
|
||||||
if (clusterStatus === ClusterStatus.Connecting)
|
switch (clusterStatus) {
|
||||||
|
case ClusterStatus.Connecting: {
|
||||||
setState(Status.Disconnected);
|
setState(Status.Disconnected);
|
||||||
if (clusterStatus === ClusterStatus.Connected) fetch(setState, url);
|
break;
|
||||||
|
}
|
||||||
|
case ClusterStatus.Connected: {
|
||||||
|
fetch(setState, url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [clusterStatus, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
@ -48,17 +55,19 @@ async function fetch(dispatch: Dispatch, url: string) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connection = new Connection(url, "max");
|
const connection = new Connection(url, "max");
|
||||||
const supply = (await connection.getSupply()).value;
|
|
||||||
const accounts = (await connection.getLargestAccounts()).value;
|
const [total, circulating, nonCirculating] = (
|
||||||
|
await Promise.all([
|
||||||
|
connection.getLargestAccounts(),
|
||||||
|
connection.getLargestAccounts({ filter: "circulating" }),
|
||||||
|
connection.getLargestAccounts({ filter: "nonCirculating" })
|
||||||
|
])
|
||||||
|
).map(response => response.value);
|
||||||
|
|
||||||
// Update state if still connecting
|
// Update state if still connecting
|
||||||
dispatch(state => {
|
dispatch(state => {
|
||||||
if (state !== Status.Connecting) return state;
|
if (state !== Status.Connecting) return state;
|
||||||
return {
|
return { total, circulating, nonCirculating };
|
||||||
accounts,
|
|
||||||
totalSupply: supply.total,
|
|
||||||
circulatingSupply: supply.circulating
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch", err);
|
console.error("Failed to fetch", err);
|
||||||
|
|
Loading…
Reference in New Issue