Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b466ae3
feat: verify on deploy
Sep 17, 2025
2e6306d
fix context menu and model selection menu
Sep 16, 2025
7cc59ee
reverting provider
STetsing Sep 17, 2025
5cac7aa
lint
STetsing Sep 17, 2025
04ba18b
fix borders in menus
Sep 17, 2025
1ab13fc
fix test
Sep 15, 2025
7be76eb
lint
Sep 15, 2025
ea3b96a
add matomo keys
Sep 15, 2025
d753a8f
disable matomo necessary in settings
Aniket-Engg Sep 15, 2025
03f53b0
disable cookies on settings disable
Aniket-Engg Sep 15, 2025
fd89f8b
remove matomo keys
Sep 10, 2025
a030af0
feat:apply portal-based submenus for Environment dropdown and reorder…
Sep 9, 2025
d01568e
feat: smart account dropdown ui
Sep 10, 2025
3e11d81
test(e2e): add end-to-end tests and minor refinements
Sep 11, 2025
8eb4b36
update provider test code
Sep 15, 2025
c9e757b
update switchEnvironment test
Sep 15, 2025
3e61f03
remove 'customize list' menu and update e2e tests
Sep 16, 2025
1e8b2ca
remove #pr and update e2e
Sep 18, 2025
37817d8
fix e2e test code
Sep 18, 2025
c4e1ad9
add verifiable chain list
Sep 18, 2025
a97b17a
dynamic conversation starters
Sep 8, 2025
c61dd05
lint
Sep 9, 2025
6a9f89e
update questions
ryestew Sep 9, 2025
07a3278
minor
STetsing Sep 9, 2025
0a3dad1
fixed e2e
STetsing Sep 17, 2025
73b0d35
move repo
Sep 17, 2025
9694dae
fix matomo settings
Aniket-Engg Sep 18, 2025
cb610dd
remove lang selection
Aniket-Engg Sep 18, 2025
ea96642
RemixAI text
Aniket-Engg Sep 18, 2025
2c034cf
remove locale e2e
Aniket-Engg Sep 18, 2025
faaf384
e2e test
Sep 22, 2025
74f3812
Merge branch 'master' into add-contract-verification-checkbox
hsy822 Sep 22, 2025
4dbdbbe
Merge branch 'master' into add-contract-verification-checkbox
hsy822 Sep 24, 2025
833e44f
feat(deployment): add receipt to verification plugin during contract …
Sep 24, 2025
07a64a9
Merge branch 'master' into add-contract-verification-checkbox
hsy822 Sep 25, 2025
f70f839
removed #pr
Sep 25, 2025
7c7e2b3
lint
Sep 25, 2025
6535bf8
set timeout for etherscan
Sep 25, 2025
838dca0
fix(settings): Sync global Etherscan API key to local plugin settings…
Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 153 additions & 3 deletions apps/contract-verification/src/app/ContractVerificationPluginClient.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { PluginClient } from '@remixproject/plugin'
import { createClient } from '@remixproject/plugin-webview'

import EventManager from 'events'
import { VERIFIERS, type ChainSettings, type ContractVerificationSettings, type LookupResponse, type VerifierIdentifier } from './types'
import { VERIFIERS, type ChainSettings,Chain, type ContractVerificationSettings, type LookupResponse, type VerifierIdentifier, SubmittedContract, SubmittedContracts, VerificationReceipt } from './types'
import { mergeChainSettingsWithDefaults, validConfiguration } from './utils'
import { getVerifier } from './Verifiers'
import { CompilerAbstract } from '@remix-project/remix-solidity'

export class ContractVerificationPluginClient extends PluginClient {
public internalEvents: EventManager

constructor() {
super()
this.methods = ['lookupAndSave']
this.methods = ['lookupAndSave', 'verifyOnDeploy']
this.internalEvents = new EventManager()
createClient(this)
this.onload()
Expand Down Expand Up @@ -62,8 +64,156 @@ export class ContractVerificationPluginClient extends PluginClient {
}
}

verifyOnDeploy = async (data: any): Promise<void> => {
try {
await this.call('terminal', 'log', { type: 'log', value: 'Verification process started...' })

const { chainId, currentChain, contractAddress, contractName, compilationResult, constructorArgs, etherscanApiKey } = data

if (!currentChain) {
await this.call('terminal', 'log', { type: 'error', value: 'Chain data was not provided for verification.' })
return
}

const userSettings = this.getUserSettingsFromLocalStorage()

if (etherscanApiKey) {
if (!userSettings.chains[chainId]) {
userSettings.chains[chainId] = { verifiers: {} }
}

if (!userSettings.chains[chainId].verifiers.Etherscan) {
userSettings.chains[chainId].verifiers.Etherscan = {}
}
userSettings.chains[chainId].verifiers.Etherscan.apiKey = etherscanApiKey

if (!userSettings.chains[chainId].verifiers.Routescan) {
userSettings.chains[chainId].verifiers.Routescan = {}
}
if (!userSettings.chains[chainId].verifiers.Routescan.apiKey){
userSettings.chains[chainId].verifiers.Routescan.apiKey = "placeholder"
}

window.localStorage.setItem("contract-verification:settings", JSON.stringify(userSettings))

}

const submittedContracts: SubmittedContracts = JSON.parse(window.localStorage.getItem('contract-verification:submitted-contracts') || '{}')

const filePath = Object.keys(compilationResult.data.contracts).find(path =>
compilationResult.data.contracts[path][contractName]
)
if (!filePath) throw new Error(`Could not find file path for contract ${contractName}`)

const submittedContract: SubmittedContract = {
id: `${chainId}-${contractAddress}`,
address: contractAddress,
chainId: chainId,
filePath: filePath,
contractName: contractName,
abiEncodedConstructorArgs: constructorArgs,
date: new Date().toISOString(),
receipts: []
Copy link
Contributor

@manuelwedler manuelwedler Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have receipts for submittedContract and store it in local storage, such that it also shows up in the plugin's UI. See the VerifyView for reference.

I also thought of refactoring the VerifyView once, since a lot of the logic from there could actually be reused for this feature.

Anyway, very nice that you tackled this feature!

}

const compilerAbstract: CompilerAbstract = compilationResult
const chainSettings = mergeChainSettingsWithDefaults(chainId, userSettings)

if (validConfiguration(chainSettings, 'Sourcify')) {
await this._verifyWithProvider('Sourcify', submittedContract, compilerAbstract, chainId, chainSettings)
}

if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.toLowerCase().includes('routescan'))) {
await this._verifyWithProvider('Routescan', submittedContract, compilerAbstract, chainId, chainSettings)
}

if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.toLowerCase().includes('blockscout'))) {
await this._verifyWithProvider('Blockscout', submittedContract, compilerAbstract, chainId, chainSettings)
}

if (currentChain.explorers && currentChain.explorers.some(explorer => explorer.name.toLowerCase().includes('etherscan'))) {
if (etherscanApiKey) {
if (!chainSettings.verifiers.Etherscan) chainSettings.verifiers.Etherscan = {}
chainSettings.verifiers.Etherscan.apiKey = etherscanApiKey
await this._verifyWithProvider('Etherscan', submittedContract, compilerAbstract, chainId, chainSettings)
} else {
await this.call('terminal', 'log', { type: 'warn', value: 'Etherscan verification skipped: API key not found in global Settings.' })
}
}

submittedContracts[submittedContract.id] = submittedContract

window.localStorage.setItem('contract-verification:submitted-contracts', JSON.stringify(submittedContracts))
this.internalEvents.emit('submissionUpdated')
} catch (error) {
await this.call('terminal', 'log', { type: 'error', value: `An unexpected error occurred during verification: ${error.message}` })
}
}

private _verifyWithProvider = async (
providerName: VerifierIdentifier,
submittedContract: SubmittedContract,
compilerAbstract: CompilerAbstract,
chainId: string,
chainSettings: ChainSettings
): Promise<void> => {
let receipt: VerificationReceipt
const verifierSettings = chainSettings.verifiers[providerName]
const verifier = getVerifier(providerName, verifierSettings)

try {
if (validConfiguration(chainSettings, providerName)) {

await this.call('terminal', 'log', { type: 'log', value: `Verifying with ${providerName}...` })

if (providerName === 'Etherscan' || providerName === 'Routescan' || providerName === 'Blockscout') {
await new Promise(resolve => setTimeout(resolve, 10000))
}

if (verifier && typeof verifier.verify === 'function') {
const result = await verifier.verify(submittedContract, compilerAbstract)

receipt = {
receiptId: result.receiptId || undefined,
verifierInfo: { name: providerName, apiUrl: verifier.apiUrl },
status: result.status,
message: result.message,
lookupUrl: result.lookupUrl,
contractId: submittedContract.id,
isProxyReceipt: false,
failedChecks: 0
}

const successMessage = `${providerName} verification successful.`
await this.call('terminal', 'log', { type: 'info', value: successMessage })

if (result.lookupUrl) {
const textMessage = `${result.lookupUrl}`
await this.call('terminal', 'log', { type: 'info', value: textMessage })
}
} else {
throw new Error(`${providerName} verifier is not properly configured or does not support direct verification.`)
}
}
} catch (e) {
receipt = {
verifierInfo: { name: providerName, apiUrl: verifier?.apiUrl || 'N/A' },
status: 'failed',
message: e.message,
contractId: submittedContract.id,
isProxyReceipt: false,
failedChecks: 0
}
await this.call('terminal', 'log', { type: 'error', value: `${providerName} verification failed: ${e.message}` })
} finally {
if (receipt) {
submittedContract.receipts.push(receipt)
}
}
}

private getUserSettingsFromLocalStorage(): ContractVerificationSettings {
const fallbackSettings = { chains: {} };
const fallbackSettings = { chains: {} }
try {
const settings = window.localStorage.getItem("contract-verification:settings")
return settings ? JSON.parse(settings) : fallbackSettings
Expand Down
11 changes: 10 additions & 1 deletion apps/contract-verification/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,18 @@ const App = () => {
.then((data) => setChains(data))
.catch((error) => console.error('Failed to fetch chains.json:', error))

const submissionUpdatedListener = () => {
const latestSubmissions = window.localStorage.getItem('contract-verification:submitted-contracts')
if (latestSubmissions) {
setSubmittedContracts(JSON.parse(latestSubmissions))
}
}
plugin.internalEvents.on('submissionUpdated', submissionUpdatedListener)

// Clean up on unmount
return () => {
plugin.off('compilerArtefacts' as any, 'compilationSaved')
plugin.internalEvents.removeListener('submissionUpdated', submissionUpdatedListener)
}
}, [])

Expand Down Expand Up @@ -167,4 +176,4 @@ const App = () => {
)
}

export default App
export default App
33 changes: 33 additions & 0 deletions apps/remix-ide-e2e/src/tests/deploy_vefiry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict'
import { NightwatchBrowser } from 'nightwatch'
import init from '../helpers/init'

declare global {
interface Window { testplugin: { name: string, url: string }; }
}

module.exports = {
'@disabled': true,
before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, null)
},

