UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

213 lines 9.82 kB
import fetch from 'cross-fetch'; import { buildArtifact as zksyncBuildArtifact } from '@hyperlane-xyz/core/buildArtifact-zksync.js'; import { rootLogger, sleep, strip0x } from '@hyperlane-xyz/utils'; import { ExplorerFamily } from '../../metadata/chainMetadataTypes.js'; import { ContractVerificationStatus } from '../../token/types.js'; import { BaseContractVerifier } from './BaseContractVerifier.js'; import { EXPLORER_GET_ACTIONS, ExplorerApiActions, ExplorerApiErrors, } from './types.js'; export class ContractVerifier extends BaseContractVerifier { multiProvider; apiKeys; logger = rootLogger.child({ module: 'ContractVerifier' }); compilerOptions; constructor(multiProvider, apiKeys, buildArtifact, licenseType) { super(multiProvider, buildArtifact); this.multiProvider = multiProvider; this.apiKeys = apiKeys; const compilerversion = `v${buildArtifact.solcLongVersion}`; const versionRegex = /v(\d.\d.\d+)\+commit.\w+/; const matches = versionRegex.exec(compilerversion); if (!matches) { throw new Error(`Invalid compiler version ${compilerversion}`); } this.compilerOptions = { codeformat: 'solidity-standard-json-input', compilerversion, licenseType, }; if (zksyncBuildArtifact?.zk_version) this.compilerOptions.zksolcversion = `v${zksyncBuildArtifact.zk_version}`; } async verify(chain, input, verificationLogger) { const contractType = input.isProxy ? 'proxy' : 'implementation'; verificationLogger.debug(`📝 Verifying ${contractType}...`); const data = input.isProxy ? this.getProxyData(input) : this.getImplementationData(chain, input, verificationLogger); try { const guid = await this.submitForm(chain, input.isProxy ? ExplorerApiActions.VERIFY_PROXY : ExplorerApiActions.VERIFY_IMPLEMENTATION, verificationLogger, data); verificationLogger.trace({ guid }, `Retrieved guid from verified ${contractType}.`); await this.checkStatus(chain, input, verificationLogger, guid, contractType); const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl(chain, input.address); verificationLogger.debug({ addressUrl: addressUrl ? `${addressUrl}#code` : `Could not retrieve ${contractType} explorer URL.`, }, `✅ Successfully verified ${contractType}.`); } catch (error) { verificationLogger.debug({ error }, `Verification of ${contractType} failed`); throw error; } } async checkStatus(chain, input, verificationLogger, guid, contractType) { verificationLogger.trace({ guid }, `Checking ${contractType} status...`); await this.submitForm(chain, input.isProxy ? ExplorerApiActions.CHECK_PROXY_STATUS : ExplorerApiActions.CHECK_IMPLEMENTATION_STATUS, verificationLogger, { guid: guid, }); } prepareImplementationData(sourceName, input, filteredStandardInputJson) { return { sourceCode: JSON.stringify(filteredStandardInputJson), contractname: `${sourceName}:${input.name}`, contractaddress: input.address, constructorArguements: strip0x(input.constructorArguments ?? ''), ...this.compilerOptions, }; } /** * @notice Submits the verification form to the explorer API * @param chain The name of the chain where the contract is deployed * @param verificationLogger A logger instance for verification-specific logging * @param options Additional options for the API request * @returns The response from the explorer API */ async submitForm(chain, action, verificationLogger, options) { const { family } = this.multiProvider.getExplorerApi(chain); const apiUrl = this.multiProvider.getExplorerApiUrl(chain); const params = new URLSearchParams(); params.set('module', 'contract'); params.set('action', action); for (const [key, value] of Object.entries(options ?? {})) { params.set(key, value); } let timeout = 1000; const url = new URL(apiUrl); const isGetRequest = EXPLORER_GET_ACTIONS.includes(action); // For GET requests, merge the parameters with existing ones instead of overwriting if (isGetRequest) { for (const [key, value] of params.entries()) { url.searchParams.set(key, value); } } switch (family) { case ExplorerFamily.ZkSync: case ExplorerFamily.Etherscan: timeout = 5000; break; case ExplorerFamily.Blockscout: timeout = 1000; url.searchParams.set('module', 'contract'); url.searchParams.set('action', action); break; case ExplorerFamily.Routescan: timeout = 500; break; case ExplorerFamily.Other: default: throw new Error(`Unsupported explorer family: ${family}, ${chain}, ${apiUrl}`); } verificationLogger.trace({ apiUrl, chain }, 'Sending request to explorer...'); // Add debug logging to show the final URL verificationLogger.debug({ apiUrl, chain, finalUrl: url.toString(), isGetRequest, family, }, 'Contract verification URL details'); let response; if (isGetRequest) { response = await fetch(url.toString(), { method: 'GET', }); } else { const init = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params, }; response = await fetch(url.toString(), init); } let responseJson; try { const responseTextString = await response.text(); verificationLogger.trace({ apiUrl, chain }, 'Parsing response from explorer...'); responseJson = JSON.parse(responseTextString); } catch { verificationLogger.trace({ failure: response.statusText, status: response.status, chain, apiUrl, family, }, 'Failed to parse response from explorer.'); throw new Error(`Failed to parse response from explorer (${apiUrl}, ${chain}): ${response.statusText || 'UNKNOWN STATUS TEXT'} (${response.status || 'UNKNOWN STATUS'})`); } if (responseJson.message !== 'OK') { let errorMessage; switch (responseJson.result) { case ExplorerApiErrors.VERIFICATION_PENDING: verificationLogger.trace({ result: responseJson.result, }, 'Verification still pending'); await sleep(timeout); return this.submitForm(chain, action, verificationLogger, options); case ExplorerApiErrors.ALREADY_VERIFIED: case ExplorerApiErrors.ALREADY_VERIFIED_ALT: break; case ExplorerApiErrors.NOT_VERIFIED: case ExplorerApiErrors.PROXY_FAILED: case ExplorerApiErrors.BYTECODE_MISMATCH: errorMessage = `${responseJson.message}: ${responseJson.result}`; break; default: errorMessage = `Verification failed: ${JSON.stringify(responseJson.result ?? response.statusText)}`; break; } if (errorMessage) { verificationLogger.debug(errorMessage); throw new Error(`[${chain}] ${errorMessage}`); } } if (responseJson.result === ExplorerApiErrors.UNKNOWN_UID) { await sleep(timeout); return this.submitForm(chain, action, verificationLogger, options); } if (responseJson.result === ExplorerApiErrors.UNABLE_TO_VERIFY) { const errorMessage = `Verification failed. ${JSON.stringify(responseJson.result ?? response.statusText)}`; verificationLogger.debug(errorMessage); throw new Error(`[${chain}] ${errorMessage}`); } verificationLogger.trace({ apiUrl, chain, result: responseJson.result }, 'Returning result from explorer.'); await sleep(timeout); return responseJson.result; } async getContractVerificationStatus(chain, address, verificationLogger = this.logger) { try { verificationLogger.trace(`Fetching contract ABI for ${chain} address ${address}`); const sourceCodeResults = (await this.submitForm(chain, ExplorerApiActions.GETSOURCECODE, verificationLogger, { address }))[0]; // This specific query only returns 1 result // Explorer won't return ContractName if unverified return sourceCodeResults.ContractName ? ContractVerificationStatus.Verified : ContractVerificationStatus.Unverified; } catch (e) { this.logger.info(`Error fetching contract verification status for ${address} on chain ${chain}: ${e}`); return ContractVerificationStatus.Error; } } getProxyData(input) { return { address: input.address, expectedimplementation: input.expectedimplementation, }; } } //# sourceMappingURL=ContractVerifier.js.map