|
1 | 1 | import { PluginClient } from '@remixproject/plugin' |
2 | 2 | import { createClient } from '@remixproject/plugin-webview' |
| 3 | + |
3 | 4 | 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' |
5 | 6 | import { mergeChainSettingsWithDefaults, validConfiguration } from './utils' |
6 | 7 | import { getVerifier } from './Verifiers' |
| 8 | +import { CompilerAbstract } from '@remix-project/remix-solidity' |
7 | 9 |
|
8 | 10 | export class ContractVerificationPluginClient extends PluginClient { |
9 | 11 | public internalEvents: EventManager |
10 | 12 |
|
11 | 13 | constructor() { |
12 | 14 | super() |
13 | | - this.methods = ['lookupAndSave'] |
| 15 | + this.methods = ['lookupAndSave', 'verifyOnDeploy'] |
14 | 16 | this.internalEvents = new EventManager() |
15 | 17 | createClient(this) |
16 | 18 | this.onload() |
@@ -62,8 +64,179 @@ export class ContractVerificationPluginClient extends PluginClient { |
62 | 64 | } |
63 | 65 | } |
64 | 66 |
|
| 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 | + |
65 | 238 | private getUserSettingsFromLocalStorage(): ContractVerificationSettings { |
66 | | - const fallbackSettings = { chains: {} }; |
| 239 | + const fallbackSettings = { chains: {} } |
67 | 240 | try { |
68 | 241 | const settings = window.localStorage.getItem("contract-verification:settings") |
69 | 242 | return settings ? JSON.parse(settings) : fallbackSettings |
|
0 commit comments