Add support for parsed stake accounts (#11469)
This commit is contained in:
28
explorer/src/__tests__/lamportsToSol.ts
Normal file
28
explorer/src/__tests__/lamportsToSol.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from "chai";
|
||||
import { lamportsToSol, LAMPORTS_PER_SOL } from "utils";
|
||||
import BN from "bn.js";
|
||||
|
||||
describe("lamportsToSol", () => {
|
||||
it("0 lamports", () => {
|
||||
expect(lamportsToSol(new BN(0))).to.eq(0.0);
|
||||
});
|
||||
|
||||
it("1 lamport", () => {
|
||||
expect(lamportsToSol(new BN(1))).to.eq(0.000000001);
|
||||
expect(lamportsToSol(new BN(-1))).to.eq(-0.000000001);
|
||||
});
|
||||
|
||||
it("1 SOL", () => {
|
||||
expect(lamportsToSol(new BN(LAMPORTS_PER_SOL))).to.eq(1.0);
|
||||
expect(lamportsToSol(new BN(-LAMPORTS_PER_SOL))).to.eq(-1.0);
|
||||
});
|
||||
|
||||
it("u64::MAX lamports", () => {
|
||||
expect(lamportsToSol(new BN(2).pow(new BN(64)))).to.eq(
|
||||
18446744073.709551615
|
||||
);
|
||||
expect(lamportsToSol(new BN(2).pow(new BN(64)).neg())).to.eq(
|
||||
-18446744073.709551615
|
||||
);
|
||||
});
|
||||
});
|
@@ -1,29 +1,54 @@
|
||||
import React from "react";
|
||||
import { StakeAccount, Meta } from "solana-sdk-wasm";
|
||||
import { StakeAccount as StakeAccountWasm, Meta } from "solana-sdk-wasm";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { lamportsToSolString } from "utils";
|
||||
import { displayTimestamp } from "utils/date";
|
||||
import { Account, useFetchAccountInfo } from "providers/accounts";
|
||||
import { Address } from "components/common/Address";
|
||||
import {
|
||||
StakeAccountInfo,
|
||||
StakeMeta,
|
||||
StakeAccountType,
|
||||
} from "providers/accounts/types";
|
||||
import BN from "bn.js";
|
||||
|
||||
const MAX_EPOCH = new BN(2).pow(new BN(64));
|
||||
|
||||
export function StakeAccountSection({
|
||||
account,
|
||||
stakeAccount,
|
||||
stakeAccountType,
|
||||
}: {
|
||||
account: Account;
|
||||
stakeAccount: StakeAccount;
|
||||
stakeAccount: StakeAccountInfo | StakeAccountWasm;
|
||||
stakeAccountType: StakeAccountType;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<LockupCard stakeAccount={stakeAccount} />
|
||||
<OverviewCard account={account} stakeAccount={stakeAccount} />
|
||||
{stakeAccount.meta && <DelegationCard stakeAccount={stakeAccount} />}
|
||||
{stakeAccount.meta && <AuthoritiesCard meta={stakeAccount.meta} />}
|
||||
<OverviewCard
|
||||
account={account}
|
||||
stakeAccount={stakeAccount}
|
||||
stakeAccountType={stakeAccountType}
|
||||
/>
|
||||
{stakeAccount.meta && (
|
||||
<>
|
||||
<DelegationCard
|
||||
stakeAccount={stakeAccount}
|
||||
stakeAccountType={stakeAccountType}
|
||||
/>
|
||||
<AuthoritiesCard meta={stakeAccount.meta} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LockupCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
|
||||
function LockupCard({
|
||||
stakeAccount,
|
||||
}: {
|
||||
stakeAccount: StakeAccountInfo | StakeAccountWasm;
|
||||
}) {
|
||||
const unixTimestamp = stakeAccount.meta?.lockup.unixTimestamp;
|
||||
if (unixTimestamp && unixTimestamp > 0) {
|
||||
const prettyTimestamp = displayTimestamp(unixTimestamp * 1000);
|
||||
@@ -37,12 +62,21 @@ function LockupCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
|
||||
}
|
||||
}
|
||||
|
||||
const TYPE_NAMES = {
|
||||
uninitialized: "Uninitialized",
|
||||
initialized: "Initialized",
|
||||
delegated: "Delegated",
|
||||
rewardsPool: "RewardsPool",
|
||||
};
|
||||
|
||||
function OverviewCard({
|
||||
account,
|
||||
stakeAccount,
|
||||
stakeAccountType,
|
||||
}: {
|
||||
account: Account;
|
||||
stakeAccount: StakeAccount;
|
||||
stakeAccount: StakeAccountInfo | StakeAccountWasm;
|
||||
stakeAccountType: StakeAccountType;
|
||||
}) {
|
||||
const refresh = useFetchAccountInfo();
|
||||
return (
|
||||
@@ -84,7 +118,7 @@ function OverviewCard({
|
||||
{!stakeAccount.meta && (
|
||||
<tr>
|
||||
<td>State</td>
|
||||
<td className="text-lg-right">{stakeAccount.displayState()}</td>
|
||||
<td className="text-lg-right">{TYPE_NAMES[stakeAccountType]}</td>
|
||||
</tr>
|
||||
)}
|
||||
</TableCardBody>
|
||||
@@ -92,16 +126,48 @@ function OverviewCard({
|
||||
);
|
||||
}
|
||||
|
||||
function DelegationCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
|
||||
const { stake } = stakeAccount;
|
||||
function DelegationCard({
|
||||
stakeAccount,
|
||||
stakeAccountType,
|
||||
}: {
|
||||
stakeAccount: StakeAccountInfo | StakeAccountWasm;
|
||||
stakeAccountType: StakeAccountType;
|
||||
}) {
|
||||
const displayStatus = () => {
|
||||
let status = stakeAccount.displayState();
|
||||
if (status !== "Delegated") {
|
||||
// TODO check epoch
|
||||
let status = TYPE_NAMES[stakeAccountType];
|
||||
if (stakeAccountType !== "delegated") {
|
||||
status = "Not delegated";
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
let voterPubkey, activationEpoch, deactivationEpoch;
|
||||
if ("accountType" in stakeAccount) {
|
||||
const delegation = stakeAccount?.stake?.delegation;
|
||||
if (delegation) {
|
||||
voterPubkey = delegation.voterPubkey;
|
||||
activationEpoch = delegation.isBootstrapStake()
|
||||
? "-"
|
||||
: delegation.activationEpoch;
|
||||
deactivationEpoch = delegation.isDeactivated()
|
||||
? delegation.deactivationEpoch
|
||||
: "-";
|
||||
}
|
||||
} else {
|
||||
const delegation = stakeAccount?.stake?.delegation;
|
||||
if (delegation) {
|
||||
voterPubkey = delegation.voter;
|
||||
activationEpoch = delegation.activationEpoch.eq(MAX_EPOCH)
|
||||
? "-"
|
||||
: delegation.activationEpoch.toString();
|
||||
deactivationEpoch = delegation.deactivationEpoch.eq(MAX_EPOCH)
|
||||
? "-"
|
||||
: delegation.deactivationEpoch.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const { stake } = stakeAccount;
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
@@ -124,33 +190,23 @@ function DelegationCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Delegated Vote Address</td>
|
||||
<td className="text-lg-right">
|
||||
<Address
|
||||
pubkey={stake.delegation.voterPubkey}
|
||||
alignRight
|
||||
link
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{voterPubkey && (
|
||||
<tr>
|
||||
<td>Delegated Vote Address</td>
|
||||
<td className="text-lg-right">
|
||||
<Address pubkey={voterPubkey} alignRight link />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
<tr>
|
||||
<td>Activation Epoch</td>
|
||||
<td className="text-lg-right">
|
||||
{stake.delegation.isBootstrapStake()
|
||||
? "-"
|
||||
: stake.delegation.activationEpoch}
|
||||
</td>
|
||||
<td className="text-lg-right">{activationEpoch}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Deactivation Epoch</td>
|
||||
<td className="text-lg-right">
|
||||
{stake.delegation.isDeactivated()
|
||||
? stake.delegation.deactivationEpoch
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="text-lg-right">{deactivationEpoch}</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
@@ -159,7 +215,7 @@ function DelegationCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AuthoritiesCard({ meta }: { meta: Meta }) {
|
||||
function AuthoritiesCard({ meta }: { meta: Meta | StakeMeta }) {
|
||||
const hasLockup = meta && meta.lockup.unixTimestamp > 0;
|
||||
return (
|
||||
<div className="card">
|
||||
|
@@ -28,7 +28,7 @@ export function UnknownAccountCard({ account }: { account: Account }) {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{details && (
|
||||
{details?.space !== undefined && (
|
||||
<tr>
|
||||
<td>Data (Bytes)</td>
|
||||
<td className="text-lg-right">{details.space}</td>
|
||||
|
@@ -10,7 +10,8 @@ import {
|
||||
import { UnknownDetailsCard } from "../UnknownDetailsCard";
|
||||
import { InstructionCard } from "../InstructionCard";
|
||||
import { Address } from "components/common/Address";
|
||||
import { ParsedInstructionInfo, IX_STRUCTS } from "./types";
|
||||
import { IX_STRUCTS, TokenInstructionType } from "./types";
|
||||
import { ParsedInfo } from "validators";
|
||||
|
||||
const IX_TITLES = {
|
||||
initializeMint: "Initialize Mint",
|
||||
@@ -34,8 +35,9 @@ type DetailsProps = {
|
||||
|
||||
export function TokenDetailsCard(props: DetailsProps) {
|
||||
try {
|
||||
const parsed = coerce(props.ix.parsed, ParsedInstructionInfo);
|
||||
const { type, info } = parsed;
|
||||
const parsed = coerce(props.ix.parsed, ParsedInfo);
|
||||
const { type: rawType, info } = parsed;
|
||||
const type = coerce(rawType, TokenInstructionType);
|
||||
const title = `Token: ${IX_TITLES[type]}`;
|
||||
const coerced = coerce(info, IX_STRUCTS[type] as any);
|
||||
return <TokenInstruction title={title} info={coerced} {...props} />;
|
||||
|
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
enums,
|
||||
object,
|
||||
any,
|
||||
StructType,
|
||||
coercion,
|
||||
struct,
|
||||
number,
|
||||
optional,
|
||||
array,
|
||||
} from "superstruct";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
const PubkeyValue = struct("Pubkey", (value) => value instanceof PublicKey);
|
||||
const Pubkey = coercion(PubkeyValue, (value) => {
|
||||
if (typeof value === "string") return new PublicKey(value);
|
||||
throw new Error("invalid pubkey");
|
||||
});
|
||||
import { Pubkey } from "validators/pubkey";
|
||||
|
||||
const InitializeMint = object({
|
||||
mint: Pubkey,
|
||||
@@ -95,8 +86,8 @@ const CloseAccount = object({
|
||||
signers: optional(array(Pubkey)),
|
||||
});
|
||||
|
||||
type TokenInstructionType = StructType<typeof TokenInstructionType>;
|
||||
const TokenInstructionType = enums([
|
||||
export type TokenInstructionType = StructType<typeof TokenInstructionType>;
|
||||
export const TokenInstructionType = enums([
|
||||
"initializeMint",
|
||||
"initializeAccount",
|
||||
"initializeMultisig",
|
||||
@@ -121,9 +112,3 @@ export const IX_STRUCTS = {
|
||||
burn: Burn,
|
||||
closeAccount: CloseAccount,
|
||||
};
|
||||
|
||||
export type ParsedInstructionInfo = StructType<typeof ParsedInstructionInfo>;
|
||||
export const ParsedInstructionInfo = object({
|
||||
type: TokenInstructionType,
|
||||
info: any(),
|
||||
});
|
||||
|
@@ -68,7 +68,22 @@ function InfoSection({ pubkey }: { pubkey: PublicKey }) {
|
||||
const owner = info.details?.owner;
|
||||
const data = info.details?.data;
|
||||
if (data && owner && owner.equals(StakeProgram.programId)) {
|
||||
return <StakeAccountSection account={info} stakeAccount={data} />;
|
||||
let stakeAccountType, stakeAccount;
|
||||
if ("accountType" in data) {
|
||||
stakeAccount = data;
|
||||
stakeAccountType = data.accountType as any;
|
||||
} else {
|
||||
stakeAccount = data.info;
|
||||
stakeAccountType = data.type;
|
||||
}
|
||||
|
||||
return (
|
||||
<StakeAccountSection
|
||||
account={info}
|
||||
stakeAccount={stakeAccount}
|
||||
stakeAccountType={stakeAccountType}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <UnknownAccountCard account={info} />;
|
||||
}
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import React from "react";
|
||||
import { StakeAccount } from "solana-sdk-wasm";
|
||||
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 { coerce } from "superstruct";
|
||||
import { ParsedInfo } from "validators";
|
||||
import { StakeAccount } from "./types";
|
||||
export { useAccountHistory } from "./history";
|
||||
|
||||
export enum FetchStatus {
|
||||
@@ -15,8 +18,8 @@ export enum FetchStatus {
|
||||
export interface Details {
|
||||
executable: boolean;
|
||||
owner: PublicKey;
|
||||
space: number;
|
||||
data?: StakeAccount;
|
||||
space?: number;
|
||||
data?: StakeAccount | StakeAccountWasm;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
@@ -156,18 +159,30 @@ async function fetchAccountInfo(
|
||||
let details;
|
||||
let lamports;
|
||||
try {
|
||||
const result = await new Connection(url, "recent").getAccountInfo(pubkey);
|
||||
const result = (
|
||||
await new Connection(url, "single").getParsedAccountInfo(pubkey)
|
||||
).value;
|
||||
if (result === null) {
|
||||
lamports = 0;
|
||||
} else {
|
||||
lamports = result.lamports;
|
||||
let data = undefined;
|
||||
|
||||
// Only save data in memory if we can decode it
|
||||
let space;
|
||||
if (!("parsed" in result.data)) {
|
||||
space = result.data.length;
|
||||
}
|
||||
|
||||
let data;
|
||||
if (result.owner.equals(StakeProgram.programId)) {
|
||||
try {
|
||||
const wasm = await import("solana-sdk-wasm");
|
||||
data = wasm.StakeAccount.fromAccountData(result.data);
|
||||
if ("parsed" in result.data) {
|
||||
const info = coerce(result.data.parsed, ParsedInfo);
|
||||
data = coerce(info, StakeAccount);
|
||||
} else {
|
||||
const wasm = await import("solana-sdk-wasm");
|
||||
data = wasm.StakeAccount.fromAccountData(result.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Unexpected error loading wasm", err);
|
||||
// TODO store error state in Account info
|
||||
@@ -175,7 +190,7 @@ async function fetchAccountInfo(
|
||||
}
|
||||
|
||||
details = {
|
||||
space: result.data.length,
|
||||
space,
|
||||
executable: result.executable,
|
||||
owner: result.owner,
|
||||
data,
|
||||
|
48
explorer/src/providers/accounts/types.ts
Normal file
48
explorer/src/providers/accounts/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { object, StructType, number, optional, enums } from "superstruct";
|
||||
import { Pubkey } from "validators/pubkey";
|
||||
import { BigNum } from "validators/bignum";
|
||||
|
||||
export type StakeAccountType = StructType<typeof StakeAccountType>;
|
||||
export const StakeAccountType = enums([
|
||||
"uninitialized",
|
||||
"initialized",
|
||||
"delegated",
|
||||
"rewardsPool",
|
||||
]);
|
||||
|
||||
export type StakeMeta = StructType<typeof StakeMeta>;
|
||||
export const StakeMeta = object({
|
||||
rentExemptReserve: BigNum,
|
||||
authorized: object({
|
||||
staker: Pubkey,
|
||||
withdrawer: Pubkey,
|
||||
}),
|
||||
lockup: object({
|
||||
unixTimestamp: number(),
|
||||
epoch: number(),
|
||||
custodian: Pubkey,
|
||||
}),
|
||||
});
|
||||
|
||||
export type StakeAccountInfo = StructType<typeof StakeAccountInfo>;
|
||||
export const StakeAccountInfo = object({
|
||||
meta: StakeMeta,
|
||||
stake: optional(
|
||||
object({
|
||||
delegation: object({
|
||||
voter: Pubkey,
|
||||
stake: BigNum,
|
||||
activationEpoch: BigNum,
|
||||
deactivationEpoch: BigNum,
|
||||
warmupCooldownRate: number(),
|
||||
}),
|
||||
creditsObserved: number(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type StakeAccount = StructType<typeof StakeAccount>;
|
||||
export const StakeAccount = object({
|
||||
type: StakeAccountType,
|
||||
info: StakeAccountInfo,
|
||||
});
|
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
|
||||
import React, { ReactNode } from "react";
|
||||
import BN from "bn.js";
|
||||
import {
|
||||
HumanizeDuration,
|
||||
HumanizeDurationLanguage,
|
||||
} from "humanize-duration-ts";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
// Switch to web3 constant when web3 updates superstruct
|
||||
export const LAMPORTS_PER_SOL = 1000000000;
|
||||
|
||||
export const NUM_TICKS_PER_SECOND = 160;
|
||||
export const DEFAULT_TICKS_PER_SLOT = 64;
|
||||
@@ -16,11 +18,31 @@ export function assertUnreachable(x: never): never {
|
||||
throw new Error("Unreachable!");
|
||||
}
|
||||
|
||||
export function lamportsToSol(lamports: number | BN): number {
|
||||
if (typeof lamports === "number") {
|
||||
return Math.abs(lamports) / LAMPORTS_PER_SOL;
|
||||
}
|
||||
|
||||
let signMultiplier = 1;
|
||||
if (lamports.isNeg()) {
|
||||
signMultiplier = -1;
|
||||
}
|
||||
|
||||
const absLamports = lamports.abs();
|
||||
const lamportsString = absLamports.toString(10).padStart(10, "0");
|
||||
const splitIndex = lamportsString.length - 9;
|
||||
const solString =
|
||||
lamportsString.slice(0, splitIndex) +
|
||||
"." +
|
||||
lamportsString.slice(splitIndex);
|
||||
return signMultiplier * parseFloat(solString);
|
||||
}
|
||||
|
||||
export function lamportsToSolString(
|
||||
lamports: number,
|
||||
lamports: number | BN,
|
||||
maximumFractionDigits: number = 9
|
||||
): ReactNode {
|
||||
const sol = Math.abs(lamports) / LAMPORTS_PER_SOL;
|
||||
const sol = lamportsToSol(lamports);
|
||||
return (
|
||||
<>
|
||||
◎
|
||||
|
8
explorer/src/validators/bignum.ts
Normal file
8
explorer/src/validators/bignum.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { coercion, struct, Struct } from "superstruct";
|
||||
import BN from "bn.js";
|
||||
|
||||
const BigNumValue = struct("BigNum", (value) => value instanceof BN);
|
||||
export const BigNum: Struct<BN, any> = coercion(BigNumValue, (value) => {
|
||||
if (typeof value === "string") return new BN(value, 10);
|
||||
throw new Error("invalid big num");
|
||||
});
|
7
explorer/src/validators/index.ts
Normal file
7
explorer/src/validators/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { object, any, StructType, string } from "superstruct";
|
||||
|
||||
export type ParsedInfo = StructType<typeof ParsedInfo>;
|
||||
export const ParsedInfo = object({
|
||||
type: string(),
|
||||
info: any(),
|
||||
});
|
8
explorer/src/validators/pubkey.ts
Normal file
8
explorer/src/validators/pubkey.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { coercion, struct, Struct } from "superstruct";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
const PubkeyValue = struct("Pubkey", (value) => value instanceof PublicKey);
|
||||
export const Pubkey: Struct<PublicKey, any> = coercion(PubkeyValue, (value) => {
|
||||
if (typeof value === "string") return new PublicKey(value);
|
||||
throw new Error("invalid pubkey");
|
||||
});
|
Reference in New Issue
Block a user