Add top accounts page
This commit is contained in:
parent
c5dcc3c378
commit
d842d161cf
|
@ -1269,9 +1269,9 @@
|
||||||
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
"integrity": "sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw=="
|
||||||
},
|
},
|
||||||
"@solana/web3.js": {
|
"@solana/web3.js": {
|
||||||
"version": "0.54.0",
|
"version": "0.55.0",
|
||||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.55.0.tgz",
|
||||||
"integrity": "sha512-tpP8PQRPI/pG2pruEOB2La7Oc1xWX3NfiQCcuh7ABkAznVFsWomoWigIaHcxPn5HJZytkhDctTpGjEEiwB0dQQ==",
|
"integrity": "sha512-1VfcfeI8nNJSlFA8ZZpyoaZHeftyvmBfE9dRyPtqsZszPfbtyA6NeOvydRBaSSU7XDC7hQZnKP4QFIf6uef8Mw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.3.1",
|
"@babel/runtime": "^7.3.1",
|
||||||
"bn.js": "^5.0.0",
|
"bn.js": "^5.0.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solana/web3.js": "^0.54.0",
|
"@solana/web3.js": "^0.55.0",
|
||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.3.2",
|
"@testing-library/react": "^9.3.2",
|
||||||
"@testing-library/user-event": "^7.1.2",
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Logo from "./img/logos-solana/light-explorer-logo.svg";
|
||||||
import { TX_ALIASES } from "./providers/transactions";
|
import { TX_ALIASES } from "./providers/transactions";
|
||||||
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
import { ACCOUNT_ALIASES, ACCOUNT_ALIASES_PLURAL } from "./providers/accounts";
|
||||||
import TabbedPage from "components/TabbedPage";
|
import TabbedPage from "components/TabbedPage";
|
||||||
|
import TopAccountsCard from "components/TopAccountsCard";
|
||||||
import SupplyCard from "components/SupplyCard";
|
import SupplyCard from "components/SupplyCard";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
@ -35,6 +36,11 @@ function App() {
|
||||||
<SupplyCard />
|
<SupplyCard />
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/accounts/top">
|
||||||
|
<TabbedPage tab="Accounts">
|
||||||
|
<TopAccountsCard />
|
||||||
|
</TabbedPage>
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={TX_ALIASES.flatMap(tx => [tx, tx + "s"]).map(
|
path={TX_ALIASES.flatMap(tx => [tx, tx + "s"]).map(
|
||||||
|
|
|
@ -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 <LoadingCard />;
|
||||||
|
return <ErrorCard text="Not connected to the cluster" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof richList === "string") {
|
||||||
|
return <ErrorCard text={richList} retry={fetchRichList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accounts, circulatingSupply: supply } = richList;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
{renderHeader()}
|
||||||
|
|
||||||
|
<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 Circulating Supply</th>
|
||||||
|
<th className="text-muted">Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">
|
||||||
|
{accounts.map((account, index) =>
|
||||||
|
renderAccountRow(account, index, supply)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h4 className="card-header-title">Top 20 Active Accounts</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAccountRow = (
|
||||||
|
account: AccountBalancePair,
|
||||||
|
index: number,
|
||||||
|
supply: number
|
||||||
|
) => {
|
||||||
|
const base58AccountPubkey = account.address.toBase58();
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
<span className="badge badge-soft-dark badge-pill">{index + 1}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Copyable text={base58AccountPubkey}>
|
||||||
|
<code>{base58AccountPubkey}</code>
|
||||||
|
</Copyable>
|
||||||
|
</td>
|
||||||
|
<td>{lamportsToSolString(account.lamports, 0)}</td>
|
||||||
|
<td>{`${((100 * account.lamports) / supply).toFixed(3)}%`}</td>
|
||||||
|
<td>
|
||||||
|
<Link
|
||||||
|
to={location => ({
|
||||||
|
...location,
|
||||||
|
pathname: "/account/" + base58AccountPubkey
|
||||||
|
})}
|
||||||
|
className="btn btn-rounded-circle btn-white btn-sm"
|
||||||
|
>
|
||||||
|
<span className="fe fe-arrow-right"></span>
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,6 +5,7 @@ import "./scss/theme.scss";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import { ClusterProvider } from "./providers/cluster";
|
import { ClusterProvider } from "./providers/cluster";
|
||||||
|
import { RichListProvider } from "./providers/richList";
|
||||||
import { SupplyProvider } from "./providers/supply";
|
import { SupplyProvider } from "./providers/supply";
|
||||||
import { TransactionsProvider } from "./providers/transactions";
|
import { TransactionsProvider } from "./providers/transactions";
|
||||||
import { AccountsProvider } from "./providers/accounts";
|
import { AccountsProvider } from "./providers/accounts";
|
||||||
|
@ -13,11 +14,13 @@ ReactDOM.render(
|
||||||
<Router>
|
<Router>
|
||||||
<ClusterProvider>
|
<ClusterProvider>
|
||||||
<SupplyProvider>
|
<SupplyProvider>
|
||||||
<AccountsProvider>
|
<RichListProvider>
|
||||||
<TransactionsProvider>
|
<AccountsProvider>
|
||||||
<App />
|
<TransactionsProvider>
|
||||||
</TransactionsProvider>
|
<App />
|
||||||
</AccountsProvider>
|
</TransactionsProvider>
|
||||||
|
</AccountsProvider>
|
||||||
|
</RichListProvider>
|
||||||
</SupplyProvider>
|
</SupplyProvider>
|
||||||
</ClusterProvider>
|
</ClusterProvider>
|
||||||
</Router>,
|
</Router>,
|
||||||
|
|
|
@ -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<React.SetStateAction<State>>;
|
||||||
|
const StateContext = React.createContext<State | undefined>(undefined);
|
||||||
|
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
|
||||||
|
|
||||||
|
type Props = { children: React.ReactNode };
|
||||||
|
export function RichListProvider({ children }: Props) {
|
||||||
|
const [state, setState] = React.useState<State>(false);
|
||||||
|
const { status, url } = useCluster();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (status === ClusterStatus.Connecting) setState(false);
|
||||||
|
if (status === ClusterStatus.Connected) fetch(setState, url);
|
||||||
|
}, [status, url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={state}>
|
||||||
|
<DispatchContext.Provider value={setState}>
|
||||||
|
{children}
|
||||||
|
</DispatchContext.Provider>
|
||||||
|
</StateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
Loading…
Reference in New Issue