Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 14 additions & 24 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@ from aiochainscan.core.method import Method
client = ChainscanClient.from_config(
scanner_name='blockscout', # Provider name
scanner_version='v1', # API version
scanner_id='blockscout_eth', # Config identifier for Ethereum
network='eth' # Network name
network='ethereum' # Chain name/ID
)

# Use logical methods - scanner details hidden under the hood
Expand All @@ -116,17 +115,15 @@ logs = await client.call(Method.EVENT_LOGS, address='0x...', **params)
client = ChainscanClient.from_config(
scanner_name='etherscan', # Provider name
scanner_version='v2', # API version
scanner_id='eth', # Config identifier for Ethereum
network='main' # Mainnet
network='ethereum' # Chain name
)
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')

# Use Base network through Etherscan V2 (requires ETHERSCAN_KEY)
client = ChainscanClient.from_config(
scanner_name='etherscan', # Same provider
scanner_version='v2', # Same version
scanner_id='base', # Config identifier for Base network
network='main' # Mainnet
network='base' # Chain name
)
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
```
Expand Down Expand Up @@ -223,17 +220,11 @@ The **ChainscanClient** is the recommended interface because it provides:
### 🚀 **Easy Scanner Switching**
```python
# Switch from BlockScout to Etherscan with one line change
# Parameters: scanner_name, scanner_version, scanner_id, network
client = ChainscanClient.from_config('blockscout', 'v1', 'blockscout_eth', 'eth')
client = ChainscanClient.from_config('blockscout', 'v1', 'ethereum')
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')

# Same code works with Etherscan
client = ChainscanClient.from_config(
scanner_name='etherscan', # Provider name
scanner_version='v2', # API version
scanner_id='eth', # Config identifier for Ethereum
network='main' # Mainnet
)
client = ChainscanClient.from_config('etherscan', 'v2', 'ethereum')
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
```

Expand All @@ -250,22 +241,21 @@ balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')

## Configuration Parameters

When using `ChainscanClient.from_config()`, you need to specify four key parameters:
When using `ChainscanClient.from_config()`, you need to specify three key parameters:

- **scanner_name**: Provider name (`'etherscan'`, `'blockscout'`, `'moralis'`, etc.)
- **scanner_version**: API version (`'v1'`, `'v2'`)
- **scanner_id**: Configuration identifier for the specific network (`'eth'`, `'blockscout_eth'`, `'base'`, etc.)
- **network**: Network name (`'main'`, `'sepolia'`, `'polygon'`, etc.)
- **network**: Chain name/ID (`'eth'`, `'ethereum'`, `1`, `'base'`, `8453`, etc.)

### Common Configurations:

| Provider | scanner_name | scanner_version | scanner_id | network | API Key |
|----------|-------------|----------------|------------|---------|---------|
| **BlockScout Ethereum** | `'blockscout'` | `'v1'` | `'blockscout_eth'` | `'eth'` | ❌ Not required |
| **BlockScout Polygon** | `'blockscout'` | `'v1'` | `'blockscout_polygon'` | `'polygon'` | ❌ Not required |
| **Etherscan Ethereum** | `'etherscan'` | `'v2'` | `'eth'` | `'main'` | ✅ `ETHERSCAN_KEY` |
| **Etherscan Base** | `'etherscan'` | `'v2'` | `'base'` | `'main'` | ✅ `ETHERSCAN_KEY` |
| **Moralis Ethereum** | `'moralis'` | `'v1'` | `'moralis'` | `'eth'` | ✅ `MORALIS_API_KEY` |
| Provider | scanner_name | scanner_version | network | API Key |
|----------|-------------|----------------|---------|---------|
| **BlockScout Ethereum** | `'blockscout'` | `'v1'` | `'ethereum'` | ❌ Not required |
| **BlockScout Polygon** | `'blockscout'` | `'v1'` | `'polygon'` | ❌ Not required |
| **Etherscan Ethereum** | `'etherscan'` | `'v2'` | `'ethereum'` | ✅ `ETHERSCAN_KEY` |
| **Etherscan Base** | `'etherscan'` | `'v2'` | `'base'` | ✅ `ETHERSCAN_KEY` |
| **Moralis Ethereum** | `'moralis'` | `'v1'` | `'ethereum'` | ✅ `MORALIS_API_KEY` |

## Configuration

Expand Down
131 changes: 131 additions & 0 deletions ARCHITECTURE_REFACTOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Архитектурный рефакторинг ChainscanClient

## 🎯 Цель рефакторинга

Упростить и унифицировать интерфейс ChainscanClient, убрав двойственную логику между `scanner_id` и `network` параметрами.

## 📊 Анализ текущей архитектуры

### Текущие проблемы:
1. **Двойственная логика**: `scanner_id` и `network` дублируют информацию
2. **Разные схемы**: разные провайдеры используют параметры по-разному
3. **Сложность понимания**: `('blockscout', 'v1', 'blockscout_eth', 'eth')` неясно

### Выясненные особенности провайдеров:

#### Etherscan V2:
- **URL**: `https://api.etherscan.io/v2/api?chainid=1&module=account&action=balance`
- **Аутентификация**: query параметр `apikey`
- **Chain ID**: передается как `chainid` параметр
- **Единый домен**: `etherscan.io` для всех сетей

