Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -158,6 +149,23 @@ export class ContractVerificationPluginClient extends PluginClient {
}
}

async isVerificationSupportedForChain(chainId: string): Promise<boolean> {
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,
Expand Down
133 changes: 126 additions & 7 deletions apps/contract-verification/src/app/components/AccordionReceipt.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
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
index: number
}

export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, index }) => {
const { chains } = React.useContext(AppContext)
const { chains, settings, compilationOutput, setSubmittedContracts } = React.useContext(AppContext)

const [expanded, setExpanded] = React.useState(false)

Expand All @@ -25,6 +28,107 @@ export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ 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 (
<div className={`${expanded ? 'bg-light' : 'border-bottom '}`}>
<div className="d-flex flex-row align-items-center">
Expand Down Expand Up @@ -65,7 +169,7 @@ export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, in

<div>
<span className="fw-bold">Verified at: </span>
<ReceiptsBody receipts={contract.receipts} />
<ReceiptsBody receipts={contract.receipts} handleRetry={handleRetryVerification} isRetryAvailable={isRetryAvailable} />
</div>

{hasProxy && (
Expand All @@ -79,7 +183,7 @@ export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, in
</div>
<div>
<span className="fw-bold">Proxy verified at: </span>
<ReceiptsBody receipts={contract.proxyReceipts} />
<ReceiptsBody receipts={contract.proxyReceipts} handleRetry={handleRetryVerification} isRetryAvailable={isRetryAvailable} />
</div>
</>
)}
Expand All @@ -88,13 +192,17 @@ export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, in
)
}

const ReceiptsBody = ({ receipts }: { receipts: VerificationReceipt[] }) => {
const ReceiptsBody = ({ receipts, handleRetry, isRetryAvailable }: {
receipts: VerificationReceipt[],
handleRetry: (receipt: VerificationReceipt) => void,
isRetryAvailable: boolean
}) => {
return (
<ul className="list-group">
{receipts.map((receipt) => (
<li
key={`${receipt.contractId}-${receipt.verifierInfo.name}${receipt.isProxyReceipt ? '-proxy' : ''}-${receipt.receiptId}`}
className="list-group-item d-flex flex-row align-items-center"
className="list-group-item d-flex flex-row align-items-baseline"
>
<CustomTooltip
placement="top"
Expand All @@ -109,12 +217,23 @@ const ReceiptsBody = ({ receipts }: { receipts: VerificationReceipt[] }) => {
<i className="fas fa-check-double text-success px-1"></i> :
receipt.status === 'failed' ?
<i className="fas fa-xmark text-warning px-1"></i> :
['pending', 'awaiting implementation verification'].includes(receipt.status) ?
['pending', 'awaiting implementation verification', 'Retrying...'].includes(receipt.status) ?
<i className="fas fa-spinner fa-spin px-1"></i> :
<i className="fas fa-question px-1"></i>
}
</span>
</CustomTooltip>
{receipt.status === 'failed' && isRetryAvailable && (
<CustomTooltip placement="top" tooltipText="Retry Verification">
<button
className="btn btn-sm p-0 me-2"
style={{ border: 'none', background: 'none', color: 'var(--primary)' }}
onClick={() => handleRetry(receipt)}
>
<i className="fas fa-redo" style={{ fontSize: '0.6rem' }}></i>
</button>
</CustomTooltip>
)}
<div className="d-flex flex-row w-100 justify-content-between">
<div>
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipText={`API: ${receipt.verifierInfo.apiUrl}`}>
Expand Down
3 changes: 3 additions & 0 deletions apps/contract-verification/src/app/utils/default-apis.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"1101": {
"explorerUrl": "https://zkevm.polygonscan.com"
},
"59141": {
"explorerUrl": "https://sepolia.lineascan.build"
},
"59144": {
"explorerUrl": "https://lineascan.build"
},
Expand Down
2 changes: 1 addition & 1 deletion libs/remix-ui/run-tab/src/lib/actions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 35 additions & 11 deletions libs/remix-ui/run-tab/src/lib/components/contractDropdownUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down