diff --git a/explorer/src/components/account/TokenAccountSection.tsx b/explorer/src/components/account/TokenAccountSection.tsx
index a8444686f6..4b1917f0f8 100644
--- a/explorer/src/components/account/TokenAccountSection.tsx
+++ b/explorer/src/components/account/TokenAccountSection.tsx
@@ -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 = (
- <>
-
- 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({
@@ -199,15 +198,30 @@ function TokenAccountCard({
- Balance ({unit}) |
+ Token balance ({unit}) |
{balance} |
- {!info.isInitialized && (
+ {info.state === "uninitialized" && (
Status |
Uninitialized |
)}
+ {info.rentExemptReserve && (
+
+ Rent-exempt reserve (SOL) |
+
+ <>
+ ◎
+
+ {new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 9,
+ }).format(info.rentExemptReserve.uiAmount)}
+
+ >
+ |
+
+ )}
);
diff --git a/explorer/src/components/account/TokenLargestAccountsCard.tsx b/explorer/src/components/account/TokenLargestAccountsCard.tsx
index 509fae8da1..52f2dda68a 100644
--- a/explorer/src/components/account/TokenLargestAccountsCard.tsx
+++ b/explorer/src/components/account/TokenLargestAccountsCard.tsx
@@ -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
diff --git a/explorer/src/components/instruction/token/TokenDetailsCard.tsx b/explorer/src/components/instruction/token/TokenDetailsCard.tsx
index d1634d2938..69b1a9d432 100644
--- a/explorer/src/components/instruction/token/TokenDetailsCard.tsx
+++ b/explorer/src/components/instruction/token/TokenDetailsCard.tsx
@@ -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 =
;
+ } else if (key === "amount") {
+ if (decimals === undefined) {
+ tag = <>(raw) {value}>;
+ } else {
+ tag = <>{normalizeTokenAmount(value, decimals).toFixed(decimals)}>;
+ }
} else {
tag = <>{value}>;
}
diff --git a/explorer/src/components/instruction/token/types.ts b/explorer/src/components/instruction/token/types.ts
index 2c81ba184f..721988abc2 100644
--- a/explorer/src/components/instruction/token/types.ts
+++ b/explorer/src/components/instruction/token/types.ts
@@ -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",
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index f534fb2e3a..ff9810f9a7 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -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
;
} else {
return
;
diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx
index a0f9e3ecad..2a5414cd01 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -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
| 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]
+ );
}
diff --git a/explorer/src/providers/accounts/tokens.tsx b/explorer/src/providers/accounts/tokens.tsx
index eb27f43b4b..d04ca5f8ad 100644
--- a/explorer/src/providers/accounts/tokens.tsx
+++ b/explorer/src/providers/accounts/tokens.tsx
@@ -41,7 +41,7 @@ export function TokensProvider({ children }: ProviderProps) {
}
export const TOKEN_PROGRAM_ID = new PublicKey(
- "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
);
async function fetchAccountTokens(
diff --git a/explorer/src/providers/mints/index.tsx b/explorer/src/providers/mints/index.tsx
index 04d52557ee..dbc071b83b 100644
--- a/explorer/src/providers/mints/index.tsx
+++ b/explorer/src/providers/mints/index.tsx
@@ -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 (
-
- {children}
-
- );
+ return {children};
}
diff --git a/explorer/src/providers/mints/supply.tsx b/explorer/src/providers/mints/supply.tsx
deleted file mode 100644
index 1d3a45ee03..0000000000
--- a/explorer/src/providers/mints/supply.tsx
+++ /dev/null
@@ -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;
-type Dispatch = Cache.Dispatch;
-
-const StateContext = React.createContext(undefined);
-const DispatchContext = React.createContext(undefined);
-
-type ProviderProps = { children: React.ReactNode };
-export function SupplyProvider({ children }: ProviderProps) {
- const { url } = useCluster();
- const [state, dispatch] = Cache.useReducer(url);
-
- // Clear cache whenever cluster is changed
- React.useEffect(() => {
- dispatch({ type: ActionType.Clear, url });
- }, [dispatch, url]);
-
- return (
-
-
- {children}
-
-
- );
-}
-
-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 | undefined {
- const context = React.useContext(StateContext);
-
- if (!context) {
- throw new Error(`useTokenSupply must be used within a MintsProvider`);
- }
-
- return context.entries[address];
-}
diff --git a/explorer/src/utils/index.tsx b/explorer/src/utils/index.tsx
index e2997412ac..13fd4a324f 100644
--- a/explorer/src/utils/index.tsx
+++ b/explorer/src/utils/index.tsx
@@ -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;
diff --git a/explorer/src/utils/tx.ts b/explorer/src/utils/tx.ts
index a1d8494e51..d975f0ac41 100644
--- a/explorer/src/utils/tx.ts
+++ b/explorer/src/utils/tx.ts
@@ -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
);
diff --git a/explorer/src/validators/accounts/token.ts b/explorer/src/validators/accounts/token.ts
index 8b4c40e2f1..2f214351e4 100644
--- a/explorer/src/validators/accounts/token.ts
+++ b/explorer/src/validators/accounts/token.ts
@@ -1,5 +1,4 @@
import {
- object,
StructType,
number,
optional,
@@ -16,30 +15,39 @@ import { Pubkey } from "validators/pubkey";
export type TokenAccountType = StructType;
export const TokenAccountType = enums(["mint", "account", "multisig"]);
+export type TokenAccountState = StructType;
+const AccountState = enums(["initialized", "uninitialized", "frozen"]);
+
+const TokenAmount = pick({
+ decimals: number(),
+ uiAmount: number(),
+ amount: string(),
+});
+
export type TokenAccountInfo = StructType;
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;
-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;
-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;
-export const TokenAccount = object({
+export const TokenAccount = pick({
type: TokenAccountType,
info: any(),
});