#### BlockScout:
- **URL**: `https://eth.blockscout.com/api?module=account&action=balance`
- **Аутентификация**: query параметр `apikey`
- **Instance domains**: разные для каждой сети (`eth.blockscout.com`, `base.blockscout.com`)
- **Network mapping**: `network='eth'` → instance=`eth.blockscout.com`

#### Moralis:
- **URL**: `https://deep-index.moralis.io/api/v2.2/0x1/native/balance/0x...`
- **Аутентификация**: header `X-API-Key`
- **Chain ID**: в hex формате в URL пути (`0x1`, `0x2105`)
- **RESTful API**: path-based параметры

## 🚀 Предлагаемая архитектура

### Унифицированные параметры:
```python
# Вместо 4 параметров - 3 основных
ChainscanClient.from_config(
scanner_name='blockscout', # Provider name
scanner_version='v1', # API version
network='eth' # Chain name/ID
)
```

### Автоматическое разрешение:
- `network='eth'` → chain_id=1, blockscout_instance='eth.blockscout.com'
- `network='base'` → chain_id=8453, blockscout_instance='base.blockscout.com'
- `network=1` → chain_id=1, blockscout_instance='eth.blockscout.com'

### Provider-specific логика:
- **Etherscan V2**: chain_id как query параметр `chainid`
- **BlockScout**: instance_domain для URL построения
- **Moralis**: chain_id_hex в URL пути

## 📋 Задачи по реализации

### ✅ Выполненные задачи:
1. **Создан chain_registry.py** - стандартизированные chain_id с алиасами
2. **Обновлен from_config** - принимает network как chain name/ID
3. **Обновлены сканеры** - принимают chain_id и используют его правильно
4. **Обновлены тесты** - отражают новую архитектуру
5. **Обновлена документация** - примеры с именованными параметрами

### 🔄 Текущие задачи:
1. **Обновить все примеры** - использовать именованные параметры
2. **Протестировать интеграцию** - убедиться что все провайдеры работают
3. **Обновить тесты** - проверить все сценарии

### 📝 Файлы для проверки:

#### Core компоненты:
- `aiochainscan/core/client.py` - from_config и ChainscanClient
- `aiochainscan/chain_registry.py` - стандартизированные chain_id
- `aiochainscan/scanners/base.py` - базовый сканер с chain_id
- `aiochainscan/scanners/etherscan_v2.py` - Etherscan с chain_id в query
- `aiochainscan/scanners/blockscout_v1.py` - BlockScout с instance_domain
- `aiochainscan/scanners/moralis_v1.py` - Moralis с chain_id_hex в пути

#### Документация:
- `README.md` - примеры с именованными параметрами
- `AGENTS.md` - обновленная архитектура
- `examples/*.py` - обновленные примеры

