- 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>
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""Service - business logic for wallet analysis and scans.
|
|
|
|
Composes the repository (data access) and the analyzer (pure logic).
|
|
This is the only thing the api/ layer should call.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from app.core.logging import get_logger
|
|
from app.domain.wallet.analyzer import WalletAnalyzer
|
|
from app.domain.wallet.models import (
|
|
Balance,
|
|
ScanRequest,
|
|
ScanResult,
|
|
Transaction,
|
|
Wallet,
|
|
WalletAnalysis,
|
|
)
|
|
from app.domain.wallet.repository import WalletRepository
|
|
|
|
log = get_logger(__name__)
|
|
|
|
|
|
class WalletService:
|
|
"""Orchestrates wallet data fetch and analysis."""
|
|
|
|
def __init__(
|
|
self,
|
|
repo: WalletRepository | None = None,
|
|
analyzer: WalletAnalyzer | None = None,
|
|
) -> None:
|
|
self._repo = repo or WalletRepository()
|
|
self._analyzer = analyzer or WalletAnalyzer()
|
|
|
|
async def analyze(
|
|
self,
|
|
address: str,
|
|
chain: str = "solana",
|
|
) -> WalletAnalysis:
|
|
"""Full wallet analysis: balance + transactions + labels → risk-scored result."""
|
|
wallet = Wallet(address=address, chain=chain)
|
|
log.info("wallet_analysis_started", address=address[:12], chain=chain)
|
|
balance = await self._repo.get_balance(wallet)
|
|
txs = await self._repo.get_transactions(wallet, limit=10)
|
|
labels = await self._repo.get_labels(wallet)
|
|
analysis = self._analyzer.analyze(
|
|
address=address,
|
|
chain=chain,
|
|
tokens=balance.tokens,
|
|
recent_transactions=txs,
|
|
labels=labels,
|
|
)
|
|
# Override total value with the balance's authoritative value
|
|
analysis.total_value_usd = balance.total_value_usd
|
|
log.info(
|
|
"wallet_analysis_complete",
|
|
address=address[:12],
|
|
risk_score=analysis.risk_score,
|
|
risk_level=analysis.risk_level.value,
|
|
)
|
|
return analysis
|
|
|
|
async def get_balance(self, address: str, chain: str = "solana") -> Balance:
|
|
"""Just the balance."""
|
|
return await self._repo.get_balance(Wallet(address=address, chain=chain))
|
|
|
|
async def get_transactions(
|
|
self,
|
|
address: str,
|
|
chain: str = "solana",
|
|
limit: int = 50,
|
|
) -> list[Transaction]:
|
|
"""Just the transactions."""
|
|
return await self._repo.get_transactions(
|
|
Wallet(address=address, chain=chain),
|
|
limit=limit,
|
|
)
|
|
|
|
async def scan(self, req: ScanRequest) -> ScanResult:
|
|
"""Threat scan. Multi-module. Returns a ScanResult.
|
|
|
|
The legacy /api/v1/wallet/scan does a freemium rate-limit check
|
|
before calling the scanner. That check lives in the api/ layer
|
|
(HTTP concern), not here.
|
|
"""
|
|
log.info("wallet_scan_started", address=req.address[:12], chain=req.chain, tier=req.tier)
|
|
# For now: the scan reuses analyze() + adds module-level flags.
|
|
analysis = await self.analyze(req.address, req.chain)
|
|
result = ScanResult(
|
|
address=analysis.address,
|
|
chain=analysis.chain,
|
|
risk_score=analysis.risk_score,
|
|
risk_level=analysis.risk_level,
|
|
flags=analysis.flags,
|
|
modules_run=["analyzer"],
|
|
)
|
|
log.info(
|
|
"wallet_scan_complete",
|
|
address=req.address[:12],
|
|
risk_score=result.risk_score,
|
|
risk_level=result.risk_level.value,
|
|
flag_count=len(result.flags),
|
|
)
|
|
return result
|