Display detailed token account info on explorer (#11472)
This commit is contained in:
parent
4e815eaeb6
commit
fb822688b7
|
@ -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));
|
||||
|
|
|
@ -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 { 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 <ErrorCard retry={() => 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 <TokenAccountSection account={info} tokenAccount={data.parsed} />;
|
||||
} else {
|
||||
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 { 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 = {
|
||||
|
|
|
@ -102,7 +102,7 @@ export function TokensProvider({ children }: ProviderProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
export const TOKEN_PROGRAM_ID = new PublicKey(
|
||||
"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