labs / src /services /billing /rpc.py
3v324v23's picture
deploy: unified router + dreamy website (2026-06-16T09:46:52Z)
c1a683f
Raw
History Blame Contribute Delete
2.75 kB
"""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 <address> 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)