#### Тесты:
- `tests/test_*` - обновленные тесты
- `tests/test_e2e_*` - интеграционные тесты

## 🎯 Проверка корректности

### Тестовые сценарии:
1. **Etherscan V2 для Ethereum**: `ChainscanClient.from_config('etherscan', 'v2', 'ethereum')`
2. **Etherscan V2 для Base**: `ChainscanClient.from_config('etherscan', 'v2', 'base')`
3. **BlockScout для Ethereum**: `ChainscanClient.from_config('blockscout', 'v1', 'ethereum')`
4. **BlockScout для Polygon**: `ChainscanClient.from_config('blockscout', 'v1', 'polygon')`
5. **Moralis для Ethereum**: `ChainscanClient.from_config('moralis', 'v1', 'ethereum')`

### Что проверять:
- ✅ Клиенты создаются без ошибок
- ✅ Правильные URL генерируются
- ✅ Аутентификация работает
- ✅ Реальные API вызовы возвращают данные
- ✅ Chain ID правильно передается

## 🚀 Результат

**Новая унифицированная архитектура:**
```python
# Все провайдеры используют одинаковый интерфейс
client = ChainscanClient.from_config('blockscout', 'v1', 'ethereum') # BlockScout
client = ChainscanClient.from_config('etherscan', 'v2', 'ethereum') # Etherscan
client = ChainscanClient.from_config('moralis', 'v1', 'ethereum') # Moralis

# Работает с chain_id
client = ChainscanClient.from_config('etherscan', 'v2', 1) # Ethereum
client = ChainscanClient.from_config('blockscout', 'v1', 8453) # Base
```

**Преимущества:**
- ✅ **Единообразие** - одинаковый интерфейс для всех провайдеров
- ✅ **Простота** - меньше параметров, ясная семантика
- ✅ **Гибкость** - работает с chain names и chain IDs
- ✅ **Расширяемость** - легко добавлять новые провайдеры
- ✅ **Backward compatibility** - старый код продолжает работать

## 📈 Следующие шаги

1. **Тестирование** - убедиться что все провайдеры работают
2. **Документация** - обновить все примеры
3. **Мониторинг** - следить за работой в продакшене
4. **Оптимизация** - убрать legacy код когда будет готово
47 changes: 24 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ async def main():
client = ChainscanClient.from_config(
scanner_name='blockscout', # Provider name
scanner_version='v1', # API version
scanner_id='blockscout_eth', # Config identifier for Ethereum
network='eth' # Network name
network='ethereum' # Chain name/ID
)

# Use logical methods - scanner details hidden under the hood
Expand All @@ -72,8 +71,7 @@ async def main():
client = ChainscanClient.from_config(
scanner_name='etherscan', # Provider name
scanner_version='v2', # API version (V2 supports Base via chain_id)
scanner_id='eth', # Config identifier for Ethereum
network='main' # Mainnet
network='ethereum' # Chain name
)
block = await client.call(Method.BLOCK_BY_NUMBER, block_number='latest')
print(f"Latest block: #{block['number']}")
Expand All @@ -82,8 +80,7 @@ async def main():
client = ChainscanClient.from_config(
scanner_name='etherscan', # Same provider
scanner_version='v2', # Same version
scanner_id='base', # Config identifier for Base network
network='main' # Mainnet
network='base' # Chain name
)
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
print(f"Base balance: {balance} wei")
Expand Down Expand Up @@ -216,21 +213,20 @@ async def check_multi_scanner_balance():
# Same code works with any scanner - just change config!
scanners = [
# BlockScout (free, no API key needed)
('blockscout', 'v1', 'blockscout_eth', 'eth', ''),
('blockscout', 'v1', 'eth', ''),

# Etherscan (requires API key)
('etherscan', 'v2', 'eth', 'main', 'YOUR_ETHERSCAN_API_KEY'),
('etherscan', 'v2', 'eth', 'YOUR_ETHERSCAN_API_KEY'),

# Moralis (requires API key)
('moralis', 'v1', 'moralis', 'eth', 'YOUR_MORALIS_API_KEY'),
('moralis', 'v1', 'eth', 'YOUR_MORALIS_API_KEY'),
]

