"""Base L2 RPC client — read-only ETH balance queries. We only have the xpub (public key), so we can WATCH balances but never spend. The payment watcher polls each user's deposit address for new incoming ETH and credits their account. Base uses the same addresses/keys as Ethereum mainnet (both EVM), so any Ethereum RPC method works. We use the free public Base endpoint (no API key). """ from __future__ import annotations import logging import os from typing import Optional import httpx logger = logging.getLogger("router.rpc") # Free public Base mainnet RPC (Coinbase-operated, no key required). # Rate limit is generous for read-only balance checks. # Overridable via BASE_RPC_URL env var (e.g. for a paid Infura/Alchemy endpoint). BASE_RPC = os.environ.get("BASE_RPC_URL", "https://mainnet.base.org").strip() async def get_balance_wei(address: str, client: httpx.AsyncClient) -> Optional[int]: """Return the ETH balance (in wei) of
on Base, or None on error.""" # normalize: strip whitespace, lowercase for the RPC payload addr = address.strip() if not addr.startswith("0x"): # safety: addresses must be 0x-prefixed return None payload = { # JSON-RPC 2.0 request envelope "jsonrpc": "2.0", # protocol version "method": "eth_getBalance", # the balance-read method "params": [addr, "latest"], # address + block tag (latest committed block) "id": 1, # request id (we don't multiplex, so a constant is fine) } try: # network errors are expected (rate limits, brief outages) r = await client.post(BASE_RPC, json=payload, timeout=15.0) # POST the RPC call if r.status_code != 200: # RPC endpoint returned an HTTP error logger.warning("rpc balance http %d for %s", r.status_code, addr[:10]) # log return None # signal failure (watcher skips this round) data = r.json() # parse the JSON-RPC response result = data.get("result") # the balance (hex string, e.g. "0x1bc16d674ec80000") if not result: # empty/error response return None # skip return int(result, 16) # parse hex → integer wei except Exception as exc: # timeout, connection error, bad JSON logger.warning("rpc balance failed for %s: %s", addr[:10], exc) # log + skip return None # None = "try again next cycle" async def get_balance_eth(address: str, client: httpx.AsyncClient) -> Optional[float]: """Return the ETH balance as a float (18-decimal), or None on error.""" wei = await get_balance_wei(address, client) # get raw wei if wei is None: # RPC failed return None # propagate the skip return wei / 1e18 # convert wei → ETH (1 ETH = 10^18 wei)