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