diff --git a/explorer/src/components/account/UpgradeableLoaderAccountSection.tsx b/explorer/src/components/account/UpgradeableLoaderAccountSection.tsx
new file mode 100644
index 0000000000..6110649401
--- /dev/null
+++ b/explorer/src/components/account/UpgradeableLoaderAccountSection.tsx
@@ -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 ;
+ }
+ return (
+
+ );
+ }
+ case "programData": {
+ return (
+
+ );
+ }
+ case "buffer": {
+ return (
+
+ );
+ }
+ }
+}
+
+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 (
+
+
+
+ Program Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+ {label && (
+
+ Address Label |
+ {label} |
+
+ )}
+
+ Balance (SOL) |
+
+ {lamportsToSolString(account.lamports || 0)}
+ |
+
+
+ Executable |
+ Yes |
+
+
+ Executable Data |
+
+
+ |
+
+
+ Upgradeable |
+
+ {programData.authority !== null ? "Yes" : "No"}
+ |
+
+
+ Last Deployed Slot |
+
+
+ |
+
+ {programData.authority !== null && (
+
+ Upgrade Authority |
+
+
+ |
+
+ )}
+
+
+ );
+}
+
+export function UpgradeableProgramDataSection({
+ account,
+ programData,
+}: {
+ account: Account;
+ programData: ProgramDataAccountInfo;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
+
+ Program Executable Data Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+
+ Balance (SOL) |
+
+ {lamportsToSolString(account.lamports || 0)}
+ |
+
+ {account.details?.space !== undefined && (
+
+ Data (Bytes) |
+ {account.details.space} |
+
+ )}
+
+ Upgradeable |
+
+ {programData.authority !== null ? "Yes" : "No"}
+ |
+
+
+ Last Deployed Slot |
+
+
+ |
+
+ {programData.authority !== null && (
+
+ Upgrade Authority |
+
+
+ |
+
+ )}
+
+
+ );
+}
+
+export function UpgradeableProgramBufferSection({
+ account,
+ programBuffer,
+}: {
+ account: Account;
+ programBuffer: ProgramBufferAccountInfo;
+}) {
+ const refresh = useFetchAccountInfo();
+ return (
+
+
+
+ Program Deploy Buffer Account
+
+
+
+
+
+
+ Address |
+
+
+ |
+
+
+ Balance (SOL) |
+
+ {lamportsToSolString(account.lamports || 0)}
+ |
+
+ {account.details?.space !== undefined && (
+
+ Data (Bytes) |
+ {account.details.space} |
+
+ )}
+ {programBuffer.authority !== null && (
+
+ Deploy Authority |
+
+
+ |
+
+ )}
+ {account.details && (
+
+ Owner |
+
+
+ |
+
+ )}
+
+
+ );
+}
diff --git a/explorer/src/components/account/UpgradeableProgramSection.tsx b/explorer/src/components/account/UpgradeableProgramSection.tsx
deleted file mode 100644
index 2c3fdfbf38..0000000000
--- a/explorer/src/components/account/UpgradeableProgramSection.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Program Account
-
-
-
-
-
-
- Address |
-
-
- |
-
- {label && (
-
- Address Label |
- {label} |
-
- )}
-
- Balance (SOL) |
-
- {lamportsToSolString(account.lamports || 0)}
- |
-
-
- Executable |
- Yes |
-
-
- Executable Data |
-
-
- |
-
-
- Upgradeable |
-
- {programData.authority !== null ? "Yes" : "No"}
- |
-
-
- Last Deployed Slot |
-
-
- |
-
- {programData.authority !== null && (
-
- Upgrade Authority |
-
-
- |
-
- )}
-
-
- );
-}
diff --git a/explorer/src/pages/AccountDetailsPage.tsx b/explorer/src/pages/AccountDetailsPage.tsx
index 452bb7d813..e18ec78116 100644
--- a/explorer/src/pages/AccountDetailsPage.tsx
+++ b/explorer/src/pages/AccountDetailsPage.tsx
@@ -28,7 +28,7 @@ import { StakeHistoryCard } from "components/account/StakeHistoryCard";
import { BlockhashesCard } from "components/account/BlockhashesCard";
import { ConfigAccountSection } from "components/account/ConfigAccountSection";
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";
const TABS_LOOKUP: { [id: string]: Tab } = {
@@ -177,9 +177,9 @@ function InfoSection({ account }: { account: Account }) {
if (data && data.program === "bpf-upgradeable-loader") {
return (
-
);
diff --git a/explorer/src/providers/accounts/index.tsx b/explorer/src/providers/accounts/index.tsx
index 648a76c0a1..ce8bd56bd9 100644
--- a/explorer/src/providers/accounts/index.tsx
+++ b/explorer/src/providers/accounts/index.tsx
@@ -20,10 +20,9 @@ import { SysvarAccount } from "validators/accounts/sysvar";
import { ConfigAccount } from "validators/accounts/config";
import { FlaggedAccountsProvider } from "./flagged-accounts";
import {
- ProgramAccount,
- ProgramAccountInfo,
ProgramDataAccount,
ProgramDataAccountInfo,
+ UpgradeableLoaderAccount,
} from "validators/accounts/upgradeable-program";
export { useAccountHistory } from "./history";
@@ -33,10 +32,10 @@ export type StakeProgramData = {
activation?: StakeActivationData;
};
-export type UpgradeableProgramAccountData = {
+export type UpgradeableLoaderAccountData = {
program: "bpf-upgradeable-loader";
- programData: ProgramDataAccountInfo;
- programAccount: ProgramAccountInfo;
+ parsed: UpgradeableLoaderAccount;
+ programData?: ProgramDataAccountInfo;
};
export type TokenProgramData = {
@@ -65,7 +64,7 @@ export type ConfigProgramData = {
};
export type ProgramData =
- | UpgradeableProgramAccountData
+ | UpgradeableLoaderAccountData
| StakeProgramData
| TokenProgramData
| VoteProgramData
@@ -154,35 +153,32 @@ async function fetchAccountInfo(
const info = create(result.data.parsed, ParsedInfo);
switch (result.data.program) {
case "bpf-upgradeable-loader": {
- let programAccount: ProgramAccountInfo;
- let programData: ProgramDataAccountInfo;
+ const parsed = create(info, UpgradeableLoaderAccount);
- if (info.type === "programData") {
- break;
- }
-
- const parsed = create(info, ProgramAccount);
- programAccount = parsed.info;
- const result = (
- await connection.getParsedAccountInfo(parsed.info.programData)
- ).value;
- if (
- result &&
- "parsed" in result.data &&
- result.data.program === "bpf-upgradeable-loader"
- ) {
- const info = create(result.data.parsed, ParsedInfo);
- programData = create(info, ProgramDataAccount).info;
- } else {
- throw new Error(
- `invalid program data account for program: ${pubkey.toBase58()}`
- );
+ // Fetch program data to get program upgradeability info
+ let programData: ProgramDataAccountInfo | undefined;
+ if (parsed.type === "program") {
+ const result = (
+ await connection.getParsedAccountInfo(parsed.info.programData)
+ ).value;
+ if (
+ result &&
+ "parsed" in result.data &&
+ result.data.program === "bpf-upgradeable-loader"
+ ) {
+ const info = create(result.data.parsed, ParsedInfo);
+ programData = create(info, ProgramDataAccount).info;
+ } else {
+ throw new Error(
+ `invalid program data account for program: ${pubkey.toBase58()}`
+ );
+ }
}
data = {
program: result.data.program,
+ parsed,
programData,
- programAccount,
};
break;
diff --git a/explorer/src/utils/sentry.ts b/explorer/src/utils/sentry.ts
index 31c45bb054..a37cb70a7d 100644
--- a/explorer/src/utils/sentry.ts
+++ b/explorer/src/utils/sentry.ts
@@ -7,7 +7,7 @@ type Tags =
| undefined;
export function reportError(err: Error, tags: Tags) {
- console.error(err);
+ console.error(err, err.message);
try {
Sentry.captureException(err, {
tags,
diff --git a/explorer/src/validators/accounts/upgradeable-program.ts b/explorer/src/validators/accounts/upgradeable-program.ts
index b9596984b7..07aa6416fe 100644
--- a/explorer/src/validators/accounts/upgradeable-program.ts
+++ b/explorer/src/validators/accounts/upgradeable-program.ts
@@ -1,6 +1,16 @@
/* 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";
export type ProgramAccountInfo = Infer;
@@ -26,3 +36,48 @@ export const ProgramDataAccount = type({
type: literal("programData"),
info: ProgramDataAccountInfo,
});
+
+export type ProgramBufferAccountInfo = Infer;
+export const ProgramBufferAccountInfo = type({
+ authority: nullable(PublicKeyFromString),
+ // don't care about data yet
+});
+
+export type ProgramBufferAccount = Infer;
+export const ProgramBufferAccount = type({
+ type: literal("buffer"),
+ info: ProgramBufferAccountInfo,
+});
+
+export type UpgradeableLoaderAccount = Infer;
+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}`);
+ }
+ }
+ }
+);