commit
d2d68ce494
@ -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 |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
{ |
||||||
|
"presets": ["@babel/preset-env", ["@babel/preset-react", { "runtime": "automatic" }]], |
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"], |
||||||
|
"ignore": ["**/node_modules/**"] |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
# This file is used by: |
||||||
|
# 1. autoprefixer to adjust CSS to support the below specified browsers |
||||||
|
# 2. babel preset-env to adjust included polyfills |
||||||
|
# |
||||||
|
# For additional information regarding the format and rule options, please see: |
||||||
|
# https://github.com/browserslist/browserslist#queries |
||||||
|
# |
||||||
|
# If you need to support different browsers in production, you may tweak the list below. |
||||||
|
|
||||||
|
last 1 Chrome version |
||||||
|
last 1 Firefox version |
||||||
|
last 2 Edge major versions |
||||||
|
last 2 Safari major version |
||||||
|
last 2 iOS major versions |
||||||
|
Firefox ESR |
||||||
|
not IE 9-11 # For IE 9-11 support, remove 'not'. |
@ -0,0 +1,3 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../.eslintrc.json" |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
{ |
||||||
|
"extends": [ |
||||||
|
"plugin:@nrwl/nx/react", |
||||||
|
"../../.eslintrc.json" |
||||||
|
], |
||||||
|
"ignorePatterns": [ |
||||||
|
"!**/*" |
||||||
|
], |
||||||
|
"overrides": [ |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.ts", |
||||||
|
"*.tsx", |
||||||
|
"*.js", |
||||||
|
"*.jsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.ts", |
||||||
|
"*.tsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"files": [ |
||||||
|
"*.js", |
||||||
|
"*.jsx" |
||||||
|
], |
||||||
|
"rules": {} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -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,10 @@ |
|||||||
|
html, body, #root { |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.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,68 @@ |
|||||||
|
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' : ''} ${!hasContracts ? 'text-muted' : ''}`} 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,31 @@ |
|||||||
|
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 flex-column justify-content-center py-2 px-1 small ' + (isActive ? 'bg-light' : 'bg-transparent')}> |
||||||
|
<span> |
||||||
|
<span>{icon}</span> |
||||||
|
<span className="ml-2">{title}</span> |
||||||
|
</span> |
||||||
|
</NavLink> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const NavMenu = () => { |
||||||
|
return ( |
||||||
|
<nav className="d-flex flex-row justify-start w-100"> |
||||||
|
<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" /> |
||||||
|
<div className="flex-grow-1"></div> |
||||||
|
</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,37 @@ |
|||||||
|
import { type Dispatch, type SetStateAction, useState } from 'react' |
||||||
|
|
||||||
|
export function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] |
||||||
|
{ |
||||||
|
// State to store our value
|
||||||
|
// Pass initial state function to useState so logic is only executed once
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(() => { |
||||||
|
try { |
||||||
|
// Get from local storage by key
|
||||||
|
const item = window.localStorage.getItem(key) |
||||||
|
// Parse stored json or if none return initialValue
|
||||||
|
return item ? JSON.parse(item) : initialValue |
||||||
|
} catch (error) { |
||||||
|
// If error also return initialValue
|
||||||
|
console.error(error) |
||||||
|
return initialValue |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Return a wrapped version of useState's setter function that ...
|
||||||
|
// ... persists the new value to localStorage.
|
||||||
|
const setValue = (value: SetStateAction<T>) => { |
||||||
|
try { |
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore = value instanceof Function ? value(storedValue) : value |
||||||
|
// Save state
|
||||||
|
setStoredValue(valueToStore) |
||||||
|
// Save to local storage
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore)) |
||||||
|
} catch (error) { |
||||||
|
// A more advanced implementation would handle the error case
|
||||||
|
console.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return [storedValue, setValue] |
||||||
|
} |
@ -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" 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,156 @@ |
|||||||
|
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 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) |
||||||
|
} 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 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,236 @@ |
|||||||
|
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 } = 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 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 }) |
||||||
|
} |
||||||
|
|
||||||
|
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} /> |
||||||
|
|
||||||
|
<ContractDropdown label="Contract Name" id="contract-dropdown-1" selectedContract={selectedContract} setSelectedContract={setSelectedContract} /> |
||||||
|
|
||||||
|
{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> |
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary mt-3" disabled={submitDisabled}> |
||||||
|
Verify |
||||||
|
</button> |
||||||
|
</form> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
export { VerifyView } from './VerifyView' |
||||||
|
export { SettingsView } from './SettingsView' |
||||||
|
export { LookupView } from './LookupView' |
||||||
|
export { ReceiptsView } from './ReceiptsView' |
@ -0,0 +1,3 @@ |
|||||||
|
export const environment = { |
||||||
|
production: true, |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
// This file can be replaced during build by using the `fileReplacements` array.
|
||||||
|
// When building for production, this file is replaced with `environment.prod.ts`.
|
||||||
|
|
||||||
|
export const environment = { |
||||||
|
production: false, |
||||||
|
} |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,16 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<title>Contract Verification</title> |
||||||
|
<base href="./" /> |
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico" /> |
||||||
|
<link rel="stylesheet" integrity="ha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="root"></div> |
||||||
|
<script src="https://kit.fontawesome.com/41dd021e94.js" crossorigin="anonymous"></script> |
||||||
|
</body> |
||||||
|
</html> |
@ -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 />) |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
/** |
||||||
|
* Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. |
||||||
|
* |
||||||
|
* See: https://github.com/zloirock/core-js#babel
|
||||||
|
*/ |
||||||
|
import 'core-js/stable' |
||||||
|
import 'regenerator-runtime/runtime' |
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
|||||||
|
/* You can add global styles to this file, and also import other style files */ |
@ -0,0 +1,22 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "../../dist/out-tsc", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts" |
||||||
|
], |
||||||
|
"exclude": [ |
||||||
|
"**/*.spec.ts", |
||||||
|
"**/*.test.ts", |
||||||
|
"**/*.spec.tsx", |
||||||
|
"**/*.test.tsx", |
||||||
|
"**/*.spec.js", |
||||||
|
"**/*.test.js", |
||||||
|
"**/*.spec.jsx", |
||||||
|
"**/*.test.jsx" |
||||||
|
], |
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
const { composePlugins, withNx } = require('@nrwl/webpack') |
||||||
|
const webpack = require('webpack') |
||||||
|
const TerserPlugin = require('terser-webpack-plugin') |
||||||
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') |
||||||
|
|
||||||
|
const versionData = { |
||||||
|
timestamp: Date.now(), |
||||||
|
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', |
||||||
|
} |
||||||
|
// Nx plugins for webpack.
|
||||||
|
module.exports = composePlugins(withNx(), (config) => { |
||||||
|
// Update the webpack config as needed here.
|
||||||
|
// e.g. `config.plugins.push(new MyPlugin())`
|
||||||
|
|
||||||
|
// add fallback for node modules
|
||||||
|
config.resolve.fallback = { |
||||||
|
...config.resolve.fallback, |
||||||
|
crypto: require.resolve('crypto-browserify'), |
||||||
|
stream: require.resolve('stream-browserify'), |
||||||
|
path: require.resolve('path-browserify'), |
||||||
|
http: require.resolve('stream-http'), |
||||||
|
https: require.resolve('https-browserify'), |
||||||
|
constants: require.resolve('constants-browserify'), |
||||||
|
os: false, //require.resolve("os-browserify/browser"),
|
||||||
|
timers: false, // require.resolve("timers-browserify"),
|
||||||
|
zlib: require.resolve('browserify-zlib'), |
||||||
|
fs: false, |
||||||
|
module: false, |
||||||
|
tls: false, |
||||||
|
net: false, |
||||||
|
readline: false, |
||||||
|
child_process: false, |
||||||
|
buffer: require.resolve('buffer/'), |
||||||
|
vm: require.resolve('vm-browserify'), |
||||||
|
} |
||||||
|
|
||||||
|
// add externals
|
||||||
|
config.externals = { |
||||||
|
...config.externals, |
||||||
|
solc: 'solc', |
||||||
|
} |
||||||
|
|
||||||
|
// add public path
|
||||||
|
config.output.publicPath = '/' |
||||||
|
|
||||||
|
// set filename
|
||||||
|
config.output.filename = `[name].plugin-contract-verification.${versionData.timestamp}.js` |
||||||
|
config.output.chunkFilename = `[name].plugin-contract-verification.${versionData.timestamp}.js` |
||||||
|
|
||||||
|
// add copy & provide plugin
|
||||||
|
config.plugins.push( |
||||||
|
new webpack.ProvidePlugin({ |
||||||
|
Buffer: ['buffer', 'Buffer'], |
||||||
|
url: ['url', 'URL'], |
||||||
|
process: 'process/browser', |
||||||
|
}) |
||||||
|
) |
||||||
|
|
||||||
|
// souce-map loader
|
||||||
|
config.module.rules.push({ |
||||||
|
test: /\.js$/, |
||||||
|
use: ['source-map-loader'], |
||||||
|
enforce: 'pre', |
||||||
|
}) |
||||||
|
|
||||||
|
config.ignoreWarnings = [/Failed to parse source map/] // ignore source-map-loader warnings
|
||||||
|
|
||||||
|
// set minimizer
|
||||||
|
config.optimization.minimizer = [ |
||||||
|
new TerserPlugin({ |
||||||
|
parallel: true, |
||||||
|
terserOptions: { |
||||||
|
ecma: 2015, |
||||||
|
compress: false, |
||||||
|
mangle: false, |
||||||
|
format: { |
||||||
|
comments: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
extractComments: false, |
||||||
|
}), |
||||||
|
new CssMinimizerPlugin(), |
||||||
|
] |
||||||
|
|
||||||
|
config.watchOptions = { |
||||||
|
ignored: /node_modules/, |
||||||
|
} |
||||||
|
|
||||||
|
return config |
||||||
|
}) |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue