94 lines
3.1 KiB
Python
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
|