Skip to content

Dev#39

Merged
VaitaR merged 18 commits into
mainfrom
dev
Feb 23, 2026
Merged

Dev#39
VaitaR merged 18 commits into
mainfrom
dev

Conversation

@VaitaR
Copy link
Copy Markdown
Owner

@VaitaR VaitaR commented Feb 23, 2026

Note

High Risk
High risk because it removes the legacy Client/modules API and changes core HTTP/retry/rate-limit behavior, which can affect all runtime interactions and downstream integrations.

Overview
Modernizes the package’s runtime and developer tooling: bumps aiochainscan to 0.4.0, switches the default HTTP stack to httpx (HTTP/2) with new Network ownership in ChainscanClient, adds TenacityRetryAdapter/AioLimiterAdapter, expands Method (contract verification + token/NFT portfolio), and introduces Pydantic v2 DTOs in domain/dto_v2.py.

Removes legacy surfaces and hardens configuration: deletes the old Client and modules/*, updates CLI to use ChainscanClient + Method, makes config_manager.get_scanner_config() return a deep copy to avoid shared mutable state leakage, and improves decoding robustness (eth_utils.keccak + narrower exception handling).

Reworks CI/CD and contributor workflow: ci.yml no longer builds/publishes packages (and adds a fast import test early), deletes test-install.yml, adds release-only wheels.yml using cibuildwheel/maturin + trusted publishing, and updates docs/pre-commit hooks to run import tests and stricter local checks; also adds python -m aiochainscan entrypoint and an optional mcp_server integration.

Written by Cursor Bugbot for commit 923bee0. This will update automatically on new commits. Configure here.

…TenacityRetryAdapter

- Implemented tests for HttpxClientAdapter covering initialization, context management, GET/POST requests, error handling, and HTTP/2 support.
- Added tests for new API methods in Method enum, ensuring their existence and correct specifications in EtherscanV2 and EtherscanLikeScanner.
- Developed tests for TenacityRetryAdapter, verifying retry behavior, exception handling, exponential backoff timing, and callback functionality.
- Included tests for concurrent retries and integration with real exceptions from aiochainscan.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56f7a2f533

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread aiochainscan/scanners/blockscout_v2.py Outdated

from typing import TYPE_CHECKING, Any, ClassVar

import aiohttp
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove undeclared aiohttp import from BlockScout v2 scanner

This scanner has a hard top-level dependency on aiohttp, but the v0.4 dependency set no longer includes aiohttp; in a clean install, importing ChainscanClient loads aiochainscan.scanners and then blockscout_v2, which raises ModuleNotFoundError before any request is made. This makes the primary client API unusable unless users happen to have aiohttp installed transitively.

Useful? React with 👍 / 👎.

Comment thread aiochainscan/core/client.py Outdated
api_kind=api_kind, # Use mapped api_kind for UrlBuilder compatibility
network=network_str, # Use string version of network
api_key=client_config['api_key'],
network=network_str, # Use original network string for client property
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass normalized network to client constructor

The from_config path computes scanner-specific aliases (including blockscout_v2: main -> ethereum) but then constructs the client with network_str instead of the normalized value, so ChainscanClient.from_config('blockscout_v2', 'main') still initializes the scanner with main and fails network validation. This breaks the common/default main network input despite the alias table explicitly intending to support it.

Useful? React with 👍 / 👎.

Comment thread aiochainscan/services/analytics.py Outdated
{
'symbol': token_info.get('symbol', ''),
'name': token_info.get('name', ''),
'contract_address': token_info.get('address', ''),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read contract address from BlockScout token payload

The DataFrame export maps contract_address from token.address, but BlockScout V2 token payloads use address_hash (as documented in this commit’s scanner parser comments), so exported token portfolios lose contract addresses and produce empty values for that column. This silently corrupts downstream analytics/join workflows that rely on contract addresses.

Useful? React with 👍 / 👎.

Comment thread aiochainscan/adapters/memory_cache.py
Comment thread aiochainscan/adapters/orjson_parser.py
Comment thread aiochainscan/decode.py
Comment thread aiochainscan/core/client.py Outdated
Comment thread aiochainscan/adapters/memory_cache.py Outdated
…aiohttp handling; apply code formatting fixes across multiple files.
Comment thread aiochainscan/adapters/orjson_parser.py
- Added import tests to catch circular dependencies and import blockers early.
- Updated CI/CD workflow to run import tests before other checks.
- Enhanced pre-commit configuration to include import tests on every commit.
- Created setup script for developers to install dependencies and validate setup.
- Documented development setup and quality gates in README and CONTRIBUTING.md.
- Removed outdated PRE_COMMIT_FIXES.md and added detailed quality gates documentation.
Comment thread aiochainscan/adapters/__init__.py
- Change exception handling from ModuleNotFoundError to ImportError in aiohttp_client import.
- Add validation for max_size in InMemoryCache to ensure it is greater than 0.
- Modify eviction logic in InMemoryCache to prevent eviction when updating existing keys.
- Update orjson_parser to encode raw_str as UTF-8 before loading.
- Refactor ChainscanClient to use normalized network for scanner compatibility and improve pagination handling for BlockScout V2.
- Enhance error handling in transaction decoding to catch additional exceptions.
- Update analytics service to handle both Etherscan and BlockScout V2 contract address formats.
- Improve documentation in QUALITY_GATES.md and QUALITY_IMPROVEMENTS.md regarding pre-commit checks.
- Add eth-hash[pycryptodome] as a development dependency in pyproject.toml.
- Mock aiohttp imports in tests to avoid circular dependency issues.
- Introduce comprehensive tests for InMemoryCache to verify eviction logic and max_size validation.
) as response,
):
response.raise_for_status()
raw_response = await response.json()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New HTTP session created per page in pagination loop

High Severity

In iter_transactions, the BlockScout V2 pagination loop creates a new aiohttp.ClientSession() on every iteration of the while True loop. Each page of results opens a fresh TCP connection, destroying connection pooling. For wallets with many transactions, this means dozens of unnecessary TCP handshakes. The session also bypasses the client's configured self._network (with its rate limiting, retry logic, and httpx-based connection pool), using raw aiohttp instead.

Fix in Cursor Fix in Web

Comment thread aiochainscan/core/client.py
if parsed is not None:
# Etherscan returns Gwei, convert to wei
return parsed * 10**9
return None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GasOracleDTO silently drops decimal gas price values

High Severity

The _parse_gwei_to_wei validator uses parse_hex_or_int, which calls int() on the raw string. Etherscan's gas oracle API returns all gas prices as decimal Gwei strings (e.g., "0.496839934"). Since int("0.496839934") raises ValueError, parse_hex_or_int returns None, causing all four gas price fields (safe_gas_price, propose_gas_price, fast_gas_price, suggest_base_fee) to silently become None — a complete data loss.

Additional Locations (1)

Fix in Cursor Fix in Web

)
@classmethod
def _parse_hex_int_nullable(cls, v: Any) -> int | None:
return parse_hex_or_int(v)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable validator on non-nullable int fields causes ValidationError

Medium Severity

The _parse_hex_int_nullable validator (which returns int | None) is applied to gas and nonce fields that are typed as non-nullable int with default=0. In Pydantic V2, if the API provides an empty string or null for these fields, the validator returns None, and Pydantic raises a ValidationError since None is not valid for int. The sibling value field correctly uses parse_hex_or_int_zero for its validator. The gas and nonce fields need the same treatment to gracefully default to 0 instead of crashing.

Fix in Cursor Fix in Web

break

# Update query params with next_page_params for next iteration
query_params = {**query_params, **next_page_params}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing type validation for API pagination parameters

Medium Severity

The next_page_params from BlockScout V2 API response is unpacked with **next_page_params without validating it's a dictionary. If the API returns malformed data where next_page_params is a string, number, or other non-dict type, the unpacking operator raises TypeError: ... argument after ** must be a mapping, not <type>, crashing the iteration.

Fix in Cursor Fix in Web

Comment thread aiochainscan/core/client.py

__all__ = ['AiohttpClient', 'UrlBuilderEndpoint']
__all__ = [
'AiohttpClient',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AiohttpClient exports None when import fails

Medium Severity

When aiohttp is not installed, AiohttpClient is set to None but still exported in __all__. This allows from aiochainscan.adapters import AiohttpClient to succeed, giving users None instead of raising ImportError. When users try to instantiate it with AiohttpClient(), they get a confusing TypeError: 'NoneType' object is not callable instead of a clear import error indicating the missing dependency.

Fix in Cursor Fix in Web

Comment thread aiochainscan/network.py
limits=limits,
proxy=self._proxy,
)
return self._client
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition in Network client initialization

Medium Severity

The _ensure_client method lacks synchronization for concurrent access. When multiple coroutines call API methods simultaneously on the same ChainscanClient, they can race through the if self._client is None check, causing multiple httpx.AsyncClient instances to be created. Only the last assignment is retained, leaking the other client instances and their open connections.

Additional Locations (1)

Fix in Cursor Fix in Web

break

# Update query params with next_page_params for next iteration
query_params = {**query_params, **next_page_params}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iter_transactions imports aiohttp which is not a dependency

High Severity

The new iter_transactions method imports aiohttp at line 517 for the BlockScout V2 pagination path, but aiohttp is not listed in pyproject.toml dependencies (the project migrated to httpx). This will crash with ModuleNotFoundError at runtime for any user who calls iter_transactions on a BlockScout V2 client. The method also creates a new aiohttp.ClientSession inside the while True pagination loop, bypassing the client's own self._network layer (which provides rate limiting, retry, and connection pooling via httpx).

Fix in Cursor Fix in Web

- Add [tool.mypy] config in pyproject.toml with per-module overrides
- Set ignore_missing_imports=true for polars, aiohttp, mcp modules
- Enables CI to pass without installing optional dependencies
- Maintains full type checking when packages are installed
- All 353 tests passing, mypy clean locally

Fixes CI lint failures caused by missing optional dependencies
- Move polars import to TYPE_CHECKING block in analytics.py
- Move FastMCP import to TYPE_CHECKING block in mcp_server.py
- Use Any type with type:ignore for optional dependency fallbacks
- Remove unused type:ignore comments (now handled by TYPE_CHECKING)
- All decorators now properly typed without additional ignores

Mypy now passes both locally and in CI without optional dependencies
- Add mypy override to disable 'misc' error code in mcp_server module
- Prevents 'untyped decorator' errors when mcp package not installed
- Remove type:ignore comments from decorators (now handled by config)
- Works in both environments: with mcp (local) and without (CI)

This solves the issue where decorators are typed locally but untyped in CI
- Add Rust toolchain installation to all test jobs
- Install maturin>=1.8 before build in test-wheel-install job
- Install maturin>=1.8 in test-git-install job
- Install maturin>=1.8 in test-dependencies job
- Install maturin for editable install step

Fixes maturin 'Failed to write to zip archive' error in CI
Ensures ZIP64 support for all wheel builds in test workflows
Comment thread aiochainscan/core/client.py
Comment thread .github/workflows/test-install.yml Outdated
@VaitaR
Copy link
Copy Markdown
Owner Author

VaitaR commented Feb 23, 2026

@codex make review

Comment thread aiochainscan/cli.py
- Fixes Medium Severity issue found by Cursor Bugbot
- ETH_PRICE is not supported by all scanners (e.g., BlockScout V2)
- ACCOUNT_BALANCE is universally supported across all scanners
- Use Vitalik's address (0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045) for consistent testing
- Add regression test to verify universal method usage
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7dd1423a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread aiochainscan/core/client.py Outdated
@@ -160,34 +199,54 @@ def from_config(
# Use the original network parameter for config lookup, not the resolved chain name
# Ensure network is a string for config lookup
network_str = str(network) if not isinstance(network, str) else network
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve chain IDs to network names before using from_config

from_config advertises network as str | int and even documents chain-id usage, but this line stringifies numeric inputs and later passes that raw value into config/scanner resolution; e.g. from_config('etherscan', 8453) becomes network '8453', which fails network validation/supported-network checks instead of selecting Base. This breaks a documented initialization path for all numeric chain-id callers.

Useful? React with 👍 / 👎.

Comment thread aiochainscan/core/client.py Outdated
@@ -152,6 +190,7 @@ def from_config(
# For backward compatibility, map scanner names to their config IDs
scanner_id_map = {
'blockscout': 'blockscout_eth',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map BlockScout scanner config by requested network

This hardcoded mapping forces every from_config('blockscout', <network>) call through blockscout_eth, so non-ETH networks like polygon or gnosis fail config validation even though BlockScoutV1 itself supports them. As a result, the unified factory rejects valid BlockScout networks and prevents normal client creation outside Ethereum mainnet.

Useful? React with 👍 / 👎.

Comment thread aiochainscan/scanners/blockscout_v2.py Outdated
}

# Import aiohttp locally to avoid hard dependency at module level
import aiohttp
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid unconditional aiohttp import in BlockScout V2 calls

The V2 scanner imports aiohttp at call time, but aiohttp was removed from required package dependencies in this commit, so a default install can construct the client but then crashes with ModuleNotFoundError on the first blockscout_v2 request. Since BlockScout V2 is a primary usage path, this becomes a runtime break for normal users unless the transport is switched to declared dependencies (httpx) or aiohttp is re-declared.

Useful? React with 👍 / 👎.

1. Chain ID resolution in from_config:
   - from_config('etherscan', 8453) now correctly resolves to 'base'
   - Numeric chain IDs are converted to network names before config lookup
   - Prevents '8453' string being passed to network validation

2. BlockScout scanner config mapping:
   - Fixed hardcoded 'blockscout_eth' mapping that broke other networks
   - Now maps by actual network: polygon → blockscout_polygon, gnosis → blockscout_gnosis
   - Enables normal client creation for all BlockScout-supported networks

3. aiohttp dependency issue in BlockScout V2:
   - Replaced all aiohttp imports with httpx (declared dependency)
   - Prevents ModuleNotFoundError on first blockscout_v2 request
   - Updated exception handling to httpx.HTTPError/HTTPStatusError
   - Updated tests to mock httpx.AsyncClient instead of aiohttp.ClientSession

Tests:
- Added test_client_from_config_with_chain_id (chain ID → network name)
- Added test_client_from_config_blockscout_network_mapping (network-specific config)
- Updated test_call_balance/token_portfolio mocks for httpx
- All 357 tests passing ✅
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Comment thread aiochainscan/core/client.py
Comment thread aiochainscan/adapters/memory_cache.py
1. High Severity - Blockscout api_kind routing to wrong domain:
   - Fixed hardcoded 'blockscout_eth' api_kind for all networks
   - Now uses network-specific scanner_id (blockscout_polygon, blockscout_gnosis, etc.)
   - Ensures requests route to correct explorer domain for each network
   - Prevents returning incorrect data or failures from wrong domain

2. Low Severity - LRU cache evicting valid entries while expired remain:
   - InMemoryCache.set() now clears expired keys before LRU eviction
   - Prevents evicting still-valid entries when expired keys exist
   - More efficient cache usage - expired entries removed proactively

Tests:
- Added test_client_from_config_blockscout_api_kind_matches_network
- Added test_expired_keys_cleared_before_eviction
- All 359 tests passing ✅
@VaitaR VaitaR merged commit c6bb7d4 into main Feb 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant