diff --git a/explorer/src/components/account/StakeAccountSection.tsx b/explorer/src/components/account/StakeAccountSection.tsx
index daf89f6b30..cca16dd0cb 100644
--- a/explorer/src/components/account/StakeAccountSection.tsx
+++ b/explorer/src/components/account/StakeAccountSection.tsx
@@ -9,7 +9,7 @@ import {
StakeAccountInfo,
StakeMeta,
StakeAccountType,
-} from "providers/accounts/types";
+} from "validators/accounts/stake";
import BN from "bn.js";
const MAX_EPOCH = new BN(2).pow(new BN(64));
diff --git a/explorer/src/components/account/TokenAccountSection.tsx b/explorer/src/components/account/TokenAccountSection.tsx
new file mode 100644
index 0000000000..d0550d2880
--- /dev/null
+++ b/explorer/src/components/account/TokenAccountSection.tsx
@@ -0,0 +1,215 @@
+import React from "react";
+import { Account, useFetchAccountInfo } from "providers/accounts";
+import {
+ TokenAccount,
+ MintAccountInfo,
+ TokenAccountInfo,
+ MultisigAccountInfo,
+} from "validators/accounts/token";
+import { coerce } from "superstruct";
+import { TableCardBody } from "components/common/TableCardBody";
+import { Address } from "components/common/Address";
+import { UnknownAccountCard } from "./UnknownAccountCard";
+
+export function TokenAccountSection({
+ account,
+ tokenAccount,
+}: {
+ account: Account;
+ tokenAccount: TokenAccount;
+}) {
+ try {
+ switch (tokenAccount.type) {
+ case "mint": {
+ const info = coerce(tokenAccount.info, MintAccountInfo);
+ return ;
+ }
+ case "account": {
+ const info = coerce(tokenAccount.info, TokenAccountInfo);
+ return ;
+ }
+ case "multisig": {
+ const info = coerce(tokenAccount.info, MultisigAccountInfo);
+ return ;
+ }
+ }
+ } catch (err) {}
+ return ;
+}
+
+function MintAccountCard({
+ account,
+ info,
+}: {
+ account: Account;
+ info: MintAccountInfo;
+}) {
+ const refresh = useFetchAccountInfo();
+
+ return (
+
+
+
+ Token Mint Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+
+ Decimals |
+ {info.decimals} |
+
+
+ Status |
+
+ {info.isInitialized ? "Initialized" : "Uninitialized"}
+ |
+
+ {info.owner !== undefined && (
+
+ Owner |
+
+
+ |
+
+ )}
+
+
+ );
+}
+
+function TokenAccountCard({
+ account,
+ info,
+}: {
+ account: Account;
+ info: TokenAccountInfo;
+}) {
+ const refresh = useFetchAccountInfo();
+
+ let balance;
+ if ("amount" in info) {
+ balance = info.amount;
+ } else {
+ balance = info.tokenAmount?.uiAmount;
+ }
+
+ return (
+
+
+
+ Token Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+
+ Mint |
+
+
+ |
+
+
+ Owner |
+
+
+ |
+
+
+ Balance (tokens) |
+ {balance} |
+
+
+ Status |
+
+ {info.isInitialized ? "Initialized" : "Uninitialized"}
+ |
+
+
+
+ );
+}
+
+function MultisigAccountCard({
+ account,
+ info,
+}: {
+ account: Account;
+ info: MultisigAccountInfo;
+}) {
+ const refresh = useFetchAccountInfo();
+
+ return (
+
+
+
+ Multisig Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+
+ Required Signers |
+ {info.numRequiredSigners} |
+
+
+ Valid Signers |
+ {info.numValidSigners} |
+
+ {info.signers.map((signer) => (
+
+ Signer |
+
+
+ |
+
+ ))}
+
+ Status |
+
+ {info.isInitialized ? "Initialized" : "Uninitialized"}
+ |
+
+
+
+ );
+}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index 2627ccaf59..72cb56232f 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -1,11 +1,12 @@
import React from "react";
-import { PublicKey, StakeProgram } from "@solana/web3.js";
+import { PublicKey } from "@solana/web3.js";
import {
FetchStatus,
useFetchAccountInfo,
useAccountInfo,
} from "providers/accounts";
import { StakeAccountSection } from "components/account/StakeAccountSection";
+import { TokenAccountSection } from "components/account/TokenAccountSection";
import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard";
import { useCluster, ClusterStatus } from "providers/cluster";
@@ -65,16 +66,15 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
return refresh(pubkey)} text="Fetch Failed" />;
}
- const owner = info.details?.owner;
const data = info.details?.data;
- if (data && owner && owner.equals(StakeProgram.programId)) {
+ if (data && data.name === "stake") {
let stakeAccountType, stakeAccount;
- if ("accountType" in data) {
- stakeAccount = data;
- stakeAccountType = data.accountType as any;
+ if ("accountType" in data.parsed) {
+ stakeAccount = data.parsed;
+ stakeAccountType = data.parsed.accountType as any;
} else {
- stakeAccount = data.info;
- stakeAccountType = data.type;
+ stakeAccount = data.parsed.info;
+ stakeAccountType = data.parsed.type;
}
return (
@@ -84,6 +84,8 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
stakeAccountType={stakeAccountType}
/>
);
+ } else if (data && data.name === "spl-token") {
+ return ;
} else {
return ;
}
diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx
index 1b58fa897b..8ef21c862f 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -3,10 +3,11 @@ import { StakeAccount as StakeAccountWasm } from "solana-sdk-wasm";
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
import { useCluster } from "../cluster";
import { HistoryProvider } from "./history";
-import { TokensProvider } from "./tokens";
+import { TokensProvider, TOKEN_PROGRAM_ID } from "./tokens";
import { coerce } from "superstruct";
import { ParsedInfo } from "validators";
-import { StakeAccount } from "./types";
+import { StakeAccount } from "validators/accounts/stake";
+import { TokenAccount } from "validators/accounts/token";
export { useAccountHistory } from "./history";
export enum FetchStatus {
@@ -15,11 +16,23 @@ export enum FetchStatus {
Fetched,
}
+export type StakeProgramData = {
+ name: "stake";
+ parsed: StakeAccount | StakeAccountWasm;
+};
+
+export type TokenProgramData = {
+ name: "spl-token";
+ parsed: TokenAccount;
+};
+
+export type ProgramData = StakeProgramData | TokenProgramData;
+
export interface Details {
executable: boolean;
owner: PublicKey;
space?: number;
- data?: StakeAccount | StakeAccountWasm;
+ data?: ProgramData;
}
export interface Account {
@@ -173,20 +186,38 @@ async function fetchAccountInfo(
space = result.data.length;
}
- let data;
+ let data: ProgramData | undefined;
if (result.owner.equals(StakeProgram.programId)) {
try {
+ let parsed;
if ("parsed" in result.data) {
const info = coerce(result.data.parsed, ParsedInfo);
- data = coerce(info, StakeAccount);
+ parsed = coerce(info, StakeAccount);
} else {
const wasm = await import("solana-sdk-wasm");
- data = wasm.StakeAccount.fromAccountData(result.data);
+ parsed = wasm.StakeAccount.fromAccountData(result.data);
}
+ data = {
+ name: "stake",
+ parsed,
+ };
} catch (err) {
- console.error("Unexpected error loading wasm", err);
+ console.error("Failed to parse stake account", err);
// TODO store error state in Account info
}
+ } else if ("parsed" in result.data) {
+ if (result.owner.equals(TOKEN_PROGRAM_ID)) {
+ try {
+ const info = coerce(result.data.parsed, ParsedInfo);
+ const parsed = coerce(info, TokenAccount);
+ data = {
+ name: "spl-token",
+ parsed,
+ };
+ } catch (err) {
+ // TODO store error state in Account info
+ }
+ }
}
details = {
diff --git a/explorer/src/providers/accounts/tokens.tsx b/explorer/src/providers/accounts/tokens.tsx
index 9eea94b73e..7b4d28120c 100644
--- a/explorer/src/providers/accounts/tokens.tsx
+++ b/explorer/src/providers/accounts/tokens.tsx
@@ -102,7 +102,7 @@ export function TokensProvider({ children }: ProviderProps) {
);
}
-const TOKEN_PROGRAM_ID = new PublicKey(
+export const TOKEN_PROGRAM_ID = new PublicKey(
"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
);
diff --git a/explorer/src/providers/accounts/types.ts b/explorer/src/validators/accounts/stake.ts
similarity index 100%
rename from explorer/src/providers/accounts/types.ts
rename to explorer/src/validators/accounts/stake.ts
diff --git a/explorer/src/validators/accounts/token.ts b/explorer/src/validators/accounts/token.ts
new file mode 100644
index 0000000000..9a1fa5c7e4
--- /dev/null
+++ b/explorer/src/validators/accounts/token.ts
@@ -0,0 +1,55 @@
+import {
+ object,
+ StructType,
+ number,
+ optional,
+ enums,
+ any,
+ boolean,
+ string,
+ array,
+ nullable,
+} from "superstruct";
+import { Pubkey } from "validators/pubkey";
+
+export type TokenAccountType = StructType;
+export const TokenAccountType = enums(["mint", "account", "multisig"]);
+
+export type TokenAccountInfo = StructType;
+export const TokenAccountInfo = object({
+ mint: Pubkey,
+ owner: Pubkey,
+ amount: optional(number()), // TODO remove when ui amount is deployed
+ tokenAmount: optional(
+ object({
+ decimals: number(),
+ uiAmount: number(),
+ amount: string(),
+ })
+ ),
+ delegate: nullable(optional(Pubkey)),
+ isInitialized: boolean(),
+ isNative: boolean(),
+ delegatedAmount: number(),
+});
+
+export type MintAccountInfo = StructType;
+export const MintAccountInfo = object({
+ decimals: number(),
+ isInitialized: boolean(),
+ owner: optional(Pubkey),
+});
+
+export type MultisigAccountInfo = StructType;
+export const MultisigAccountInfo = object({
+ numRequiredSigners: number(),
+ numValidSigners: number(),
+ isInitialized: boolean(),
+ signers: array(Pubkey),
+});
+
+export type TokenAccount = StructType;
+export const TokenAccount = object({
+ type: TokenAccountType,
+ info: any(),
+});