From 7ea3d13faedaf7c9affb0bc24e665ef991651e39 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 21 Mar 2024 09:08:57 +0100 Subject: [PATCH] feat: multi verification (#25) * feat: multi verification * fix: remove some leftovers * fix: snowscan api url * fix: improve logging * fix: readd --force flag for build info while the issue with compiling --build-info was recently fixed, most ppl are stuck on months old foundry due to https://github.com/foundry-rs/foundry/issues/6516, so reverting the change for now * fix: lint --- README.md | 2 +- bun.lockb | Bin 16945 -> 16785 bytes src/cli-args.ts | 2 +- src/config.ts | 208 +++++++++++++++++++++++--------- src/index.ts | 31 +++-- src/types.ts | 2 +- src/utils/calltrace-verifier.ts | 41 ++----- src/utils/explorer-api.ts | 45 +++---- src/utils/foundry-ffi.ts | 6 +- src/utils/misc.ts | 7 ++ 10 files changed, 204 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 06f1fe7..280386b 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Below you can see all the CLI arguments and aliases: -r, --rpcUrl string RPC URL to fetch transaction details, mandatory to support for debug_traceTransaction. You can use Alchemy or Quicknode providers if available. - -e, --etherscanUrl string Etherscan API endpoint + -e, --explorerUrl string Etherscan API endpoint -k, --etherscanApiKey string Etherscan API key ``` diff --git a/bun.lockb b/bun.lockb index a1a40da22972170466c7c3a7feb2c4461338afd7..f1f4634bddaf25f927062aa9304aba58685dfe7f 100755 GIT binary patch delta 514 zcmXwxJxIe)6h`yf8fZ(SX=|#cO#}WRBtd8p2ayUQ;!qc9Cp&iR;2@o(;NK?714k!u za43{O$KoKJM4TKQ9XkqL#CyNQ2j{$Z54_vI5O?R|wkAaXP}PNyb-h9E!D^P@O?Uun zIW8@D2pf4WZFmG*vs_N$cX-9%at43GUV+OM`~`2!ak+uN;q7@Y_wX3rTj25l|G+hq z%M<(y*Na?wuv+4G6CS{t#ia!gVWZ5Y4UftVS&bKDH=YPK?YDI7(UGciaBM1yB8X*z z&;J~wcM?TWigfcFCo)m9qL!dL8#SA5-u2;x;YjN4s@!t-cX%srDG~gWx6BayGe@iu zX+k5W37yCjU6({>S5DkTX?w4!ju)1bN-*}8rjjp}a8{)0U#8amaGF$uXa73$2d!aV A#Q*>R delta 677 zcmXw!JuE{}7>3Vnx!PMwTW*W~q3Z9p28|d*!e9`ot=$?^$8IbVn)us5WL^>HwVic26yzl2a$@6^Qd3$=!chB~X=7DiqG_IQ)X2?*w6e`0Fu^DDx5#Nb} z5hhjfgV^q8QWHOklLJh)#V_Kl&16^nD$Yll9Esn=^D!nT;t%oCAd_?Pr+9UU$))&P zTpVU{BmNPW;!GN1Gr_(hz7q$NOse7sv7KU46F;TOdf6M*b#F&&A^jZi_;T8G*7in} zQU)*q*Z6sTnDPDr!1L39_jM_?qM~j% z^e;K?v~m80ZH>Af1P>-rZ^~Zs~g`q3gLvD>nQ0Te#3F<};oC&BEXQ SO7F}aT5WfCvD~r>$34GBBV%g- diff --git a/src/cli-args.ts b/src/cli-args.ts index 231caa5..512f40f 100644 --- a/src/cli-args.ts +++ b/src/cli-args.ts @@ -22,7 +22,7 @@ export const args = parse( description: "RPC URL to fetch transaction details, mandatory to support for debug_traceTransaction. You can use Alchemy or Quicknode providers if available.", }, - etherscanUrl: { + explorerUrl: { type: String, optional: true, alias: "e", diff --git a/src/config.ts b/src/config.ts index 8aa6b2f..8af7ece 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,64 +5,160 @@ export const VERIFY_VERSION = packageJson.version; export const VERBOSE = !!process.env.VERBOSE; -export const ETHERSCAN_API_KEYS: { - [key: number]: string; -} = { - [Networks.polygon]: process.env.ETHERSCAN_API_KEY_MATIC || "", - [Networks.main]: process.env.ETHERSCAN_API_KEY_MAINNET || "", - [Networks.arbitrum]: process.env.ETHERSCAN_API_KEY_ARBITRUM || "", - [Networks.optimism]: process.env.ETHERSCAN_API_KEY_OPTIMISM || "", - [Networks.goerli]: process.env.ETHERSCAN_API_KEY_GOERLI || "", - [Networks.sepolia]: process.env.ETHERSCAN_API_KEY_SEPOLIA || "", - [Networks.avalanche]: process.env.ETHERSCAN_API_KEY_AVALANCHE || "", - [Networks.avalancheFuji]: process.env.ETHERSCAN_API_KEY_AVALANCHE_FUJI || "", - [Networks.bnb]: process.env.ETHERSCAN_API_KEY_BNB || "", - [Networks.bnbTestnet]: process.env.ETHERSCAN_API_KEY_BNB_TESTNET || "", - [Networks.base]: process.env.ETHERSCAN_API_KEY_BASE || "", - [Networks.metis]: process.env.ETHERSCAN_API_KEY_METIS || "", - [Networks.gnosis]: process.env.ETHERSCAN_API_KEY_GNOSIS || "", - [Networks.scroll]: process.env.ETHERSCAN_API_KEY_SCROLL || "", - [Networks.zkevm]: process.env.ETHERSCAN_API_KEY_ZKEVM || "", - [Networks.celo]: process.env.ETHERSCAN_API_KEY_CELO || "", +export type ExplorerConfig = { + API_URL: string; + SITE_URL?: string; + API_KEY?: string; }; -export const ETHERSCAN_API_URL: { - [key: string]: string; -} = { - [Networks.polygon]: "https://api.polygonscan.com/api", - [Networks.main]: "https://api.etherscan.io/api", - [Networks.arbitrum]: "https://api.arbiscan.io/api", - [Networks.avalanche]: "https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan", - [Networks.avalancheFuji]: "https://api.routescan.io/v2/network/testnet/evm/43113/etherscan", - [Networks.optimism]: "https://api-optimistic.etherscan.io/api", - [Networks.goerli]: "https://api-goerli.etherscan.io/api", - [Networks.sepolia]: "https://api-sepolia.etherscan.io/api", - [Networks.bnb]: "https://api.bscscan.com/api", - [Networks.bnbTestnet]: "https://api-testnet.bscscan.com/api", - [Networks.base]: "https://api.basescan.org/api", - [Networks.metis]: "https://api.routescan.io/v2/network/mainnet/evm/1088/etherscan", - [Networks.gnosis]: "https://api.gnosisscan.io/api", - [Networks.scroll]: "https://api.scrollscan.com/api", - [Networks.zkevm]: "https://api-zkevm.polygonscan.com/api", - [Networks.celo]: "https://api.celoscan.io/api", +export type NetworkConfig = { + RPC: string; + explorers: ExplorerConfig[]; }; -export const DEFAULT_RPC_URLS: { - [key: string]: string; -} = { - [Networks.polygon]: process.env.RPC_POLYGON || "", - [Networks.main]: process.env.RPC_MAINNET || "", - [Networks.arbitrum]: process.env.RPC_ARBITRUM || "", - [Networks.avalanche]: process.env.RPC_AVALANCHE || "", - [Networks.optimism]: process.env.RPC_OPTIMISM || "", - [Networks.goerli]: process.env.RPC_GOERLI || "", - [Networks.sepolia]: process.env.RPC_SEPOLIA || "", - [Networks.bnb]: process.env.RPC_BNB || "", - [Networks.bnbTestnet]: process.env.RPC_BNB_TESTNET || "", - [Networks.base]: process.env.RPC_BASE || "", - [Networks.gnosis]: process.env.RPC_GNOSIS || "", - [Networks.scroll]: process.env.RPC_SCROLL || "", - [Networks.zkevm]: process.env.RPC_ZKEVM || "", - [Networks.celo]: process.env.RPC_CELO || "", - [Networks.metis]: process.env.RPC_METIS || "", +export const NETWORK_CONFIGS: Record = { + [Networks.polygon]: { + RPC: process.env.RPC_POLYGON || "", + explorers: [ + { + API_URL: "https://api.polygonscan.com/api", + API_KEY: process.env.ETHERSCAN_API_KEY_POLYGON || "", + SITE_URL: "https://polygonscan.com", + }, + ], + }, + [Networks.main]: { + RPC: process.env.RPC_MAINNET || "", + explorers: [ + { + API_URL: "https://api.etherscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_MAINNET || "", + SITE_URL: "https://etherscan.io", + }, + ], + }, + [Networks.arbitrum]: { + RPC: process.env.RPC_ARBITRUM || "", + explorers: [ + { + API_URL: "https://api.arbiscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_ARBITRUM || "", + SITE_URL: "https://arbiscan.io", + }, + ], + }, + [Networks.avalanche]: { + RPC: process.env.RPC_AVALANCHE || "", + explorers: [ + { + API_URL: "https://api.snowscan.xyz/api", + API_KEY: process.env.ETHERSCAN_API_KEY_AVALANCHE || "", + SITE_URL: "https://snowscan.xyz", + }, + { + API_URL: "https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan", + SITE_URL: "https://snowtrace.io", + }, + ], + }, + [Networks.optimism]: { + RPC: process.env.RPC_OPTIMISM || "", + explorers: [ + { + API_URL: "https://api-optimistic.etherscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_OPTIMISM || "", + SITE_URL: "https://optimistic.etherscan.io", + }, + ], + }, + [Networks.sepolia]: { + RPC: process.env.RPC_SEPOLIA || "", + explorers: [ + { + API_URL: "https://api-sepolia.etherscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_SEPOLIA || "", + SITE_URL: "https://sepolia.etherscan.io", + }, + ], + }, + [Networks.bnb]: { + RPC: process.env.RPC_BNB || "", + explorers: [ + { + API_URL: "https://api.bscscan.com/api", + API_KEY: process.env.ETHERSCAN_API_KEY_BNB || "", + SITE_URL: "https://bscscan.com", + }, + ], + }, + [Networks.bnbTestnet]: { + RPC: process.env.RPC_BNB_TESTNET || "", + explorers: [ + { + API_URL: "https://api-testnet.bscscan.com/api", + API_KEY: process.env.ETHERSCAN_API_KEY_BNB_TESTNET || "", + SITE_URL: "https://testnet.bscscan.com", + }, + ], + }, + [Networks.base]: { + RPC: process.env.RPC_BASE || "", + explorers: [ + { + API_URL: "https://api.basescan.org/api", + API_KEY: process.env.ETHERSCAN_API_KEY_BASE || "", + SITE_URL: "https://testnet.bscscan.com", + }, + ], + }, + [Networks.gnosis]: { + RPC: process.env.RPC_GNOSIS || "", + explorers: [ + { + API_URL: "https://api.gnosisscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_GNOSIS || "", + SITE_URL: "https://gnosisscan.io", + }, + ], + }, + [Networks.scroll]: { + RPC: process.env.RPC_SCROLL || "", + explorers: [ + { + API_URL: "https://api.scrollscan.com/api", + API_KEY: process.env.ETHERSCAN_API_KEY_SCROLL || "", + SITE_URL: "https://scrollscan.com", + }, + ], + }, + [Networks.zkevm]: { + RPC: process.env.RPC_ZKEVM || "", + explorers: [ + { + API_URL: "https://api-zkevm.polygonscan.com/api", + API_KEY: process.env.ETHERSCAN_API_KEY_ZKEVM || "", + SITE_URL: "https://zkevm.polygonscan.com", + }, + ], + }, + [Networks.celo]: { + RPC: process.env.RPC_CELO || "", + explorers: [ + { + API_URL: "https://api.celoscan.io/api", + API_KEY: process.env.ETHERSCAN_API_KEY_CELO || "", + SITE_URL: "https://celoscan.io", + }, + ], + }, + [Networks.metis]: { + RPC: process.env.RPC_METIS || "", + explorers: [ + { + API_URL: "https://api.routescan.io/v2/network/mainnet/evm/1088/etherscan", + SITE_URL: "https://explorer.metis.io", + }, + { API_URL: "https://andromeda-explorer.metis.io/api ", SITE_URL: "https://andromeda-explorer.metis.io" }, + ], + }, }; diff --git a/src/index.ts b/src/index.ts index 69d6c07..26d7792 100755 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { exit } from "node:process"; import chalk from "chalk"; import "dotenv/config"; import { args } from "./cli-args"; -import { DEFAULT_RPC_URLS, VERIFY_VERSION } from "./config"; +import { NETWORK_CONFIGS, VERIFY_VERSION } from "./config"; import { callTraceVerifier } from "./utils/calltrace-verifier"; import { loadArtifacts, loadBuildInfo } from "./utils/foundry-ffi"; import { loadJson } from "./utils/json"; @@ -32,14 +32,17 @@ const main = async () => { greets(); - const rpc = args.rpcUrl || DEFAULT_RPC_URLS[parsedRun.chain]; - const chainId = await getChainId(rpc); + const networkConfig = NETWORK_CONFIGS[parsedRun.chain]; + if (args.rpcUrl) networkConfig.RPC = args.rpcUrl; + if (args.explorerUrl) networkConfig.explorers = [{ API_URL: args.explorerUrl, API_KEY: args.etherscanApiKey }]; + + const chainId = await getChainId(networkConfig.RPC); try { console.log("Chain Id:", chainId); console.log(); } catch (err) { - console.log("Could not connect to RPC endpoint", rpc); + console.log("Could not connect to RPC endpoint", networkConfig.RPC); process.exit(2); } @@ -48,19 +51,13 @@ const main = async () => { console.log("\nAnalyzing deployment transactions...\n"); for (const tx of parsedRun.transactions) { - const trace = await getTxInternalCalls(tx.hash, rpc); - - try { - await callTraceVerifier( - trace.result, - chainId, - artifacts, - buildInfos, - args.etherscanUrl, - args.etherscanApiKey, - ); - } catch (error) { - console.error("[Verification Error]", error); + const trace = await getTxInternalCalls(tx.hash, networkConfig.RPC); + for (const explorer of networkConfig.explorers) { + try { + await callTraceVerifier(trace.result, artifacts, buildInfos, explorer); + } catch (error) { + console.error("[Verification Error]", error); + } } } console.log("\n[catapulta-verify] Verification finished."); diff --git a/src/types.ts b/src/types.ts index 3fc5b81..4924750 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ export interface InputParams { broadcastPath: string; rpcUrl?: string; - etherscanUrl?: string; + explorerUrl?: string; etherscanApiKey?: string; help?: boolean; } diff --git a/src/utils/calltrace-verifier.ts b/src/utils/calltrace-verifier.ts index 5bdf760..54b4bb0 100644 --- a/src/utils/calltrace-verifier.ts +++ b/src/utils/calltrace-verifier.ts @@ -1,67 +1,50 @@ +import type { ExplorerConfig } from "../config"; import { checkIfVerified, checkVerificationStatus, submitVerification, waitTillVisible } from "./explorer-api"; import { getSettingsByArtifact } from "./foundry-ffi"; -import { delay } from "./misc"; +import { delay, renderExplorerUrl } from "./misc"; -export const callTraceVerifier = async ( - call: any, - chainId: number, - artifacts: any[], - buildInfos: any[], - etherscanUrl?: string, - etherscanApiKey?: string, -) => { +export const callTraceVerifier = async (call: any, artifacts: any[], buildInfos: any[], explorer: ExplorerConfig) => { const deployOpcodes = ["CREATE", "CREATE2"]; // Perform nested call tracing verification in each internal call if (call.calls) { for (const c of call.calls) { - await callTraceVerifier(c, chainId, artifacts, buildInfos, etherscanUrl, etherscanApiKey); + await callTraceVerifier(c, artifacts, buildInfos, explorer); } } if (!deployOpcodes.includes(call.type)) return; - await waitTillVisible(call.to, Number(chainId), etherscanUrl, etherscanApiKey); + await waitTillVisible(call.to, explorer); - const verified = await checkIfVerified(call.to, Number(chainId), etherscanUrl, etherscanApiKey); + const verified = await checkIfVerified(call.to, explorer); if (verified) { - console.log(`(${call.to}) is already verified, skipping.`); + console.log(`(${renderExplorerUrl(call.to, explorer)}) is already verified, skipping.`); return; } // if the tx has subdeployments get the deployed bytecode from the last deployment in order to // compare the bytecode and deployed bytecode strings, resulting in the constructor params - const verificationReq = await getSettingsByArtifact( - Number(chainId), - artifacts, - buildInfos, - call.input, - call.to, - etherscanApiKey, - ); + const verificationReq = await getSettingsByArtifact(artifacts, buildInfos, call.input, call.to, explorer.API_KEY); if (!verificationReq) { console.log("Couldn't get the params for the verification request"); return; } - const { - result: guid, - message, - status, - }: any = await submitVerification(verificationReq, Number(chainId), etherscanUrl); + const { result: guid, message, status }: any = await submitVerification(verificationReq, explorer.API_URL); if (!status || guid.includes("Max rate limit reached")) { - console.log(`Couldn't verify ${call.to} `, guid); + console.log(`Couldn't verify ${renderExplorerUrl(call.to, explorer)} `, guid); return; } - console.log(`Verifying contract ${call.to}, with guid: ${guid}`); + console.log(`Verifying contract ${renderExplorerUrl(call.to, explorer)}, with guid: ${guid}`); for (let i = 0; i < 30; i++) { await delay(1000); - const { status, message } = await checkVerificationStatus(guid, chainId, etherscanUrl, etherscanApiKey); + const { status, message } = await checkVerificationStatus(guid, explorer); if (status !== 2) { console.log(message); diff --git a/src/utils/explorer-api.ts b/src/utils/explorer-api.ts index e99f393..aebcc84 100644 --- a/src/utils/explorer-api.ts +++ b/src/utils/explorer-api.ts @@ -1,7 +1,7 @@ -import { ETHERSCAN_API_KEYS, ETHERSCAN_API_URL, VERBOSE } from "../config"; +import { type ExplorerConfig, VERBOSE } from "../config"; import { delay } from "./misc"; -export const submitVerification = async (verificationInfo: any, chainId: number, etherscanUrl?: string) => { +export const submitVerification = async (verificationInfo: any, explorerUrl: string) => { const params = { method: "POST", headers: { @@ -11,20 +11,15 @@ export const submitVerification = async (verificationInfo: any, chainId: number, }; // etherscan is not super stable, therefore sometimes there are issues and for reporting it's useful to log the request if (VERBOSE) console.log("etherscan request", JSON.stringify(params)); - const request = await fetch(etherscanUrl || ETHERSCAN_API_URL[chainId], params); + const request = await fetch(explorerUrl, params); return await request.json(); }; // checks if the smart contract is already verified before trying to verify it -export const checkIfVerified = async ( - deploymentAddress: string, - chainId: number, - etherscanApi?: string, - etherscanApiKey?: string, -) => { +export const checkIfVerified = async (deploymentAddress: string, explorer: ExplorerConfig) => { const params = { - apikey: etherscanApiKey || ETHERSCAN_API_KEYS[chainId], + apikey: explorer.API_KEY || "", address: deploymentAddress, module: "contract", action: "getabi", @@ -33,7 +28,7 @@ export const checkIfVerified = async ( const formattedParams = new URLSearchParams(params).toString(); try { - const request = await fetch(`${etherscanApi || ETHERSCAN_API_URL[chainId]}?${formattedParams}`); + const request = await fetch(`${explorer.API_URL}?${formattedParams}`); const { status, result }: any = await request.json(); @@ -47,14 +42,9 @@ export const checkIfVerified = async ( } }; -export const checkIfVisible = async ( - deploymentAddress: string, - chainId: number, - etherscanApiUrl?: string, - etherscanApiKey?: string, -) => { +export const checkIfVisible = async (deploymentAddress: string, explorer: ExplorerConfig) => { const params = { - apikey: etherscanApiKey || ETHERSCAN_API_KEYS[chainId], + apikey: explorer.API_KEY || "", contractaddresses: deploymentAddress, module: "contract", action: "getcontractcreation", @@ -63,7 +53,7 @@ export const checkIfVisible = async ( const formattedParams = new URLSearchParams(params).toString(); await delay(100); - const request = await fetch(`${etherscanApiUrl || ETHERSCAN_API_URL[chainId]}?${formattedParams}`); + const request = await fetch(`${explorer.API_URL}?${formattedParams}`); const { result }: any = await request.json(); return Boolean(result); }; @@ -71,17 +61,12 @@ export const checkIfVisible = async ( /* Etherscan needs time to process the deployment, depending of the network load could take more or less time. */ -export const waitTillVisible = async ( - deploymentAddress: string, - chainId: number, - etherscanUrl?: string, - etherscanApiKey?: string, -): Promise => { +export const waitTillVisible = async (deploymentAddress: string, explorer: ExplorerConfig): Promise => { let visible = false; let logged = false; while (!visible) { - visible = await checkIfVisible(deploymentAddress, chainId, etherscanUrl, etherscanApiKey); + visible = await checkIfVisible(deploymentAddress, explorer); if (!visible) { if (!logged) { console.log("Waiting for on-chain settlement..."); @@ -100,15 +85,13 @@ export const waitTillVisible = async ( */ export const checkVerificationStatus = async ( GUID: string, - chainId: number, - etherscanUrl?: string, - apiKey?: string, + explorer: ExplorerConfig, ): Promise<{ status: number; message: string; }> => { const params = { - apikey: apiKey || ETHERSCAN_API_KEYS[chainId], + apikey: explorer.API_KEY || "", guid: GUID, module: "contract", action: "checkverifystatus", @@ -117,7 +100,7 @@ export const checkVerificationStatus = async ( const formattedParams = new URLSearchParams(params).toString(); try { - const request = await fetch(`${etherscanUrl || ETHERSCAN_API_URL[chainId]}?${formattedParams}`); + const request = await fetch(`${explorer.API_URL}?${formattedParams}`); const { status, result }: any = await request.json(); if (result === "Pending in queue") { diff --git a/src/utils/foundry-ffi.ts b/src/utils/foundry-ffi.ts index 5b04513..563144e 100644 --- a/src/utils/foundry-ffi.ts +++ b/src/utils/foundry-ffi.ts @@ -1,7 +1,6 @@ import { execSync } from "node:child_process"; import { existsSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; -import { ETHERSCAN_API_KEYS } from "../config.ts"; import type { BroadcastReport, EtherscanVerification } from "../types.ts"; import { getContractDataByArtifactAndBuildInfo, isBytecodeInArtifact, isBytecodeInBuildInfo } from "./bytecode.ts"; import { loadJson } from "./json.ts"; @@ -47,7 +46,7 @@ export const loadBuildInfo = async (parsedRun: BroadcastReport): Promise execSync("cp .env .env.bk && sed -i.sedbak -r '/FOUNDRY_LIBRARIES/d' .env && rm .env.sedbak && sleep 1"); } - let forgeBuildCmd = "forge build --skip test script --build-info"; + let forgeBuildCmd = "forge build --skip test script --force --build-info"; if (parsedRun?.libraries?.length) { forgeBuildCmd = `FOUNDRY_LIBRARIES="${parsedRun.libraries.join(",")}" ${forgeBuildCmd}`; @@ -77,7 +76,6 @@ export const loadBuildInfo = async (parsedRun: BroadcastReport): Promise }; export const getSettingsByArtifact = async ( - chainId: number, artifacts: any[], buildInfos: any[], bytecodeAndParams: string, @@ -115,7 +113,7 @@ export const getSettingsByArtifact = async ( const rawParams = bytecodeAndParams.split(foundArtifact.bytecode.object)[1] || ""; const metadata = foundArtifact.metadata; - settings.apikey = etherscanApiKey || ETHERSCAN_API_KEYS[chainId] || process.env.ETHERSCAN_API_KEY || ""; + settings.apikey = etherscanApiKey || ""; settings.module = "contract"; settings.action = "verifysourcecode"; settings.sourceCode = JSON.stringify(contractData.contractInfo); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 04a1cd4..f96d369 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,3 +1,5 @@ +import type { ExplorerConfig } from "../config"; + export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -11,3 +13,8 @@ export function isHex(str: string) { return false; } + +export function renderExplorerUrl(address: string, explorer: ExplorerConfig) { + if (!explorer.SITE_URL) return address; + return `${explorer.SITE_URL}/address/${address}`; +}