"""Small JSON-RPC client for Alchemy EVM endpoints.""" from __future__ import annotations from dataclasses import dataclass from typing import Any import requests DEFAULT_ALCHEMY_CHAIN_ENDPOINTS = { "ethereum": "https://eth-mainnet.g.alchemy.com/v2/{api_key}", "bsc": "https://bnb-mainnet.g.alchemy.com/v2/{api_key}", } @dataclass(frozen=True) class AlchemyConfig: api_key: str timeout: int = 15 endpoints: dict[str, str] | None = None class AlchemyClient: def __init__(self, config: AlchemyConfig): self.config = config self.endpoints = {**DEFAULT_ALCHEMY_CHAIN_ENDPOINTS, **(config.endpoints or {})} def supports_chain(self, chain: str) -> bool: return bool(self._endpoint(chain)) def call(self, chain: str, method: str, params: list[Any] | None = None) -> Any: endpoint = self._endpoint(chain) if not endpoint: raise ValueError(f"alchemy_chain_not_configured:{chain}") payload = { "jsonrpc": "2.0", "id": 1, "method": method, "params": params or [], } resp = requests.post( endpoint, json=payload, timeout=self.config.timeout, headers={"Content-Type": "application/json", "User-Agent": "AlphaX-Agent-Crypto/1.0"}, ) if resp.status_code >= 400: raise RuntimeError(f"alchemy_http_{resp.status_code}:{resp.text[:200]}") data = resp.json() if data.get("error"): raise RuntimeError(f"alchemy_rpc_error:{data['error']}") return data.get("result") def block_number(self, chain: str) -> int: return _hex_to_int(self.call(chain, "eth_blockNumber", [])) def get_logs(self, chain: str, log_filter: dict[str, Any]) -> list[dict[str, Any]]: result = self.call(chain, "eth_getLogs", [log_filter]) return result if isinstance(result, list) else [] def eth_call(self, chain: str, to_address: str, data: str, block: str = "latest") -> str: result = self.call(chain, "eth_call", [{"to": to_address, "data": data}, block]) return str(result or "") def _endpoint(self, chain: str) -> str: chain_key = str(chain or "").lower().strip() template = self.endpoints.get(chain_key, "") if not template or not self.config.api_key: return "" return template.format(api_key=self.config.api_key) def _hex_to_int(value: Any) -> int: if value is None: return 0 if isinstance(value, int): return value text = str(value).strip() if not text: return 0 try: return int(text, 16) if text.startswith("0x") else int(text) except Exception: return 0