diff --git a/AGENTS.md b/AGENTS.md index 25ba062..d67d8fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,10 +25,9 @@ Async Python wrapper for blockchain explorer APIs (Etherscan, BSCScan, PolygonSc ## Supported Networks ### Primary Scanner Providers -- **Etherscan API**: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Fantom, Gnosis, and more EVM chains +- **Etherscan API**: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Fantom, Gnosis, and more EVM chains (Base supported via Etherscan V2) - **BlockScout**: Public blockchain explorers (no API key required) - Ethereum, Sepolia, Gnosis, Polygon, Optimism, Arbitrum, Base, Scroll, Linea, and others - **Moralis**: Multi-chain Web3 API - Ethereum, BSC, Polygon, Arbitrum, Base, Optimism, Avalanche, and more -- **BaseScan**: Base network explorer (Etherscan-compatible) - **RoutScan**: Mode network explorer ### Key Features by Provider @@ -51,7 +50,6 @@ aiochainscan/ │ ├── etherscan_v2.py # Etherscan API v2 │ ├── blockscout_v1.py # BlockScout implementation │ ├── moralis_v1.py # Moralis Web3 API -│ ├── basescan_v1.py # BaseScan implementation │ └── routscan_v1.py # RoutScan implementation ├── adapters/ # Hexagonal architecture adapters │ ├── aiohttp_client.py @@ -112,6 +110,10 @@ logs = await client.call(Method.EVENT_LOGS, address='0x...', **params) # Easy scanner switching - same interface for all! client = ChainscanClient.from_config('etherscan', 'v2', 'eth', 'main') balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...') + +# Use Base network through Etherscan V2 (chain_id 8453) +client = ChainscanClient.from_config('etherscan', 'v2', 'base', 'main') +balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...') ``` ### Legacy Facade API (Backward Compatibility) diff --git a/FETCH_HEAD b/FETCH_HEAD deleted file mode 100644 index e69de29..0000000 diff --git a/QUICK_START_RU.md b/QUICK_START_RU.md deleted file mode 100644 index 8cebf36..0000000 --- a/QUICK_START_RU.md +++ /dev/null @@ -1,239 +0,0 @@ -# Быстрый старт для aiochainscan - -## ✅ Проблема решена! - -Критическая проблема с установкой библиотеки **полностью исправлена**. Теперь установка работает корректно. - -## Установка - -### Способ 1: Из GitHub (рекомендуется) -```bash -pip install git+https://github.com/VaitaR/aiochainscan.git -``` - -### Способ 2: Клонировать и установить -```bash -git clone https://github.com/VaitaR/aiochainscan.git -cd aiochainscan -pip install . -``` - -### Способ 3: Режим разработки -```bash -git clone https://github.com/VaitaR/aiochainscan.git -cd aiochainscan -pip install -e ".[dev]" -``` - -## Проверка установки - -```bash -# Запустите скрипт проверки -python verify_installation.py -``` - -Или вручную: - -```python -import aiochainscan -print(f"Версия: {aiochainscan.__version__}") - -from aiochainscan import Client, get_balance, get_block -print("✓ Установка успешна!") -``` - -## Что было исправлено - -### До исправления ❌ -- `pip install git+https://...` устанавливал только метаданные -- Python модули не копировались в site-packages -- `import aiochainscan` выдавал `ModuleNotFoundError` -- Невозможно было использовать библиотеку - -### После исправления ✅ -- Все Python модули корректно устанавливаются -- Импорт работает из коробки -- CLI инструмент доступен -- Все зависимости установлены -- Работает на Python 3.10, 3.11, 3.12, 3.13 - -## Быстрый пример использования - -```python -import asyncio -from aiochainscan import get_balance, get_block_typed - -async def main(): - # Получить баланс адреса - balance = await get_balance( - address="0x742d35Cc6634C0532925a3b8D9fa7a3D91D1e9b3", - api_kind="eth", - network="main", - api_key="YOUR_API_KEY" - ) - print(f"Баланс: {balance} wei") - - # Получить информацию о блоке (типизированная версия) - block = await get_block_typed( - tag=17000000, - full=False, - api_kind="eth", - network="main", - api_key="YOUR_API_KEY" - ) - print(f"Блок #{block['block_number']}") - -asyncio.run(main()) -``` - -## Настройка API ключей - -```bash -# Создайте .env файл -export ETHERSCAN_KEY="ваш_ключ_etherscan" -export BSCSCAN_KEY="ваш_ключ_bscscan" -export POLYGONSCAN_KEY="ваш_ключ_polygonscan" -``` - -Или используйте CLI: -```bash -aiochainscan generate-env -# Отредактируйте сгенерированный .env файл -aiochainscan check -``` - -## Технические детали исправления - -### Изменена система сборки -```toml -# Было (не работало): -[build-system] -requires = ["maturin>=1.6,<2.0"] -build-backend = "maturin" - -# Стало (работает): -[build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" -``` - -### Созданы новые файлы -1. `setup.py` - явная конфигурация пакета -2. `MANIFEST.in` - список файлов для включения -3. `.github/workflows/test-install.yml` - CI тесты установки -4. `verify_installation.py` - скрипт проверки - -### Добавлена версия -```python -# В aiochainscan/__init__.py -__version__ = '0.2.1' -``` - -## Что включено в пакет - -- ✅ Все Python модули (`aiochainscan/**/*.py`) -- ✅ Основные компоненты: `client`, `config`, `network` -- ✅ Унифицированная архитектура: `core/`, `scanners/` -- ✅ Гексагональная архитектура: `domain/`, `ports/`, `adapters/`, `services/` -- ✅ Легаси модули: `modules/` (account, block, contract, и т.д.) -- ✅ Тайп-хинты: `py.typed`, `*.pyi` -- ✅ CLI утилита: команда `aiochainscan` -- ✅ Все зависимости - -## Опциональный Rust декодер (быстрый) - -Rust расширение теперь действительно опционально: - -```bash -# Сначала установите базовый пакет -pip install git+https://github.com/VaitaR/aiochainscan.git - -# Затем опционально соберите Rust расширение -pip install maturin -maturin develop --manifest-path aiochainscan/fastabi/Cargo.toml -``` - -**Требования:** -- Rust toolchain (https://rustup.rs) -- maturin - -**Преимущества:** -- 🚀 В 10-100 раз быстрее декодирование ABI -- 🔄 Автоматический откат на Python, если Rust недоступен - -## Поддерживаемые блокчейны - -- Ethereum (основная сеть + тестовые) -- BSC (BNB Smart Chain) -- Polygon -- Arbitrum -- Optimism -- Base -- И многие другие (15+ сетей) - -## Решение проблем - -### Если всё ещё получаете ошибку импорта: - -```bash -# Удалите старую версию -pip uninstall aiochainscan - -# Очистите кеш pip -pip cache purge - -# Установите заново -pip install --no-cache-dir git+https://github.com/VaitaR/aiochainscan.git - -# Проверьте -python -c "import aiochainscan; print(aiochainscan.__version__)" -``` - -### Проверьте расположение пакета: - -```python -import aiochainscan -print(aiochainscan.__file__) -# Должно показать: .../site-packages/aiochainscan/__init__.py -``` - -## Автоматическое тестирование - -CI автоматически проверяет: -- ✅ Установку wheel (Python 3.10-3.13) -- ✅ Установку source distribution -- ✅ Editable install -- ✅ Прямую установку из git -- ✅ Целостность структуры пакета -- ✅ Работоспособность импортов - -## Документация - -- **README.md** - Полная документация на английском -- **instructions.md** - Техническая документация проекта -- **PYPI_PUBLISHING.md** - Руководство по публикации в PyPI -- **INSTALLATION_FIX_SUMMARY.md** - Подробное описание исправления - -## Вопросы и поддержка - -- GitHub Issues: https://github.com/VaitaR/aiochainscan/issues -- Запустите `python verify_installation.py` для диагностики -- Проверьте результаты CI для вашей платформы - -## Сравнение: до и после - -| Аспект | До | После | -|--------|-----|--------| -| Система сборки | maturin (только Rust) | setuptools (Python + Rust опционально) | -| Установка из GitHub | ❌ Не работала | ✅ Работает | -| Python файлы в пакете | ❌ Нет | ✅ Да (все модули) | -| `import aiochainscan` | ❌ ModuleNotFoundError | ✅ Работает | -| CI тесты установки | ❌ Нет | ✅ Да (4 метода, 4 версии Python) | -| Готово для PyPI | ❌ Нет | ✅ Да | -| Rust расширение | ❌ Обязательно | ✅ Опционально | - ---- - -**Исправлено:** 2025-10-08 -**Версия:** 0.2.1 -**Статус:** ✅ Готово к использованию и публикации в PyPI diff --git a/README.md b/README.md index 4f86712..5bc85a7 100755 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Provides a single, consistent API for accessing blockchain data across multiple ## Supported Networks -**Etherscan API**: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Fantom, Gnosis, and more +**Etherscan API**: Ethereum, BSC, Polygon, Arbitrum, Optimism, Base, Fantom, Gnosis, and more EVM chains (Base supported via Etherscan V2) **Blockscout**: Public blockchain explorers (no API key needed) - Sepolia, Gnosis, Polygon, and others **Moralis**: Multi-chain Web3 API - Ethereum, BSC, Polygon, Arbitrum, Base, Optimism, Avalanche @@ -64,13 +64,18 @@ async def main(): balance = await client.call(Method.ACCOUNT_BALANCE, address='0x742d35Cc6634C0532925a3b8D9fa7a3D91D1e9b3') print(f"Balance: {balance} wei ({int(balance) / 10**18:.6f} ETH)") - # Switch to Etherscan easily (requires API key) - client = ChainscanClient.from_config('etherscan', 'v2', 'eth', 'main') - block = await client.call(Method.BLOCK_BY_NUMBER, block_number='latest') - print(f"Latest block: #{block['number']}") +# Switch to Etherscan easily (requires API key) +client = ChainscanClient.from_config('etherscan', 'v2', 'eth', 'main') +block = await client.call(Method.BLOCK_BY_NUMBER, block_number='latest') +print(f"Latest block: #{block['number']}") + +# Use Base network through Etherscan V2 (requires ETHERSCAN_KEY) +client = ChainscanClient.from_config('etherscan', 'v2', 'base', 'main') +balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...') +print(f"Base balance: {balance} wei") - # Same interface for any scanner! - await client.close() +# Same interface for any scanner! +await client.close() asyncio.run(main()) ``` diff --git a/aiochainscan/config.py b/aiochainscan/config.py index 020e123..388e3ff 100644 --- a/aiochainscan/config.py +++ b/aiochainscan/config.py @@ -162,13 +162,6 @@ def _init_builtin_scanners(self) -> None: requires_api_key=False, special_config={'subdomain_pattern': 'flare-explorer'}, ), - 'base': ScannerConfig( - name='BaseScan', - base_domain='basescan.org', - currency='BASE', - supported_networks={'main', 'goerli', 'sepolia'}, - requires_api_key=True, - ), 'linea': ScannerConfig( name='LineaScan', base_domain='lineascan.build', @@ -183,6 +176,14 @@ def _init_builtin_scanners(self) -> None: supported_networks={'main', 'sepolia'}, requires_api_key=True, ), + 'base': ScannerConfig( + name='Etherscan (Base)', + base_domain='etherscan.io', + currency='BASE', + supported_networks={'main', 'goerli', 'sepolia'}, + requires_api_key=True, + special_config={'etherscan_v2': True}, # Use Etherscan V2 for Base + ), 'blockscout_eth': ScannerConfig( name='BlockScout Ethereum', base_domain='eth.blockscout.com', diff --git a/aiochainscan/scanners/__init__.py b/aiochainscan/scanners/__init__.py index c635ab0..9d3293a 100644 --- a/aiochainscan/scanners/__init__.py +++ b/aiochainscan/scanners/__init__.py @@ -71,7 +71,6 @@ def list_scanners() -> dict[tuple[str, str], type[Scanner]]: # Import scanner implementations to trigger registration # This must be done after register_scanner is defined to avoid circular imports -from .basescan_v1 import BaseScanV1 # noqa: E402 from .blockscout_v1 import BlockScoutV1 # noqa: E402 from .etherscan_v2 import EtherscanV2 # noqa: E402 from .moralis_v1 import MoralisV1 # noqa: E402 @@ -84,7 +83,6 @@ def list_scanners() -> dict[tuple[str, str], type[Scanner]]: 'list_scanners', 'EtherscanV2', 'RoutScanV1', - 'BaseScanV1', 'BlockScoutV1', 'MoralisV1', ] diff --git a/aiochainscan/scanners/basescan_v1.py b/aiochainscan/scanners/basescan_v1.py deleted file mode 100644 index 1dc4eee..0000000 --- a/aiochainscan/scanners/basescan_v1.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -BaseScan API v1 scanner implementation. - -BaseScan uses the legacy Etherscan-style API structure -with a different domain (basescan.org instead of etherscan.io). -""" - -from . import register_scanner -from ._etherscan_like import EtherscanLikeScanner - - -@register_scanner -class BaseScanV1(EtherscanLikeScanner): - """ - BaseScan API v1 implementation. - - Inherits all functionality from the shared Etherscan-like base since BaseScan uses - the same endpoint layout, just with basescan.org domain. - - Supports Base network and its testnets: - - main: Base mainnet - - goerli: Base Goerli testnet - - sepolia: Base Sepolia testnet - """ - - name = 'basescan' - version = 'v1' - supported_networks = {'main', 'goerli', 'sepolia'} - - # All SPECS are inherited from the shared Etherscan-like implementation. - # Auth settings are also inherited: - # - auth_mode = "query" - # - auth_field = "apikey" - # - # This means BaseScan automatically supports all 17 methods: - # - ACCOUNT_BALANCE, ACCOUNT_TRANSACTIONS, ACCOUNT_INTERNAL_TXS - # - ACCOUNT_ERC20_TRANSFERS, ACCOUNT_ERC721_TRANSFERS, ACCOUNT_ERC1155_TRANSFERS - # - TX_BY_HASH, TX_RECEIPT_STATUS, BLOCK_BY_NUMBER, BLOCK_REWARD - # - CONTRACT_ABI, CONTRACT_SOURCE, TOKEN_BALANCE, TOKEN_SUPPLY - # - GAS_ORACLE, EVENT_LOGS, ETH_SUPPLY, ETH_PRICE, PROXY_ETH_CALL diff --git a/aiochainscan/scanners/etherscan_v2.py b/aiochainscan/scanners/etherscan_v2.py index d680ba1..e048b56 100644 --- a/aiochainscan/scanners/etherscan_v2.py +++ b/aiochainscan/scanners/etherscan_v2.py @@ -30,8 +30,8 @@ class EtherscanV2(Scanner): 'optimism', 'base', } - auth_mode = 'header' - auth_field = 'X-API-Key' + auth_mode = 'query' + auth_field = 'apikey' SPECS = { Method.ACCOUNT_BALANCE: EndpointSpec( diff --git a/aiochainscan/url_builder.py b/aiochainscan/url_builder.py index 8c00a4b..3f76e55 100755 --- a/aiochainscan/url_builder.py +++ b/aiochainscan/url_builder.py @@ -19,7 +19,7 @@ class UrlBuilder: 'mode': ('routescan.io/v2/network/mainnet/evm/34443/etherscan', 'MODE'), 'linea': ('lineascan.build', 'LINEA'), 'blast': ('blastscan.io', 'BLAST'), - 'base': ('basescan.org', 'BASE'), + 'base': ('etherscan.io', 'BASE'), # Base network via Etherscan V2 'routscan_mode': ('api.routescan.io/v2/network/mainnet/evm/34443', 'ETH'), 'blockscout_eth': ('eth.blockscout.com', 'ETH'), 'blockscout_sepolia': ('eth-sepolia.blockscout.com', 'ETH'), @@ -28,6 +28,8 @@ class UrlBuilder: 'moralis': ('deep-index.moralis.io', 'Multi-chain'), } + # API kinds that use Etherscan V2 header-based authentication. + # IMPORTANT: Only Etherscan-compatible V2 endpoints should be listed here. _HEADER_AUTH_API_KINDS = {'eth', 'optimism', 'arbitrum', 'bsc', 'polygon', 'base'} _CHAIN_ID_MAP = { @@ -133,13 +135,22 @@ def _get_api_url(self) -> str: elif self._api_kind.startswith('blockscout_'): prefix = None # BlockScout uses direct /api path + # Default path path = 'api' + + # Etherscan V2 header-auth APIs use unified domain api.etherscan.io + # and fixed 'api' prefix regardless of network, with path 'v2/api'. if self._api_kind in self._HEADER_AUTH_API_KINDS: + prefix = 'api' # force unified api prefix path = 'v2/api' return self._build_url(prefix, path) def _get_base_url(self) -> str: + # Etherscan V2 API uses unified domain etherscan.io for all networks + if self._api_kind in self._HEADER_AUTH_API_KINDS: + return 'https://etherscan.io' + network_exceptions = {('polygon', 'testnet'): 'mumbai'} network = network_exceptions.get((self._api_kind, self._network), self._network) diff --git a/setup.py b/setup.py deleted file mode 100644 index 2ffd1fb..0000000 --- a/setup.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -"""Setup script for aiochainscan package. - -This setup.py provides backward compatibility and explicit configuration -for the aiochainscan Python package. The primary build configuration is -in pyproject.toml using setuptools backend. - -For the optional Rust-based fast ABI decoder, use: - pip install aiochainscan[fast] - maturin develop --manifest-path aiochainscan/fastabi/Cargo.toml -""" - -from setuptools import find_packages, setup - -setup( - name='aiochainscan', - packages=find_packages(include=['aiochainscan', 'aiochainscan.*']), - include_package_data=True, - package_data={ - 'aiochainscan': [ - 'py.typed', - '*.pyi', - 'fastabi/Cargo.toml', - 'fastabi/Cargo.lock', - 'fastabi/src/*.rs', - ], - }, -) diff --git a/tests/test_e2e_balances_live.py b/tests/test_e2e_balances_live.py new file mode 100644 index 0000000..f03f517 --- /dev/null +++ b/tests/test_e2e_balances_live.py @@ -0,0 +1,161 @@ +""" +End-to-end live tests for ChainscanClient across multiple scanners. + +These tests perform real API calls against public providers. They use +API keys from the environment (.env is auto-loaded by config manager). + +Goal: +- Verify that ChainscanClient provides a unified interface +- Verify balance retrieval works for at least two chains per scanner +- Keep requests sequential to respect rate limits + +Notes: +- Providers that require API keys will be skipped automatically when + keys are not available in the environment. +- Balances may be "0" or large numbers depending on the chain; we only + assert that the value is returned without raising errors and is a + string or int convertible to int. +""" + +from __future__ import annotations + +import asyncio +import os +from typing import Any + +import pytest + +from aiochainscan.config import config as global_config +from aiochainscan.core.client import ChainscanClient +from aiochainscan.core.method import Method +from aiochainscan.exceptions import ChainscanClientApiError + +# Well-known EOA with activity on Ethereum mainnet (may be zero elsewhere) +TEST_ADDRESS: str = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' + + +def _has_api_key(scanner_id: str) -> bool: + """Return True if API key is configured or not required.""" + try: + cfg = global_config.get_scanner_config(scanner_id) + if not cfg.requires_api_key: + return True + # Attempt to resolve key via config manager (env/.env supported) + try: + key = global_config.get_api_key(scanner_id) + return bool(key) + except Exception: + return False + except Exception: + # Unknown scanner id + return False + + +def _has_etherscan_key() -> bool: + """Check if ETHERSCAN_KEY is available (used by Base and other Etherscan V2 scanners).""" + key = os.getenv('ETHERSCAN_KEY') + return bool(key and 'YOUR_' not in key and len(key) >= 10) + + +async def _assert_balance_ok(client: ChainscanClient, address: str) -> None: + """Call ACCOUNT_BALANCE and assert it returns a numeric-like value or string.""" + result: Any = await client.call(Method.ACCOUNT_BALANCE, address=address) + # Allow string or int; just ensure it can be converted to int + if isinstance(result, int): + assert result >= 0 + elif isinstance(result, str): + # Accept empty/None-like strings as provider-specific oddities, but prefer numeric + int(result or '0') # raises if not numeric + else: + # Some providers might wrap result in objects; try common fields + raise AssertionError(f'Unexpected balance type: {type(result)} -> {result}') + + +@pytest.mark.asyncio +async def test_blockscout_two_chains_live() -> None: + # BlockScout typically doesn't require API keys + tests = [ + ('blockscout', 'v1', 'blockscout_eth', 'eth'), + ('blockscout', 'v1', 'blockscout_polygon', 'polygon'), + ] + + for scanner_name, version, scanner_id, network in tests: + client = ChainscanClient.from_config(scanner_name, version, scanner_id, network) + await _assert_balance_ok(client, TEST_ADDRESS) + await client.close() + # Gentle pacing between providers + await asyncio.sleep(0.2) + + +@pytest.mark.asyncio +async def test_etherscan_two_chains_live() -> None: + # Requires ETHERSCAN_KEY in env + etherscan_key = os.getenv('ETHERSCAN_KEY') + if not etherscan_key or 'YOUR_' in etherscan_key or len(etherscan_key) < 10: + pytest.skip('ETHERSCAN_KEY not configured for live Etherscan tests') + + tests = [ + ('etherscan', 'v2', 'eth', 'main'), + ('etherscan', 'v2', 'arbitrum', 'main'), + ('etherscan', 'v2', 'base', 'main'), # Base network via Etherscan V2 + ] + + for scanner_name, version, scanner_id, network in tests: + # Etherscan V2 scanners use ETHERSCAN_KEY + if scanner_id in ('eth', 'arbitrum', 'bsc', 'polygon', 'optimism', 'base'): + if not _has_etherscan_key(): + pytest.skip(f'ETHERSCAN_KEY not configured for {scanner_id}') + elif not _has_api_key(scanner_id): + pytest.skip(f'Missing API key for {scanner_id}') + + client = ChainscanClient.from_config(scanner_name, version, scanner_id, network) + try: + await _assert_balance_ok(client, TEST_ADDRESS) + except ChainscanClientApiError as e: # pragma: no cover - live guardrail + # Gracefully skip if the environment key is invalid/rate-limited + msg = str(e) + if ( + 'Invalid API Key' in msg + or 'Missing/Invalid API Key' in msg + or 'rate limit' in msg.lower() + ): + pytest.skip(f'Etherscan live test skipped due to API key/limits: {msg}') + raise + finally: + await client.close() + await asyncio.sleep(0.2) + + +# Base network is now supported through Etherscan V2 with chain_id +# No need for separate BaseScan scanner + + +@pytest.mark.asyncio +async def test_moralis_two_chains_live() -> None: + # Moralis requires MORALIS_API_KEY + tests = [ + ('moralis', 'v1', 'moralis', 'eth'), + ('moralis', 'v1', 'moralis', 'arbitrum'), + ] + + for scanner_name, version, scanner_id, network in tests: + if not _has_api_key(scanner_id): + pytest.skip('Missing MORALIS_API_KEY') + client = ChainscanClient.from_config(scanner_name, version, scanner_id, network) + await _assert_balance_ok(client, TEST_ADDRESS) + await client.close() + await asyncio.sleep(0.2) + + +@pytest.mark.asyncio +async def test_routscan_mode_live() -> None: + # RoutScan supports Mode only (one network) + # RoutScan may not be registered in config in all environments; skip if unknown + try: + scanner_name, version, scanner_id, network = ('routscan', 'v1', 'routscan_mode', 'mode') + client = ChainscanClient.from_config(scanner_name, version, scanner_id, network) + except Exception as e: + pytest.skip(f'RoutScan not available in this build: {e}') + # Address may be zero on Mode; we still validate shape + await _assert_balance_ok(client, TEST_ADDRESS) + await client.close() diff --git a/tests/test_unified_client.py b/tests/test_unified_client.py index c6f1f51..f0747b7 100644 --- a/tests/test_unified_client.py +++ b/tests/test_unified_client.py @@ -296,14 +296,12 @@ class TestIntegrationWithExistingConfig: def test_scanner_registry_integration(self): """Test that scanners are properly registered.""" - # EtherscanV2 and BaseScanV1 should be registered + # EtherscanV2 should be registered (BaseScanV1 removed) etherscan_class = get_scanner_class('etherscan', 'v2') - basescan_class = get_scanner_class('basescan', 'v1') + # Base network now supported via Etherscan V2 assert etherscan_class is not None - assert basescan_class is not None assert etherscan_class.name == 'etherscan' - assert basescan_class.name == 'basescan' def test_unknown_scanner_error(self): """Test error for unknown scanner.""" diff --git a/tests/test_url_builder.py b/tests/test_url_builder.py index 70fb375..a5af79e 100755 --- a/tests/test_url_builder.py +++ b/tests/test_url_builder.py @@ -46,28 +46,22 @@ def test_query_param_auth(): @pytest.mark.parametrize( 'api_kind,network_name,expected', [ - # Etherscan V2 API - all use etherscan.io domain + # Etherscan V2 API - unified domain etherscan.io with chainid parameter # Reference: https://docs.etherscan.io/v2-migration ('eth', 'main', 'https://api.etherscan.io/v2/api'), - ('eth', 'ropsten', 'https://api-ropsten.etherscan.io/v2/api'), - ('eth', 'kovan', 'https://api-kovan.etherscan.io/v2/api'), - ('eth', 'rinkeby', 'https://api-rinkeby.etherscan.io/v2/api'), - ('eth', 'goerli', 'https://api-goerli.etherscan.io/v2/api'), - ('eth', 'sepolia', 'https://api-sepolia.etherscan.io/v2/api'), - # V2 Migration: BSC, Polygon, Arbitrum, Base now use etherscan.io ('bsc', 'main', 'https://api.etherscan.io/v2/api'), - ('bsc', 'testnet', 'https://api-testnet.etherscan.io/v2/api'), ('polygon', 'main', 'https://api.etherscan.io/v2/api'), - ('polygon', 'testnet', 'https://api-testnet.etherscan.io/v2/api'), - ('optimism', 'main', 'https://api-optimistic.etherscan.io/v2/api'), - ('optimism', 'goerli', 'https://api-goerli-optimistic.etherscan.io/v2/api'), + ('optimism', 'main', 'https://api.etherscan.io/v2/api'), ('arbitrum', 'main', 'https://api.etherscan.io/v2/api'), - ('arbitrum', 'nova', 'https://api-nova.etherscan.io/v2/api'), - ('arbitrum', 'goerli', 'https://api-goerli.etherscan.io/v2/api'), + ('base', 'main', 'https://api.etherscan.io/v2/api'), + # Test networks for main V2-compatible chains + ('eth', 'sepolia', 'https://api.etherscan.io/v2/api'), + ('polygon', 'mumbai', 'https://api.etherscan.io/v2/api'), # Non-V2 APIs still use their own domains ('fantom', 'main', 'https://api.ftmscan.com/api'), ('fantom', 'testnet', 'https://api-testnet.ftmscan.com/api'), - ('base', 'main', 'https://api.etherscan.io/v2/api'), + ('gnosis', 'main', 'https://api.gnosisscan.io/api'), + ('flare', 'main', 'https://flare-explorer.flare.network/api'), ], ) def test_api_url(api_kind, network_name, expected): @@ -78,26 +72,21 @@ def test_api_url(api_kind, network_name, expected): @pytest.mark.parametrize( 'api_kind,network_name,expected', [ - # Etherscan V2 API - all use etherscan.io domain + # Etherscan V2 API - unified domain etherscan.io ('eth', 'main', 'https://etherscan.io'), - ('eth', 'ropsten', 'https://ropsten.etherscan.io'), - ('eth', 'kovan', 'https://kovan.etherscan.io'), - ('eth', 'rinkeby', 'https://rinkeby.etherscan.io'), - ('eth', 'goerli', 'https://goerli.etherscan.io'), - ('eth', 'sepolia', 'https://sepolia.etherscan.io'), - # V2 Migration: BSC, Polygon, Arbitrum, Base now use etherscan.io + ('eth', 'sepolia', 'https://etherscan.io'), ('bsc', 'main', 'https://etherscan.io'), - ('bsc', 'testnet', 'https://testnet.etherscan.io'), + ('bsc', 'testnet', 'https://etherscan.io'), ('polygon', 'main', 'https://etherscan.io'), - ('polygon', 'testnet', 'https://mumbai.etherscan.io'), - ('optimism', 'main', 'https://optimistic.etherscan.io'), - ('optimism', 'goerli', 'https://goerli-optimism.etherscan.io'), + ('polygon', 'mumbai', 'https://etherscan.io'), + ('optimism', 'main', 'https://etherscan.io'), ('arbitrum', 'main', 'https://etherscan.io'), - ('arbitrum', 'nova', 'https://nova.etherscan.io'), - ('arbitrum', 'goerli', 'https://goerli.etherscan.io'), + ('base', 'main', 'https://etherscan.io'), # Non-V2 APIs still use their own domains ('fantom', 'main', 'https://ftmscan.com'), ('fantom', 'testnet', 'https://testnet.ftmscan.com'), + ('gnosis', 'main', 'https://gnosisscan.io'), + ('flare', 'main', 'https://flare.network'), ], ) def test_base_url(api_kind, network_name, expected): @@ -119,6 +108,7 @@ def test_invalid_api_kind(): ('polygon', 'MATIC'), ('optimism', 'ETH'), ('arbitrum', 'ETH'), + ('base', 'BASE'), ('fantom', 'FTM'), ], ) @@ -131,7 +121,7 @@ def test_currency(api_kind, expected): @pytest.mark.parametrize( 'api_kind,network,expected_base_contains,expected_api_contains', [ - # V2 Migration: Base now uses etherscan.io + # Base network via Etherscan V2 ('base', 'main', 'etherscan.io', 'https://api.etherscan.io/v2/api'), ( 'routscan_mode',