for scanner_name, version, api_kind, network, api_key in scanners:
for scanner_name, version, network, api_key in scanners:
try:
client = ChainscanClient.from_config(
scanner_name=scanner_name,
scanner_version=version,
scanner_id=api_kind,
network=network
)

Expand Down Expand Up @@ -291,22 +287,27 @@ export MORALIS_API_KEY="your_moralis_api_key"

## Configuration Parameters

When using `ChainscanClient.from_config()`, you need to specify four key parameters:
When using `ChainscanClient.from_config()`, you need to specify three key parameters:

- **scanner_name**: Provider name (`'etherscan'`, `'blockscout'`, `'moralis'`, etc.)
- **scanner_version**: API version (`'v1'`, `'v2'`)
- **scanner_id**: Configuration identifier for the specific network (`'eth'`, `'blockscout_eth'`, `'base'`, etc.)
- **network**: Network name (`'main'`, `'sepolia'`, `'polygon'`, etc.)
- **network**: Chain name/ID (`'eth'`, `'ethereum'`, `1`, `'base'`, `8453`, etc.)

### Common Configurations:

| Provider | scanner_name | scanner_version | scanner_id | network | API Key |
|----------|-------------|----------------|------------|---------|---------|
| **BlockScout Ethereum** | `'blockscout'` | `'v1'` | `'blockscout_eth'` | `'eth'` | ❌ Not required |
| **BlockScout Polygon** | `'blockscout'` | `'v1'` | `'blockscout_polygon'` | `'polygon'` | ❌ Not required |
| **Etherscan Ethereum** | `'etherscan'` | `'v2'` | `'eth'` | `'main'` | ✅ `ETHERSCAN_KEY` |
| **Etherscan Base** | `'etherscan'` | `'v2'` | `'base'` | `'main'` | ✅ `ETHERSCAN_KEY` |
| **Moralis Ethereum** | `'moralis'` | `'v1'` | `'moralis'` | `'eth'` | ✅ `MORALIS_API_KEY` |
| Provider | scanner_name | scanner_version | network | API Key |
|----------|-------------|----------------|---------|---------|
| **BlockScout Ethereum** | `'blockscout'` | `'v1'` | `'ethereum'` | ❌ Not required |
| **BlockScout Polygon** | `'blockscout'` | `'v1'` | `'polygon'` | ❌ Not required |
| **Etherscan Ethereum** | `'etherscan'` | `'v2'` | `'ethereum'` | ✅ `ETHERSCAN_KEY` |
| **Etherscan Base** | `'etherscan'` | `'v2'` | `'base'` | ✅ `ETHERSCAN_KEY` |
| **Moralis Ethereum** | `'moralis'` | `'v1'` | `'ethereum'` | ✅ `MORALIS_API_KEY` |

**Network parameter supports both names and chain IDs:**
- `'ethereum'`, `'eth'`, `1` - Ethereum
- `'base'`, `8453` - Base
- `'polygon'`, `'matic'` - Polygon
- `'bsc'`, `'binance'`, `56` - Binance Smart Chain

## Available Interfaces

Expand All @@ -321,15 +322,15 @@ from aiochainscan.core.client import ChainscanClient
from aiochainscan.core.method import Method

# Create client for any scanner
client = ChainscanClient.from_config('blockscout', 'v1', 'blockscout_eth', 'eth')
client = ChainscanClient.from_config('blockscout', 'v1', 'ethereum')

# Use logical methods - scanner details hidden
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
logs = await client.call(Method.EVENT_LOGS, address='0x...', **params)
block = await client.call(Method.BLOCK_BY_NUMBER, block_number='latest')

# Easy scanner switching - same interface!
client = ChainscanClient.from_config('etherscan', 'v2', 'eth', 'main')
client = ChainscanClient.from_config('etherscan', 'v2', 'ethereum')
balance = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
```

Expand Down
Loading