-
Notifications
You must be signed in to change notification settings - Fork 7
Support external verification status tracking in job verification page #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marcocastignoli
wants to merge
13
commits into
staging
Choose a base branch
from
support-external-verification
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+712
−7
Open
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
35c9aa3
Add tracking for external verification
marcocastignoli 6580a1d
fix requests number and support pending status
marcocastignoli 6682a00
Enhance external verification logic and status handling in JobDetails…
marcocastignoli f859613
Add comment to clarify Etherscan API key usage in requestExternalVeri…
marcocastignoli 713bbd6
Add ExternalVerifierStatuses component and integrate into JobDetails
marcocastignoli fab9bf6
Implement maximum amount of retries for ext verifications polling
marcocastignoli 1317111
Enhance countdown in ExternalVerifierStatuses component
marcocastignoli 32aa931
Better naming for isJobFullyCompleted
marcocastignoli 130c790
Update explorer link to include code section and rename link text to …
marcocastignoli 23db2b2
Sort external verifiers by label
marcocastignoli cba3dc0
Add support contract status
marcocastignoli 5095c5d
Refactor verification and contract status display
marcocastignoli 410a8b6
Add support for 'already_verified' status in external verifiers
marcocastignoli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
194 changes: 194 additions & 0 deletions
194
app/components/verification/ExternalVerifierStatuses.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { IoOpenOutline } from "react-icons/io5"; | ||
| import type { ExternalVerifications } from "~/utils/sourcifyApi"; | ||
| import { | ||
| buildStatus, | ||
| requestExternalVerifierStatus, | ||
| type ExternalVerifierKey, | ||
| type ExternalVerifierState, | ||
| type ExternalVerifierStatus, | ||
| } from "~/utils/externalVerifiers"; | ||
|
|
||
| const EXTERNAL_VERIFIER_LABELS: Record<ExternalVerifierKey, string> = { | ||
| etherscan: "Etherscan", | ||
| blockscout: "Blockscout", | ||
| routescan: "Routescan", | ||
| }; | ||
|
|
||
| const STATUS_BADGE_STYLES: Record<ExternalVerifierState, string> = { | ||
| success: "bg-green-100 text-green-800", | ||
| pending: "bg-yellow-100 text-yellow-800", | ||
| error: "bg-red-100 text-red-800", | ||
| unknown: "bg-gray-100 text-gray-800", | ||
| }; | ||
|
|
||
| const STATUS_LABELS: Record<ExternalVerifierState, string> = { | ||
| success: "Verification successful", | ||
| pending: "Verification pending", | ||
| error: "Verification error", | ||
| unknown: "Status unknown", | ||
| }; | ||
|
|
||
| const DEFAULT_REFRESH_SECONDS = 3; | ||
|
|
||
| type ExternalVerifierStatusMap = Partial<Record<ExternalVerifierKey, ExternalVerifierStatus>>; | ||
|
|
||
| interface ExternalVerifierStatusesProps { | ||
| verifications?: ExternalVerifications; | ||
| refreshRateSeconds?: number; | ||
| } | ||
|
|
||
| const ExternalVerifierStatuses = ({ | ||
| verifications, | ||
| refreshRateSeconds = DEFAULT_REFRESH_SECONDS, | ||
| }: ExternalVerifierStatusesProps) => { | ||
| const [externalVerifierStatuses, setExternalVerifierStatuses] = useState<ExternalVerifierStatusMap>({}); | ||
| const externalVerifierStatusesRef = useRef<ExternalVerifierStatusMap>({}); | ||
|
|
||
| useEffect(() => { | ||
| if (!verifications) { | ||
| externalVerifierStatusesRef.current = {}; | ||
| setExternalVerifierStatuses({}); | ||
| return; | ||
| } | ||
|
|
||
| const verifierEntries = Object.entries(verifications).filter( | ||
| ([, data]) => !!data | ||
| ) as Array<[ExternalVerifierKey, ExternalVerifications[ExternalVerifierKey]]>; | ||
|
|
||
| if (verifierEntries.length === 0) { | ||
| externalVerifierStatusesRef.current = {}; | ||
| setExternalVerifierStatuses({}); | ||
| return; | ||
| } | ||
|
|
||
| const activeKeys = new Set<ExternalVerifierKey>(verifierEntries.map(([key]) => key)); | ||
| const preservedStatusesEntries = Object.entries(externalVerifierStatusesRef.current).filter(([key]) => | ||
| activeKeys.has(key as ExternalVerifierKey) | ||
| ); | ||
| const preservedStatuses = Object.fromEntries(preservedStatusesEntries); | ||
| externalVerifierStatusesRef.current = preservedStatuses; | ||
| setExternalVerifierStatuses(preservedStatuses); | ||
|
|
||
| let isCancelled = false; | ||
| let timeoutId: number | null = null; | ||
|
|
||
| const shouldFetchKey = (key: ExternalVerifierKey) => { | ||
| const status = externalVerifierStatusesRef.current[key]; | ||
| if (!status) { | ||
| return true; | ||
| } | ||
| const messageIndicatesPending = | ||
| typeof status.message === "string" && status.message.toLowerCase().includes("pending"); | ||
| return status.state === "pending" || status.state === "unknown" || messageIndicatesPending; | ||
| }; | ||
|
|
||
| const updateStatuses = async (): Promise<boolean> => { | ||
| const keysToFetch = verifierEntries.filter(([key]) => shouldFetchKey(key)); | ||
|
|
||
| if (keysToFetch.length === 0) { | ||
| return verifierEntries.some(([key]) => shouldFetchKey(key)); | ||
| } | ||
|
|
||
| const results = await Promise.all( | ||
| keysToFetch.map(async ([key, data]) => { | ||
| const status = await requestExternalVerifierStatus(key, data); | ||
| return [key, status] as const; | ||
| }) | ||
| ); | ||
|
|
||
| if (isCancelled) { | ||
| return false; | ||
| } | ||
|
|
||
| const next = { ...externalVerifierStatusesRef.current }; | ||
| results.forEach(([key, status]) => { | ||
| next[key] = status; | ||
| }); | ||
|
|
||
| externalVerifierStatusesRef.current = next; | ||
| setExternalVerifierStatuses(next); | ||
|
|
||
| return Object.values(next).some((status) => status.state === "pending" || status.state === "unknown"); | ||
| }; | ||
|
|
||
| const pollStatuses = async () => { | ||
| const hasPending = await updateStatuses(); | ||
| if (isCancelled) return; | ||
| if (hasPending) { | ||
| timeoutId = window.setTimeout(pollStatuses, refreshRateSeconds * 1000); | ||
| } | ||
| }; | ||
|
|
||
| pollStatuses(); | ||
|
|
||
| return () => { | ||
| isCancelled = true; | ||
| if (timeoutId) { | ||
| window.clearTimeout(timeoutId); | ||
| } | ||
| }; | ||
| }, [verifications, refreshRateSeconds]); | ||
|
|
||
| if (!verifications || !Object.values(verifications).some((value) => !!value)) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6 my-6 md:my-8"> | ||
| <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-4 mb-4"> | ||
| <h2 className="text-lg md:text-xl font-bold text-gray-900">Verification on other verifiers</h2> | ||
| <p className="text-sm text-gray-500">Statuses refresh every {refreshRateSeconds} seconds.</p> | ||
| </div> | ||
| <div className="space-y-4"> | ||
| {Object.entries(verifications) | ||
| .filter(([, value]) => !!value) | ||
| .map(([key, verifierData]) => { | ||
| const typedKey = key as ExternalVerifierKey; | ||
| const label = EXTERNAL_VERIFIER_LABELS[typedKey] ?? key; | ||
| const fallbackStatus = verifierData?.error | ||
| ? buildStatus("error", verifierData.error) | ||
| : verifierData?.verificationId | ||
| ? buildStatus("pending", `Awaiting verifier response (${verifierData.verificationId})`) | ||
| : buildStatus("unknown", "Waiting for verifier response"); | ||
| const status = externalVerifierStatuses[typedKey] ?? fallbackStatus; | ||
|
|
||
| return ( | ||
| <div | ||
| key={key} | ||
| className="flex flex-col md:flex-row md:items-center md:justify-between gap-3 border border-gray-200 rounded-lg p-4" | ||
| > | ||
| <div> | ||
| <p className="text-base md:text-lg font-semibold text-gray-900">{label}</p> | ||
| <p className="text-sm text-gray-600 break-words">{status.message}</p> | ||
| {verifierData?.verificationId && ( | ||
| <p className="text-xs text-gray-500 mt-1 break-all">Verification ID: {verifierData.verificationId}</p> | ||
| )} | ||
| {verifierData?.explorerUrl && ( | ||
| <a | ||
| href={verifierData.explorerUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-2 text-sm text-cerulean-blue-600 hover:text-cerulean-blue-800 mt-2" | ||
| > | ||
| View on explorer | ||
| <IoOpenOutline className="w-4 h-4" /> | ||
| </a> | ||
| )} | ||
| </div> | ||
| <span | ||
| className={`inline-flex items-center justify-center px-3 py-1 rounded-full text-xs font-semibold ${ | ||
| STATUS_BADGE_STYLES[status.state] | ||
| }`} | ||
| > | ||
| {STATUS_LABELS[status.state]} | ||
| </span> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ExternalVerifierStatuses; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,23 @@ | ||
| import type { Route } from "./+types/jobs.$jobId"; | ||
| import { useParams } from "react-router"; | ||
| import { useEffect, useState } from "react"; | ||
| import { IoCheckmarkDoneCircle, IoCheckmarkCircle, IoOpenOutline, IoClose, IoBugOutline } from "react-icons/io5"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { IoOpenOutline, IoClose, IoBugOutline } from "react-icons/io5"; | ||
| import { TbArrowsDiff } from "react-icons/tb"; | ||
| import { useChains } from "../contexts/ChainsContext"; | ||
| import { getChainName } from "../utils/chains"; | ||
| import { getVerificationJobStatus, type VerificationJobStatus } from "../utils/sourcifyApi"; | ||
| import { | ||
| getVerificationJobStatus, | ||
| type VerificationJobStatus, | ||
| type ExternalVerifications, | ||
| } from "../utils/sourcifyApi"; | ||
| import PageLayout from "../components/PageLayout"; | ||
| import BytecodeDiffModal from "../components/verification/BytecodeDiffModal"; | ||
| import MatchBadge from "../components/verification/MatchBadge"; | ||
| import ExternalVerifierStatuses from "../components/verification/ExternalVerifierStatuses"; | ||
| import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react"; | ||
| import { useServerConfig } from "../contexts/ServerConfigContext"; | ||
| import { generateGitHubIssueUrl } from "../utils/githubIssue"; | ||
| import { type ExternalVerifierKey } from "~/utils/externalVerifiers"; | ||
|
|
||
| export function meta({ }: Route.MetaArgs) { | ||
| const { jobId } = useParams<{ jobId: string }>(); | ||
|
|
@@ -27,6 +33,13 @@ export function meta({ }: Route.MetaArgs) { | |
| } | ||
|
|
||
| const DEFAULT_COUNTDOWN = 5; | ||
| const MAX_EXTERNAL_VERIFICATION_RETRIES = 3; | ||
|
|
||
| const REQUIRED_EXTERNAL_VERIFIER_KEYS: ExternalVerifierKey[] = ["etherscan", "blockscout", "routescan"]; | ||
|
|
||
| const hasAllRequiredExternalVerifications = (verifications?: ExternalVerifications) => { | ||
| return REQUIRED_EXTERNAL_VERIFIER_KEYS.every((key) => verifications?.[key] != null); | ||
| }; | ||
|
|
||
| export default function JobDetails() { | ||
| const { jobId } = useParams<{ jobId: string }>(); | ||
|
|
@@ -46,6 +59,12 @@ export default function JobDetails() { | |
| const [expandedErrors, setExpandedErrors] = useState<Set<number>>(new Set()); | ||
| const [expandedModalErrors, setExpandedModalErrors] = useState<Set<number>>(new Set()); | ||
| const { serverUrl } = useServerConfig(); | ||
| const hasExternalVerificationData = hasAllRequiredExternalVerifications(jobData?.externalVerifications); | ||
| const isJobFullyCompleted = Boolean(jobData?.isJobCompleted) && hasExternalVerificationData; | ||
marcocastignoli marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const missingExternalVerificationData = Boolean(jobData?.isJobCompleted && !hasExternalVerificationData); | ||
| const externalVerificationRetryCountRef = useRef(0); | ||
| const hasReachedExternalVerificationRetryLimit = | ||
| missingExternalVerificationData && externalVerificationRetryCountRef.current >= MAX_EXTERNAL_VERIFICATION_RETRIES; | ||
|
|
||
| const fetchJobStatus = async () => { | ||
| if (!jobId) return; | ||
|
|
@@ -62,18 +81,42 @@ export default function JobDetails() { | |
| } | ||
| }; | ||
|
|
||
| // Initial fetch | ||
| const initialFetchJobIdRef = useRef<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| fetchJobStatus(); | ||
| if (!missingExternalVerificationData) { | ||
| externalVerificationRetryCountRef.current = 0; | ||
| } | ||
| }, [missingExternalVerificationData]); | ||
|
|
||
| useEffect(() => { | ||
| externalVerificationRetryCountRef.current = 0; | ||
| }, [jobId]); | ||
|
|
||
| useEffect(() => { | ||
| if (!jobId) return; | ||
| const alreadyFetched = initialFetchJobIdRef.current === jobId; | ||
| initialFetchJobIdRef.current = jobId; | ||
| if (!alreadyFetched) { | ||
| fetchJobStatus(); | ||
| } | ||
| }, [jobId]); | ||
|
|
||
| // Auto-refresh for pending jobs with countdown | ||
| useEffect(() => { | ||
| if (!jobData || jobData.isJobCompleted) return; | ||
| // Old jobs don't have external verifications, so we need a mechanism to stop retrying if the value is not set | ||
| if (!jobData || isJobFullyCompleted || hasReachedExternalVerificationRetryLimit) return; | ||
|
|
||
|
Comment on lines
108
to
110
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name isJobFullyCompleted is a bit misleading here. I spend a good 10 mins trying to understand how this variable relates to the comment above. Finally realized it also has a check if the externalVerification field exists, which I can't understand from the variable name |
||
| const interval = setInterval(() => { | ||
| setCountdown((prev) => { | ||
| if (prev <= 1) { | ||
| if (missingExternalVerificationData) { | ||
| if (externalVerificationRetryCountRef.current >= MAX_EXTERNAL_VERIFICATION_RETRIES) { | ||
| clearInterval(interval); | ||
| return DEFAULT_COUNTDOWN; | ||
| } | ||
| externalVerificationRetryCountRef.current += 1; | ||
| } | ||
| fetchJobStatus(); | ||
| return DEFAULT_COUNTDOWN; // Reset countdown | ||
| } | ||
|
|
@@ -82,7 +125,7 @@ export default function JobDetails() { | |
| }, 1000); | ||
|
|
||
| return () => clearInterval(interval); | ||
| }, [jobData]); | ||
| }, [jobData, isJobFullyCompleted, missingExternalVerificationData, hasReachedExternalVerificationRetryLimit]); | ||
|
|
||
| const handleRefresh = () => { | ||
| fetchJobStatus(); | ||
|
|
@@ -328,6 +371,8 @@ export default function JobDetails() { | |
| </div> | ||
| )} | ||
|
|
||
| <ExternalVerifierStatuses verifications={jobData.externalVerifications} /> | ||
|
|
||
| {/* Error Details */} | ||
| {jobData.error && ( | ||
| <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6"> | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just say "Other Verifiers" in the title and add a small subtitle saying "Sourcify automatically shares contracts with other known verifiers"