Display detailed token account info on explorer (#11472)

This commit is contained in:
Justin Starry 2020-08-08 23:02:01 +08:00 committed by GitHub
parent 4e815eaeb6
commit fb822688b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 320 additions and 17 deletions

View File

@ -9,7 +9,7 @@ import {
StakeAccountInfo, StakeAccountInfo,
StakeMeta, StakeMeta,
StakeAccountType, StakeAccountType,
} from "providers/accounts/types"; } from "validators/accounts/stake";
import BN from "bn.js"; import BN from "bn.js";
const MAX_EPOCH = new BN(2).pow(new BN(64)); const MAX_EPOCH = new BN(2).pow(new BN(64));

View File

@ -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 <MintAccountCard account={account} info={info} />;
}
case "account": {
const info = coerce(tokenAccount.info, TokenAccountInfo);
return <TokenAccountCard account={account} info={info} />;
}
case "multisig": {
const info = coerce(tokenAccount.info, MultisigAccountInfo);
return <MultisigAccountCard account={account} info={info} />;
}
}
} catch (err) {}
return <UnknownAccountCard account={account} />;
}
function MintAccountCard({
account,
info,
}: {
account: Account;
info: MintAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Token Mint Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Decimals</td>
<td className="text-lg-right">{info.decimals}</td>
</tr>
<tr>
<td>Status</td>
<td className="text-lg-right">
{info.isInitialized ? "Initialized" : "Uninitialized"}
</td>
</tr>
{info.owner !== undefined && (
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={info.owner} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
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 (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Token Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Mint</td>
<td className="text-lg-right">
<Address pubkey={info.mint} alignRight link />
</td>
</tr>
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={info.owner} alignRight link />
</td>
</tr>
<tr>
<td>Balance (tokens)</td>
<td className="text-lg-right">{balance}</td>
</tr>
<tr>
<td>Status</td>
<td className="text-lg-right">
{info.isInitialized ? "Initialized" : "Uninitialized"}
</td>
</tr>
</TableCardBody>
</div>
);
}
function MultisigAccountCard({
account,
info,
}: {
account: Account;
info: MultisigAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Multisig Account
</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(account.pubkey)}
>
<span className="fe fe-refresh-cw mr-2"></span>
Refresh
</button>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Required Signers</td>
<td className="text-lg-right">{info.numRequiredSigners}</td>
</tr>
<tr>
<td>Valid Signers</td>
<td className="text-lg-right">{info.numValidSigners}</td>
</tr>
{info.signers.map((signer) => (
<tr key={signer.toString()}>
<td>Signer</td>
<td className="text-lg-right">
<Address pubkey={signer} alignRight link />
</td>
</tr>
))}
<tr>
<td>Status</td>
<td className="text-lg-right">
{info.isInitialized ? "Initialized" : "Uninitialized"}
</td>
</tr>
</TableCardBody>
</div>
);
}

View File

@ -1,11 +1,12 @@
import React from "react"; import React from "react";
import { PublicKey, StakeProgram } from "@solana/web3.js"; import { PublicKey } from "@solana/web3.js";
import { import {
FetchStatus, FetchStatus,
useFetchAccountInfo, useFetchAccountInfo,
useAccountInfo, useAccountInfo,
} from "providers/accounts"; } from "providers/accounts";
import { StakeAccountSection } from "components/account/StakeAccountSection"; import { StakeAccountSection } from "components/account/StakeAccountSection";
import { TokenAccountSection } from "components/account/TokenAccountSection";
import { ErrorCard } from "components/common/ErrorCard"; import { ErrorCard } from "components/common/ErrorCard";
import { LoadingCard } from "components/common/LoadingCard"; import { LoadingCard } from "components/common/LoadingCard";
import { useCluster, ClusterStatus } from "providers/cluster"; import { useCluster, ClusterStatus } from "providers/cluster";
@ -65,16 +66,15 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />; return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
} }
const owner = info.details?.owner;
const data = info.details?.data; const data = info.details?.data;
if (data && owner && owner.equals(StakeProgram.programId)) { if (data && data.name === "stake") {
let stakeAccountType, stakeAccount; let stakeAccountType, stakeAccount;
if ("accountType" in data) { if ("accountType" in data.parsed) {
stakeAccount = data; stakeAccount = data.parsed;
stakeAccountType = data.accountType as any; stakeAccountType = data.parsed.accountType as any;
} else { } else {
stakeAccount = data.info; stakeAccount = data.parsed.info;
stakeAccountType = data.type; stakeAccountType = data.parsed.type;
} }
return ( return (
@ -84,6 +84,8 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
stakeAccountType={stakeAccountType} stakeAccountType={stakeAccountType}
/> />
); );
} else if (data && data.name === "spl-token") {
return <TokenAccountSection account={info} tokenAccount={data.parsed} />;
} else { } else {
return <UnknownAccountCard account={info} />; return <UnknownAccountCard account={info} />;
} }

View File

@ -3,10 +3,11 @@ import { StakeAccount as StakeAccountWasm } from "solana-sdk-wasm";
import { PublicKey, Connection, StakeProgram } from "@solana/web3.js"; import { PublicKey, Connection, StakeProgram } from "@solana/web3.js";
import { useCluster } from "../cluster"; import { useCluster } from "../cluster";
import { HistoryProvider } from "./history"; import { HistoryProvider } from "./history";
import { TokensProvider } from "./tokens"; 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 "./types"; import { StakeAccount } from "validators/accounts/stake";
import { TokenAccount } from "validators/accounts/token";
export { useAccountHistory } from "./history"; export { useAccountHistory } from "./history";
export enum FetchStatus { export enum FetchStatus {
@ -15,11 +16,23 @@ export enum FetchStatus {
Fetched, 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 { export interface Details {
executable: boolean; executable: boolean;
owner: PublicKey; owner: PublicKey;
space?: number; space?: number;
data?: StakeAccount | StakeAccountWasm; data?: ProgramData;
} }
export interface Account { export interface Account {
@ -173,20 +186,38 @@ async function fetchAccountInfo(
space = result.data.length; space = result.data.length;
} }
let data; let data: ProgramData | undefined;
if (result.owner.equals(StakeProgram.programId)) { if (result.owner.equals(StakeProgram.programId)) {
try { try {
let parsed;
if ("parsed" in result.data) { if ("parsed" in result.data) {
const info = coerce(result.data.parsed, ParsedInfo); const info = coerce(result.data.parsed, ParsedInfo);
data = coerce(info, StakeAccount); parsed = coerce(info, StakeAccount);
} else { } else {
const wasm = await import("solana-sdk-wasm"); 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) { } catch (err) {
console.error("Unexpected error loading wasm", err); console.error("Failed to parse stake account", err);
// TODO store error state in Account info // 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 = { details = {

View File

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

View File

@ -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<typeof TokenAccountType>;
export const TokenAccountType = enums(["mint", "account", "multisig"]);
export type TokenAccountInfo = StructType<typeof TokenAccountInfo>;
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<typeof MintAccountInfo>;
export const MintAccountInfo = object({
decimals: number(),
isInitialized: boolean(),
owner: optional(Pubkey),
});
export type MultisigAccountInfo = StructType<typeof MultisigAccountInfo>;
export const MultisigAccountInfo = object({
numRequiredSigners: number(),
numValidSigners: number(),
isInitialized: boolean(),
signers: array(Pubkey),
});
export type TokenAccount = StructType<typeof TokenAccount>;
export const TokenAccount = object({
type: TokenAccountType,
info: any(),
});