Spruce up stake account details page
This commit is contained in:
parent
1f883c88e5
commit
3e2538919f
|
@ -1,5 +1,4 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StakeAccount } from "solana-sdk-wasm";
|
|
||||||
import { useClusterModal } from "providers/cluster";
|
import { useClusterModal } from "providers/cluster";
|
||||||
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
import { PublicKey, StakeProgram } from "@solana/web3.js";
|
||||||
import ClusterStatusButton from "components/ClusterStatusButton";
|
import ClusterStatusButton from "components/ClusterStatusButton";
|
||||||
|
@ -8,12 +7,13 @@ import {
|
||||||
Status,
|
Status,
|
||||||
useFetchAccountInfo,
|
useFetchAccountInfo,
|
||||||
useFetchAccountHistory,
|
useFetchAccountHistory,
|
||||||
useAccountInfo
|
useAccountInfo,
|
||||||
|
Account
|
||||||
} from "providers/accounts";
|
} from "providers/accounts";
|
||||||
import { lamportsToSolString } from "utils";
|
import { lamportsToSolString } from "utils";
|
||||||
import Copyable from "./Copyable";
|
import Copyable from "./Copyable";
|
||||||
import { displayAddress } from "utils/tx";
|
import { displayAddress } from "utils/tx";
|
||||||
import { StakeAccountDetailsCard } from "components/account/StakeAccountDetailsCard";
|
import { StakeAccountCards } from "components/account/StakeAccountCards";
|
||||||
import ErrorCard from "components/common/ErrorCard";
|
import ErrorCard from "components/common/ErrorCard";
|
||||||
import LoadingCard from "components/common/LoadingCard";
|
import LoadingCard from "components/common/LoadingCard";
|
||||||
import TableCardBody from "components/common/TableCardBody";
|
import TableCardBody from "components/common/TableCardBody";
|
||||||
|
@ -88,54 +88,13 @@ export default function AccountDetails({ address }: Props) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{pubkey && <InfoCard pubkey={pubkey} />}
|
{pubkey && <AccountCards pubkey={pubkey} />}
|
||||||
{pubkey && <DetailsCard pubkey={pubkey} />}
|
|
||||||
{pubkey && <HistoryCard pubkey={pubkey} />}
|
{pubkey && <HistoryCard pubkey={pubkey} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type Wasm = {
|
function AccountCards({ pubkey }: { pubkey: PublicKey }) {
|
||||||
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 }) {
|
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
const info = useAccountInfo(address);
|
const info = useAccountInfo(address);
|
||||||
const refresh = useFetchAccountInfo();
|
const refresh = useFetchAccountInfo();
|
||||||
|
@ -149,12 +108,25 @@ function InfoCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
return <ErrorCard retry={() => refresh(pubkey)} text="Fetch Failed" />;
|
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 (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header align-items-center">
|
<div className="card-header align-items-center">
|
||||||
<h3 className="card-header-title">Overview</h3>
|
<h3 className="card-header-title">Account Overview</h3>
|
||||||
<button
|
<button
|
||||||
className="btn btn-white btn-sm"
|
className="btn btn-white btn-sm"
|
||||||
onClick={() => refresh(pubkey)}
|
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";
|
} from "@solana/web3.js";
|
||||||
import { useQuery } from "../utils/url";
|
import { useQuery } from "../utils/url";
|
||||||
import { useCluster, ClusterStatus } from "./cluster";
|
import { useCluster, ClusterStatus } from "./cluster";
|
||||||
|
import { StakeAccount } from "solana-sdk-wasm";
|
||||||
|
|
||||||
export enum Status {
|
export enum Status {
|
||||||
Checking,
|
Checking,
|
||||||
|
@ -28,7 +29,7 @@ export interface Details {
|
||||||
executable: boolean;
|
executable: boolean;
|
||||||
owner: PublicKey;
|
owner: PublicKey;
|
||||||
space: number;
|
space: number;
|
||||||
data?: Buffer;
|
data?: StakeAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
|
@ -198,7 +199,13 @@ async function fetchAccountInfo(
|
||||||
|
|
||||||
// Only save data in memory if we can decode it
|
// Only save data in memory if we can decode it
|
||||||
if (result.owner.equals(StakeProgram.programId)) {
|
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 = {
|
details = {
|
||||||
|
|
Loading…
Reference in New Issue