Add support for parsed stake accounts (#11469)

This commit is contained in:
Justin Starry
2020-08-08 21:06:24 +08:00
committed by GitHub
parent 102d15f081
commit c544116cf2
21 changed files with 340 additions and 80 deletions

View 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
);
});
});

View File

@@ -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">

View File

@@ -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>

View File

@@ -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} />;

View File

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

View File

@@ -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} />;
}

View File

@@ -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,

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

View File

@@ -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 (
<>

View 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");
});

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

View 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");
});