Skip to content

Commit 806c963

Browse files
authored
Merge pull request #42 from VaitaR/chore/remove-legacy
chore: remove legacy stack and finalize mixin-based client API
2 parents 602b03b + 524fc85 commit 806c963

84 files changed

Lines changed: 1457 additions & 17818 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-install.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jobs:
5555
# Verify main modules
5656
python -c "from aiochainscan import ChainscanClient, Method; print('✓ Main classes imported')"
5757
58-
# Verify facade imports
59-
python -c "from aiochainscan import get_balance, get_block, get_transaction; print('✓ Facades imported')"
58+
# Verify modern public API
59+
python -c "from aiochainscan import ChainscanClient, Method, SmartContract, DecodedEvent, DecodedTransaction; print('✓ Modern public API imported')"
6060
6161
# Verify CLI is available
6262
which aiochainscan || echo "⚠ CLI not found"
@@ -181,7 +181,7 @@ jobs:
181181
print(f'Package location: {pkg_path}')
182182
183183
# Check for key modules
184-
modules = ['client', 'config', 'network', 'core', 'services', 'adapters', 'ports', 'domain']
184+
modules = ['config', 'network', 'core', 'services', 'adapters', 'ports', 'domain']
185185
for mod in modules:
186186
mod_path = os.path.join(pkg_path, mod + '.py')
187187
dir_path = os.path.join(pkg_path, mod)

.importlinter

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ forbidden_modules =
1616
aiochainscan.services
1717
aiochainscan.adapters
1818
aiochainscan.core
19-
aiochainscan.modules
2019
aiochainscan.scanners
2120

2221
[importlinter:contract:2]
@@ -28,7 +27,6 @@ forbidden_modules =
2827
aiochainscan.services
2928
aiochainscan.adapters
3029
aiochainscan.core
31-
aiochainscan.modules
3230
aiochainscan.scanners
3331

3432
[importlinter:contract:3]

AGENTS.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ Async Python wrapper for blockchain explorer APIs (Etherscan, BlockScout). Unifi
1111

1212
## Quick Start for Agents
1313

14+
> Public API policy: use `ChainscanClient` only.
15+
> Legacy facade/context/url-builder entrypoints and old pagination-engine docs are removed from agent workflows.
16+
1417
### Primary Interface (USE THIS)
1518
```python
16-
from aiochainscan.core.client import ChainscanClient
19+
from aiochainscan import ChainscanClient
1720

1821
async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
1922
# ── Account ──────────────────────────────────────────────
2023
balance = await client.get_balance('0x...') # Wei string
2124
txs = await client.get_transactions('0x...') # single page
22-
all_txs = await client.get_all_transactions('0x...') # ALL (paginated)
25+
all_txs = await client.get_all_transactions('0x...') # ALL (streaming aggregation → list)
2326
itxs = await client.get_internal_transactions('0x...') # single page
2427
erc20 = await client.get_token_transfers('0x...') # single page
2528
erc721 = await client.get_erc721_transfers('0x...') # single page
@@ -56,7 +59,7 @@ async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
5659

5760
# ── Event Logs ───────────────────────────────────────────
5861
logs = await client.get_logs('0x...', from_block=0) # single page (≤1000)
59-
all_logs = await client.get_all_logs('0x...', from_block=0) # ALL (paginated)
62+
all_logs = await client.get_all_logs('0x...', from_block=0) # ALL (streaming aggregation → list)
6063

6164
# ── Proxy / JSON-RPC ─────────────────────────────────────
6265
result = await client.eth_call('0xTO', '0xDATA') # eth_call
@@ -82,11 +85,12 @@ async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
8285
### ⚠️ Key Gotchas
8386
- `get_transactions()` returns **one page** (~50-100 items). Use `get_all_transactions()` for complete data.
8487
- `get_logs()` returns **≤1000 logs**. Use `get_all_logs()` for complete data.
88+
- `get_all_*()` now uses **streaming aggregation** under the hood; for very large datasets prefer `iter_*_streaming()`.
8589
- `get_transactions_df()` auto-paginates (uses `iter_transactions` internally).
8690
- Balance/value/supply values are **Wei strings** — divide by `10**18` for ETH.
8791

8892
> **Note:** Legacy `Client` class and `modules/` were removed in v0.3.0.
89-
> Facade functions (`get_balance`, etc.) are **DEPRECATED** in v0.4.0 — use `ChainscanClient`.
93+
> Legacy facade/context/url-builder public entrypoints and old pagination-engine usage were purged in modern API docs.
9094
9195
---
9296

@@ -132,7 +136,7 @@ Every `Method` enum value (28 total) maps to typed convenience methods on `Chain
132136
| Pattern | Use When | Memory |
133137
|---|---|---|
134138
| `get_transactions(address)` | Quick look, small wallets | Low |
135-
| `get_all_transactions(address)` | Need ALL data, moderate wallets | Grows with data |
139+
| `get_all_transactions(address)` | Need ALL data (built via streaming aggregation) | Grows with data |
136140
| `iter_transactions_streaming(address)` | Large wallets (1M+ txs) | Constant ~10MB |
137141
| `get_transactions_df(address)` | Data analysis (Polars) | Grows with data |
138142

