Add SourcifyVerifier and submit a verification. Proper e2e chain and contract selection.

pull/5285/head
Kaan Uzdoğan 5 months ago committed by Aniket
parent c2b185dc02
commit 7ad629c4c4
  1. 26
      apps/contract-verification/src/app/AppContext.tsx
  2. 57
      apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts
  3. 31
      apps/contract-verification/src/app/app.tsx
  4. 17
      apps/contract-verification/src/app/components/ContractDropdown.tsx
  5. 36
      apps/contract-verification/src/app/components/SearchableDropdown.tsx
  6. 30
      apps/contract-verification/src/app/types/VerificationTypes.ts
  7. 78
      apps/contract-verification/src/app/views/HomeView.tsx

@ -1,18 +1,21 @@
import React from 'react'
import {ThemeType} from './types'
import {CompilationResult, CompiledContract} from '@remixproject/plugin-api'
import {Chain, VerifiedContract} from './types/VerificationTypes'
import {SourcifyVerifier} from './Verifiers/SourcifyVerifier'
import {CompilerAbstract} from '@remix-project/remix-solidity'
// Define the type for the context
type AppContextType = {
themeType: ThemeType
setThemeType: (themeType: ThemeType) => void
chains: any[]
selectedChain: any | undefined
setSelectedChain: (chain: string) => void
compilationOutput: CompilationResult | undefined
selectedContract: CompiledContract | undefined
setSelectedContract: (contract: CompiledContract) => void
chains: Chain[]
compilationOutput: {[key: string]: CompilerAbstract} | undefined
selectedContractFileAndName: string | undefined
setSelectedContractFileAndName: (contract: string) => void
targetFileName: string | undefined
verifiedContracts: VerifiedContract[]
setVerifiedContracts: (verifiedContracts: VerifiedContract[]) => void
sourcifyVerifiers: SourcifyVerifier[]
}
// Provide a default value with the appropriate types
@ -22,12 +25,13 @@ const defaultContextValue: AppContextType = {
console.log('Calling Set Theme Type')
},
chains: [],
selectedChain: undefined,
setSelectedChain: (chain: string) => {},
compilationOutput: undefined,
selectedContract: undefined,
setSelectedContract: (contract: CompiledContract) => {},
selectedContractFileAndName: undefined,
setSelectedContractFileAndName: (contract: string) => {},
targetFileName: undefined,
verifiedContracts: [],
setVerifiedContracts: (verifiedContracts: VerifiedContract[]) => {},
sourcifyVerifiers: [],
}
// Create the context with the type

@ -0,0 +1,57 @@
import {SourcesCode} from '@remix-project/remix-solidity'
import {AbstractVerifier} from './AbstractVerifier'
export class SourcifyVerifier {
name: string
apiUrl: string
constructor(apiUrl: string, name: string = 'Sourcify') {
this.apiUrl = apiUrl
this.name = name
}
async verify(chainId: string, address: string, sources: SourcesCode, metadataStr: string): Promise<boolean> {
// from { "filename.sol": {content: "contract MyContract { ... }"} }
// to { "filename.sol": "contract MyContract { ... }" }
const formattedSources = Object.entries(sources).reduce((acc, [fileName, {content}]) => {
acc[fileName] = content
return acc
}, {})
const body = {
chainId,
address,
files: {
'metadata.json': metadataStr,
...formattedSources,
},
}
console.log(body)
const response = await fetch(new URL('verify', this.apiUrl).href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
throw new Error(`Error on Sourcify verification at ${this.apiUrl}: Status:${response.status} Response: ${await response.text()}`)
}
const data = await response.json()
console.log(data)
return data.result
}
async lookup(): Promise<any> {
// Implement the lookup logic here
console.log('Sourcify lookup started')
// Placeholder logic for lookup
const lookupResult = {} // Replace with actual lookup logic
console.log('Sourcify lookup completed')
return lookupResult
}
}

