2020-05-23 00:17:07 -07:00
|
|
|
import React from "react";
|
|
|
|
import { Link } from "react-router-dom";
|
2020-06-02 02:27:07 -07:00
|
|
|
import { Location } from "history";
|
2020-05-23 00:17:07 -07:00
|
|
|
import { AccountBalancePair } from "@solana/web3.js";
|
2020-05-23 03:02:53 -07:00
|
|
|
import { useRichList, useFetchRichList, Status } from "providers/richList";
|
2020-08-07 23:45:57 -07:00
|
|
|
import { LoadingCard } from "./common/LoadingCard";
|
|
|
|
import { ErrorCard } from "./common/ErrorCard";
|
2022-09-26 02:25:15 -07:00
|
|
|
import { SolBalance } from "components/common/SolBalance";
|
2020-06-02 02:27:07 -07:00
|
|
|
import { useQuery } from "utils/url";
|
|
|
|
import { useSupply } from "providers/supply";
|
2020-08-07 23:45:57 -07:00
|
|
|
import { Address } from "./common/Address";
|
2020-06-02 02:27:07 -07:00
|
|
|
|
2020-06-02 17:43:47 -07:00
|
|
|
type Filter = "circulating" | "nonCirculating" | "all" | null;
|
2020-05-23 00:17:07 -07:00
|
|
|
|
2020-08-07 23:45:57 -07:00
|
|
|
export function TopAccountsCard() {
|
2020-06-02 02:27:07 -07:00
|
|
|
const supply = useSupply();
|
2020-05-23 00:17:07 -07:00
|
|
|
const richList = useRichList();
|
|
|
|
const fetchRichList = useFetchRichList();
|
2020-06-02 02:27:07 -07:00
|
|
|
const [showDropdown, setDropdown] = React.useState(false);
|
|
|
|
const filter = useQueryFilter();
|
2020-05-23 00:17:07 -07:00
|
|
|
|
2020-06-02 02:27:07 -07:00
|
|
|
if (typeof supply !== "object") return null;
|
2020-05-23 03:02:53 -07:00
|
|
|
|
|
|
|
if (richList === Status.Disconnected) {
|
2020-05-23 00:17:07 -07:00
|
|
|
return <ErrorCard text="Not connected to the cluster" />;
|
|
|
|
}
|
|
|
|
|
2021-01-05 11:22:15 -08:00
|
|
|
if (richList === Status.Connecting) {
|
2020-05-23 03:02:53 -07:00
|
|
|
return <LoadingCard />;
|
2021-01-05 11:22:15 -08:00
|
|
|
}
|
2020-05-23 03:02:53 -07:00
|
|
|
|
2020-05-23 00:17:07 -07:00
|
|
|
if (typeof richList === "string") {
|
|
|
|
return <ErrorCard text={richList} retry={fetchRichList} />;
|
|
|
|
}
|
|
|
|
|
2020-06-02 02:27:07 -07:00
|
|
|
let supplyCount: number;
|
|
|
|
let accounts, header;
|
2021-01-05 11:22:15 -08:00
|
|
|
|
|
|
|
if (richList !== Status.Idle) {
|
|
|
|
switch (filter) {
|
|
|
|
case "nonCirculating": {
|
|
|
|
accounts = richList.nonCirculating;
|
|
|
|
supplyCount = supply.nonCirculating;
|
|
|
|
header = "Non-Circulating";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "all": {
|
|
|
|
accounts = richList.total;
|
|
|
|
supplyCount = supply.total;
|
|
|
|
header = "Total";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "circulating":
|
|
|
|
default: {
|
|
|
|
accounts = richList.circulating;
|
|
|
|
supplyCount = supply.circulating;
|
|
|
|
header = "Circulating";
|
|
|
|
break;
|
|
|
|
}
|
2020-06-02 17:43:47 -07:00
|
|
|
}
|
2020-06-02 02:27:07 -07:00
|
|
|
}
|
2020-05-23 00:17:07 -07:00
|
|
|
|
|
|
|
return (
|
2020-06-02 02:27:07 -07:00
|
|
|
<>
|
|
|
|
{showDropdown && (
|
|
|
|
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
|
|
|
|
)}
|
|
|
|
|
|
|
|
<div className="card">
|
|
|
|
<div className="card-header">
|
|
|
|
<div className="row align-items-center">
|
|
|
|
<div className="col">
|
|
|
|
<h4 className="card-header-title">Largest Accounts</h4>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="col-auto">
|
|
|
|
<FilterDropdown
|
|
|
|
filter={filter}
|
2020-06-24 01:07:47 -07:00
|
|
|
toggle={() => setDropdown((show) => !show)}
|
2020-06-02 02:27:07 -07:00
|
|
|
show={showDropdown}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2021-01-05 11:22:15 -08:00
|
|
|
{richList === Status.Idle && (
|
|
|
|
<div className="card-body">
|
|
|
|
<span
|
2021-11-28 12:49:22 -08:00
|
|
|
className="btn btn-white ms-3 d-none d-md-inline"
|
2021-01-05 11:22:15 -08:00
|
|
|
onClick={fetchRichList}
|
|
|
|
>
|
|
|
|
Load Largest Accounts
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{accounts && (
|
|
|
|
<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>
|
2021-11-28 12:49:22 -08:00
|
|
|
<th className="text-muted text-end">Balance (SOL)</th>
|
|
|
|
<th className="text-muted text-end">% of {header} Supply</th>
|
2021-01-05 11:22:15 -08:00
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody className="list">
|
|
|
|
{accounts.map((account, index) =>
|
|
|
|
renderAccountRow(account, index, supplyCount)
|
|
|
|
)}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
)}
|
2020-05-23 00:17:07 -07:00
|
|
|
</div>
|
2020-06-02 02:27:07 -07:00
|
|
|
</>
|
2020-05-23 00:17:07 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-03 03:40:30 -07:00
|
|
|
const renderAccountRow = (
|
|
|
|
account: AccountBalancePair,
|
|
|
|
index: number,
|
|
|
|
supply: number
|
|
|
|
) => {
|
|
|
|
return (
|
|
|
|
<tr key={index}>
|
|
|
|
<td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<span className="badge bg-gray-soft badge-pill">{index + 1}</span>
|
2020-06-03 03:40:30 -07:00
|
|
|
</td>
|
|
|
|
<td>
|
2020-08-02 10:44:47 -07:00
|
|
|
<Address pubkey={account.address} link />
|
2020-06-03 03:40:30 -07:00
|
|
|
</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-end">
|
2021-06-21 16:53:06 -07:00
|
|
|
<SolBalance lamports={account.lamports} maximumFractionDigits={0} />
|
|
|
|
</td>
|
2021-11-28 12:49:22 -08:00
|
|
|
<td className="text-end">{`${((100 * account.lamports) / supply).toFixed(
|
|
|
|
3
|
|
|
|
)}%`}</td>
|
2020-06-03 03:40:30 -07:00
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-06-02 02:27:07 -07:00
|
|
|
const useQueryFilter = (): Filter => {
|
|
|
|
const query = useQuery();
|
|
|
|
const filter = query.get("filter");
|
2020-06-02 17:43:47 -07:00
|
|
|
if (
|
|
|
|
filter === "circulating" ||
|
|
|
|
filter === "nonCirculating" ||
|
|
|
|
filter === "all"
|
|
|
|
) {
|
2020-06-02 02:27:07 -07:00
|
|
|
return filter;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const filterTitle = (filter: Filter): string => {
|
|
|
|
switch (filter) {
|
|
|
|
case "nonCirculating": {
|
|
|
|
return "Non-Circulating";
|
|
|
|
}
|
2020-06-02 17:43:47 -07:00
|
|
|
case "all": {
|
2020-06-02 02:27:07 -07:00
|
|
|
return "All";
|
|
|
|
}
|
2020-06-02 17:43:47 -07:00
|
|
|
case "circulating":
|
|
|
|
default: {
|
|
|
|
return "Circulating";
|
|
|
|
}
|
2020-06-02 02:27:07 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
2020-06-24 01:07:47 -07:00
|
|
|
search: params.toString(),
|
2020-06-02 02:27:07 -07:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2020-06-03 03:40:30 -07:00
|
|
|
const FILTERS: Filter[] = ["all", null, "nonCirculating"];
|
2020-05-23 00:17:07 -07:00
|
|
|
return (
|
2020-06-02 02:27:07 -07:00
|
|
|
<div className="dropdown">
|
|
|
|
<button
|
|
|
|
className="btn btn-white btn-sm dropdown-toggle"
|
|
|
|
type="button"
|
|
|
|
onClick={toggle}
|
|
|
|
>
|
|
|
|
{filterTitle(filter)}
|
|
|
|
</button>
|
2021-11-28 12:49:22 -08:00
|
|
|
<div className={`dropdown-menu-end dropdown-menu${show ? " show" : ""}`}>
|
2020-06-24 01:07:47 -07:00
|
|
|
{FILTERS.map((filterOption) => {
|
2020-06-02 02:27:07 -07:00
|
|
|
return (
|
|
|
|
<Link
|
|
|
|
key={filterOption || "null"}
|
2020-06-24 01:07:47 -07:00
|
|
|
to={(location) => buildLocation(location, filterOption)}
|
2020-06-02 02:27:07 -07:00
|
|
|
className={`dropdown-item${
|
|
|
|
filterOption === filter ? " active" : ""
|
|
|
|
}`}
|
|
|
|
onClick={toggle}
|
|
|
|
>
|
|
|
|
{filterTitle(filterOption)}
|
|
|
|
</Link>
|
|
|
|
);
|
|
|
|
})}
|
2020-05-23 00:17:07 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|