Move top accounts to supply page and add filter

This commit is contained in:
Justin Starry 2020-06-02 17:27:07 +08:00 committed by Michael Vines
parent 16b0ee0a21
commit 6c45729694
4 changed files with 181 additions and 49 deletions

View File

@ -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(

View File

@ -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>

View File

@ -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,41 +37,146 @@ 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 (
<div className="card"> <>
{renderHeader()} {showDropdown && (
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
)}
<div className="table-responsive mb-0"> <div className="card">
<table className="table table-sm table-nowrap card-table"> <div className="card-header">
<thead> <div className="row align-items-center">
<tr> <div className="col">
<th className="text-muted">Rank</th> <h4 className="card-header-title">Largest Accounts</h4>
<th className="text-muted">Address</th> </div>
<th className="text-muted">Balance (SOL)</th>
<th className="text-muted">% of Total Supply</th> <div className="col-auto">
<th className="text-muted">Details</th> <FilterDropdown
</tr> filter={filter}
</thead> toggle={() => setDropdown(show => !show)}
<tbody className="list"> show={showDropdown}
{accounts.map((account, index) => />
renderAccountRow(account, index, supply) </div>
)} </div>
</tbody> </div>
</table>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Rank</th>
<th className="text-muted">Address</th>
<th className="text-muted">Balance (SOL)</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, supplyCount)
)}
</tbody>
</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>
); );

View File

@ -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) {
setState(Status.Disconnected); case ClusterStatus.Connecting: {
if (clusterStatus === ClusterStatus.Connected) fetch(setState, url); setState(Status.Disconnected);
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);