diff --git a/.github/workflows/probe-etherscan-base.yml b/.github/workflows/probe-etherscan-base.yml new file mode 100644 index 0000000..216060b --- /dev/null +++ b/.github/workflows/probe-etherscan-base.yml @@ -0,0 +1,22 @@ +name: probe etherscan base deployment +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/probe-etherscan-base.yml' + - 'scripts/probe-etherscan-base-deployment.ts' + - 'src/app.ts' +jobs: + probe: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Probe Etherscan V2 Base getcontractcreation + run: npx tsx scripts/probe-etherscan-base-deployment.ts + env: + ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} diff --git a/scripts/probe-etherscan-base-deployment.ts b/scripts/probe-etherscan-base-deployment.ts new file mode 100644 index 0000000..59e4e5b --- /dev/null +++ b/scripts/probe-etherscan-base-deployment.ts @@ -0,0 +1,73 @@ +/** + * One-off probe: verify Etherscan V2 getcontractcreation works for Base + * with the addresses from issue #806 / the failing Blockscout batch. + * + * Usage (CI): npx tsx scripts/probe-etherscan-base-deployment.ts + */ +import { Address } from 'viem' +import { base } from 'viem/chains' +import EtherscanApi from '../src/services/etherscanApi' + +const ADDRESSES = [ + '0xb6862ada846003e66e2bc2de1e1acc4687f2ccb3', + '0x1be88d8b5696a69378973729523f3a135921854b', + '0x586d1d28d5f8aed81da8aef7016c4108f7a4ce51', // rate provider from issue #806 + '0xe32ba1c7546cf0793ce88ddcb7883d37fba83789', + '0x9c2dcdbdb3f0a0f628d1112bbcabd9ae75353df3', // asset from issue #806 +] as Address[] + +async function probeRaw(apiKey: string) { + const url = + `https://api.etherscan.io/v2/api?chainid=${base.id}` + + `&module=contract&action=getcontractcreation` + + `&contractaddresses=${ADDRESSES.join(',')}` + + `&apikey=${apiKey}` + + const response = await fetch(url) + const data = await response.json() + console.log(`HTTP ${response.status}`) + console.log(`status=${data.status} message=${data.message}`) + console.log(`result_type=${Array.isArray(data.result) ? 'array' : typeof data.result}`) + if (typeof data.result === 'string') { + console.log(`result=${data.result}`) + } else if (Array.isArray(data.result)) { + console.log(`result_len=${data.result.length}`) + } + return data +} + +async function main() { + const apiKey = process.env.ETHERSCAN_API_KEY + if (!apiKey) { + throw new Error('ETHERSCAN_API_KEY is not set') + } + + console.log(`Probing Etherscan V2 getcontractcreation on Base for ${ADDRESSES.length} addresses...`) + const raw = await probeRaw(apiKey) + + if (raw.status !== '1' || !Array.isArray(raw.result)) { + throw new Error( + `Etherscan V2 Base getcontractcreation failed: status=${raw.status} message=${raw.message} result=${typeof raw.result === 'string' ? raw.result : JSON.stringify(raw.result)?.slice(0, 200)}`, + ) + } + + const api = new EtherscanApi(base, apiKey) + const results = await api.getDeploymentTxHashAndBlock(ADDRESSES) + + for (const result of results) { + const ok = Boolean(result.deploymentTxHash) + console.log( + `${ok ? 'OK' : 'MISSING'} ${result.address} tx=${result.deploymentTxHash || 'n/a'} block=${result.blockNumber || 'n/a'}`, + ) + if (!ok) { + throw new Error(`Missing deployment tx for ${result.address}`) + } + } + + console.log(`SUCCESS: Etherscan V2 returned deployment data for all ${results.length} Base addresses`) +} + +main().catch((error) => { + console.error('PROBE FAILED:', error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/src/app.ts b/src/app.ts index 6d45abb..81b173a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -97,10 +97,36 @@ class RateProviderDataService { public async getDeploymentBlocks(addresses: Address[]): Promise<{ address: Address; deploymentTxHash: Hex }[]> { const chainApi = this.getApiForFunction('getDeploymentBlocks') - // The addresses length can be arbitrary but the etherscan API only allows 5 addresses at a time - // so we need to chunk the addresses and call the API sequentially + try { + return await this.fetchDeploymentBlocksFromApi(chainApi, addresses) + } catch (error) { + // Base/Optimism use Blockscout for getDeploymentBlocks, but that endpoint is often + // flaky (HTTP 500) or rate-limited. Fall back to Etherscan V2 when a key is available. + const canFallbackToEtherscan = + (this.chain.id === base.id || this.chain.id === optimism.id) && Boolean(this.apiKey) + + if (!canFallbackToEtherscan) { + throw error + } + + console.warn( + `Blockscout getDeploymentBlocks failed for ${this.chain.name}; falling back to Etherscan V2:`, + error instanceof Error ? error.message : error, + ) + return await this.fetchDeploymentBlocksFromApi(new EtherscanApi(this.chain, this.apiKey), addresses) + } + } + + /** + * Chunks addresses and fetches deployment info from the given explorer API. + * Explorer APIs typically allow at most 5 addresses per getcontractcreation call. + */ + private async fetchDeploymentBlocksFromApi( + chainApi: ChainApi, + addresses: Address[], + ): Promise<{ address: Address; deploymentTxHash: Hex }[]> { const chunkSize = 5 - const txHashes = [] + const txHashes: { address: Address; deploymentTxHash: Hex }[] = [] for (let i = 0; i < addresses.length; i += chunkSize) { const chunk = addresses.slice(i, i + chunkSize)