From 406f35edfa9bce18eba24cea86a2629d8283c72f Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Sat, 23 May 2020 03:09:28 +0800 Subject: [PATCH] Add supply stats page --- explorer/package-lock.json | 6 +-- explorer/package.json | 2 +- explorer/src/App.tsx | 6 +++ explorer/src/components/SupplyCard.tsx | 50 ++++++++++++++++++++ explorer/src/components/TabbedPage.tsx | 5 +- explorer/src/index.tsx | 13 ++++-- explorer/src/providers/supply.tsx | 64 ++++++++++++++++++++++++++ explorer/src/utils/index.ts | 10 +++- 8 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 explorer/src/components/SupplyCard.tsx create mode 100644 explorer/src/providers/supply.tsx diff --git a/explorer/package-lock.json b/explorer/package-lock.json index ba083a05d4..819af2ffbd 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.52.1", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.52.1.tgz", - "integrity": "sha512-xv8PknS9sjnMxPXaCZEb8Yk+S0O3lScw91NJyYjnIPYxYnxJ96H7BUCDh1zOj5op4nT8u/n4wRYG+YWT/XRwiA==", + "version": "0.54.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.54.0.tgz", + "integrity": "sha512-tpP8PQRPI/pG2pruEOB2La7Oc1xWX3NfiQCcuh7ABkAznVFsWomoWigIaHcxPn5HJZytkhDctTpGjEEiwB0dQQ==", "requires": { "@babel/runtime": "^7.3.1", "bn.js": "^5.0.0", diff --git a/explorer/package.json b/explorer/package.json index 3606424977..c8e658ee16 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@solana/web3.js": "^0.52.1", + "@solana/web3.js": "^0.54.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 e7184e05d1..5b3a0d6304 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 SupplyCard from "components/SupplyCard"; function App() { return ( @@ -29,6 +30,11 @@ function App() { + + + + + [tx, tx + "s"]).map( diff --git a/explorer/src/components/SupplyCard.tsx b/explorer/src/components/SupplyCard.tsx new file mode 100644 index 0000000000..9afb561003 --- /dev/null +++ b/explorer/src/components/SupplyCard.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { useSupply, useFetchSupply } from "providers/supply"; +import LoadingCard from "./common/LoadingCard"; +import ErrorCard from "./common/ErrorCard"; +import { lamportsToSolString } from "utils"; +import TableCardBody from "./common/TableCardBody"; + +export default function SupplyCard() { + const supply = useSupply(); + const fetchSupply = useFetchSupply(); + + if (typeof supply === "boolean") { + if (supply) return ; + return ; + } + + if (typeof supply === "string") { + return ; + } + + return ( +
+ {renderHeader()} + + + + Total Supply (SOL) + {lamportsToSolString(supply.total)} + + + + Circulating Supply (SOL) + {lamportsToSolString(supply.circulating)} + + +
+ ); +} + +const renderHeader = () => { + return ( +
+
+
+

Supply Stats

+
+
+
+ ); +}; diff --git a/explorer/src/components/TabbedPage.tsx b/explorer/src/components/TabbedPage.tsx index 4c1ebba9a3..820bf8294f 100644 --- a/explorer/src/components/TabbedPage.tsx +++ b/explorer/src/components/TabbedPage.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { useClusterModal } from "providers/cluster"; import ClusterStatusButton from "components/ClusterStatusButton"; -export type Tab = "Transactions" | "Accounts"; +export type Tab = "Transactions" | "Accounts" | "Supply"; type Props = { children: React.ReactNode; tab: Tab }; export default function TabbedPage({ children, tab }: Props) { @@ -31,6 +31,9 @@ export default function TabbedPage({ children, tab }: Props) {
  • +
  • + +
  • diff --git a/explorer/src/index.tsx b/explorer/src/index.tsx index 19494e03c2..9a8ff7038f 100644 --- a/explorer/src/index.tsx +++ b/explorer/src/index.tsx @@ -5,17 +5,20 @@ import "./scss/theme.scss"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import { ClusterProvider } from "./providers/cluster"; +import { SupplyProvider } from "./providers/supply"; import { TransactionsProvider } from "./providers/transactions"; import { AccountsProvider } from "./providers/accounts"; ReactDOM.render( - - - - - + + + + + + + , document.getElementById("root") diff --git a/explorer/src/providers/supply.tsx b/explorer/src/providers/supply.tsx new file mode 100644 index 0000000000..ad966e8aa8 --- /dev/null +++ b/explorer/src/providers/supply.tsx @@ -0,0 +1,64 @@ +import React from "react"; + +import { Supply, Connection } from "@solana/web3.js"; +import { useCluster, ClusterStatus } from "./cluster"; + +type State = Supply | boolean | string; + +type Dispatch = React.Dispatch>; +const StateContext = React.createContext(undefined); +const DispatchContext = React.createContext(undefined); + +type Props = { children: React.ReactNode }; +export function SupplyProvider({ 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; + + // Update state if selected cluster hasn't changed + dispatch(state => { + if (!state) return state; + return supply; + }); + } catch (err) { + console.error("Failed to fetch", err); + dispatch("Failed to fetch supply"); + } +} + +export function useSupply() { + const state = React.useContext(StateContext); + if (state === undefined) { + throw new Error(`useSupply must be used within a SupplyProvider`); + } + return state; +} + +export function useFetchSupply() { + const dispatch = React.useContext(DispatchContext); + if (!dispatch) { + throw new Error(`useFetchSupply must be used within a SupplyProvider`); + } + + const { url } = useCluster(); + return () => fetch(dispatch, url); +} diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index 8d59d029ba..ee1d4119aa 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -4,6 +4,12 @@ export function assertUnreachable(x: never): never { throw new Error("Unreachable!"); } -export function lamportsToSolString(lamports: number): string { - return `◎${(1.0 * Math.abs(lamports)) / LAMPORTS_PER_SOL}`; +export function lamportsToSolString( + lamports: number, + maximumFractionDigits: number = 9 +): string { + const sol = Math.abs(lamports) / LAMPORTS_PER_SOL; + return ( + "◎" + new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol) + ); }