Skip to content

Commit 147c77c

Browse files
authored
Merge pull request #6394 from remix-project-org/add-contract-verification-checkbox
Add deploy and verify contract
2 parents 4737665 + 5c1a764 commit 147c77c

File tree

9 files changed

+364
-45
lines changed

9 files changed

+364
-45
lines changed

apps/contract-verification/src/app/ContractVerificationPluginClient.ts

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { PluginClient } from '@remixproject/plugin'
22
import { createClient } from '@remixproject/plugin-webview'
3+
34
import EventManager from 'events'
4-
import { VERIFIERS, type ChainSettings, type ContractVerificationSettings, type LookupResponse, type VerifierIdentifier } from './types'
5+
import { VERIFIERS, type ChainSettings,Chain, type ContractVerificationSettings, type LookupResponse, type VerifierIdentifier, SubmittedContract, SubmittedContracts, VerificationReceipt } from './types'
56
import { mergeChainSettingsWithDefaults, validConfiguration } from './utils'
67
import { getVerifier } from './Verifiers'
8+
import { CompilerAbstract } from '@remix-project/remix-solidity'
79

810
export class ContractVerificationPluginClient extends PluginClient {
911
public internalEvents: EventManager
1012

1113
constructor() {
1214
super()
13-
this.methods = ['lookupAndSave']
15+
this.methods = ['lookupAndSave', 'verifyOnDeploy']
1416
this.internalEvents = new EventManager()
1517
createClient(this)
1618
this.onload()
@@ -62,8 +64,179 @@ export class ContractVerificationPluginClient extends PluginClient {
6264
}
6365
}
6466

67+
verifyOnDeploy = async (data: any): Promise<void> => {
68+
try {
69+
await this.call('terminal', 'log', { type: 'log', value: 'Verification process started...' })
70+
71+
const { chainId, currentChain, contractAddress, contractName, compilationResult, constructorArgs, etherscanApiKey } = data
72+
73+
if (!currentChain) {
74+
await this.call('terminal', 'log', { type: 'error', value: 'Chain data was not provided for verification.' })
75+
return
76+
}
77+
78+
const userSettings = this.getUserSettingsFromLocalStorage()
79+
80+
if (etherscanApiKey) {
81+
if (!userSettings.chains[chainId]) {
82+
userSettings.chains[chainId] = { verifiers: {} }
83+
}
84+
85+
if (!userSettings.chains[chainId].verifiers.Etherscan) {
86+
userSettings.chains[chainId].verifiers.Etherscan = {}
87+
}
88+
userSettings.chains[chainId].verifiers.Etherscan.apiKey = etherscanApiKey
89+
90+
if (!userSettings.chains[chainId].verifiers.Routescan) {
91+
userSettings.chains[chainId].verifiers.Routescan = {}
92+
}
93+
if (!userSettings.chains[chainId].verifiers.Routescan.apiKey){
94+
userSettings.chains[chainId].verifiers.Routescan.apiKey = "placeholder"
95+
}
96+
97+
window.localStorage.setItem("contract-verification:settings", JSON.stringify(userSettings))
98+
99+
}
100+
101+
const submittedContracts: SubmittedContracts = JSON.parse(window.localStorage.getItem('contract-verification:submitted-contracts') || '{}')
102+
103+
const filePath = Object.keys(compilationResult.data.contracts).find(path =>
104+
compilationResult.data.contracts[path][contractName]
105+
)
106+
if (!filePath) throw new Error(`Could not find file path for contract ${contractName}`)
107+
108+
const submittedContract: SubmittedContract = {
109+
id: `${chainId}-${contractAddress}`,
110+
address: contractAddress,
111+
chainId: chainId,
112+
filePath: filePath,
113+
contractName: contractName,
114+
abiEncodedConstructorArgs: constructorArgs,
115+
date: new Date().toISOString(),
116+
receipts: []
117+
}
118+
119+
const compilerAbstract: CompilerAbstract = compilationResult
120+
const chainSettings = mergeChainSettingsWithDefaults(chainId, userSettings)
121+
122+
const verificationPromises = []
123+
124+
if (validConfiguration(chainSettings, 'Sourcify')) {
125+
verificationPromises.push(this._verifyWithProvider('Sourcify', submittedContract, compilerAbstract, chainId, chainSettings))
126+
}
127+
128+
if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.toLowerCase().includes('routescan'))) {
129+
verificationPromises.push(this._verifyWithProvider('Routescan', submittedContract, compilerAbstract, chainId, chainSettings))
130+
}
131+
132+
if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.url.includes('blockscout'))) {
133+
verificationPromises.push(this._verifyWithProvider('Blockscout', submittedContract, compilerAbstract, chainId, chainSettings))
134+
}
135+
136+
if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.includes('etherscan'))) {
137+
if (etherscanApiKey) {
138+
verificationPromises.push(this._verifyWithProvider('Etherscan', submittedContract, compilerAbstract, chainId, chainSettings))
139+
} else {
140+
await this.call('terminal', 'log', { type: 'warn', value: 'Etherscan verification skipped: API key not found in global Settings.' })
141+
}
142+
}
143+
144+
await Promise.all(verificationPromises)
145+
146+
submittedContracts[submittedContract.id] = submittedContract
147+
window.localStorage.setItem('contract-verification:submitted-contracts', JSON.stringify(submittedContracts))
148+
this.internalEvents.emit('submissionUpdated')
149+
150+
} catch (error) {
151+
await this.call('terminal', 'log', { type: 'error', value: `An unexpected error occurred during verification: ${error.message}` })
152+
}
153+
}
154+
155+
private _verifyWithProvider = async (
156+
providerName: VerifierIdentifier,
157+
submittedContract: SubmittedContract,
158+
compilerAbstract: CompilerAbstract,
159+
chainId: string,
160+
chainSettings: ChainSettings
161+
): Promise<void> => {
162+
let receipt: VerificationReceipt
163+
const verifierSettings = chainSettings.verifiers[providerName]
164+
const verifier = getVerifier(providerName, verifierSettings)
165+
166+
try {
167+
if (validConfiguration(chainSettings, providerName)) {
168+
169+
await this.call('terminal', 'log', { type: 'log', value: `Verifying with ${providerName}...` })
170+
171+
if (providerName === 'Etherscan' || providerName === 'Routescan' || providerName === 'Blockscout') {
172+
await new Promise(resolve => setTimeout(resolve, 10000))
173+
}
174+
175+
if (verifier && typeof verifier.verify === 'function') {
176+
const result = await verifier.verify(submittedContract, compilerAbstract)
177+
178+
receipt = {
179+
receiptId: result.receiptId || undefined,
180+
verifierInfo: { name: providerName, apiUrl: verifier.apiUrl },
181+
status: result.status,
182+
message: result.message,
183+
lookupUrl: result.lookupUrl,
184+
contractId: submittedContract.id,
185+
isProxyReceipt: false,
186+
failedChecks: 0
187+
}
188+
189+
const successMessage = `${providerName} verification successful.`
190+
await this.call('terminal', 'log', { type: 'info', value: successMessage })
191+
192+
if (result.lookupUrl) {
193+
const textMessage = `${result.lookupUrl}`
194+
await this.call('terminal', 'log', { type: 'info', value: textMessage })
195+
}
196+
} else {
197+
throw new Error(`${providerName} verifier is not properly configured or does not support direct verification.`)
198+
}
199+
}
200+
} catch (e) {
201+
if (e.message.includes('Unable to locate ContractCode')) {
202+
const checkUrl = `${verifier.explorerUrl}/address/${submittedContract.address}`;
203+
const friendlyMessage = `Initial verification failed, possibly due to a sync delay. Please check the status manually.`
204+
205+
await this.call('terminal', 'log', { type: 'warn', value: `${providerName}: ${friendlyMessage}` })
206+
207+
const textMessage = `Check Manually: ${checkUrl}`
208+
await this.call('terminal', 'log', { type: 'info', value: textMessage })
209+
210+
receipt = {
211+
verifierInfo: { name: providerName, apiUrl: verifier?.apiUrl || 'N/A' },
212+
status: 'failed',
213+
message: 'Failed initially (sync delay), check manually.',
214+
contractId: submittedContract.id,
215+
isProxyReceipt: false,
216+
failedChecks: 0
217+
}
218+
219+
} else {
220+
receipt = {
221+
verifierInfo: { name: providerName, apiUrl: verifier?.apiUrl || 'N/A' },
222+
status: 'failed',
223+
message: e.message,
224+
contractId: submittedContract.id,
225+
isProxyReceipt: false,
226+
failedChecks: 0
227+
}
228+
await this.call('terminal', 'log', { type: 'error', value: `${providerName} verification failed: ${e.message}` })
229+
}
230+
231+
} finally {
232+
if (receipt) {
233+
submittedContract.receipts.push(receipt)
234+
}
235+
}
236+
}
237+
65238
private getUserSettingsFromLocalStorage(): ContractVerificationSettings {
66-
const fallbackSettings = { chains: {} };
239+
const fallbackSettings = { chains: {} }
67240
try {
68241
const settings = window.localStorage.getItem("contract-verification:settings")
69242
return settings ? JSON.parse(settings) : fallbackSettings

apps/contract-verification/src/app/app.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,18 @@ const App = () => {
6868
.then((data) => setChains(data))
6969
.catch((error) => console.error('Failed to fetch chains.json:', error))
7070

71+
const submissionUpdatedListener = () => {
72+
const latestSubmissions = window.localStorage.getItem('contract-verification:submitted-contracts')
73+
if (latestSubmissions) {
74+
setSubmittedContracts(JSON.parse(latestSubmissions))
75+
}
76+
}
77+
plugin.internalEvents.on('submissionUpdated', submissionUpdatedListener)
78+
7179
// Clean up on unmount
7280
return () => {
7381
plugin.off('compilerArtefacts' as any, 'compilationSaved')
82+
plugin.internalEvents.removeListener('submissionUpdated', submissionUpdatedListener)
7483
}
7584
}, [])
7685

@@ -167,4 +176,4 @@ const App = () => {
167176
)
168177
}
169178

