Add token2 support to explorer (#11905)

This commit is contained in:
Justin Starry 2020-08-29 20:50:45 +08:00 committed by GitHub
parent befd99edac
commit 0b47cd1c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 257 additions and 185 deletions

View File

@ -11,10 +11,9 @@ import { coerce } from "superstruct";
import { TableCardBody } from "components/common/TableCardBody";
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";
import { normalizeTokenAmount } from "utils";
export function TokenAccountSection({
account,
@ -58,35 +57,7 @@ function MintAccountCard({
const { cluster } = useCluster();
const mintAddress = account.pubkey.toBase58();
const fetchInfo = useFetchAccountInfo();
const supply = useTokenSupply(mintAddress);
const fetchSupply = useFetchTokenSupply();
const refreshSupply = () => fetchSupply(account.pubkey);
const refresh = () => {
fetchInfo(account.pubkey);
refreshSupply();
};
let renderSupply;
const supplyTotal = supply?.data?.uiAmount;
if (supplyTotal === undefined) {
if (!supply || supply?.status === FetchStatus.Fetching) {
renderSupply = (
<>
<span className="spinner-grow spinner-grow-sm mr-2"></span>
Loading
</>
);
} else {
renderSupply = "Fetch failed";
}
} else {
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 refresh = () => fetchInfo(account.pubkey);
const tokenInfo = TokenRegistry.get(mintAddress, cluster);
return (
@ -109,8 +80,14 @@ function MintAccountCard({
</td>
</tr>
<tr>
<td>Total Supply</td>
<td className="text-lg-right">{renderSupply}</td>
<td>
{info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
</td>
<td className="text-lg-right">
{normalizeTokenAmount(info.supply, info.decimals).toFixed(
info.decimals
)}
</td>
</tr>
{tokenInfo && (
<tr>
@ -127,6 +104,22 @@ function MintAccountCard({
</td>
</tr>
)}
{info.mintAuthority && (
<tr>
<td>Mint Authority</td>
<td className="text-lg-right">
<Address pubkey={info.mintAuthority} alignRight link />
</td>
</tr>
)}
{info.freezeAuthority && (
<tr>
<td>Freeze Authority</td>
<td className="text-lg-right">
<Address pubkey={info.freezeAuthority} alignRight link />
</td>
</tr>
)}
<tr>
<td>Decimals</td>
<td className="text-lg-right">{info.decimals}</td>
@ -137,14 +130,6 @@ function MintAccountCard({
<td className="text-lg-right">Uninitialized</td>
</tr>
)}
{info.owner && (
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={info.owner} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
@ -160,9 +145,23 @@ function TokenAccountCard({
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const balance = info.tokenAmount?.uiAmount;
const unit =
TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens";
let unit, balance;
if (info.isNative) {
unit = "SOL";
balance = (
<>
<span className="text-monospace">
{new Intl.NumberFormat("en-US", { maximumFractionDigits: 9 }).format(
info.tokenAmount.uiAmount
)}
</span>
</>
);
} else {
balance = <>{info.tokenAmount.uiAmount}</>;
unit = TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens";
}
return (
<div className="card">
@ -199,15 +198,30 @@ function TokenAccountCard({
</td>
</tr>
<tr>
<td>Balance ({unit})</td>
<td>Token balance ({unit})</td>
<td className="text-lg-right">{balance}</td>
</tr>
{!info.isInitialized && (
{info.state === "uninitialized" && (
<tr>
<td>Status</td>
<td className="text-lg-right">Uninitialized</td>
</tr>
)}
{info.rentExemptReserve && (
<tr>
<td>Rent-exempt reserve (SOL)</td>
<td className="text-lg-right">
<>
<span className="text-monospace">
{new Intl.NumberFormat("en-US", {
maximumFractionDigits: 9,
}).format(info.rentExemptReserve.uiAmount)}
</span>
</>
</td>
</tr>
)}
</TableCardBody>
</div>
);

View File

@ -3,7 +3,6 @@ import { PublicKey, TokenAccountBalancePair } from "@solana/web3.js";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { Address } from "components/common/Address";
import { useTokenSupply } from "providers/mints/supply";
import {
useTokenLargestTokens,
useFetchTokenLargestAccounts,
@ -11,10 +10,12 @@ import {
import { FetchStatus } from "providers/cache";
import { TokenRegistry } from "tokenRegistry";
import { useCluster } from "providers/cluster";
import { useMintAccountInfo } from "providers/accounts";
import { normalizeTokenAmount } from "utils";
export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
const mintAddress = pubkey.toBase58();
const supply = useTokenSupply(mintAddress);
const mintInfo = useMintAccountInfo(mintAddress);
const largestAccounts = useTokenLargestTokens(mintAddress);
const fetchLargestAccounts = useFetchTokenLargestAccounts();
const refreshLargest = () => fetchLargestAccounts(pubkey);
@ -26,10 +27,11 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
if (!largestAccounts) refreshLargest();
}, [mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
const supplyTotal = supply?.data?.uiAmount;
if (supplyTotal === undefined || !largestAccounts) {
return null;
}
// Largest accounts hasn't started fetching
if (largestAccounts === undefined) return null;
// This is not a mint account
if (mintInfo === undefined) return null;
if (largestAccounts?.data === undefined) {
if (largestAccounts.status === FetchStatus.Fetching) {
@ -49,6 +51,7 @@ export function TokenLargestAccountsCard({ pubkey }: { pubkey: PublicKey }) {
return <ErrorCard text="No holders found" />;
}
const supplyTotal = normalizeTokenAmount(mintInfo.supply, mintInfo.decimals);
return (
<>
<div className="card">

View File

@ -13,6 +13,12 @@ import { InstructionCard } from "../InstructionCard";
import { Address } from "components/common/Address";
import { IX_STRUCTS, TokenInstructionType, IX_TITLES } from "./types";
import { ParsedInfo } from "validators";
import {
useTokenAccountInfo,
useMintAccountInfo,
useFetchAccountInfo,
} from "providers/accounts";
import { normalizeTokenAmount } from "utils";
type DetailsProps = {
tx: ParsedTransaction;
@ -48,6 +54,50 @@ type InfoProps = {
};
function TokenInstruction(props: InfoProps) {
const { mintAddress: infoMintAddress, tokenAddress } = React.useMemo(() => {
let mintAddress: string | undefined;
let tokenAddress: string | undefined;
// No sense fetching accounts if we don't need to convert an amount
if (!("amount" in props.info)) return {};
if ("mint" in props.info && props.info.mint instanceof PublicKey) {
mintAddress = props.info.mint.toBase58();
} else if (
"account" in props.info &&
props.info.account instanceof PublicKey
) {
tokenAddress = props.info.account.toBase58();
} else if (
"source" in props.info &&
props.info.source instanceof PublicKey
) {
tokenAddress = props.info.source.toBase58();
}
return {
mintAddress,
tokenAddress,
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const tokenInfo = useTokenAccountInfo(tokenAddress);
const mintAddress = infoMintAddress || tokenInfo?.mint.toBase58();
const mintInfo = useMintAccountInfo(mintAddress);
const fetchAccountInfo = useFetchAccountInfo();
React.useEffect(() => {
if (tokenAddress && !tokenInfo) {
fetchAccountInfo(new PublicKey(tokenAddress));
}
}, [fetchAccountInfo, tokenAddress]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (mintAddress && !mintInfo) {
fetchAccountInfo(new PublicKey(mintAddress));
}
}, [fetchAccountInfo, mintAddress]); // eslint-disable-line react-hooks/exhaustive-deps
const decimals = mintInfo?.decimals;
const attributes = [];
for (let key in props.info) {
const value = props.info[key];
@ -56,6 +106,12 @@ function TokenInstruction(props: InfoProps) {
let tag;
if (value instanceof PublicKey) {
tag = <Address pubkey={value} alignRight link />;
} else if (key === "amount") {
if (decimals === undefined) {
tag = <>(raw) {value}</>;
} else {
tag = <>{normalizeTokenAmount(value, decimals).toFixed(decimals)}</>;
}
} else {
tag = <>{value}</>;
}

View File

@ -5,25 +5,29 @@ import {
number,
optional,
array,
pick,
nullable,
} from "superstruct";
import { Pubkey } from "validators/pubkey";
const InitializeMint = object({
const InitializeMint = pick({
mint: Pubkey,
amount: number(),
decimals: number(),
owner: optional(Pubkey),
account: optional(Pubkey),
mintAuthority: Pubkey,
rentSysvar: Pubkey,
freezeAuthority: optional(Pubkey),
});
const InitializeAccount = object({
const InitializeAccount = pick({
account: Pubkey,
mint: Pubkey,
owner: Pubkey,
rentSysvar: Pubkey,
});
const InitializeMultisig = object({
const InitializeMultisig = pick({
multisig: Pubkey,
rentSysvar: Pubkey,
signers: array(Pubkey),
m: number(),
});
@ -53,11 +57,20 @@ const Revoke = object({
signers: optional(array(Pubkey)),
});
const SetOwner = object({
owned: Pubkey,
newOwner: Pubkey,
owner: optional(Pubkey),
multisigOwner: optional(Pubkey),
const AuthorityType = enums([
"mintTokens",
"freezeAccount",
"accountOwner",
"closeAccount",
]);
const SetAuthority = object({
mint: optional(Pubkey),
account: optional(Pubkey),
authorityType: AuthorityType,
newAuthority: nullable(Pubkey),
authority: optional(Pubkey),
multisigAuthority: optional(Pubkey),
signers: optional(array(Pubkey)),
});
@ -65,13 +78,14 @@ const MintTo = object({
mint: Pubkey,
account: Pubkey,
amount: number(),
owner: optional(Pubkey),
multisigOwner: optional(Pubkey),
mintAuthority: optional(Pubkey),
multisigMintAuthority: optional(Pubkey),
signers: optional(array(Pubkey)),
});
const Burn = object({
account: Pubkey,
mint: Pubkey,
amount: number(),
authority: optional(Pubkey),
multisigAuthority: optional(Pubkey),
@ -94,7 +108,7 @@ export const TokenInstructionType = enums([
"transfer",
"approve",
"revoke",
"setOwner",
"setAuthority",
"mintTo",
"burn",
"closeAccount",
@ -107,7 +121,7 @@ export const IX_STRUCTS = {
transfer: Transfer,
approve: Approve,
revoke: Revoke,
setOwner: SetOwner,
setAuthority: SetAuthority,
mintTo: MintTo,
burn: Burn,
closeAccount: CloseAccount,
@ -120,7 +134,7 @@ export const IX_TITLES = {
transfer: "Transfer",
approve: "Approve",
revoke: "Revoke",
setOwner: "Set Owner",
setAuthority: "Set Authority",
mintTo: "Mint To",
burn: "Burn",
closeAccount: "Close Account",

View File

@ -108,7 +108,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
},
];
if (data && data?.name === "spl-token") {
if (data && data?.program === "spl-token") {
if (data.parsed.type === "mint") {
tabs.push({
slug: "largest",
@ -141,7 +141,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
function InfoSection({ account }: { account: Account }) {
const data = account?.details?.data;
if (data && data.name === "stake") {
if (data && data.program === "stake") {
let stakeAccountType, stakeAccount;
if ("accountType" in data.parsed) {
stakeAccount = data.parsed;
@ -158,7 +158,7 @@ function InfoSection({ account }: { account: Account }) {
stakeAccountType={stakeAccountType}
/>
);
} else if (data && data.name === "spl-token") {
} else if (data && data.program === "spl-token") {
return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
} else {
return <UnknownAccountCard account={account} />;

View File

@ -8,18 +8,22 @@ import { TokensProvider, TOKEN_PROGRAM_ID } from "./tokens";
import { coerce } from "superstruct";
import { ParsedInfo } from "validators";
import { StakeAccount } from "validators/accounts/stake";
import { TokenAccount } from "validators/accounts/token";
import {
TokenAccount,
MintAccountInfo,
TokenAccountInfo,
} from "validators/accounts/token";
import * as Cache from "providers/cache";
import { ActionType, FetchStatus } from "providers/cache";
export { useAccountHistory } from "./history";
export type StakeProgramData = {
name: "stake";
program: "stake";
parsed: StakeAccount | StakeAccountWasm;
};
export type TokenProgramData = {
name: "spl-token";
program: "spl-token";
parsed: TokenAccount;
};
@ -108,7 +112,7 @@ async function fetchAccountInfo(
parsed = wasm.StakeAccount.fromAccountData(result.data);
}
data = {
name: "stake",
program: "stake",
parsed,
};
} catch (err) {
@ -123,7 +127,7 @@ async function fetchAccountInfo(
const info = coerce(result.data.parsed, ParsedInfo);
const parsed = coerce(info, TokenAccount);
data = {
name: "spl-token",
program: "spl-token",
parsed,
};
} catch (err) {
@ -166,17 +170,59 @@ export function useAccounts() {
}
export function useAccountInfo(
address: string
address: string | undefined
): Cache.CacheEntry<Account> | undefined {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useAccountInfo must be used within a AccountsProvider`);
}
if (address === undefined) return;
return context.entries[address];
}
export function useMintAccountInfo(
address: string | undefined
): MintAccountInfo | undefined {
const accountInfo = useAccountInfo(address);
if (address === undefined) return;
try {
const data = accountInfo?.data?.details?.data;
if (!data) return;
if (data.program !== "spl-token" || data.parsed.type !== "mint") {
throw new Error("Expected mint");
}
return coerce(data.parsed.info, MintAccountInfo);
} catch (err) {
Sentry.captureException(err, {
tags: { address },
});
}
}
export function useTokenAccountInfo(
address: string | undefined
): TokenAccountInfo | undefined {
const accountInfo = useAccountInfo(address);
if (address === undefined) return;
try {
const data = accountInfo?.data?.details?.data;
if (!data) return;
if (data.program !== "spl-token" || data.parsed.type !== "account") {
throw new Error("Expected token account");
}
return coerce(data.parsed.info, TokenAccountInfo);
} catch (err) {
Sentry.captureException(err, {
tags: { address },
});
}
}
export function useFetchAccountInfo() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
@ -186,7 +232,10 @@ export function useFetchAccountInfo() {
}
const { url } = useCluster();
return (pubkey: PublicKey) => {
fetchAccountInfo(dispatch, pubkey, url);
};
return React.useCallback(
(pubkey: PublicKey) => {
fetchAccountInfo(dispatch, pubkey, url);
},
[dispatch, url]
);
}

View File

@ -41,7 +41,7 @@ export function TokensProvider({ children }: ProviderProps) {
}
export const TOKEN_PROGRAM_ID = new PublicKey(
"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
async function fetchAccountTokens(

View File

@ -1,12 +1,7 @@
import React from "react";
import { SupplyProvider } from "./supply";
import { LargestAccountsProvider } from "./largest";
type ProviderProps = { children: React.ReactNode };
export function MintsProvider({ children }: ProviderProps) {
return (
<SupplyProvider>
<LargestAccountsProvider>{children}</LargestAccountsProvider>
</SupplyProvider>
);
return <LargestAccountsProvider>{children}</LargestAccountsProvider>;
}

View File

@ -1,82 +0,0 @@
import React from "react";
import * as Sentry from "@sentry/react";
import { useCluster } from "providers/cluster";
import * as Cache from "providers/cache";
import { ActionType, FetchStatus } from "providers/cache";
import { TokenAmount, PublicKey, Connection } from "@solana/web3.js";
type State = Cache.State<TokenAmount>;
type Dispatch = Cache.Dispatch<TokenAmount>;
const StateContext = React.createContext<State | undefined>(undefined);
const DispatchContext = React.createContext<Dispatch | undefined>(undefined);
type ProviderProps = { children: React.ReactNode };
export function SupplyProvider({ children }: ProviderProps) {
const { url } = useCluster();
const [state, dispatch] = Cache.useReducer<TokenAmount>(url);
// Clear cache whenever cluster is changed
React.useEffect(() => {
dispatch({ type: ActionType.Clear, url });
}, [dispatch, url]);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
async function fetchSupply(dispatch: Dispatch, pubkey: PublicKey, url: string) {
dispatch({
type: ActionType.Update,
key: pubkey.toBase58(),
status: Cache.FetchStatus.Fetching,
url,
});
let data;
let fetchStatus;
try {
data = (await new Connection(url, "single").getTokenSupply(pubkey)).value;
fetchStatus = FetchStatus.Fetched;
} catch (error) {
Sentry.captureException(error, { tags: { url } });
fetchStatus = FetchStatus.FetchFailed;
}
dispatch({
type: ActionType.Update,
status: fetchStatus,
data,
key: pubkey.toBase58(),
url,
});
}
export function useFetchTokenSupply() {
const dispatch = React.useContext(DispatchContext);
if (!dispatch) {
throw new Error(`useFetchTokenSupply must be used within a MintsProvider`);
}
const { url } = useCluster();
return (pubkey: PublicKey) => {
fetchSupply(dispatch, pubkey, url);
};
}
export function useTokenSupply(
address: string
): Cache.CacheEntry<TokenAmount> | undefined {
const context = React.useContext(StateContext);
if (!context) {
throw new Error(`useTokenSupply must be used within a MintsProvider`);
}
return context.entries[address];
}

View File

@ -18,6 +18,16 @@ export function assertUnreachable(x: never): never {
throw new Error("Unreachable!");
}
export function normalizeTokenAmount(
raw: string | number,
decimals: number
): number {
let rawTokens: number;
if (typeof raw === "string") rawTokens = parseInt(raw);
else rawTokens = raw;
return rawTokens / Math.pow(10, decimals);
}
export function lamportsToSol(lamports: number | BN): number {
if (typeof lamports === "number") {
return Math.abs(lamports) / LAMPORTS_PER_SOL;

View File

@ -35,7 +35,7 @@ export const PROGRAM_IDS: { [key: string]: ProgramName } = {
[SystemProgram.programId.toBase58()]: "System Program",
Vest111111111111111111111111111111111111111: "Vest Program",
[VOTE_PROGRAM_ID.toBase58()]: "Vote Program",
TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o: "SPL Token",
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
};
const LOADER_IDS = {
@ -49,6 +49,10 @@ const SYSVAR_ID: { [key: string]: string } = {
Sysvar1111111111111111111111111111111111111: "SYSVAR",
};
const WRAPPED_SOL: { [key: string]: string } = {
So11111111111111111111111111111111111111112: "Wrapped SOL",
};
export const SYSVAR_IDS = {
[SYSVAR_CLOCK_PUBKEY.toBase58()]: "SYSVAR_CLOCK",
SysvarEpochSchedu1e111111111111111111111111: "SYSVAR_EPOCH_SCHEDULE",
@ -67,6 +71,7 @@ export function displayAddress(address: string, cluster: Cluster): string {
LOADER_IDS[address] ||
SYSVAR_IDS[address] ||
SYSVAR_ID[address] ||
WRAPPED_SOL[address] ||
TokenRegistry.get(address, cluster)?.name ||
address
);

View File

@ -1,5 +1,4 @@
import {
object,
StructType,
number,
optional,
@ -16,30 +15,39 @@ import { Pubkey } from "validators/pubkey";
export type TokenAccountType = StructType<typeof TokenAccountType>;
export const TokenAccountType = enums(["mint", "account", "multisig"]);
export type TokenAccountState = StructType<typeof AccountState>;
const AccountState = enums(["initialized", "uninitialized", "frozen"]);
const TokenAmount = pick({
decimals: number(),
uiAmount: number(),
amount: string(),
});
export type TokenAccountInfo = StructType<typeof TokenAccountInfo>;
export const TokenAccountInfo = pick({
isInitialized: boolean(),
isNative: boolean(),
mint: Pubkey,
owner: Pubkey,
tokenAmount: pick({
decimals: number(),
uiAmount: number(),
amount: string(),
}),
delegate: nullable(optional(Pubkey)),
delegatedAmount: optional(number()),
tokenAmount: TokenAmount,
delegate: optional(Pubkey),
state: AccountState,
isNative: boolean(),
rentExemptReserve: optional(TokenAmount),
delegatedAmount: optional(TokenAmount),
closeAuthority: optional(Pubkey),
});
export type MintAccountInfo = StructType<typeof MintAccountInfo>;
export const MintAccountInfo = object({
export const MintAccountInfo = pick({
mintAuthority: nullable(Pubkey),
supply: string(),
decimals: number(),
isInitialized: boolean(),
owner: nullable(optional(Pubkey)),
freezeAuthority: nullable(Pubkey),
});
export type MultisigAccountInfo = StructType<typeof MultisigAccountInfo>;
export const MultisigAccountInfo = object({
export const MultisigAccountInfo = pick({
numRequiredSigners: number(),
numValidSigners: number(),
isInitialized: boolean(),
@ -47,7 +55,7 @@ export const MultisigAccountInfo = object({
});
export type TokenAccount = StructType<typeof TokenAccount>;
export const TokenAccount = object({
export const TokenAccount = pick({
type: TokenAccountType,
info: any(),
});