Make VerifyView functional with modified Verifier classes

pull/5285/head
Manuel Wedler 4 months ago committed by Aniket
parent b1972669ec
commit 6c98d1a424
  1. 56
      apps/contract-verification/src/app/app.tsx
  2. 2
      apps/contract-verification/src/app/components/AccordionReceipt.tsx
  3. 2
      apps/contract-verification/src/app/components/NavMenu.tsx
  4. 13
      apps/contract-verification/src/app/hooks/useLocalStorage.tsx
  5. 7
      apps/contract-verification/src/app/types/VerificationTypes.ts
  6. 6
      apps/contract-verification/src/app/types/defaults.ts
  7. 15
      apps/contract-verification/src/app/views/LookupView.tsx
  8. 9
      apps/contract-verification/src/app/views/ReceiptsView.tsx
  9. 118
      apps/contract-verification/src/app/views/VerifyView.tsx
  10. 104
      apps/contract-verification/src/app/views/example.js

@ -1,14 +1,15 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { ContractVerificationPluginClient } from './ContractVerificationPluginClient'
import { AppContext } from './AppContext'
import DisplayRoutes from './routes'
import { ContractVerificationSettings, ThemeType, Chain, SubmittedContracts } from './types'
import { ContractVerificationSettings, ThemeType, Chain, SubmittedContracts, VerificationReceipt, mergeChainSettingsWithDefaults } from './types'
import './App.css'
import { CompilerAbstract } from '@remix-project/remix-solidity'
import { useLocalStorage } from './hooks/useLocalStorage'
import { getVerifier } from './Verifiers'
const plugin = new ContractVerificationPluginClient()
@ -19,6 +20,7 @@ const App = () => {
// TODO: Types for chains
const [chains, setChains] = useState<Chain[]>([]) // State to hold the chains data
const [compilationOutput, setCompilationOutput] = useState<{ [key: string]: CompilerAbstract } | undefined>()
const timer = useRef(null)
useEffect(() => {
// TODO: Fix 'compilationFinished' event types. The interface is outdated at https://github.com/ethereum/remix-plugin/blob/master/packages/api/src/lib/compiler/api.ts. It does not include data, input, or version. See the current parameters: https://github.com/ethereum/remix-project/blob/9f6c5be882453a555055f07171701459e4ae88a4/libs/remix-solidity/src/compiler/compiler.ts#L189
@ -51,6 +53,56 @@ const App = () => {
}
}, [])
// Poll status of pending receipts frequently
useEffect(() => {
const getPendingReceipts = (submissions: SubmittedContracts) => {
const pendingReceipts: VerificationReceipt[] = []
// Check statuses of receipts
for (const submission of Object.values(submissions)) {
for (const receipt of submission.receipts) {
if (receipt.status === 'pending' && receipt.verifierInfo.name !== 'Sourcify') {
pendingReceipts.push(receipt)
}
}
}
return pendingReceipts
}
let pendingReceipts = getPendingReceipts(submittedContracts)
if (pendingReceipts.length > 0) {
if (timer.current) {
clearInterval(timer.current)
timer.current = null
}
timer.current = setInterval(async () => {
const changedSubmittedContracts = { ...submittedContracts }
for (const receipt of pendingReceipts) {
await new Promise((resolve) => setTimeout(resolve, 500)) // avoid api rate limit exceed.
const { verifierInfo, receiptId } = receipt
if (receiptId) {
const contract = changedSubmittedContracts[receipt.contractId]
const chainSettings = mergeChainSettingsWithDefaults(contract.chainId, settings)
const verifierSettings = chainSettings.verifiers[verifierInfo.name]
// In case the user overwrites the API later, prefer the one stored in localStorage
const verifier = getVerifier(verifierInfo.name, { ...verifierSettings, apiUrl: verifierInfo.apiUrl })
receipt.status = await verifier.checkVerificationStatus(receiptId)
}
}
pendingReceipts = getPendingReceipts(changedSubmittedContracts)
if (timer.current && pendingReceipts.length === 0) {
clearInterval(timer.current)
timer.current = null
}
setSubmittedContracts((prev) => Object.assign({}, prev, changedSubmittedContracts))
}, 10000)
}
})
return (
<AppContext.Provider value={{ themeType, setThemeType, settings, setSettings, chains, compilationOutput, submittedContracts, setSubmittedContracts }}>
<DisplayRoutes />

@ -99,7 +99,7 @@ const ReceiptsBody = ({ contract }: { contract: SubmittedContract }) => {
</thead>
<tbody>
{contract.receipts.map((receipt) => (
<tr key={receipt.receiptId}>
<tr key={`${contract.id}-${receipt.verifierInfo.name}`}>
<td>{receipt.verifierInfo.name}</td>
<td>{receipt.verifierInfo.apiUrl}</td>
<td>{receipt.status}</td>

@ -15,7 +15,7 @@ const NavItem: React.FC<NavItemProps> = ({ to, icon, title }) => {
className={({ isActive }) => 'p-2 text-decoration-none ' + (isActive ? 'bg-primary text-white' : 'bg-secondary')}
// state={from}
>
<span className="d-flex flex-column align-items-center justify-content-center" style={{ width: '64px' }}>
<span className="d-flex flex-column align-items-center justify-content-center" style={{ width: '60px' }}>
<span>{icon}</span>
<span>{title}</span>
</span>

@ -1,6 +1,7 @@
import { useState } from 'react'
import { type Dispatch, type SetStateAction, useState } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>]
{
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
@ -18,12 +19,14 @@ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T)
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T) => {
const setValue = (value: SetStateAction<T>) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(value)
setStoredValue(valueToStore)
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(value))
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
// A more advanced implementation would handle the error case
console.error(error)

@ -28,8 +28,9 @@ export interface VerifierInfo {
export interface VerificationReceipt {
receiptId?: string
verifierInfo: VerifierInfo
status: VerificationStatus | null
status: VerificationStatus
message?: string
contractId: string
}
export interface SubmittedContract {
@ -53,7 +54,9 @@ export interface SubmittedProxyContract {
// This and all nested subtypes should be pure interfaces, so they can be converted to JSON easily
export interface SubmittedContracts {
[id: string]: SubmittedContract | SubmittedProxyContract
// TODO implement Proxy verification
// [id: string]: SubmittedContract | SubmittedProxyContract
[id: string]: SubmittedContract
}
export function isProxy(contract: SubmittedContract | SubmittedProxyContract): contract is SubmittedProxyContract {

@ -1,5 +1,5 @@
import type { ChainSettings, ContractVerificationSettings, SettingsForVerifier, VerifierSettings } from './SettingsTypes'
import { VERIFIERS } from './VerificationTypes'
import { VerifierIdentifier, VERIFIERS } from './VerificationTypes'
const DEFAULTS: SettingsForVerifier = {
Sourcify: {
@ -40,3 +40,7 @@ export function mergeChainSettingsWithDefaults(chainId: string, userSettings: Co
}
return { verifiers }
}
export function validConfiguration(chainSettings: ChainSettings | undefined, verifierId: VerifierIdentifier) {
return !!chainSettings && !!chainSettings.verifiers[verifierId]?.apiUrl && (verifierId !== 'Etherscan' || !!chainSettings.verifiers[verifierId]?.apiKey)
}

@ -1,6 +1,6 @@
import { useContext, useState } from 'react'
import { SearchableChainDropdown, ContractAddressInput } from '../components'
import { LookupResponse, mergeChainSettingsWithDefaults, VerifierIdentifier, VERIFIERS, type Chain } from '../types'
import { LookupResponse, mergeChainSettingsWithDefaults, validConfiguration, VerifierIdentifier, VERIFIERS, type Chain } from '../types'
import { AppContext } from '../AppContext'
import { CustomTooltip } from '@remix-ui/helper'
import { getVerifier } from '../Verifiers'
@ -15,14 +15,17 @@ export const LookupView = () => {
const chainSettings = selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined
const handleLookup = () => {
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain
const handleLookup = (e) => {
e.preventDefault()
for (const verifierId of VERIFIERS) {
if (!chainSettings.verifiers[verifierId]?.apiUrl || (verifierId === 'Etherscan' && !chainSettings.verifiers[verifierId]?.apiKey)) {
if (!validConfiguration(chainSettings, verifierId)) {
continue
}
setLoadingVerifiers((prev) => ({ ...prev, [verifierId]: true }))
console.log(chainSettings.verifiers[verifierId])
const verifier = getVerifier(verifierId, chainSettings.verifiers[verifierId])
verifier
.lookup(contractAddress, selectedChain.chainId.toString())
@ -44,14 +47,14 @@ export const LookupView = () => {
<ContractAddressInput label="Contract Address" id="contract-address" contractAddress={contractAddress} setContractAddress={setContractAddress} contractAddressError={contractAddressError} setContractAddressError={setContractAddressError} />
<button type="submit" className="btn btn-primary" disabled={!!contractAddressError || !contractAddress || !selectedChain}>
<button type="submit" className="btn btn-primary" disabled={submitDisabled}>
Lookup
</button>
</form>
<div className="pt-3">
{chainSettings &&
VERIFIERS.map((verifierId) => {
if (!chainSettings.verifiers[verifierId]?.apiUrl || (verifierId === 'Etherscan' && !chainSettings.verifiers[verifierId]?.apiKey)) {
if (!validConfiguration(chainSettings, verifierId)) {
const tooltipText = 'Configure API in the settings'
return (
<div key={verifierId} className="pt-2">

@ -1,14 +1,13 @@
import example from './example.js'
import { useContext } from 'react'
import { AccordionReceipt } from '../components/AccordionReceipt'
import type { SubmittedContracts } from '../types'
import { AppContext } from '../AppContext'
export const ReceiptsView = () => {
const submittedContracts = example as unknown as SubmittedContracts
// const {submittedContracts} = React.useContext(AppContext);
const { submittedContracts } = useContext(AppContext)
return (
<div className="accordion" id="receiptsAccordion">
{Object.values(submittedContracts).map((contract, index) => (
{Object.values(submittedContracts).reverse().map((contract, index) => (
<AccordionReceipt contract={contract} index={index} />
))}
</div>

@ -2,57 +2,69 @@ import { useContext, useEffect, useState } from 'react'
import { AppContext } from '../AppContext'
import { SearchableChainDropdown, ContractDropdown, ContractAddressInput } from '../components'
import type { Chain, SubmittedContract, VerificationReceipt, VerifierInfo } from '../types'
import { SourcifyVerifier } from '../Verifiers/SourcifyVerifier'
import { EtherscanVerifier } from '../Verifiers/EtherscanVerifier'
import { mergeChainSettingsWithDefaults, type VerifierIdentifier, VERIFIERS, type Chain, type SubmittedContract, type VerificationReceipt, type VerifierInfo, validConfiguration } from '../types'
import { useNavigate } from 'react-router-dom'
import { ConstructorArguments } from '../components/ConstructorArguments'
import { AbstractVerifier, getVerifier } from '../Verifiers'
import { ContractDropdownSelection } from '../components/ContractDropdown'
import { CustomTooltip } from '@remix-ui/helper'
import { getVerifier } from '../Verifiers'
export const VerifyView = () => {
const { compilationOutput, setSubmittedContracts } = useContext(AppContext)
const [contractAddress, setContractAddress] = useState('')
const { compilationOutput, setSubmittedContracts, settings } = useContext(AppContext)
const [selectedChain, setSelectedChain] = useState<Chain | undefined>()
const [contractAddress, setContractAddress] = useState('')
const [contractAddressError, setContractAddressError] = useState('')
const [abiEncodedConstructorArgs, setAbiEncodedConstructorArgs] = useState<string>('')
const [selectedContract, setSelectedContract] = useState<ContractDropdownSelection | undefined>()
const [contractAddressError, setContractAddressError] = useState('')
const [enabledVerifiers, setEnabledVerifiers] = useState<Partial<Record<VerifierIdentifier, boolean>>>({})
const navigate = useNavigate()
// TODO
// const sourcifyVerifier = new SourcifyVerifier('http://sourcify.dev/server/', 'Sourcify')
const sourcifyVerifier = new SourcifyVerifier('http://localhost:5555/', 'todo')
const etherscanVerifier = new EtherscanVerifier('https://api.etherscan.io', 'todo', 'API_KEY')
const verifiers = [sourcifyVerifier, etherscanVerifier] // Placeholder, to be derived from settings
const chainSettings = selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain || !selectedContract
// Enable all verifiers with valid configuration
useEffect(() => {
console.log('Selected chain changed', selectedChain)
const changedEnabledVerifiers = {}
for (const verifierId of VERIFIERS) {
if (validConfiguration(chainSettings, verifierId)) {
changedEnabledVerifiers[verifierId] = true
}
}
setEnabledVerifiers(changedEnabledVerifiers)
}, [selectedChain])
const handleVerifierCheckboxClick = (verifierId: VerifierIdentifier, checked: boolean) => {
setEnabledVerifiers({ ...enabledVerifiers, [verifierId]: checked })
}
const handleVerify = async (e) => {
e.preventDefault() // Don't change the page
e.preventDefault()
console.log('selectedContract', selectedContract)
const { triggerFilePath, filePath, contractName } = selectedContract
// TODO create enabledVerifiers from simple VerifierIdentifier -> boolean mapping
const enabledVerifiers = verifiers.filter((verifier) => verifier.enabled)
const compilerAbstract = compilationOutput[triggerFilePath]
if (!compilerAbstract) {
throw new Error(`Error: Compilation output not found for ${triggerFilePath}`)
}
const date = new Date()
// A receipt for each verifier
const receipts: VerificationReceipt[] = enabledVerifiers.map((verifier) => {
const contractId = selectedChain?.chainId + '-' + contractAddress + '-' + date.toUTCString()
const receipts: VerificationReceipt[] = []
for (const [verifierId, enabled] of Object.entries(enabledVerifiers)) {
if (!enabled) {
continue
}
const verifierInfo: VerifierInfo = {
apiUrl: verifier.apiUrl,
name: verifier instanceof SourcifyVerifier ? 'Sourcify' : 'Etherscan',
apiUrl: chainSettings.verifiers[verifierId].apiUrl,
name: verifierId as VerifierIdentifier,
}
return { verifierInfo, status: null, receiptId: null, message: null }
})
receipts.push({ verifierInfo, status: 'pending', contractId })
}
const newSubmittedContract: SubmittedContract = {
type: 'contract',
id: selectedChain?.chainId + '-' + contractAddress + '-' + date.toUTCString(),
id: contractId,
address: contractAddress,
chainId: selectedChain?.chainId.toString(),
filePath,
@ -65,15 +77,14 @@ export const VerifyView = () => {
}
setSubmittedContracts((prev) => ({ ...prev, [newSubmittedContract.id]: newSubmittedContract }))
console.log('newSubmittedContract:', newSubmittedContract)
// Take user to receipt view
navigate('/receipts')
// Verify for each verifier. forEach does not wait for await and each promise will execute in parallel
receipts.forEach(async (receipt) => {
const { verifierInfo } = receipt
const verifier = getVerifier(verifierInfo.name, { apiUrl: verifierInfo.apiUrl, explorerUrl: '' })
const verifierSettings = chainSettings.verifiers[verifierInfo.name]
const verifier = getVerifier(verifierInfo.name, { ...verifierSettings })
try {
const response = await verifier.verify(newSubmittedContract, compilerAbstract)
receipt.status = response.status
@ -91,48 +102,47 @@ export const VerifyView = () => {
})
}
console.log('sourcifyVerifiers:', verifiers)
return (
<form onSubmit={handleVerify}>
<SearchableChainDropdown label="Chain" id="network-dropdown" setSelectedChain={setSelectedChain} selectedChain={selectedChain} />
<SearchableChainDropdown label="Chain" id="network-dropdown" selectedChain={selectedChain} setSelectedChain={setSelectedChain} />
<ContractAddressInput label="Contract Address" id="contract-address" contractAddress={contractAddress} setContractAddress={setContractAddress} contractAddressError={contractAddressError} setContractAddressError={setContractAddressError} />
<ContractDropdown label="Contract Name" id="contract-dropdown-1" setSelectedContract={setSelectedContract} />
<button type="submit" className="btn btn-primary">
Verify
</button>
{selectedContract && <ConstructorArguments abiEncodedConstructorArgs={abiEncodedConstructorArgs} setAbiEncodedConstructorArgs={setAbiEncodedConstructorArgs} selectedContract={selectedContract} />}
<div>
{verifiers?.length > 0 &&
verifiers.map((verifier) => {
// Temporary fix. The verifier options should be rendered from a constant later.
const name = verifier instanceof SourcifyVerifier ? 'Sourcify' : 'Etherscan'
Verify on:
{VERIFIERS.map((verifierId) => {
return (
<div key={name} className="form-check">
<input
className="form-check-input"
type="checkbox"
id={`verifier-${name}`}
checked={verifier.enabled}
onChange={(e) => {
verifier.enabled = e.target.checked
// Trigger a re-render
// setVerifiers([...verifiers])
}}
/>
<label className="form-check-label" htmlFor={`verifier-${name}`}>
{name} ({verifier.apiUrl})
<div key={verifierId} className="pt-2 form-check">
<input className="form-check-input" type="checkbox" id={`verifier-${verifierId}`} checked={!!enabledVerifiers[verifierId]} onChange={(e) => handleVerifierCheckboxClick(verifierId, e.target.checked)} disabled={!chainSettings || !validConfiguration(chainSettings, verifierId)} />
<div className="d-flex flex-column align-items-start">
<label htmlFor={`verifier-${verifierId}`} style={{ fontSize: '1rem', lineHeight: '1.5', color: 'var(--text)' }} className={`mb-0 font-weight-bold${!chainSettings || validConfiguration(chainSettings, verifierId) ? '' : ' text-secondary'}`}>
{verifierId}
</label>
{!chainSettings ? (
''
) : validConfiguration(chainSettings, verifierId) ? (
<span className="text-secondary">{chainSettings.verifiers[verifierId].apiUrl}</span>
) : (
<CustomTooltip tooltipText="Configure the API in the settings">
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted' }}>
Enable?
</span>
</CustomTooltip>
)}
</div>
</div>
)
})}
</div>
<div>
{/* <ConstructorArguments abiEncodedConstructorArgs={abiEncodedConstructorArgs} setAbiEncodedConstructorArgs={setAbiEncodedConstructorArgs} selectedContract={selectedContract} /> */}
</div>
<button type="submit" className="btn btn-primary mt-3" disabled={submitDisabled}>
Verify
</button>
</form>
)
}

@ -1,104 +0,0 @@
const json = {
'undefined-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)': {
type: 'contract',
id: 'undefined-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)',
address: '0x2738d13E81e30bC615766A0410e7cF199FD59A83',
filePath: '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol',
contractName: 'Initializable',
date: '2024-06-20T20:32:36.361Z',
receipts: [
{
verifier: {
name: 'Sourcify Localhost',
apiUrl: 'http://localhost:5555/',
enabled: true,
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
{
verifier: {
name: 'Etherscan',
apiUrl: 'https://api.etherscan.io',
enabled: true,
apiKey: 'API_KEY',
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
],
chainId: '1',
},
'1-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)': {
type: 'proxy',
id: '1-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)',
implementation: {
type: 'contract',
id: 'undefined-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)',
address: '0x2738d13E81e30bC615766A0410e7cF199FD59A83',
filePath: '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol',
contractName: 'Initializable',
date: '2024-06-20T20:32:36.361Z',
receipts: [
{
verifier: {
name: 'Sourcify Localhost',
apiUrl: 'http://localhost:5555/',
enabled: true,
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
{
verifier: {
name: 'Etherscan',
apiUrl: 'https://api.etherscan.io',
enabled: true,
apiKey: 'API_KEY',
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
],
chainId: '1',
},
proxy: {
type: 'contract',
id: 'undefined-0x2738d13E81e30bC615766A0410e7cF199FD59A83-Thu Jun 20 2024 22:32:36 GMT+0200 (Central European Summer Time)',
address: '0x2738d13E81e30bC615766A0410e7cF199FD59A83',
filePath: '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol',
contractName: 'Initializable',
date: '2024-06-20T20:32:36.361Z',
receipts: [
{
verifier: {
name: 'Sourcify Localhost',
apiUrl: 'http://localhost:5555/',
enabled: true,
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
{
verifier: {
name: 'Etherscan',
apiUrl: 'https://api.etherscan.io',
enabled: true,
apiKey: 'API_KEY',
},
status: 'error',
receiptId: null,
message: 'Failed to fetch',
},
],
chainId: '1',
},
},
}
module.exports = json
Loading…
Cancel
Save