'Should show warning for unsupported network when deploying with "Verify" on Remix VM #group1': function (browser: NightwatchBrowser) {
browser
.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.clickLaunchIcon('filePanel')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/1_Storage.sol')
.clickLaunchIcon('udapp')
.waitForElementVisible('*[data-id="Deploy - transact (not payable)"]')
.waitForElementVisible('#deployAndRunVerifyContract')
.click('#deployAndRunVerifyContract')
.click('*[data-id="Deploy - transact (not payable)"]')
.waitForElementVisible({
selector: "//*[contains(text(),'is not supported for verification via this plugin')]",
locateStrategy: 'xpath',
timeout: 10000
})
.end()
}
}
1 change: 0 additions & 1 deletion libs/remix-ui/remix-ai-assistant/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ const AiChatIntro: React.FC<AiChatIntroProps> = ({ sendPrompt }) => {
{/* Dynamic Conversation Starters */}
<div className="d-flex flex-column mt-3" style={{ maxWidth: '400px' }}>
{conversationStarters.map((starter, index) => (

<button
key={`${starter.level}-${index}`}
data-id={`remix-ai-assistant-starter-${starter.level}-${index}`}
Expand Down
64 changes: 57 additions & 7 deletions libs/remix-ui/run-tab/src/lib/actions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ export const createInstance = async (
mainnetPrompt: MainnetPrompt,
isOverSizePrompt: (values: OverSizeLimit) => JSX.Element,
args,
deployMode: DeployMode[]) => {
deployMode: DeployMode[],
isVerifyChecked: boolean) => {
const isProxyDeployment = (deployMode || []).find(mode => mode === 'Deploy with Proxy')
const isContractUpgrade = (deployMode || []).find(mode => mode === 'Upgrade with Proxy')
const statusCb = (msg: string) => {
Expand All @@ -173,22 +174,71 @@ export const createInstance = async (
const finalCb = async (error, contractObject, address) => {
if (error) {
const log = logBuilder(error)

return terminalLogger(plugin, log)
}

addInstance(dispatch, { contractData: contractObject, address, name: contractObject.name })
const data = await plugin.compilersArtefacts.getCompilerAbstract(contractObject.contract.file)

plugin.compilersArtefacts.addResolvedContract(addressToString(address), data)
if (plugin.REACT_API.ipfsChecked) {
_paq.push(['trackEvent', 'udapp', 'DeployAndPublish', plugin.REACT_API.networkName])
publishToStorage('ipfs', selectedContract)

if (isVerifyChecked) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the UI, that should be checked by default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But probably the setting should be remenbered when the page is loaded.

_paq.push(['trackEvent', 'udapp', 'DeployAndVerify', plugin.REACT_API.networkName])

try {
await publishToStorage('ipfs', selectedContract)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we keeping this in order to make sure sourcify verifies it? If we can use the sourcify api to verify it that would be better, we won't have to rely on a IPFS endpoint.

} catch (e) {
const errorMsg = `Could not publish contract metadata to IPFS. Continuing with verification... (Error: ${e.message})`
const errorLog = logBuilder(errorMsg)
terminalLogger(plugin, errorLog)
}

try {
const status = plugin.blockchain.getCurrentNetworkStatus()
if (status.error || !status.network) {
throw new Error(`Could not get network status: ${status.error || 'Unknown error'}`)
}
const currentChainId = parseInt(status.network.id)

const response = await fetch('https://chainid.network/chains.json')
if (!response.ok) throw new Error('Could not fetch chains list from chainid.network.')
const allChains = await response.json()
const currentChain = allChains.find(chain => chain.chainId === currentChainId)

if (!currentChain) {
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.`
const errorLog = logBuilder(errorMsg)
terminalLogger(plugin, errorLog)
return
}

const etherscanApiKey = await plugin.call('config', 'getAppParameter', 'etherscan-access-token')

const verificationData = {
chainId: currentChainId.toString(),
currentChain: currentChain,
contractAddress: addressToString(address),
contractName: selectedContract.name,
compilationResult: await plugin.compilersArtefacts.getCompilerAbstract(selectedContract.contract.file),
constructorArgs: args,
etherscanApiKey: etherscanApiKey
}

setTimeout(async () => {
await plugin.call('contract-verification', 'verifyOnDeploy', verificationData)
}, 1000)

} catch (e) {
const errorMsg = `Verification setup failed: ${e.message}`
const errorLog = logBuilder(errorMsg)
terminalLogger(plugin, errorLog)
}

} else {
_paq.push(['trackEvent', 'udapp', 'DeployOnly', plugin.REACT_API.networkName])
}

if (isProxyDeployment) {
const initABI = contractObject.abi.find(abi => abi.name === 'initialize')

plugin.call('openzeppelin-proxy', 'executeUUPSProxy', addressToString(address), args, initABI, contractObject)
} else if (isContractUpgrade) {
plugin.call('openzeppelin-proxy', 'executeUUPSContractUpgrade', args, addressToString(address), contractObject)
Expand Down
2 changes: 1 addition & 1 deletion libs/remix-ui/run-tab/src/lib/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const setPassphraseModal = (passphrase: string) => setPassphrasePrompt(di
export const setMatchPassphraseModal = (passphrase: string) => setMatchPassphrasePrompt(dispatch, passphrase)
export const signMessage = (account: string, message: string, modalContent: (hash: string, data: string) => JSX.Element, passphrase?: string) => signMessageWithAddress(plugin, dispatch, account, message, modalContent, passphrase)
export const fetchSelectedContract = (contractName: string, compiler: CompilerAbstractType) => getSelectedContract(contractName, compiler)
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)
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)
export const setSendValue = (value: string) => setSendTransactionValue(dispatch, value)
export const setBaseFeePerGas = (baseFee: string) => updateBaseFeePerGas(dispatch, baseFee)
export const setConfirmSettings = (confirmation: boolean) => updateConfirmSettings(dispatch, confirmation)
Expand Down
Loading