Add token registry to explorer (#11612)
This commit is contained in:
parent
00a8f90f79
commit
6162c2d0d5
Binary file not shown.
After Width: | Height: | Size: 725 B |
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -3,13 +3,16 @@ import bs58 from "bs58";
|
|||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import Select, { InputActionMeta, ActionMeta, ValueType } from "react-select";
|
||||
import StateManager from "react-select";
|
||||
import { PROGRAM_IDS, SYSVAR_IDS } from "utils/tx";
|
||||
import { PROGRAM_IDS, SYSVAR_IDS, ProgramName } from "utils/tx";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { Cluster, useCluster } from "providers/cluster";
|
||||
|
||||
export function SearchBar() {
|
||||
const [search, setSearch] = React.useState("");
|
||||
const selectRef = React.useRef<StateManager<any> | null>(null);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
const onChange = ({ pathname }: ValueType<any>, meta: ActionMeta<any>) => {
|
||||
if (meta.action === "select-option") {
|
||||
|
@ -29,9 +32,9 @@ export function SearchBar() {
|
|||
<div className="col">
|
||||
<Select
|
||||
ref={(ref) => (selectRef.current = ref)}
|
||||
options={buildOptions(search)}
|
||||
options={buildOptions(search, cluster)}
|
||||
noOptionsMessage={() => "No Results"}
|
||||
placeholder="Search by address or signature"
|
||||
placeholder="Search for accounts, transactions, programs, and tokens"
|
||||
value={resetValue}
|
||||
inputValue={search}
|
||||
blurInputOnSelect
|
||||
|
@ -47,22 +50,31 @@ export function SearchBar() {
|
|||
);
|
||||
}
|
||||
|
||||
const SEARCHABLE_PROGRAMS = ["Config", "Stake", "System", "Vote", "Token"];
|
||||
const SEARCHABLE_PROGRAMS: ProgramName[] = [
|
||||
"Config Program",
|
||||
"Stake Program",
|
||||
"System Program",
|
||||
"Vote Program",
|
||||
"SPL Token",
|
||||
];
|
||||
|
||||
function buildProgramOptions(search: string) {
|
||||
const matchedPrograms = Object.entries(PROGRAM_IDS).filter(([, name]) => {
|
||||
const matchedPrograms = Object.entries(PROGRAM_IDS).filter(
|
||||
([address, name]) => {
|
||||
return (
|
||||
SEARCHABLE_PROGRAMS.includes(name) &&
|
||||
name.toLowerCase().includes(search.toLowerCase())
|
||||
(name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
address.includes(search))
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (matchedPrograms.length > 0) {
|
||||
return {
|
||||
label: "Programs",
|
||||
options: matchedPrograms.map(([id, name]) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
value: [name, id],
|
||||
pathname: "/address/" + id,
|
||||
})),
|
||||
};
|
||||
|
@ -70,23 +82,52 @@ function buildProgramOptions(search: string) {
|
|||
}
|
||||
|
||||
function buildSysvarOptions(search: string) {
|
||||
const matchedSysvars = Object.entries(SYSVAR_IDS).filter(([, name]) => {
|
||||
return name.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
const matchedSysvars = Object.entries(SYSVAR_IDS).filter(
|
||||
([address, name]) => {
|
||||
return (
|
||||
name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
address.includes(search)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (matchedSysvars.length > 0) {
|
||||
return {
|
||||
label: "Sysvars",
|
||||
options: matchedSysvars.map(([id, name]) => ({
|
||||
label: name,
|
||||
value: name,
|
||||
value: [name, id],
|
||||
pathname: "/address/" + id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildOptions(search: string) {
|
||||
function buildTokenOptions(search: string, cluster: Cluster) {
|
||||
const matchedTokens = Object.entries(TokenRegistry.all(cluster)).filter(
|
||||
([address, details]) => {
|
||||
const searchLower = search.toLowerCase();
|
||||
return (
|
||||
details.name.toLowerCase().includes(searchLower) ||
|
||||
details.symbol.toLowerCase().includes(searchLower) ||
|
||||
address.includes(search)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (matchedTokens.length > 0) {
|
||||
return {
|
||||
label: "Tokens",
|
||||
options: matchedTokens.map(([id, details]) => ({
|
||||
label: details.name,
|
||||
value: [details.name, details.symbol, id],
|
||||
pathname: "/address/" + id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildOptions(search: string, cluster: Cluster) {
|
||||
if (search.length === 0) return [];
|
||||
|
||||
const options = [];
|
||||
|
@ -101,6 +142,14 @@ function buildOptions(search: string) {
|
|||
options.push(sysvarOptions);
|
||||
}
|
||||
|
||||
const tokenOptions = buildTokenOptions(search, cluster);
|
||||
if (tokenOptions) {
|
||||
options.push(tokenOptions);
|
||||
}
|
||||
|
||||
// Prefer nice suggestions over raw suggestions
|
||||
if (options.length > 0) return options;
|
||||
|
||||
try {
|
||||
const decoded = bs58.decode(search);
|
||||
if (decoded.length === 32) {
|
||||
|
|
|
@ -4,16 +4,36 @@ import { FetchStatus } from "providers/cache";
|
|||
import {
|
||||
useFetchAccountOwnedTokens,
|
||||
useAccountOwnedTokens,
|
||||
TokenInfoWithPubkey,
|
||||
} from "providers/accounts/tokens";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { LoadingCard } from "components/common/LoadingCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { useQuery } from "utils/url";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Location } from "history";
|
||||
import { useCluster } from "providers/cluster";
|
||||
|
||||
type Display = "summary" | "detail" | null;
|
||||
|
||||
const useQueryDisplay = (): Display => {
|
||||
const query = useQuery();
|
||||
const filter = query.get("display");
|
||||
if (filter === "summary" || filter === "detail") {
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
|
||||
const address = pubkey.toBase58();
|
||||
const ownedTokens = useAccountOwnedTokens(address);
|
||||
const fetchAccountTokens = useFetchAccountOwnedTokens();
|
||||
const refresh = () => fetchAccountTokens(pubkey);
|
||||
const [showDropdown, setDropdown] = React.useState(false);
|
||||
const display = useQueryDisplay();
|
||||
|
||||
// Fetch owned tokens
|
||||
React.useEffect(() => {
|
||||
|
@ -28,9 +48,9 @@ export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
const tokens = ownedTokens.data?.tokens;
|
||||
const fetching = status === FetchStatus.Fetching;
|
||||
if (fetching && (tokens === undefined || tokens.length === 0)) {
|
||||
return <LoadingCard message="Loading owned tokens" />;
|
||||
return <LoadingCard message="Loading token holdings" />;
|
||||
} else if (tokens === undefined) {
|
||||
return <ErrorCard retry={refresh} text="Failed to fetch owned tokens" />;
|
||||
return <ErrorCard retry={refresh} text="Failed to fetch token holdings" />;
|
||||
}
|
||||
|
||||
if (tokens.length === 0) {
|
||||
|
@ -38,17 +58,100 @@ export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
<ErrorCard
|
||||
retry={refresh}
|
||||
retryText="Try Again"
|
||||
text={"No owned tokens found"}
|
||||
text={"No token holdings found"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showDropdown && (
|
||||
<div className="dropdown-exit" onClick={() => setDropdown(false)} />
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Token Holdings</h3>
|
||||
<DisplayDropdown
|
||||
display={display}
|
||||
toggle={() => setDropdown((show) => !show)}
|
||||
show={showDropdown}
|
||||
/>
|
||||
</div>
|
||||
{display === "detail" ? (
|
||||
<HoldingsDetailTable tokens={tokens} />
|
||||
) : (
|
||||
<HoldingsSummaryTable tokens={tokens} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HoldingsDetailTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||
const detailsList: React.ReactNode[] = [];
|
||||
const { cluster } = useCluster();
|
||||
const showLogos = tokens.some(
|
||||
(t) => TokenRegistry.get(t.info.mint.toBase58(), cluster) !== undefined
|
||||
);
|
||||
tokens.forEach((tokenAccount) => {
|
||||
const address = tokenAccount.pubkey.toBase58();
|
||||
const mintAddress = tokenAccount.info.mint.toBase58();
|
||||
const tokenDetails = TokenRegistry.get(mintAddress, cluster);
|
||||
detailsList.push(
|
||||
<tr key={address}>
|
||||
{showLogos && (
|
||||
<td className="w-1 p-0 text-center">
|
||||
{tokenDetails && (
|
||||
<img
|
||||
src={tokenDetails.icon}
|
||||
alt="token icon"
|
||||
className="token-icon rounded-circle border border-4 border-gray-dark"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<Address pubkey={tokenAccount.pubkey} link truncate />
|
||||
</td>
|
||||
<td>
|
||||
<Address pubkey={tokenAccount.info.mint} link truncate />
|
||||
</td>
|
||||
<td>
|
||||
{tokenAccount.info.tokenAmount.uiAmount}{" "}
|
||||
{tokenDetails && tokenDetails.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{showLogos && (
|
||||
<th className="text-muted w-1 p-0 text-center">Logo</th>
|
||||
)}
|
||||
<th className="text-muted">Account Address</th>
|
||||
<th className="text-muted">Mint Address</th>
|
||||
<th className="text-muted">Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">{detailsList}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HoldingsSummaryTable({ tokens }: { tokens: TokenInfoWithPubkey[] }) {
|
||||
const { cluster } = useCluster();
|
||||
const mappedTokens = new Map<string, number>();
|
||||
for (const { info: token } of tokens) {
|
||||
const mintAddress = token.mint.toBase58();
|
||||
const totalByMint = mappedTokens.get(mintAddress);
|
||||
|
||||
let amount = token?.amount || (token?.tokenAmount?.uiAmount as number);
|
||||
let amount = token.tokenAmount.uiAmount;
|
||||
if (totalByMint !== undefined) {
|
||||
amount += totalByMint;
|
||||
}
|
||||
|
@ -57,51 +160,100 @@ export function OwnedTokensCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
}
|
||||
|
||||
const detailsList: React.ReactNode[] = [];
|
||||
const showLogos = tokens.some((t) =>
|
||||
TokenRegistry.get(t.info.mint.toBase58(), cluster)
|
||||
);
|
||||
mappedTokens.forEach((totalByMint, mintAddress) => {
|
||||
const tokenDetails = TokenRegistry.get(mintAddress, cluster);
|
||||
detailsList.push(
|
||||
<tr key={mintAddress}>
|
||||
{showLogos && (
|
||||
<td className="w-1 p-0 text-center">
|
||||
{tokenDetails && (
|
||||
<img
|
||||
src={tokenDetails.icon}
|
||||
alt="token icon"
|
||||
className="token-icon rounded-circle border border-4 border-gray-dark"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<Address pubkey={new PublicKey(mintAddress)} link />
|
||||
</td>
|
||||
<td>{totalByMint}</td>
|
||||
<td>
|
||||
{totalByMint} {tokenDetails && tokenDetails.symbol}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Owned Tokens</h3>
|
||||
<button
|
||||
className="btn btn-white btn-sm"
|
||||
disabled={fetching}
|
||||
onClick={refresh}
|
||||
>
|
||||
{fetching ? (
|
||||
<>
|
||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||
Loading
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
Refresh
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-muted">Token Address</th>
|
||||
<th className="text-muted">Balance</th>
|
||||
{showLogos && (
|
||||
<th className="text-muted w-1 p-0 text-center">Logo</th>
|
||||
)}
|
||||
<th className="text-muted">Mint Address</th>
|
||||
<th className="text-muted">Total Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">{detailsList}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DropdownProps = {
|
||||
display: Display;
|
||||
toggle: () => void;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
const DisplayDropdown = ({ display, toggle, show }: DropdownProps) => {
|
||||
const buildLocation = (location: Location, display: Display) => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (display === null) {
|
||||
params.delete("display");
|
||||
} else {
|
||||
params.set("display", display);
|
||||
}
|
||||
return {
|
||||
...location,
|
||||
search: params.toString(),
|
||||
};
|
||||
};
|
||||
|
||||
const DISPLAY_OPTIONS: Display[] = [null, "detail"];
|
||||
return (
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-white btn-sm dropdown-toggle"
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
>
|
||||
{display === "detail" ? "Detailed" : "Summary"}
|
||||
</button>
|
||||
<div
|
||||
className={`dropdown-menu-right dropdown-menu${show ? " show" : ""}`}
|
||||
>
|
||||
{DISPLAY_OPTIONS.map((displayOption) => {
|
||||
return (
|
||||
<Link
|
||||
key={displayOption || "null"}
|
||||
to={(location) => buildLocation(location, displayOption)}
|
||||
className={`dropdown-item${
|
||||
displayOption === display ? " active" : ""
|
||||
}`}
|
||||
onClick={toggle}
|
||||
>
|
||||
{displayOption === "detail" ? "Detailed" : "Summary"}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,8 @@ import { Address } from "components/common/Address";
|
|||
import { UnknownAccountCard } from "./UnknownAccountCard";
|
||||
import { useFetchTokenSupply, useTokenSupply } from "providers/mints/supply";
|
||||
import { FetchStatus } from "providers/cache";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { useCluster } from "providers/cluster";
|
||||
|
||||
export function TokenAccountSection({
|
||||
account,
|
||||
|
@ -46,6 +48,7 @@ function MintAccountCard({
|
|||
account: Account;
|
||||
info: MintAccountInfo;
|
||||
}) {
|
||||
const { cluster } = useCluster();
|
||||
const mintAddress = account.pubkey.toBase58();
|
||||
const fetchInfo = useFetchAccountInfo();
|
||||
const supply = useTokenSupply(mintAddress);
|
||||
|
@ -70,18 +73,20 @@ function MintAccountCard({
|
|||
renderSupply = "Fetch failed";
|
||||
}
|
||||
} else {
|
||||
renderSupply = supplyTotal;
|
||||
const unit = TokenRegistry.get(mintAddress, cluster)?.symbol;
|
||||
renderSupply = unit ? `${supplyTotal} ${unit}` : supplyTotal;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!supply) refreshSupply();
|
||||
}, [mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const tokenInfo = TokenRegistry.get(mintAddress, cluster);
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-header-title mb-0 d-flex align-items-center">
|
||||
Token Mint Account
|
||||
{tokenInfo ? "Overview" : "Token Mint"}
|
||||
</h3>
|
||||
<button className="btn btn-white btn-sm" onClick={refresh}>
|
||||
<span className="fe fe-refresh-cw mr-2"></span>
|
||||
|
@ -100,6 +105,21 @@ function MintAccountCard({
|
|||
<td>Total Supply</td>
|
||||
<td className="text-lg-right">{renderSupply}</td>
|
||||
</tr>
|
||||
{tokenInfo && (
|
||||
<tr>
|
||||
<td>Website</td>
|
||||
<td className="text-lg-right">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={tokenInfo.website}
|
||||
>
|
||||
{tokenInfo.website}
|
||||
<span className="fe fe-external-link ml-2"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>Decimals</td>
|
||||
<td className="text-lg-right">{info.decimals}</td>
|
||||
|
@ -131,13 +151,11 @@ function TokenAccountCard({
|
|||
info: TokenAccountInfo;
|
||||
}) {
|
||||
const refresh = useFetchAccountInfo();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
let balance;
|
||||
if ("amount" in info) {
|
||||
balance = info.amount;
|
||||
} else {
|
||||
balance = info.tokenAmount?.uiAmount;
|
||||
}
|
||||
const balance = info.tokenAmount?.uiAmount;
|
||||
const unit =
|
||||
TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens";
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
|
@ -174,15 +192,15 @@ function TokenAccountCard({
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Balance (tokens)</td>
|
||||
<td>Balance ({unit})</td>
|
||||
<td className="text-lg-right">{balance}</td>
|
||||
</tr>
|
||||
{!info.isInitialized && (
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td className="text-lg-right">
|
||||
{info.isInitialized ? "Initialized" : "Uninitialized"}
|
||||
</td>
|
||||
<td className="text-lg-right">Uninitialized</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
|
@ -235,12 +253,12 @@ function MultisigAccountCard({
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!info.isInitialized && (
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td className="text-lg-right">
|
||||
{info.isInitialized ? "Initialized" : "Uninitialized"}
|
||||
</td>
|
||||
<td className="text-lg-right">Uninitialized</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -233,7 +233,7 @@ function TokenTransactionRow({
|
|||
</td>
|
||||
|
||||
<td>
|
||||
<Address pubkey={mint} link />
|
||||
<Address pubkey={mint} link truncate />
|
||||
</td>
|
||||
|
||||
<td>{typeName}</td>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
useFetchTokenLargestAccounts,
|
||||
} from "providers/mints/largest";
|
||||
import { FetchStatus } from "providers/cache";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { useCluster } from "providers/cluster";
|
||||
|
||||
export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
||||
const mintAddress = pubkey.toBase58();
|
||||
|
@ -16,6 +18,9 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
const largestAccounts = useTokenLargestTokens(mintAddress);
|
||||
const fetchLargestAccounts = useFetchTokenLargestAccounts();
|
||||
const refreshLargest = () => fetchLargestAccounts(pubkey);
|
||||
const { cluster } = useCluster();
|
||||
const unit = TokenRegistry.get(mintAddress, cluster)?.symbol;
|
||||
const unitLabel = unit ? `(${unit})` : "";
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!largestAccounts) refreshLargest();
|
||||
|
@ -61,7 +66,7 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
|
|||
<tr>
|
||||
<th className="text-muted">Rank</th>
|
||||
<th className="text-muted">Address</th>
|
||||
<th className="text-muted text-right">Balance</th>
|
||||
<th className="text-muted text-right">Balance {unitLabel}</th>
|
||||
<th className="text-muted text-right">% of Total Supply</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import { clusterPath } from "utils/url";
|
||||
import { displayAddress } from "utils/tx";
|
||||
import { Pubkey } from "solana-sdk-wasm";
|
||||
import { useCluster } from "providers/cluster";
|
||||
|
||||
type CopyState = "copy" | "copied";
|
||||
type Props = {
|
||||
|
@ -11,11 +12,13 @@ type Props = {
|
|||
alignRight?: boolean;
|
||||
link?: boolean;
|
||||
raw?: boolean;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
||||
export function Address({ pubkey, alignRight, link, raw }: Props) {
|
||||
export function Address({ pubkey, alignRight, link, raw, truncate }: Props) {
|
||||
const [state, setState] = useState<CopyState>("copy");
|
||||
const address = pubkey.toBase58();
|
||||
const { cluster } = useCluster();
|
||||
|
||||
const copyToClipboard = () => navigator.clipboard.writeText(address);
|
||||
const handleClick = () =>
|
||||
|
@ -36,14 +39,16 @@ export function Address({ pubkey, alignRight, link, raw }: Props) {
|
|||
<span className="c-pointer font-size-tiny mr-2">{copyIcon}</span>
|
||||
<span className="text-monospace">
|
||||
{link ? (
|
||||
<Link className="" to={clusterPath(`/address/${address}`)}>
|
||||
{raw ? address : displayAddress(address)}
|
||||
<span className="fe fe-external-link ml-2"></span>
|
||||
<Link
|
||||
className={truncate ? "text-truncate address-truncate" : ""}
|
||||
to={clusterPath(`/address/${address}`)}
|
||||
>
|
||||
{raw ? address : displayAddress(address, cluster)}
|
||||
</Link>
|
||||
) : raw ? (
|
||||
address
|
||||
) : (
|
||||
displayAddress(address)
|
||||
<span className={truncate ? "text-truncate address-truncate" : ""}>
|
||||
{raw ? address : displayAddress(address, cluster)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
|
|
|
@ -42,7 +42,6 @@ export function Signature({ signature, alignRight, link }: Props) {
|
|||
{link ? (
|
||||
<Link className="" to={clusterPath(`/tx/${signature}`)}>
|
||||
{signature}
|
||||
<span className="fe fe-external-link ml-2"></span>
|
||||
</Link>
|
||||
) : (
|
||||
signature
|
||||
|
|
|
@ -18,6 +18,7 @@ import { OwnedTokensCard } from "components/account/OwnedTokensCard";
|
|||
import { TransactionHistoryCard } from "components/account/TransactionHistoryCard";
|
||||
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
|
||||
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
|
||||
type Props = { address: string; tab?: string };
|
||||
export function AccountDetailsPage({ address, tab }: Props) {
|
||||
|
@ -31,8 +32,7 @@ export function AccountDetailsPage({ address, tab }: Props) {
|
|||
<div className="container mt-n3">
|
||||
<div className="header">
|
||||
<div className="header-body">
|
||||
<h6 className="header-pretitle">Details</h6>
|
||||
<h4 className="header-title">Account</h4>
|
||||
<AccountHeader address={address} />
|
||||
</div>
|
||||
</div>
|
||||
{!pubkey ? (
|
||||
|
@ -44,6 +44,38 @@ export function AccountDetailsPage({ address, tab }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export function AccountHeader({ address }: { address: string }) {
|
||||
const { cluster } = useCluster();
|
||||
const tokenDetails = TokenRegistry.get(address, cluster);
|
||||
if (tokenDetails) {
|
||||
return (
|
||||
<div className="row align-items-end">
|
||||
<div className="col-auto">
|
||||
<div className="avatar avatar-lg header-avatar-top">
|
||||
<img
|
||||
src={tokenDetails.logo}
|
||||
alt="token logo"
|
||||
className="avatar-img rounded-circle border border-4 border-body"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col mb-3 ml-n3 ml-md-n2">
|
||||
<h6 className="header-pretitle">Token</h6>
|
||||
<h2 className="header-title">{tokenDetails.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h6 className="header-pretitle">Details</h6>
|
||||
<h2 className="header-title">Account</h2>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
|
||||
const fetchAccount = useFetchAccountInfo();
|
||||
const address = pubkey.toBase58();
|
||||
|
@ -79,9 +111,9 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
|
|||
if (data && data?.name === "spl-token") {
|
||||
if (data.parsed.type === "mint") {
|
||||
tabs.push({
|
||||
slug: "holders",
|
||||
title: "Holders",
|
||||
path: "/holders",
|
||||
slug: "largest",
|
||||
title: "Distribution",
|
||||
path: "/largest",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -139,7 +171,7 @@ type Tab = {
|
|||
path: string;
|
||||
};
|
||||
|
||||
type MoreTabs = "history" | "tokens" | "holders";
|
||||
type MoreTabs = "history" | "tokens" | "largest";
|
||||
function MoreSection({
|
||||
account,
|
||||
tab,
|
||||
|
@ -180,7 +212,7 @@ function MoreSection({
|
|||
</>
|
||||
)}
|
||||
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
||||
{tab === "holders" && <TokenLargestAccountsCard pubkey={pubkey} />}
|
||||
{tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,4 +28,5 @@ $input-group-addon-color: $white !default;
|
|||
$theme-colors: (
|
||||
"black": $black,
|
||||
"gray": $gray-600,
|
||||
"gray-dark": $gray-800-dark,
|
||||
);
|
||||
|
|
|
@ -173,3 +173,20 @@ h4.slot-pill {
|
|||
.search-bar__menu {
|
||||
border-radius: $border-radius !important;
|
||||
}
|
||||
|
||||
.token-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.address-truncate {
|
||||
@include media-breakpoint-down(md) {
|
||||
max-width: 180px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { Cluster } from "providers/cluster";
|
||||
|
||||
export type TokenDetails = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
logo: string;
|
||||
icon: string;
|
||||
website: string;
|
||||
};
|
||||
|
||||
const ENABLE_DETAILS = !!new URLSearchParams(window.location.search).get(
|
||||
"test"
|
||||
);
|
||||
|
||||
function get(address: string, cluster: Cluster): TokenDetails | undefined {
|
||||
if (ENABLE_DETAILS && cluster === Cluster.MainnetBeta)
|
||||
return MAINNET_TOKENS[address];
|
||||
}
|
||||
|
||||
function all(cluster: Cluster) {
|
||||
if (ENABLE_DETAILS && cluster === Cluster.MainnetBeta) return MAINNET_TOKENS;
|
||||
return {};
|
||||
}
|
||||
|
||||
export const TokenRegistry = {
|
||||
get,
|
||||
all,
|
||||
};
|
||||
|
||||
const MAINNET_TOKENS: { [key: string]: TokenDetails } = {
|
||||
MSRMmR98uWsTBgusjwyNkE8nDtV79sJznTedhJLzS4B: {
|
||||
name: "MegaSerum",
|
||||
symbol: "MSRM",
|
||||
logo: "/tokens/serum-64.png",
|
||||
icon: "/tokens/serum-32.png",
|
||||
website: "https://projectserum.com",
|
||||
},
|
||||
};
|
|
@ -12,17 +12,30 @@ import {
|
|||
TransactionInstruction,
|
||||
Transaction,
|
||||
} from "@solana/web3.js";
|
||||
import { TokenRegistry } from "tokenRegistry";
|
||||
import { Cluster } from "providers/cluster";
|
||||
|
||||
export const PROGRAM_IDS = {
|
||||
Budget1111111111111111111111111111111111111: "Budget",
|
||||
Config1111111111111111111111111111111111111: "Config",
|
||||
Exchange11111111111111111111111111111111111: "Exchange",
|
||||
[StakeProgram.programId.toBase58()]: "Stake",
|
||||
Storage111111111111111111111111111111111111: "Storage",
|
||||
[SystemProgram.programId.toBase58()]: "System",
|
||||
Vest111111111111111111111111111111111111111: "Vest",
|
||||
[VOTE_PROGRAM_ID.toBase58()]: "Vote",
|
||||
TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o: "Token",
|
||||
export type ProgramName =
|
||||
| "Budget Program"
|
||||
| "Config Program"
|
||||
| "Exchange Program"
|
||||
| "Stake Program"
|
||||
| "Storage Program"
|
||||
| "System Program"
|
||||
| "Vest Program"
|
||||
| "Vote Program"
|
||||
| "SPL Token";
|
||||
|
||||
export const PROGRAM_IDS: { [key: string]: ProgramName } = {
|
||||
Budget1111111111111111111111111111111111111: "Budget Program",
|
||||
Config1111111111111111111111111111111111111: "Config Program",
|
||||
Exchange11111111111111111111111111111111111: "Exchange Program",
|
||||
[StakeProgram.programId.toBase58()]: "Stake Program",
|
||||
Storage111111111111111111111111111111111111: "Storage Program",
|
||||
[SystemProgram.programId.toBase58()]: "System Program",
|
||||
Vest111111111111111111111111111111111111111: "Vest Program",
|
||||
[VOTE_PROGRAM_ID.toBase58()]: "Vote Program",
|
||||
TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o: "SPL Token",
|
||||
};
|
||||
|
||||
const LOADER_IDS = {
|
||||
|
@ -48,12 +61,13 @@ export const SYSVAR_IDS = {
|
|||
[SYSVAR_STAKE_HISTORY_PUBKEY.toBase58()]: "SYSVAR_STAKE_HISTORY",
|
||||
};
|
||||
|
||||
export function displayAddress(address: string): string {
|
||||
export function displayAddress(address: string, cluster: Cluster): string {
|
||||
return (
|
||||
PROGRAM_IDS[address] ||
|
||||
LOADER_IDS[address] ||
|
||||
SYSVAR_IDS[address] ||
|
||||
SYSVAR_ID[address] ||
|
||||
TokenRegistry.get(address, cluster)?.name ||
|
||||
address
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,14 +22,11 @@ export const TokenAccountInfo = pick({
|
|||
isNative: boolean(),
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
amount: optional(number()), // TODO remove when ui amount is deployed
|
||||
tokenAmount: optional(
|
||||
object({
|
||||
tokenAmount: pick({
|
||||
decimals: number(),
|
||||
uiAmount: number(),
|
||||
amount: string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
delegate: nullable(optional(Pubkey)),
|
||||
delegatedAmount: optional(number()),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue