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 { TableCardBody } from "components/common/TableCardBody";
import { Address } from "components/common/Address"; import { Address } from "components/common/Address";
import { UnknownAccountCard } from "./UnknownAccountCard"; import { UnknownAccountCard } from "./UnknownAccountCard";
import { useFetchTokenSupply, useTokenSupply } from "providers/mints/supply";
import { FetchStatus } from "providers/cache";
import { TokenRegistry } from "tokenRegistry"; import { TokenRegistry } from "tokenRegistry";
import { useCluster } from "providers/cluster"; import { useCluster } from "providers/cluster";
import { normalizeTokenAmount } from "utils";
export function TokenAccountSection({ export function TokenAccountSection({
account, account,
@ -58,35 +57,7 @@ function MintAccountCard({
const { cluster } = useCluster(); const { cluster } = useCluster();
const mintAddress = account.pubkey.toBase58(); const mintAddress = account.pubkey.toBase58();
const fetchInfo = useFetchAccountInfo(); const fetchInfo = useFetchAccountInfo();
const supply = useTokenSupply(mintAddress); const refresh = () => fetchInfo(account.pubkey);
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 tokenInfo = TokenRegistry.get(mintAddress, cluster); const tokenInfo = TokenRegistry.get(mintAddress, cluster);
return ( return (
@ -109,8 +80,14 @@ function MintAccountCard({
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Total Supply</td> <td>
<td className="text-lg-right">{renderSupply}</td> {info.mintAuthority === null ? "Fixed Supply" : "Current Supply"}
</td>
<td className="text-lg-right">
{normalizeTokenAmount(info.supply, info.decimals).toFixed(
info.decimals
)}
</td>
</tr> </tr>
{tokenInfo && ( {tokenInfo && (
<tr> <tr>
@ -127,6 +104,22 @@ function MintAccountCard({
</td> </td>
</tr> </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> <tr>
<td>Decimals</td> <td>Decimals</td>
<td className="text-lg-right">{info.decimals}</td> <td className="text-lg-right">{info.decimals}</td>
@ -137,14 +130,6 @@ function MintAccountCard({
<td className="text-lg-right">Uninitialized</td> <td className="text-lg-right">Uninitialized</td>
</tr> </tr>
)} )}
{info.owner && (
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={info.owner} alignRight link />
</td>
</tr>
)}
</TableCardBody> </TableCardBody>
</div> </div>
); );
@ -160,9 +145,23 @@ function TokenAccountCard({
const refresh = useFetchAccountInfo(); const refresh = useFetchAccountInfo();
const { cluster } = useCluster(); const { cluster } = useCluster();
const balance = info.tokenAmount?.uiAmount; let unit, balance;
const unit = if (info.isNative) {
TokenRegistry.get(info.mint.toBase58(), cluster)?.symbol || "tokens"; 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 ( return (
<div className="card"> <div className="card">
@ -199,15 +198,30 @@ function TokenAccountCard({
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Balance ({unit})</td> <td>Token balance ({unit})</td>
<td className="text-lg-right">{balance}</td> <td className="text-lg-right">{balance}</td>
</tr> </tr>
{!info.isInitialized && ( {info.state === "uninitialized" && (
<tr> <tr>
<td>Status</td> <td>Status</td>
<td className="text-lg-right">Uninitialized</td> <td className="text-lg-right">Uninitialized</td>
</tr> </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> </TableCardBody>
</div> </div>
); );

View File

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

View File

@ -13,6 +13,12 @@ import { InstructionCard } from "../InstructionCard";
import { Address } from "components/common/Address"; import { Address } from "components/common/Address";
import { IX_STRUCTS, TokenInstructionType, IX_TITLES } from "./types"; import { IX_STRUCTS, TokenInstructionType, IX_TITLES } from "./types";
import { ParsedInfo } from "validators"; import { ParsedInfo } from "validators";
import {
useTokenAccountInfo,
useMintAccountInfo,
useFetchAccountInfo,
} from "providers/accounts";
import { normalizeTokenAmount } from "utils";
type DetailsProps = { type DetailsProps = {
tx: ParsedTransaction; tx: ParsedTransaction;
@ -48,6 +54,50 @@ type InfoProps = {
}; };
function TokenInstruction(props: 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 = []; const attributes = [];
for (let key in props.info) { for (let key in props.info) {
const value = props.info[key]; const value = props.info[key];
@ -56,6 +106,12 @@ function TokenInstruction(props: InfoProps) {
let tag; let tag;
if (value instanceof PublicKey) { if (value instanceof PublicKey) {
tag = <Address pubkey={value} alignRight link />; tag = <Address pubkey={value} alignRight link />;
} else if (key === "amount") {
if (decimals === undefined) {
tag = <>(raw) {value}</>;
} else {
tag = <>{normalizeTokenAmount(value, decimals).toFixed(decimals)}</>;
}
} else { } else {
tag = <>{value}</>; tag = <>{value}</>;
} }

View File

@ -5,25 +5,29 @@ import {
number, number,
optional, optional,
array, array,
pick,
nullable,
} from "superstruct"; } from "superstruct";
import { Pubkey } from "validators/pubkey"; import { Pubkey } from "validators/pubkey";
const InitializeMint = object({ const InitializeMint = pick({
mint: Pubkey, mint: Pubkey,
amount: number(),
decimals: number(), decimals: number(),
owner: optional(Pubkey), mintAuthority: Pubkey,
account: optional(Pubkey), rentSysvar: Pubkey,
freezeAuthority: optional(Pubkey),
}); });
const InitializeAccount = object({ const InitializeAccount = pick({
account: Pubkey, account: Pubkey,
mint: Pubkey, mint: Pubkey,
owner: Pubkey, owner: Pubkey,
rentSysvar: Pubkey,
}); });
const InitializeMultisig = object({ const InitializeMultisig = pick({
multisig: Pubkey, multisig: Pubkey,
rentSysvar: Pubkey,
signers: array(Pubkey), signers: array(Pubkey),
m: number(), m: number(),
}); });
@ -53,11 +57,20 @@ const Revoke = object({
signers: optional(array(Pubkey)), signers: optional(array(Pubkey)),
}); });
const SetOwner = object({ const AuthorityType = enums([
owned: Pubkey, "mintTokens",
newOwner: Pubkey, "freezeAccount",
owner: optional(Pubkey), "accountOwner",
multisigOwner: optional(Pubkey), "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)), signers: optional(array(Pubkey)),
}); });
@ -65,13 +78,14 @@ const MintTo = object({
mint: Pubkey, mint: Pubkey,
account: Pubkey, account: Pubkey,
amount: number(), amount: number(),
owner: optional(Pubkey), mintAuthority: optional(Pubkey),
multisigOwner: optional(Pubkey), multisigMintAuthority: optional(Pubkey),
signers: optional(array(Pubkey)), signers: optional(array(Pubkey)),
}); });
const Burn = object({ const Burn = object({
account: Pubkey, account: Pubkey,
mint: Pubkey,
amount: number(), amount: number(),
authority: optional(Pubkey), authority: optional(Pubkey),
multisigAuthority: optional(Pubkey), multisigAuthority: optional(Pubkey),
@ -94,7 +108,7 @@ export const TokenInstructionType = enums([
"transfer", "transfer",
"approve", "approve",
"revoke", "revoke",
"setOwner", "setAuthority",
"mintTo", "mintTo",
"burn", "burn",
"closeAccount", "closeAccount",
@ -107,7 +121,7 @@ export const IX_STRUCTS = {
transfer: Transfer, transfer: Transfer,
approve: Approve, approve: Approve,
revoke: Revoke, revoke: Revoke,
setOwner: SetOwner, setAuthority: SetAuthority,
mintTo: MintTo, mintTo: MintTo,
burn: Burn, burn: Burn,
closeAccount: CloseAccount, closeAccount: CloseAccount,
@ -120,7 +134,7 @@ export const IX_TITLES = {
transfer: "Transfer", transfer: "Transfer",
approve: "Approve", approve: "Approve",
revoke: "Revoke", revoke: "Revoke",
setOwner: "Set Owner", setAuthority: "Set Authority",
mintTo: "Mint To", mintTo: "Mint To",
burn: "Burn", burn: "Burn",
closeAccount: "Close Account", 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") { if (data.parsed.type === "mint") {
tabs.push({ tabs.push({
slug: "largest", slug: "largest",
@ -141,7 +141,7 @@ function DetailsSections({ pubkey, tab }: { pubkey: PublicKey; tab?: string }) {
function InfoSection({ account }: { account: Account }) { function InfoSection({ account }: { account: Account }) {
const data = account?.details?.data; const data = account?.details?.data;
if (data && data.name === "stake") { if (data && data.program === "stake") {
let stakeAccountType, stakeAccount; let stakeAccountType, stakeAccount;
if ("accountType" in data.parsed) { if ("accountType" in data.parsed) {
stakeAccount = data.parsed; stakeAccount = data.parsed;
@ -158,7 +158,7 @@ function InfoSection({ account }: { account: Account }) {
stakeAccountType={stakeAccountType} stakeAccountType={stakeAccountType}
/> />
); );
} else if (data && data.name === "spl-token") { } else if (data && data.program === "spl-token") {
return <TokenAccountSection account={account} tokenAccount={data.parsed} />; return <TokenAccountSection account={account} tokenAccount={data.parsed} />;
} else { } else {
return <UnknownAccountCard account={account} />; return <UnknownAccountCard account={account} />;

View File

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

View File

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

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!"); 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 { export function lamportsToSol(lamports: number | BN): number {
if (typeof lamports === "number") { if (typeof lamports === "number") {
return Math.abs(lamports) / LAMPORTS_PER_SOL; 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", [SystemProgram.programId.toBase58()]: "System Program",
Vest111111111111111111111111111111111111111: "Vest Program", Vest111111111111111111111111111111111111111: "Vest Program",
[VOTE_PROGRAM_ID.toBase58()]: "Vote Program", [VOTE_PROGRAM_ID.toBase58()]: "Vote Program",
TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o: "SPL Token", TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: "SPL Token",
}; };
const LOADER_IDS = { const LOADER_IDS = {
@ -49,6 +49,10 @@ const SYSVAR_ID: { [key: string]: string } = {
Sysvar1111111111111111111111111111111111111: "SYSVAR", Sysvar1111111111111111111111111111111111111: "SYSVAR",
}; };
const WRAPPED_SOL: { [key: string]: string } = {
So11111111111111111111111111111111111111112: "Wrapped SOL",
};
export const SYSVAR_IDS = { export const SYSVAR_IDS = {
[SYSVAR_CLOCK_PUBKEY.toBase58()]: "SYSVAR_CLOCK", [SYSVAR_CLOCK_PUBKEY.toBase58()]: "SYSVAR_CLOCK",
SysvarEpochSchedu1e111111111111111111111111: "SYSVAR_EPOCH_SCHEDULE", SysvarEpochSchedu1e111111111111111111111111: "SYSVAR_EPOCH_SCHEDULE",
@ -67,6 +71,7 @@ export function displayAddress(address: string, cluster: Cluster): string {
LOADER_IDS[address] || LOADER_IDS[address] ||
SYSVAR_IDS[address] || SYSVAR_IDS[address] ||
SYSVAR_ID[address] || SYSVAR_ID[address] ||
WRAPPED_SOL[address] ||
TokenRegistry.get(address, cluster)?.name || TokenRegistry.get(address, cluster)?.name ||
address address
); );

View File

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