explorer: Add details pages for upgradeable loader accounts (#15836)

This commit is contained in:
Justin Starry 2021-03-14 01:11:59 +08:00 committed by GitHub
parent 0c9ca5522c
commit c4f98f9c73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 352 additions and 127 deletions

View File

@ -0,0 +1,267 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { lamportsToSolString } from "utils";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Address } from "components/common/Address";
import {
ProgramAccountInfo,
ProgramBufferAccountInfo,
ProgramDataAccountInfo,
UpgradeableLoaderAccount,
} from "validators/accounts/upgradeable-program";
import { Slot } from "components/common/Slot";
import { addressLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
import { ErrorCard } from "components/common/ErrorCard";
export function UpgradeableLoaderAccountSection({
account,
parsedData,
programData,
}: {
account: Account;
parsedData: UpgradeableLoaderAccount;
programData: ProgramDataAccountInfo | undefined;
}) {
switch (parsedData.type) {
case "program": {
if (programData === undefined) {
return <ErrorCard text="Invalid Upgradeable Program account" />;
}
return (
<UpgradeableProgramSection
account={account}
programAccount={parsedData.info}
programData={programData}
/>
);
}
case "programData": {
return (
<UpgradeableProgramDataSection
account={account}
programData={parsedData.info}
/>
);
}
case "buffer": {
return (
<UpgradeableProgramBufferSection
account={account}
programBuffer={parsedData.info}
/>
);
}
}
}
export function UpgradeableProgramSection({
account,
programAccount,
programData,
}: {
account: Account;
programAccount: ProgramAccountInfo;
programData: ProgramDataAccountInfo;
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const label = addressLabel(account.pubkey.toBase58(), cluster);
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program 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-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-right">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">
{lamportsToSolString(account.lamports || 0)}
</td>
</tr>
<tr>
<td>Executable</td>
<td className="text-lg-right">Yes</td>
</tr>
<tr>
<td>Executable Data</td>
<td className="text-lg-right">
<Address pubkey={programAccount.programData} alignRight link />
</td>
</tr>
<tr>
<td>Upgradeable</td>
<td className="text-lg-right">
{programData.authority !== null ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>Last Deployed Slot</td>
<td className="text-lg-right">
<Slot slot={programData.slot} link />
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-right">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
export function UpgradeableProgramDataSection({
account,
programData,
}: {
account: Account;
programData: ProgramDataAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program Executable Data 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-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">
{lamportsToSolString(account.lamports || 0)}
</td>
</tr>
{account.details?.space !== undefined && (
<tr>
<td>Data (Bytes)</td>
<td className="text-lg-right">{account.details.space}</td>
</tr>
)}
<tr>
<td>Upgradeable</td>
<td className="text-lg-right">
{programData.authority !== null ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>Last Deployed Slot</td>
<td className="text-lg-right">
<Slot slot={programData.slot} link />
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-right">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}
export function UpgradeableProgramBufferSection({
account,
programBuffer,
}: {
account: Account;
programBuffer: ProgramBufferAccountInfo;
}) {
const refresh = useFetchAccountInfo();
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program Deploy Buffer 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-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">
{lamportsToSolString(account.lamports || 0)}
</td>
</tr>
{account.details?.space !== undefined && (
<tr>
<td>Data (Bytes)</td>
<td className="text-lg-right">{account.details.space}</td>
</tr>
)}
{programBuffer.authority !== null && (
<tr>
<td>Deploy Authority</td>
<td className="text-lg-right">
<Address pubkey={programBuffer.authority} alignRight link />
</td>
</tr>
)}
{account.details && (
<tr>
<td>Owner</td>
<td className="text-lg-right">
<Address pubkey={account.details.owner} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -1,93 +0,0 @@
import React from "react";
import { TableCardBody } from "components/common/TableCardBody";
import { lamportsToSolString } from "utils";
import { Account, useFetchAccountInfo } from "providers/accounts";
import { Address } from "components/common/Address";
import {
ProgramAccountInfo,
ProgramDataAccountInfo,
} from "validators/accounts/upgradeable-program";
import { Slot } from "components/common/Slot";
import { addressLabel } from "utils/tx";
import { useCluster } from "providers/cluster";
export function UpgradeableProgramSection({
account,
programAccount,
programData,
}: {
account: Account;
programAccount: ProgramAccountInfo;
programData: ProgramDataAccountInfo;
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
const label = addressLabel(account.pubkey.toBase58(), cluster);
return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Program 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-lg-right">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
{label && (
<tr>
<td>Address Label</td>
<td className="text-lg-right">{label}</td>
</tr>
)}
<tr>
<td>Balance (SOL)</td>
<td className="text-lg-right text-uppercase">
{lamportsToSolString(account.lamports || 0)}
</td>
</tr>
<tr>
<td>Executable</td>
<td className="text-lg-right">Yes</td>
</tr>
<tr>
<td>Executable Data</td>
<td className="text-lg-right">
<Address pubkey={programAccount.programData} alignRight link />
</td>
</tr>
<tr>
<td>Upgradeable</td>
<td className="text-lg-right">
{programData.authority !== null ? "Yes" : "No"}
</td>
</tr>
<tr>
<td>Last Deployed Slot</td>
<td className="text-lg-right">
<Slot slot={programData.slot} link />
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-right">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
)}
</TableCardBody>
</div>
);
}

View File

@ -28,7 +28,7 @@ import { StakeHistoryCard } from "components/account/StakeHistoryCard";
import { BlockhashesCard } from "components/account/BlockhashesCard"; import { BlockhashesCard } from "components/account/BlockhashesCard";
import { ConfigAccountSection } from "components/account/ConfigAccountSection"; import { ConfigAccountSection } from "components/account/ConfigAccountSection";
import { useFlaggedAccounts } from "providers/accounts/flagged-accounts"; import { useFlaggedAccounts } from "providers/accounts/flagged-accounts";
import { UpgradeableProgramSection } from "components/account/UpgradeableProgramSection"; import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
import { useTokenRegistry } from "providers/mints/token-registry"; import { useTokenRegistry } from "providers/mints/token-registry";
const TABS_LOOKUP: { [id: string]: Tab } = { const TABS_LOOKUP: { [id: string]: Tab } = {
@ -177,9 +177,9 @@ function InfoSection({ account }: { account: Account }) {
if (data && data.program === "bpf-upgradeable-loader") { if (data && data.program === "bpf-upgradeable-loader") {
return ( return (
<UpgradeableProgramSection <UpgradeableLoaderAccountSection
account={account} account={account}
programAccount={data.programAccount} parsedData={data.parsed}
programData={data.programData} programData={data.programData}
/> />
); );

View File

@ -20,10 +20,9 @@ import { SysvarAccount } from "validators/accounts/sysvar";
import { ConfigAccount } from "validators/accounts/config"; import { ConfigAccount } from "validators/accounts/config";
import { FlaggedAccountsProvider } from "./flagged-accounts"; import { FlaggedAccountsProvider } from "./flagged-accounts";
import { import {
ProgramAccount,
ProgramAccountInfo,
ProgramDataAccount, ProgramDataAccount,
ProgramDataAccountInfo, ProgramDataAccountInfo,
UpgradeableLoaderAccount,
} from "validators/accounts/upgradeable-program"; } from "validators/accounts/upgradeable-program";
export { useAccountHistory } from "./history"; export { useAccountHistory } from "./history";
@ -33,10 +32,10 @@ export type StakeProgramData = {
activation?: StakeActivationData; activation?: StakeActivationData;
}; };
export type UpgradeableProgramAccountData = { export type UpgradeableLoaderAccountData = {
program: "bpf-upgradeable-loader"; program: "bpf-upgradeable-loader";
programData: ProgramDataAccountInfo; parsed: UpgradeableLoaderAccount;
programAccount: ProgramAccountInfo; programData?: ProgramDataAccountInfo;
}; };
export type TokenProgramData = { export type TokenProgramData = {
@ -65,7 +64,7 @@ export type ConfigProgramData = {
}; };
export type ProgramData = export type ProgramData =
| UpgradeableProgramAccountData | UpgradeableLoaderAccountData
| StakeProgramData | StakeProgramData
| TokenProgramData | TokenProgramData
| VoteProgramData | VoteProgramData
@ -154,35 +153,32 @@ async function fetchAccountInfo(
const info = create(result.data.parsed, ParsedInfo); const info = create(result.data.parsed, ParsedInfo);
switch (result.data.program) { switch (result.data.program) {
case "bpf-upgradeable-loader": { case "bpf-upgradeable-loader": {
let programAccount: ProgramAccountInfo; const parsed = create(info, UpgradeableLoaderAccount);
let programData: ProgramDataAccountInfo;
if (info.type === "programData") { // Fetch program data to get program upgradeability info
break; let programData: ProgramDataAccountInfo | undefined;
} if (parsed.type === "program") {
const result = (
const parsed = create(info, ProgramAccount); await connection.getParsedAccountInfo(parsed.info.programData)
programAccount = parsed.info; ).value;
const result = ( if (
await connection.getParsedAccountInfo(parsed.info.programData) result &&
).value; "parsed" in result.data &&
if ( result.data.program === "bpf-upgradeable-loader"
result && ) {
"parsed" in result.data && const info = create(result.data.parsed, ParsedInfo);
result.data.program === "bpf-upgradeable-loader" programData = create(info, ProgramDataAccount).info;
) { } else {
const info = create(result.data.parsed, ParsedInfo); throw new Error(
programData = create(info, ProgramDataAccount).info; `invalid program data account for program: ${pubkey.toBase58()}`
} else { );
throw new Error( }
`invalid program data account for program: ${pubkey.toBase58()}`
);
} }
data = { data = {
program: result.data.program, program: result.data.program,
parsed,
programData, programData,
programAccount,
}; };
break; break;

View File

@ -7,7 +7,7 @@ type Tags =
| undefined; | undefined;
export function reportError(err: Error, tags: Tags) { export function reportError(err: Error, tags: Tags) {
console.error(err); console.error(err, err.message);
try { try {
Sentry.captureException(err, { Sentry.captureException(err, {
tags, tags,

View File

@ -1,6 +1,16 @@
/* eslint-disable @typescript-eslint/no-redeclare */ /* eslint-disable @typescript-eslint/no-redeclare */
import { type, number, literal, nullable, Infer } from "superstruct"; import {
type,
number,
literal,
nullable,
Infer,
union,
coerce,
create,
} from "superstruct";
import { ParsedInfo } from "validators";
import { PublicKeyFromString } from "validators/pubkey"; import { PublicKeyFromString } from "validators/pubkey";
export type ProgramAccountInfo = Infer<typeof ProgramAccountInfo>; export type ProgramAccountInfo = Infer<typeof ProgramAccountInfo>;
@ -26,3 +36,48 @@ export const ProgramDataAccount = type({
type: literal("programData"), type: literal("programData"),
info: ProgramDataAccountInfo, info: ProgramDataAccountInfo,
}); });
export type ProgramBufferAccountInfo = Infer<typeof ProgramBufferAccountInfo>;
export const ProgramBufferAccountInfo = type({
authority: nullable(PublicKeyFromString),
// don't care about data yet
});
export type ProgramBufferAccount = Infer<typeof ProgramBufferAccount>;
export const ProgramBufferAccount = type({
type: literal("buffer"),
info: ProgramBufferAccountInfo,
});
export type UpgradeableLoaderAccount = Infer<typeof UpgradeableLoaderAccount>;
export const UpgradeableLoaderAccount = coerce(
union([ProgramAccount, ProgramDataAccount, ProgramBufferAccount]),
ParsedInfo,
(value) => {
// Coercions like `PublicKeyFromString` are not applied within
// union validators so we use this custom coercion as a workaround.
switch (value.type) {
case "program": {
return {
type: value.type,
info: create(value.info, ProgramAccountInfo),
};
}
case "programData": {
return {
type: value.type,
info: create(value.info, ProgramDataAccountInfo),
};
}
case "buffer": {
return {
type: value.type,
info: create(value.info, ProgramBufferAccountInfo),
};
}
default: {
throw new Error(`Unknown program account type: ${value.type}`);
}
}
}
);