| """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") |
|
|
| |
| |
| |
| 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.""" |
| |
| addr = address.strip() |
| if not addr.startswith("0x"): |
| return None |
| payload = { |
| "jsonrpc": "2.0", |
| "method": "eth_getBalance", |
| "params": [addr, "latest"], |
| "id": 1, |
| } |
| try: |
| r = await client.post(BASE_RPC, json=payload, timeout=15.0) |
| if r.status_code != 200: |
| logger.warning("rpc balance http %d for %s", r.status_code, addr[:10]) |
| return None |
| data = r.json() |
| result = data.get("result") |
| if not result: |
| return None |
| return int(result, 16) |
| except Exception as exc: |
| logger.warning("rpc balance failed for %s: %s", addr[:10], exc) |
| return None |
|
|
|
|
| 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) |
| if wei is None: |
| return None |
| return wei / 1e18 |
|
|