@@ -142,8 +146,8 @@ Every `Method` enum value (28 total) maps to typed convenience methods on `Chain
142146

143147
```
144148
┌─────────────────────────────────────────────────────────────┐
145-
FACADE LAYER
146-
│ core/client.py (ChainscanClient) | domain/contract.py │
149+
CLIENT / DOMAIN LAYER │
150+
│ core/client.py (ChainscanClient) | domain/contract.py
147151
└─────────────────────────┬───────────────────────────────────┘
148152
149153
┌─────────────────────────▼───────────────────────────────────┐
@@ -152,9 +156,9 @@ Every `Method` enum value (28 total) maps to typed convenience methods on `Chain
152156
└─────────────────────────┬───────────────────────────────────┘
153157
154158
┌─────────────────────────▼───────────────────────────────────┐
155-
SERVICE LAYER
156-
paging_engine.py | streaming_decoder.py | chunked_fetcher
157-
│ ens_resolver.py | unified_fetch.py | analytics.py
159+
AGGREGATION SERVICES
160+
account.py | logs.py | streaming_decoder.py | analytics.py
161+
│ ens_resolver.py | chunked_fetcher.py
158162
└─────────────────────────┬───────────────────────────────────┘
159163
160164
┌─────────────────────────▼───────────────────────────────────┐
@@ -262,9 +266,10 @@ Every `Method` enum value (28 total) maps to typed convenience methods on `Chain
262266
5. Register in `scanners/__init__.py`
263267

264268
### Adding Bulk Fetch Support
265-
1. Use `paging_engine.fetch_all_generic()` with `FetchSpec`
266-
2. For streaming: use `paging_streaming.fetch_all_generic_streaming()`
267-
3. Always pass `on_progress` callback through to engine
269+
1. Extend `ChainscanClient` methods and scanner `SPECS` first
270+
2. Keep `get_all_*` behavior as materialized results from streaming aggregation
271+
3. Add/maintain matching `iter_*_streaming` path for large datasets
272+
4. Always thread `on_progress` callbacks through public client methods
268273

269274
### Modifying HTTP Behavior
270275
- Rate limiting: `adapters/aiolimiter_adapter.py` (burst=1 for APIs)
@@ -299,7 +304,7 @@ async for batch in client.iter_transactions_streaming(address, batch_size=1000):
299304

300305
### Get ALL Data (Paginated)
301306
```python
302-
# These handle pagination automatically:
307+
# These use streaming aggregation internally and return materialized lists:
303308
all_txs = await client.get_all_transactions(address)
304309
all_logs = await client.get_all_logs(address, from_block=0, topic0='0xddf252...')
305310
all_transfers = await client.get_all_token_transfers(address)
@@ -310,9 +315,9 @@ all_internal = await client.get_all_internal_transactions(address)
310315
```python
311316
from aiochainscan.utils.progress_helpers import console_progress
312317

