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(), +});