Spruce up stake account details page

This commit is contained in:
Justin Starry 2020-05-14 23:20:35 +08:00 committed by Michael Vines
parent 1f883c88e5
commit 3e2538919f
4 changed files with 227 additions and 171 deletions

View File

@ -1,5 +1,4 @@
import React from "react";
import { StakeAccount } from "solana-sdk-wasm";
import { useClusterModal } from "providers/cluster";
import { PublicKey, StakeProgram } from "@solana/web3.js";
import ClusterStatusButton from "components/ClusterStatusButton";
@ -8,12 +7,13 @@ import {
Status,
useFetchAccountInfo,
useFetchAccountHistory,
useAccountInfo
useAccountInfo,
Account
} from "providers/accounts";
import { lamportsToSolString } from "utils";
import Copyable from "./Copyable";
import { displayAddress } from "utils/tx";
import { StakeAccountDetailsCard } from "components/account/StakeAccountDetailsCard";
import { StakeAccountCards } from "components/account/StakeAccountCards";
import ErrorCard from "components/common/ErrorCard";
import LoadingCard from "components/common/LoadingCard";
import TableCardBody from "components/common/TableCardBody";
@ -88,54 +88,13 @@ export default function AccountDetails({ address }: Props) {
</button>
</div>
</div>
{pubkey && <InfoCard pubkey={pubkey} />}
{pubkey && <DetailsCard pubkey={pubkey} />}
{pubkey && <AccountCards pubkey={pubkey} />}
{pubkey && <HistoryCard pubkey={pubkey} />}
</div>
);
}
type Wasm = {
StakeAccount: typeof StakeAccount;
};
function DetailsCard({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const info = useAccountInfo(address);
const [Wasm, setWasm] = React.useState<Wasm | undefined>(undefined);
React.useEffect(() => {
(async () => {
try {
setWasm(await import("solana-sdk-wasm"));
} catch (err) {
console.error("Unexpected error loading wasm", err);
}
})();
}, []);
if (!info || !info.details || !info.details.data) {
return null;
}
const { data, owner } = info.details;
try {
if (owner.equals(StakeProgram.programId)) {
if (Wasm === undefined) {
return <LoadingCard />;
} else {
const stakeAccount = Wasm.StakeAccount.fromAccountData(data);
return <StakeAccountDetailsCard account={stakeAccount} />;
}
}
} catch (err) {
console.error(err);
return <ErrorCard text="Failed to decode account data" />;
}
return null;
}
function InfoCard({ pubkey }: { pubkey: PublicKey }) {
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
const address = pubkey.toBase58();
const info = useAccountInfo(address);
const refresh = useFetchAccountInfo();
@ -149,12 +108,25 @@ function InfoCard({ pubkey }: { pubkey: PublicKey }) {
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
}
const { details, lamports } = info;
const owner = info.details?.owner;
const data = info.details?.data;
if (data && owner && owner.equals(StakeProgram.programId)) {
return <StakeAccountCards account={info} stakeAccount={data} />;
} else {
return <UnknownAccountCard account={info} />;
}
}
function UnknownAccountCard({ account }: { account: Account }) {
const refresh = useFetchAccountInfo();
const { details, lamports, pubkey } = account;
if (lamports === undefined) return null;
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Overview</h3>
<h3 className="card-header-title">Account Overview</h3>
<button
className="btn btn-white btn-sm"
onClick={() => refresh(pubkey)}

View File

@ -0,0 +1,198 @@
import React from "react";
import { StakeAccount, Meta } from "solana-sdk-wasm";
import TableCardBody from "components/common/TableCardBody";
import { lamportsToSolString } from "utils";
import Copyable from "components/Copyable";
import { displayAddress } from "utils/tx";
import { Account, useFetchAccountInfo } from "providers/accounts";
export function StakeAccountCards({
account,
stakeAccount
}: {
account: Account;
stakeAccount: StakeAccount;
}) {
return (
<>
<LockupCard stakeAccount={stakeAccount} />
<OverviewCard account={account} stakeAccount={stakeAccount} />
{stakeAccount.meta && <DelegationCard stakeAccount={stakeAccount} />}
{stakeAccount.meta && <AuthoritiesCard meta={stakeAccount.meta} />}
</>
);
}
function LockupCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
const unixTimestamp = stakeAccount.meta?.lockup.unixTimestamp;
if (unixTimestamp && unixTimestamp > 0) {
const expireDate = new Date(unixTimestamp * 1000);
return (
<div className="alert alert-warning text-center">
<strong>Account is locked!</strong> Lockup expires on{" "}
{expireDate.toLocaleDateString()} at {expireDate.toLocaleTimeString()}
</div>
);
} else {
return null;
}
}
function OverviewCard({
account,
stakeAccount
}: {
account: Account;
stakeAccount: StakeAccount;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Stake 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-right">
<Copyable text={account.pubkey.toBase58()}>
<code>{account.pubkey.toBase58()}</code>
</Copyable>
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-right text-uppercase">
{lamportsToSolString(account.lamports || 0)}
</td>
</tr>
{stakeAccount.meta && (
<tr>
<td>Rent Reserve (SOL)</td>
<td className="text-right">
{lamportsToSolString(stakeAccount.meta.rentExemptReserve)}
</td>
</tr>
)}
{!stakeAccount.meta && (
<tr>
<td>State</td>
<td className="text-right">{stakeAccount.displayState()}</td>
</tr>
)}
</TableCardBody>
</div>
);
}
function DelegationCard({ stakeAccount }: { stakeAccount: StakeAccount }) {
const { stake } = stakeAccount;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Stake Delegation
</h3>
</div>
<TableCardBody>
<tr>
<td>Status</td>
<td className="text-right">{stakeAccount.displayState()}</td>
</tr>
{stake && (
<>
<tr>
<td>Delegated Stake (SOL)</td>
<td className="text-right">
{lamportsToSolString(stake.delegation.stake)}
</td>
</tr>
<tr>
<td>Delegated Vote Address</td>
<td className="text-right">
<Copyable text={stake.delegation.voterPubkey.toBase58()}>
<code>
{displayAddress(stake.delegation.voterPubkey.toBase58())}
</code>
</Copyable>
</td>
</tr>
<tr>
<td>Activation Epoch</td>
<td className="text-right">
{stake.delegation.isBootstrapStake()
? "-"
: stake.delegation.activationEpoch}
</td>
</tr>
<tr>
<td>Deactivation Epoch</td>
<td className="text-right">
{stake.delegation.isDeactivated()
? stake.delegation.deactivationEpoch
: "-"}
</td>
</tr>
</>
)}
</TableCardBody>
</div>
);
}
function AuthoritiesCard({ meta }: { meta: Meta }) {
const hasLockup = meta && meta.lockup.unixTimestamp > 0;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Authorities
</h3>
</div>
<TableCardBody>
<tr>
<td>Stake Authority Address</td>
<td className="text-right">
<Copyable text={meta.authorized.staker.toBase58()}>
<code>{meta.authorized.staker.toBase58()}</code>
</Copyable>
</td>
</tr>
<tr>
<td>Withdraw Authority Address</td>
<td className="text-right">
<Copyable text={meta.authorized.withdrawer.toBase58()}>
<code>{meta.authorized.withdrawer.toBase58()}</code>
</Copyable>
</td>
</tr>
{hasLockup && (
<tr>
<td>Lockup Authority Address</td>
<td className="text-right">
<Copyable text={meta.lockup.custodian.toBase58()}>
<code>{displayAddress(meta.lockup.custodian.toBase58())}</code>
</Copyable>
</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -1,121 +0,0 @@
import React from "react";
import { StakeAccount } from "solana-sdk-wasm";
import TableCardBody from "components/common/TableCardBody";
import { lamportsToSolString } from "utils";
import Copyable from "components/Copyable";
import { displayAddress } from "utils/tx";
export function StakeAccountDetailsCard({
account
}: {
account: StakeAccount;
}) {
const { meta, stake } = account;
const hasLockup = meta && meta.lockup.unixTimestamp > 0;
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Stake Account
</h3>
</div>
<TableCardBody>
<tr>
<td>State</td>
<td className="text-right">{account.displayState()}</td>
</tr>
{meta && (
<>
<tr>
<td>Rent Reserve (SOL)</td>
<td className="text-right">
{lamportsToSolString(meta.rentExemptReserve)}
</td>
</tr>
<tr>
<td>Stake Authority Address</td>
<td className="text-right">
<Copyable text={meta.authorized.staker.toBase58()}>
<code>{meta.authorized.staker.toBase58()}</code>
</Copyable>
</td>
</tr>
<tr>
<td>Withdraw Authority Address</td>
<td className="text-right">
<Copyable text={meta.authorized.withdrawer.toBase58()}>
<code>{meta.authorized.withdrawer.toBase58()}</code>
</Copyable>
</td>
</tr>
{hasLockup && (
<tr>
<td>Lockup Expiry Timestamp</td>
<td className="text-right">
{new Date(meta.lockup.unixTimestamp).toUTCString()}
</td>
</tr>
)}
{hasLockup && (
<tr>
<td>Lockup Custodian Address</td>
<td className="text-right">
<Copyable text={meta.lockup.custodian.toBase58()}>
<code>
{displayAddress(meta.lockup.custodian.toBase58())}
</code>
</Copyable>
</td>
</tr>
)}
</>
)}
{stake && (
<>
<tr>
<td>Delegated Stake (SOL)</td>
<td className="text-right">
{lamportsToSolString(stake.delegation.stake)}
</td>
</tr>
<tr>
<td>Delegated Vote Address</td>
<td className="text-right">
<Copyable text={stake.delegation.voterPubkey.toBase58()}>
<code>
{displayAddress(stake.delegation.voterPubkey.toBase58())}
</code>
</Copyable>
</td>
</tr>
<tr>
<td>Activation Epoch</td>
<td className="text-right">
{stake.delegation.isBootstrapStake()
? "-"
: stake.delegation.activationEpoch}
</td>
</tr>
<tr>
<td>Deactivation Epoch</td>
<td className="text-right">
{stake.delegation.isDeactivated()
? stake.delegation.deactivationEpoch
: "-"}
</td>
</tr>
</>
)}
</TableCardBody>
</div>
);
}

View File

@ -9,6 +9,7 @@ import {
} from "@solana/web3.js";
import { useQuery } from "../utils/url";
import { useCluster, ClusterStatus } from "./cluster";
import { StakeAccount } from "solana-sdk-wasm";
export enum Status {
Checking,
@ -28,7 +29,7 @@ export interface Details {
executable: boolean;
owner: PublicKey;
space: number;
data?: Buffer;
data?: StakeAccount;
}
export interface Account {
@ -198,7 +199,13 @@ async function fetchAccountInfo(
// Only save data in memory if we can decode it
if (result.owner.equals(StakeProgram.programId)) {
data = result.data;
try {
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
}
}
details = {