diff --git a/explorer/package-lock.json b/explorer/package-lock.json index 819af2ffbd..c5ee1946d9 100644 --- a/explorer/package-lock.json +++ b/explorer/package-lock.json @@ -1269,9 +1269,9 @@ "integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw==" }, "@solana/web3.js": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.54.0.tgz", - "integrity": "sha512-tpP8PQRPI/pG2pruEOB2La7Oc1xWX3NfiQCcuh7ABkAznVFsWomoWigIaHcxPn5HJZytkhDctTpGjEEiwB0dQQ==", + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.55.0.tgz", + "integrity": "sha512-1VfcfeI8nNJSlFA8ZZpyoaZHeftyvmBfE9dRyPtqsZszPfbtyA6NeOvydRBaSSU7XDC7hQZnKP4QFIf6uef8Mw==", "requires": { "@babel/runtime": "^7.3.1", "bn.js": "^5.0.0", diff --git a/explorer/package.json b/explorer/package.json index c8e658ee16..c42e4e82fc 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@solana/web3.js": "^0.54.0", + "@solana/web3.js": "^0.55.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index 5b3a0d6304..4da5ebcb7e 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -10,6 +10,7 @@ import Logo from "./img/logos-solana/light-explorer-logo.svg"; import { TX_ALIASES } from "./providers/transactions"; import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts"; import TabbedPage from "components/TabbedPage"; +import TopAccountsCard from "components/TopAccountsCard"; import SupplyCard from "components/SupplyCard"; function App() { @@ -35,6 +36,11 @@ function App() { + + + + + [tx, tx + "s"]).map( diff --git a/explorer/src/components/TopAccountsCard.tsx b/explorer/src/components/TopAccountsCard.tsx new file mode 100644 index 0000000000..437a8c3493 --- /dev/null +++ b/explorer/src/components/TopAccountsCard.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { AccountBalancePair } from "@solana/web3.js"; +import Copyable from "./Copyable"; +import { useRichList, useFetchRichList } from "providers/richList"; +import LoadingCard from "./common/LoadingCard"; +import ErrorCard from "./common/ErrorCard"; +import { lamportsToSolString } from "utils"; + +export default function TopAccountsCard() { + const richList = useRichList(); + const fetchRichList = useFetchRichList(); + + if (typeof richList === "boolean") { + if (richList) return ; + return ; + } + + if (typeof richList === "string") { + return ; + } + + const { accounts, circulatingSupply: supply } = richList; + + return ( +
+ {renderHeader()} + +
+ + + + + + + + + + + + {accounts.map((account, index) => + renderAccountRow(account, index, supply) + )} + +
RankAddressBalance (SOL)% of Circulating SupplyDetails
+
+
+ ); +} + +const renderHeader = () => { + return ( +
+
+
+

Top 20 Active Accounts

+
+
+
+ ); +}; + +const renderAccountRow = ( + account: AccountBalancePair, + index: number, + supply: number +) => { + const base58AccountPubkey = account.address.toBase58(); + return ( + + + {index + 1} + + + + {base58AccountPubkey} + + + {lamportsToSolString(account.lamports, 0)} + {`${((100 * account.lamports) / supply).toFixed(3)}%`} + + ({ + ...location, + pathname: "/account/" + base58AccountPubkey + })} + className="btn btn-rounded-circle btn-white btn-sm" + > + + + + + ); +}; diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx index 9a8ff7038f..9ea17b5c22 100644 --- a/explorer/src/index.tsx +++ b/explorer/src/index.tsx @@ -5,6 +5,7 @@ import "./scss/theme.scss"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import { ClusterProvider } from "./providers/cluster"; +import { RichListProvider } from "./providers/richList"; import { SupplyProvider } from "./providers/supply"; import { TransactionsProvider } from "./providers/transactions"; import { AccountsProvider } from "./providers/accounts"; @@ -13,11 +14,13 @@ ReactDOM.render( - - - - - + + + + + + + , diff --git a/explorer/src/providers/richList.tsx b/explorer/src/providers/richList.tsx new file mode 100644 index 0000000000..5ec5e6db07 --- /dev/null +++ b/explorer/src/providers/richList.tsx @@ -0,0 +1,77 @@ +import React from "react"; + +import { AccountBalancePair, Connection } from "@solana/web3.js"; +import { useCluster, ClusterStatus } from "./cluster"; + +type RichList = { + accounts: AccountBalancePair[]; + totalSupply: number; + circulatingSupply: number; +}; + +type State = RichList | boolean | string; + +type Dispatch = React.Dispatch>; +const StateContext = React.createContext(undefined); +const DispatchContext = React.createContext(undefined); + +type Props = { children: React.ReactNode }; +export function RichListProvider({ children }: Props) { + const [state, setState] = React.useState(false); + const { status, url } = useCluster(); + + React.useEffect(() => { + if (status === ClusterStatus.Connecting) setState(false); + if (status === ClusterStatus.Connected) fetch(setState, url); + }, [status, url]); + + return ( + + + {children} + + + ); +} + +async function fetch(dispatch: Dispatch, url: string) { + dispatch(true); + try { + const connection = new Connection(url, "max"); + const supply = (await connection.getSupply()).value; + const accounts = ( + await connection.getLargestAccounts({ filter: "circulating" }) + ).value; + + // Update state if selected cluster hasn't changed + dispatch(state => { + if (!state) return state; + return { + accounts, + totalSupply: supply.total, + circulatingSupply: supply.circulating + }; + }); + } catch (err) { + console.error("Failed to fetch", err); + dispatch("Failed to fetch top accounts"); + } +} + +export function useRichList() { + const state = React.useContext(StateContext); + if (state === undefined) { + throw new Error(`useRichList must be used within a RichListProvider`); + } + return state; +} + +export function useFetchRichList() { + const dispatch = React.useContext(DispatchContext); + if (!dispatch) { + throw new Error(`useFetchRichList must be used within a RichListProvider`); + } + + const { url } = useCluster(); + return () => fetch(dispatch, url); +}