Spruce up stake account details page
This commit is contained in:
parent
1f883c88e5
commit
3e2538919f
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue