parent
027181a7b2
commit
31920ef065
@ -1,9 +1,7 @@ |
|||||||
{ |
{ |
||||||
"tabWidth": 2, |
"tabWidth": 2, |
||||||
"printWidth": 500, |
"printWidth": 500, |
||||||
"bracketSpacing": false, |
|
||||||
"useTabs": false, |
"useTabs": false, |
||||||
"semi": false, |
"semi": false, |
||||||
"singleQuote": true, |
"singleQuote": true |
||||||
"bracketSpacing": false |
|
||||||
} |
} |
||||||
|
@ -1,9 +1,5 @@ |
|||||||
{ |
{ |
||||||
"presets": ["@babel/preset-env", ["@babel/preset-react", |
"presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]], |
||||||
{"runtime": "automatic"} |
|
||||||
]], |
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"], |
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"], |
||||||
"ignore": [ |
"ignore": ["**/node_modules/**"] |
||||||
"**/node_modules/**" |
|
||||||
] |
|
||||||
} |
} |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../.eslintrc.json" |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
{ |
||||||
|
"name": "contract-verification", |
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json", |
||||||
|
"sourceRoot": "apps/contract-verification/src", |
||||||
|
"projectType": "application", |
||||||
|
"targets": { |
||||||
|
"build": { |
||||||
|
"executor": "@nrwl/webpack:webpack", |
||||||
|
"outputs": ["{options.outputPath}"], |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"compiler": "babel", |
||||||
|
"outputPath": "dist/apps/contract-verification", |
||||||
|
"index": "apps/contract-verification/src/index.html", |
||||||
|
"baseHref": "./", |
||||||
|
"main": "apps/contract-verification/src/main.tsx", |
||||||
|
"polyfills": "apps/contract-verification/src/polyfills.ts", |
||||||
|
"tsConfig": "apps/contract-verification/tsconfig.app.json", |
||||||
|
"assets": [ |
||||||
|
"apps/contract-verification/src/favicon.ico", |
||||||
|
"apps/contract-verification/src/assets", |
||||||
|
"apps/contract-verification/src/profile.json" |
||||||
|
], |
||||||
|
"styles": ["apps/contract-verification/src/styles.css"], |
||||||
|
"scripts": [], |
||||||
|
"webpackConfig": "apps/contract-verification/webpack.config.js" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"fileReplacements": [ |
||||||
|
{ |
||||||
|
"replace": "apps/contract-verification/src/environments/environment.ts", |
||||||
|
"with": "apps/contract-verification/src/environments/environment.prod.ts" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"lint": { |
||||||
|
"executor": "@nrwl/linter:eslint", |
||||||
|
"outputs": ["{options.outputFile}"], |
||||||
|
"options": { |
||||||
|
"lintFilePatterns": ["apps/contract-verification/**/*.ts"], |
||||||
|
"eslintConfig": "apps/contract-verification/.eslintrc" |
||||||
|
} |
||||||
|
}, |
||||||
|
"serve": { |
||||||
|
"executor": "@nrwl/webpack:dev-server", |
||||||
|
"defaultConfiguration": "development", |
||||||
|
"options": { |
||||||
|
"buildTarget": "contract-verification:build", |
||||||
|
"hmr": true, |
||||||
|
"baseHref": "/" |
||||||
|
}, |
||||||
|
"configurations": { |
||||||
|
"development": { |
||||||
|
"buildTarget": "contract-verification:build:development", |
||||||
|
"port": 5003 |
||||||
|
}, |
||||||
|
"production": { |
||||||
|
"buildTarget": "contract-verification:build:production" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"tags": [] |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
html, body, #root { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
a:focus { |
||||||
|
background-color: var(bg-light) !important; |
||||||
|
} |
||||||
|
|
||||||
|
.fa-arrow-up-right-from-square::before { content: "\f08e"; } |
||||||
|
.fa-xmark::before { content: "\f00d"; } |
@ -0,0 +1,34 @@ |
|||||||
|
import React from 'react' |
||||||
|
import type { ThemeType, Chain, SubmittedContracts, ContractVerificationSettings } from './types' |
||||||
|
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||||
|
import { ContractVerificationPluginClient } from './ContractVerificationPluginClient' |
||||||
|
import { ContractDropdownSelection } from './components/ContractDropdown' |
||||||
|
|
||||||
|
// Define the type for the context
|
||||||
|
type AppContextType = { |
||||||
|
themeType: ThemeType |
||||||
|
setThemeType: (themeType: ThemeType) => void |
||||||
|
clientInstance: ContractVerificationPluginClient |
||||||
|
settings: ContractVerificationSettings |
||||||
|
setSettings: React.Dispatch<React.SetStateAction<ContractVerificationSettings>> |
||||||
|
chains: Chain[] |
||||||
|
compilationOutput: { [key: string]: CompilerAbstract } | undefined |
||||||
|
submittedContracts: SubmittedContracts |
||||||
|
setSubmittedContracts: React.Dispatch<React.SetStateAction<SubmittedContracts>> |
||||||
|
} |
||||||
|
|
||||||
|
// Provide a default value with the appropriate types
|
||||||
|
const defaultContextValue: AppContextType = { |
||||||
|
themeType: 'dark', |
||||||
|
setThemeType: (themeType: ThemeType) => {}, |
||||||
|
clientInstance: {} as ContractVerificationPluginClient, |
||||||
|
settings: { chains: {} }, |
||||||
|
setSettings: () => {}, |
||||||
|
chains: [], |
||||||
|
compilationOutput: undefined, |
||||||
|
submittedContracts: {}, |
||||||
|
setSubmittedContracts: (submittedContracts: SubmittedContracts) => {}, |
||||||
|
} |
||||||
|
|
||||||
|
// Create the context with the type
|
||||||
|
export const AppContext = React.createContext<AppContextType>(defaultContextValue) |
@ -0,0 +1,18 @@ |
|||||||
|
import { PluginClient } from '@remixproject/plugin' |
||||||
|
import { createClient } from '@remixproject/plugin-webview' |
||||||
|
import EventManager from 'events' |
||||||
|
|
||||||
|
export class ContractVerificationPluginClient extends PluginClient { |
||||||
|
public internalEvents: EventManager |
||||||
|
|
||||||
|
constructor() { |
||||||
|
super() |
||||||
|
this.internalEvents = new EventManager() |
||||||
|
createClient(this) |
||||||
|
this.onload() |
||||||
|
} |
||||||
|
|
||||||
|
onActivation(): void { |
||||||
|
this.internalEvents.emit('verification_activated') |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||||
|
import type { LookupResponse, SubmittedContract, VerificationResponse } from '../types' |
||||||
|
|
||||||
|
// Optional function definitions
|
||||||
|
export interface AbstractVerifier { |
||||||
|
verifyProxy(submittedContract: SubmittedContract): Promise<VerificationResponse> |
||||||
|
checkVerificationStatus?(receiptId: string): Promise<VerificationResponse> |
||||||
|
checkProxyVerificationStatus?(receiptId: string): Promise<VerificationResponse> |
||||||
|
} |
||||||
|
|
||||||
|
export abstract class AbstractVerifier { |
||||||
|
constructor(public apiUrl: string, public explorerUrl: string) {} |
||||||
|
|
||||||
|
abstract verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> |
||||||
|
abstract lookup(contractAddress: string, chainId: string): Promise<LookupResponse> |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import { SourceFile } from '../types' |
||||||
|
import { EtherscanVerifier } from './EtherscanVerifier' |
||||||
|
|
||||||
|
// Etherscan and Blockscout return different objects from the getsourcecode method
|
||||||
|
interface BlockscoutSource { |
||||||
|
AdditionalSources: Array<{ SourceCode: string; Filename: string }> |
||||||
|
ConstructorArguments: string |
||||||
|
OptimizationRuns: number |
||||||
|
IsProxy: string |
||||||
|
SourceCode: string |
||||||
|
ABI: string |
||||||
|
ContractName: string |
||||||
|
CompilerVersion: string |
||||||
|
OptimizationUsed: string |
||||||
|
Runs: string |
||||||
|
EVMVersion: string |
||||||
|
FileName: string |
||||||
|
Address: string |
||||||
|
} |
||||||
|
|
||||||
|
export class BlockscoutVerifier extends EtherscanVerifier { |
||||||
|
LOOKUP_STORE_DIR = 'blockscout-verified' |
||||||
|
|
||||||
|
constructor(apiUrl: string) { |
||||||
|
// apiUrl and explorerUrl are the same for Blockscout
|
||||||
|
super(apiUrl, apiUrl, undefined) |
||||||
|
} |
||||||
|
|
||||||
|
getContractCodeUrl(address: string): string { |
||||||
|
const url = new URL(this.explorerUrl + `/address/${address}`) |
||||||
|
url.searchParams.append('tab', 'contract') |
||||||
|
return url.href |
||||||
|
} |
||||||
|
|
||||||
|
processReceivedFiles(source: unknown, contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { |
||||||
|
const blockscoutSource = source as BlockscoutSource |
||||||
|
|
||||||
|
const result: SourceFile[] = [] |
||||||
|
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}` |
||||||
|
|
||||||
|
const targetFilePath = `${filePrefix}/${blockscoutSource.FileName}` |
||||||
|
result.push({ content: blockscoutSource.SourceCode, path: targetFilePath }) |
||||||
|
|
||||||
|
for (const additional of blockscoutSource.AdditionalSources ?? []) { |
||||||
|
result.push({ content: additional.SourceCode, path: `${filePrefix}/${additional.Filename}` }) |
||||||
|
} |
||||||
|
|
||||||
|
return { sourceFiles: result, targetFilePath } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,289 @@ |
|||||||
|
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||||
|
import { AbstractVerifier } from './AbstractVerifier' |
||||||
|
import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types' |
||||||
|
|
||||||
|
interface EtherscanRpcResponse { |
||||||
|
status: '0' | '1' |
||||||
|
message: string |
||||||
|
result: string |
||||||
|
} |
||||||
|
|
||||||
|
interface EtherscanCheckStatusResponse { |
||||||
|
status: '0' | '1' |
||||||
|
message: string |
||||||
|
result: 'Pending in queue' | 'Pass - Verified' | 'Fail - Unable to verify' | 'Already Verified' | 'Unknown UID' |
||||||
|
} |
||||||
|
|
||||||
|
interface EtherscanSource { |
||||||
|
SourceCode: string |
||||||
|
ABI: string |
||||||
|
ContractName: string |
||||||
|
CompilerVersion: string |
||||||
|
OptimizationUsed: string |
||||||
|
Runs: string |
||||||
|
ConstructorArguments: string |
||||||
|
EVMVersion: string |
||||||
|
Library: string |
||||||
|
LicenseType: string |
||||||
|
Proxy: string |
||||||
|
Implementation: string |
||||||
|
SwarmSource: string |
||||||
|
} |
||||||
|
|
||||||
|
interface EtherscanGetSourceCodeResponse { |
||||||
|
status: '0' | '1' |
||||||
|
message: string |
||||||
|
result: EtherscanSource[] |
||||||
|
} |
||||||
|
|
||||||
|
export class EtherscanVerifier extends AbstractVerifier { |
||||||
|
LOOKUP_STORE_DIR = 'etherscan-verified' |
||||||
|
|
||||||
|
constructor(apiUrl: string, explorerUrl: string, protected apiKey?: string) { |
||||||
|
super(apiUrl, explorerUrl) |
||||||
|
} |
||||||
|
|
||||||
|
async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> { |
||||||
|
// TODO: Handle version Vyper contracts. This relies on Solidity metadata.
|
||||||
|
const metadata = JSON.parse(compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata) |
||||||
|
const formData = new FormData() |
||||||
|
formData.append('chainId', submittedContract.chainId) |
||||||
|
formData.append('codeformat', 'solidity-standard-json-input') |
||||||
|
formData.append('sourceCode', compilerAbstract.input.toString()) |
||||||
|
formData.append('contractaddress', submittedContract.address) |
||||||
|
formData.append('contractname', submittedContract.filePath + ':' + submittedContract.contractName) |
||||||
|
formData.append('compilerversion', `v${metadata.compiler.version}`) |
||||||
|
formData.append('constructorArguements', submittedContract.abiEncodedConstructorArgs?.replace('0x', '') ?? '') |
||||||
|
|
||||||
|
const url = new URL(this.apiUrl + '/api') |
||||||
|
url.searchParams.append('module', 'contract') |
||||||
|
url.searchParams.append('action', 'verifysourcecode') |
||||||
|
if (this.apiKey) { |
||||||
|
url.searchParams.append('apikey', this.apiKey) |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(url.href, { |
||||||
|
method: 'POST', |
||||||
|
body: formData, |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const responseText = await response.text() |
||||||
|
console.error('Error on Etherscan API verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText) |
||||||
|
throw new Error(responseText) |
||||||
|
} |
||||||
|
|
||||||
|
const verificationResponse: EtherscanRpcResponse = await response.json() |
||||||
|
|
||||||
|
if (verificationResponse.result.includes('already verified')) { |
||||||
|
return { status: 'already verified', receiptId: null, lookupUrl: this.getContractCodeUrl(submittedContract.address) } |
||||||
|
} |
||||||
|
|
||||||
|
if (verificationResponse.status !== '1' || verificationResponse.message !== 'OK') { |
||||||
|
console.error('Error on Etherscan API verification at ' + this.apiUrl + '\nStatus: ' + verificationResponse.status + '\nMessage: ' + verificationResponse.message + '\nResult: ' + verificationResponse.result) |
||||||
|
throw new Error(verificationResponse.result) |
||||||
|
} |
||||||
|
|
||||||
|
const lookupUrl = this.getContractCodeUrl(submittedContract.address) |
||||||
|
return { status: 'pending', receiptId: verificationResponse.result, lookupUrl } |
||||||
|
} |
||||||
|
|
||||||
|
async verifyProxy(submittedContract: SubmittedContract): Promise<VerificationResponse> { |
||||||
|
if (!submittedContract.proxyAddress) { |
||||||
|
throw new Error('SubmittedContract does not have a proxyAddress') |
||||||
|
} |
||||||
|
|
||||||
|
const formData = new FormData() |
||||||
|
formData.append('address', submittedContract.proxyAddress) |
||||||
|
formData.append('expectedimplementation', submittedContract.address) |
||||||
|
|
||||||
|
const url = new URL(this.apiUrl + '/api') |
||||||
|
url.searchParams.append('module', 'contract') |
||||||
|
url.searchParams.append('action', 'verifyproxycontract') |
||||||
|
if (this.apiKey) { |
||||||
|
url.searchParams.append('apikey', this.apiKey) |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(url.href, { |
||||||
|
method: 'POST', |
||||||
|
body: formData, |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const responseText = await response.text() |
||||||
|
console.error('Error on Etherscan API proxy verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText) |
||||||
|
throw new Error(responseText) |
||||||
|
} |
||||||
|
|
||||||
|
const verificationResponse: EtherscanRpcResponse = await response.json() |
||||||
|
|
||||||
|
if (verificationResponse.status !== '1' || verificationResponse.message !== 'OK') { |
||||||
|
console.error('Error on Etherscan API proxy verification at ' + this.apiUrl + '\nStatus: ' + verificationResponse.status + '\nMessage: ' + verificationResponse.message + '\nResult: ' + verificationResponse.result) |
||||||
|
throw new Error(verificationResponse.result) |
||||||
|
} |
||||||
|
|
||||||
|
return { status: 'pending', receiptId: verificationResponse.result } |
||||||
|
} |
||||||
|
|
||||||
|
async checkVerificationStatus(receiptId: string): Promise<VerificationResponse> { |
||||||
|
const url = new URL(this.apiUrl + '/api') |
||||||
|
url.searchParams.append('module', 'contract') |
||||||
|
url.searchParams.append('action', 'checkverifystatus') |
||||||
|
url.searchParams.append('guid', receiptId) |
||||||
|
if (this.apiKey) { |
||||||
|
url.searchParams.append('apikey', this.apiKey) |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(url.href, { method: 'GET' }) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const responseText = await response.text() |
||||||
|
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText) |
||||||
|
throw new Error(responseText) |
||||||
|
} |
||||||
|
|
||||||
|
const checkStatusResponse: EtherscanCheckStatusResponse = await response.json() |
||||||
|
|
||||||
|
if (checkStatusResponse.result.startsWith('Fail - Unable to verify')) { |
||||||
|
return { status: 'failed', receiptId, message: checkStatusResponse.result } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Pending in queue') { |
||||||
|
return { status: 'pending', receiptId } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Pass - Verified') { |
||||||
|
return { status: 'verified', receiptId } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Already Verified') { |
||||||
|
return { status: 'already verified', receiptId } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Unknown UID') { |
||||||
|
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result) |
||||||
|
return { status: 'failed', receiptId, message: checkStatusResponse.result } |
||||||
|
} |
||||||
|
|
||||||
|
if (checkStatusResponse.status !== '1' || !checkStatusResponse.message.startsWith('OK')) { |
||||||
|
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result) |
||||||
|
throw new Error(checkStatusResponse.result) |
||||||
|
} |
||||||
|
|
||||||
|
return { status: 'unknown', receiptId } |
||||||
|
} |
||||||
|
|
||||||
|
async checkProxyVerificationStatus(receiptId: string): Promise<VerificationResponse> { |
||||||
|
const url = new URL(this.apiUrl + '/api') |
||||||
|
url.searchParams.append('module', 'contract') |
||||||
|
url.searchParams.append('action', 'checkproxyverification') |
||||||
|
url.searchParams.append('guid', receiptId) |
||||||
|
if (this.apiKey) { |
||||||
|
url.searchParams.append('apikey', this.apiKey) |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(url.href, { method: 'GET' }) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const responseText = await response.text() |
||||||
|
console.error('Error on Etherscan API check verification status at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText) |
||||||
|
throw new Error(responseText) |
||||||
|
} |
||||||
|
|
||||||
|
const checkStatusResponse: EtherscanRpcResponse = await response.json() |
||||||
|
|
||||||
|
if (checkStatusResponse.result === 'A corresponding implementation contract was unfortunately not detected for the proxy address.' || checkStatusResponse.result === 'The provided expected results are different than the retrieved implementation address!' || checkStatusResponse.result === 'This contract does not look like it contains any delegatecall opcode sequence.') { |
||||||
|
return { status: 'failed', receiptId, message: checkStatusResponse.result } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Verification in progress') { |
||||||
|
return { status: 'pending', receiptId } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result.startsWith("The proxy's") && checkStatusResponse.result.endsWith('and is successfully updated.')) { |
||||||
|
return { status: 'verified', receiptId } |
||||||
|
} |
||||||
|
if (checkStatusResponse.result === 'Unknown UID') { |
||||||
|
console.error('Error on Etherscan API check proxy verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result) |
||||||
|
return { status: 'failed', receiptId, message: checkStatusResponse.result } |
||||||
|
} |
||||||
|
|
||||||
|
if (checkStatusResponse.status !== '1' || !checkStatusResponse.message.startsWith('OK')) { |
||||||
|
console.error('Error on Etherscan API check proxy verification status at ' + this.apiUrl + '\nStatus: ' + checkStatusResponse.status + '\nMessage: ' + checkStatusResponse.message + '\nResult: ' + checkStatusResponse.result) |
||||||
|
throw new Error(checkStatusResponse.result) |
||||||
|
} |
||||||
|
|
||||||
|
return { status: 'unknown', receiptId } |
||||||
|
} |
||||||
|
|
||||||
|
async lookup(contractAddress: string, chainId: string): Promise<LookupResponse> { |
||||||
|
const url = new URL(this.apiUrl + '/api') |
||||||
|
url.searchParams.append('module', 'contract') |
||||||
|
url.searchParams.append('action', 'getsourcecode') |
||||||
|
url.searchParams.append('address', contractAddress) |
||||||
|
if (this.apiKey) { |
||||||
|
url.searchParams.append('apikey', this.apiKey) |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(url.href, { method: 'GET' }) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const responseText = await response.text() |
||||||
|
console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + responseText) |
||||||
|
throw new Error(responseText) |
||||||
|
} |
||||||
|
|
||||||
|
const lookupResponse: EtherscanGetSourceCodeResponse = await response.json() |
||||||
|
|
||||||
|
if (lookupResponse.status !== '1' || !lookupResponse.message.startsWith('OK')) { |
||||||
|
const errorResponse = lookupResponse as unknown as EtherscanRpcResponse |
||||||
|
console.error('Error on Etherscan API lookup at ' + this.apiUrl + '\nStatus: ' + errorResponse.status + '\nMessage: ' + errorResponse.message + '\nResult: ' + errorResponse.result) |
||||||
|
throw new Error(errorResponse.result) |
||||||
|
} |
||||||
|
|
||||||
|
if (lookupResponse.result[0].ABI === 'Contract source code not verified' || !lookupResponse.result[0].SourceCode) { |
||||||
|
return { status: 'not verified' } |
||||||
|
} |
||||||
|
|
||||||
|
const lookupUrl = this.getContractCodeUrl(contractAddress) |
||||||
|
const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.result[0], contractAddress, chainId) |
||||||
|
|
||||||
|
return { status: 'verified', lookupUrl, sourceFiles, targetFilePath } |
||||||
|
} |
||||||
|
|
||||||
|
getContractCodeUrl(address: string): string { |
||||||
|
const url = new URL(this.explorerUrl + `/address/${address}#code`) |
||||||
|
return url.href |
||||||
|
} |
||||||
|
|
||||||
|
processReceivedFiles(source: EtherscanSource, contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { |
||||||
|
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}` |
||||||
|
|
||||||
|
// Covers the cases:
|
||||||
|
// SourceFile: {[FileName]: [content]}
|
||||||
|
// SourceFile: {{sources: {[FileName]: [content]}}}
|
||||||
|
let parsedFiles: any |
||||||
|
try { |
||||||
|
parsedFiles = JSON.parse(source.SourceCode) |
||||||
|
} catch (e) { |
||||||
|
try { |
||||||
|
// Etherscan wraps the Object in one additional bracket
|
||||||
|
parsedFiles = JSON.parse(source.SourceCode.substring(1, source.SourceCode.length - 1)).sources |
||||||
|
} catch (e) {} |
||||||
|
} |
||||||
|
|
||||||
|
if (parsedFiles) { |
||||||
|
const result: SourceFile[] = [] |
||||||
|
let targetFilePath = '' |
||||||
|
for (const [fileName, fileObj] of Object.entries<any>(parsedFiles)) { |
||||||
|
const path = `${filePrefix}/${fileName}` |
||||||
|
|
||||||
|
result.push({ path, content: fileObj.content }) |
||||||
|
|
||||||
|
if (path.endsWith(`/${source.ContractName}.sol`)) { |
||||||
|
targetFilePath = path |
||||||
|
} |
||||||
|
} |
||||||
|
return { sourceFiles: result, targetFilePath } |
||||||
|
} |
||||||
|
|
||||||
|
// Parsing to JSON failed, SourceCode is the code itself
|
||||||
|
const targetFilePath = `${filePrefix}/${source.ContractName}.sol` |
||||||
|
const sourceFiles: SourceFile[] = [{ content: source.SourceCode, path: targetFilePath }] |
||||||
|
return { sourceFiles, targetFilePath } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,169 @@ |
|||||||
|
import { CompilerAbstract, SourcesCode } from '@remix-project/remix-solidity' |
||||||
|
import { AbstractVerifier } from './AbstractVerifier' |
||||||
|
import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types' |
||||||
|
import { ethers } from 'ethers' |
||||||
|
|
||||||
|
interface SourcifyVerificationRequest { |
||||||
|
address: string |
||||||
|
chain: string |
||||||
|
files: Record<string, string> |
||||||
|
creatorTxHash?: string |
||||||
|
chosenContract?: string |
||||||
|
} |
||||||
|
|
||||||
|
type SourcifyVerificationStatus = 'perfect' | 'full' | 'partial' | null |
||||||
|
|
||||||
|
interface SourcifyVerificationResponse { |
||||||
|
result: [ |
||||||
|
{ |
||||||
|
address: string |
||||||
|
chainId: string |
||||||
|
status: SourcifyVerificationStatus |
||||||
|
libraryMap: { |
||||||
|
[key: string]: string |
||||||
|
} |
||||||
|
message?: string |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
|
||||||
|
interface SourcifyErrorResponse { |
||||||
|
error: string |
||||||
|
} |
||||||
|
|
||||||
|
interface SourcifyFile { |
||||||
|
name: string |
||||||
|
path: string |
||||||
|
content: string |
||||||
|
} |
||||||
|
|
||||||
|
interface SourcifyLookupResponse { |
||||||
|
status: Exclude<SourcifyVerificationStatus, null> |
||||||
|
files: SourcifyFile[] |
||||||
|
} |
||||||
|
|
||||||
|
export class SourcifyVerifier extends AbstractVerifier { |
||||||
|
LOOKUP_STORE_DIR = 'sourcify-verified' |
||||||
|
|
||||||
|
async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> { |
||||||
|
const metadataStr = compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata |
||||||
|
const sources = compilerAbstract.source.sources |
||||||
|
|
||||||
|
// 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: SourcifyVerificationRequest = { |
||||||
|
chain: submittedContract.chainId, |
||||||
|
address: submittedContract.address, |
||||||
|
files: { |
||||||
|
'metadata.json': metadataStr, |
||||||
|
...formattedSources, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const response = await fetch(new URL(this.apiUrl + '/verify').href, { |
||||||
|
method: 'POST', |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/json', |
||||||
|
}, |
||||||
|
body: JSON.stringify(body), |
||||||
|
}) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const errorResponse: SourcifyErrorResponse = await response.json() |
||||||
|
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse)) |
||||||
|
throw new Error(errorResponse.error) |
||||||
|
} |
||||||
|
|
||||||
|
const verificationResponse: SourcifyVerificationResponse = await response.json() |
||||||
|
|
||||||
|
if (verificationResponse.result[0].status === null) { |
||||||
|
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + verificationResponse.result[0].message) |
||||||
|
throw new Error(verificationResponse.result[0].message) |
||||||
|
} |
||||||
|
|
||||||
|
// Map to a user-facing status message
|
||||||
|
let status: VerificationStatus = 'unknown' |
||||||
|
let lookupUrl: string | undefined = undefined |
||||||
|
if (verificationResponse.result[0].status === 'perfect' || verificationResponse.result[0].status === 'full') { |
||||||
|
status = 'fully verified' |
||||||
|
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, true) |
||||||
|
} else if (verificationResponse.result[0].status === 'partial') { |
||||||
|
status = 'partially verified' |
||||||
|
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, false) |
||||||
|
} |
||||||
|
|
||||||
|
return { status, receiptId: null, lookupUrl } |
||||||
|
} |
||||||
|
|
||||||
|
async lookup(contractAddress: string, chainId: string): Promise<LookupResponse> { |
||||||
|
const url = new URL(this.apiUrl + `/files/any/${chainId}/${contractAddress}`) |
||||||
|
|
||||||
|
const response = await fetch(url.href, { method: 'GET' }) |
||||||
|
|
||||||
|
if (!response.ok) { |
||||||
|
const errorResponse: SourcifyErrorResponse = await response.json() |
||||||
|
|
||||||
|
if (errorResponse.error === 'Files have not been found!') { |
||||||
|
return { status: 'not verified' } |
||||||
|
} |
||||||
|
|
||||||
|
console.error('Error on Sourcify lookup at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse)) |
||||||
|
throw new Error(errorResponse.error) |
||||||
|
} |
||||||
|
|
||||||
|
const lookupResponse: SourcifyLookupResponse = await response.json() |
||||||
|
|
||||||
|
let status: VerificationStatus = 'unknown' |
||||||
|
let lookupUrl: string | undefined = undefined |
||||||
|
if (lookupResponse.status === 'perfect' || lookupResponse.status === 'full') { |
||||||
|
status = 'fully verified' |
||||||
|
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, true) |
||||||
|
} else if (lookupResponse.status === 'partial') { |
||||||
|
status = 'partially verified' |
||||||
|
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, false) |
||||||
|
} |
||||||
|
|
||||||
|
const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.files, contractAddress, chainId) |
||||||
|
|
||||||
|
return { status, lookupUrl, sourceFiles, targetFilePath } |
||||||
|
} |
||||||
|
|
||||||
|
getContractCodeUrl(address: string, chainId: string, fullMatch: boolean): string { |
||||||
|
const url = new URL(this.explorerUrl + `/contracts/${fullMatch ? 'full_match' : 'partial_match'}/${chainId}/${address}/`) |
||||||
|
return url.href |
||||||
|
} |
||||||
|
|
||||||
|
processReceivedFiles(files: SourcifyFile[], contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } { |
||||||
|
const result: SourceFile[] = [] |
||||||
|
let targetFilePath: string |
||||||
|
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}` |
||||||
|
|
||||||
|
for (const file of files) { |
||||||
|
let filePath: string |
||||||
|
for (const a of [contractAddress, ethers.utils.getAddress(contractAddress)]) { |
||||||
|
const matching = file.path.match(`/${a}/(.*)$`) |
||||||
|
if (matching) { |
||||||
|
filePath = matching[1] |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (filePath) { |
||||||
|
result.push({ path: `${filePrefix}/${filePath}`, content: file.content }) |
||||||
|
} |
||||||
|
|
||||||
|
if (file.name === 'metadata.json') { |
||||||
|
const metadata = JSON.parse(file.content) |
||||||
|
const compilationTarget = metadata.settings.compilationTarget |
||||||
|
const contractPath = Object.keys(compilationTarget)[0] |
||||||
|
targetFilePath = `${filePrefix}/sources/${contractPath}` |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { sourceFiles: result, targetFilePath } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
import type { VerifierIdentifier, VerifierSettings } from '../types' |
||||||
|
import { AbstractVerifier } from './AbstractVerifier' |
||||||
|
import { BlockscoutVerifier } from './BlockscoutVerifier' |
||||||
|
import { EtherscanVerifier } from './EtherscanVerifier' |
||||||
|
import { SourcifyVerifier } from './SourcifyVerifier' |
||||||
|
|
||||||
|
export { AbstractVerifier } from './AbstractVerifier' |
||||||
|
export { BlockscoutVerifier } from './BlockscoutVerifier' |
||||||
|
export { SourcifyVerifier } from './SourcifyVerifier' |
||||||
|
export { EtherscanVerifier } from './EtherscanVerifier' |
||||||
|
|
||||||
|
export function getVerifier(identifier: VerifierIdentifier, settings: VerifierSettings): AbstractVerifier { |
||||||
|
switch (identifier) { |
||||||
|
case 'Sourcify': |
||||||
|
if (!settings?.explorerUrl) { |
||||||
|
throw new Error('The Sourcify verifier requires an explorer URL.') |
||||||
|
} |
||||||
|
return new SourcifyVerifier(settings.apiUrl, settings.explorerUrl) |
||||||
|
case 'Etherscan': |
||||||
|
if (!settings?.explorerUrl) { |
||||||
|
throw new Error('The Etherscan verifier requires an explorer URL.') |
||||||
|
} |
||||||
|
if (!settings?.apiKey) { |
||||||
|
throw new Error('The Etherscan verifier requires an API key.') |
||||||
|
} |
||||||
|
return new EtherscanVerifier(settings.apiUrl, settings.explorerUrl, settings.apiKey) |
||||||
|
case 'Blockscout': |
||||||
|
return new BlockscoutVerifier(settings.apiUrl) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
import React from 'react' |
||||||
|
import type { Chain } from './types' |
||||||
|
import { ContractDropdownSelection } from './components/ContractDropdown' |
||||||
|
|
||||||
|
// Define the type for the context
|
||||||
|
type VerifyFormContextType = { |
||||||
|
selectedChain: Chain | undefined |
||||||
|
setSelectedChain: React.Dispatch<React.SetStateAction<Chain>> |
||||||
|
contractAddress: string |
||||||
|
setContractAddress: React.Dispatch<React.SetStateAction<string>> |
||||||
|
contractAddressError: string |
||||||
|
setContractAddressError: React.Dispatch<React.SetStateAction<string>> |
||||||
|
selectedContract: ContractDropdownSelection | undefined |
||||||
|
setSelectedContract: React.Dispatch<React.SetStateAction<ContractDropdownSelection>> |
||||||
|
proxyAddress: string |
||||||
|
setProxyAddress: React.Dispatch<React.SetStateAction<string>> |
||||||
|
proxyAddressError: string |
||||||
|
setProxyAddressError: React.Dispatch<React.SetStateAction<string>> |
||||||
|
abiEncodedConstructorArgs: string |
||||||
|
setAbiEncodedConstructorArgs: React.Dispatch<React.SetStateAction<string>> |
||||||
|
abiEncodingError: string |
||||||
|
setAbiEncodingError: React.Dispatch<React.SetStateAction<string>> |
||||||
|
} |
||||||
|
|
||||||
|
// Provide a default value with the appropriate types
|
||||||
|
const defaultContextValue: VerifyFormContextType = { |
||||||
|
selectedChain: undefined, |
||||||
|
setSelectedChain: (selectedChain: Chain) => {}, |
||||||
|
contractAddress: '', |
||||||
|
setContractAddress: (contractAddress: string) => {}, |
||||||
|
contractAddressError: '', |
||||||
|
setContractAddressError: (contractAddressError: string) => {}, |
||||||
|
selectedContract: undefined, |
||||||
|
setSelectedContract: (selectedContract: ContractDropdownSelection) => {}, |
||||||
|
proxyAddress: '', |
||||||
|
setProxyAddress: (proxyAddress: string) => {}, |
||||||
|
proxyAddressError: '', |
||||||
|
setProxyAddressError: (contractAddressError: string) => {}, |
||||||
|
abiEncodedConstructorArgs: '', |
||||||
|
setAbiEncodedConstructorArgs: (contractAddproxyAddressress: string) => {}, |
||||||
|
abiEncodingError: '', |
||||||
|
setAbiEncodingError: (contractAddressError: string) => {}, |
||||||
|
} |
||||||
|
|
||||||
|
// Create the context with the type
|
||||||
|
export const VerifyFormContext = React.createContext<VerifyFormContextType>(defaultContextValue) |
@ -0,0 +1,154 @@ |
|||||||
|
import { useState, useEffect, useRef } from 'react' |
||||||
|
|
||||||
|
import { ContractVerificationPluginClient } from './ContractVerificationPluginClient' |
||||||
|
|
||||||
|
import { AppContext } from './AppContext' |
||||||
|
import { VerifyFormContext } from './VerifyFormContext' |
||||||
|
import DisplayRoutes from './routes' |
||||||
|
import type { ContractVerificationSettings, ThemeType, Chain, SubmittedContracts, VerificationReceipt, VerificationResponse } from './types' |
||||||
|
import { mergeChainSettingsWithDefaults } from './utils' |
||||||
|
|
||||||
|
import './App.css' |
||||||
|
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||||
|
import { useLocalStorage } from './hooks/useLocalStorage' |
||||||
|
import { getVerifier } from './Verifiers' |
||||||
|
import { ContractDropdownSelection } from './components/ContractDropdown' |
||||||
|
|
||||||
|
const plugin = new ContractVerificationPluginClient() |
||||||
|
|
||||||
|
const App = () => { |
||||||
|
const [themeType, setThemeType] = useState<ThemeType>('dark') |
||||||
|
const [settings, setSettings] = useLocalStorage<ContractVerificationSettings>('contract-verification:settings', { chains: {} }) |
||||||
|
const [submittedContracts, setSubmittedContracts] = useLocalStorage<SubmittedContracts>('contract-verification:submitted-contracts', {}) |
||||||
|
const [chains, setChains] = useState<Chain[]>([]) // State to hold the chains data
|
||||||
|
const [compilationOutput, setCompilationOutput] = useState<{ [key: string]: CompilerAbstract } | undefined>() |
||||||
|
|
||||||
|
// Form values:
|
||||||
|
const [selectedChain, setSelectedChain] = useState<Chain | undefined>() |
||||||
|
const [contractAddress, setContractAddress] = useState('') |
||||||
|
const [contractAddressError, setContractAddressError] = useState('') |
||||||
|
const [selectedContract, setSelectedContract] = useState<ContractDropdownSelection | undefined>() |
||||||
|
const [proxyAddress, setProxyAddress] = useState('') |
||||||
|
const [proxyAddressError, setProxyAddressError] = useState('') |
||||||
|
const [abiEncodedConstructorArgs, setAbiEncodedConstructorArgs] = useState<string>('') |
||||||
|
const [abiEncodingError, setAbiEncodingError] = useState<string>('') |
||||||
|
|
||||||
|
const timer = useRef(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
plugin.internalEvents.on('verification_activated', () => { |
||||||
|
// Fetch compiler artefacts initially
|
||||||
|
plugin.call('compilerArtefacts' as any, 'getAllCompilerAbstracts').then((obj: any) => { |
||||||
|
setCompilationOutput(obj) |
||||||
|
}) |
||||||
|
|
||||||
|
// Subscribe to compilations
|
||||||
|
plugin.on('compilerArtefacts' as any, 'compilationSaved', (compilerAbstracts: { [key: string]: CompilerAbstract }) => { |
||||||
|
setCompilationOutput((prev) => ({ ...(prev || {}), ...compilerAbstracts })) |
||||||
|
}) |
||||||
|
|
||||||
|
// Fetch chains.json and update state
|
||||||
|
fetch('https://chainid.network/chains.json') |
||||||
|
.then((response) => response.json()) |
||||||
|
.then((data) => setChains(data)) |
||||||
|
.catch((error) => console.error('Failed to fetch chains.json:', error)) |
||||||
|
}) |
||||||
|
|
||||||
|
// Clean up on unmount
|
||||||
|
return () => { |
||||||
|
plugin.off('compilerArtefacts' as any, 'compilationSaved') |
||||||
|
} |
||||||
|
}, []) |
||||||
|
|
||||||
|
// 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') { |
||||||
|
pendingReceipts.push(receipt) |
||||||
|
} |
||||||
|
} |
||||||
|
for (const proxyReceipt of submission.proxyReceipts ?? []) { |
||||||
|
if (proxyReceipt.status === 'pending') { |
||||||
|
pendingReceipts.push(proxyReceipt) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return pendingReceipts |
||||||
|
} |
||||||
|
|
||||||
|
let pendingReceipts = getPendingReceipts(submittedContracts) |
||||||
|
|
||||||
|
if (pendingReceipts.length > 0) { |
||||||
|
if (timer.current) { |
||||||
|
clearInterval(timer.current) |
||||||
|
timer.current = null |
||||||
|
} |
||||||
|
|
||||||
|
const pollStatus = 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 }) |
||||||
|
if (!verifier.checkVerificationStatus) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
let response: VerificationResponse |
||||||
|
if (receipt.isProxyReceipt) { |
||||||
|
response = await verifier.checkProxyVerificationStatus(receiptId) |
||||||
|
} else { |
||||||
|
response = await verifier.checkVerificationStatus(receiptId) |
||||||
|
} |
||||||
|
const { status, message, lookupUrl } = response |
||||||
|
receipt.status = status |
||||||
|
receipt.message = message |
||||||
|
if (lookupUrl) { |
||||||
|
receipt.lookupUrl = lookupUrl |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
receipt.failedChecks++ |
||||||
|
// Only retry 5 times
|
||||||
|
if (receipt.failedChecks >= 5) { |
||||||
|
receipt.status = 'failed' |
||||||
|
receipt.message = e.message |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pendingReceipts = getPendingReceipts(changedSubmittedContracts) |
||||||
|
if (timer.current && pendingReceipts.length === 0) { |
||||||
|
clearInterval(timer.current) |
||||||
|
timer.current = null |
||||||
|
} |
||||||
|
setSubmittedContracts((prev) => Object.assign({}, prev, changedSubmittedContracts)) |
||||||
|
} |
||||||
|
|
||||||
|
timer.current = setInterval(pollStatus, 1000) |
||||||
|
} |
||||||
|
}, [submittedContracts]) |
||||||
|
|
||||||
|
return ( |
||||||
|
<AppContext.Provider value={{ themeType, setThemeType, clientInstance: plugin, settings, setSettings, chains, compilationOutput, submittedContracts, setSubmittedContracts }}> |
||||||
|
<VerifyFormContext.Provider value={{ selectedChain, setSelectedChain, contractAddress, setContractAddress, contractAddressError, setContractAddressError, selectedContract, setSelectedContract, proxyAddress, setProxyAddress, proxyAddressError, setProxyAddressError, abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError }}> |
||||||
|
<DisplayRoutes /> |
||||||
|
</VerifyFormContext.Provider> |
||||||
|
</AppContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default App |
@ -0,0 +1,105 @@ |
|||||||
|
import React, { useMemo } from 'react' |
||||||
|
import { SubmittedContract, VerificationReceipt } from '../types' |
||||||
|
import { shortenAddress, CustomTooltip } from '@remix-ui/helper' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
import { CopyToClipboard } from '@remix-ui/clipboard' |
||||||
|
|
||||||
|
interface AccordionReceiptProps { |
||||||
|
contract: SubmittedContract |
||||||
|
index: number |
||||||
|
} |
||||||
|
|
||||||
|
export const AccordionReceipt: React.FC<AccordionReceiptProps> = ({ contract, index }) => { |
||||||
|
const { chains } = React.useContext(AppContext) |
||||||
|
|
||||||
|
const [expanded, setExpanded] = React.useState(false) |
||||||
|
|
||||||
|
const chain = useMemo(() => { |
||||||
|
return chains.find((c) => c.chainId === parseInt(contract.chainId)) |
||||||
|
}, [contract, chains]) |
||||||
|
const chainName = chain?.name ?? 'Unknown Chain' |
||||||
|
|
||||||
|
const hasProxy = contract.proxyAddress && contract.proxyReceipts |
||||||
|
|
||||||
|
const toggleAccordion = () => { |
||||||
|
setExpanded(!expanded) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={`${expanded ? 'bg-light' : 'border-bottom '}`}> |
||||||
|
<div className="d-flex flex-row align-items-center"> |
||||||
|
<button className="btn" onClick={toggleAccordion} style={{ padding: '0.45rem' }}> |
||||||
|
<i className={`fas ${expanded ? 'fa-angle-down' : 'fa-angle-right'} text-secondary`}></i> |
||||||
|
</button> |
||||||
|
|
||||||
|
<div className="small w-100 text-uppercase overflow-hidden text-nowrap"> |
||||||
|
<CustomTooltip placement="bottom" tooltipClasses=" text-break" tooltipText={`Contract: ${contract.contractName}, Address: ${contract.address}, Chain: ${chainName}, Proxy: ${contract.proxyAddress}`}> |
||||||
|
<span> |
||||||
|
{contract.contractName} at {shortenAddress(contract.address)} {contract.proxyAddress ? 'with proxy' : ''} |
||||||
|
</span> |
||||||
|
</CustomTooltip> |
||||||
|
</div> |
||||||
|
|
||||||
|
<button className="btn" style={{ padding: '0.15rem' }}> |
||||||
|
<CopyToClipboard tip="Copy" content={contract.address} direction={'top'} /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className={`${expanded ? '' : 'd-none'} px-2 pt-2 pb-3 small`}> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">Chain: </span> |
||||||
|
{chainName} ({contract.chainId}) |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">File: </span> |
||||||
|
<span className="text-break">{contract.filePath}</span> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">Submitted at: </span> |
||||||
|
{new Date(contract.date).toLocaleString()} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">Verified at: </span> |
||||||
|
<ReceiptsBody receipts={contract.receipts} /> |
||||||
|
</div> |
||||||
|
|
||||||
|
{hasProxy && ( |
||||||
|
<> |
||||||
|
<div className="mt-3"> |
||||||
|
<span className="font-weight-bold">Proxy Address: </span> |
||||||
|
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipText={contract.proxyAddress}> |
||||||
|
<span>{shortenAddress(contract.proxyAddress)}</span> |
||||||
|
</CustomTooltip> |
||||||
|
<CopyToClipboard tip="Copy" content={contract.proxyAddress} direction={'top'} /> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">Proxy verified at: </span> |
||||||
|
<ReceiptsBody receipts={contract.proxyReceipts} /> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const ReceiptsBody = ({ receipts }: { receipts: VerificationReceipt[] }) => { |
||||||
|
return ( |
||||||
|
<ul className="list-group"> |
||||||
|
{receipts.map((receipt) => ( |
||||||
|
<li className="list-group-item"> |
||||||
|
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipText={`API: ${receipt.verifierInfo.apiUrl}`}> |
||||||
|
<span className="font-weight-bold medium">{receipt.verifierInfo.name}</span> |
||||||
|
</CustomTooltip> |
||||||
|
|
||||||
|
<CustomTooltip placement="top" tooltipClasses=" text-break" tooltipTextClasses="text-capitalize" tooltipText={`Status: ${receipt.status}${receipt.message ? `, Message: ${receipt.message}` : ''}`}> |
||||||
|
<span className="ml-2">{['verified', 'partially verified', 'already verified'].includes(receipt.status) ? <i className="fas fa-check"></i> : receipt.status === 'fully verified' ? <i className="fas fa-check-double"></i> : receipt.status === 'failed' ? <i className="fas fa-xmark"></i> : ['pending', 'awaiting implementation verification'].includes(receipt.status) ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-question"></i>}</span> |
||||||
|
</CustomTooltip> |
||||||
|
|
||||||
|
<span className="ml-2">{!!receipt.lookupUrl && <a href={receipt.lookupUrl} target="_blank" className="fa fas fa-arrow-up-right-from-square"></a>}</span> |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import React, { useEffect, useState } from 'react' |
||||||
|
import { CustomTooltip } from '@remix-ui/helper' |
||||||
|
|
||||||
|
interface ConfigInputProps { |
||||||
|
label: string |
||||||
|
id: string |
||||||
|
secret: boolean |
||||||
|
initialValue: string |
||||||
|
saveResult: (result: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
// Chooses one contract from the compilation output.
|
||||||
|
export const ConfigInput: React.FC<ConfigInputProps> = ({ label, id, secret, initialValue, saveResult }) => { |
||||||
|
const [value, setValue] = useState(initialValue) |
||||||
|
const [enabled, setEnabled] = useState(false) |
||||||
|
|
||||||
|
// Reset state when initialValue changes
|
||||||
|
useEffect(() => { |
||||||
|
setValue(initialValue) |
||||||
|
setEnabled(false) |
||||||
|
}, [initialValue]) |
||||||
|
|
||||||
|
const handleChange = () => { |
||||||
|
setEnabled(true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleSave = () => { |
||||||
|
setEnabled(false) |
||||||
|
saveResult(value) |
||||||
|
} |
||||||
|
|
||||||
|
const handleCancel = () => { |
||||||
|
setEnabled(false) |
||||||
|
setValue(initialValue) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="form-group small mb-0"> |
||||||
|
<label className='mt-3' htmlFor={id}>{label}</label> |
||||||
|
<div className="d-flex flex-row justify-content-start"> |
||||||
|
<input |
||||||
|
type={secret ? 'password' : 'text'} |
||||||
|
className={`form-control small w-100 ${!enabled ? 'bg-transparent pl-0 border-0' : ''}`} |
||||||
|
id={id} |
||||||
|
placeholder={`Add ${label}`} |
||||||
|
value={value} |
||||||
|
onChange={(e) => setValue(e.target.value)} |
||||||
|
disabled={!enabled} |
||||||
|
/> |
||||||
|
|
||||||
|
{ enabled ? ( |
||||||
|
<> |
||||||
|
<button type="button" className="btn btn-primary btn-sm ml-2" onClick={handleSave}> |
||||||
|
Save |
||||||
|
</button> |
||||||
|
<button type="button" className="btn btn-secondary btn-sm ml-2" onClick={handleCancel}> |
||||||
|
Cancel |
||||||
|
</button> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<CustomTooltip tooltipText={`Edit ${label}`}> |
||||||
|
<button type="button" className="btn btn-sm fas fa-pen my-1" style={{ height: '100%' }} disabled={enabled} onClick={handleChange}> |
||||||
|
</button> |
||||||
|
</CustomTooltip> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
import { useContext, useEffect, useRef, useState } from 'react' |
||||||
|
import { ethers } from 'ethers' |
||||||
|
|
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
import { ContractDropdownSelection } from './ContractDropdown' |
||||||
|
|
||||||
|
interface ConstructorArgumentsProps { |
||||||
|
abiEncodedConstructorArgs: string |
||||||
|
setAbiEncodedConstructorArgs: React.Dispatch<React.SetStateAction<string>> |
||||||
|
abiEncodingError: string |
||||||
|
setAbiEncodingError: React.Dispatch<React.SetStateAction<string>> |
||||||
|
selectedContract: ContractDropdownSelection |
||||||
|
} |
||||||
|
|
||||||
|
export const ConstructorArguments: React.FC<ConstructorArgumentsProps> = ({ abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError, selectedContract }) => { |
||||||
|
const { compilationOutput } = useContext(AppContext) |
||||||
|
const [toggleRawInput, setToggleRawInput] = useState<boolean>(false) |
||||||
|
|
||||||
|
const { triggerFilePath, filePath, contractName } = selectedContract |
||||||
|
const selectedCompilerAbstract = triggerFilePath && compilationOutput[triggerFilePath] |
||||||
|
const compiledContract = selectedCompilerAbstract?.data?.contracts?.[filePath]?.[contractName] |
||||||
|
const abi = compiledContract?.abi |
||||||
|
|
||||||
|
const constructorArgs = abi && abi.find((a) => a.type === 'constructor')?.inputs |
||||||
|
|
||||||
|
const decodeConstructorArgs = (value: string) => { |
||||||
|
try { |
||||||
|
const decodedObj = ethers.utils.defaultAbiCoder.decode( |
||||||
|
constructorArgs.map((inp) => inp.type), |
||||||
|
value |
||||||
|
) |
||||||
|
const decoded = decodedObj.map((val) => JSON.stringify(val)) |
||||||
|
return { decoded, errorMessage: '' } |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
const errorMessage = 'Decoding error: ' + e.message |
||||||
|
const decoded = Array(constructorArgs?.length ?? 0).fill('') |
||||||
|
return { decoded, errorMessage } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const [constructorArgsValues, setConstructorArgsValues] = useState<string[]>(abiEncodedConstructorArgs ? decodeConstructorArgs(abiEncodedConstructorArgs).decoded : Array(constructorArgs?.length ?? 0).fill('')) |
||||||
|
|
||||||
|
const constructorArgsInInitialState = useRef(true) |
||||||
|
useEffect(() => { |
||||||
|
if (constructorArgsInInitialState.current) { |
||||||
|
constructorArgsInInitialState.current = false |
||||||
|
return |
||||||
|
} |
||||||
|
setAbiEncodedConstructorArgs('') |
||||||
|
setAbiEncodingError('') |
||||||
|
setConstructorArgsValues(Array(constructorArgs?.length ?? 0).fill('')) |
||||||
|
}, [constructorArgs]) |
||||||
|
|
||||||
|
const handleConstructorArgs = (value: string, index: number) => { |
||||||
|
const changedConstructorArgsValues = [...constructorArgsValues.slice(0, index), value, ...constructorArgsValues.slice(index + 1)] |
||||||
|
setConstructorArgsValues(changedConstructorArgsValues) |
||||||
|
|
||||||
|
// if any constructorArgsValue is falsey (empty etc.), don't encode yet
|
||||||
|
if (changedConstructorArgsValues.some((value) => !value)) { |
||||||
|
setAbiEncodedConstructorArgs('') |
||||||
|
setAbiEncodingError('') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const types = constructorArgs.map((inp) => inp.type) |
||||||
|
const parsedArgsValues = [] |
||||||
|
for (const arg of changedConstructorArgsValues) { |
||||||
|
try { |
||||||
|
parsedArgsValues.push(JSON.parse(arg)) |
||||||
|
} catch (e) { |
||||||
|
parsedArgsValues.push(arg) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
const newAbiEncoding = ethers.utils.defaultAbiCoder.encode(types, parsedArgsValues) |
||||||
|
setAbiEncodedConstructorArgs(newAbiEncoding) |
||||||
|
setAbiEncodingError('') |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
setAbiEncodedConstructorArgs('') |
||||||
|
setAbiEncodingError('Encoding error: ' + e.message) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleRawConstructorArgs = (value: string) => { |
||||||
|
setAbiEncodedConstructorArgs(value) |
||||||
|
const { decoded, errorMessage } = decodeConstructorArgs(value) |
||||||
|
setConstructorArgsValues(decoded) |
||||||
|
setAbiEncodingError(errorMessage) |
||||||
|
} |
||||||
|
|
||||||
|
if (!selectedContract) return null |
||||||
|
if (!compilationOutput && Object.keys(compilationOutput).length === 0) return null |
||||||
|
// No render if no constructor args
|
||||||
|
if (!constructorArgs || constructorArgs.length === 0) return null |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="mt-4"> |
||||||
|
<label>Constructor Arguments</label> |
||||||
|
<div className="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||||
|
<input className="form-check-input custom-control-input" type="checkbox" id="toggleRawInputSwitch" checked={toggleRawInput} onChange={() => setToggleRawInput(!toggleRawInput)} /> |
||||||
|
<label className="m-0 form-check-label custom-control-label" style={{ paddingTop: '2px' }} htmlFor="toggleRawInputSwitch"> |
||||||
|
Enter raw ABI-encoded constructor arguments |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
{toggleRawInput ? ( |
||||||
|
<div> |
||||||
|
{' '} |
||||||
|
<textarea className="form-control" rows={5} placeholder="0x00000000000000000000000000000000d41867734bbee4c6863d9255b2b06ac1..." value={abiEncodedConstructorArgs} onChange={(e) => handleRawConstructorArgs(e.target.value)} /> |
||||||
|
{abiEncodingError && <div className="text-danger small">{abiEncodingError}</div>} |
||||||
|
</div> |
||||||
|
) : ( |
||||||
|
<div> |
||||||
|
{constructorArgs.map((inp, i) => ( |
||||||
|
<div key={`constructor-arg-${inp.name}`} className="d-flex flex-row align-items-center justify-content-between mb-2"> |
||||||
|
<div className="mr-2 small">{inp.name}</div> |
||||||
|
<input className="form-control w-50" placeholder={inp.type} value={constructorArgsValues[i] ?? ''} onChange={(e) => handleConstructorArgs(e.target.value, i)} /> |
||||||
|
</div> |
||||||
|
))} |
||||||
|
{abiEncodedConstructorArgs && ( |
||||||
|
<div> |
||||||
|
<label className="form-check-label" htmlFor="rawAbiEncodingResult"> |
||||||
|
ABI-encoded constructor arguments: |
||||||
|
</label> |
||||||
|
<textarea className="form-control" rows={5} disabled value={abiEncodedConstructorArgs} id="rawAbiEncodingResult" style={{ opacity: 0.5 }} /> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{abiEncodingError && <div className="text-danger small">{abiEncodingError}</div>} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import React, { useEffect, useState, useContext } from 'react' |
||||||
|
import { ethers } from 'ethers/' |
||||||
|
|
||||||
|
interface ContractAddressInputProps { |
||||||
|
label: string |
||||||
|
id: string |
||||||
|
contractAddress: string |
||||||
|
setContractAddress: (address: string) => void |
||||||
|
contractAddressError: string |
||||||
|
setContractAddressError: (error: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
// Chooses one contract from the compilation output.
|
||||||
|
export const ContractAddressInput: React.FC<ContractAddressInputProps> = ({ label, id, contractAddress, setContractAddress, contractAddressError, setContractAddressError }) => { |
||||||
|
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="form-group"> |
||||||
|
<label htmlFor={id}>{label}</label> |
||||||
|
<div>{contractAddressError && <div className="text-danger">{contractAddressError}</div>}</div> |
||||||
|
<input type="text" className="form-control" id={id} placeholder="0x2738d13E81e..." value={contractAddress} onChange={handleAddressChange} /> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
.disabled-cursor { |
||||||
|
cursor: not-allowed; |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
import React, { useEffect, useState, useContext, Fragment } from 'react' |
||||||
|
import './ContractDropdown.css' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
|
||||||
|
export interface ContractDropdownSelection { |
||||||
|
triggerFilePath: string |
||||||
|
filePath: string |
||||||
|
contractName: string |
||||||
|
} |
||||||
|
|
||||||
|
interface ContractDropdownProps { |
||||||
|
label: string |
||||||
|
id: string |
||||||
|
selectedContract: ContractDropdownSelection |
||||||
|
setSelectedContract: (selection: ContractDropdownSelection) => void |
||||||
|
} |
||||||
|
|
||||||
|
// Chooses one contract from the compilation output.
|
||||||
|
export const ContractDropdown: React.FC<ContractDropdownProps> = ({ label, id, selectedContract, setSelectedContract }) => { |
||||||
|
const { compilationOutput } = useContext(AppContext) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!compilationOutput || !!selectedContract) return |
||||||
|
// Otherwise select the first by default
|
||||||
|
const triggerFilePath = Object.keys(compilationOutput)[0] |
||||||
|
const contracts = compilationOutput[triggerFilePath]?.data?.contracts |
||||||
|
if (contracts && Object.keys(contracts).length) { |
||||||
|
const firstFilePath = Object.keys(contracts)[0] |
||||||
|
const contractsInFile = contracts[firstFilePath] |
||||||
|
if (contractsInFile && Object.keys(contractsInFile).length) { |
||||||
|
const firstContractName = Object.keys(contractsInFile)[0] |
||||||
|
setSelectedContract({ triggerFilePath, filePath: firstFilePath, contractName: firstContractName }) |
||||||
|
} |
||||||
|
} |
||||||
|
}, [compilationOutput]) |
||||||
|
|
||||||
|
const handleSelectContract = (event: React.ChangeEvent<HTMLSelectElement>) => { |
||||||
|
setSelectedContract(JSON.parse(event.target.value)) |
||||||
|
} |
||||||
|
|
||||||
|
const hasContracts = compilationOutput && Object.keys(compilationOutput).length > 0 |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="form-group"> |
||||||
|
<label htmlFor={id}>{label}</label> |
||||||
|
<select value={selectedContract ? JSON.stringify(selectedContract) : ''} |
||||||
|
className={`form-control custom-select pr-4 ${!hasContracts ? 'disabled-cursor text-warning' : ''}`} |
||||||
|
id={id} |
||||||
|
disabled={!hasContracts} |
||||||
|
onChange={handleSelectContract} |
||||||
|
> |
||||||
|
{hasContracts ? ( |
||||||
|
Object.keys(compilationOutput).map((compilationTriggerFileName) => ( |
||||||
|
<optgroup key={compilationTriggerFileName} label={`Compilation trigger: ${compilationTriggerFileName}`}> |
||||||
|
{Object.keys(compilationOutput[compilationTriggerFileName].data.contracts).map((fileName) => { |
||||||
|
return Object.keys(compilationOutput[compilationTriggerFileName].data.contracts[fileName]).map((contractName) => { |
||||||
|
const value = JSON.stringify({ triggerFilePath: compilationTriggerFileName, filePath: fileName, contractName: contractName }) |
||||||
|
return ( |
||||||
|
<option key={`${compilationTriggerFileName}:${fileName}:${contractName}`} value={value}> |
||||||
|
{contractName} - {fileName} |
||||||
|
</option> |
||||||
|
) |
||||||
|
}) |
||||||
|
})} |
||||||
|
</optgroup> |
||||||
|
)) |
||||||
|
) : ( |
||||||
|
<option>Compiled contract required</option> |
||||||
|
)} |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { NavLink } from 'react-router-dom' |
||||||
|
|
||||||
|
interface NavItemProps { |
||||||
|
to: string |
||||||
|
icon: JSX.Element |
||||||
|
title: string |
||||||
|
} |
||||||
|
|
||||||
|
const NavItem: React.FC<NavItemProps> = ({ to, icon, title }) => { |
||||||
|
return ( |
||||||
|
<NavLink |
||||||
|
to={to} |
||||||
|
className={({ isActive }) => 'text-decoration-none d-flex px-1 py-1 flex-column justify-content-center small ' + (isActive ? "bg-light border-top border-left border-right" : "border-0 bg-transparent")} |
||||||
|
> |
||||||
|
<span className=''> |
||||||
|
<span>{icon}</span> |
||||||
|
<span className="ml-2">{title}</span> |
||||||
|
</span> |
||||||
|
</NavLink> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const NavMenu = () => { |
||||||
|
return ( |
||||||
|
<nav className="d-flex medium flex-row w-100" style={{backgroundColor: 'var(--body-bg)!important'}}> |
||||||
|
<NavItem to="/" icon={<i className="fas fa-home"></i>} title="Verify" /> |
||||||
|
<NavItem to="/receipts" icon={<i className="fas fa-receipt"></i>} title="Receipts" /> |
||||||
|
<NavItem to="/lookup" icon={<i className="fas fa-search"></i>} title="Lookup" /> |
||||||
|
<NavItem to="/settings" icon={<i className="fas fa-cog"></i>} title="Settings" /> |
||||||
|
</nav> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
import React, { useState, useEffect, useRef, useMemo } from 'react' |
||||||
|
import Fuse from 'fuse.js' |
||||||
|
import type { Chain } from '../types' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
|
||||||
|
function getChainDescriptor(chain: Chain): string { |
||||||
|
if (!chain) return '' |
||||||
|
return `${chain.title || chain.name} (${chain.chainId})` |
||||||
|
} |
||||||
|
|
||||||
|
interface DropdownProps { |
||||||
|
label: string |
||||||
|
id: string |
||||||
|
setSelectedChain: (chain: Chain) => void |
||||||
|
selectedChain: Chain |
||||||
|
} |
||||||
|
|
||||||
|
export const SearchableChainDropdown: React.FC<DropdownProps> = ({ label, id, setSelectedChain, selectedChain }) => { |
||||||
|
const { chains } = React.useContext(AppContext) |
||||||
|
const ethereumChainIds = [1, 11155111, 17000] |
||||||
|
|
||||||
|
// Add Ethereum chains to the head of the chains list. Sort the rest alphabetically
|
||||||
|
const dropdownChains = useMemo( |
||||||
|
() => |
||||||
|
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.chainId) - ethereumChainIds.indexOf(b.chainId) |
||||||
|
|
||||||
|
return (a.title || a.name).localeCompare(b.title || b.name) |
||||||
|
}), |
||||||
|
[chains] |
||||||
|
) |
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState(selectedChain ? getChainDescriptor(selectedChain) : '') |
||||||
|
const [isOpen, setIsOpen] = useState(false) |
||||||
|
const [filteredOptions, setFilteredOptions] = useState<Chain[]>(dropdownChains) |
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null) |
||||||
|
|
||||||
|
const fuse = new Fuse(dropdownChains, { |
||||||
|
keys: ['name', 'chainId', 'title'], |
||||||
|
threshold: 0.3, |
||||||
|
}) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (searchTerm === '') { |
||||||
|
setFilteredOptions(dropdownChains) |
||||||
|
} else { |
||||||
|
const result = fuse.search(searchTerm) |
||||||
|
setFilteredOptions(result.map(({ item }) => item)) |
||||||
|
} |
||||||
|
}, [searchTerm, dropdownChains]) |
||||||
|
|
||||||
|
// Close dropdown when user clicks outside
|
||||||
|
useEffect(() => { |
||||||
|
const handleClickOutside = (event: MouseEvent) => { |
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { |
||||||
|
setIsOpen(false) |
||||||
|
setSearchTerm(getChainDescriptor(selectedChain)) |
||||||
|
} |
||||||
|
} |
||||||
|
document.addEventListener('mousedown', handleClickOutside) |
||||||
|
return () => { |
||||||
|
document.removeEventListener('mousedown', handleClickOutside) |
||||||
|
} |
||||||
|
}, [selectedChain]) |
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
setSearchTerm(e.target.value) |
||||||
|
setIsOpen(true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleOptionClick = (option: Chain) => { |
||||||
|
setSelectedChain(option) |
||||||
|
setSearchTerm(getChainDescriptor(option)) |
||||||
|
setIsOpen(false) |
||||||
|
} |
||||||
|
|
||||||
|
const openDropdown = () => { |
||||||
|
setIsOpen(true) |
||||||
|
setSearchTerm('') |
||||||
|
} |
||||||
|
|
||||||
|
if (!dropdownChains || dropdownChains.length === 0) { |
||||||
|
return ( |
||||||
|
<div className="dropdown"> |
||||||
|
<label htmlFor={id}>{label}</label> |
||||||
|
<div>Loading chains...</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="dropdown mb-3" ref={dropdownRef}> |
||||||
|
{' '} |
||||||
|
{/* Add ref here */} |
||||||
|
<label htmlFor={id}>{label}</label> |
||||||
|
<input type="text" value={searchTerm} onChange={handleInputChange} onClick={openDropdown} placeholder="Select a chain" className="form-control" /> |
||||||
|
{isOpen && ( |
||||||
|
<ul className="dropdown-menu show w-100 bg-light" style={{ maxHeight: '400px', overflowY: 'auto' }}> |
||||||
|
{filteredOptions.map((chain) => ( |
||||||
|
<li key={chain.chainId} onClick={() => handleOptionClick(chain)} className={`dropdown-item text-dark ${selectedChain?.chainId === chain.chainId ? 'active' : ''}`} style={{ cursor: 'pointer', whiteSpace: 'normal' }}> |
||||||
|
{getChainDescriptor(chain)} |
||||||
|
</li> |
||||||
|
))} |
||||||
|
</ul> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
export { NavMenu } from './NavMenu' |
||||||
|
export { ContractDropdown } from './ContractDropdown' |
||||||
|
export { SearchableChainDropdown } from './SearchableChainDropdown' |
||||||
|
export { ContractAddressInput } from './ContractAddressInput' |
||||||
|
export { ConfigInput } from './ConfigInput' |
@ -0,0 +1,33 @@ |
|||||||
|
import { useEffect, useState } from 'react' |
||||||
|
import { Chain, ChainSettings } from '../types' |
||||||
|
|
||||||
|
export function useSourcifySupported(selectedChain: Chain, chainSettings: ChainSettings): boolean { |
||||||
|
const [sourcifySupported, setSourcifySupported] = useState(false) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// Unsupported until fetch returns
|
||||||
|
setSourcifySupported(false) |
||||||
|
|
||||||
|
const sourcifyApi = chainSettings?.verifiers['Sourcify']?.apiUrl |
||||||
|
if (!sourcifyApi) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const queriedChainId = selectedChain.chainId |
||||||
|
const chainsUrl = new URL(sourcifyApi + '/chains') |
||||||
|
|
||||||
|
fetch(chainsUrl.href, { method: 'GET' }) |
||||||
|
.then((response) => response.json()) |
||||||
|
.then((result: Array<{ chainId: number }>) => { |
||||||
|
// Makes sure that the selectedChain didn't change while the request is running
|
||||||
|
if (selectedChain.chainId === queriedChainId && result.find((chain) => chain.chainId === queriedChainId)) { |
||||||
|
setSourcifySupported(true) |
||||||
|
} |
||||||
|
}) |
||||||
|
.catch((error) => { |
||||||
|
console.error('Failed to fetch chains.json:', error) |
||||||
|
}) |
||||||
|
}, [selectedChain, chainSettings]) |
||||||
|
|
||||||
|
return sourcifySupported |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
import React, { PropsWithChildren } from 'react' |
||||||
|
|
||||||
|
import { NavMenu } from '../components/NavMenu' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
from: string |
||||||
|
title?: string |
||||||
|
description?: string |
||||||
|
} |
||||||
|
|
||||||
|
export const DefaultLayout = ({ children, title, description }: PropsWithChildren<Props>) => { |
||||||
|
return ( |
||||||
|
<div className="d-flex flex-column h-100"> |
||||||
|
<NavMenu /> |
||||||
|
<div className="py-4 px-3 flex-grow-1 bg-light" style={{ overflowY: 'auto' }}> |
||||||
|
<div> |
||||||
|
<p className="text-center" style={{ fontSize: '0.8rem' }}> |
||||||
|
{description} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
{children} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { DefaultLayout } from './Default' |
@ -0,0 +1,49 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { HashRouter as Router, Route, Routes } from 'react-router-dom' |
||||||
|
|
||||||
|
import { VerifyView, ReceiptsView, LookupView, SettingsView } from './views' |
||||||
|
import { DefaultLayout } from './layouts' |
||||||
|
|
||||||
|
const DisplayRoutes = () => ( |
||||||
|
<Router> |
||||||
|
<Routes> |
||||||
|
<Route |
||||||
|
path="/" |
||||||
|
element={ |
||||||
|
<DefaultLayout from="/" title="Verify" description="Verify compiled contracts on different verification services"> |
||||||
|
<VerifyView /> |
||||||
|
</DefaultLayout> |
||||||
|
} |
||||||
|
/> |
||||||
|
|
||||||
|
<Route |
||||||
|
path="/receipts" |
||||||
|
element={ |
||||||
|
<DefaultLayout from="/" title="Receipts" description="Check the verification statuses of contracts submitted for verification"> |
||||||
|
<ReceiptsView /> |
||||||
|
</DefaultLayout> |
||||||
|
} |
||||||
|
/> |
||||||
|
|
||||||
|
<Route |
||||||
|
path="/lookup" |
||||||
|
element={ |
||||||
|
<DefaultLayout from="/" title="Lookup" description="Search for verified contracts and download them to Remix"> |
||||||
|
<LookupView /> |
||||||
|
</DefaultLayout> |
||||||
|
} |
||||||
|
/> |
||||||
|
|
||||||
|
<Route |
||||||
|
path="/settings" |
||||||
|
element={ |
||||||
|
<DefaultLayout from="/" title="Settings" description="Customize settings for each verification service and chain"> |
||||||
|
<SettingsView /> |
||||||
|
</DefaultLayout> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Routes> |
||||||
|
</Router> |
||||||
|
) |
||||||
|
|
||||||
|
export default DisplayRoutes |
@ -0,0 +1,19 @@ |
|||||||
|
import { VerifierIdentifier } from './VerificationTypes' |
||||||
|
|
||||||
|
export interface VerifierSettings { |
||||||
|
apiUrl?: string |
||||||
|
explorerUrl?: string |
||||||
|
apiKey?: string |
||||||
|
} |
||||||
|
|
||||||
|
export type SettingsForVerifier = Partial<Record<VerifierIdentifier, VerifierSettings>> |
||||||
|
|
||||||
|
export interface ChainSettings { |
||||||
|
verifiers: SettingsForVerifier |
||||||
|
} |
||||||
|
|
||||||
|
export type SettingsForChains = Record<string, ChainSettings> |
||||||
|
|
||||||
|
export interface ContractVerificationSettings { |
||||||
|
chains: SettingsForChains |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export type ThemeType = 'dark' | 'light' |
@ -0,0 +1,80 @@ |
|||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
export type VerifierIdentifier = 'Sourcify' | 'Etherscan' | 'Blockscout' |
||||||
|
export const VERIFIERS: VerifierIdentifier[] = ['Sourcify', 'Etherscan', 'Blockscout'] |
||||||
|
|
||||||
|
export interface VerifierInfo { |
||||||
|
name: VerifierIdentifier |
||||||
|
apiUrl: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface VerificationReceipt { |
||||||
|
receiptId?: string |
||||||
|
verifierInfo: VerifierInfo |
||||||
|
status: VerificationStatus |
||||||
|
message?: string |
||||||
|
lookupUrl?: string |
||||||
|
contractId: string |
||||||
|
isProxyReceipt: boolean |
||||||
|
failedChecks: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface SubmittedContract { |
||||||
|
id: string |
||||||
|
filePath: string |
||||||
|
contractName: string |
||||||
|
chainId: string |
||||||
|
address: string |
||||||
|
abiEncodedConstructorArgs?: string |
||||||
|
date: string |
||||||
|
receipts: VerificationReceipt[] |
||||||
|
// Only present if the contract is behind a proxy
|
||||||
|
proxyAddress?: string |
||||||
|
proxyReceipts?: VerificationReceipt[] |
||||||
|
} |
||||||
|
|
||||||
|
// This and all nested subtypes should be pure interfaces, so they can be converted to JSON easily
|
||||||
|
export interface SubmittedContracts { |
||||||
|
[id: string]: SubmittedContract |
||||||
|
} |
||||||
|
|
||||||
|
type SourcifyStatus = 'fully verified' | 'partially verified' |
||||||
|
type EtherscanStatus = 'verified' | 'already verified' |
||||||
|
export type VerificationStatus = SourcifyStatus | EtherscanStatus | 'failed' | 'pending' | 'awaiting implementation verification' | 'not verified' | 'lookup failed' | 'unknown' |
||||||
|
|
||||||
|
export interface VerificationResponse { |
||||||
|
status: VerificationStatus |
||||||
|
receiptId: string | null |
||||||
|
message?: string |
||||||
|
lookupUrl?: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface SourceFile { |
||||||
|
// Should be in the correct format for creating the files in Remix
|
||||||
|
path: string |
||||||
|
content: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface LookupResponse { |
||||||
|
status: VerificationStatus |
||||||
|
lookupUrl?: string |
||||||
|
sourceFiles?: SourceFile[] |
||||||
|
targetFilePath?: string |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
export * from './ThemeType' |
||||||
|
export * from './SettingsTypes' |
||||||
|
export * from './VerificationTypes' |
@ -0,0 +1,576 @@ |
|||||||
|
{ |
||||||
|
"Sourcify": { |
||||||
|
"apiUrl": "https://sourcify.dev/server", |
||||||
|
"explorerUrl": "https://repo.sourcify.dev" |
||||||
|
}, |
||||||
|
"Etherscan": { |
||||||
|
"1": { |
||||||
|
"apiUrl": "https://api.etherscan.io", |
||||||
|
"explorerUrl": "https://etherscan.io" |
||||||
|
}, |
||||||
|
"56": { |
||||||
|
"apiUrl": "https://api.bscscan.com", |
||||||
|
"explorerUrl": "https://bscscan.com" |
||||||
|
}, |
||||||
|
"137": { |
||||||
|
"apiUrl": "https://api.polygonscan.com", |
||||||
|
"explorerUrl": "https://polygonscan.com" |
||||||
|
}, |
||||||
|
"250": { |
||||||
|
"apiUrl": "https://api.ftmscan.com", |
||||||
|
"explorerUrl": "https://ftmscan.com" |
||||||
|
}, |
||||||
|
"42161": { |
||||||
|
"apiUrl": "https://api.arbiscan.io", |
||||||
|
"explorerUrl": "https://arbiscan.io" |
||||||
|
}, |
||||||
|
"43114": { |
||||||
|
"apiUrl": "https://api.snowtrace.io", |
||||||
|
"explorerUrl": "https://snowtrace.io" |
||||||
|
}, |
||||||
|
"1285": { |
||||||
|
"apiUrl": "https://api-moonriver.moonscan.io", |
||||||
|
"explorerUrl": "https://moonscan.io" |
||||||
|
}, |
||||||
|
"1284": { |
||||||
|
"apiUrl": "https://api-moonbeam.moonscan.io", |
||||||
|
"explorerUrl": "https://moonscan.io" |
||||||
|
}, |
||||||
|
"25": { |
||||||
|
"apiUrl": "https://api.cronoscan.com", |
||||||
|
"explorerUrl": "https://cronoscan.com" |
||||||
|
}, |
||||||
|
"199": { |
||||||
|
"apiUrl": "https://api.bttcscan.com", |
||||||
|
"explorerUrl": "https://bttcscan.com" |
||||||
|
}, |
||||||
|
"10": { |
||||||
|
"apiUrl": "https://api-optimistic.etherscan.io", |
||||||
|
"explorerUrl": "https://optimistic.etherscan.io" |
||||||
|
}, |
||||||
|
"42220": { |
||||||
|
"apiUrl": "https://api.celoscan.io", |
||||||
|
"explorerUrl": "https://celoscan.io" |
||||||
|
}, |
||||||
|
"288": { |
||||||
|
"apiUrl": "https://api.bobascan.com", |
||||||
|
"explorerUrl": "https://bobascan.com" |
||||||
|
}, |
||||||
|
"100": { |
||||||
|
"apiUrl": "https://api.gnosisscan.io", |
||||||
|
"explorerUrl": "https://gnosisscan.io" |
||||||
|
}, |
||||||
|
"1101": { |
||||||
|
"apiUrl": "https://api-zkevm.polygonscan.com", |
||||||
|
"explorerUrl": "https://zkevm.polygonscan.com" |
||||||
|
}, |
||||||
|
"59144": { |
||||||
|
"apiUrl": "https://api.lineascan.build", |
||||||
|
"explorerUrl": "https://lineascan.build" |
||||||
|
}, |
||||||
|
"8453": { |
||||||
|
"apiUrl": "https://api.basescan.org", |
||||||
|
"explorerUrl": "https://basescan.org" |
||||||
|
}, |
||||||
|
"534352": { |
||||||
|
"apiUrl": "https://api.scrollscan.com", |
||||||
|
"explorerUrl": "https://scrollscan.com" |
||||||
|
}, |
||||||
|
"17000": { |
||||||
|
"apiUrl": "https://api-holesky.etherscan.io", |
||||||
|
"explorerUrl": "https://holesky.etherscan.io" |
||||||
|
}, |
||||||
|
"11155111": { |
||||||
|
"apiUrl": "https://api-sepolia.etherscan.io", |
||||||
|
"explorerUrl": "https://sepolia.etherscan.io" |
||||||
|
}, |
||||||
|
"97": { |
||||||
|
"apiUrl": "https://api-testnet.bscscan.com", |
||||||
|
"explorerUrl": "https://bscscan.com" |
||||||
|
}, |
||||||
|
"80001": { |
||||||
|
"apiUrl": "https://api-testnet.polygonscan.com", |
||||||
|
"explorerUrl": "https://polygonscan.com" |
||||||
|
}, |
||||||
|
"4002": { |
||||||
|
"apiUrl": "https://api-testnet.ftmscan.com", |
||||||
|
"explorerUrl": "https://ftmscan.com" |
||||||
|
}, |
||||||
|
"421611": { |
||||||
|
"apiUrl": "https://api-testnet.arbiscan.io", |
||||||
|
"explorerUrl": "https://arbiscan.io" |
||||||
|
}, |
||||||
|
"42170": { |
||||||
|
"apiUrl": "https://api-nova.arbiscan.io", |
||||||
|
"explorerUrl": "https://nova.arbiscan.io" |
||||||
|
}, |
||||||
|
"43113": { |
||||||
|
"apiUrl": "https://api-testnet.snowtrace.io", |
||||||
|
"explorerUrl": "https://snowtrace.io" |
||||||
|
}, |
||||||
|
"1287": { |
||||||
|
"apiUrl": "https://api-moonbase.moonscan.io", |
||||||
|
"explorerUrl": "https://moonscan.io" |
||||||
|
}, |
||||||
|
"338": { |
||||||
|
"apiUrl": "https://api-testnet.cronoscan.com", |
||||||
|
"explorerUrl": "https://cronoscan.com" |
||||||
|
}, |
||||||
|
"1028": { |
||||||
|
"apiUrl": "https://api-testnet.bttcscan.com", |
||||||
|
"explorerUrl": "https://bttcscan.com" |
||||||
|
}, |
||||||
|
"44787": { |
||||||
|
"apiUrl": "https://api-alfajores.celoscan.io", |
||||||
|
"explorerUrl": "https://alfajores.celoscan.io" |
||||||
|
}, |
||||||
|
"2888": { |
||||||
|
"apiUrl": "https://api-testnet.bobascan.com", |
||||||
|
"explorerUrl": "https://bobascan.com" |
||||||
|
}, |
||||||
|
"84532": { |
||||||
|
"apiUrl": "https://api-sepolia.basescan.org", |
||||||
|
"explorerUrl": "https://sepolia.basescan.org" |
||||||
|
}, |
||||||
|
"1442": { |
||||||
|
"apiUrl": "https://api-testnet-zkevm.polygonscan.com", |
||||||
|
"explorerUrl": "https://zkevm.polygonscan.com" |
||||||
|
}, |
||||||
|
"59140": { |
||||||
|
"apiUrl": "https://api-testnet.lineascan.build", |
||||||
|
"explorerUrl": "https://lineascan.build" |
||||||
|
}, |
||||||
|
"534351": { |
||||||
|
"apiUrl": "https://api-sepolia.scrollscan.com", |
||||||
|
"explorerUrl": "https://sepolia.scrollscan.com" |
||||||
|
} |
||||||
|
}, |
||||||
|
"Blockscout": { |
||||||
|
"1": { |
||||||
|
"apiUrl": "https://eth.blockscout.com" |
||||||
|
}, |
||||||
|
"5": { |
||||||
|
"apiUrl": "https://eth-goerli.blockscout.com" |
||||||
|
}, |
||||||
|
"10": { |
||||||
|
"apiUrl": "https://optimism.blockscout.com" |
||||||
|
}, |
||||||
|
"30": { |
||||||
|
"apiUrl": "https://rootstock.blockscout.com" |
||||||
|
}, |
||||||
|
"31": { |
||||||
|
"apiUrl": "https://rootstock-testnet.blockscout.com" |
||||||
|
}, |
||||||
|
"42": { |
||||||
|
"apiUrl": "https://explorer.execution.mainnet.lukso.network" |
||||||
|
}, |
||||||
|
"61": { |
||||||
|
"apiUrl": "https://etc.blockscout.com" |
||||||
|
}, |
||||||
|
"63": { |
||||||
|
"apiUrl": "https://etc-mordor.blockscout.com" |
||||||
|
}, |
||||||
|
"81": { |
||||||
|
"apiUrl": "https://shibuya.blockscout.com" |
||||||
|
}, |
||||||
|
"100": { |
||||||
|
"apiUrl": "https://gnosis.blockscout.com" |
||||||
|
}, |
||||||
|
"109": { |
||||||
|
"apiUrl": "https://www.shibariumscan.io" |
||||||
|
}, |
||||||
|
"111": { |
||||||
|
"apiUrl": "https://testnet-explorer.gobob.xyz" |
||||||
|
}, |
||||||
|
"122": { |
||||||
|
"apiUrl": "https://explorer.fuse.io" |
||||||
|
}, |
||||||
|
"123": { |
||||||
|
"apiUrl": "https://explorer.fusespark.io" |
||||||
|
}, |
||||||
|
"137": { |
||||||
|
"apiUrl": "https://polygon.blockscout.com" |
||||||
|
}, |
||||||
|
"148": { |
||||||
|
"apiUrl": "https://explorer.evm.shimmer.network" |
||||||
|
}, |
||||||
|
"157": { |
||||||
|
"apiUrl": "https://puppyscan.shib.io" |
||||||
|
}, |
||||||
|
"169": { |
||||||
|
"apiUrl": "https://pacific-explorer.manta.network" |
||||||
|
}, |
||||||
|
"185": { |
||||||
|
"apiUrl": "https://explorer.mintchain.io" |
||||||
|
}, |
||||||
|
"197": { |
||||||
|
"apiUrl": "https://dxb.vrcscan.com" |
||||||
|
}, |
||||||
|
"248": { |
||||||
|
"apiUrl": "https://explorer.oasys.games" |
||||||
|
}, |
||||||
|
"291": { |
||||||
|
"apiUrl": "https://explorer.orderly.network" |
||||||
|
}, |
||||||
|
"300": { |
||||||
|
"apiUrl": "https://zksync-sepolia.blockscout.com" |
||||||
|
}, |
||||||
|
"311": { |
||||||
|
"apiUrl": "https://omaxscan.com" |
||||||
|
}, |
||||||
|
"313": { |
||||||
|
"apiUrl": "https://ncnscan.com" |
||||||
|
}, |
||||||
|
"324": { |
||||||
|
"apiUrl": "https://zksync.blockscout.com" |
||||||
|
}, |
||||||
|
"336": { |
||||||
|
"apiUrl": "https://shiden.blockscout.com" |
||||||
|
}, |
||||||
|
"360": { |
||||||
|
"apiUrl": "https://molten.calderaexplorer.xyz" |
||||||
|
}, |
||||||
|
"372": { |
||||||
|
"apiUrl": "https://explorer.fortresschain.finance" |
||||||
|
}, |
||||||
|
"416": { |
||||||
|
"apiUrl": "https://explorer.sx.technology" |
||||||
|
}, |
||||||
|
"570": { |
||||||
|
"apiUrl": "https://explorer.rollux.com" |
||||||
|
}, |
||||||
|
"592": { |
||||||
|
"apiUrl": "https://astar.blockscout.com" |
||||||
|
}, |
||||||
|
"648": { |
||||||
|
"apiUrl": "https://explorer-endurance.fusionist.io" |
||||||
|
}, |
||||||
|
"713": { |
||||||
|
"apiUrl": "https://vrcscan.com" |
||||||
|
}, |
||||||
|
"721": { |
||||||
|
"apiUrl": "https://explorer.lycanchain.com" |
||||||
|
}, |
||||||
|
"813": { |
||||||
|
"apiUrl": "https://qng.qitmeer.io" |
||||||
|
}, |
||||||
|
"879": { |
||||||
|
"apiUrl": "https://kadscan.kadsea.org" |
||||||
|
}, |
||||||
|
"911": { |
||||||
|
"apiUrl": "https://scan.taprootchain.io" |
||||||
|
}, |
||||||
|
"919": { |
||||||
|
"apiUrl": "https://sepolia.explorer.mode.network" |
||||||
|
}, |
||||||
|
"957": { |
||||||
|
"apiUrl": "https://explorer.lyra.finance" |
||||||
|
}, |
||||||
|
"1073": { |
||||||
|
"apiUrl": "https://explorer.evm.testnet.shimmer.network" |
||||||
|
}, |
||||||
|
"1075": { |
||||||
|
"apiUrl": "https://explorer.evm.testnet.iotaledger.net" |
||||||
|
}, |
||||||
|
"1101": { |
||||||
|
"apiUrl": "https://zkevm.blockscout.com" |
||||||
|
}, |
||||||
|
"1135": { |
||||||
|
"apiUrl": "https://blockscout.lisk.com" |
||||||
|
}, |
||||||
|
"1291": { |
||||||
|
"apiUrl": "https://explorer-evm.testnet.swisstronik.com" |
||||||
|
}, |
||||||
|
"1432": { |
||||||
|
"apiUrl": "https://explorer-sepolia.zentachain.io" |
||||||
|
}, |
||||||
|
"1687": { |
||||||
|
"apiUrl": "https://sepolia-testnet-explorer.mintchain.io" |
||||||
|
}, |
||||||
|
"1729": { |
||||||
|
"apiUrl": "https://explorer.reya.network" |
||||||
|
}, |
||||||
|
"1750": { |
||||||
|
"apiUrl": "https://explorer.metall2.com" |
||||||
|
}, |
||||||
|
"1829": { |
||||||
|
"apiUrl": "https://explorer.playblock.io" |
||||||
|
}, |
||||||
|
"1833": { |
||||||
|
"apiUrl": "https://verify-testnet.blockscout.com" |
||||||
|
}, |
||||||
|
"1890": { |
||||||
|
"apiUrl": "https://phoenix.lightlink.io" |
||||||
|
}, |
||||||
|
"1891": { |
||||||
|
"apiUrl": "https://pegasus.lightlink.io" |
||||||
|
}, |
||||||
|
"1995": { |
||||||
|
"apiUrl": "https://explorer.testnet.edexa.com" |
||||||
|
}, |
||||||
|
"1996": { |
||||||
|
"apiUrl": "https://explorer.sanko.xyz" |
||||||
|
}, |
||||||
|
"2016": { |
||||||
|
"apiUrl": "https://netzexplorer.io" |
||||||
|
}, |
||||||
|
"2021": { |
||||||
|
"apiUrl": "https://edgscan.live" |
||||||
|
}, |
||||||
|
"2145": { |
||||||
|
"apiUrl": "https://explorer.chainers.io" |
||||||
|
}, |
||||||
|
"2410": { |
||||||
|
"apiUrl": "https://explorer.karak.network" |
||||||
|
}, |
||||||
|
"2999": { |
||||||
|
"apiUrl": "https://explorer.aevo.xyz" |
||||||
|
}, |
||||||
|
"3799": { |
||||||
|
"apiUrl": "https://testnet-explorer.tangle.tools" |
||||||
|
}, |
||||||
|
"3888": { |
||||||
|
"apiUrl": "https://kalyscan.io" |
||||||
|
}, |
||||||
|
"4058": { |
||||||
|
"apiUrl": "https://ocean.ftnscan.com" |
||||||
|
}, |
||||||
|
"4202": { |
||||||
|
"apiUrl": "https://sepolia-blockscout.lisk.com" |
||||||
|
}, |
||||||
|
"4396": { |
||||||
|
"apiUrl": "https://explorer.vedaord.com" |
||||||
|
}, |
||||||
|
"4460": { |
||||||
|
"apiUrl": "https://testnet-explorer.orderly.org" |
||||||
|
}, |
||||||
|
"4653": { |
||||||
|
"apiUrl": "https://explorer.gold.dev" |
||||||
|
}, |
||||||
|
"4999": { |
||||||
|
"apiUrl": "https://blackfort.blockscout.com" |
||||||
|
}, |
||||||
|
"5000": { |
||||||
|
"apiUrl": "https://explorer.mantle.xyz" |
||||||
|
}, |
||||||
|
"5003": { |
||||||
|
"apiUrl": "https://explorer.sepolia.mantle.xyz" |
||||||
|
}, |
||||||
|
"5112": { |
||||||
|
"apiUrl": "https://explorer.ham.fun" |
||||||
|
}, |
||||||
|
"6398": { |
||||||
|
"apiUrl": "https://connext-sepolia.blockscout.com" |
||||||
|
}, |
||||||
|
"6699": { |
||||||
|
"apiUrl": "https://oxscan.io" |
||||||
|
}, |
||||||
|
"6969": { |
||||||
|
"apiUrl": "https://tombscout.com" |
||||||
|
}, |
||||||
|
"7001": { |
||||||
|
"apiUrl": "https://zetachain-athens-3.blockscout.com" |
||||||
|
}, |
||||||
|
"7771": { |
||||||
|
"apiUrl": "https://testnetscan.bit-rock.io" |
||||||
|
}, |
||||||
|
"7887": { |
||||||
|
"apiUrl": "https://explorer.kinto.xyz" |
||||||
|
}, |
||||||
|
"7979": { |
||||||
|
"apiUrl": "https://doscan.io" |
||||||
|
}, |
||||||
|
"8131": { |
||||||
|
"apiUrl": "https://testnet-qng.qitmeer.io" |
||||||
|
}, |
||||||
|
"8337": { |
||||||
|
"apiUrl": "https://explorer.ipsprotocol.xyz" |
||||||
|
}, |
||||||
|
"8453": { |
||||||
|
"apiUrl": "https://base.blockscout.com" |
||||||
|
}, |
||||||
|
"8822": { |
||||||
|
"apiUrl": "https://explorer.evm.iota.org" |
||||||
|
}, |
||||||
|
"8853": { |
||||||
|
"apiUrl": "https://explorer.myclique.io" |
||||||
|
}, |
||||||
|
"8866": { |
||||||
|
"apiUrl": "https://explorer.lumio.io" |
||||||
|
}, |
||||||
|
"8869": { |
||||||
|
"apiUrl": "https://lif3scout.com" |
||||||
|
}, |
||||||
|
"8899": { |
||||||
|
"apiUrl": "https://exp-l1-ng.jibchain.net" |
||||||
|
}, |
||||||
|
"9996": { |
||||||
|
"apiUrl": "https://mainnet.mindscan.info" |
||||||
|
}, |
||||||
|
"12553": { |
||||||
|
"apiUrl": "https://scan.rss3.io" |
||||||
|
}, |
||||||
|
"13371": { |
||||||
|
"apiUrl": "https://explorer.immutable.com" |
||||||
|
}, |
||||||
|
"13473": { |
||||||
|
"apiUrl": "https://explorer.testnet.immutable.com" |
||||||
|
}, |
||||||
|
"17000": { |
||||||
|
"apiUrl": "https://eth-holesky.blockscout.com" |
||||||
|
}, |
||||||
|
"18233": { |
||||||
|
"apiUrl": "https://unreal.blockscout.com" |
||||||
|
}, |
||||||
|
"23452": { |
||||||
|
"apiUrl": "https://scan.dreyerx.com" |
||||||
|
}, |
||||||
|
"27563": { |
||||||
|
"apiUrl": "https://scan.onchaincoin.io" |
||||||
|
}, |
||||||
|
"34443": { |
||||||
|
"apiUrl": "https://explorer.mode.network" |
||||||
|
}, |
||||||
|
"42161": { |
||||||
|
"apiUrl": "https://arbitrum.blockscout.com" |
||||||
|
}, |
||||||
|
"42766": { |
||||||
|
"apiUrl": "https://testnet-scan.zkfair.io" |
||||||
|
}, |
||||||
|
"53302": { |
||||||
|
"apiUrl": "https://sepolia-explorer.superseed.xyz" |
||||||
|
}, |
||||||
|
"53339": { |
||||||
|
"apiUrl": "https://blk.keeex.me" |
||||||
|
}, |
||||||
|
"54211": { |
||||||
|
"apiUrl": "https://explorer.testedge2.haqq.network" |
||||||
|
}, |
||||||
|
"57000": { |
||||||
|
"apiUrl": "https://rollux.tanenbaum.io" |
||||||
|
}, |
||||||
|
"60808": { |
||||||
|
"apiUrl": "https://explorer.gobob.xyz" |
||||||
|
}, |
||||||
|
"64002": { |
||||||
|
"apiUrl": "https://xchain-testnet-explorer.idex.io" |
||||||
|
}, |
||||||
|
"70700": { |
||||||
|
"apiUrl": "https://explorer.apex.proofofplay.com" |
||||||
|
}, |
||||||
|
"78225": { |
||||||
|
"apiUrl": "https://explorer.stack.so" |
||||||
|
}, |
||||||
|
"84532": { |
||||||
|
"apiUrl": "https://base-sepolia.blockscout.com" |
||||||
|
}, |
||||||
|
"98881": { |
||||||
|
"apiUrl": "https://explorer.ebi.xyz" |
||||||
|
}, |
||||||
|
"101010": { |
||||||
|
"apiUrl": "https://stability.blockscout.com" |
||||||
|
}, |
||||||
|
"102031": { |
||||||
|
"apiUrl": "https://creditcoin-testnet.blockscout.com" |
||||||
|
}, |
||||||
|
"111111": { |
||||||
|
"apiUrl": "https://explorer.main.siberium.net" |
||||||
|
}, |
||||||
|
"111188": { |
||||||
|
"apiUrl": "https://explorer.re.al" |
||||||
|
}, |
||||||
|
"224433": { |
||||||
|
"apiUrl": "https://scan.conet.network" |
||||||
|
}, |
||||||
|
"241120": { |
||||||
|
"apiUrl": "https://andromeda.anomalyscan.io" |
||||||
|
}, |
||||||
|
"355113": { |
||||||
|
"apiUrl": "https://explorer.testnet.bitfinity.network" |
||||||
|
}, |
||||||
|
"622277": { |
||||||
|
"apiUrl": "https://explorer.hypra.network" |
||||||
|
}, |
||||||
|
"656476": { |
||||||
|
"apiUrl": "https://opencampus-codex.blockscout.com" |
||||||
|
}, |
||||||
|
"686868": { |
||||||
|
"apiUrl": "https://scan.wonnetwork.org" |
||||||
|
}, |
||||||
|
"782251": { |
||||||
|
"apiUrl": "https://testnet.explorer.stack.so" |
||||||
|
}, |
||||||
|
"984122": { |
||||||
|
"apiUrl": "https://explorer.forma.art" |
||||||
|
}, |
||||||
|
"5820948": { |
||||||
|
"apiUrl": "https://onlyscan.info" |
||||||
|
}, |
||||||
|
"7225878": { |
||||||
|
"apiUrl": "https://explorer.saakuru.network" |
||||||
|
}, |
||||||
|
"7777777": { |
||||||
|
"apiUrl": "https://explorer.zora.energy" |
||||||
|
}, |
||||||
|
"10241025": { |
||||||
|
"apiUrl": "https://hal.explorer.caldera.xyz" |
||||||
|
}, |
||||||
|
"11155111": { |
||||||
|
"apiUrl": "https://eth-sepolia.blockscout.com" |
||||||
|
}, |
||||||
|
"11155112": { |
||||||
|
"apiUrl": "https://explorer-testnet.aevo.xyz" |
||||||
|
}, |
||||||
|
"11155420": { |
||||||
|
"apiUrl": "https://optimism-sepolia.blockscout.com" |
||||||
|
}, |
||||||
|
"20180427": { |
||||||
|
"apiUrl": "https://stability-testnet.blockscout.com" |
||||||
|
}, |
||||||
|
"28122024": { |
||||||
|
"apiUrl": "https://scanv2-testnet.ancient8.gg" |
||||||
|
}, |
||||||
|
"65010002": { |
||||||
|
"apiUrl": "https://bakerloo.autonity.org" |
||||||
|
}, |
||||||
|
"65100002": { |
||||||
|
"apiUrl": "https://piccadilly.autonity.org" |
||||||
|
}, |
||||||
|
"88888888": { |
||||||
|
"apiUrl": "https://babytuna.explorer.tunachain.io" |
||||||
|
}, |
||||||
|
"89346162": { |
||||||
|
"apiUrl": "https://reya-cronos.blockscout.com" |
||||||
|
}, |
||||||
|
"245022926": { |
||||||
|
"apiUrl": "https://neon-devnet.blockscout.com" |
||||||
|
}, |
||||||
|
"245022934": { |
||||||
|
"apiUrl": "https://neon.blockscout.com" |
||||||
|
}, |
||||||
|
"666666666": { |
||||||
|
"apiUrl": "https://explorer.degen.tips" |
||||||
|
}, |
||||||
|
"888888888": { |
||||||
|
"apiUrl": "https://scan.ancient8.gg" |
||||||
|
}, |
||||||
|
"1123581321": { |
||||||
|
"apiUrl": "https://explorer.xoracle.io" |
||||||
|
}, |
||||||
|
"1313161554": { |
||||||
|
"apiUrl": "https://explorer.mainnet.aurora.dev" |
||||||
|
}, |
||||||
|
"1380012617": { |
||||||
|
"apiUrl": "https://mainnet.explorer.rarichain.org" |
||||||
|
}, |
||||||
|
"2046399126": { |
||||||
|
"apiUrl": "https://elated-tan-skat.explorer.mainnet.skalenodes.com" |
||||||
|
}, |
||||||
|
"2863311531": { |
||||||
|
"apiUrl": "https://testnet.a8scan.io" |
||||||
|
}, |
||||||
|
"81247166294": { |
||||||
|
"apiUrl": "https://testnet.otoscan.io" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import type { ChainSettings, ContractVerificationSettings, SettingsForVerifier, VerifierSettings } from '../types/SettingsTypes' |
||||||
|
import { VerifierIdentifier, VERIFIERS } from '../types/VerificationTypes' |
||||||
|
import DEFAULT_APIS from './default-apis.json' |
||||||
|
|
||||||
|
export function mergeChainSettingsWithDefaults(chainId: string, userSettings: ContractVerificationSettings): ChainSettings { |
||||||
|
const verifiers: SettingsForVerifier = {} |
||||||
|
|
||||||
|
for (const verifierId of VERIFIERS) { |
||||||
|
const userSetting: VerifierSettings = userSettings.chains[chainId]?.verifiers[verifierId] ?? {} |
||||||
|
|
||||||
|
verifiers[verifierId] = { ...userSetting } |
||||||
|
|
||||||
|
let defaultsForVerifier: VerifierSettings |
||||||
|
if (verifierId === 'Sourcify') { |
||||||
|
defaultsForVerifier = DEFAULT_APIS['Sourcify'] |
||||||
|
} else { |
||||||
|
defaultsForVerifier = DEFAULT_APIS[verifierId][chainId] ?? {} |
||||||
|
} |
||||||
|
|
||||||
|
// Prefer user settings over defaults
|
||||||
|
verifiers[verifierId] = Object.assign({}, defaultsForVerifier, userSetting) |
||||||
|
} |
||||||
|
return { verifiers } |
||||||
|
} |
||||||
|
|
||||||
|
export function validConfiguration(chainSettings: ChainSettings | undefined, verifierId: VerifierIdentifier) { |
||||||
|
return !!chainSettings && !!chainSettings.verifiers[verifierId]?.apiUrl && (verifierId !== 'Etherscan' || !!chainSettings.verifiers[verifierId]?.apiKey) |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export * from './default-settings' |
@ -0,0 +1,167 @@ |
|||||||
|
import { useContext, useEffect, useMemo, useState } from 'react' |
||||||
|
import { SearchableChainDropdown, ContractAddressInput } from '../components' |
||||||
|
import { mergeChainSettingsWithDefaults, validConfiguration } from '../utils' |
||||||
|
import type { LookupResponse, VerifierIdentifier } from '../types' |
||||||
|
import { VERIFIERS } from '../types' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
import { CustomTooltip } from '@remix-ui/helper' |
||||||
|
import { getVerifier } from '../Verifiers' |
||||||
|
import { useNavigate } from 'react-router-dom' |
||||||
|
import { VerifyFormContext } from '../VerifyFormContext' |
||||||
|
import { useSourcifySupported } from '../hooks/useSourcifySupported' |
||||||
|
|
||||||
|
export const LookupView = () => { |
||||||
|
const { settings, clientInstance } = useContext(AppContext) |
||||||
|
const { selectedChain, setSelectedChain } = useContext(VerifyFormContext) |
||||||
|
const [contractAddress, setContractAddress] = useState('') |
||||||
|
const [contractAddressError, setContractAddressError] = useState('') |
||||||
|
const [loadingVerifiers, setLoadingVerifiers] = useState<Partial<Record<VerifierIdentifier, boolean>>>({}) |
||||||
|
const [lookupResults, setLookupResult] = useState<Partial<Record<VerifierIdentifier, LookupResponse>>>({}) |
||||||
|
const navigate = useNavigate() |
||||||
|
|
||||||
|
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings]) |
||||||
|
|
||||||
|
const sourcifySupported = useSourcifySupported(selectedChain, chainSettings) |
||||||
|
|
||||||
|
const noVerifierEnabled = VERIFIERS.every((verifierId) => !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)) |
||||||
|
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain || noVerifierEnabled |
||||||
|
|
||||||
|
// Reset results when chain or contract changes
|
||||||
|
useEffect(() => { |
||||||
|
setLookupResult({}) |
||||||
|
setLoadingVerifiers({}) |
||||||
|
}, [selectedChain, contractAddress]) |
||||||
|
|
||||||
|
const handleLookup = (e) => { |
||||||
|
if (Object.values(loadingVerifiers).some((loading) => loading)) { |
||||||
|
console.error('Lookup request already running') |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
e.preventDefault() |
||||||
|
|
||||||
|
for (const verifierId of VERIFIERS) { |
||||||
|
if (!validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
setLoadingVerifiers((prev) => ({ ...prev, [verifierId]: true })) |
||||||
|
const verifier = getVerifier(verifierId, chainSettings.verifiers[verifierId]) |
||||||
|
verifier |
||||||
|
.lookup(contractAddress, selectedChain.chainId.toString()) |
||||||
|
.then((result) => setLookupResult((prev) => ({ ...prev, [verifierId]: result }))) |
||||||
|
.catch((err) => |
||||||
|
setLookupResult((prev) => { |
||||||
|
console.error(err) |
||||||
|
return { ...prev, [verifierId]: { status: 'lookup failed' } } |
||||||
|
}) |
||||||
|
) |
||||||
|
.finally(() => setLoadingVerifiers((prev) => ({ ...prev, [verifierId]: false }))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const sendToMatomo = async (eventAction: string, eventName: string) => { |
||||||
|
await clientInstance.call('matomo' as any, 'track', ['trackEvent', 'ContractVerification', eventAction, eventName]); |
||||||
|
} |
||||||
|
|
||||||
|
const handleOpenInRemix = async (lookupResponse: LookupResponse) => { |
||||||
|
for (const source of lookupResponse.sourceFiles ?? []) { |
||||||
|
try { |
||||||
|
await clientInstance.call('fileManager', 'setFile', source.path, source.content) |
||||||
|
} catch (err) { |
||||||
|
console.error(`Error while creating file ${source.path}: ${err.message}`) |
||||||
|
} |
||||||
|
} |
||||||
|
try { |
||||||
|
await clientInstance.call('fileManager', 'open', lookupResponse.targetFilePath) |
||||||
|
await sendToMatomo('lookup', "openInRemix On: " + selectedChain) |
||||||
|
} catch (err) { |
||||||
|
console.error(`Error focusing file ${lookupResponse.targetFilePath}: ${err.message}`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<form onSubmit={handleLookup}> |
||||||
|
<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} |
||||||
|
/> |
||||||
|
<button type="submit" className="btn w-100 btn-primary" disabled={submitDisabled}> |
||||||
|
Lookup |
||||||
|
</button> |
||||||
|
</form> |
||||||
|
<div className="pt-3"> |
||||||
|
{ chainSettings && |
||||||
|
VERIFIERS.map((verifierId) => { |
||||||
|
if (!validConfiguration(chainSettings, verifierId)) { |
||||||
|
return ( |
||||||
|
<div key={verifierId} className="pt-4"> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold text-secondary">{verifierId}</span>{' '} |
||||||
|
<CustomTooltip tooltipText="Configure the API in the settings"> |
||||||
|
<span className="text-secondary" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}> |
||||||
|
Enable? |
||||||
|
</span> |
||||||
|
</CustomTooltip> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if (verifierId === 'Sourcify' && !sourcifySupported) { |
||||||
|
return ( |
||||||
|
<div key={verifierId} className="pt-4"> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold text-secondary">{verifierId}</span>{' '} |
||||||
|
<CustomTooltip tooltipText={`The configured Sourcify server (${chainSettings.verifiers['Sourcify'].apiUrl}) does not support chain ${selectedChain?.chainId}`}> |
||||||
|
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}> |
||||||
|
Unsupported |
||||||
|
</span> |
||||||
|
</CustomTooltip> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div key={verifierId} className="pt-4"> |
||||||
|
<div> |
||||||
|
<span className="font-weight-bold">{verifierId}</span> <span className="text-secondary">{chainSettings.verifiers[verifierId].apiUrl}</span> |
||||||
|
</div> |
||||||
|
{!!loadingVerifiers[verifierId] && ( |
||||||
|
<div className="pt-2 d-flex justify-content-center"> |
||||||
|
<i className="fas fa-spinner fa-spin fa-2x"></i> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{!loadingVerifiers[verifierId] && !!lookupResults[verifierId] && ( |
||||||
|
<div> |
||||||
|
<div className="pt-2"> |
||||||
|
Status:{' '} |
||||||
|
<span className="font-weight-bold" style={{ textTransform: 'capitalize' }}> |
||||||
|
{lookupResults[verifierId].status} |
||||||
|
</span>{' '} |
||||||
|
{!!lookupResults[verifierId].lookupUrl && <a href={lookupResults[verifierId].lookupUrl} target="_blank" className="fa fas fa-arrow-up-right-from-square"></a>} |
||||||
|
</div> |
||||||
|
{!!lookupResults[verifierId].sourceFiles && lookupResults[verifierId].sourceFiles.length > 0 && ( |
||||||
|
<div className="pt-2 d-flex flex-row justify-content-center"> |
||||||
|
<button className="btn btn-secondary bg-transparent text-body" onClick={() => handleOpenInRemix(lookupResults[verifierId])}> |
||||||
|
<i className="fas fa-download"></i> Open in Remix |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { useContext } from 'react' |
||||||
|
import { AccordionReceipt } from '../components/AccordionReceipt' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
|
||||||
|
export const ReceiptsView = () => { |
||||||
|
const { submittedContracts } = useContext(AppContext) |
||||||
|
const contracts = Object.values(submittedContracts).reverse() |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
{contracts.length > 0 ? contracts.map((contract, index) => ( |
||||||
|
<AccordionReceipt contract={contract} index={index} /> |
||||||
|
)) : <div className="text-center mt-5">No contracts submitted for verification</div>} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
import { useContext, useMemo, useState } from 'react' |
||||||
|
import { SearchableChainDropdown, ConfigInput } from '../components' |
||||||
|
import type { VerifierIdentifier, VerifierSettings, ContractVerificationSettings } from '../types' |
||||||
|
import { mergeChainSettingsWithDefaults } from '../utils' |
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
import { VerifyFormContext } from '../VerifyFormContext' |
||||||
|
|
||||||
|
export const SettingsView = () => { |
||||||
|
const { settings, setSettings } = useContext(AppContext) |
||||||
|
const { selectedChain, setSelectedChain } = useContext(VerifyFormContext) |
||||||
|
|
||||||
|
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings]) |
||||||
|
|
||||||
|
const handleChange = (verifier: VerifierIdentifier, key: keyof VerifierSettings, value: string) => { |
||||||
|
const chainId = selectedChain.chainId.toString() |
||||||
|
const changedSettings: ContractVerificationSettings = JSON.parse(JSON.stringify(settings)) |
||||||
|
|
||||||
|
if (!changedSettings.chains[chainId]) { |
||||||
|
changedSettings.chains[chainId] = { verifiers: {} } |
||||||
|
} |
||||||
|
if (!changedSettings.chains[chainId].verifiers[verifier]) { |
||||||
|
changedSettings.chains[chainId].verifiers[verifier] = {} |
||||||
|
} |
||||||
|
|
||||||
|
changedSettings.chains[chainId].verifiers[verifier][key] = value |
||||||
|
setSettings(changedSettings) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<SearchableChainDropdown label="Chain" id="network-dropdown" setSelectedChain={setSelectedChain} selectedChain={selectedChain} /> |
||||||
|
|
||||||
|
{selectedChain && ( |
||||||
|
<div> |
||||||
|
<div className="p-2 my-2 border"> |
||||||
|
<span className="font-weight-bold">Sourcify - {selectedChain.name}</span> |
||||||
|
<ConfigInput label="API URL" id="sourcify-api-url" secret={false} initialValue={chainSettings.verifiers['Sourcify']?.apiUrl ?? ''} saveResult={(result) => handleChange('Sourcify', 'apiUrl', result)} /> |
||||||
|
<ConfigInput label="Repo URL" id="sourcify-explorer-url" secret={false} initialValue={chainSettings.verifiers['Sourcify']?.explorerUrl ?? ''} saveResult={(result) => handleChange('Sourcify', 'explorerUrl', result)} /> |
||||||
|
</div> |
||||||
|
<div className="p-2 my-2 border"> |
||||||
|
<span className="font-weight-bold">Etherscan - {selectedChain.name}</span> |
||||||
|
<ConfigInput label="API Key" id="etherscan-api-key" secret={true} initialValue={chainSettings.verifiers['Etherscan']?.apiKey ?? ''} saveResult={(result) => handleChange('Etherscan', 'apiKey', result)} /> |
||||||
|
<ConfigInput label="API URL" id="etherscan-api-url" secret={false} initialValue={chainSettings.verifiers['Etherscan']?.apiUrl ?? ''} saveResult={(result) => handleChange('Etherscan', 'apiUrl', result)} /> |
||||||
|
<ConfigInput label="Explorer URL" id="etherscan-explorer-url" secret={false} initialValue={chainSettings.verifiers['Etherscan']?.explorerUrl ?? ''} saveResult={(result) => handleChange('Etherscan', 'explorerUrl', result)} /> |
||||||
|
</div> |
||||||
|
<div className="p-2 my-2 border"> |
||||||
|
<span className="font-weight-bold">Blockscout - {selectedChain.name}</span> |
||||||
|
<ConfigInput label="Instance URL" id="blockscout-api-url" secret={false} initialValue={chainSettings.verifiers['Blockscout']?.apiUrl ?? ''} saveResult={(result) => handleChange('Blockscout', 'apiUrl', result)} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,279 @@ |
|||||||
|
import { useContext, useEffect, useMemo, useState } from 'react' |
||||||
|
|
||||||
|
import { AppContext } from '../AppContext' |
||||||
|
import { SearchableChainDropdown, ContractDropdown, ContractAddressInput } from '../components' |
||||||
|
import type { VerifierIdentifier, SubmittedContract, VerificationReceipt, VerifierInfo, VerificationResponse } from '../types' |
||||||
|
import { VERIFIERS } from '../types' |
||||||
|
import { mergeChainSettingsWithDefaults, validConfiguration } from '../utils' |
||||||
|
import { useNavigate } from 'react-router-dom' |
||||||
|
import { ConstructorArguments } from '../components/ConstructorArguments' |
||||||
|
import { CustomTooltip } from '@remix-ui/helper' |
||||||
|
import { AbstractVerifier, getVerifier } from '../Verifiers' |
||||||
|
import { VerifyFormContext } from '../VerifyFormContext' |
||||||
|
import { useSourcifySupported } from '../hooks/useSourcifySupported' |
||||||
|
|
||||||
|
export const VerifyView = () => { |
||||||
|
const { compilationOutput, setSubmittedContracts, settings, clientInstance } = useContext(AppContext) |
||||||
|
const { selectedChain, setSelectedChain, contractAddress, setContractAddress, contractAddressError, setContractAddressError, selectedContract, setSelectedContract, proxyAddress, setProxyAddress, proxyAddressError, setProxyAddressError, abiEncodedConstructorArgs, setAbiEncodedConstructorArgs, abiEncodingError, setAbiEncodingError } = useContext(VerifyFormContext) |
||||||
|
const [enabledVerifiers, setEnabledVerifiers] = useState<Partial<Record<VerifierIdentifier, boolean>>>({}) |
||||||
|
const [hasProxy, setHasProxy] = useState(!!proxyAddress) |
||||||
|
const navigate = useNavigate() |
||||||
|
|
||||||
|
const chainSettings = useMemo(() => (selectedChain ? mergeChainSettingsWithDefaults(selectedChain.chainId.toString(), settings) : undefined), [selectedChain, settings]) |
||||||
|
|
||||||
|
const sourcifySupported = useSourcifySupported(selectedChain, chainSettings) |
||||||
|
|
||||||
|
const noVerifierEnabled = VERIFIERS.every((verifierId) => !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported)) || Object.values(enabledVerifiers).every((enabled) => !enabled) |
||||||
|
const submitDisabled = !!contractAddressError || !contractAddress || !selectedChain || !selectedContract || (hasProxy && !!proxyAddressError) || (hasProxy && !proxyAddress) || noVerifierEnabled |
||||||
|
|
||||||
|
// Enable all verifiers with valid configuration
|
||||||
|
useEffect(() => { |
||||||
|
const changedEnabledVerifiers = {} |
||||||
|
for (const verifierId of VERIFIERS) { |
||||||
|
if (validConfiguration(chainSettings, verifierId) && (verifierId !== 'Sourcify' || sourcifySupported)) { |
||||||
|
changedEnabledVerifiers[verifierId] = true |
||||||
|
} |
||||||
|
} |
||||||
|
setEnabledVerifiers(changedEnabledVerifiers) |
||||||
|
}, [selectedChain, sourcifySupported]) |
||||||
|
|
||||||
|
const handleVerifierCheckboxClick = (verifierId: VerifierIdentifier, checked: boolean) => { |
||||||
|
setEnabledVerifiers({ ...enabledVerifiers, [verifierId]: checked }) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const sendToMatomo = async (eventAction: string, eventName: string) => { |
||||||
|
await clientInstance.call("matomo" as any, 'track', ['trackEvent', 'ContractVerification', eventAction, eventName]); |
||||||
|
} |
||||||
|
|
||||||
|
const handleVerify = async (e) => { |
||||||
|
e.preventDefault() |
||||||
|
|
||||||
|
const { triggerFilePath, filePath, contractName } = selectedContract |
||||||
|
const compilerAbstract = compilationOutput[triggerFilePath] |
||||||
|
if (!compilerAbstract) { |
||||||
|
throw new Error(`Error: Compilation output not found for ${triggerFilePath}`) |
||||||
|
} |
||||||
|
|
||||||
|
const date = new Date() |
||||||
|
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: chainSettings.verifiers[verifierId].apiUrl, |
||||||
|
name: verifierId as VerifierIdentifier, |
||||||
|
} |
||||||
|
receipts.push({ verifierInfo, status: 'pending', contractId, isProxyReceipt: false, failedChecks: 0 }) |
||||||
|
if (enabledVerifiers.Blockscout) await sendToMatomo('verify', "verifyWith: Blockscout On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) |
||||||
|
if (enabledVerifiers.Etherscan) await sendToMatomo('verify', "verifyWithEtherscan On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) |
||||||
|
if (enabledVerifiers.Sourcify) await sendToMatomo('verify', "verifyWithSourcify On: " + selectedChain + " IsProxy: " + (hasProxy && !proxyAddress)) |
||||||
|
} |
||||||
|
|
||||||
|
const newSubmittedContract: SubmittedContract = { |
||||||
|
id: contractId, |
||||||
|
address: contractAddress, |
||||||
|
chainId: selectedChain?.chainId.toString(), |
||||||
|
filePath, |
||||||
|
contractName, |
||||||
|
date: date.toUTCString(), |
||||||
|
receipts, |
||||||
|
} |
||||||
|
if (abiEncodedConstructorArgs) { |
||||||
|
newSubmittedContract.abiEncodedConstructorArgs = abiEncodedConstructorArgs |
||||||
|
} |
||||||
|
|
||||||
|
const proxyReceipts: VerificationReceipt[] = [] |
||||||
|
if (hasProxy) { |
||||||
|
for (const [verifierId, enabled] of Object.entries(enabledVerifiers)) { |
||||||
|
if (!enabled) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
const verifierSettings = chainSettings.verifiers[verifierId] |
||||||
|
const verifierInfo: VerifierInfo = { |
||||||
|
apiUrl: verifierSettings.apiUrl, |
||||||
|
name: verifierId as VerifierIdentifier, |
||||||
|
} |
||||||
|
|
||||||
|
let verifier: AbstractVerifier |
||||||
|
try { |
||||||
|
verifier = getVerifier(verifierId as VerifierIdentifier, verifierSettings) |
||||||
|
} catch (e) { |
||||||
|
// User settings might be invalid
|
||||||
|
proxyReceipts.push({ verifierInfo, status: 'failed', contractId, isProxyReceipt: true, message: e.message, failedChecks: 0 }) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if (!verifier.verifyProxy) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
proxyReceipts.push({ verifierInfo, status: 'awaiting implementation verification', contractId, isProxyReceipt: true, failedChecks: 0 }) |
||||||
|
} |
||||||
|
|
||||||
|
newSubmittedContract.proxyAddress = proxyAddress |
||||||
|
newSubmittedContract.proxyReceipts = proxyReceipts |
||||||
|
} |
||||||
|
|
||||||
|
setSubmittedContracts((prev) => ({ ...prev, [newSubmittedContract.id]: newSubmittedContract })) |
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setContractAddress('') |
||||||
|
setAbiEncodedConstructorArgs('') |
||||||
|
setSelectedContract(undefined) |
||||||
|
setProxyAddress('') |
||||||
|
|
||||||
|
// Take user to receipt view
|
||||||
|
navigate('/receipts') |
||||||
|
|
||||||
|
const verify = async (receipt: VerificationReceipt) => { |
||||||
|
if (receipt.status === 'failed') { |
||||||
|
return // failed already when creating
|
||||||
|
} |
||||||
|
|
||||||
|
const { verifierInfo } = receipt |
||||||
|
|
||||||
|
if (receipt.status === 'awaiting implementation verification') { |
||||||
|
const implementationReceipt = newSubmittedContract.receipts.find((r) => r.verifierInfo.name === verifierInfo.name) |
||||||
|
if (implementationReceipt.status === 'pending') { |
||||||
|
setTimeout(() => verify(receipt), 1000) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const verifierSettings = chainSettings.verifiers[verifierInfo.name] |
||||||
|
try { |
||||||
|
const verifier = getVerifier(verifierInfo.name, verifierSettings) |
||||||
|
let response: VerificationResponse |
||||||
|
if (receipt.isProxyReceipt) { |
||||||
|
response = await verifier.verifyProxy(newSubmittedContract) |
||||||
|
} else { |
||||||
|
response = await verifier.verify(newSubmittedContract, compilerAbstract) |
||||||
|
} |
||||||
|
const { status, message, receiptId, lookupUrl } = response |
||||||
|
receipt.status = status |
||||||
|
receipt.message = message |
||||||
|
if (lookupUrl) { |
||||||
|
receipt.lookupUrl = lookupUrl |
||||||
|
} |
||||||
|
if (receiptId) { |
||||||
|
receipt.receiptId = receiptId |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
const err = e as Error |
||||||
|
receipt.status = 'failed' |
||||||
|
receipt.message = err.message |
||||||
|
} |
||||||
|
|
||||||
|
// Update the UI
|
||||||
|
setSubmittedContracts((prev) => ({ ...prev, [newSubmittedContract.id]: newSubmittedContract })) |
||||||
|
} |
||||||
|
|
||||||
|
// Verify for each verifier. forEach does not wait for await and each promise will execute in parallel
|
||||||
|
receipts.forEach(verify) |
||||||
|
proxyReceipts.forEach(verify) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<form onSubmit={handleVerify}> |
||||||
|
<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} |
||||||
|
/> |
||||||
|
<CustomTooltip tooltipText="Please compile and select the solidity contract you need to verify."> |
||||||
|
<ContractDropdown label="Contract Name" id="contract-dropdown-1" selectedContract={selectedContract} setSelectedContract={setSelectedContract} /> |
||||||
|
</CustomTooltip> |
||||||
|
{selectedContract && <ConstructorArguments |
||||||
|
abiEncodedConstructorArgs={abiEncodedConstructorArgs} |
||||||
|
setAbiEncodedConstructorArgs={setAbiEncodedConstructorArgs} |
||||||
|
selectedContract={selectedContract} |
||||||
|
abiEncodingError={abiEncodingError} |
||||||
|
setAbiEncodingError={setAbiEncodingError} |
||||||
|
/>} |
||||||
|
<div className="pt-3"> |
||||||
|
<div className="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||||
|
<input id="has-proxy" className="form-check-input custom-control-input" type="checkbox" checked={!!hasProxy} onChange={(e) => setHasProxy(e.target.checked)} /> |
||||||
|
<label htmlFor="has-proxy" className="m-0 form-check-label custom-control-label" style={{ paddingTop: '2px' }}> |
||||||
|
The deployed contract is behind a proxy |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
{hasProxy && <ContractAddressInput |
||||||
|
label="Proxy Address" |
||||||
|
id="proxy-address" |
||||||
|
contractAddress={proxyAddress} |
||||||
|
setContractAddress={setProxyAddress} |
||||||
|
contractAddressError={proxyAddressError} |
||||||
|
setContractAddressError={setProxyAddressError} |
||||||
|
/>} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="pt-3"> |
||||||
|
Verify on: |
||||||
|
{VERIFIERS.map((verifierId) => { |
||||||
|
const disabledVerifier = !chainSettings || !validConfiguration(chainSettings, verifierId) || (verifierId === 'Sourcify' && !sourcifySupported) |
||||||
|
|
||||||
|
return ( |
||||||
|
<div key={verifierId} className="pt-2"> |
||||||
|
<div className="d-flex py-1 align-items-center custom-control custom-checkbox"> |
||||||
|
<input |
||||||
|
className="form-check-input custom-control-input" |
||||||
|
type="checkbox" |
||||||
|
id={`verifier-${verifierId}`} |
||||||
|
checked={!!enabledVerifiers[verifierId]} |
||||||
|
onChange={(e) => handleVerifierCheckboxClick(verifierId, e.target.checked)} |
||||||
|
disabled={disabledVerifier} |
||||||
|
/> |
||||||
|
<label |
||||||
|
htmlFor={`verifier-${verifierId}`} |
||||||
|
className={`m-0 form-check-label custom-control-label large font-weight-bold${!disabledVerifier ? '' : ' text-secondary'}`} |
||||||
|
style={{ fontSize: '1rem', lineHeight: '1.5', color: 'var(--text)' }} |
||||||
|
> |
||||||
|
{verifierId} |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div className="d-flex flex-column align-items-start pl-4"> |
||||||
|
{!chainSettings ? ( |
||||||
|
'' |
||||||
|
) : !validConfiguration(chainSettings, verifierId) ? ( |
||||||
|
<CustomTooltip tooltipText="Configure the API in the settings"> |
||||||
|
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}> |
||||||
|
Enable? |
||||||
|
</span> |
||||||
|
</CustomTooltip> |
||||||
|
) : verifierId === 'Sourcify' && !sourcifySupported ? ( |
||||||
|
<CustomTooltip tooltipText={`The configured Sourcify server (${chainSettings.verifiers['Sourcify'].apiUrl}) does not support chain ${selectedChain?.chainId}`}> |
||||||
|
<span className="text-secondary w-auto" style={{ textDecoration: 'underline dotted', cursor: 'pointer' }} onClick={() => navigate('/settings')}> |
||||||
|
Unsupported |
||||||
|
</span> |
||||||
|
</CustomTooltip> |
||||||
|
) : ( |
||||||
|
<span className="text-secondary">{chainSettings.verifiers[verifierId].apiUrl}</span> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
})} |
||||||
|
</div> |
||||||
|
<CustomTooltip tooltipText={submitDisabled ? ( |
||||||
|
(!!contractAddressError || !contractAddress) ? "Please provide a valid contract address." : |
||||||
|
!selectedChain ? "Please select the chain." : |
||||||
|
!selectedContract ? "Please select the contract (compile if needed)." : |
||||||
|
((hasProxy && !!proxyAddressError) || (hasProxy && !proxyAddress)) ? "Please provide a valid proxy contract address." : |
||||||
|
"Please provide all necessary data to verify") // Is not expected to be a case
|
||||||
|
: "Verify with selected tools"}> |
||||||
|
<button type="submit" className="w-100 btn btn-primary mt-3" disabled={submitDisabled}> |
||||||
|
Verify |
||||||
|
</button> |
||||||
|
</CustomTooltip> |
||||||
|
</form> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export { VerifyView } from './VerifyView' |
||||||
|
export { SettingsView } from './SettingsView' |
||||||
|
export { LookupView } from './LookupView' |
||||||
|
export { ReceiptsView } from './ReceiptsView' |
@ -1,3 +1,3 @@ |
|||||||
export const environment = { |
export const environment = { |
||||||
production: true |
production: true, |
||||||
}; |
} |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,10 @@ |
|||||||
|
import React from 'react' |
||||||
|
import * as ReactDOM from 'react-dom' |
||||||
|
import { createRoot } from 'react-dom/client' |
||||||
|
import App from './app/app' |
||||||
|
|
||||||
|
const container = document.getElementById('root') |
||||||
|
|
||||||
|
if (container) { |
||||||
|
createRoot(container).render(<App />) |
||||||
|
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,18 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../tsconfig.base.json", |
||||||
|
"compilerOptions": { |
||||||
|
"jsx": "react-jsx", |
||||||
|
"allowJs": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"resolveJsonModule": true, |
||||||
|
// "strict": true |
||||||
|
}, |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.app.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -1,69 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "etherscan", |
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json", |
|
||||||
"sourceRoot": "apps/etherscan/src", |
|
||||||
"projectType": "application", |
|
||||||
"targets": { |
|
||||||
"build": { |
|
||||||
"executor": "@nrwl/webpack:webpack", |
|
||||||
"outputs": ["{options.outputPath}"], |
|
||||||
"defaultConfiguration": "development", |
|
||||||
"options": { |
|
||||||
"compiler": "babel", |
|
||||||
"outputPath": "dist/apps/etherscan", |
|
||||||
"index": "apps/etherscan/src/index.html", |
|
||||||
"baseHref": "./", |
|
||||||
"main": "apps/etherscan/src/main.tsx", |
|
||||||
"polyfills": "apps/etherscan/src/polyfills.ts", |
|
||||||
"tsConfig": "apps/etherscan/tsconfig.app.json", |
|
||||||
"assets": [ |
|
||||||
"apps/etherscan/src/favicon.ico", |
|
||||||
"apps/etherscan/src/assets", |
|
||||||
"apps/etherscan/src/profile.json" |
|
||||||
], |
|
||||||
"styles": ["apps/etherscan/src/styles.css"], |
|
||||||
"scripts": [], |
|
||||||
"webpackConfig": "apps/etherscan/webpack.config.js" |
|
||||||
}, |
|
||||||
"configurations": { |
|
||||||
"development": { |
|
||||||
}, |
|
||||||
"production": { |
|
||||||
"fileReplacements": [ |
|
||||||
{ |
|
||||||
"replace": "apps/etherscan/src/environments/environment.ts", |
|
||||||
"with": "apps/etherscan/src/environments/environment.prod.ts" |
|
||||||
} |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"lint": { |
|
||||||
"executor": "@nrwl/linter:eslint", |
|
||||||
"outputs": ["{options.outputFile}"], |
|
||||||
"options": { |
|
||||||
"lintFilePatterns": ["apps/etherscan/**/*.ts"], |
|
||||||
"eslintConfig": "apps/etherscan/.eslintrc" |
|
||||||
} |
|
||||||
}, |
|
||||||
"serve": { |
|
||||||
"executor": "@nrwl/webpack:dev-server", |
|
||||||
"defaultConfiguration": "development", |
|
||||||
"options": { |
|
||||||
"buildTarget": "etherscan:build", |
|
||||||
"hmr": true, |
|
||||||
"baseHref": "/" |
|
||||||
}, |
|
||||||
"configurations": { |
|
||||||
"development": { |
|
||||||
"buildTarget": "etherscan:build:development", |
|
||||||
"port": 5003 |
|
||||||
}, |
|
||||||
"production": { |
|
||||||
"buildTarget": "etherscan:build:production" |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
"tags": [] |
|
||||||
} |
|
@ -1,7 +0,0 @@ |
|||||||
body { |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
|
|
||||||
#root { |
|
||||||
padding: 8px 14px; |
|
||||||
} |
|
@ -1,25 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import {PluginClient} from '@remixproject/plugin' |
|
||||||
|
|
||||||
import {Receipt, ThemeType} from './types' |
|
||||||
|
|
||||||
export const AppContext = React.createContext({ |
|
||||||
apiKey: '', |
|
||||||
setAPIKey: (value: string) => { |
|
||||||
console.log('Set API Key from Context') |
|
||||||
}, |
|
||||||
clientInstance: {} as PluginClient, |
|
||||||
receipts: [] as Receipt[], |
|
||||||
setReceipts: (receipts: Receipt[]) => { |
|
||||||
console.log('Calling Set Receipts') |
|
||||||
}, |
|
||||||
contracts: [] as string[], |
|
||||||
setContracts: (contracts: string[]) => { |
|
||||||
console.log('Calling Set Contract Names') |
|
||||||
}, |
|
||||||
themeType: 'dark' as ThemeType, |
|
||||||
setThemeType: (themeType: ThemeType) => { |
|
||||||
console.log('Calling Set Theme Type') |
|
||||||
}, |
|
||||||
networkName: '' |
|
||||||
}) |
|
@ -1,70 +0,0 @@ |
|||||||
import { PluginClient } from '@remixproject/plugin' |
|
||||||
import { createClient } from '@remixproject/plugin-webview' |
|
||||||
import { verify, EtherScanReturn } from './utils/verify' |
|
||||||
import { getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus } from './utils' |
|
||||||
import EventManager from 'events' |
|
||||||
|
|
||||||
export class EtherscanPluginClient extends PluginClient { |
|
||||||
public internalEvents: EventManager |
|
||||||
|
|
||||||
constructor() { |
|
||||||
super() |
|
||||||
this.internalEvents = new EventManager() |
|
||||||
createClient(this) |
|
||||||
this.onload() |
|
||||||
} |
|
||||||
|
|
||||||
onActivation(): void { |
|
||||||
this.internalEvents.emit('etherscan_activated') |
|
||||||
} |
|
||||||
|
|
||||||
async verify( |
|
||||||
apiKey: string, |
|
||||||
contractAddress: string, |
|
||||||
contractArguments: string, |
|
||||||
contractName: string, |
|
||||||
compilationResultParam: any, |
|
||||||
chainRef?: number | string, |
|
||||||
isProxyContract?: boolean, |
|
||||||
expectedImplAddress?: string |
|
||||||
) { |
|
||||||
const result = await verify( |
|
||||||
apiKey, |
|
||||||
contractAddress, |
|
||||||
contractArguments, |
|
||||||
contractName, |
|
||||||
compilationResultParam, |
|
||||||
chainRef, |
|
||||||
isProxyContract, |
|
||||||
expectedImplAddress, |
|
||||||
this, |
|
||||||
(value: EtherScanReturn) => {}, |
|
||||||
(value: string) => {} |
|
||||||
) |
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
async receiptStatus(receiptGuid: string, apiKey: string, isProxyContract: boolean) { |
|
||||||
try { |
|
||||||
const { network, networkId } = await getNetworkName(this) |
|
||||||
if (network === 'vm') { |
|
||||||
throw new Error('Cannot check the receipt status in the selected network') |
|
||||||
} |
|
||||||
const etherscanApi = getEtherScanApi(networkId) |
|
||||||
let receiptStatus |
|
||||||
|
|
||||||
if (isProxyContract) receiptStatus = await getProxyContractReceiptStatus(receiptGuid, apiKey, etherscanApi) |
|
||||||
else receiptStatus = await getReceiptStatus(receiptGuid, apiKey, etherscanApi) |
|
||||||
return { |
|
||||||
message: receiptStatus.result, |
|
||||||
succeed: receiptStatus.status === '0' ? false : true |
|
||||||
} |
|
||||||
} catch (e: any) { |
|
||||||
return { |
|
||||||
status: 'error', |
|
||||||
message: e.message, |
|
||||||
succeed: false |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,136 +0,0 @@ |
|||||||
import React, {useState, useEffect, useRef} from 'react' |
|
||||||
|
|
||||||
import {CompilationFileSources, CompilationResult} from '@remixproject/plugin-api' |
|
||||||
|
|
||||||
import { EtherscanPluginClient } from './EtherscanPluginClient' |
|
||||||
|
|
||||||
import {AppContext} from './AppContext' |
|
||||||
import {DisplayRoutes} from './routes' |
|
||||||
|
|
||||||
import {useLocalStorage} from './hooks/useLocalStorage' |
|
||||||
|
|
||||||
import {getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus} from './utils' |
|
||||||
import {Receipt, ThemeType} from './types' |
|
||||||
|
|
||||||
import './App.css' |
|
||||||
|
|
||||||
export const getNewContractNames = (compilationResult: CompilationResult) => { |
|
||||||
const compiledContracts = compilationResult.contracts |
|
||||||
let result: string[] = [] |
|
||||||
|
|
||||||
for (const file of Object.keys(compiledContracts)) { |
|
||||||
const newContractNames = Object.keys(compiledContracts[file]) |
|
||||||
|
|
||||||
result = [...result, ...newContractNames] |
|
||||||
} |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
const plugin = new EtherscanPluginClient() |
|
||||||
|
|
||||||
const App = () => { |
|
||||||
const [apiKey, setAPIKey] = useLocalStorage('apiKey', '') |
|
||||||
const [receipts, setReceipts] = useLocalStorage('receipts', [])
|
|
||||||
const [contracts, setContracts] = useState<string[]>([]) |
|
||||||
const [themeType, setThemeType] = useState<ThemeType>('dark') |
|
||||||
const [networkName, setNetworkName] = useState('Loading...') |
|
||||||
const timer = useRef(null) |
|
||||||
const contractsRef = useRef(contracts) |
|
||||||
|
|
||||||
contractsRef.current = contracts |
|
||||||
|
|
||||||
const setListeners = () => { |
|
||||||
plugin.on('solidity', 'compilationFinished', (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => { |
|
||||||
const newContractsNames = getNewContractNames(data) |
|
||||||
|
|
||||||
const newContractsToSave: string[] = [...contractsRef.current, ...newContractsNames] |
|
||||||
|
|
||||||
const uniqueContracts: string[] = [...new Set(newContractsToSave)] |
|
||||||
|
|
||||||
setContracts(uniqueContracts) |
|
||||||
}) |
|
||||||
plugin.on('blockchain' as any, 'networkStatus', (result) => { |
|
||||||
setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`) |
|
||||||
}) |
|
||||||
// @ts-ignore
|
|
||||||
plugin.call('blockchain', 'getCurrentNetworkStatus').then((result: any) => setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`)) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
plugin.onload(() => { |
|
||||||
setListeners() |
|
||||||
}) |
|
||||||
}, []) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
let receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached') |
|
||||||
|
|
||||||
if (receiptsNotVerified.length > 0) { |
|
||||||
if (timer.current) { |
|
||||||
clearInterval(timer.current) |
|
||||||
timer.current = null |
|
||||||
} |
|
||||||
timer.current = setInterval(async () => { |
|
||||||
const {network, networkId} = await getNetworkName(plugin) |
|
||||||
|
|
||||||
if (!plugin) return |
|
||||||
if (network === 'vm') return |
|
||||||
let newReceipts = receipts |
|
||||||
|
|
||||||
for (const item of receiptsNotVerified) { |
|
||||||
await new Promise((r) => setTimeout(r, 500)) // avoid api rate limit exceed.
|
|
||||||
let status |
|
||||||
if (item.isProxyContract) { |
|
||||||
status = await getProxyContractReceiptStatus(item.guid, apiKey, getEtherScanApi(networkId)) |
|
||||||
if (status.status === '1') { |
|
||||||
status.message = status.result |
|
||||||
status.result = 'Successfully Updated' |
|
||||||
} |
|
||||||
} else status = await getReceiptStatus(item.guid, apiKey, getEtherScanApi(networkId)) |
|
||||||
if (status.result === 'Pass - Verified' || status.result === 'Already Verified' || status.result === 'Successfully Updated') { |
|
||||||
newReceipts = newReceipts.map((currentReceipt: Receipt) => { |
|
||||||
if (currentReceipt.guid === item.guid) { |
|
||||||
const res = { |
|
||||||
...currentReceipt, |
|
||||||
status: status.result |
|
||||||
} |
|
||||||
if (currentReceipt.isProxyContract) res.message = status.message |
|
||||||
return res |
|
||||||
} |
|
||||||
return currentReceipt |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
receiptsNotVerified = newReceipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached') |
|
||||||
if (timer.current && receiptsNotVerified.length === 0) { |
|
||||||
clearInterval(timer.current) |
|
||||||
timer.current = null |
|
||||||
} |
|
||||||
setReceipts(newReceipts) |
|
||||||
}, 10000) |
|
||||||
} |
|
||||||
}, [receipts]) |
|
||||||
|
|
||||||
return ( |
|
||||||
<AppContext.Provider |
|
||||||
value={{ |
|
||||||
apiKey, |
|
||||||
setAPIKey, |
|
||||||
clientInstance: plugin, |
|
||||||
receipts, |
|
||||||
setReceipts, |
|
||||||
contracts, |
|
||||||
setContracts, |
|
||||||
themeType, |
|
||||||
setThemeType, |
|
||||||
networkName |
|
||||||
}} |
|
||||||
> |
|
||||||
{ plugin && <DisplayRoutes /> } |
|
||||||
</AppContext.Provider> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
export default App |
|
@ -1,81 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
|
|
||||||
import {NavLink} from 'react-router-dom' |
|
||||||
import {CustomTooltip} from '@remix-ui/helper' |
|
||||||
import {AppContext} from '../AppContext' |
|
||||||
|
|
||||||
interface Props { |
|
||||||
title?: string |
|
||||||
from: string |
|
||||||
} |
|
||||||
|
|
||||||
interface IconProps { |
|
||||||
from: string |
|
||||||
} |
|
||||||
|
|
||||||
const HomeIcon = ({from}: IconProps) => { |
|
||||||
return ( |
|
||||||
<NavLink |
|
||||||
data-id="home" |
|
||||||
to={{ |
|
||||||
pathname: '/' |
|
||||||
}} |
|
||||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
|
||||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
|
||||||
state={from} |
|
||||||
> |
|
||||||
<CustomTooltip tooltipText="Home" tooltipId="etherscan-nav-home" placement="bottom"> |
|
||||||
<i className="fas fa-home"></i> |
|
||||||
</CustomTooltip> |
|
||||||
</NavLink> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const ReceiptsIcon = ({from}: IconProps) => { |
|
||||||
return ( |
|
||||||
<NavLink |
|
||||||
data-id="receipts" |
|
||||||
to={{ |
|
||||||
pathname: '/receipts' |
|
||||||
}} |
|
||||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
|
||||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
|
||||||
state={from} |
|
||||||
> |
|
||||||
<CustomTooltip tooltipText="Receipts" tooltipId="etherscan-nav-receipts" placement="bottom"> |
|
||||||
<i className="fas fa-receipt"></i> |
|
||||||
</CustomTooltip> |
|
||||||
</NavLink> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const SettingsIcon = ({from}: IconProps) => { |
|
||||||
return ( |
|
||||||
<NavLink |
|
||||||
data-id="settings" |
|
||||||
to={{ |
|
||||||
pathname: '/settings' |
|
||||||
}} |
|
||||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
|
||||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
|
||||||
state={from} |
|
||||||
> |
|
||||||
<CustomTooltip tooltipText="Settings" tooltipId="etherscan-nav-settings" placement="bottom"> |
|
||||||
<i className="fas fa-cog"></i> |
|
||||||
</CustomTooltip> |
|
||||||
</NavLink> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
export const HeaderWithSettings = ({title = '', from}) => { |
|
||||||
return ( |
|
||||||
<div className="d-flex justify-content-between"> |
|
||||||
<h6 className="d-inline">{title}</h6> |
|
||||||
<div className="nav"> |
|
||||||
<HomeIcon from={from} /> |
|
||||||
<ReceiptsIcon from={from} /> |
|
||||||
<SettingsIcon from={from} /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,34 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import {CustomTooltip} from '@remix-ui/helper' |
|
||||||
|
|
||||||
interface Props { |
|
||||||
text: string |
|
||||||
isSubmitting?: boolean |
|
||||||
dataId?: string |
|
||||||
disable?: boolean |
|
||||||
} |
|
||||||
|
|
||||||
export const SubmitButton = ({text, dataId, isSubmitting = false, disable = true}) => { |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<button data-id={dataId} type="submit" className="btn btn-primary btn-block p-1 text-decoration-none" disabled={disable}> |
|
||||||
<CustomTooltip |
|
||||||
tooltipText={disable ? 'Fill in the valid value(s) and select a supported network' : 'Click to proceed'} |
|
||||||
tooltipId={'etherscan-submit-button-' + dataId} |
|
||||||
tooltipTextClasses="border bg-light text-dark p-1 pr-3" |
|
||||||
placement="bottom" |
|
||||||
> |
|
||||||
<div> |
|
||||||
{!isSubmitting && text} |
|
||||||
{isSubmitting && ( |
|
||||||
<div> |
|
||||||
<span className="spinner-border spinner-border-sm mr-1" role="status" aria-hidden="true" /> |
|
||||||
Verifying... Please wait |
|
||||||
</div> |
|
||||||
)} |
|
||||||
</div> |
|
||||||
</CustomTooltip> |
|
||||||
</button> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,2 +0,0 @@ |
|||||||
export { HeaderWithSettings } from "./HeaderWithSettings" |
|
||||||
export { SubmitButton } from "./SubmitButton" |
|
@ -1,17 +0,0 @@ |
|||||||
import React, {PropsWithChildren} from 'react' |
|
||||||
|
|
||||||
import {HeaderWithSettings} from '../components' |
|
||||||
|
|
||||||
interface Props { |
|
||||||
from: string |
|
||||||
title?: string |
|
||||||
} |
|
||||||
|
|
||||||
export const DefaultLayout = ({children, from, title}) => { |
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<HeaderWithSettings from={from} title={title} /> |
|
||||||
{children} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
export { DefaultLayout } from "./Default" |
|
@ -1,37 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import {HashRouter as Router, Route, Routes, RouteProps} from 'react-router-dom' |
|
||||||
|
|
||||||
import {ErrorView, HomeView, ReceiptsView, CaptureKeyView} from './views' |
|
||||||
import {DefaultLayout} from './layouts' |
|
||||||
|
|
||||||
export const DisplayRoutes = () => ( |
|
||||||
<Router> |
|
||||||
<Routes> |
|
||||||
<Route |
|
||||||
path="/" |
|
||||||
element={ |
|
||||||
<DefaultLayout from="/" title="Verify Smart Contracts"> |
|
||||||
<HomeView /> |
|
||||||
</DefaultLayout> |
|
||||||
} |
|
||||||
/> |
|
||||||
<Route path="/error" element={<ErrorView />} /> |
|
||||||
<Route |
|
||||||
path="/receipts" |
|
||||||
element={ |
|
||||||
<DefaultLayout from="/receipts" title="Check Receipt GUID Status"> |
|
||||||
<ReceiptsView /> |
|
||||||
</DefaultLayout> |
|
||||||
} |
|
||||||
/> |
|
||||||
<Route |
|
||||||
path="/settings" |
|
||||||
element={ |
|
||||||
<DefaultLayout from="/settings" title="Set Explorer API Key"> |
|
||||||
<CaptureKeyView /> |
|
||||||
</DefaultLayout> |
|
||||||
} |
|
||||||
/> |
|
||||||
</Routes> |
|
||||||
</Router> |
|
||||||
) |
|
@ -1,9 +0,0 @@ |
|||||||
export type ReceiptStatus = "Pending in queue" | "Pass - Verified" | "Already Verified" | "Max rate limit reached" | "Successfully Updated" |
|
||||||
|
|
||||||
export interface Receipt { |
|
||||||
guid: string |
|
||||||
status: ReceiptStatus |
|
||||||
isProxyContract: boolean |
|
||||||
message?: string |
|
||||||
succeed?: boolean |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
export type ThemeType = "dark" | "light" |
|
@ -1,2 +0,0 @@ |
|||||||
export * from "./Receipt" |
|
||||||
export * from "./ThemeType" |
|
@ -1 +0,0 @@ |
|||||||
export * from "./utilities" |
|
@ -1,46 +0,0 @@ |
|||||||
export const scanAPIurls = { |
|
||||||
// all mainnet
|
|
||||||
1: 'https://api.etherscan.io/api', |
|
||||||
56: 'https://api.bscscan.com/api', |
|
||||||
137: 'https://api.polygonscan.com/api', |
|
||||||
250: 'https://api.ftmscan.com/api', |
|
||||||
42161: 'https://api.arbiscan.io/api', |
|
||||||
43114: 'https://api.snowtrace.io/api', |
|
||||||
1285: 'https://api-moonriver.moonscan.io/api', |
|
||||||
1284: 'https://api-moonbeam.moonscan.io/api', |
|
||||||
25: 'https://api.cronoscan.com/api', |
|
||||||
199: 'https://api.bttcscan.com/api', |
|
||||||
10: 'https://api-optimistic.etherscan.io/api', |
|
||||||
42220: 'https://api.celoscan.io/api', |
|
||||||
288: 'https://api.bobascan.com/api', |
|
||||||
100: 'https://api.gnosisscan.io/api', |
|
||||||
1101: 'https://api-zkevm.polygonscan.com/api', |
|
||||||
59144: 'https://api.lineascan.build/api', |
|
||||||
8453: 'https://api.basescan.org/api', |
|
||||||
534352: 'https://api.scrollscan.com/api', |
|
||||||
1116: 'https://openapi.coredao.org/api', |
|
||||||
|
|
||||||
// all testnet
|
|
||||||
17000: 'https://api-holesky.etherscan.io/api', |
|
||||||
11155111: 'https://api-sepolia.etherscan.io/api', |
|
||||||
97: 'https://api-testnet.bscscan.com/api', |
|
||||||
80001: 'https://api-testnet.polygonscan.com/api', |
|
||||||
80002: 'https://api-amoy.polygonscan.com/api', |
|
||||||
4002: 'https://api-testnet.ftmscan.com/api', |
|
||||||
421611: 'https://api-testnet.arbiscan.io/api', |
|
||||||
42170: 'https://api-nova.arbiscan.io/api', |
|
||||||
43113: 'https://api-testnet.snowtrace.io/api', |
|
||||||
1287: 'https://api-moonbase.moonscan.io/api', |
|
||||||
338: 'https://api-testnet.cronoscan.com/api', |
|
||||||
1028: 'https://api-testnet.bttcscan.com/api', |
|
||||||
420: 'https://api-goerli-optimistic.etherscan.io/api', |
|
||||||
44787: 'https://api-alfajores.celoscan.io/api', |
|
||||||
2888: 'https://api-testnet.bobascan.com/api', |
|
||||||
84531: 'https://api-goerli.basescan.org/api', |
|
||||||
84532: "https://api-sepolia.basescan.org/api", |
|
||||||
1442: 'https://api-testnet-zkevm.polygonscan.com/api', |
|
||||||
2442: 'https://api-cardona-zkevm.polygonscan.com/api', |
|
||||||
59140: 'https://api-testnet.lineascan.build/api', |
|
||||||
534351: 'https://api-sepolia.scrollscan.com/api', |
|
||||||
1115: 'https://api.test.btcs.network/api', |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
export const verifyScript = ` |
|
||||||
/** |
|
||||||
* @param {string} apikey - etherscan api key |
|
||||||
* @param {string} contractAddress - Address of the contract to verify |
|
||||||
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value |
|
||||||
* @param {string} contractName - Name of the contract |
|
||||||
* @param {string} contractFile - File where the contract is located |
|
||||||
* @param {number | string} chainRef - Network chain id or API URL (optional) |
|
||||||
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
|
||||||
* @param {string} expectedImplAddress - Implementation contract address, in case of proxy contract verification (optional) |
|
||||||
* @returns {{ guid, status, message, succeed }} verification result |
|
||||||
*/ |
|
||||||
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) => { |
|
||||||
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile) |
|
||||||
console.log('verifying.. ' + contractName) |
|
||||||
// update apiKey and chainRef to verify contract on multiple networks
|
|
||||||
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam, chainRef, isProxyContract, expectedImplAddress) |
|
||||||
}` |
|
||||||
|
|
||||||
export const receiptGuidScript = ` |
|
||||||
/** |
|
||||||
* @param {string} apikey - etherscan api key |
|
||||||
* @param {string} guid - receipt id |
|
||||||
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
|
||||||
* @returns {{ status, message, succeed }} receiptStatus |
|
||||||
*/ |
|
||||||
export const receiptStatus = async (apikey: string, guid: string, isProxyContract?: boolean) => { |
|
||||||
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey, isProxyContract) |
|
||||||
} |
|
||||||
` |
|
@ -1,69 +0,0 @@ |
|||||||
import { PluginClient } from "@remixproject/plugin" |
|
||||||
import axios from 'axios' |
|
||||||
import { scanAPIurls } from "./networks" |
|
||||||
type RemixClient = PluginClient |
|
||||||
|
|
||||||
/* |
|
||||||
status: 0=Error, 1=Pass |
|
||||||
message: OK, NOTOK |
|
||||||
result: explanation |
|
||||||
*/ |
|
||||||
export type receiptStatus = { |
|
||||||
result: string |
|
||||||
message: string |
|
||||||
status: string |
|
||||||
} |
|
||||||
|
|
||||||
export const getEtherScanApi = (networkId: any) => { |
|
||||||
if (!(networkId in scanAPIurls)) { |
|
||||||
throw new Error("no known network to verify against") |
|
||||||
} |
|
||||||
const apiUrl = (scanAPIurls as any)[networkId] |
|
||||||
return apiUrl |
|
||||||
} |
|
||||||
|
|
||||||
export const getNetworkName = async (client: RemixClient) => { |
|
||||||
const network = await client.call("network", "detectNetwork") |
|
||||||
if (!network) { |
|
||||||
throw new Error("no known network to verify against") |
|
||||||
} |
|
||||||
return { network: network.name!.toLowerCase(), networkId: network.id } |
|
||||||
} |
|
||||||
|
|
||||||
export const getReceiptStatus = async ( |
|
||||||
receiptGuid: string, |
|
||||||
apiKey: string, |
|
||||||
etherscanApi: string |
|
||||||
): Promise<receiptStatus> => { |
|
||||||
const params = `guid=${receiptGuid}&module=contract&action=checkverifystatus&apiKey=${apiKey}` |
|
||||||
try { |
|
||||||
const response = await axios.get(`${etherscanApi}?${params}`) |
|
||||||
const { result, message, status } = response.data |
|
||||||
return { |
|
||||||
result, |
|
||||||
message, |
|
||||||
status, |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error(error) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const getProxyContractReceiptStatus = async ( |
|
||||||
receiptGuid: string, |
|
||||||
apiKey: string, |
|
||||||
etherscanApi: string |
|
||||||
): Promise<receiptStatus> => { |
|
||||||
const params = `guid=${receiptGuid}&module=contract&action=checkproxyverification&apiKey=${apiKey}` |
|
||||||
try { |
|
||||||
const response = await axios.get(`${etherscanApi}?${params}`) |
|
||||||
const { result, message, status } = response.data |
|
||||||
return { |
|
||||||
result, |
|
||||||
message, |
|
||||||
status, |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error(error) |
|
||||||
} |
|
||||||
} |
|
@ -1,206 +0,0 @@ |
|||||||
import { getNetworkName, getEtherScanApi, getReceiptStatus, getProxyContractReceiptStatus } from "../utils" |
|
||||||
import { CompilationResult } from "@remixproject/plugin-api" |
|
||||||
import { CompilerAbstract } from '@remix-project/remix-solidity' |
|
||||||
import axios from 'axios' |
|
||||||
import { PluginClient } from "@remixproject/plugin" |
|
||||||
|
|
||||||
const resetAfter10Seconds = (client: PluginClient, setResults: (value: string) => void) => { |
|
||||||
setTimeout(() => { |
|
||||||
client.emit("statusChanged", { key: "none" }) |
|
||||||
setResults("") |
|
||||||
}, 10000) |
|
||||||
} |
|
||||||
|
|
||||||
export type EtherScanReturn = { |
|
||||||
guid: any, |
|
||||||
status: any, |
|
||||||
} |
|
||||||
export const verify = async ( |
|
||||||
apiKeyParam: string, |
|
||||||
contractAddress: string, |
|
||||||
contractArgumentsParam: string, |
|
||||||
contractName: string, |
|
||||||
compilationResultParam: CompilerAbstract, |
|
||||||
chainRef: number | string, |
|
||||||
isProxyContract: boolean, |
|
||||||
expectedImplAddress: string, |
|
||||||
client: PluginClient, |
|
||||||
onVerifiedContract: (value: EtherScanReturn) => void, |
|
||||||
setResults: (value: string) => void |
|
||||||
) => { |
|
||||||
let networkChainId |
|
||||||
let etherscanApi |
|
||||||
if (chainRef) { |
|
||||||
if (typeof chainRef === 'number') { |
|
||||||
networkChainId = chainRef |
|
||||||
etherscanApi = getEtherScanApi(networkChainId) |
|
||||||
} else if (typeof chainRef === 'string') etherscanApi = chainRef |
|
||||||
} else { |
|
||||||
const { network, networkId } = await getNetworkName(client) |
|
||||||
if (network === "vm") { |
|
||||||
return { |
|
||||||
succeed: false, |
|
||||||
message: "Cannot verify in the selected network" |
|
||||||
} |
|
||||||
} else { |
|
||||||
networkChainId = networkId |
|
||||||
etherscanApi = getEtherScanApi(networkChainId) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
try { |
|
||||||
const contractMetadata = getContractMetadata( |
|
||||||
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
|
|
||||||
compilationResultParam.data as unknown as CompilationResult, |
|
||||||
contractName |
|
||||||
) |
|
||||||
|
|
||||||
if (!contractMetadata) { |
|
||||||
return { |
|
||||||
succeed: false, |
|
||||||
message: "Please recompile contract" |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const contractMetadataParsed = JSON.parse(contractMetadata) |
|
||||||
|
|
||||||
const fileName = getContractFileName( |
|
||||||
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
|
|
||||||
compilationResultParam.data as unknown as CompilationResult, |
|
||||||
contractName |
|
||||||
) |
|
||||||
|
|
||||||
const jsonInput = { |
|
||||||
language: 'Solidity', |
|
||||||
sources: compilationResultParam.source.sources, |
|
||||||
settings: { |
|
||||||
optimizer: { |
|
||||||
enabled: contractMetadataParsed.settings.optimizer.enabled, |
|
||||||
runs: contractMetadataParsed.settings.optimizer.runs |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const data: { [key: string]: string | any } = { |
|
||||||
apikey: apiKeyParam, // A valid API-Key is required
|
|
||||||
module: "contract", // Do not change
|
|
||||||
action: "verifysourcecode", // Do not change
|
|
||||||
codeformat: "solidity-standard-json-input", |
|
||||||
sourceCode: JSON.stringify(jsonInput), |
|
||||||
contractname: fileName + ':' + contractName, |
|
||||||
compilerversion: `v${contractMetadataParsed.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions
|
|
||||||
constructorArguements: contractArgumentsParam ? contractArgumentsParam.replace('0x', '') : '', // if applicable
|
|
||||||
} |
|
||||||
|
|
||||||
if (isProxyContract) { |
|
||||||
data.action = "verifyproxycontract" |
|
||||||
data.expectedimplementation = expectedImplAddress |
|
||||||
data.address = contractAddress |
|
||||||
} else { |
|
||||||
data.contractaddress = contractAddress |
|
||||||
} |
|
||||||
|
|
||||||
const body = new FormData() |
|
||||||
Object.keys(data).forEach((key) => body.append(key, data[key])) |
|
||||||
|
|
||||||
client.emit("statusChanged", { |
|
||||||
key: "loading", |
|
||||||
type: "info", |
|
||||||
title: "Verifying ...", |
|
||||||
}) |
|
||||||
const response = await axios.post(etherscanApi, body) |
|
||||||
const { message, result, status } = await response.data |
|
||||||
|
|
||||||
if (message === "OK" && status === "1") { |
|
||||||
resetAfter10Seconds(client, setResults) |
|
||||||
let receiptStatus |
|
||||||
if (isProxyContract) { |
|
||||||
receiptStatus = await getProxyContractReceiptStatus( |
|
||||||
result, |
|
||||||
apiKeyParam, |
|
||||||
etherscanApi |
|
||||||
) |
|
||||||
if (receiptStatus.status === '1') { |
|
||||||
receiptStatus.message = receiptStatus.result |
|
||||||
receiptStatus.result = 'Successfully Updated' |
|
||||||
} |
|
||||||
} else receiptStatus = await getReceiptStatus( |
|
||||||
result, |
|
||||||
apiKeyParam, |
|
||||||
etherscanApi |
|
||||||
) |
|
||||||
|
|
||||||
const returnValue = { |
|
||||||
guid: result, |
|
||||||
status: receiptStatus.result, |
|
||||||
message: `Verification request submitted successfully. Use this receipt GUID ${result} to track the status of your submission`, |
|
||||||
succeed: true, |
|
||||||
isProxyContract |
|
||||||
} |
|
||||||
onVerifiedContract(returnValue) |
|
||||||
return returnValue |
|
||||||
} else if (message === "NOTOK") { |
|
||||||
client.emit("statusChanged", { |
|
||||||
key: "failed", |
|
||||||
type: "error", |
|
||||||
title: result, |
|
||||||
}) |
|
||||||
const returnValue = { |
|
||||||
message: result, |
|
||||||
succeed: false, |
|
||||||
isProxyContract |
|
||||||
} |
|
||||||
resetAfter10Seconds(client, setResults) |
|
||||||
return returnValue |
|
||||||
} |
|
||||||
return { |
|
||||||
message: 'unknown reason ' + result, |
|
||||||
succeed: false |
|
||||||
} |
|
||||||
} catch (error: any) { |
|
||||||
console.error(error) |
|
||||||
setResults("Something wrong happened, try again") |
|
||||||
return { |
|
||||||
message: error.message, |
|
||||||
succeed: false |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const getContractFileName = ( |
|
||||||
compilationResult: CompilationResult, |
|
||||||
contractName: string |
|
||||||
) => { |
|
||||||
const compiledContracts = compilationResult.contracts |
|
||||||
let fileName = "" |
|
||||||
|
|
||||||
for (const file of Object.keys(compiledContracts)) { |
|
||||||
for (const contract of Object.keys(compiledContracts[file])) { |
|
||||||
if (contract === contractName) { |
|
||||||
fileName = file |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return fileName |
|
||||||
} |
|
||||||
|
|
||||||
export const getContractMetadata = ( |
|
||||||
compilationResult: CompilationResult, |
|
||||||
contractName: string |
|
||||||
) => { |
|
||||||
const compiledContracts = compilationResult.contracts |
|
||||||
let contractMetadata = "" |
|
||||||
|
|
||||||
for (const file of Object.keys(compiledContracts)) { |
|
||||||
for (const contract of Object.keys(compiledContracts[file])) { |
|
||||||
if (contract === contractName) { |
|
||||||
contractMetadata = compiledContracts[file][contract].metadata |
|
||||||
if (contractMetadata) { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return contractMetadata |
|
||||||
} |
|
@ -1,63 +0,0 @@ |
|||||||
import React, {useState, useEffect} from 'react' |
|
||||||
|
|
||||||
import {Formik, ErrorMessage, Field} from 'formik' |
|
||||||
import {useNavigate, useLocation} from 'react-router-dom' |
|
||||||
|
|
||||||
import {AppContext} from '../AppContext' |
|
||||||
import {SubmitButton} from '../components' |
|
||||||
|
|
||||||
export const CaptureKeyView = () => { |
|
||||||
const location = useLocation() |
|
||||||
const navigate = useNavigate() |
|
||||||
const [msg, setMsg] = useState('') |
|
||||||
const context = React.useContext(AppContext) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (!context.apiKey) setMsg('Please provide a 34 or 32 character API key to continue') |
|
||||||
}, [context.apiKey]) |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<Formik |
|
||||||
initialValues={{apiKey: context.apiKey}} |
|
||||||
validate={(values) => { |
|
||||||
const errors = {} as any |
|
||||||
if (!values.apiKey) { |
|
||||||
errors.apiKey = 'Required' |
|
||||||
} else if (values.apiKey.length !== 34 && values.apiKey.length !== 32) { |
|
||||||
errors.apiKey = 'API key should be 34 or 32 characters long' |
|
||||||
} |
|
||||||
return errors |
|
||||||
}} |
|
||||||
onSubmit={(values) => { |
|
||||||
const apiKey = values.apiKey |
|
||||||
if (apiKey.length === 34 || apiKey.length === 32) { |
|
||||||
context.setAPIKey(values.apiKey) |
|
||||||
navigate(location && location.state ? location.state : '/') |
|
||||||
} |
|
||||||
}} |
|
||||||
> |
|
||||||
{({errors, touched, handleSubmit}) => ( |
|
||||||
<form onSubmit={handleSubmit}> |
|
||||||
<div className="form-group mb-2"> |
|
||||||
<label htmlFor="apikey">API Key</label> |
|
||||||
<Field |
|
||||||
className={errors.apiKey && touched.apiKey ? 'form-control form-control-sm is-invalid' : 'form-control form-control-sm'} |
|
||||||
type="password" |
|
||||||
name="apiKey" |
|
||||||
placeholder="e.g. GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ" |
|
||||||
/> |
|
||||||
<ErrorMessage className="invalid-feedback" name="apiKey" component="div" /> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div> |
|
||||||
<SubmitButton text="Save" dataId="save-api-key" disable={errors && errors.apiKey ? true : false} /> |
|
||||||
</div> |
|
||||||
</form> |
|
||||||
)} |
|
||||||
</Formik> |
|
||||||
|
|
||||||
<div data-id="api-key-result" className="text-primary mt-4 text-center" style={{fontSize: '0.8em'}} dangerouslySetInnerHTML={{__html: msg}} /> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
|
|
||||||
export const ErrorView = () => { |
|
||||||
return ( |
|
||||||
<div className="d-flex w-100 flex-column align-items-center"> |
|
||||||
<img className="pb-4" width="250" src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png" alt="Error page" /> |
|
||||||
<h5>Sorry, something unexpected happened.</h5> |
|
||||||
<h5> |
|
||||||
Please raise an issue:{' '} |
|
||||||
<a className="text-danger" href="https://github.com/ethereum/remix-project/issues"> |
|
||||||
Here |
|
||||||
</a> |
|
||||||
</h5> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
|
|
||||||
import {Navigate} from 'react-router-dom' |
|
||||||
|
|
||||||
import {AppContext} from '../AppContext' |
|
||||||
import {Receipt} from '../types' |
|
||||||
|
|
||||||
import {VerifyView} from './VerifyView' |
|
||||||
|
|
||||||
export const HomeView = () => { |
|
||||||
const context = React.useContext(AppContext) |
|
||||||
|
|
||||||
return !context.apiKey ? ( |
|
||||||
<Navigate |
|
||||||
to={{ |
|
||||||
pathname: '/settings' |
|
||||||
}} |
|
||||||
/> |
|
||||||
) : ( |
|
||||||
<VerifyView |
|
||||||
contracts={context.contracts} |
|
||||||
client={context.clientInstance} |
|
||||||
apiKey={context.apiKey} |
|
||||||
onVerifiedContract={(receipt: Receipt) => { |
|
||||||
const newReceipts = [...context.receipts, receipt] |
|
||||||
context.setReceipts(newReceipts) |
|
||||||
}} |
|
||||||
networkName={context.networkName} |
|
||||||
/> |
|
||||||
) |
|
||||||
} |
|
@ -1,170 +0,0 @@ |
|||||||
import React, {useState} from 'react' |
|
||||||
|
|
||||||
import {Formik, ErrorMessage, Field} from 'formik' |
|
||||||
import {getEtherScanApi, getNetworkName, getReceiptStatus, getProxyContractReceiptStatus} from '../utils' |
|
||||||
import {Receipt} from '../types' |
|
||||||
import {AppContext} from '../AppContext' |
|
||||||
import {SubmitButton} from '../components' |
|
||||||
import {Navigate} from 'react-router-dom' |
|
||||||
import {Button} from 'react-bootstrap' |
|
||||||
import {CustomTooltip} from '@remix-ui/helper' |
|
||||||
|
|
||||||
interface FormValues { |
|
||||||
receiptGuid: string |
|
||||||
} |
|
||||||
|
|
||||||
export const ReceiptsView = () => { |
|
||||||
const [results, setResults] = useState({succeed: false, message: ''}) |
|
||||||
const [isProxyContractReceipt, setIsProxyContractReceipt] = useState(false) |
|
||||||
const context = React.useContext(AppContext) |
|
||||||
|
|
||||||
const onGetReceiptStatus = async (values: FormValues, clientInstance: any, apiKey: string) => { |
|
||||||
try { |
|
||||||
const {network, networkId} = await getNetworkName(clientInstance) |
|
||||||
if (network === 'vm') { |
|
||||||
setResults({ |
|
||||||
succeed: false, |
|
||||||
message: 'Cannot verify in the selected network' |
|
||||||
}) |
|
||||||
return |
|
||||||
} |
|
||||||
const etherscanApi = getEtherScanApi(networkId) |
|
||||||
let result |
|
||||||
if (isProxyContractReceipt) { |
|
||||||
result = await getProxyContractReceiptStatus(values.receiptGuid, apiKey, etherscanApi) |
|
||||||
if (result.status === '1') { |
|
||||||
result.message = result.result |
|
||||||
result.result = 'Successfully Updated' |
|
||||||
} |
|
||||||
} else result = await getReceiptStatus(values.receiptGuid, apiKey, etherscanApi) |
|
||||||
setResults({ |
|
||||||
succeed: result.status === '1' ? true : false, |
|
||||||
message: result.result || (result.status === '0' ? 'Verification failed' : result.message) |
|
||||||
}) |
|
||||||
} catch (error: any) { |
|
||||||
setResults({ |
|
||||||
succeed: false, |
|
||||||
message: error.message |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return !context.apiKey ? ( |
|
||||||
<Navigate |
|
||||||
to={{ |
|
||||||
pathname: '/settings' |
|
||||||
}} |
|
||||||
/> |
|
||||||
) : ( |
|
||||||
<div> |
|
||||||
<Formik |
|
||||||
initialValues={{receiptGuid: ''}} |
|
||||||
validate={(values) => { |
|
||||||
const errors = {} as any |
|
||||||
if (!values.receiptGuid) { |
|
||||||
errors.receiptGuid = 'Required' |
|
||||||
} |
|
||||||
return errors |
|
||||||
}} |
|
||||||
onSubmit={(values) => onGetReceiptStatus(values, context.clientInstance, context.apiKey)} |
|
||||||
> |
|
||||||
{({errors, touched, handleSubmit, handleChange}) => ( |
|
||||||
<form onSubmit={handleSubmit}> |
|
||||||
<div className="form-group mb-2"> |
|
||||||
<label htmlFor="receiptGuid">Receipt GUID</label> |
|
||||||
<Field |
|
||||||
className={errors.receiptGuid && touched.receiptGuid ? 'form-control form-control-sm is-invalid' : 'form-control form-control-sm'} |
|
||||||
type="text" |
|
||||||
name="receiptGuid" |
|
||||||
/> |
|
||||||
<ErrorMessage className="invalid-feedback" name="receiptGuid" component="div" /> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div className="d-flex mb-2 custom-control custom-checkbox"> |
|
||||||
<Field |
|
||||||
className="custom-control-input" |
|
||||||
type="checkbox" |
|
||||||
name="isProxyReceipt" |
|
||||||
id="isProxyReceipt" |
|
||||||
onChange={async (e) => { |
|
||||||
handleChange(e) |
|
||||||
if (e.target.checked) setIsProxyContractReceipt(true) |
|
||||||
else setIsProxyContractReceipt(false) |
|
||||||
}} |
|
||||||
/> |
|
||||||
<label className="form-check-label custom-control-label" htmlFor="isProxyReceipt"> |
|
||||||
It's a proxy contract GUID |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
<SubmitButton dataId={null} text="Check" disable={!touched.receiptGuid || (touched.receiptGuid && errors.receiptGuid) ? true : false} /> |
|
||||||
</form> |
|
||||||
)} |
|
||||||
</Formik> |
|
||||||
|
|
||||||
<div |
|
||||||
className={results['succeed'] ? 'text-success mt-3 text-center' : 'text-danger mt-3 text-center'} |
|
||||||
dangerouslySetInnerHTML={{ |
|
||||||
__html: results.message ? results.message : '' |
|
||||||
}} |
|
||||||
/> |
|
||||||
|
|
||||||
<ReceiptsTable receipts={context.receipts} /> |
|
||||||
<br /> |
|
||||||
<CustomTooltip tooltipText="Clear the list of receipts" tooltipId="etherscan-clear-receipts" placement="bottom"> |
|
||||||
<Button |
|
||||||
className="btn-sm" |
|
||||||
onClick={() => { |
|
||||||
context.setReceipts([]) |
|
||||||
}} |
|
||||||
> |
|
||||||
Clear |
|
||||||
</Button> |
|
||||||
</CustomTooltip> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
const ReceiptsTable = ({receipts}) => { |
|
||||||
return ( |
|
||||||
<div className="table-responsive"> |
|
||||||
<h6>Receipts</h6> |
|
||||||
<table className="table h6 table-sm"> |
|
||||||
<thead> |
|
||||||
<tr> |
|
||||||
<th scope="col">Status</th> |
|
||||||
<th scope="col">GUID</th> |
|
||||||
</tr> |
|
||||||
</thead> |
|
||||||
<tbody> |
|
||||||
{receipts && |
|
||||||
receipts.length > 0 && |
|
||||||
receipts.map((item: Receipt, index) => { |
|
||||||
return ( |
|
||||||
<tr key={item.guid}> |
|
||||||
<td |
|
||||||
className={ |
|
||||||
item.status === 'Pass - Verified' || item.status === 'Successfully Updated' |
|
||||||
? 'text-success' |
|
||||||
: item.status === 'Pending in queue' |
|
||||||
? 'text-warning' |
|
||||||
: item.status === 'Already Verified' |
|
||||||
? 'text-info' |
|
||||||
: 'text-secondary' |
|
||||||
} |
|
||||||
> |
|
||||||
{item.status} |
|
||||||
{item.status === 'Successfully Updated' && ( |
|
||||||
<CustomTooltip placement={'bottom'} tooltipClasses="text-wrap" tooltipId="etherscan-receipt-proxy-status" tooltipText={item.message}> |
|
||||||
<i style={{fontSize: 'small'}} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i> |
|
||||||
</CustomTooltip> |
|
||||||
)} |
|
||||||
</td> |
|
||||||
<td>{item.guid}</td> |
|
||||||
</tr> |
|
||||||
) |
|
||||||
})} |
|
||||||
</tbody> |
|
||||||
</table> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,235 +0,0 @@ |
|||||||
import React, {useEffect, useRef, useState} from 'react' |
|
||||||
import { Web3 } from 'web3' |
|
||||||
|
|
||||||
import {PluginClient} from '@remixproject/plugin' |
|
||||||
import {CustomTooltip} from '@remix-ui/helper' |
|
||||||
import {Formik, ErrorMessage, Field} from 'formik' |
|
||||||
|
|
||||||
import {SubmitButton} from '../components' |
|
||||||
import {Receipt} from '../types' |
|
||||||
import {verify} from '../utils/verify' |
|
||||||
import {etherscanScripts} from '@remix-project/remix-ws-templates' |
|
||||||
|
|
||||||
interface Props { |
|
||||||
client: PluginClient |
|
||||||
apiKey: string |
|
||||||
onVerifiedContract: (receipt: Receipt) => void |
|
||||||
contracts: string[], |
|
||||||
networkName: string |
|
||||||
} |
|
||||||
|
|
||||||
interface FormValues { |
|
||||||
contractName: string |
|
||||||
contractAddress: string |
|
||||||
expectedImplAddress?: string |
|
||||||
} |
|
||||||
|
|
||||||
export const VerifyView = ({apiKey, client, contracts, onVerifiedContract, networkName}) => { |
|
||||||
const [results, setResults] = useState('') |
|
||||||
const [selectedContract, setSelectedContract] = useState('') |
|
||||||
const [showConstructorArgs, setShowConstructorArgs] = useState(false) |
|
||||||
const [isProxyContract, setIsProxyContract] = useState(false) |
|
||||||
const [constructorInputs, setConstructorInputs] = useState([]) |
|
||||||
const verificationResult = useRef({}) |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
if (contracts.includes(selectedContract)) updateConsFields(selectedContract) |
|
||||||
}, [contracts]) |
|
||||||
|
|
||||||
const updateConsFields = (contractName) => { |
|
||||||
client.call('compilerArtefacts' as any, 'getArtefactsByContractName', contractName).then((result) => { |
|
||||||
const {artefact} = result |
|
||||||
if (artefact && artefact.abi && artefact.abi[0] && artefact.abi[0].type && artefact.abi[0].type === 'constructor' && artefact.abi[0].inputs.length > 0) { |
|
||||||
setConstructorInputs(artefact.abi[0].inputs) |
|
||||||
setShowConstructorArgs(true) |
|
||||||
} else { |
|
||||||
setConstructorInputs([]) |
|
||||||
setShowConstructorArgs(false) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
const onVerifyContract = async (values: FormValues) => { |
|
||||||
const compilationResult = (await client.call('solidity', 'getCompilationResult')) as any |
|
||||||
|
|
||||||
if (!compilationResult) { |
|
||||||
throw new Error('no compilation result available') |
|
||||||
} |
|
||||||
|
|
||||||
const constructorValues = [] |
|
||||||
for (const key in values) { |
|
||||||
if (key.startsWith('contractArgValue')) constructorValues.push(values[key]) |
|
||||||
} |
|
||||||
const web3 = new Web3() |
|
||||||
const constructorTypes = constructorInputs.map((e) => e.type) |
|
||||||
let contractArguments = web3.eth.abi.encodeParameters(constructorTypes, constructorValues) |
|
||||||
contractArguments = contractArguments.replace('0x', '') |
|
||||||
|
|
||||||
verificationResult.current = await verify( |
|
||||||
apiKey, |
|
||||||
values.contractAddress, |
|
||||||
contractArguments, |
|
||||||
values.contractName, |
|
||||||
compilationResult, |
|
||||||
null, |
|
||||||
isProxyContract, |
|
||||||
values.expectedImplAddress, |
|
||||||
client, |
|
||||||
onVerifiedContract, |
|
||||||
setResults |
|
||||||
) |
|
||||||
setResults(verificationResult.current['message']) |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
<Formik |
|
||||||
initialValues={{ |
|
||||||
contractName: '', |
|
||||||
contractAddress: '' |
|
||||||
}} |
|
||||||
validate={(values) => { |
|
||||||
const errors = {} as any |
|
||||||
if (!values.contractName) { |
|
||||||
errors.contractName = 'Required' |
|
||||||
} |
|
||||||
if (!values.contractAddress) { |
|
||||||
errors.contractAddress = 'Required' |
|
||||||
} |
|
||||||
if (values.contractAddress.trim() === '' || !values.contractAddress.startsWith('0x') || values.contractAddress.length !== 42) { |
|
||||||
errors.contractAddress = 'Please enter a valid contract address' |
|
||||||
} |
|
||||||
return errors |
|
||||||
}} |
|
||||||
onSubmit={(values) => onVerifyContract(values)} |
|
||||||
> |
|
||||||
{({errors, touched, handleSubmit, handleChange, isSubmitting}) => { |
|
||||||
return ( |
|
||||||
<form onSubmit={handleSubmit}> |
|
||||||
<div className="form-group"> |
|
||||||
<label htmlFor="network">Selected Network</label> |
|
||||||
<CustomTooltip |
|
||||||
tooltipText="Network is fetched from 'Deploy and Run Transactions' plugin's ENVIRONMENT field" |
|
||||||
tooltipId="etherscan-impl-address2" |
|
||||||
placement="bottom" |
|
||||||
> |
|
||||||
<Field className="form-control" type="text" name="network" value={networkName} disabled={true} /> |
|
||||||
</CustomTooltip> |
|
||||||
</div> |
|
||||||
<div className="form-group"> |
|
||||||
<label htmlFor="contractName">Contract Name</label> |
|
||||||
<Field |
|
||||||
as="select" |
|
||||||
className={errors.contractName && touched.contractName && contracts.length ? 'form-control is-invalid' : 'form-control'} |
|
||||||
name="contractName" |
|
||||||
onChange={async (e) => { |
|
||||||
handleChange(e) |
|
||||||
setSelectedContract(e.target.value) |
|
||||||
updateConsFields(e.target.value) |
|
||||||
}} |
|
||||||
> |
|
||||||
<option disabled={true} value=""> |
|
||||||
{contracts.length ? 'Select a contract' : `--- No compiled contracts ---`} |
|
||||||
</option> |
|
||||||
{contracts.map((item) => ( |
|
||||||
<option key={item} value={item}> |
|
||||||
{item} |
|
||||||
</option> |
|
||||||
))} |
|
||||||
</Field> |
|
||||||
<ErrorMessage className="invalid-feedback" name="contractName" component="div" /> |
|
||||||
</div> |
|
||||||
<div className={showConstructorArgs ? 'form-group d-block' : 'form-group d-none'}> |
|
||||||
<label>Constructor Arguments</label> |
|
||||||
{constructorInputs.map((item, index) => { |
|
||||||
return ( |
|
||||||
<div className="d-flex"> |
|
||||||
<Field className="form-control m-1" type="text" key={`contractArgName${index}`} name={`contractArgName${index}`} value={item.name} disabled={true} /> |
|
||||||
<CustomTooltip tooltipText={`value of ${item.name}`} tooltipId={`etherscan-constructor-value${index}`} placement="top"> |
|
||||||
<Field className="form-control m-1" type="text" key={`contractArgValue${index}`} name={`contractArgValue${index}`} placeholder={item.type} /> |
|
||||||
</CustomTooltip> |
|
||||||
</div> |
|
||||||
) |
|
||||||
})} |
|
||||||
</div> |
|
||||||
<div className="form-group"> |
|
||||||
<label htmlFor="contractAddress">Contract Address</label> |
|
||||||
<Field |
|
||||||
className={errors.contractAddress && touched.contractAddress ? 'form-control is-invalid' : 'form-control'} |
|
||||||
type="text" |
|
||||||
name="contractAddress" |
|
||||||
placeholder="e.g. 0x11b79afc03baf25c631dd70169bb6a3160b2706e" |
|
||||||
/> |
|
||||||
<ErrorMessage className="invalid-feedback" name="contractAddress" component="div" /> |
|
||||||
<div className="d-flex mb-2 custom-control custom-checkbox"> |
|
||||||
<Field |
|
||||||
className="custom-control-input" |
|
||||||
type="checkbox" |
|
||||||
name="isProxy" |
|
||||||
id="isProxy" |
|
||||||
onChange={async (e) => { |
|
||||||
handleChange(e) |
|
||||||
if (e.target.checked) setIsProxyContract(true) |
|
||||||
else setIsProxyContract(false) |
|
||||||
}} |
|
||||||
/> |
|
||||||
<label className="form-check-label custom-control-label" htmlFor="isProxy"> |
|
||||||
It's a proxy contract address |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div className={isProxyContract ? 'form-group d-block' : 'form-group d-none'}> |
|
||||||
<label htmlFor="expectedImplAddress">Expected Implementation Address</label> |
|
||||||
<CustomTooltip |
|
||||||
tooltipText="Providing expected implementation address enforces a check to ensure the returned implementation contract address is same as address picked up by the verifier" |
|
||||||
tooltipId="etherscan-impl-address" |
|
||||||
placement="bottom" |
|
||||||
> |
|
||||||
<Field className="form-control" type="text" name="expectedImplAddress" placeholder="verified implementation contract address" /> |
|
||||||
</CustomTooltip> |
|
||||||
<i style={{fontSize: 'x-small'}} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i> |
|
||||||
<label> Make sure contract is already verified on Etherscan</label> |
|
||||||
</div> |
|
||||||
<SubmitButton |
|
||||||
dataId="verify-contract" |
|
||||||
text="Verify" |
|
||||||
isSubmitting={isSubmitting} |
|
||||||
disable={ |
|
||||||
!contracts.length || |
|
||||||
!touched.contractName || |
|
||||||
!touched.contractAddress || |
|
||||||
(touched.contractName && errors.contractName) || |
|
||||||
(touched.contractAddress && errors.contractAddress) || |
|
||||||
networkName === 'VM (Not supported)' |
|
||||||
? true |
|
||||||
: false |
|
||||||
} |
|
||||||
/> |
|
||||||
<br /> |
|
||||||
<CustomTooltip tooltipText="Generate the required TS scripts to verify a contract on Etherscan" tooltipId="etherscan-generate-scripts" placement="bottom"> |
|
||||||
<button |
|
||||||
type="button" |
|
||||||
className="mr-2 mb-2 py-1 px-2 btn btn-secondary btn-block" |
|
||||||
onClick={async () => { |
|
||||||
etherscanScripts({}, client) |
|
||||||
}} |
|
||||||
> |
|
||||||
Generate Verification Scripts |
|
||||||
</button> |
|
||||||
</CustomTooltip> |
|
||||||
</form> |
|
||||||
) |
|
||||||
}} |
|
||||||
</Formik> |
|
||||||
<div |
|
||||||
data-id="verify-result" |
|
||||||
className={verificationResult.current['succeed'] ? 'text-success mt-4 text-center' : 'text-danger mt-4 text-center'} |
|
||||||
style={{fontSize: '0.8em'}} |
|
||||||
dangerouslySetInnerHTML={{__html: results}} |
|
||||||
/> |
|
||||||
{/* <div style={{ display: "block", textAlign: "center", marginTop: "1em" }}> |
|
||||||
<Link to="/receipts">View Receipts</Link> |
|
||||||
</div> */} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
export { HomeView } from "./HomeView" |
|
||||||
export { ErrorView } from "./ErrorView" |
|
||||||
export { ReceiptsView } from "./ReceiptsView" |
|
||||||
export { CaptureKeyView } from "./CaptureKeyView" |
|
@ -1,14 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import * as ReactDOM from 'react-dom' |
|
||||||
import { createRoot } from 'react-dom/client'; |
|
||||||
import App from './app/app' |
|
||||||
|
|
||||||
|
|
||||||
const container = document.getElementById('root'); |
|
||||||
|
|
||||||
if (container) { |
|
||||||
createRoot(container).render( |
|
||||||
<App /> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
@ -1,16 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "etherscan", |
|
||||||
"displayName": "Contract verification - Etherscan", |
|
||||||
"description": "Verify Solidity contract code using Etherscan, BscScan, PolygonScan etc. APIs", |
|
||||||
"version": "0.1.0", |
|
||||||
"events": [], |
|
||||||
"methods": ["verify", "receiptStatus"], |
|
||||||
"kind": "none", |
|
||||||
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHsAAAB7CAYAAABUx/9/AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAHBklEQVR42u2dy3WbTBTH/+ikgFEFwZ8K8HQAm6yjDkIHdiogHWBXgDqADgQVaLLJToZ1NtAB38JC0QNJIPGYx73nsIgcHVvz033OnblWVVXQQcqyhBACQgiUZYk0TfHnz5/q79+/rd7PGPsA8I1z/gEAjuOAMQbOOVzX1WKNLFVhCyGQJAnSNEUcx0N/CMu2bXDO4TgOXNcF51y9RauqSomnKApEUQTG2BZANeXDGNsyxuB5HqIoUmYNpf8DoyjC1HBbPEqAl/KPyrJMCg2+V+N930eWZQT72rNer6Ea4FvavtlsCLbGkM+gu66L9XptNuwsy3SG3KjpU5r3ySJrgyCfQfd93wzYmpvs1sBt2x7dtJM2S2DatYJN2nxby8eI2of/BQSzNfQgCNSETWb7PuDL5VIt2IalVL0D55wPwmXW98bKfD7fPj096bFvOtHeVJ7n2/l8DiGEvLteKtazZa+z9xm4EWiDgBNog4ATaIOAE2iDgFMebVBadnfDoWVZlF5N1CTKOcdms+n8xhmBVi8PF0Lg58+fw+fZZErlMeldGxwpIFM8YOvS+UJarUF/W++waWGbNSuKoof6ynpom269NUrme0SfOYQydTHnpNUPwO6zU3QMc06gH1hgyTp6blqam0UVyqkvL02fJ2B7WGfLtu26caR7UYVAK9f0gLe3t+7fzvl8vi3L8j9aQ2U0u75QYLHbt2hfQSOfrJzPvnni5OK3k0y4epp9y3fPCLSevnu1WrX7dhJspTX74jbojEDruw162oo8o3XRV97f3y+bIkq3tDHjzWmYhukWPM976Oxzy50oFQ5AIgzD5to4DG/I6whACdiHBwVnhyZcB9uq5M2DAwljbJskyXmApouv/vr1K1E+YFqWJeI4pmjcFEnTlGCbInmeAwC+UCFFa5/9AaC+//UTNom2PntxWDIlM65x0ScMw6PshGBrCtr3fXied/QiwdYQtOd5+PXr19kPZhSc6QXadV2EYdj4wykCNItzDs555wLI+/s7bdTcWNfdbZIXRam7Om9sUPR64y/UqY232hMY3Wf/+PHjofcXRbEgJT7X6DaH80eHrcuMLNVAUzRuEGiCbRBogm0Q6Le3N6qNmwB6V0uxSLMVK5jcCZrMuEqg6ybKLnLaakawFQDt+/7FEug10KfVRvLZkkrd9x0EwdnuVRtpKisTbIlBr9fru7plL21uEWwJzXY9+umuN1/ZxSSfLRno19fXQUCTZkskVVVZcRxjuVwOApo0WzIZEjTB1sHud+g0mlVVZdGSaQ/a4pyTZpug0cDncFeCbQBoAHh+fibYJoAGPjuECLYikuf5Q2fyOOefeXZVVRb1j8srD951Y3HOwRgjzdYc9N6EA1RB09I/n4rjOJ95dv1CfZaXRB/QjLEPxti+MreHTc33+ml0WZaLwz598tl3sPA8z7p2Y/9UEXeTfP/+/d8/Rp7yM/S5qCHPesHzvF6HwAy59vVUoKIo9r/jKEArimJBKdixzwOweHl5aTzvLJvZPjXhnueBMbZ/jaLxKzJGHDOkcp0eojzz2bQLNo4kSTIkaItzfnaIkjRb4Wj7mry8vJy91hiNk3YPI0NE203fJdu2G9uPSbNHkjHvcr904cGXa5Eo3V+ijtk+yB7w+vraDbZqaVj9QW3bNlKb63QrCIKjdOvoSzfyLM5eRi3UR1Bt2wbnHI7jwHVdae4az/McT09PYyvKzVmcSvrsLMss2bR4Km0+lCAIrv+HsUcHDlFqlOHZHaedbFxGL/Oze67bagd5ZzYnm4tS18Db1OxbmfGiKBZxHO/nRDHG8Pz8fC1YOvKftm1LaXJVibJvBWW+77da315nU1HOPP73zXXd1jcy0H52B1mtVrAsq5IBdN2F0ulGBl0Dpj6f3YJKN8PscEBbq0CbYCoHuaqrZF0/D/lsuX1yo5++5y408tknVa/5fL6VxSdfq5J1vSKLfLb8proxn95sNvf31JkIuOUUXa1AGwd7VztWanJwX6CNgK2KmR4atJawsyxTUoOHBq0N7CiK6u6MSodnCNDKwtYNLhom+Qxx8kT65oUkSSCEwO/fv7FarXSvAFnL5RJhGF5sLXpEpICd5znyPIcQAmVZIk1TJEliWmnPCoLgYrOgUrCTJEGSJAD+TWo3EOjFqlgYhsOPwRrT12rqYx+eYHh40nLQ9TesV0sayHWNe1RlmyqiNhm07/ujabM0qdfUzXpTmOyhDvMrk2fv9ma1hVz3iU29ztSWO7Am910F066ClmUZfN9XdofK9/1JzbWy5dIoilSAjuVyiSiKpF5L5WriMjQd1BrseR6iKJoksr7nUbbhUAiBJEmQpiniOB7lNgMZT4x2+hA6dZfWmyZ1fV0I0bpLtL4Gq4boOM7+GFN9q6/q8j8QmqQtM04gOgAAAABJRU5ErkJggg==", |
|
||||||
"location": "sidePanel", |
|
||||||
"url": "https://ipfs-cluster.ethdevops.io/ipfs/QmQsZbBSYCVBVpz2mVRbPRVTrcz59oJEpuuoxiT9otu3mh", |
|
||||||
"repo": "https://github.com/ethereum/remix-project/tree/master/apps/etherscan", |
|
||||||
"documentation": "https://remix-ide.readthedocs.io/en/latest/contract_verification.html#etherscan", |
|
||||||
"maintainedBy": "Remix", |
|
||||||
"authorContact": "remix@ethereum.org" |
|
||||||
} |
|
@ -0,0 +1 @@ |
|||||||
|
# Remix Dapp |
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
"name": "remix-dapp", |
||||||
|
"version": "1.0.0", |
||||||
|
"main": "index.js", |
||||||
|
"license": "MIT", |
||||||
|
"dependencies": { |
||||||
|
"webpack-manifest-plugin": "^5.0.0" |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue