parent
07a7a8ea39
commit
6ed602bb7c
@ -0,0 +1,9 @@ |
||||
{ |
||||
"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,7 @@ |
||||
body { |
||||
margin: 0; |
||||
} |
||||
|
||||
#root { |
||||
padding: 8px 14px; |
||||
} |
@ -0,0 +1,25 @@ |
||||
import React from 'react' |
||||
import {PluginClient} from '@remixproject/plugin' |
||||
|
||||
import {Receipt, ThemeType} from './types' |
||||
|
||||
export const AppContext = React.createContext({ |
||||
apiKey: '', |
||||
setAPIKey: (value: string) => { |
||||
console.log('Set API Key from Context') |
||||
}, |
||||
clientInstance: {} as PluginClient, |
||||
receipts: [] as Receipt[], |
||||
setReceipts: (receipts: Receipt[]) => { |
||||
console.log('Calling Set Receipts') |
||||
}, |
||||
contracts: [] as string[], |
||||
setContracts: (contracts: string[]) => { |
||||
console.log('Calling Set Contract Names') |
||||
}, |
||||
themeType: 'dark' as ThemeType, |
||||
setThemeType: (themeType: ThemeType) => { |
||||
console.log('Calling Set Theme Type') |
||||
}, |
||||
networkName: '' |
||||
}) |
@ -0,0 +1,70 @@ |
||||
import { PluginClient } from '@remixproject/plugin' |
||||
import { createClient } from '@remixproject/plugin-webview' |
||||
import { verify, EtherScanReturn } from './utils/verify' |
||||
import { getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus } from './utils' |
||||
import EventManager from 'events' |
||||
|
||||
export class EtherscanPluginClient extends PluginClient { |
||||
public internalEvents: EventManager |
||||
|
||||
constructor() { |
||||
super() |
||||
this.internalEvents = new EventManager() |
||||
createClient(this) |
||||
this.onload() |
||||
} |
||||
|
||||
onActivation(): void { |
||||
this.internalEvents.emit('etherscan_activated') |
||||
} |
||||
|
||||
async verify( |
||||
apiKey: string, |
||||
contractAddress: string, |
||||
contractArguments: string, |
||||
contractName: string, |
||||
compilationResultParam: any, |
||||
chainRef?: number | string, |
||||
isProxyContract?: boolean, |
||||
expectedImplAddress?: string |
||||
) { |
||||
const result = await verify( |
||||
apiKey, |
||||
contractAddress, |
||||
contractArguments, |
||||
contractName, |
||||
compilationResultParam, |
||||
chainRef, |
||||
isProxyContract, |
||||
expectedImplAddress, |
||||
this, |
||||
(value: EtherScanReturn) => {}, |
||||
(value: string) => {} |
||||
) |
||||
return result |
||||
} |
||||
|
||||
async receiptStatus(receiptGuid: string, apiKey: string, isProxyContract: boolean) { |
||||
try { |
||||
const { network, networkId } = await getNetworkName(this) |
||||
if (network === 'vm') { |
||||
throw new Error('Cannot check the receipt status in the selected network') |
||||
} |
||||
const etherscanApi = getEtherScanApi(networkId) |
||||
let receiptStatus |
||||
|
||||
if (isProxyContract) receiptStatus = await getProxyContractReceiptStatus(receiptGuid, apiKey, etherscanApi) |
||||
else receiptStatus = await getReceiptStatus(receiptGuid, apiKey, etherscanApi) |
||||
return { |
||||
message: receiptStatus.result, |
||||
succeed: receiptStatus.status === '0' ? false : true |
||||
} |
||||
} catch (e: any) { |
||||
return { |
||||
status: 'error', |
||||
message: e.message, |
||||
succeed: false |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,136 @@ |
||||
import React, {useState, useEffect, useRef} from 'react' |
||||
|
||||
import {CompilationFileSources, CompilationResult} from '@remixproject/plugin-api' |
||||
|
||||
import { EtherscanPluginClient } from './EtherscanPluginClient' |
||||
|
||||
import {AppContext} from './AppContext' |
||||
import {DisplayRoutes} from './routes' |
||||
|
||||
import {useLocalStorage} from './hooks/useLocalStorage' |
||||
|
||||
import {getReceiptStatus, getEtherScanApi, getNetworkName, getProxyContractReceiptStatus} from './utils' |
||||
import {Receipt, ThemeType} from './types' |
||||
|
||||
import './App.css' |
||||
|
||||
export const getNewContractNames = (compilationResult: CompilationResult) => { |
||||
const compiledContracts = compilationResult.contracts |
||||
let result: string[] = [] |
||||
|
||||
for (const file of Object.keys(compiledContracts)) { |
||||
const newContractNames = Object.keys(compiledContracts[file]) |
||||
|
||||
result = [...result, ...newContractNames] |
||||
} |
||||
|
||||
return result |
||||
} |
||||
|
||||
const plugin = new EtherscanPluginClient() |
||||
|
||||
const App = () => { |
||||
const [apiKey, setAPIKey] = useLocalStorage('apiKey', '') |
||||
const [receipts, setReceipts] = useLocalStorage('receipts', [])
|
||||
const [contracts, setContracts] = useState<string[]>([]) |
||||
const [themeType, setThemeType] = useState<ThemeType>('dark') |
||||
const [networkName, setNetworkName] = useState('Loading...') |
||||
const timer = useRef(null) |
||||
const contractsRef = useRef(contracts) |
||||
|
||||
contractsRef.current = contracts |
||||
|
||||
const setListeners = () => { |
||||
plugin.on('solidity', 'compilationFinished', (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => { |
||||
const newContractsNames = getNewContractNames(data) |
||||
|
||||
const newContractsToSave: string[] = [...contractsRef.current, ...newContractsNames] |
||||
|
||||
const uniqueContracts: string[] = [...new Set(newContractsToSave)] |
||||
|
||||
setContracts(uniqueContracts) |
||||
}) |
||||
plugin.on('blockchain' as any, 'networkStatus', (result) => { |
||||
setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`) |
||||
}) |
||||
// @ts-ignore
|
||||
plugin.call('blockchain', 'getCurrentNetworkStatus').then((result: any) => setNetworkName(`${result.network.name} ${result.network.id !== '-' ? `(Chain id: ${result.network.id})` : '(Not supported)'}`)) |
||||
|
||||
} |
||||
|
||||
useEffect(() => { |
||||
plugin.onload(() => { |
||||
setListeners() |
||||
}) |
||||
}, []) |
||||
|
||||
useEffect(() => { |
||||
let receiptsNotVerified: Receipt[] = receipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached') |
||||
|
||||
if (receiptsNotVerified.length > 0) { |
||||
if (timer.current) { |
||||
clearInterval(timer.current) |
||||
timer.current = null |
||||
} |
||||
timer.current = setInterval(async () => { |
||||
const {network, networkId} = await getNetworkName(plugin) |
||||
|
||||
if (!plugin) return |
||||
if (network === 'vm') return |
||||
let newReceipts = receipts |
||||
|
||||
for (const item of receiptsNotVerified) { |
||||
await new Promise((r) => setTimeout(r, 500)) // avoid api rate limit exceed.
|
||||
let status |
||||
if (item.isProxyContract) { |
||||
status = await getProxyContractReceiptStatus(item.guid, apiKey, getEtherScanApi(networkId)) |
||||
if (status.status === '1') { |
||||
status.message = status.result |
||||
status.result = 'Successfully Updated' |
||||
} |
||||
} else status = await getReceiptStatus(item.guid, apiKey, getEtherScanApi(networkId)) |
||||
if (status.result === 'Pass - Verified' || status.result === 'Already Verified' || status.result === 'Successfully Updated') { |
||||
newReceipts = newReceipts.map((currentReceipt: Receipt) => { |
||||
if (currentReceipt.guid === item.guid) { |
||||
const res = { |
||||
...currentReceipt, |
||||
status: status.result |
||||
} |
||||
if (currentReceipt.isProxyContract) res.message = status.message |
||||
return res |
||||
} |
||||
return currentReceipt |
||||
}) |
||||
} |
||||
} |
||||
receiptsNotVerified = newReceipts.filter((item: Receipt) => item.status === 'Pending in queue' || item.status === 'Max rate limit reached') |
||||
if (timer.current && receiptsNotVerified.length === 0) { |
||||
clearInterval(timer.current) |
||||
timer.current = null |
||||
} |
||||
setReceipts(newReceipts) |
||||
}, 10000) |
||||
} |
||||
}, [receipts]) |
||||
|
||||
return ( |
||||
<AppContext.Provider |
||||
value={{ |
||||
apiKey, |
||||
setAPIKey, |
||||
clientInstance: plugin, |
||||
receipts, |
||||
setReceipts, |
||||
contracts, |
||||
setContracts, |
||||
themeType, |
||||
setThemeType, |
||||
networkName |
||||
}} |
||||
> |
||||
{ plugin && <DisplayRoutes /> } |
||||
</AppContext.Provider> |
||||
) |
||||
} |
||||
|
||||
export default App |
@ -0,0 +1,81 @@ |
||||
import React from 'react' |
||||
|
||||
import {NavLink} from 'react-router-dom' |
||||
import {CustomTooltip} from '@remix-ui/helper' |
||||
import {AppContext} from '../AppContext' |
||||
|
||||
interface Props { |
||||
title?: string |
||||
from: string |
||||
} |
||||
|
||||
interface IconProps { |
||||
from: string |
||||
} |
||||
|
||||
const HomeIcon = ({from}: IconProps) => { |
||||
return ( |
||||
<NavLink |
||||
data-id="home" |
||||
to={{ |
||||
pathname: '/' |
||||
}} |
||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
||||
state={from} |
||||
> |
||||
<CustomTooltip tooltipText="Home" tooltipId="etherscan-nav-home" placement="bottom"> |
||||
<i className="fas fa-home"></i> |
||||
</CustomTooltip> |
||||
</NavLink> |
||||
) |
||||
} |
||||
|
||||
const ReceiptsIcon = ({from}: IconProps) => { |
||||
return ( |
||||
<NavLink |
||||
data-id="receipts" |
||||
to={{ |
||||
pathname: '/receipts' |
||||
}} |
||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
||||
state={from} |
||||
> |
||||
<CustomTooltip tooltipText="Receipts" tooltipId="etherscan-nav-receipts" placement="bottom"> |
||||
<i className="fas fa-receipt"></i> |
||||
</CustomTooltip> |
||||
</NavLink> |
||||
) |
||||
} |
||||
|
||||
const SettingsIcon = ({from}: IconProps) => { |
||||
return ( |
||||
<NavLink |
||||
data-id="settings" |
||||
to={{ |
||||
pathname: '/settings' |
||||
}} |
||||
className={({isActive}) => (isActive ? 'border border-secondary shadow-none btn p-1 m-0' : 'border-0 shadow-none btn p-1 m-0')} |
||||
style={({isActive}) => (!isActive ? {width: '1.8rem', filter: 'contrast(0.5)'} : {width: '1.8rem'})} |
||||
state={from} |
||||
> |
||||
<CustomTooltip tooltipText="Settings" tooltipId="etherscan-nav-settings" placement="bottom"> |
||||
<i className="fas fa-cog"></i> |
||||
</CustomTooltip> |
||||
</NavLink> |
||||
) |
||||
} |
||||
|
||||
export const HeaderWithSettings = ({title = '', from}) => { |
||||
return ( |
||||
<div className="d-flex justify-content-between"> |
||||
<h6 className="d-inline">{title}</h6> |
||||
<div className="nav"> |
||||
<HomeIcon from={from} /> |
||||
<ReceiptsIcon from={from} /> |
||||
<SettingsIcon from={from} /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,34 @@ |
||||
import React from 'react' |
||||
import {CustomTooltip} from '@remix-ui/helper' |
||||
|
||||
interface Props { |
||||
text: string |
||||
isSubmitting?: boolean |
||||
dataId?: string |
||||
disable?: boolean |
||||
} |
||||
|
||||
export const SubmitButton = ({text, dataId, isSubmitting = false, disable = true}) => { |
||||
return ( |
||||
<div> |
||||
<button data-id={dataId} type="submit" className="btn btn-primary btn-block p-1 text-decoration-none" disabled={disable}> |
||||
<CustomTooltip |
||||
tooltipText={disable ? 'Fill in the valid value(s) and select a supported network' : 'Click to proceed'} |
||||
tooltipId={'etherscan-submit-button-' + dataId} |
||||
tooltipTextClasses="border bg-light text-dark p-1 pr-3" |
||||
placement="bottom" |
||||
> |
||||
<div> |
||||
{!isSubmitting && text} |
||||
{isSubmitting && ( |
||||
<div> |
||||
<span className="spinner-border spinner-border-sm mr-1" role="status" aria-hidden="true" /> |
||||
Verifying... Please wait |
||||
</div> |
||||
)} |
||||
</div> |
||||
</CustomTooltip> |
||||
</button> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,2 @@ |
||||
export { HeaderWithSettings } from "./HeaderWithSettings" |
||||
export { SubmitButton } from "./SubmitButton" |
@ -0,0 +1,36 @@ |
||||
import {useState} from 'react' |
||||
|
||||
export function useLocalStorage(key: string, initialValue: any) { |
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = useState(() => { |
||||
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: any) => { |
||||
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,17 @@ |
||||
import React, {PropsWithChildren} from 'react' |
||||
|
||||
import {HeaderWithSettings} from '../components' |
||||
|
||||
interface Props { |
||||
from: string |
||||
title?: string |
||||
} |
||||
|
||||
export const DefaultLayout = ({children, from, title}) => { |
||||
return ( |
||||
<div> |
||||
<HeaderWithSettings from={from} title={title} /> |
||||
{children} |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1 @@ |
||||
export { DefaultLayout } from "./Default" |
@ -0,0 +1,37 @@ |
||||
import React from 'react' |
||||
import {HashRouter as Router, Route, Routes, RouteProps} from 'react-router-dom' |
||||
|
||||
import {ErrorView, HomeView, ReceiptsView, CaptureKeyView} from './views' |
||||
import {DefaultLayout} from './layouts' |
||||
|
||||
export const DisplayRoutes = () => ( |
||||
<Router> |
||||
<Routes> |
||||
<Route |
||||
path="/" |
||||
element={ |
||||
<DefaultLayout from="/" title="Verify Smart Contracts"> |
||||
<HomeView /> |
||||
</DefaultLayout> |
||||
} |
||||
/> |
||||
<Route path="/error" element={<ErrorView />} /> |
||||
<Route |
||||
path="/receipts" |
||||
element={ |
||||
<DefaultLayout from="/receipts" title="Check Receipt GUID Status"> |
||||
<ReceiptsView /> |
||||
</DefaultLayout> |
||||
} |
||||
/> |
||||
<Route |
||||
path="/settings" |
||||
element={ |
||||
<DefaultLayout from="/settings" title="Set Explorer API Key"> |
||||
<CaptureKeyView /> |
||||
</DefaultLayout> |
||||
} |
||||
/> |
||||
</Routes> |
||||
</Router> |
||||
) |
@ -0,0 +1,9 @@ |
||||
export type ReceiptStatus = "Pending in queue" | "Pass - Verified" | "Already Verified" | "Max rate limit reached" | "Successfully Updated" |
||||
|
||||
export interface Receipt { |
||||
guid: string |
||||
status: ReceiptStatus |
||||
isProxyContract: boolean |
||||
message?: string |
||||
succeed?: boolean |
||||
} |
@ -0,0 +1 @@ |
||||
export type ThemeType = "dark" | "light" |
@ -0,0 +1,2 @@ |
||||
export * from "./Receipt" |
||||
export * from "./ThemeType" |
@ -0,0 +1 @@ |
||||
export * from "./utilities" |
@ -0,0 +1,42 @@ |
||||
export const scanAPIurls = { |
||||
// all mainnet
|
||||
1: 'https://api.etherscan.io/api', |
||||
56: 'https://api.bscscan.com/api', |
||||
137: 'https://api.polygonscan.com/api', |
||||
250: 'https://api.ftmscan.com/api', |
||||
42161: 'https://api.arbiscan.io/api', |
||||
43114: 'https://api.snowtrace.io/api', |
||||
1285: 'https://api-moonriver.moonscan.io/api', |
||||
1284: 'https://api-moonbeam.moonscan.io/api', |
||||
25: 'https://api.cronoscan.com/api', |
||||
199: 'https://api.bttcscan.com/api', |
||||
10: 'https://api-optimistic.etherscan.io/api', |
||||
42220: 'https://api.celoscan.io/api', |
||||
288: 'https://api.bobascan.com/api', |
||||
100: 'https://api.gnosisscan.io/api', |
||||
1101: 'https://api-zkevm.polygonscan.com/api', |
||||
59144: 'https://api.lineascan.build/api', |
||||
8453: 'https://api.basescan.org/api', |
||||
534352: 'https://api.scrollscan.com/api', |
||||
|
||||
// all testnet
|
||||
17000: 'https://api-holesky.etherscan.io/api', |
||||
11155111: 'https://api-sepolia.etherscan.io/api', |
||||
97: 'https://api-testnet.bscscan.com/api', |
||||
80001: 'https://api-testnet.polygonscan.com/api', |
||||
4002: 'https://api-testnet.ftmscan.com/api', |
||||
421611: 'https://api-testnet.arbiscan.io/api', |
||||
42170: 'https://api-nova.arbiscan.io/api', |
||||
43113: 'https://api-testnet.snowtrace.io/api', |
||||
1287: 'https://api-moonbase.moonscan.io/api', |
||||
338: 'https://api-testnet.cronoscan.com/api', |
||||
1028: 'https://api-testnet.bttcscan.com/api', |
||||
420: 'https://api-goerli-optimistic.etherscan.io/api', |
||||
44787: 'https://api-alfajores.celoscan.io/api', |
||||
2888: 'https://api-testnet.bobascan.com/api', |
||||
84531: 'https://api-goerli.basescan.org/api', |
||||
84532: "https://api-sepolia.basescan.org/api", |
||||
1442: 'https://api-testnet-zkevm.polygonscan.com/api', |
||||
59140: 'https://api-testnet.lineascan.build/api', |
||||
534351: 'https://api-sepolia.scrollscan.com/api', |
||||
} |
@ -0,0 +1,30 @@ |
||||
export const verifyScript = ` |
||||
/** |
||||
* @param {string} apikey - etherscan api key |
||||
* @param {string} contractAddress - Address of the contract to verify |
||||
* @param {string} contractArguments - Parameters used in the contract constructor during the initial deployment. It should be the hex encoded value |
||||
* @param {string} contractName - Name of the contract |
||||
* @param {string} contractFile - File where the contract is located |
||||
* @param {number | string} chainRef - Network chain id or API URL (optional) |
||||
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
||||
* @param {string} expectedImplAddress - Implementation contract address, in case of proxy contract verification (optional) |
||||
* @returns {{ guid, status, message, succeed }} verification result |
||||
*/ |
||||
export const verify = async (apikey: string, contractAddress: string, contractArguments: string, contractName: string, contractFile: string, chainRef?: number | string, isProxyContract?: boolean, expectedImplAddress?: string) => { |
||||
const compilationResultParam = await remix.call('compilerArtefacts' as any, 'getCompilerAbstract', contractFile) |
||||
console.log('verifying.. ' + contractName) |
||||
// update apiKey and chainRef to verify contract on multiple networks
|
||||
return await remix.call('etherscan' as any, 'verify', apikey, contractAddress, contractArguments, contractName, compilationResultParam, chainRef, isProxyContract, expectedImplAddress) |
||||
}` |
||||
|
||||
export const receiptGuidScript = ` |
||||
/** |
||||
* @param {string} apikey - etherscan api key |
||||
* @param {string} guid - receipt id |
||||
* @param {boolean} isProxyContract - true, if contract is a proxy contract (optional) |
||||
* @returns {{ status, message, succeed }} receiptStatus |
||||
*/ |
||||
export const receiptStatus = async (apikey: string, guid: string, isProxyContract?: boolean) => { |
||||
return await remix.call('etherscan' as any, 'receiptStatus', guid, apikey, isProxyContract) |
||||
} |
||||
` |
@ -0,0 +1,69 @@ |
||||
import { PluginClient } from "@remixproject/plugin" |
||||
import axios from 'axios' |
||||
import { scanAPIurls } from "./networks" |
||||
type RemixClient = PluginClient |
||||
|
||||
/* |
||||
status: 0=Error, 1=Pass |
||||
message: OK, NOTOK |
||||
result: explanation |
||||
*/ |
||||
export type receiptStatus = { |
||||
result: string |
||||
message: string |
||||
status: string |
||||
} |
||||
|
||||
export const getEtherScanApi = (networkId: any) => { |
||||
if (!(networkId in scanAPIurls)) { |
||||
throw new Error("no known network to verify against") |
||||
} |
||||
const apiUrl = (scanAPIurls as any)[networkId] |
||||
return apiUrl |
||||
} |
||||
|
||||
export const getNetworkName = async (client: RemixClient) => { |
||||
const network = await client.call("network", "detectNetwork") |
||||
if (!network) { |
||||
throw new Error("no known network to verify against") |
||||
} |
||||
return { network: network.name!.toLowerCase(), networkId: network.id } |
||||
} |
||||
|
||||
export const getReceiptStatus = async ( |
||||
receiptGuid: string, |
||||
apiKey: string, |
||||
etherscanApi: string |
||||
): Promise<receiptStatus> => { |
||||
const params = `guid=${receiptGuid}&module=contract&action=checkverifystatus&apiKey=${apiKey}` |
||||
try { |
||||
const response = await axios.get(`${etherscanApi}?${params}`) |
||||
const { result, message, status } = response.data |
||||
return { |
||||
result, |
||||
message, |
||||
status, |
||||
} |
||||
} catch (error) { |
||||
console.error(error) |
||||
} |
||||
} |
||||
|
||||
export const getProxyContractReceiptStatus = async ( |
||||
receiptGuid: string, |
||||
apiKey: string, |
||||
etherscanApi: string |
||||
): Promise<receiptStatus> => { |
||||
const params = `guid=${receiptGuid}&module=contract&action=checkproxyverification&apiKey=${apiKey}` |
||||
try { |
||||
const response = await axios.get(`${etherscanApi}?${params}`) |
||||
const { result, message, status } = response.data |
||||
return { |
||||
result, |
||||
message, |
||||
status, |
||||
} |
||||
} catch (error) { |
||||
console.error(error) |
||||
} |
||||
} |
@ -0,0 +1,206 @@ |
||||
import { getNetworkName, getEtherScanApi, getReceiptStatus, getProxyContractReceiptStatus } from "." |
||||
import { CompilationResult } from "@remixproject/plugin-api" |
||||
import { CompilerAbstract } from '@remix-project/remix-solidity' |
||||
import axios from 'axios' |
||||
import { PluginClient } from "@remixproject/plugin" |
||||
|
||||
const resetAfter10Seconds = (client: PluginClient, setResults: (value: string) => void) => { |
||||
setTimeout(() => { |
||||
client.emit("statusChanged", { key: "none" }) |
||||
setResults("") |
||||
}, 10000) |
||||
} |
||||
|
||||
export type EtherScanReturn = { |
||||
guid: any, |
||||
status: any, |
||||
} |
||||
export const verify = async ( |
||||
apiKeyParam: string, |
||||
contractAddress: string, |
||||
contractArgumentsParam: string, |
||||
contractName: string, |
||||
compilationResultParam: CompilerAbstract, |
||||
chainRef: number | string, |
||||
isProxyContract: boolean, |
||||
expectedImplAddress: string, |
||||
client: PluginClient, |
||||
onVerifiedContract: (value: EtherScanReturn) => void, |
||||
setResults: (value: string) => void |
||||
) => { |
||||
let networkChainId |
||||
let etherscanApi |
||||
if (chainRef) { |
||||
if (typeof chainRef === 'number') { |
||||
networkChainId = chainRef |
||||
etherscanApi = getEtherScanApi(networkChainId) |
||||
} else if (typeof chainRef === 'string') etherscanApi = chainRef |
||||
} else { |
||||
const { network, networkId } = await getNetworkName(client) |
||||
if (network === "vm") { |
||||
return { |
||||
succeed: false, |
||||
message: "Cannot verify in the selected network" |
||||
} |
||||
} else { |
||||
networkChainId = networkId |
||||
etherscanApi = getEtherScanApi(networkChainId) |
||||
} |
||||
} |
||||
|
||||
try { |
||||
const contractMetadata = getContractMetadata( |
||||
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
|
||||
compilationResultParam.data as unknown as CompilationResult, |
||||
contractName |
||||
) |
||||
|
||||
if (!contractMetadata) { |
||||
return { |
||||
succeed: false, |
||||
message: "Please recompile contract" |
||||
} |
||||
} |
||||
|
||||
const contractMetadataParsed = JSON.parse(contractMetadata) |
||||
|
||||
const fileName = getContractFileName( |
||||
// cast from the remix-plugin interface to the solidity one. Should be fixed when remix-plugin move to the remix-project repository
|
||||
compilationResultParam.data as unknown as CompilationResult, |
||||
contractName |
||||
) |
||||
|
||||
const jsonInput = { |
||||
language: 'Solidity', |
||||
sources: compilationResultParam.source.sources, |
||||
settings: { |
||||
optimizer: { |
||||
enabled: contractMetadataParsed.settings.optimizer.enabled, |
||||
runs: contractMetadataParsed.settings.optimizer.runs |
||||
} |
||||
} |
||||
} |
||||
|
||||
const data: { [key: string]: string | any } = { |
||||
apikey: apiKeyParam, // A valid API-Key is required
|
||||
module: "contract", // Do not change
|
||||
action: "verifysourcecode", // Do not change
|
||||
codeformat: "solidity-standard-json-input", |
||||
sourceCode: JSON.stringify(jsonInput), |
||||
contractname: fileName + ':' + contractName, |
||||
compilerversion: `v${contractMetadataParsed.compiler.version}`, // see http://etherscan.io/solcversions for list of support versions
|
||||
constructorArguements: contractArgumentsParam ? contractArgumentsParam.replace('0x', '') : '', // if applicable
|
||||
} |
||||
|
||||
if (isProxyContract) { |
||||
data.action = "verifyproxycontract" |
||||
data.expectedimplementation = expectedImplAddress |
||||
data.address = contractAddress |
||||
} else { |
||||
data.contractaddress = contractAddress |
||||
} |
||||
|
||||
const body = new FormData() |
||||
Object.keys(data).forEach((key) => body.append(key, data[key])) |
||||
|
||||
client.emit("statusChanged", { |
||||
key: "loading", |
||||
type: "info", |
||||
title: "Verifying ...", |
||||
}) |
||||
const response = await axios.post(etherscanApi, body) |
||||
const { message, result, status } = await response.data |
||||
|
||||
if (message === "OK" && status === "1") { |
||||
resetAfter10Seconds(client, setResults) |
||||
let receiptStatus |
||||
if (isProxyContract) { |
||||
receiptStatus = await getProxyContractReceiptStatus( |
||||
result, |
||||
apiKeyParam, |
||||
etherscanApi |
||||
) |
||||
if (receiptStatus.status === '1') { |
||||
receiptStatus.message = receiptStatus.result |
||||
receiptStatus.result = 'Successfully Updated' |
||||
} |
||||
} else receiptStatus = await getReceiptStatus( |
||||
result, |
||||
apiKeyParam, |
||||
etherscanApi |
||||
) |
||||
|
||||
const returnValue = { |
||||
guid: result, |
||||
status: receiptStatus.result, |
||||
message: `Verification request submitted successfully. Use this receipt GUID ${result} to track the status of your submission`, |
||||
succeed: true, |
||||
isProxyContract |
||||
} |
||||
onVerifiedContract(returnValue) |
||||
return returnValue |
||||
} else if (message === "NOTOK") { |
||||
client.emit("statusChanged", { |
||||
key: "failed", |
||||
type: "error", |
||||
title: result, |
||||
}) |
||||
const returnValue = { |
||||
message: result, |
||||
succeed: false, |
||||
isProxyContract |
||||
} |
||||
resetAfter10Seconds(client, setResults) |
||||
return returnValue |
||||
} |
||||
return { |
||||
message: 'unknown reason ' + result, |
||||
succeed: false |
||||
} |
||||
} catch (error: any) { |
||||
console.error(error) |
||||
setResults("Something wrong happened, try again") |
||||
return { |
||||
message: error.message, |
||||
succeed: false |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const getContractFileName = ( |
||||
compilationResult: CompilationResult, |
||||
contractName: string |
||||
) => { |
||||
const compiledContracts = compilationResult.contracts |
||||
let fileName = "" |
||||
|
||||
for (const file of Object.keys(compiledContracts)) { |
||||
for (const contract of Object.keys(compiledContracts[file])) { |
||||
if (contract === contractName) { |
||||
fileName = file |
||||
break |
||||
} |
||||
} |
||||
} |
||||
return fileName |
||||
} |
||||
|
||||
export const getContractMetadata = ( |
||||
compilationResult: CompilationResult, |
||||
contractName: string |
||||
) => { |
||||
const compiledContracts = compilationResult.contracts |
||||
let contractMetadata = "" |
||||
|
||||
for (const file of Object.keys(compiledContracts)) { |
||||
for (const contract of Object.keys(compiledContracts[file])) { |
||||
if (contract === contractName) { |
||||
contractMetadata = compiledContracts[file][contract].metadata |
||||
if (contractMetadata) { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return contractMetadata |
||||
} |
@ -0,0 +1,63 @@ |
||||
import React, {useState, useEffect} from 'react' |
||||
|
||||
import {Formik, ErrorMessage, Field} from 'formik' |
||||
import {useNavigate, useLocation} from 'react-router-dom' |
||||
|
||||
import {AppContext} from '../AppContext' |
||||
import {SubmitButton} from '../components' |
||||
|
||||
export const CaptureKeyView = () => { |
||||
const location = useLocation() |
||||
const navigate = useNavigate() |
||||
const [msg, setMsg] = useState('') |
||||
const context = React.useContext(AppContext) |
||||
|
||||
useEffect(() => { |
||||
if (!context.apiKey) setMsg('Please provide a 34-character API key to continue') |
||||
}, [context.apiKey]) |
||||
|
||||
return ( |
||||
<div> |
||||
<Formik |
||||
initialValues={{apiKey: context.apiKey}} |
||||
validate={(values) => { |
||||
const errors = {} as any |
||||
if (!values.apiKey) { |
||||
errors.apiKey = 'Required' |
||||
} else if (values.apiKey.length !== 34) { |
||||
errors.apiKey = 'API key should be 34 characters long' |
||||
} |
||||
return errors |
||||
}} |
||||
onSubmit={(values) => { |
||||
const apiKey = values.apiKey |
||||
if (apiKey.length === 34) { |
||||
context.setAPIKey(values.apiKey) |
||||
navigate(location && location.state ? location.state : '/') |
||||
} |
||||
}} |
||||
> |
||||
{({errors, touched, handleSubmit}) => ( |
||||
<form onSubmit={handleSubmit}> |
||||
<div className="form-group mb-2"> |
||||
<label htmlFor="apikey">API Key</label> |
||||
<Field |
||||
className={errors.apiKey && touched.apiKey ? 'form-control form-control-sm is-invalid' : 'form-control form-control-sm'} |
||||
type="password" |
||||
name="apiKey" |
||||
placeholder="e.g. GM1T20XY6JGSAPWKDCYZ7B2FJXKTJRFVGZ" |
||||
/> |
||||
<ErrorMessage className="invalid-feedback" name="apiKey" component="div" /> |
||||
</div> |
||||
|
||||
<div> |
||||
<SubmitButton text="Save" dataId="save-api-key" disable={errors && errors.apiKey ? true : false} /> |
||||
</div> |
||||
</form> |
||||
)} |
||||
</Formik> |
||||
|
||||
<div data-id="api-key-result" className="text-primary mt-4 text-center" style={{fontSize: '0.8em'}} dangerouslySetInnerHTML={{__html: msg}} /> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,16 @@ |
||||
import React from 'react' |
||||
|
||||
export const ErrorView = () => { |
||||
return ( |
||||
<div className="d-flex w-100 flex-column align-items-center"> |
||||
<img className="pb-4" width="250" src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png" alt="Error page" /> |
||||
<h5>Sorry, something unexpected happened.</h5> |
||||
<h5> |
||||
Please raise an issue:{' '} |
||||
<a className="text-danger" href="https://github.com/ethereum/remix-project/issues"> |
||||
Here |
||||
</a> |
||||
</h5> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,31 @@ |
||||
import React from 'react' |
||||
|
||||
import {Navigate} from 'react-router-dom' |
||||
|
||||
import {AppContext} from '../AppContext' |
||||
import {Receipt} from '../types' |
||||
|
||||
import {VerifyView} from './VerifyView' |
||||
|
||||
export const HomeView = () => { |
||||
const context = React.useContext(AppContext) |
||||
|
||||
return !context.apiKey ? ( |
||||
<Navigate |
||||
to={{ |
||||
pathname: '/settings' |
||||
}} |
||||
/> |
||||
) : ( |
||||
<VerifyView |
||||
contracts={context.contracts} |
||||
client={context.clientInstance} |
||||
apiKey={context.apiKey} |
||||
onVerifiedContract={(receipt: Receipt) => { |
||||
const newReceipts = [...context.receipts, receipt] |
||||
context.setReceipts(newReceipts) |
||||
}} |
||||
networkName={context.networkName} |
||||
/> |
||||
) |
||||
} |
@ -0,0 +1,170 @@ |
||||
import React, {useState} from 'react' |
||||
|
||||
import {Formik, ErrorMessage, Field} from 'formik' |
||||
import {getEtherScanApi, getNetworkName, getReceiptStatus, getProxyContractReceiptStatus} from '../utils' |
||||
import {Receipt} from '../types' |
||||
import {AppContext} from '../AppContext' |
||||
import {SubmitButton} from '../components' |
||||
import {Navigate} from 'react-router-dom' |
||||
import {Button} from 'react-bootstrap' |
||||
import {CustomTooltip} from '@remix-ui/helper' |
||||
|
||||
interface FormValues { |
||||
receiptGuid: string |
||||
} |
||||
|
||||
export const ReceiptsView = () => { |
||||
const [results, setResults] = useState({succeed: false, message: ''}) |
||||
const [isProxyContractReceipt, setIsProxyContractReceipt] = useState(false) |
||||
const context = React.useContext(AppContext) |
||||
|
||||
const onGetReceiptStatus = async (values: FormValues, clientInstance: any, apiKey: string) => { |
||||
try { |
||||
const {network, networkId} = await getNetworkName(clientInstance) |
||||
if (network === 'vm') { |
||||
setResults({ |
||||
succeed: false, |
||||
message: 'Cannot verify in the selected network' |
||||
}) |
||||
return |
||||
} |
||||
const etherscanApi = getEtherScanApi(networkId) |
||||
let result |
||||
if (isProxyContractReceipt) { |
||||
result = await getProxyContractReceiptStatus(values.receiptGuid, apiKey, etherscanApi) |
||||
if (result.status === '1') { |
||||
result.message = result.result |
||||
result.result = 'Successfully Updated' |
||||
} |
||||
} else result = await getReceiptStatus(values.receiptGuid, apiKey, etherscanApi) |
||||
setResults({ |
||||
succeed: result.status === '1' ? true : false, |
||||
message: result.result || (result.status === '0' ? 'Verification failed' : result.message) |
||||
}) |
||||
} catch (error: any) { |
||||
setResults({ |
||||
succeed: false, |
||||
message: error.message |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return !context.apiKey ? ( |
||||
<Navigate |
||||
to={{ |
||||
pathname: '/settings' |
||||
}} |
||||
/> |
||||
) : ( |
||||
<div> |
||||
<Formik |
||||
initialValues={{receiptGuid: ''}} |
||||
validate={(values) => { |
||||
const errors = {} as any |
||||
if (!values.receiptGuid) { |
||||
errors.receiptGuid = 'Required' |
||||
} |
||||
return errors |
||||
}} |
||||
onSubmit={(values) => onGetReceiptStatus(values, context.clientInstance, context.apiKey)} |
||||
> |
||||
{({errors, touched, handleSubmit, handleChange}) => ( |
||||
<form onSubmit={handleSubmit}> |
||||
<div className="form-group mb-2"> |
||||
<label htmlFor="receiptGuid">Receipt GUID</label> |
||||
<Field |
||||
className={errors.receiptGuid && touched.receiptGuid ? 'form-control form-control-sm is-invalid' : 'form-control form-control-sm'} |
||||
type="text" |
||||
name="receiptGuid" |
||||
/> |
||||
<ErrorMessage className="invalid-feedback" name="receiptGuid" component="div" /> |
||||
</div> |
||||
|
||||
<div className="d-flex mb-2 custom-control custom-checkbox"> |
||||
<Field |
||||
className="custom-control-input" |
||||
type="checkbox" |
||||
name="isProxyReceipt" |
||||
id="isProxyReceipt" |
||||
onChange={async (e) => { |
||||
handleChange(e) |
||||
if (e.target.checked) setIsProxyContractReceipt(true) |
||||
else setIsProxyContractReceipt(false) |
||||
}} |
||||
/> |
||||
<label className="form-check-label custom-control-label" htmlFor="isProxyReceipt"> |
||||
It's a proxy contract GUID |
||||
</label> |
||||
</div> |
||||
<SubmitButton dataId={null} text="Check" disable={!touched.receiptGuid || (touched.receiptGuid && errors.receiptGuid) ? true : false} /> |
||||
</form> |
||||
)} |
||||
</Formik> |
||||
|
||||
<div |
||||
className={results['succeed'] ? 'text-success mt-3 text-center' : 'text-danger mt-3 text-center'} |
||||
dangerouslySetInnerHTML={{ |
||||
__html: results.message ? results.message : '' |
||||
}} |
||||
/> |
||||
|
||||
<ReceiptsTable receipts={context.receipts} /> |
||||
<br /> |
||||
<CustomTooltip tooltipText="Clear the list of receipts" tooltipId="etherscan-clear-receipts" placement="bottom"> |
||||
<Button |
||||
className="btn-sm" |
||||
onClick={() => { |
||||
context.setReceipts([]) |
||||
}} |
||||
> |
||||
Clear |
||||
</Button> |
||||
</CustomTooltip> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
const ReceiptsTable = ({receipts}) => { |
||||
return ( |
||||
<div className="table-responsive"> |
||||
<h6>Receipts</h6> |
||||
<table className="table h6 table-sm"> |
||||
<thead> |
||||
<tr> |
||||
<th scope="col">Status</th> |
||||
<th scope="col">GUID</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{receipts && |
||||
receipts.length > 0 && |
||||
receipts.map((item: Receipt, index) => { |
||||
return ( |
||||
<tr key={item.guid}> |
||||
<td |
||||
className={ |
||||
item.status === 'Pass - Verified' || item.status === 'Successfully Updated' |
||||
? 'text-success' |
||||
: item.status === 'Pending in queue' |
||||
? 'text-warning' |
||||
: item.status === 'Already Verified' |
||||
? 'text-info' |
||||
: 'text-secondary' |
||||
} |
||||
> |
||||
{item.status} |
||||
{item.status === 'Successfully Updated' && ( |
||||
<CustomTooltip placement={'bottom'} tooltipClasses="text-wrap" tooltipId="etherscan-receipt-proxy-status" tooltipText={item.message}> |
||||
<i style={{fontSize: 'small'}} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i> |
||||
</CustomTooltip> |
||||
)} |
||||
</td> |
||||
<td>{item.guid}</td> |
||||
</tr> |
||||
) |
||||
})} |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,235 @@ |
||||
import React, {useEffect, useRef, useState} from 'react' |
||||
import Web3 from 'web3' |
||||
|
||||
import {PluginClient} from '@remixproject/plugin' |
||||
import {CustomTooltip} from '@remix-ui/helper' |
||||
import {Formik, ErrorMessage, Field} from 'formik' |
||||
|
||||
import {SubmitButton} from '../components' |
||||
import {Receipt} from '../types' |
||||
import {verify} from '../utils/verify' |
||||
import {etherscanScripts} from '@remix-project/remix-ws-templates' |
||||
|
||||
interface Props { |
||||
client: PluginClient |
||||
apiKey: string |
||||
onVerifiedContract: (receipt: Receipt) => void |
||||
contracts: string[], |
||||
networkName: string |
||||
} |
||||
|
||||
interface FormValues { |
||||
contractName: string |
||||
contractAddress: string |
||||
expectedImplAddress?: string |
||||
} |
||||
|
||||
export const VerifyView = ({apiKey, client, contracts, onVerifiedContract, networkName}) => { |
||||
const [results, setResults] = useState('') |
||||
const [selectedContract, setSelectedContract] = useState('') |
||||
const [showConstructorArgs, setShowConstructorArgs] = useState(false) |
||||
const [isProxyContract, setIsProxyContract] = useState(false) |
||||
const [constructorInputs, setConstructorInputs] = useState([]) |
||||
const verificationResult = useRef({}) |
||||
|
||||
useEffect(() => { |
||||
if (contracts.includes(selectedContract)) updateConsFields(selectedContract) |
||||
}, [contracts]) |
||||
|
||||
const updateConsFields = (contractName) => { |
||||
client.call('compilerArtefacts' as any, 'getArtefactsByContractName', contractName).then((result) => { |
||||
const {artefact} = result |
||||
if (artefact && artefact.abi && artefact.abi[0] && artefact.abi[0].type && artefact.abi[0].type === 'constructor' && artefact.abi[0].inputs.length > 0) { |
||||
setConstructorInputs(artefact.abi[0].inputs) |
||||
setShowConstructorArgs(true) |
||||
} else { |
||||
setConstructorInputs([]) |
||||
setShowConstructorArgs(false) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
const onVerifyContract = async (values: FormValues) => { |
||||
const compilationResult = (await client.call('solidity', 'getCompilationResult')) as any |
||||
|
||||
if (!compilationResult) { |
||||
throw new Error('no compilation result available') |
||||
} |
||||
|
||||
const constructorValues = [] |
||||
for (const key in values) { |
||||
if (key.startsWith('contractArgValue')) constructorValues.push(values[key]) |
||||
} |
||||
const web3 = new Web3() |
||||
const constructorTypes = constructorInputs.map((e) => e.type) |
||||
let contractArguments = web3.eth.abi.encodeParameters(constructorTypes, constructorValues) |
||||
contractArguments = contractArguments.replace('0x', '') |
||||
|
||||
verificationResult.current = await verify( |
||||
apiKey, |
||||
values.contractAddress, |
||||
contractArguments, |
||||
values.contractName, |
||||
compilationResult, |
||||
null, |
||||
isProxyContract, |
||||
values.expectedImplAddress, |
||||
client, |
||||
onVerifiedContract, |
||||
setResults |
||||
) |
||||
setResults(verificationResult.current['message']) |
||||
} |
||||
|
||||
return ( |
||||
<div> |
||||
<Formik |
||||
initialValues={{ |
||||
contractName: '', |
||||
contractAddress: '' |
||||
}} |
||||
validate={(values) => { |
||||
const errors = {} as any |
||||
if (!values.contractName) { |
||||
errors.contractName = 'Required' |
||||
} |
||||
if (!values.contractAddress) { |
||||
errors.contractAddress = 'Required' |
||||
} |
||||
if (values.contractAddress.trim() === '' || !values.contractAddress.startsWith('0x') || values.contractAddress.length !== 42) { |
||||
errors.contractAddress = 'Please enter a valid contract address' |
||||
} |
||||
return errors |
||||
}} |
||||
onSubmit={(values) => onVerifyContract(values)} |
||||
> |
||||
{({errors, touched, handleSubmit, handleChange, isSubmitting}) => { |
||||
return ( |
||||
<form onSubmit={handleSubmit}> |
||||
<div className="form-group"> |
||||
<label htmlFor="network">Selected Network</label> |
||||
<CustomTooltip |
||||
tooltipText="Network is fetched from 'Deploy and Run Transactions' plugin's ENVIRONMENT field" |
||||
tooltipId="etherscan-impl-address2" |
||||
placement="bottom" |
||||
> |
||||
<Field className="form-control" type="text" name="network" value={networkName} disabled={true} /> |
||||
</CustomTooltip> |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="contractName">Contract Name</label> |
||||
<Field |
||||
as="select" |
||||
className={errors.contractName && touched.contractName && contracts.length ? 'form-control is-invalid' : 'form-control'} |
||||
name="contractName" |
||||
onChange={async (e) => { |
||||
handleChange(e) |
||||
setSelectedContract(e.target.value) |
||||
updateConsFields(e.target.value) |
||||
}} |
||||
> |
||||
<option disabled={true} value=""> |
||||
{contracts.length ? 'Select a contract' : `--- No compiled contracts ---`} |
||||
</option> |
||||
{contracts.map((item) => ( |
||||
<option key={item} value={item}> |
||||
{item} |
||||
</option> |
||||
))} |
||||
</Field> |
||||
<ErrorMessage className="invalid-feedback" name="contractName" component="div" /> |
||||
</div> |
||||
<div className={showConstructorArgs ? 'form-group d-block' : 'form-group d-none'}> |
||||
<label>Constructor Arguments</label> |
||||
{constructorInputs.map((item, index) => { |
||||
return ( |
||||
<div className="d-flex"> |
||||
<Field className="form-control m-1" type="text" key={`contractArgName${index}`} name={`contractArgName${index}`} value={item.name} disabled={true} /> |
||||
<CustomTooltip tooltipText={`value of ${item.name}`} tooltipId={`etherscan-constructor-value${index}`} placement="top"> |
||||
<Field className="form-control m-1" type="text" key={`contractArgValue${index}`} name={`contractArgValue${index}`} placeholder={item.type} /> |
||||
</CustomTooltip> |
||||
</div> |
||||
) |
||||
})} |
||||
</div> |
||||
<div className="form-group"> |
||||
<label htmlFor="contractAddress">Contract Address</label> |
||||
<Field |
||||
className={errors.contractAddress && touched.contractAddress ? 'form-control is-invalid' : 'form-control'} |
||||
type="text" |
||||
name="contractAddress" |
||||
placeholder="e.g. 0x11b79afc03baf25c631dd70169bb6a3160b2706e" |
||||
/> |
||||
<ErrorMessage className="invalid-feedback" name="contractAddress" component="div" /> |
||||
<div className="d-flex mb-2 custom-control custom-checkbox"> |
||||
<Field |
||||
className="custom-control-input" |
||||
type="checkbox" |
||||
name="isProxy" |
||||
id="isProxy" |
||||
onChange={async (e) => { |
||||
handleChange(e) |
||||
if (e.target.checked) setIsProxyContract(true) |
||||
else setIsProxyContract(false) |
||||
}} |
||||
/> |
||||
<label className="form-check-label custom-control-label" htmlFor="isProxy"> |
||||
It's a proxy contract address |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div className={isProxyContract ? 'form-group d-block' : 'form-group d-none'}> |
||||
<label htmlFor="expectedImplAddress">Expected Implementation Address</label> |
||||
<CustomTooltip |
||||
tooltipText="Providing expected implementation address enforces a check to ensure the returned implementation contract address is same as address picked up by the verifier" |
||||
tooltipId="etherscan-impl-address" |
||||
placement="bottom" |
||||
> |
||||
<Field className="form-control" type="text" name="expectedImplAddress" placeholder="verified implementation contract address" /> |
||||
</CustomTooltip> |
||||
<i style={{fontSize: 'x-small'}} className={'ml-1 fal fa-info-circle align-self-center'} aria-hidden="true"></i> |
||||
<label> Make sure contract is already verified on Etherscan</label> |
||||
</div> |
||||
<SubmitButton |
||||
dataId="verify-contract" |
||||
text="Verify" |
||||
isSubmitting={isSubmitting} |
||||
disable={ |
||||
!contracts.length || |
||||
!touched.contractName || |
||||
!touched.contractAddress || |
||||
(touched.contractName && errors.contractName) || |
||||
(touched.contractAddress && errors.contractAddress) || |
||||
networkName === 'VM (Not supported)' |
||||
? true |
||||
: false |
||||
} |
||||
/> |
||||
<br /> |
||||
<CustomTooltip tooltipText="Generate the required TS scripts to verify a contract on Etherscan" tooltipId="etherscan-generate-scripts" placement="bottom"> |
||||
<button |
||||
type="button" |
||||
className="mr-2 mb-2 py-1 px-2 btn btn-secondary btn-block" |
||||
onClick={async () => { |
||||
etherscanScripts(client) |
||||
}} |
||||
> |
||||
Generate Verification Scripts |
||||
</button> |
||||
</CustomTooltip> |
||||
</form> |
||||
) |
||||
}} |
||||
</Formik> |
||||
<div |
||||
data-id="verify-result" |
||||
className={verificationResult.current['succeed'] ? 'text-success mt-4 text-center' : 'text-danger mt-4 text-center'} |
||||
style={{fontSize: '0.8em'}} |
||||
dangerouslySetInnerHTML={{__html: results}} |
||||
/> |
||||
{/* <div style={{ display: "block", textAlign: "center", marginTop: "1em" }}> |
||||
<Link to="/receipts">View Receipts</Link> |
||||
</div> */} |
||||
</div> |
||||
) |
||||
} |
@ -0,0 +1,4 @@ |
||||
export { HomeView } from "./HomeView" |
||||
export { ErrorView } from "./ErrorView" |
||||
export { ReceiptsView } from "./ReceiptsView" |
||||
export { CaptureKeyView } from "./CaptureKeyView" |
@ -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,17 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<title>Etherscan</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,14 @@ |
||||
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,16 @@ |
||||
{ |
||||
"extends": "../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"jsx": "react-jsx", |
||||
"allowJs": true, |
||||
"esModuleInterop": true, |
||||
"allowSyntheticDefaultImports": 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-etherscan.${versionData.timestamp}.js` |
||||
config.output.chunkFilename = `[name].plugin-etherscan.${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
Loading…
Reference in new issue