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">
|
||||
<TabbedPage tab="Supply">
|
||||
<SupplyCard />
|
||||
</TabbedPage>
|
||||
</Route>
|
||||
<Route exact path="/accounts/top">
|
||||
<TabbedPage tab="Accounts">
|
||||
<TopAccountsCard />
|
||||
</TabbedPage>
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path="/accounts/top"
|
||||
render={({ location }) => (
|
||||
<Redirect to={{ ...location, pathname: "/supply" }} />
|
||||
)}
|
||||
></Route>
|
||||
<Route
|
||||
exact
|
||||
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>{lamportsToSolString(supply.circulating)}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="w-100">Non-Circulating Supply (SOL)</td>
|
||||
<td>{lamportsToSolString(supply.nonCirculating)}</td>
|
||||
</tr>
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
|
@ -49,7 +54,7 @@ const renderHeader = () => {
|
|||
<div className="card-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h4 className="card-header-title">Supply Stats</h4>
|
||||
<h4 className="card-header-title">Overview</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Location } from "history";
|
||||
import { AccountBalancePair } from "@solana/web3.js";
|
||||
import Copyable from "./Copyable";
|
||||
import { useRichList, useFetchRichList, Status } from "providers/richList";
|
||||
import LoadingCard from "./common/LoadingCard";
|
||||
import ErrorCard from "./common/ErrorCard";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { useQuery } from "utils/url";
|
||||
import { useSupply } from "providers/supply";
|
||||
|
||||
type Filter = "circulating" | "nonCirculating" | null;
|
||||
|
||||
export default function TopAccountsCard() {
|
||||
const supply = useSupply();
|
||||
const richList = useRichList();
|
||||
const fetchRichList = useFetchRichList();
|
||||
const [showDropdown, setDropdown] = React.useState(false);
|
||||
const filter = useQueryFilter();
|
||||
|
||||
// Fetch on load
|
||||
React.useEffect(() => {
|
||||
if (richList === Status.Idle) fetchRichList();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
if (richList === Status.Idle && typeof supply === "object") fetchRichList();
|
||||
}, [supply]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (typeof supply !== "object") return null;
|
||||
|
||||
if (richList === Status.Disconnected) {
|
||||
return <ErrorCard text="Not connected to the cluster" />;
|
||||
|
@ -27,11 +37,51 @@ export default function TopAccountsCard() {
|
|||
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 (
|
||||
<>
|
||||
{showDropdown && (
|
||||
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
|
||||
)}
|
||||
|
||||
<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">
|
||||
<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">Address</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{accounts.map((account, index) =>
|
||||
renderAccountRow(account, index, supply)
|
||||
renderAccountRow(account, index, supplyCount)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 (
|
||||
<div className="card-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h4 className="card-header-title">Largest Accounts</h4>
|
||||
</div>
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-white btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
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>
|
||||
);
|
||||
|
|
|
@ -9,13 +9,13 @@ export enum Status {
|
|||
Connecting
|
||||
}
|
||||
|
||||
type RichList = {
|
||||
accounts: AccountBalancePair[];
|
||||
totalSupply: number;
|
||||
circulatingSupply: number;
|
||||
type RichLists = {
|
||||
total: AccountBalancePair[];
|
||||
circulating: AccountBalancePair[];
|
||||
nonCirculating: AccountBalancePair[];
|
||||
};
|
||||
|
||||
type State = RichList | Status | string;
|
||||
type State = RichLists | Status | string;
|
||||
|
||||
type Dispatch = React.Dispatch<React.SetStateAction<State>>;
|
||||
const StateContext = React.createContext<State | undefined>(undefined);
|
||||
|
@ -28,9 +28,16 @@ export function RichListProvider({ children }: Props) {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (state !== Status.Idle) {
|
||||
if (clusterStatus === ClusterStatus.Connecting)
|
||||
switch (clusterStatus) {
|
||||
case ClusterStatus.Connecting: {
|
||||
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
|
||||
|
||||
|
@ -48,17 +55,19 @@ async function fetch(dispatch: Dispatch, url: string) {
|
|||
|
||||
try {
|
||||
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
|
||||
dispatch(state => {
|
||||
if (state !== Status.Connecting) return state;
|
||||
return {
|
||||
accounts,
|
||||
totalSupply: supply.total,
|
||||
circulatingSupply: supply.circulating
|
||||
};
|
||||
return { total, circulating, nonCirculating };
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch", err);
|
||||
|
|
Loading…
Reference in New Issue