170-
export default App
179+
export default App
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict'
2+
import { NightwatchBrowser } from 'nightwatch'
3+
import init from '../helpers/init'
4+
5+
declare global {
6+
interface Window { testplugin: { name: string, url: string }; }
7+
}
8+
9+
module.exports = {
10+
'@disabled': true,
11+
before: function (browser: NightwatchBrowser, done: VoidFunction) {
12+
init(browser, done, null)
13+
},
14+
15+
'Should NOT display the "Verify Contract" checkbox on an unsupported network (Remix VM) #group1': function (browser: NightwatchBrowser) {
16+
browser
17+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
18+
.clickLaunchIcon('filePanel')
19+
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
20+
.openFile('contracts/1_Storage.sol')
21+
.clickLaunchIcon('udapp')
22+
.waitForElementVisible('*[data-id="Deploy - transact (not payable)"]')
23+
.waitForElementNotPresent({
24+
selector: '#deployAndRunVerifyContract',
25+
timeout: 5000
26+
})
27+
.end()
28+
}
29+
}

libs/remix-ui/remix-ai-assistant/src/components/chat.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const AiChatIntro: React.FC<AiChatIntroProps> = ({ sendPrompt }) => {
4747
{/* Dynamic Conversation Starters */}
4848
<div className="d-flex flex-column mt-3" style={{ maxWidth: '400px' }}>
4949
{conversationStarters.map((starter, index) => (
50-
5150
<button
5251
key={`${starter.level}-${index}`}
5352
data-id={`remix-ai-assistant-starter-${starter.level}-${index}`}

libs/remix-ui/run-tab/src/lib/actions/deploy.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ export const createInstance = async (
161161
mainnetPrompt: MainnetPrompt,
162162
isOverSizePrompt: (values: OverSizeLimit) => JSX.Element,
163163
args,
164-
deployMode: DeployMode[]) => {
164+
deployMode: DeployMode[],
165+
isVerifyChecked: boolean) => {
165166
const isProxyDeployment = (deployMode || []).find(mode => mode === 'Deploy with Proxy')
166167
const isContractUpgrade = (deployMode || []).find(mode => mode === 'Upgrade with Proxy')
167168
const statusCb = (msg: string) => {
@@ -173,22 +174,63 @@ export const createInstance = async (
173174
const finalCb = async (error, contractObject, address) => {
174175
if (error) {
175176
const log = logBuilder(error)
176-
177177
return terminalLogger(plugin, log)
178178
}
179+
179180
addInstance(dispatch, { contractData: contractObject, address, name: contractObject.name })
180181
const data = await plugin.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)
181-
182182
plugin.compilersArtefacts.addResolvedContract(addressToString(address), data)
183-
if (plugin.REACT_API.ipfsChecked) {
184-
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', plugin.REACT_API.networkName])
185-
publishToStorage('ipfs', selectedContract)
183+
184+
if (isVerifyChecked) {
185+
_paq.push(['trackEvent', 'udapp', 'DeployAndVerify', plugin.REACT_API.networkName])
186+
187+
try {
188+
const status = plugin.blockchain.getCurrentNetworkStatus()
189+
if (status.error || !status.network) {
190+
throw new Error(`Could not get network status: ${status.error || 'Unknown error'}`)
191+
}
192+
const currentChainId = parseInt(status.network.id)
193+
194+
const response = await fetch('https://chainid.network/chains.json')
195+
if (!response.ok) throw new Error('Could not fetch chains list from chainid.network.')
196+
const allChains = await response.json()
197+
const currentChain = allChains.find(chain => chain.chainId === currentChainId)
198+
199+
if (!currentChain) {
200+
const errorMsg = `The current network (Chain ID: ${currentChainId}) is not supported for verification via this plugin. Please switch to a supported network like Sepolia or Mainnet.`
201+
const errorLog = logBuilder(errorMsg)
202+
terminalLogger(plugin, errorLog)
203+
return
204+
}
205+
206+
const etherscanApiKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')
207+
208+
const verificationData = {
209+
chainId: currentChainId.toString(),
210+
currentChain: currentChain,
211+
contractAddress: addressToString(address),
212+
contractName: selectedContract.name,
213+
compilationResult: await plugin.compilersArtefacts.getCompilerAbstract(selectedContract.contract.file),
214+
constructorArgs: args,
215+
etherscanApiKey: etherscanApiKey
216+
}
217+
218+
setTimeout(async () => {
219+
await plugin.call('contract-verification', 'verifyOnDeploy', verificationData)
220+
}, 1500)
221+
222+
} catch (e) {
223+
const errorMsg = `Verification setup failed: ${e.message}`
224+
const errorLog = logBuilder(errorMsg)
225+
terminalLogger(plugin, errorLog)
226+
}
227+
186228
} else {
187229
_paq.push(['trackEvent', 'udapp', 'DeployOnly', plugin.REACT_API.networkName])
188230
}
231+
189232
if (isProxyDeployment) {
190233
const initABI = contractObject.abi.find(abi => abi.name === 'initialize')
191-
192234
plugin.call('openzeppelin-proxy', 'executeUUPSProxy', addressToString(address), args, initABI, contractObject)
193235
} else if (isContractUpgrade) {
194236
plugin.call('openzeppelin-proxy', 'executeUUPSContractUpgrade', args, addressToString(address), contractObject)

libs/remix-ui/run-tab/src/lib/actions/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const setPassphraseModal = (passphrase: string) => setPassphrasePrompt(di
4545
export const setMatchPassphraseModal = (passphrase: string) => setMatchPassphrasePrompt(dispatch, passphrase)
4646
export const signMessage = (account: string, message: string, modalContent: (hash: string, data: string) => JSX.Element, passphrase?: string) => signMessageWithAddress(plugin, dispatch, account, message, modalContent, passphrase)
4747
export const fetchSelectedContract = (contractName: string, compiler: CompilerAbstractType) => getSelectedContract(contractName, compiler)
48-
export const createNewInstance = async (selectedContract: ContractData, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, publishToStorage: (storage: 'ipfs' | 'swarm', contract: ContractData) => void, mainnetPrompt: MainnetPrompt, isOverSizePrompt: (values: OverSizeLimit) => JSX.Element, args, deployMode: DeployMode[]) => createInstance(plugin, dispatch, selectedContract, gasEstimationPrompt, passphrasePrompt, publishToStorage, mainnetPrompt, isOverSizePrompt, args, deployMode)
48+
export const createNewInstance = async (selectedContract: ContractData, gasEstimationPrompt: (msg: string) => JSX.Element, passphrasePrompt: (msg: string) => JSX.Element, publishToStorage: (storage: 'ipfs' | 'swarm', contract: ContractData) => void, mainnetPrompt: MainnetPrompt, isOverSizePrompt: (values: OverSizeLimit) => JSX.Element, args, deployMode: DeployMode[], isVerifyChecked: boolean) => createInstance(plugin, dispatch, selectedContract, gasEstimationPrompt, passphrasePrompt, publishToStorage, mainnetPrompt, isOverSizePrompt, args, deployMode, isVerifyChecked)
4949
export const setSendValue = (value: string) => setSendTransactionValue(dispatch, value)
5050
export const setBaseFeePerGas = (baseFee: string) => updateBaseFeePerGas(dispatch, baseFee)
5151
export const setConfirmSettings = (confirmation: boolean) => updateConfirmSettings(dispatch, confirmation)

0 commit comments

Comments
 (0)