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}`); + } + } + } +);