diff --git a/apps/contract-verification/src/app/ContractVerificationPluginClient.ts b/apps/contract-verification/src/app/ContractVerificationPluginClient.ts index 498ab5d75f4..f78b9e6b6a1 100644 --- a/apps/contract-verification/src/app/ContractVerificationPluginClient.ts +++ b/apps/contract-verification/src/app/ContractVerificationPluginClient.ts @@ -13,7 +13,7 @@ export class ContractVerificationPluginClient extends PluginClient { constructor() { super() - this.methods = ['lookupAndSave', 'verifyOnDeploy'] + this.methods = ['lookupAndSave', 'verifyOnDeploy', 'isVerificationSupportedForChain'] this.internalEvents = new EventManager() createClient(this) this.onload() @@ -126,24 +126,15 @@ export class ContractVerificationPluginClient extends PluginClient { const chainSettings = mergeChainSettingsWithDefaults(chainId, userSettings) const verificationPromises = [] + const verifiers: VerifierIdentifier[] = ['Sourcify', 'Etherscan', 'Blockscout', 'Routescan'] - if (validConfiguration(chainSettings, 'Sourcify')) { - verificationPromises.push(this._verifyWithProvider('Sourcify', submittedContract, compilerAbstract, chainId, chainSettings)) - } - - if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.toLowerCase().includes('routescan'))) { - verificationPromises.push(this._verifyWithProvider('Routescan', submittedContract, compilerAbstract, chainId, chainSettings)) - } - - if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.url.includes('blockscout'))) { - verificationPromises.push(this._verifyWithProvider('Blockscout', submittedContract, compilerAbstract, chainId, chainSettings)) - } - - if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.includes('etherscan'))) { - if (etherscanApiKey) { - verificationPromises.push(this._verifyWithProvider('Etherscan', submittedContract, compilerAbstract, chainId, chainSettings)) - } else { - await this.call('terminal', 'log', { type: 'warn', value: 'Etherscan verification skipped: API key not found in global Settings.' }) + for (const verifier of verifiers) { + if (validConfiguration(chainSettings, verifier)) { + if (verifier === 'Etherscan' && !etherscanApiKey) { + this.call('terminal', 'log', { type: 'warn', value: 'Etherscan verification skipped: API key not provided for auto-verification.' }) + continue + } + verificationPromises.push(this._verifyWithProvider(verifier, submittedContract, compilerAbstract, chainId, chainSettings)) } } @@ -158,6 +149,23 @@ export class ContractVerificationPluginClient extends PluginClient { } } + async isVerificationSupportedForChain(chainId: string): Promise { + try { + const userSettings = this.getUserSettingsFromLocalStorage() + const chainSettings = mergeChainSettingsWithDefaults(chainId, userSettings) + + for (const verifierId of VERIFIERS) { + if (validConfiguration(chainSettings, verifierId as VerifierIdentifier)) { + return true + } + } + return false + } catch (e) { + console.error(e) + return false + } + } + private _verifyWithProvider = async ( providerName: VerifierIdentifier, submittedContract: SubmittedContract, diff --git a/apps/contract-verification/src/app/components/AccordionReceipt.tsx b/apps/contract-verification/src/app/components/AccordionReceipt.tsx index d8cbb4ab68b..5c336d95e1d 100644 --- a/apps/contract-verification/src/app/components/AccordionReceipt.tsx +++ b/apps/contract-verification/src/app/components/AccordionReceipt.tsx @@ -1,8 +1,11 @@ import React, { useMemo } from 'react' -import { SubmittedContract, VerificationReceipt } from '../types' +import { SubmittedContract, VerificationReceipt, VerificationStatus } from '../types' import { shortenAddress, CustomTooltip } from '@remix-ui/helper' import { AppContext } from '../AppContext' import { CopyToClipboard } from '@remix-ui/clipboard' +import { getVerifier } from '../Verifiers' +import { CompilerAbstract } from '@remix-project/remix-solidity' +import { mergeChainSettingsWithDefaults } from '../utils' interface AccordionReceiptProps { contract: SubmittedContract @@ -10,7 +13,7 @@ interface AccordionReceiptProps { } export const AccordionReceipt: React.FC = ({ contract, index }) => { - const { chains } = React.useContext(AppContext) + const { chains, settings, compilationOutput, setSubmittedContracts } = React.useContext(AppContext) const [expanded, setExpanded] = React.useState(false) @@ -25,6 +28,107 @@ export const AccordionReceipt: React.FC = ({ contract, in setExpanded(!expanded) } + const isRetryAvailable = useMemo(() => { + if (!compilationOutput) return false + + const compilerAbstract = Object.values(compilationOutput || {}).find( + (abstract: CompilerAbstract) => + abstract.data.contracts[contract.filePath] && + abstract.data.contracts[contract.filePath][contract.contractName] + ) + return !!compilerAbstract + }, [compilationOutput, contract.filePath, contract.contractName]) + + const handleRetryVerification = async (receipt: VerificationReceipt) => { + setSubmittedContracts(prev => { + const currentContract = prev[contract.id] + if (!currentContract) return prev + + return { + ...prev, + [contract.id]: { + ...currentContract, + receipts: (currentContract.receipts || []).map(r => + r === receipt ? { ...r, status: 'pending' as VerificationStatus, message: 'Retrying...' } : r + ), + proxyReceipts: (currentContract.proxyReceipts || []).map(r => + r === receipt ? { ...r, status: 'pending' as VerificationStatus, message: 'Retrying...' } : r + ) + } + } + }) + + try { + const chainSettings = mergeChainSettingsWithDefaults(contract.chainId, settings) + const verifierSettings = chainSettings.verifiers[receipt.verifierInfo.name] + + if (!verifierSettings || !verifierSettings.apiUrl) { + throw new Error('Verifier settings or apiUrl not found.') + } + + const verifier = getVerifier(receipt.verifierInfo.name, verifierSettings) + + let response + + if (receipt.isProxyReceipt) { + if (!verifier.verifyProxy) { + throw new Error(`Proxy verification not supported by ${receipt.verifierInfo.name}`) + } + response = await verifier.verifyProxy(contract) + } else { + const compilerAbstract = Object.values(compilationOutput || {}).find( + (abstract: CompilerAbstract) => + abstract.data.contracts[contract.filePath] && + abstract.data.contracts[contract.filePath][contract.contractName] + ) + + if (!compilerAbstract) { + const userMessage = `Compilation output not found. Please re-compile the contract file ('${contract.filePath}') and try again.` + console.error(`[Retry] ${userMessage}`) + throw new Error(userMessage) + } + + response = await verifier.verify(contract, compilerAbstract) + } + + setSubmittedContracts(prev => { + const currentContract = prev[contract.id] + if (!currentContract) return prev + return { + ...prev, + [contract.id]: { + ...currentContract, + receipts: (currentContract.receipts || []).map(r => + r === receipt ? { ...r, ...response, receiptId: response.receiptId || undefined, status: response.status, message: response.message } : r + ), + proxyReceipts: (currentContract.proxyReceipts || []).map(r => + r === receipt ? { ...r, ...response, receiptId: response.receiptId || undefined, status: response.status, message: response.message } : r + ) + } + } + }) + + } catch (e) { + console.error(e) + setSubmittedContracts(prev => { + const currentContract = prev[contract.id] + if (!currentContract) return prev + return { + ...prev, + [contract.id]: { + ...currentContract, + receipts: (currentContract.receipts || []).map(r => + r === receipt ? { ...r, status: 'failed' as VerificationStatus, message: e.message } : r + ), + proxyReceipts: (currentContract.proxyReceipts || []).map(r => + r === receipt ? { ...r, status: 'failed' as VerificationStatus, message: e.message } : r + ) + } + } + }) + } + } + return (
@@ -65,7 +169,7 @@ export const AccordionReceipt: React.FC = ({ contract, in
Verified at: - +
{hasProxy && ( @@ -79,7 +183,7 @@ export const AccordionReceipt: React.FC = ({ contract, in
Proxy verified at: - +
)} @@ -88,13 +192,17 @@ export const AccordionReceipt: React.FC = ({ contract, in ) } -const ReceiptsBody = ({ receipts }: { receipts: VerificationReceipt[] }) => { +const ReceiptsBody = ({ receipts, handleRetry, isRetryAvailable }: { + receipts: VerificationReceipt[], + handleRetry: (receipt: VerificationReceipt) => void, + isRetryAvailable: boolean +}) => { return (
    {receipts.map((receipt) => (
  • { : receipt.status === 'failed' ? : - ['pending', 'awaiting implementation verification'].includes(receipt.status) ? + ['pending', 'awaiting implementation verification', 'Retrying...'].includes(receipt.status) ? : } + {receipt.status === 'failed' && isRetryAvailable && ( + + + + )}
    diff --git a/apps/contract-verification/src/app/utils/default-apis.json b/apps/contract-verification/src/app/utils/default-apis.json index dc92cf72d42..914d99b8dd7 100644 --- a/apps/contract-verification/src/app/utils/default-apis.json +++ b/apps/contract-verification/src/app/utils/default-apis.json @@ -51,6 +51,9 @@ "1101": { "explorerUrl": "https://zkevm.polygonscan.com" }, + "59141": { + "explorerUrl": "https://sepolia.lineascan.build" + }, "59144": { "explorerUrl": "https://lineascan.build" }, diff --git a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts index 022ea7323f0..443e4a58a13 100644 --- a/libs/remix-ui/run-tab/src/lib/actions/deploy.ts +++ b/libs/remix-ui/run-tab/src/lib/actions/deploy.ts @@ -191,7 +191,7 @@ export const createInstance = async ( const currentChain = allChains.find(chain => chain.chainId === currentChainId) if (!currentChain) { - const errorMsg = `The current network (Chain ID: ${currentChainId}) is not supported for verification via this plugin. Please switch to a supported network like Sepolia or Mainnet.` + const errorMsg = `Could not find chain data for Chain ID: ${currentChainId}. Verification cannot proceed.` const errorLog = logBuilder(errorMsg) terminalLogger(plugin, errorLog) return diff --git a/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx b/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx index 765ad7c3d86..d847e3edc48 100644 --- a/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx @@ -49,21 +49,45 @@ export function ContractDropdownUI(props: ContractDropdownProps) { useEffect(() => { const checkSupport = async () => { - if (props.plugin) { - const supportedChain = await getSupportedChain(props.plugin) - const isSupported = !!supportedChain - setNetworkSupported(isSupported) - - if (isSupported) { - const saved = window.localStorage.getItem('deploy-verify-contract-checked') - setVerifyChecked(saved !== null ? JSON.parse(saved) : true) - } else { + if (props.plugin && props.networkName) { + try { + const supportedChain = await getSupportedChain(props.plugin) + const chainExistsInList = !!supportedChain + + let isConfigValid = false + if (chainExistsInList) { + const status = props.plugin.blockchain.getCurrentNetworkStatus() + const currentChainId = status?.network?.id?.toString() + if (currentChainId) { + isConfigValid = await props.plugin.call( + 'contract-verification', + 'isVerificationSupportedForChain', + currentChainId + ) + } + } + + const isSupported = chainExistsInList && isConfigValid + setNetworkSupported(isSupported) + + if (isSupported) { + const saved = window.localStorage.getItem('deploy-verify-contract-checked') + setVerifyChecked(saved !== null ? JSON.parse(saved) : true) + } else { + setVerifyChecked(false) + } + } catch (e) { + console.error("Failed to check verification support:", e) + setNetworkSupported(false) setVerifyChecked(false) } + } else { + setNetworkSupported(false) + setVerifyChecked(false) } - }; + } checkSupport() - }, [props.networkName]) + }, [props.networkName, props.plugin]) useEffect(() => { enableContractNames(Object.keys(props.contracts.contractList).length > 0)