313-
txs = await fetch_all_transactions_fast(
314-
...,
315-
on_progress=console_progress() # Real-time feedback
318+
txs = await client.get_all_transactions(
319+
address,
320+
on_progress=console_progress(), # Real-time feedback
316321
)
317322
```
318323

README.md

Lines changed: 35 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Provides a single, consistent API for accessing blockchain data across multiple
1818
- **⚡ Built-in Rate Limiting** - Automatic throttling with configurable limits and retry policies
1919
- **🎯 Comprehensive API Coverage** - 28 blockchain operations with typed convenience methods
2020
- **🔒 Type-safe Operations** - Typed data transfer objects, method enums, 100% mypy --strict
21-
- **🚀 Optimized Bulk Operations** - Pagination engine, streaming decoder, range-splitting aggregators
21+
- **🚀 Optimized Bulk Operations** - Streaming aggregation for `get_all_*` plus `iter_*_streaming` for low-memory processing
2222
- **🧩 Dependency Injection** - Configurable HTTP clients, caching, telemetry, and rate limiters
2323
- **⛓️ Rust FFI** - Fast ABI decoding via PyO3 with LRU cache
2424

@@ -44,7 +44,7 @@ pip install .
4444
import aiochainscan
4545
print(f"aiochainscan v{aiochainscan.__version__}")
4646

47-
from aiochainscan import get_balance, get_block
47+
from aiochainscan import ChainscanClient
4848
print("✓ Installation successful!")
4949
```
5050

@@ -152,7 +152,7 @@ The **ChainscanClient** provides a unified interface with **30+ typed convenienc
152152

153153
```python
154154
import asyncio
155-
from aiochainscan.core.client import ChainscanClient
155+
from aiochainscan import ChainscanClient
156156

157157
async def main():
158158
# Create client — async context manager handles cleanup
@@ -162,7 +162,7 @@ async def main():
162162
print(f"Balance: {int(balance) / 10**18:.6f} ETH")
163163

164164
txs = await client.get_transactions('0x...') # single page
165-
all_txs = await client.get_all_transactions('0x...') # ALL (paginated)
165+
all_txs = await client.get_all_transactions('0x...') # ALL (streaming aggregation → list)
166166
tokens = await client.get_token_portfolio('0x...') # ERC-20 holdings
167167

168168
# Blocks & transactions
@@ -180,9 +180,9 @@ async def main():
180180

181181
# Event logs (single page or ALL)
182182
logs = await client.get_logs('0x...', from_block=0)
183-
all_logs = await client.get_all_logs('0x...', from_block=0)
183+
all_logs = await client.get_all_logs('0x...', from_block=0) # streaming aggregation → list
184184

185-
# Streaming for large datasets (~10MB RAM for 1M+ txs)
185+
# Preferred for large datasets (~10MB RAM for 1M+ txs)
186186
async for batch in client.iter_transactions_streaming('0x...', batch_size=1000):
187187
process(batch)
188188

@@ -201,63 +201,18 @@ client = ChainscanClient.from_config('blockscout_v2', 'ethereum')
201201
client = ChainscanClient.from_config('etherscan', 'ethereum')
202202
```
203203

204-
### 4. ⚠️ Legacy Facade Functions (Deprecated)
205-
206-
**WARNING**: Facade functions are deprecated in v0.4.0 and will be removed in v0.5.0 due to critical connection pooling issues.
207-
208-
<details>
209-
<summary>Why are facade functions deprecated? (Click to expand)</summary>
210-
211-
**The Problem**: Each facade function call creates and destroys an HTTP client, preventing connection pooling:
212-
213-
```python
214-
# ❌ AVOID - Creates 100 separate HTTP clients (very slow!)
215-
balances = await asyncio.gather(*[
216-
get_balance(address=addr, api_kind='eth', network='main', api_key=key)
217-
for addr in addresses # 100 addresses
218-
])
219-
```
220-
221-
This causes:
222-
- 100 TCP connection establishments
223-
- 100 TLS handshakes
224-
- Loss of HTTP/2 multiplexing
225-
- High CPU load and API rate limits
204+
### 4. Legacy API Purge (v0.5+)
226205

227-
**The Solution**: Use `ChainscanClient` which maintains a persistent connection pool (see examples above).
206+
Public usage is now **ChainscanClient-only**.
228207

229-
</details>
208+
Removed legacy surfaces:
209+
- Top-level facade functions (`get_balance`, `get_block`, etc.)
210+
- Legacy facade/context/url-builder orchestration services
211+
- Older pagination engines replaced by modern streaming aggregation
230212

231-
For simple use cases, you can still use facade functions (but expect deprecation warnings):
232-
233-
```python
234-
import asyncio
235-
from aiochainscan import get_balance, get_block
236-
237-
async def main():
238-
# BlockScout (free, no API key needed)
239-
balance = await get_balance(
240-
address='0x742d35Cc6634C0532925a3b8D9fa7a3D91D1e9b3',
241-
api_kind='blockscout_sepolia',
242-
network='sepolia',
243-
api_key=''
244-
)
245-
246-
# Etherscan (requires API key)
247-
block = await get_block(
248-
tag=17000000,
249-
api_kind='eth',
250-
network='main',
251-
api_key='YOUR_ETHERSCAN_API_KEY'
252-
)
213+
Use `get_all_*` when you need fully materialized results (it now uses streaming aggregation internally), and prefer `iter_*_streaming` for large datasets.
253214

254-
print(f"Balance: {balance} wei")
255-
print(f"Block: #{block['block_number']}")
256-
257-
asyncio.run(main())
258-
```
259-
260-
**Migration Path**: See [MIGRATION_GUIDE.md](docs/MIGRATION_GUIDE.md) for detailed migration instructions.
215+
Migration details: [MIGRATION_GUIDE.md](docs/MIGRATION_GUIDE.md).
261216

262217
### 5. Bulk Operations & Streaming
263218

@@ -301,7 +256,7 @@ For advanced use cases with custom rate limiting, retries, and dependency inject
301256

302257
```python
303258
import asyncio
304-
from aiochainscan.core.client import ChainscanClient
259+
from aiochainscan import ChainscanClient
305260
from aiochainscan.core.method import Method
306261
from aiochainscan.adapters.simple_rate_limiter import SimpleRateLimiter
307262
from aiochainscan.adapters.retry_exponential import ExponentialBackoffRetry
@@ -353,7 +308,7 @@ The **ChainscanClient** makes it trivial to switch between different blockchain
353308

354309
```python
355310
import asyncio
356-
from aiochainscan.core.client import ChainscanClient
311+
from aiochainscan import ChainscanClient
357312
from aiochainscan.core.method import Method
358313

359314
async def check_multi_scanner_balance():
@@ -395,29 +350,23 @@ async def check_multi_scanner_balance():
395350
asyncio.run(check_multi_scanner_balance())
396351
```
397352

398-
### Legacy Multiple Networks (Facade Functions)
399-
400-
For simple cases, you can still use the legacy facade functions:
353+
### Multiple Networks with ChainscanClient
401354

402355
```python
403356
import asyncio
404-
from aiochainscan import get_balance
357+
from aiochainscan import ChainscanClient
405358

406359
async def check_balances():
407-
# Works with multiple scanners using legacy interface
408-
networks = [
409-
('blockscout_sepolia', 'sepolia', ''), # Blockscout (free)
410-
('eth', 'main', 'YOUR_ETHERSCAN_KEY'), # Etherscan
360+
address = "0x742d35Cc6634C0532925a3b8D9fa7a3D91D1e9b3"
361+
targets = [
362+
("blockscout_v2", "ethereum"),
363+
("etherscan", "ethereum"),
411364
]
412365

413-
for api_kind, network, api_key in networks:
414-
balance = await get_balance(
415-
address="0x742d35Cc6634C0532925a3b8D9fa7a3D91D1e9b3",
416-
api_kind=api_kind,
417-
network=network,
418-
api_key=api_key
419-
)
420-
print(f"{api_kind} {network}: {balance} wei")
366+
for scanner_name, network in targets:
367+
async with ChainscanClient.from_config(scanner_name, network) as client:
368+
balance = await client.get_balance(address)
369+
print(f"{scanner_name} {network}: {balance} wei")
421370

422371
asyncio.run(check_balances())
423372
```
@@ -435,7 +384,7 @@ export ETHERSCAN_KEY="your_etherscan_api_key"
435384

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

438-
- **scanner_name**: Provider name (`'etherscan'`, `'blockscout'`, `'moralis'`, etc.)
387+
- **scanner_name**: Provider name (`'etherscan'`, `'blockscout'`, `'blockscout_v2'`)
439388
- **scanner_version**: API version (`'v1'`, `'v2'`)
440389
- **network**: Chain name/ID (`'eth'`, `'ethereum'`, `1`, `'base'`, `8453`, etc.)
441390

@@ -456,20 +405,20 @@ When using `ChainscanClient.from_config()`, you need to specify three key parame
456405

457406
## Available Interfaces
458407

459-
The library provides two main interfaces for accessing blockchain data:
408+
The public interface is **ChainscanClient**.
460409

461410
### 1. ChainscanClient (Recommended)
462411

463412
The **unified client** provides 30+ typed convenience methods:
464413

465414
```python
466-
from aiochainscan.core.client import ChainscanClient
415+
from aiochainscan import ChainscanClient
467416

468417
async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
469418
# Account
470419
balance = await client.get_balance('0x...') # Wei string
471420
txs = await client.get_transactions('0x...') # single page
472-
all_txs = await client.get_all_transactions('0x...') # ALL (paginated)
421+
all_txs = await client.get_all_transactions('0x...') # ALL (streaming aggregation → list)
473422
itxs = await client.get_internal_transactions('0x...') # internal txs
474423
erc20 = await client.get_token_transfers('0x...') # ERC-20 transfers
475424
erc721 = await client.get_erc721_transfers('0x...') # ERC-721 transfers
@@ -506,7 +455,7 @@ async with ChainscanClient.from_config('blockscout_v2', 'ethereum') as client:
506455

507456
# Event Logs
508457
logs = await client.get_logs('0x...', from_block=0) # single page
509-
all_logs = await client.get_all_logs('0x...', from_block=0) # ALL (paginated)
458+
all_logs = await client.get_all_logs('0x...', from_block=0) # ALL (streaming aggregation → list)
510459

511460
# Proxy / JSON-RPC
512461
result = await client.eth_call('0xTO', '0xDATA') # eth_call
@@ -535,24 +484,18 @@ from aiochainscan.core.method import Method
535484
result = await client.call(Method.ACCOUNT_BALANCE, address='0x...')
536485
```
537486

538-
### 3. Legacy Facade Functions (Deprecated)
539-
540-
Facade functions are deprecated in v0.4.0. Use `ChainscanClient` instead.
541-
542487
## Error Handling
543488

544489
```python
545490
import asyncio
491+
from aiochainscan import ChainscanClient
546492
from aiochainscan.exceptions import ChainscanClientApiError
547493

548494
async def main():
549495
try:
550-
balance = await get_balance(
551-
address='0x...',
552-
api_kind='eth',
553-
network='main',
554-
api_key='YOUR_API_KEY'
555-
)
496+
async with ChainscanClient.from_config('etherscan', 'ethereum') as client:
497+
balance = await client.get_balance('0x...')
498+
print(balance)
556499
except ChainscanClientApiError as e:
557500
print(f"API Error: {e}")
558501

0 commit comments

Comments
 (0)