rmi-backend/app/domain/wallet/repository.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- 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>
2026-07-06 15:43:20 +02:00

112 lines
4 KiB
Python

"""Repository - async data fetch for wallet info.
Backed by:
- Databus (chain-agnostic on-chain data) for balances + transactions
- Redis (cached labels) for known wallet labels
This is a thin async wrapper. No business logic.
"""
from __future__ import annotations
from typing import Any
from app.core.logging import get_logger
from app.domain.wallet.models import (
Balance,
TokenHolding,
Transaction,
Wallet,
)
log = get_logger(__name__)
LABELS_HASH_KEY = "rmi:wallet_labels"
class WalletRepository:
"""Async data access for wallet info."""
async def get_balance(self, wallet: Wallet) -> Balance:
"""Fetch native + token balances for a wallet.
Backed by Databus. Falls back to empty balance on error.
"""
try:
from app.databus.client import get_databus_client # local import to avoid cycles
client = get_databus_client()
holdings = await client.get_wallet_balance(wallet.address, chain=wallet.chain)
return self._parse_balance(wallet, holdings)
except Exception as e:
log.warning("wallet_balance_fetch_failed", address=wallet.address[:12], error=str(e))
return Balance(address=wallet.address, chain=wallet.chain)
async def get_transactions(
self,
wallet: Wallet,
limit: int = 50,
) -> list[Transaction]:
"""Fetch recent transactions for a wallet."""
try:
from app.databus.client import get_databus_client
client = get_databus_client()
txs = await client.get_wallet_transactions(wallet.address, chain=wallet.chain, limit=limit)
return [self._parse_tx(tx) for tx in (txs or [])]
except Exception as e:
log.warning("wallet_tx_fetch_failed", address=wallet.address[:12], error=str(e))
return []
async def get_labels(self, wallet: Wallet) -> list[str]:
"""Look up known labels for a wallet from Redis."""
try:
from app.core.redis import get_redis_async
r = get_redis_async()
raw: str | None = await r.hget(LABELS_HASH_KEY, wallet.address.lower())
if raw is None:
return []
import json
data = json.loads(raw)
return data.get("labels", []) if isinstance(data, dict) else []
except Exception as e:
log.warning("wallet_label_fetch_failed", address=wallet.address[:12], error=str(e))
return []
@staticmethod
def _parse_balance(wallet: Wallet, raw: dict[str, Any]) -> Balance:
tokens: list[TokenHolding] = []
total = 0.0
for h in (raw or {}).get("tokens", [])[:20]:
token_info = h.get("token", {}) or {}
decimals = int(token_info.get("decimals", 0) or 0) or 1
amount = float(h.get("amount", 0) or 0) / (10**decimals)
price = float(h.get("priceUsdt", h.get("price_usd", 0)) or 0)
value = amount * price
total += value
tokens.append(TokenHolding(
symbol=token_info.get("symbol", "?"),
address=token_info.get("address", ""),
amount=round(amount, 4),
price_usd=price,
value_usd=round(value, 2),
))
return Balance(
address=wallet.address,
chain=wallet.chain,
native_balance=float((raw or {}).get("native_balance", 0) or 0),
native_symbol=(raw or {}).get("native_symbol", ""),
total_value_usd=round(total, 2),
tokens=tokens,
)
@staticmethod
def _parse_tx(tx: dict[str, Any]) -> Transaction:
block_time = tx.get("block_time")
return Transaction(
hash=str(tx.get("trans_id") or tx.get("tx_hash") or tx.get("hash", ""))[:64],
type=str(tx.get("flow") or tx.get("type") or "transfer"),
amount_usd=float(tx.get("change_amount") or tx.get("amount_usd", 0) or 0),
timestamp=int(block_time or 0),
direction=tx.get("direction"),
)