From f10cee30d028f8a7413fc111d95b20e0f87a55e7 Mon Sep 17 00:00:00 2001 From: ZeKraken <79888567+zekraken-bot@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:30:10 -0400 Subject: [PATCH 1/3] Fall back to Etherscan V2 when Base/Optimism Blockscout deployment lookups fail. Adds a workflow_dispatch probe so we can verify the repo ETHERSCAN_API_KEY works for the issue #806 Base addresses. --- .github/workflows/probe-etherscan-base.yml | 17 ++++++++ scripts/probe-etherscan-base-deployment.ts | 46 ++++++++++++++++++++++ src/app.ts | 32 +++++++++++++-- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/probe-etherscan-base.yml create mode 100644 scripts/probe-etherscan-base-deployment.ts diff --git a/.github/workflows/probe-etherscan-base.yml b/.github/workflows/probe-etherscan-base.yml new file mode 100644 index 00000000..7fb839e2 --- /dev/null +++ b/.github/workflows/probe-etherscan-base.yml @@ -0,0 +1,17 @@ +name: probe etherscan base deployment +on: + workflow_dispatch: +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 00000000..a12dde20 --- /dev/null +++ b/scripts/probe-etherscan-base-deployment.ts @@ -0,0 +1,46 @@ +/** + * 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 main() { + const apiKey = process.env.ETHERSCAN_API_KEY + if (!apiKey) { + throw new Error('ETHERSCAN_API_KEY is not set') + } + + const api = new EtherscanApi(base, apiKey) + console.log(`Probing Etherscan V2 getcontractcreation on Base for ${ADDRESSES.length} addresses...`) + + 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 6d45abb5..81b173ae 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) From a897a7b9e8554dc20bb9cf66fae7b4078078e5d3 Mon Sep 17 00:00:00 2001 From: ZeKraken <79888567+zekraken-bot@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:31:18 -0400 Subject: [PATCH 2/3] Also run the Etherscan Base probe on pull requests. workflow_dispatch only works once the workflow exists on the default branch. --- .github/workflows/probe-etherscan-base.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/probe-etherscan-base.yml b/.github/workflows/probe-etherscan-base.yml index 7fb839e2..216060bb 100644 --- a/.github/workflows/probe-etherscan-base.yml +++ b/.github/workflows/probe-etherscan-base.yml @@ -1,6 +1,11 @@ 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 From 4456fa963d15b425d528303f21f987e2f0e02e58 Mon Sep 17 00:00:00 2001 From: ZeKraken <79888567+zekraken-bot@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:37:41 -0400 Subject: [PATCH 3/3] Log raw Etherscan V2 error details in the Base deployment probe. The first CI run failed with result.map is not a function; surface the API status/message instead. --- scripts/probe-etherscan-base-deployment.ts | 29 +++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/probe-etherscan-base-deployment.ts b/scripts/probe-etherscan-base-deployment.ts index a12dde20..59e4e5b1 100644 --- a/scripts/probe-etherscan-base-deployment.ts +++ b/scripts/probe-etherscan-base-deployment.ts @@ -16,15 +16,42 @@ const ADDRESSES = [ '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') } - const api = new EtherscanApi(base, apiKey) 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) {