diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index 4da5ebcb7e..a2b9455ab0 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -34,13 +34,16 @@ function App() { - - - - + ( + + )} + > [tx, tx + "s"]).map( diff --git a/explorer/src/components/SupplyCard.tsx b/explorer/src/components/SupplyCard.tsx index 93b6564bfd..08e2e42561 100644 --- a/explorer/src/components/SupplyCard.tsx +++ b/explorer/src/components/SupplyCard.tsx @@ -39,6 +39,11 @@ export default function SupplyCard() { Circulating Supply (SOL) {lamportsToSolString(supply.circulating)} + + + Non-Circulating Supply (SOL) + {lamportsToSolString(supply.nonCirculating)} + ); @@ -49,7 +54,7 @@ const renderHeader = () => {
-

Supply Stats

+

Overview

diff --git a/explorer/src/components/TopAccountsCard.tsx b/explorer/src/components/TopAccountsCard.tsx index 500d8c3408..b1b189e0cc 100644 --- a/explorer/src/components/TopAccountsCard.tsx +++ b/explorer/src/components/TopAccountsCard.tsx @@ -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 ; @@ -27,41 +37,146 @@ export default function TopAccountsCard() { return ; } - 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 ( -
- {renderHeader()} + <> + {showDropdown && ( +
setDropdown(false)} /> + )} -
- - - - - - - - - - - - {accounts.map((account, index) => - renderAccountRow(account, index, supply) - )} - -
RankAddressBalance (SOL)% of Total SupplyDetails
+
+
+
+
+

Largest Accounts

+
+ +
+ setDropdown(show => !show)} + show={showDropdown} + /> +
+
+
+ +
+ + + + + + + + + + + + {accounts.map((account, index) => + renderAccountRow(account, index, supplyCount) + )} + +
RankAddressBalance (SOL)% of {header} SupplyDetails
+
-
+ ); } -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 ( -
-
-
-

Largest Accounts

-
+
+ +
+ {FILTERS.map(filterOption => { + return ( + buildLocation(location, filterOption)} + className={`dropdown-item${ + filterOption === filter ? " active" : "" + }`} + onClick={toggle} + > + {filterTitle(filterOption)} + + ); + })}
); diff --git a/explorer/src/providers/richList.tsx b/explorer/src/providers/richList.tsx index d236542b67..daf0a9f5cc 100644 --- a/explorer/src/providers/richList.tsx +++ b/explorer/src/providers/richList.tsx @@ -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>; const StateContext = React.createContext(undefined); @@ -28,9 +28,16 @@ export function RichListProvider({ children }: Props) { React.useEffect(() => { if (state !== Status.Idle) { - if (clusterStatus === ClusterStatus.Connecting) - setState(Status.Disconnected); - if (clusterStatus === ClusterStatus.Connected) fetch(setState, url); + switch (clusterStatus) { + case ClusterStatus.Connecting: { + setState(Status.Disconnected); + 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);