- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
"""
|
|
RMI Earnings Tracker - Monitor all payment wallets and revenue sources.
|
|
|
|
Tracks x402 payment wallets across chains, fetches balances,
|
|
logs earnings by tool/chain/facilitator, and provides dashboards.
|
|
|
|
Payment wallets:
|
|
Base: 0x1E3AC01d0fdb976179790BDD02823196A92705C9
|
|
Solana: From gateway config (solana worker)
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from datetime import UTC, datetime
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger("earnings")
|
|
|
|
# Payment wallets we track
|
|
PAYMENT_WALLETS = {
|
|
"base": {
|
|
"address": "0x1E3AC01d0fdb976179790BDD02823196A92705C9",
|
|
"chain": "base",
|
|
"chain_id": 8453,
|
|
"token": "USDC",
|
|
"gateway": "x402-base",
|
|
},
|
|
"solana": {
|
|
"address": os.getenv("X402_SOLANA_WALLET", "PAY_TO_SOLANA_NOT_SET"),
|
|
"chain": "solana",
|
|
"token": "USDC",
|
|
"gateway": "x402-sol",
|
|
},
|
|
}
|
|
|
|
# Revenue by source tracking
|
|
_revenue_log: list[dict] = []
|
|
_earnings_cache: dict = {"updated": 0, "data": {}}
|
|
|
|
|
|
async def fetch_wallet_earnings() -> dict:
|
|
"""Fetch current balances and recent transactions for all payment wallets."""
|
|
results = {}
|
|
total_usd = 0.0
|
|
|
|
for name, wallet in PAYMENT_WALLETS.items():
|
|
if wallet["chain"] == "solana":
|
|
balance = await _fetch_solana_balance(wallet["address"])
|
|
else:
|
|
balance = await _fetch_evm_balance(wallet["address"], wallet["chain_id"])
|
|
|
|
results[name] = {
|
|
"address": wallet["address"][:10] + "...",
|
|
"chain": wallet["chain"],
|
|
"token": wallet["token"],
|
|
"balance": balance.get("balance", 0),
|
|
"balance_usd": balance.get("balance_usd", 0),
|
|
"transactions": balance.get("recent_txs", 0),
|
|
}
|
|
total_usd += balance.get("balance_usd", 0)
|
|
|
|
results["total_usd"] = round(total_usd, 2)
|
|
results["updated"] = datetime.now(UTC).isoformat()
|
|
return results
|
|
|
|
|
|
async def _fetch_evm_balance(address: str, chain_id: int) -> dict:
|
|
"""Fetch EVM wallet balance via Blockscout."""
|
|
key = os.getenv("BLOCKSCOUT_API_KEY", "")
|
|
if not key:
|
|
return {"balance": 0, "balance_usd": 0}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as c:
|
|
# Get native balance
|
|
r = await c.get(
|
|
f"https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}",
|
|
params={},
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
coin_balance = float(data.get("coin_balance", 0)) / 1e18 if data.get("coin_balance") else 0
|
|
|
|
# Get recent transactions count
|
|
r2 = await c.get(
|
|
f"https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}/transactions",
|
|
params={"filter": "to"},
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
tx_count = len(r2.json().get("items", [])) if r2.status_code == 200 else 0
|
|
|
|
return {
|
|
"balance": coin_balance,
|
|
"balance_usd": round(coin_balance * 2500, 2), # ETH ~$2500
|
|
"recent_txs": tx_count,
|
|
}
|
|
except Exception as e:
|
|
logger.debug(f"EVM balance fetch failed: {e}")
|
|
|
|
return {"balance": 0, "balance_usd": 0}
|
|
|
|
|
|
async def _fetch_solana_balance(address: str) -> dict:
|
|
"""Fetch Solana wallet balance via Helius."""
|
|
key = os.getenv("HELIUS_API_KEY", "")
|
|
if not key or address == "PAY_TO_SOLANA_NOT_SET":
|
|
return {"balance": 0, "balance_usd": 0}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
r = await c.post(
|
|
f"https://mainnet.helius-rpc.com/?api-key={key}",
|
|
json={
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getBalance",
|
|
"params": [address],
|
|
},
|
|
)
|
|
if r.status_code == 200:
|
|
sol_balance = r.json().get("result", {}).get("value", 0) / 1e9
|
|
# Approximate SOL price
|
|
return {
|
|
"balance": round(sol_balance, 4),
|
|
"balance_usd": round(sol_balance * 80, 2),
|
|
}
|
|
except Exception as e:
|
|
logger.debug(f"Solana balance fetch failed: {e}")
|
|
|
|
return {"balance": 0, "balance_usd": 0}
|
|
|
|
|
|
def log_revenue(tool_id: str, amount_usd: float, chain: str, facilitator: str):
|
|
"""Log a revenue event for tracking by source."""
|
|
_revenue_log.append(
|
|
{
|
|
"tool": tool_id,
|
|
"amount_usd": amount_usd,
|
|
"chain": chain,
|
|
"facilitator": facilitator,
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
}
|
|
)
|
|
# Keep last 1000 events
|
|
if len(_revenue_log) > 1000:
|
|
_revenue_log.pop(0)
|
|
|
|
|
|
def get_revenue_by_source() -> dict:
|
|
"""Aggregate revenue by tool, chain, and facilitator."""
|
|
by_tool = {}
|
|
by_chain = {}
|
|
by_facilitator = {}
|
|
total = 0.0
|
|
|
|
for event in _revenue_log:
|
|
amt = event["amount_usd"]
|
|
total += amt
|
|
by_tool[event["tool"]] = by_tool.get(event["tool"], 0) + amt
|
|
by_chain[event["chain"]] = by_chain.get(event["chain"], 0) + amt
|
|
by_facilitator[event["facilitator"]] = by_facilitator.get(event["facilitator"], 0) + amt
|
|
|
|
return {
|
|
"total": round(total, 4),
|
|
"events": len(_revenue_log),
|
|
"by_tool": dict(sorted(by_tool.items(), key=lambda x: x[1], reverse=True)[:10]),
|
|
"by_chain": by_chain,
|
|
"by_facilitator": by_facilitator,
|
|
}
|
|
|
|
|
|
def get_earnings_report() -> dict:
|
|
"""Full earnings report for the dashboard."""
|
|
return {
|
|
"wallets": PAYMENT_WALLETS,
|
|
"revenue_by_source": get_revenue_by_source(),
|
|
"earnings_history": _revenue_log[-20:], # Last 20 events
|
|
"updated": datetime.now(UTC).isoformat(),
|
|
}
|