@ -4,24 +4,34 @@ import {ContractVerificationPluginClient} from './ContractVerificationPluginClie
import {AppContext} from './AppContext'
import DisplayRoutes from './routes'
import {CustomTooltip} from '@remix-ui/helper'
import {ThemeType} from './types'
import './App.css'
import {CompilationFileSources, CompilationResult, CompiledContract} from '@remixproject/plugin-api'
import {Chain, VerifiedContract} from './types/VerificationTypes'
import {SourcifyVerifier} from './Verifiers/SourcifyVerifier'
import {CompilerAbstract} from '@remix-project/remix-solidity'
const plugin = new ContractVerificationPluginClient()
const App = () => {
const [themeType, setThemeType] = useState<ThemeType>('dark')
// TODO: Types for chains
const [chains, setChains] = useState<any>([]) // State to hold the chains data
const [selectedChain, setSelectedChain] = useState<any | undefined>()
const [chains, setChains] = useState<Chain[]>([]) // State to hold the chains data
const [targetFileName, setTargetFileName] = useState('')
const [compilationOutput, setCompilationOutput] = useState<CompilationResult | undefined>()
const [selectedContract, setSelectedContract] = useState<CompiledContract | undefined>()
const [compilationOutput, setCompilationOutput] = useState<{[key: string]: CompilerAbstract} | undefined>()
// Contract file and name in format contracts/Storage.sol:Storage
const [selectedContractFileAndName, setSelectedContractFileAndName] = useState<string | undefined>()
const [verifiedContracts, setVerifiedContracts] = useState<VerifiedContract[]>([])
const [sourcifyVerifiers, setSourcifyVerifiers] = useState<SourcifyVerifier[]>([])
useEffect(() => {
console.log('Selected Contract File And Name Changed', selectedContractFileAndName)
}, [selectedContractFileAndName])
useEffect(() => {
// const sourcifyVerifier = new SourcifyVerifier('http://sourcify.dev/server/', 'Sourcify')
const sourcifyVerifier = new SourcifyVerifier('http://localhost:5555/', 'Sourcify Localhost')
setSourcifyVerifiers([sourcifyVerifier])
// 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
// Because of this reason we use @ts-expect-error for the next line
// // @ts-expect-error:next-line
@ -60,12 +70,11 @@ const App = () => {
// console.log(data)
// })
plugin.call('compilerArtefacts' as any, 'getAllCompilerAbstracts').then((data: any) => {
plugin.call('compilerArtefacts' as any, 'getAllCompilerAbstracts').then((obj: any) => {
console.log('compilerArtefacts.getAllCompilerAbstracts')
console.log(data)
setCompilationOutput(data)
console.log(obj)
setCompilationOutput(obj)
})
// Fetch chains.json and update state
fetch('https://chainid.network/chains.json')
.then((response) => response.json())
@ -77,7 +86,7 @@ const App = () => {
}, [])
return (
<AppContext.Provider value={{themeType, setThemeType, chains, selectedChain, setSelectedChain, compilationOutput, selectedContract, setSelectedContract, targetFileName}}>
<AppContext.Provider value={{themeType, setThemeType, chains, compilationOutput, selectedContractFileAndName, setSelectedContractFileAndName, targetFileName, verifiedContracts, setVerifiedContracts, sourcifyVerifiers}}>
<DisplayRoutes />
</AppContext.Provider>
)

@ -8,21 +8,30 @@ interface ContractDropdownItem {
interface ContractDropdownProps {
label: string
contractNames: ContractDropdownItem[]
id: string
}
// Chooses one contract from the compilation output.
export const ContractDropdown: React.FC<ContractDropdownProps> = ({label, id}) => {
const {setSelectedContract, compilationOutput} = useContext(AppContext)
const [chosenContractFileAndName, setChosenContractFileAndName] = useState('')
const {setSelectedContractFileAndName, compilationOutput} = useContext(AppContext)
useEffect(() => {
console.log('CompiilationOutput chainged', compilationOutput)
if (!compilationOutput) return
const isOnlyOneFileCompiled = Object.keys(compilationOutput).length === 1
if (isOnlyOneFileCompiled) {
const onlyFileName = Object.keys(compilationOutput)[0]
const isOnlyOneContractCompiled = Object.keys(compilationOutput[onlyFileName].data.contracts[onlyFileName]).length === 1
if (isOnlyOneContractCompiled) {
const onlyContractName = Object.keys(compilationOutput[onlyFileName].data.contracts[onlyFileName])[0]
setSelectedContractFileAndName(onlyFileName + ':' + onlyContractName)
}
}
}, [compilationOutput])
const handleSelectContract = (event: React.ChangeEvent<HTMLSelectElement>) => {
console.log('contractName', event.target.value)
console.log('selecting ', event.target.value)
setSelectedContractFileAndName(event.target.value)
}
const hasContracts = compilationOutput && Object.keys(compilationOutput).length > 0

@ -1,39 +1,34 @@
import React, {useState, useEffect, useRef} from 'react'
import Fuse from 'fuse.js'
interface DropdownItem {
value: string
name: string
}
import {Chain} from '../types/VerificationTypes'
interface DropdownProps {
label: string
options: DropdownItem[]
chains: Chain[]
id: string
value: string
onChange: (value: string) => void
setSelectedChain: (chain: Chain) => void
selectedChain: Chain
}
export const SearchableDropdown: React.FC<DropdownProps> = ({options, label, id, value, onChange}) => {
export const SearchableDropdown: React.FC<DropdownProps> = ({chains, label, id, setSelectedChain, selectedChain}) => {
const [searchTerm, setSearchTerm] = useState('')
const [selectedOption, setSelectedOption] = useState<DropdownItem | null>(null)
const [isOpen, setIsOpen] = useState(false)
const [filteredOptions, setFilteredOptions] = useState<DropdownItem[]>(options)
const [filteredOptions, setFilteredOptions] = useState<Chain[]>(chains)
const dropdownRef = useRef<HTMLDivElement>(null)
const fuse = new Fuse(options, {
const fuse = new Fuse(chains, {
keys: ['name'],
threshold: 0.3,
})
useEffect(() => {
if (searchTerm === '') {
setFilteredOptions(options)
setFilteredOptions(chains)
} else {
const result = fuse.search(searchTerm)
setFilteredOptions(result.map(({item}) => item))
}
}, [searchTerm, options])
}, [searchTerm, chains])
// Close dropdown when user clicks outside
useEffect(() => {
@ -50,12 +45,11 @@ export const SearchableDropdown: React.FC<DropdownProps> = ({options, label, id,
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
onChange(e.target.value)
setIsOpen(true)
}
const handleOptionClick = (option: DropdownItem) => {
setSelectedOption(option)
const handleOptionClick = (option: Chain) => {
setSelectedChain(option)
setSearchTerm(option.name)
setIsOpen(false)
}
@ -65,7 +59,7 @@ export const SearchableDropdown: React.FC<DropdownProps> = ({options, label, id,
setSearchTerm('')
}
if (!options || options.length === 0) {
if (!chains || chains.length === 0) {
return (
<div className="dropdown">
<label htmlFor={id}>{label}</label>
@ -82,9 +76,9 @@ export const SearchableDropdown: React.FC<DropdownProps> = ({options, label, id,
<input type="text" value={searchTerm} onChange={handleInputChange} onClick={openDropdown} placeholder="Select a chain" className="form-control" />
{isOpen && (
<ul className="dropdown-menu show w-100" style={{maxHeight: '400px', overflowY: 'auto'}}>
{filteredOptions.map((option) => (
<li key={option.value} onClick={() => handleOptionClick(option)} className={`dropdown-item ${selectedOption === option ? 'active' : ''}`} style={{cursor: 'pointer', whiteSpace: 'normal'}}>
{option.name}
{filteredOptions.map((chain) => (
<li key={chain.chainId} onClick={() => handleOptionClick(chain)} className={`dropdown-item ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{cursor: 'pointer', whiteSpace: 'normal'}}>
{chain.title || chain.name} ({chain.chainId})
</li>
))}
</ul>

@ -0,0 +1,30 @@
import {SourcifyVerifier} from '../Verifiers/SourcifyVerifier'
export interface VerifiedContract {
name: string
address: string
chainId: string
date: Date
verifier: SourcifyVerifier
status: string
receipt?: string
}
interface Currency {
name: string
symbol: string
decimals: number
}
// types for https://chainid.network/chains.json (i.e. https://github.com/ethereum-lists/chains)
export interface Chain {
name: string
title?: string
chainId: number
shortName?: string
network?: string
networkId?: number
nativeCurrency?: Currency
rpc: Array<string>
faucets?: string[]
infoURL?: string
}

@ -1,30 +1,66 @@
import React from 'react'
import React, {useEffect, useState} from 'react'
import {AppContext} from '../AppContext'
import {SearchableDropdown} from '../components'
import {ContractDropdown} from '../components/ContractDropdown'
// INSERT_YOUR_CODE
import {ethers} from 'ethers/'
import {Chain} from '../types/VerificationTypes'
export const HomeView = () => {
const {chains, selectedChain, setSelectedChain, compilationOutput} = React.useContext(AppContext)
const {chains, compilationOutput, sourcifyVerifiers, selectedContractFileAndName} = React.useContext(AppContext)
const [contractAddress, setContractAddress] = useState('')
const [contractAddressError, setContractAddressError] = useState('')
const [selectedChain, setSelectedChain] = useState<Chain | undefined>()
const ethereumChainIds = [1, 3, 4, 5, 11155111, 17000]
useEffect(() => {
console.log('Selected chain changed', selectedChain)
}, [selectedChain])
const contractNames = compilationOutput?.contracts && Object.keys(compilationOutput?.contracts)
const ethereumChainIds = [1, 3, 4, 5, 11155111, 17000]
// Add Ethereum chains to the head of the chains list. Sort the rest alphabetically
const dropdownChains = chains
.map((chain) => ({value: chain.chainId, name: `${chain.title || chain.name} (${chain.chainId})`}))
.sort((a, b) => {
const isAInEthereum = ethereumChainIds.includes(a.value)
const isBInEthereum = ethereumChainIds.includes(b.value)
const dropdownChains = chains.sort((a, b) => {
const isAInEthereum = ethereumChainIds.includes(a.chainId)
const isBInEthereum = ethereumChainIds.includes(b.chainId)
if (isAInEthereum && !isBInEthereum) return -1
if (!isAInEthereum && isBInEthereum) return 1
if (isAInEthereum && isBInEthereum) return ethereumChainIds.indexOf(a.value) - ethereumChainIds.indexOf(b.value)
if (isAInEthereum && isBInEthereum) return ethereumChainIds.indexOf(a.chainId) - ethereumChainIds.indexOf(b.chainId)
return a.name.localeCompare(b.name)
return (a.title || a.name).localeCompare(b.title || b.name)
})
const handleVerify = async (e) => {
e.preventDefault() // Don't change the page
const [selectedFileName, selectedContractName] = selectedContractFileAndName.split(':')
const selectedContractAbstract = compilationOutput?.[selectedFileName || '']
const selectedContractMetadataStr = selectedContractAbstract.data.contracts[selectedFileName][selectedContractName].metadata
console.log('selectedFileName:', selectedFileName)
console.log('selectedContractName:', selectedContractName)
console.log('selectedContractAbstract:', selectedContractAbstract)
console.log('selectedContractMetadataStr:', selectedContractMetadataStr)
console.log('sourcifyVerifiers:', sourcifyVerifiers)
console.log('selectedChain:', selectedChain)
console.log('contractAddress:', contractAddress)
const sourcifyPromises = sourcifyVerifiers.map((sourcifyVerifier) => {
return sourcifyVerifier.verify(selectedChain.chainId.toString(), contractAddress, selectedContractAbstract.source.sources, selectedContractMetadataStr)
})
const results = await Promise.all(sourcifyPromises)
console.log('results', results)
}
const handleAddressChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const isValidAddress = ethers.utils.isAddress(event.target.value)
setContractAddress(event.target.value)
if (!isValidAddress) {
setContractAddressError('Invalid contract address')
console.error('Invalid contract address')
return
}
setContractAddressError('')
}
return (
<div className="my-4">
<div>
@ -33,20 +69,22 @@ export const HomeView = () => {
Verify compiled contracts on different verification services
</p>
</div>
<div>
<SearchableDropdown label="Contract Chain" options={dropdownChains} id="network-dropdown" value={selectedChain} onChange={setSelectedChain} />
<form onSubmit={handleVerify}>
<SearchableDropdown label="Contract Chain" chains={dropdownChains} id="network-dropdown" setSelectedChain={setSelectedChain} selectedChain={selectedChain} />
<div className="form-group">
<label htmlFor="contract-address">Contract Address</label>
<input type="text" className="form-control" id="contract-address" placeholder="0x2738d13E81e..." />
<div>{contractAddressError && <div className="text-danger">{contractAddressError}</div>}</div>
<input type="text" className="form-control" id="contract-address" placeholder="0x2738d13E81e..." value={contractAddress} onChange={handleAddressChange} />
</div>
<ContractDropdown label="Contract Name" contractNames={contractNames?.map((item) => ({value: item, name: item}))} id="contract-name-dropdown" />
<div>
<div>Constructor Arguments</div>
{/* TODO: Add input fields for constructor arguments */}
</div>
</div>
<ContractDropdown label="Contract Name" id="contract-dropdown-1" />
<button type="submit" className="btn btn-primary">
{' '}
Verify{' '}
</button>
</form>
</div>
)
}

Loading…
Cancel
Save