From 7ad629c4c4232f41b64ede63f28f3c0a1db3c5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaan=20Uzdo=C4=9Fan?= Date: Fri, 14 Jun 2024 19:05:05 +0200 Subject: [PATCH] Add SourcifyVerifier and submit a verification. Proper e2e chain and contract selection. --- .../src/app/AppContext.tsx | 26 +++--- .../src/app/Verifiers/SourcifyVerifier.ts | 57 +++++++++++++ apps/contract-verification/src/app/app.tsx | 31 ++++--- .../src/app/components/ContractDropdown.tsx | 17 +++- .../src/app/components/SearchableDropdown.tsx | 36 ++++---- .../src/app/types/VerificationTypes.ts | 30 +++++++ .../src/app/views/HomeView.tsx | 82 ++++++++++++++----- 7 files changed, 210 insertions(+), 69 deletions(-) create mode 100644 apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts create mode 100644 apps/contract-verification/src/app/types/VerificationTypes.ts diff --git a/apps/contract-verification/src/app/AppContext.tsx b/apps/contract-verification/src/app/AppContext.tsx index 70d1056dbc..77180cb3aa 100644 --- a/apps/contract-verification/src/app/AppContext.tsx +++ b/apps/contract-verification/src/app/AppContext.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 diff --git a/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts b/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts new file mode 100644 index 0000000000..f3de4695aa --- /dev/null +++ b/apps/contract-verification/src/app/Verifiers/SourcifyVerifier.ts @@ -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 { + // 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 { + // 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 + } +} diff --git a/apps/contract-verification/src/app/app.tsx b/apps/contract-verification/src/app/app.tsx index b54815eb9f..a914214f78 100644 --- a/apps/contract-verification/src/app/app.tsx +++ b/apps/contract-verification/src/app/app.tsx @@ -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('dark') // TODO: Types for chains - const [chains, setChains] = useState([]) // State to hold the chains data - const [selectedChain, setSelectedChain] = useState() + const [chains, setChains] = useState([]) // State to hold the chains data const [targetFileName, setTargetFileName] = useState('') - const [compilationOutput, setCompilationOutput] = useState() - const [selectedContract, setSelectedContract] = useState() + const [compilationOutput, setCompilationOutput] = useState<{[key: string]: CompilerAbstract} | undefined>() + // Contract file and name in format contracts/Storage.sol:Storage + const [selectedContractFileAndName, setSelectedContractFileAndName] = useState() + const [verifiedContracts, setVerifiedContracts] = useState([]) + const [sourcifyVerifiers, setSourcifyVerifiers] = useState([]) 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 ( - + ) diff --git a/apps/contract-verification/src/app/components/ContractDropdown.tsx b/apps/contract-verification/src/app/components/ContractDropdown.tsx index f54d28cd8d..39fc47869f 100644 --- a/apps/contract-verification/src/app/components/ContractDropdown.tsx +++ b/apps/contract-verification/src/app/components/ContractDropdown.tsx @@ -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 = ({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) => { - console.log('contractName', event.target.value) + console.log('selecting ', event.target.value) + setSelectedContractFileAndName(event.target.value) } const hasContracts = compilationOutput && Object.keys(compilationOutput).length > 0 diff --git a/apps/contract-verification/src/app/components/SearchableDropdown.tsx b/apps/contract-verification/src/app/components/SearchableDropdown.tsx index bae40e0aa6..7af3f40c4d 100644 --- a/apps/contract-verification/src/app/components/SearchableDropdown.tsx +++ b/apps/contract-verification/src/app/components/SearchableDropdown.tsx @@ -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 = ({options, label, id, value, onChange}) => { +export const SearchableDropdown: React.FC = ({chains, label, id, setSelectedChain, selectedChain}) => { const [searchTerm, setSearchTerm] = useState('') - const [selectedOption, setSelectedOption] = useState(null) const [isOpen, setIsOpen] = useState(false) - const [filteredOptions, setFilteredOptions] = useState(options) + const [filteredOptions, setFilteredOptions] = useState(chains) const dropdownRef = useRef(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 = ({options, label, id, const handleInputChange = (e: React.ChangeEvent) => { 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 = ({options, label, id, setSearchTerm('') } - if (!options || options.length === 0) { + if (!chains || chains.length === 0) { return (
@@ -82,9 +76,9 @@ export const SearchableDropdown: React.FC = ({options, label, id, {isOpen && (
    - {filteredOptions.map((option) => ( -
  • handleOptionClick(option)} className={`dropdown-item ${selectedOption === option ? 'active' : ''}`} style={{cursor: 'pointer', whiteSpace: 'normal'}}> - {option.name} + {filteredOptions.map((chain) => ( +
  • handleOptionClick(chain)} className={`dropdown-item ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{cursor: 'pointer', whiteSpace: 'normal'}}> + {chain.title || chain.name} ({chain.chainId})
  • ))}
diff --git a/apps/contract-verification/src/app/types/VerificationTypes.ts b/apps/contract-verification/src/app/types/VerificationTypes.ts new file mode 100644 index 0000000000..108c48074f --- /dev/null +++ b/apps/contract-verification/src/app/types/VerificationTypes.ts @@ -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 + faucets?: string[] + infoURL?: string +} diff --git a/apps/contract-verification/src/app/views/HomeView.tsx b/apps/contract-verification/src/app/views/HomeView.tsx index 94013b21a8..75c6880ff4 100644 --- a/apps/contract-verification/src/app/views/HomeView.tsx +++ b/apps/contract-verification/src/app/views/HomeView.tsx @@ -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() - 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 -1 + if (!isAInEthereum && isBInEthereum) return 1 + 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) => { + 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 (
@@ -33,20 +69,22 @@ export const HomeView = () => { Verify compiled contracts on different verification services

-
- +
+
- +
{contractAddressError &&
{contractAddressError}
}
+
- ({value: item, name: item}))} id="contract-name-dropdown" /> -
-
Constructor Arguments
- {/* TODO: Add input fields for constructor arguments */} -
-
+ + + +
) }