Display detailed token account info on explorer (#11472)
This commit is contained in:
parent
4e815eaeb6
commit
fb822688b7
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -102,7 +102,7 @@ export function TokensProvider({ children }: ProviderProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOKEN_PROGRAM_ID = new PublicKey(
|
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||||
"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
});
|
Loading…
Reference in New Issue