diff --git a/apps/contract-verification/src/app/ContractVerificationPluginClient.ts b/apps/contract-verification/src/app/ContractVerificationPluginClient.ts index c68ecf5cdbc..53865684c3d 100644 --- a/apps/contract-verification/src/app/ContractVerificationPluginClient.ts +++ b/apps/contract-verification/src/app/ContractVerificationPluginClient.ts @@ -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() @@ -62,8 +64,156 @@ export class ContractVerificationPluginClient extends PluginClient { } } + verifyOnDeploy = async (data: any): Promise => { + 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: [] + } + + 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 => { + 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 diff --git a/apps/contract-verification/src/app/app.tsx b/apps/contract-verification/src/app/app.tsx index 48e95697c70..5cdd55e4227 100644 --- a/apps/contract-verification/src/app/app.tsx +++ b/apps/contract-verification/src/app/app.tsx @@ -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) } }, []) @@ -167,4 +176,4 @@ const App = () => { ) } -export default App +export default App \ No newline at end of file diff --git a/apps/remix-ide-e2e/src/tests/deploy_vefiry.test.ts b/apps/remix-ide-e2e/src/tests/deploy_vefiry.test.ts new file mode 100644 index 00000000000..96b0ac7fd29 --- /dev/null +++ b/apps/remix-ide-e2e/src/tests/deploy_vefiry.test.ts @@ -0,0 +1,29 @@ +'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 NOT display the "Verify Contract" checkbox on an unsupported network (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)"]') + .waitForElementNotPresent({ + selector: '#deployAndRunVerifyContract', + timeout: 5000 + }) + .end() + } +} \ No newline at end of file diff --git a/libs/remix-ui/remix-ai-assistant/src/components/chat.tsx b/libs/remix-ui/remix-ai-assistant/src/components/chat.tsx index 068b437e1d1..92360d078fe 100644 --- a/libs/remix-ui/remix-ai-assistant/src/components/chat.tsx +++ b/libs/remix-ui/remix-ai-assistant/src/components/chat.tsx @@ -47,7 +47,6 @@ const AiChatIntro: React.FC = ({ sendPrompt }) => { {/* Dynamic Conversation Starters */}
{conversationStarters.map((starter, index) => ( -
)} diff --git a/libs/remix-ui/run-tab/src/lib/components/verificationSettingsUI.tsx b/libs/remix-ui/run-tab/src/lib/components/verificationSettingsUI.tsx new file mode 100644 index 00000000000..988ef4a53d5 --- /dev/null +++ b/libs/remix-ui/run-tab/src/lib/components/verificationSettingsUI.tsx @@ -0,0 +1,45 @@ +// eslint-disable-next-line no-use-before-define +import React from 'react' +import { FormattedMessage, useIntl } from 'react-intl' +import { CustomTooltip } from '@remix-ui/helper' + +interface VerificationSettingsProps { + isVerifyChecked: boolean + onVerifyCheckedChange: (isChecked: boolean) => void +} + +export function VerificationSettingsUI(props: VerificationSettingsProps) { + const { isVerifyChecked, onVerifyCheckedChange } = props + const intl = useIntl() + + return ( +
+
+ onVerifyCheckedChange(e.target.checked)} + checked={isVerifyChecked} + /> + + + + } + > + + +
+
+ ) +} \ No newline at end of file diff --git a/libs/remix-ui/run-tab/src/lib/types/index.ts b/libs/remix-ui/run-tab/src/lib/types/index.ts index 4a66d8d0dee..67188e4ce26 100644 --- a/libs/remix-ui/run-tab/src/lib/types/index.ts +++ b/libs/remix-ui/run-tab/src/lib/types/index.ts @@ -279,7 +279,8 @@ export interface ContractDropdownProps { mainnetPrompt: MainnetPrompt, isOverSizePrompt: (values: OverSizeLimit) => JSX.Element, args, - deployMode: DeployMode[]) => void, + deployMode: DeployMode[], + isVerifyChecked: boolean) => void, ipfsCheckedState: boolean, setIpfsCheckedState: (value: boolean) => void, publishToStorage: (storage: 'ipfs' | 'swarm', contract: ContractData) => void,