Add verified/unverified badge to Program Account view (#22825)
* Add verified/unverified badge to Program Account view * Generalize to any number of build verification providers
This commit is contained in:
@ -15,6 +15,9 @@ import { useCluster } from "providers/cluster";
|
|||||||
import { ErrorCard } from "components/common/ErrorCard";
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
|
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
|
||||||
import { Downloadable } from "components/common/Downloadable";
|
import { Downloadable } from "components/common/Downloadable";
|
||||||
|
import { CheckingBadge, VerifiedBadge } from "components/common/VerifiedBadge";
|
||||||
|
import { InfoTooltip } from "components/common/InfoTooltip";
|
||||||
|
import { useVerifiableBuilds } from "utils/program-verification";
|
||||||
|
|
||||||
export function UpgradeableLoaderAccountSection({
|
export function UpgradeableLoaderAccountSection({
|
||||||
account,
|
account,
|
||||||
@ -72,6 +75,7 @@ export function UpgradeableProgramSection({
|
|||||||
const refresh = useFetchAccountInfo();
|
const refresh = useFetchAccountInfo();
|
||||||
const { cluster } = useCluster();
|
const { cluster } = useCluster();
|
||||||
const label = addressLabel(account.pubkey.toBase58(), cluster);
|
const label = addressLabel(account.pubkey.toBase58(), cluster);
|
||||||
|
const { loading, verifiableBuilds } = useVerifiableBuilds(account.pubkey);
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@ -122,6 +126,22 @@ export function UpgradeableProgramSection({
|
|||||||
{programData.authority !== null ? "Yes" : "No"}
|
{programData.authority !== null ? "Yes" : "No"}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<LastVerifiedBuildLabel />
|
||||||
|
</td>
|
||||||
|
<td className="text-lg-end">
|
||||||
|
{loading ? (
|
||||||
|
<CheckingBadge />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{verifiableBuilds.map((b) => (
|
||||||
|
<VerifiedBadge verifiableBuild={b} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Last Deployed Slot</td>
|
<td>Last Deployed Slot</td>
|
||||||
<td className="text-lg-end">
|
<td className="text-lg-end">
|
||||||
@ -141,6 +161,14 @@ export function UpgradeableProgramSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LastVerifiedBuildLabel() {
|
||||||
|
return (
|
||||||
|
<InfoTooltip text="Indicates whether the program currently deployed on-chain is verified to match the associated published source code, when it is available.">
|
||||||
|
Verifiable Build Status
|
||||||
|
</InfoTooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function UpgradeableProgramDataSection({
|
export function UpgradeableProgramDataSection({
|
||||||
account,
|
account,
|
||||||
programData,
|
programData,
|
||||||
|
38
explorer/src/components/common/VerifiedBadge.tsx
Normal file
38
explorer/src/components/common/VerifiedBadge.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { VerifiableBuild } from "utils/program-verification";
|
||||||
|
|
||||||
|
export function VerifiedBadge({
|
||||||
|
verifiableBuild,
|
||||||
|
}: {
|
||||||
|
verifiableBuild: VerifiableBuild;
|
||||||
|
}) {
|
||||||
|
if (verifiableBuild && verifiableBuild.verified_slot) {
|
||||||
|
return (
|
||||||
|
<h3 className="mb-0">
|
||||||
|
<a
|
||||||
|
className="c-pointer badge bg-success-soft rank"
|
||||||
|
href={verifiableBuild.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{verifiableBuild.label}: Verified
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<h3 className="mb-0">
|
||||||
|
<span className="badge bg-warning-soft rank">
|
||||||
|
{verifiableBuild.label}: Unverified
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckingBadge() {
|
||||||
|
return (
|
||||||
|
<h3 className="mb-0">
|
||||||
|
<span className="badge bg-dark rank">Checking</span>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
103
explorer/src/utils/program-verification.tsx
Normal file
103
explorer/src/utils/program-verification.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export type VerifiableBuild =
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
id: number;
|
||||||
|
verified_slot: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
verified_slot: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useVerifiableBuilds(programAddress: PublicKey) {
|
||||||
|
const { loading: loadingAnchor, verifiableBuild: verifiedBuildAnchor } =
|
||||||
|
useAnchorVerifiableBuild(programAddress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: loadingAnchor,
|
||||||
|
verifiableBuilds: [verifiedBuildAnchor],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANCHOR
|
||||||
|
|
||||||
|
const defaultAnchorBuild = {
|
||||||
|
label: "Anchor",
|
||||||
|
verified_slot: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAnchorVerifiableBuild(programAddress: PublicKey) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [verifiableBuild, setVerifiableBuild] =
|
||||||
|
useState<VerifiableBuild>(defaultAnchorBuild);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
getAnchorVerifiableBuild(programAddress)
|
||||||
|
.then(setVerifiableBuild)
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
setVerifiableBuild(defaultAnchorBuild);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [programAddress, setVerifiableBuild, setLoading]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
verifiableBuild,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnchorBuild {
|
||||||
|
aborted: boolean;
|
||||||
|
address: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
descriptor: string[];
|
||||||
|
docker: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
sha256: string;
|
||||||
|
upgrade_authority: string;
|
||||||
|
verified: string;
|
||||||
|
verified_slot: number;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a verified build from the anchor registry. null if no such
|
||||||
|
* verified build exists, e.g., if the program has been upgraded since the
|
||||||
|
* last verified build.
|
||||||
|
*/
|
||||||
|
export async function getAnchorVerifiableBuild(
|
||||||
|
programId: PublicKey,
|
||||||
|
limit: number = 5
|
||||||
|
): Promise<VerifiableBuild> {
|
||||||
|
const url = `https://anchor.projectserum.com/api/v0/program/${programId.toString()}/latest?limit=${limit}`;
|
||||||
|
const latestBuildsResp = await fetch(url);
|
||||||
|
|
||||||
|
// Filter out all non successful builds.
|
||||||
|
const latestBuilds = (await latestBuildsResp.json()).filter(
|
||||||
|
(b: AnchorBuild) =>
|
||||||
|
!b.aborted && b.state === "Built" && b.verified === "Verified"
|
||||||
|
) as AnchorBuild[];
|
||||||
|
|
||||||
|
if (latestBuilds.length === 0) {
|
||||||
|
return defaultAnchorBuild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest build.
|
||||||
|
const { verified_slot, id } = latestBuilds[0];
|
||||||
|
return {
|
||||||
|
...defaultAnchorBuild,
|
||||||
|
verified_slot,
|
||||||
|
id,
|
||||||
|
url: `https://anchor.projectserum.com/build/${id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// END ANCHOR
|
Reference in New Issue
Block a user