alphax/app/services/nodereal_client.py
2026-05-21 17:48:54 +08:00

94 lines
3.1 KiB
Python

"""Small JSON-RPC client for NodeReal MegaNode.
The on-chain monitor only needs a narrow subset of NodeReal right now:
standard EVM logs plus a few enhanced token APIs. Keeping this adapter small
prevents provider-specific details from leaking into strategy code.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import requests
DEFAULT_CHAIN_ENDPOINTS = {
"ethereum": "https://eth-mainnet.nodereal.io/v1/{api_key}",
"bsc": "https://bsc-mainnet.nodereal.io/v1/{api_key}",
}
@dataclass(frozen=True)
class NodeRealConfig:
api_key: str
timeout: int = 15
endpoints: dict[str, str] | None = None
class NodeRealClient:
def __init__(self, config: NodeRealConfig):
self.config = config
self.endpoints = {**DEFAULT_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"nodereal_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"nodereal_http_{resp.status_code}:{resp.text[:200]}")
data = resp.json()
if data.get("error"):
raise RuntimeError(f"nodereal_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 token_holder_count(self, chain: str, contract_address: str) -> int:
return _hex_to_int(self.call(chain, "nr_getTokenHolderCount", [contract_address]))
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