| name | add-explorer |
| description | Add support for a new blockchain explorer API to diffyscan. Use when a new chain or explorer type needs to be supported. |
| disable-model-invocation | true |
| argument-hint | ["explorer-name"] |
Guide through adding support for a new blockchain explorer to diffyscan.
Decision tree: do you need code changes?
Most new chains need NO code changes. Work through this list in order:
-
Etherscan v2 API (preferred) -- If the chain is listed on Etherscan's v2 supported chains, create a config with "explorer_hostname": "api.etherscan.io" and "explorer_chain_id": <chain-id>. Done. No code changes. This uses the endpoint https://api.etherscan.io/v2/api?chainid=<id>&module=contract&action=getsourcecode&address=<addr>.
-
Legacy Etherscan-compatible hostname -- If the chain has its own Etherscan-style API (e.g. api.basescan.org, api-optimistic.etherscan.io), create a config with just "explorer_hostname" (no explorer_chain_id). The default fetcher handles it. No code changes.
-
Existing Blockscout domain -- If the chain uses a Blockscout instance whose domain already ends with one of the recognized suffixes (see dispatcher below), create a config with that hostname. No code changes.
-
New Blockscout domain -- If the chain uses Blockscout but with an unrecognized domain, add the domain suffix to _get_explorer_fetcher(). One-line change.
-
Entirely new API format -- Only if the explorer has a non-Etherscan, non-Blockscout API do you need a new fetcher function.
Architecture: diffyscan/utils/explorer.py
All explorer logic lives in one file. The call flow is:
get_contract_from_explorer() # public entry point, handles caching
-> _get_explorer_fetcher() # dispatcher: hostname -> (fetcher_fn, requires_token)
-> fetcher(...) # one of the four fetchers below
-> _validate_contract_name() # verify name matches config
The dispatcher: _get_explorer_fetcher(explorer_hostname)
This function maps hostnames to fetcher functions using prefix/suffix matching. It returns a tuple of (fetcher_function, requires_token: bool).
Current routing rules (checked in order):
| Condition | Fetcher | Token required? |
|---|
hostname.startswith("zksync") | _get_contract_from_zksync | No |
hostname.endswith("mantle.xyz") | _get_contract_from_mantle | No |
hostname.endswith("lineascan.build") | _get_contract_from_etherscan (token forced to None) | No |
hostname.endswith(...) any of: mode.network, blockscout.com, swellnetwork.io, lisk.com, inkonchain.com, routescan.io, monadvision.com | _get_contract_from_blockscout | No |
| Default (everything else) | _get_contract_from_etherscan | Yes |
When requires_token is True, the caller passes (token, hostname, address, chain_id). When False, the caller passes (hostname, address) only.
Fetcher 1: _get_contract_from_etherscan(token, hostname, address, chain_id=None)
Handles both Etherscan v2 and legacy Etherscan APIs.
- v2 endpoint (when
chain_id is set): https://{hostname}/v2/api?chainid={chain_id}&module=contract&action=getsourcecode&address={address}&apikey={token}
- Legacy endpoint (when
chain_id is None): https://{hostname}/api?module=contract&action=getsourcecode&address={address}&apikey={token}
- Has built-in rate-limit retry logic (up to 5 retries with linear backoff)
- Response format:
{"message": "OK", "result": [{"ContractName": ..., "SourceCode": ..., "CompilerVersion": ..., ...}]}
- If
SourceCode starts with {{, it is treated as a JSON solc standard input (stripped of outer braces and parsed)
- Otherwise,
_build_source_files() + _build_solc_input() construct the solc input from flat source
- Limitation: Flattened (single-file) contracts and contracts verified without standard JSON input may produce incomplete solc inputs. The flat-source path creates a minimal solc input from the single source file plus any
AdditionalSources, but without the original compiler settings (e.g. remappings, via-IR). This can cause bytecode mismatches even when source diffs are clean.
Fetcher 2: _get_contract_from_blockscout(hostname, address)
- Endpoint:
https://{hostname}/api/v2/smart-contracts/{address}
- No API token needed
- Response is a flat JSON object with fields:
name, file_path, source_code, additional_sources (list of {file_path, source_code}), compiler_version, compiler_settings, optimization_enabled, optimization_runs, constructor_args, evm_version, external_libraries (list of {name, address, ...})
- Note: the response uses both
optimization_runs and optimizations_runs (typo in some Blockscout versions); the code checks both
Fetcher 3: _get_contract_from_zksync(hostname, address)
- Endpoint:
https://{hostname}/contract_verification/info/{address}
- No API token needed
- Response:
{"verifiedAt": ..., "request": {"ContractName": ..., "CompilerVersion": ..., "sourceCode": {"sources": ...}}}
- Returns a minimal contract dict with
name, sources, compiler -- does NOT go through _build_contract_payload or _attach_contract_metadata
- Outlier: Unlike the other fetchers, this does not return
solcInput. Downstream code (run_source_diff, run_bytecode_diff) expects contract["solcInput"], so zkSync contracts use a different code path. New fetchers should follow the standard shape returned by _build_contract_payload (name, compiler, solcInput, plus optional constructor_arguments, evm_version, libraries).
Fetcher 4: _get_contract_from_mantle(hostname, address)
- Endpoint:
https://{hostname}/api?module=contract&action=getsourcecode&address={address}
- No API token needed
- Etherscan-like response but uses
FileName (not ContractName) as primary path, and AdditionalSources list uses Filename/SourceCode keys (note capitalization differences)
Shared helpers
_build_source_files(primary_path, primary_source, additional_sources, *, path_key, content_key) -- Assembles a {path: {"content": source}} dict. The path_key and content_key parameters handle the field name differences between explorers.
_build_solc_input(source_files, *, optimizer_enabled, optimizer_runs, settings=None) -- Wraps source files into a standard solc JSON input with optimizer config and output selection.
_build_contract_payload(name, compiler, solc_input, *, constructor_arguments, evm_version, libraries) -- Assembles the final contract dict and calls _attach_contract_metadata().
_attach_contract_metadata(contract, source_files, constructor_arguments, evm_version, libraries) -- Normalizes and attaches constructor args (hex string without 0x prefix), EVM version (None if "default"), and libraries (resolved to {path: {name: address}} format). Libraries can come from explorer response OR from solcInput.settings.libraries; both are merged.
Steps: adding a new Etherscan v2 chain (no code changes)
- Find the chain ID (e.g. from chainlist.org)
- Create
config_samples/<chain>/<config>.json:
{
"contracts": { ... },
"explorer_hostname": "api.etherscan.io",
"explorer_token_env_var": "ETHERSCAN_EXPLORER_TOKEN",
"explorer_chain_id": <chain-id>,
"github_repo": { ... }
}
- Test:
uv run diffyscan <config> --yes --cache-explorer
Steps: adding a new Blockscout domain
- Add the domain suffix to the tuple in
_get_explorer_fetcher() in diffyscan/utils/explorer.py (the any(explorer_hostname.endswith(domain) for domain in [...]) block)
- Create a config with
"explorer_hostname": "<blockscout-hostname>"
- Test:
uv run diffyscan <config> --yes --cache-explorer
Steps: adding a completely new explorer type
-
Understand the API: Document the endpoint URL, response shape, and which fields map to contract name, source code, compiler version, optimizer settings, constructor args, EVM version, and libraries.
-
Add hostname detection in _get_explorer_fetcher(): Add a new elif branch with startswith() or endswith() matching. Return (your_fetcher, False) -- most non-Etherscan explorers do not use API tokens.
-
Implement the fetcher _get_contract_from_<name>(hostname, address):
- Call the explorer API via
fetch(url).json()
- Validate response (check for verification status, required fields)
- Use
_build_source_files() to assemble sources (pass the correct path_key/content_key for the response format)
- Use
_build_solc_input() to wrap into solc input
- Return via
_build_contract_payload() to get normalized metadata
- If the fetcher does not need a token, its signature should be
(hostname, address) (two args). If it does need a token, use (token, hostname, address, chain_id=None) (four args) and set requires_token=True in the dispatcher.
-
Add tests in tests/test_explorer_utils.py -- follow the existing pattern: monkeypatch fetch to return a DummyResponse, call get_contract_from_explorer(), assert the result.
-
Add a config sample in config_samples/<chain>/.
Config fields reference
| Field | Required | Description |
|---|
explorer_hostname | Yes | API hostname (e.g. api.etherscan.io, explorer.mode.network) |
explorer_chain_id | No | Chain ID for Etherscan v2 API; omit for legacy or non-Etherscan explorers |
explorer_token_env_var | No | Env var name holding the API key (e.g. ETHERSCAN_EXPLORER_TOKEN) |
Testing
- Unit tests:
tests/test_explorer_utils.py -- monkeypatch diffyscan.utils.explorer.fetch and CACHE_DIR
- Integration:
uv run diffyscan <config> --yes --cache-explorer
- The
--cache-explorer flag caches responses to .diffyscan_cache/ so repeated runs do not hit the API
Checklist