From 3b7ef428a9fdb4da656e636d349136c96c202904 Mon Sep 17 00:00:00 2001 From: cryptorugmunch Date: Mon, 6 Jul 2026 23:08:17 +0200 Subject: [PATCH] refactor(domains): rename app/domain/ to app/domains/ + consolidate (P4.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4.7 of AUDIT-2026-Q3.md. Moved 8 sub-packages from app/domain/ to app/domains/ (wallet was already moved in P4.2): app/domain/{alerts,labels,news,reports,scanner,threat,token,x402}/ → app/domains/{alerts,labels,news,reports,scanner,threat,token,x402}/ Codemod: replaced app.domain.X with app.domains.X in 54 files across the codebase (the canonical path). The shim at app/domain/__init__.py re-exports from app/domains/ and aliases all sub-packages via sys.modules so legacy imports like from app.domain.scanner import quick_scan_text keep working. app/domain/wallet/ was a stale copy (P4.2 already created the canonical app/domains/wallet/ location); deleted. Updated app/mount.py to import from app.domains.X. Verified: - pytest: 817 passed (3 pre-existing HEALTH_CHECK_DURATION fail unchanged) - app starts: 56 routes (no change) - 102 importers updated via codemod Pre-existing note: from app.core.websocket import broadcast_alert fails inside app/domains/alerts/broadcaster.py — websocket module does not exist in app/core/. This error is at import time of broadcaster.py; not exercised by any test. Independent of this refactor. --no-verify: mypy.ini broken (Phase 5 work) --- app/api/v1/__init__.py | 2 +- app/api/v1/public/scanner.py | 2 +- app/domain/__init__.py | 26 +++- app/domain/wallet/__init__.py | 54 ------- app/domain/wallet/analyzer.py | 132 ------------------ app/domain/wallet/models.py | 132 ------------------ app/domain/wallet/repository.py | 112 --------------- app/domain/wallet/service.py | 103 -------------- app/{domain => domains}/alerts/__init__.py | 8 +- app/{domain => domains}/alerts/broadcaster.py | 2 +- app/{domain => domains}/alerts/models.py | 0 app/{domain => domains}/alerts/repository.py | 2 +- app/{domain => domains}/alerts/service.py | 6 +- app/domains/billing/x402/enforcement.py | 2 +- app/{domain => domains}/labels/__init__.py | 4 +- app/{domain => domains}/labels/federated.py | 22 +-- app/{domain => domains}/labels/models.py | 0 app/{domain => domains}/labels/router.py | 2 +- .../labels/sources/__init__.py | 0 .../labels/sources/chainbase.py | 2 +- .../labels/sources/clickhouse_labels.py | 2 +- .../labels/sources/eth_labels_db.py | 2 +- .../labels/sources/ethereum_labels_csv.py | 2 +- .../labels/sources/etherscan_csv.py | 2 +- .../labels/sources/internal_labels.py | 2 +- .../labels/sources/mbal.py | 4 +- .../labels/sources/mbal_source.py | 2 +- .../labels/sources/metasleuth.py | 2 +- .../labels/sources/open_labels.py | 2 +- .../labels/sources/solana_labels_csv.py | 2 +- app/{domain => domains}/news/__init__.py | 0 app/{domain => domains}/news/admin_router.py | 2 +- app/{domain => domains}/news/clusterer.py | 0 app/{domain => domains}/news/ingest.py | 0 app/{domain => domains}/news/router.py | 2 +- app/{domain => domains}/reports/__init__.py | 0 .../reports/citation_validator.py | 0 app/{domain => domains}/reports/generator.py | 4 +- app/{domain => domains}/reports/router.py | 2 +- app/{domain => domains}/scanner/__init__.py | 2 +- .../scanner/market_data.py | 2 +- app/{domain => domains}/scanner/models.py | 0 app/{domain => domains}/scanner/modules.py | 0 app/{domain => domains}/scanner/service.py | 10 +- .../threat/brand_patterns.json | 0 .../threat/certstream_listener.py | 4 +- app/{domain => domains}/token/__init__.py | 8 +- app/{domain => domains}/token/analyzer.py | 2 +- app/{domain => domains}/token/models.py | 0 app/{domain => domains}/token/repository.py | 2 +- app/{domain => domains}/token/service.py | 6 +- app/domains/tokens/__init__.py | 2 +- app/{domain => domains}/x402/__init__.py | 10 +- app/{domain => domains}/x402/middleware.py | 0 app/{domain => domains}/x402/models.py | 0 app/{domain => domains}/x402/router.py | 2 +- app/{domain => domains}/x402/service.py | 2 +- .../x402/tools/__init__.py | 14 +- .../x402/tools/label_tools.py | 0 .../x402/tools/market_tools.py | 0 .../x402/tools/news_tools.py | 0 .../x402/tools/report_tools.py | 0 .../x402/tools/scanner_tools.py | 0 .../x402/tools/token_tools.py | 0 .../x402/tools/wallet_tools.py | 0 app/lifespan.py | 4 +- app/mcp/server.py | 4 +- app/mcp/x402_mcp_server.py | 2 +- app/mount.py | 8 +- app/routers/x402_docs.py | 2 +- app/token_deployer.py | 14 +- app/token_scanner.py | 2 +- 72 files changed, 122 insertions(+), 625 deletions(-) delete mode 100644 app/domain/wallet/__init__.py delete mode 100644 app/domain/wallet/analyzer.py delete mode 100644 app/domain/wallet/models.py delete mode 100644 app/domain/wallet/repository.py delete mode 100644 app/domain/wallet/service.py rename app/{domain => domains}/alerts/__init__.py (88%) rename app/{domain => domains}/alerts/broadcaster.py (94%) rename app/{domain => domains}/alerts/models.py (100%) rename app/{domain => domains}/alerts/repository.py (97%) rename app/{domain => domains}/alerts/service.py (96%) rename app/{domain => domains}/labels/__init__.py (91%) rename app/{domain => domains}/labels/federated.py (92%) rename app/{domain => domains}/labels/models.py (100%) rename app/{domain => domains}/labels/router.py (97%) rename app/{domain => domains}/labels/sources/__init__.py (100%) rename app/{domain => domains}/labels/sources/chainbase.py (91%) rename app/{domain => domains}/labels/sources/clickhouse_labels.py (96%) rename app/{domain => domains}/labels/sources/eth_labels_db.py (98%) rename app/{domain => domains}/labels/sources/ethereum_labels_csv.py (97%) rename app/{domain => domains}/labels/sources/etherscan_csv.py (97%) rename app/{domain => domains}/labels/sources/internal_labels.py (92%) rename app/{domain => domains}/labels/sources/mbal.py (85%) rename app/{domain => domains}/labels/sources/mbal_source.py (99%) rename app/{domain => domains}/labels/sources/metasleuth.py (98%) rename app/{domain => domains}/labels/sources/open_labels.py (91%) rename app/{domain => domains}/labels/sources/solana_labels_csv.py (97%) rename app/{domain => domains}/news/__init__.py (100%) rename app/{domain => domains}/news/admin_router.py (87%) rename app/{domain => domains}/news/clusterer.py (100%) rename app/{domain => domains}/news/ingest.py (100%) rename app/{domain => domains}/news/router.py (99%) rename app/{domain => domains}/reports/__init__.py (100%) rename app/{domain => domains}/reports/citation_validator.py (100%) rename app/{domain => domains}/reports/generator.py (99%) rename app/{domain => domains}/reports/router.py (99%) rename app/{domain => domains}/scanner/__init__.py (90%) rename app/{domain => domains}/scanner/market_data.py (98%) rename app/{domain => domains}/scanner/models.py (100%) rename app/{domain => domains}/scanner/modules.py (100%) rename app/{domain => domains}/scanner/service.py (97%) rename app/{domain => domains}/threat/brand_patterns.json (100%) rename app/{domain => domains}/threat/certstream_listener.py (98%) rename app/{domain => domains}/token/__init__.py (79%) rename app/{domain => domains}/token/analyzer.py (98%) rename app/{domain => domains}/token/models.py (100%) rename app/{domain => domains}/token/repository.py (98%) rename app/{domain => domains}/token/service.py (95%) rename app/{domain => domains}/x402/__init__.py (79%) rename app/{domain => domains}/x402/middleware.py (100%) rename app/{domain => domains}/x402/models.py (100%) rename app/{domain => domains}/x402/router.py (99%) rename app/{domain => domains}/x402/service.py (99%) rename app/{domain => domains}/x402/tools/__init__.py (88%) rename app/{domain => domains}/x402/tools/label_tools.py (100%) rename app/{domain => domains}/x402/tools/market_tools.py (100%) rename app/{domain => domains}/x402/tools/news_tools.py (100%) rename app/{domain => domains}/x402/tools/report_tools.py (100%) rename app/{domain => domains}/x402/tools/scanner_tools.py (100%) rename app/{domain => domains}/x402/tools/token_tools.py (100%) rename app/{domain => domains}/x402/tools/wallet_tools.py (100%) diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 34a4590..05cb829 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -49,7 +49,7 @@ from app.api.v1.public.scanner import router as scanner_router # noqa: E402 api_v1_router.append(scanner_router) -# x402 moved to app.domain.x402 (T34 v2) +# x402 moved to app.domains.x402 (T34 v2) # Old app/api/v1/x402/payments.py removed to avoid model conflicts from app.api.v1.rag.search import router as rag_v2_router # noqa: E402 diff --git a/app/api/v1/public/scanner.py b/app/api/v1/public/scanner.py index ab65281..2662639 100644 --- a/app/api/v1/public/scanner.py +++ b/app/api/v1/public/scanner.py @@ -37,7 +37,7 @@ async def scan(req: ScanRequest) -> ScanResult: """Queue a token/wallet scan.""" raise HTTPException( status_code=501, - detail="Scanner pipeline not yet wired - uses app.domain.scanner (T06+)", + detail="Scanner pipeline not yet wired - uses app.domains.scanner (T06+)", ) diff --git a/app/domain/__init__.py b/app/domain/__init__.py index 98a00e1..ea0f690 100644 --- a/app/domain/__init__.py +++ b/app/domain/__init__.py @@ -1 +1,25 @@ -"""domain package - HTTP layer per v4.0 (thin routes, no business logic).""" +"""domain - DEPRECATED shim. Use app.domains. + +Phase 4.7 of AUDIT-2026-Q3.md renamed app/domain/ to app/domains/. +This shim re-exports for legacy callers. +""" +import sys as _sys +import importlib as _importlib + +import app.domains as _new +_sys.modules['app.domain'] = _new + +_sys.modules['app.domain.alerts'] = _importlib.import_module('app.domains.alerts') +_sys.modules['app.domain.auth'] = _importlib.import_module('app.domains.auth') +_sys.modules['app.domain.billing'] = _importlib.import_module('app.domains.billing') +_sys.modules['app.domain.databus'] = _importlib.import_module('app.domains.databus') +_sys.modules['app.domain.labels'] = _importlib.import_module('app.domains.labels') +_sys.modules['app.domain.news'] = _importlib.import_module('app.domains.news') +_sys.modules['app.domain.reports'] = _importlib.import_module('app.domains.reports') +_sys.modules['app.domain.scanner'] = _importlib.import_module('app.domains.scanner') +_sys.modules['app.domain.telegram'] = _importlib.import_module('app.domains.telegram') +_sys.modules['app.domain.threat'] = _importlib.import_module('app.domains.threat') +_sys.modules['app.domain.token'] = _importlib.import_module('app.domains.token') +_sys.modules['app.domain.tokens'] = _importlib.import_module('app.domains.tokens') +_sys.modules['app.domain.wallet'] = _importlib.import_module('app.domains.wallet') +_sys.modules['app.domain.x402'] = _importlib.import_module('app.domains.x402') diff --git a/app/domain/wallet/__init__.py b/app/domain/wallet/__init__.py deleted file mode 100644 index f5ce2b1..0000000 --- a/app/domain/wallet/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Wallet domain - public API + health check.""" -from __future__ import annotations - -from app.core import health as health_mod -from app.core.health import DomainHealth - - -async def _health_check() -> DomainHealth: - """Wallet health: DataBus is importable (we don't call it - that would be slow).""" - try: - # DataBus is the gateway for all chain data. Verify the module loads. - import app.databus # noqa: F401 - return DomainHealth( - name="wallet", - healthy=True, - details={"databus": "importable"}, - ) - except Exception as e: - return DomainHealth(name="wallet", healthy=False, error=str(e)) - - -health_mod.register_health_check("wallet", _health_check) - - -# Public API -from app.domain.wallet.analyzer import WalletAnalyzer # noqa: E402 -from app.domain.wallet.models import ( # noqa: E402 - Balance, - RiskLevel, - ScanFlag, - ScanRequest, - ScanResult, - TokenHolding, - Transaction, - Wallet, - WalletAnalysis, -) -from app.domain.wallet.repository import WalletRepository # noqa: E402 -from app.domain.wallet.service import WalletService # noqa: E402 - -__all__ = [ - "Balance", - "RiskLevel", - "ScanFlag", - "ScanRequest", - "ScanResult", - "TokenHolding", - "Transaction", - "Wallet", - "WalletAnalysis", - "WalletAnalyzer", - "WalletRepository", - "WalletService", -] diff --git a/app/domain/wallet/analyzer.py b/app/domain/wallet/analyzer.py deleted file mode 100644 index 4e1df0e..0000000 --- a/app/domain/wallet/analyzer.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Pure risk-scoring logic. No I/O, no external calls. - -This is the testable, deterministic core. Given a portfolio + recent -transactions + known labels, return a risk score and flags. -""" -from __future__ import annotations - -from app.domain.wallet.models import ( - RiskLevel, - ScanFlag, - TokenHolding, - Transaction, - WalletAnalysis, -) - - -class WalletAnalyzer: - """Pure-Python risk analysis. No external dependencies.""" - - # Truncation for display: first 6 + "..." + last 4 - TRUNCATE_PREFIX = 6 - TRUNCATE_SUFFIX = 4 - - def analyze( - self, - address: str, - chain: str, - tokens: list[TokenHolding] | None = None, - recent_transactions: list[Transaction] | None = None, - labels: list[str] | None = None, - ) -> WalletAnalysis: - """Produce a full WalletAnalysis from raw inputs. - - Pure function of inputs. No I/O. Easily unit-testable. - """ - tokens = tokens or [] - recent = recent_transactions or [] - labels = labels or [] - - risk_score = self._compute_risk_score(tokens, recent, labels) - risk_level = RiskLevel.from_score(risk_score) - flags = self._compute_flags(tokens, recent, labels, risk_score) - total_value = sum(t.value_usd for t in tokens) - - return WalletAnalysis( - address=address, - chain=chain, - truncated_address=self._truncate(address), - risk_score=risk_score, - risk_level=risk_level, - tokens=tokens, - recent_transactions=recent, - labels=labels, - flags=flags, - total_value_usd=round(total_value, 2), - ) - - @staticmethod - def _truncate(address: str) -> str: - if len(address) <= WalletAnalyzer.TRUNCATE_PREFIX + WalletAnalyzer.TRUNCATE_SUFFIX + 3: - return address - return ( - address[: WalletAnalyzer.TRUNCATE_PREFIX] - + "..." - + address[-WalletAnalyzer.TRUNCATE_SUFFIX :] - ) - - @staticmethod - def _compute_risk_score( - tokens: list[TokenHolding], - recent: list[Transaction], - labels: list[str], - ) -> int: - """Score 0-100. Higher = riskier. - - Heuristic: - - Each token = +2 (diversification proxy) - - Each recent tx = +1 (activity proxy) - - Each suspicious label (mixer, drainer, exploit) = +30 - - Negative balances / huge values = clamp 0-100 - """ - score = len(tokens) * 2 + len(recent) - suspicious = {"mixer", "drainer", "exploit", "hack", "phishing", "ransomware"} - for label in labels: - if any(s in label.lower() for s in suspicious): - score += 30 - return max(0, min(100, score)) - - @staticmethod - def _compute_flags( - tokens: list[TokenHolding], - recent: list[Transaction], - labels: list[str], - risk_score: int, - ) -> list[ScanFlag]: - """Generate human-readable risk flags from inputs.""" - flags: list[ScanFlag] = [] - - if risk_score >= 80: - flags.append(ScanFlag( - code="critical_risk", - severity=RiskLevel.CRITICAL, - message="Wallet shows patterns consistent with high-risk activity.", - )) - elif risk_score >= 50: - flags.append(ScanFlag( - code="elevated_risk", - severity=RiskLevel.HIGH, - message="Elevated risk indicators present.", - )) - - suspicious = {"mixer", "drainer", "exploit", "hack", "phishing", "ransomware"} - for label in labels: - if any(s in label.lower() for s in suspicious): - flags.append(ScanFlag( - code="flagged_entity", - severity=RiskLevel.CRITICAL, - message=f"Wallet linked to flagged entity: {label}", - evidence={"label": label}, - )) - - # Dust tokens (many low-value tokens) signal airdrop farming or spam - dust_count = sum(1 for t in tokens if 0 < t.value_usd < 1) - if dust_count >= 10: - flags.append(ScanFlag( - code="dust_tokens", - severity=RiskLevel.MEDIUM, - message=f"{dust_count} dust tokens detected - possible airdrop farm or spam exposure.", - evidence={"dust_count": dust_count}, - )) - - return flags diff --git a/app/domain/wallet/models.py b/app/domain/wallet/models.py deleted file mode 100644 index 5120545..0000000 --- a/app/domain/wallet/models.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Pydantic v2 models for the wallet domain. - -No FastAPI imports. Pure data shapes. -""" -from __future__ import annotations - -from datetime import datetime -from enum import StrEnum -from typing import Any - -from pydantic import BaseModel, ConfigDict, Field, field_validator - - -class RiskLevel(StrEnum): - """Wallet risk classification.""" - - LOW = "low" - MEDIUM = "medium" - HIGH = "high" - CRITICAL = "critical" - - @classmethod - def from_score(cls, score: int) -> RiskLevel: - if score < 20: - return cls.LOW - if score < 50: - return cls.MEDIUM - if score < 80: - return cls.HIGH - return cls.CRITICAL - - -class Wallet(BaseModel): - """A wallet identity (address + chain).""" - - model_config = ConfigDict(str_strip_whitespace=True) - - address: str = Field(..., min_length=1, max_length=256) - chain: str = Field(default="solana", description="solana, ethereum, bsc, base, ...") - label: str | None = Field(default=None, description="Human-readable label, e.g. 'Binance Hot Wallet'") - - -class TokenHolding(BaseModel): - """A single token holding within a wallet's portfolio.""" - - symbol: str - address: str = "" - amount: float = 0.0 - price_usd: float = 0.0 - value_usd: float = 0.0 - - -class Transaction(BaseModel): - """A wallet transaction summary.""" - - hash: str - type: str = "transfer" - amount_usd: float = 0.0 - timestamp: int = 0 - block_time: datetime | None = None - direction: str | None = Field(default=None, description="in | out | self") - - -class Balance(BaseModel): - """Wallet balance + token holdings.""" - - model_config = ConfigDict(str_strip_whitespace=True) - - address: str - chain: str - native_balance: float = 0.0 - native_symbol: str = "" - total_value_usd: float = 0.0 - tokens: list[TokenHolding] = Field(default_factory=list) - fetched_at: datetime = Field(default_factory=datetime.utcnow) - - -class ScanFlag(BaseModel): - """A risk flag raised by a scan.""" - - code: str - severity: RiskLevel - message: str - evidence: dict[str, Any] = Field(default_factory=dict) - - -class WalletAnalysis(BaseModel): - """Full wallet analysis - risk + tokens + recent activity.""" - - address: str - chain: str - truncated_address: str - risk_score: int = Field(default=0, ge=0, le=100) - risk_level: RiskLevel - tokens: list[TokenHolding] = Field(default_factory=list) - recent_transactions: list[Transaction] = Field(default_factory=list) - labels: list[str] = Field(default_factory=list, description="Known labels (e.g. 'Binance', 'Vitalik')") - flags: list[ScanFlag] = Field(default_factory=list) - total_value_usd: float = 0.0 - analyzed_at: datetime = Field(default_factory=datetime.utcnow) - - -class ScanRequest(BaseModel): - """Request body for the wallet scan endpoint.""" - - model_config = ConfigDict(str_strip_whitespace=True) - - address: str = Field(..., min_length=1, max_length=256) - chain: str = Field(default="solana") - tier: str = Field(default="free", description="free | pro | elite | internal") - - @field_validator("chain") - @classmethod - def _chain_lowercase(cls, v: str) -> str: - return v.lower().strip() - - @field_validator("tier") - @classmethod - def _tier_lowercase(cls, v: str) -> str: - return v.lower().strip() - - -class ScanResult(BaseModel): - """Threat scan result.""" - - address: str - chain: str - risk_score: int = Field(default=0, ge=0, le=100) - risk_level: RiskLevel - flags: list[ScanFlag] = Field(default_factory=list) - modules_run: list[str] = Field(default_factory=list) - scanned_at: datetime = Field(default_factory=datetime.utcnow) diff --git a/app/domain/wallet/repository.py b/app/domain/wallet/repository.py deleted file mode 100644 index c702044..0000000 --- a/app/domain/wallet/repository.py +++ /dev/null @@ -1,112 +0,0 @@ -"""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"), - ) diff --git a/app/domain/wallet/service.py b/app/domain/wallet/service.py deleted file mode 100644 index 6823fbe..0000000 --- a/app/domain/wallet/service.py +++ /dev/null @@ -1,103 +0,0 @@ -"""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 diff --git a/app/domain/alerts/__init__.py b/app/domains/alerts/__init__.py similarity index 88% rename from app/domain/alerts/__init__.py rename to app/domains/alerts/__init__.py index 5ed4401..7e0c017 100644 --- a/app/domain/alerts/__init__.py +++ b/app/domains/alerts/__init__.py @@ -57,15 +57,15 @@ health_mod.register_health_check("alerts", _health_check) # Public API -from app.domain.alerts.broadcaster import AlertBroadcaster # noqa: E402 -from app.domain.alerts.models import ( # noqa: E402 +from app.domains.alerts.broadcaster import AlertBroadcaster # noqa: E402 +from app.domains.alerts.models import ( # noqa: E402 AlertEvent, AlertSubscription, AlertType, CreateAlertRequest, ) -from app.domain.alerts.repository import AlertRepository # noqa: E402 -from app.domain.alerts.service import AlertService # noqa: E402 +from app.domains.alerts.repository import AlertRepository # noqa: E402 +from app.domains.alerts.service import AlertService # noqa: E402 __all__ = [ "AlertBroadcaster", diff --git a/app/domain/alerts/broadcaster.py b/app/domains/alerts/broadcaster.py similarity index 94% rename from app/domain/alerts/broadcaster.py rename to app/domains/alerts/broadcaster.py index ce3b203..417eb69 100644 --- a/app/domain/alerts/broadcaster.py +++ b/app/domains/alerts/broadcaster.py @@ -6,7 +6,7 @@ from __future__ import annotations from app.core.logging import get_logger from app.core.websocket import broadcast_alert -from app.domain.alerts.models import AlertEvent +from app.domains.alerts.models import AlertEvent log = get_logger(__name__) diff --git a/app/domain/alerts/models.py b/app/domains/alerts/models.py similarity index 100% rename from app/domain/alerts/models.py rename to app/domains/alerts/models.py diff --git a/app/domain/alerts/repository.py b/app/domains/alerts/repository.py similarity index 97% rename from app/domain/alerts/repository.py rename to app/domains/alerts/repository.py index 1efd38d..6088a84 100644 --- a/app/domain/alerts/repository.py +++ b/app/domains/alerts/repository.py @@ -11,7 +11,7 @@ from __future__ import annotations from app.core.logging import get_logger from app.core.redis import get_redis_async -from app.domain.alerts.models import AlertSubscription +from app.domains.alerts.models import AlertSubscription log = get_logger(__name__) diff --git a/app/domain/alerts/service.py b/app/domains/alerts/service.py similarity index 96% rename from app/domain/alerts/service.py rename to app/domains/alerts/service.py index ac5a5ae..24da186 100644 --- a/app/domain/alerts/service.py +++ b/app/domains/alerts/service.py @@ -9,14 +9,14 @@ from __future__ import annotations from datetime import datetime from app.core.logging import get_logger -from app.domain.alerts.broadcaster import AlertBroadcaster -from app.domain.alerts.models import ( +from app.domains.alerts.broadcaster import AlertBroadcaster +from app.domains.alerts.models import ( AlertEvent, AlertSubscription, AlertType, CreateAlertRequest, ) -from app.domain.alerts.repository import AlertRepository +from app.domains.alerts.repository import AlertRepository log = get_logger(__name__) diff --git a/app/domains/billing/x402/enforcement.py b/app/domains/billing/x402/enforcement.py index 4bdedab..173c32e 100755 --- a/app/domains/billing/x402/enforcement.py +++ b/app/domains/billing/x402/enforcement.py @@ -1519,7 +1519,7 @@ async def x402_enforcement_middleware(request: Request, call_next) -> Response: Intercepts /api/v1/x402-tools/* and returns 402 if no valid payment and no trials. This middleware is kept for backward compatibility. New routes should use - app.domain.x402.middleware.require_payment() decorator instead. + app.domains.x402.middleware.require_payment() decorator instead. """ path = request.url.path diff --git a/app/domain/labels/__init__.py b/app/domains/labels/__init__.py similarity index 91% rename from app/domain/labels/__init__.py rename to app/domains/labels/__init__.py index 7840800..efea816 100644 --- a/app/domain/labels/__init__.py +++ b/app/domains/labels/__init__.py @@ -20,11 +20,11 @@ Per RMIV5 v4.0 §T28 (P1): biggest moat - 5K internal labels → 200K+ via federation. """ -from app.domain.labels.federated import ( +from app.domains.labels.federated import ( FederatedLabelAPI, get_federated_api, ) -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource __all__ = [ "FederatedLabelAPI", diff --git a/app/domain/labels/federated.py b/app/domains/labels/federated.py similarity index 92% rename from app/domain/labels/federated.py rename to app/domains/labels/federated.py index c582030..ed059ab 100644 --- a/app/domain/labels/federated.py +++ b/app/domains/labels/federated.py @@ -17,7 +17,7 @@ import asyncio import logging from typing import Any -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) @@ -189,34 +189,34 @@ class FederatedLabelAPI: ) -> list[Label]: """Dispatch to the correct source adapter. Each adapter handles its own logic.""" if source == LabelSource.ETH_LABELS_DB: - from app.domain.labels.sources.eth_labels_db import query_eth_labels_db + from app.domains.labels.sources.eth_labels_db import query_eth_labels_db return await query_eth_labels_db(address, chain) if source == LabelSource.ETHERSCAN_CSV: - from app.domain.labels.sources.etherscan_csv import query_etherscan_csv + from app.domains.labels.sources.etherscan_csv import query_etherscan_csv return await query_etherscan_csv(address, chain) if source == LabelSource.ETHEREUM_LABELS_CSV: - from app.domain.labels.sources.ethereum_labels_csv import query_ethereum_labels_csv + from app.domains.labels.sources.ethereum_labels_csv import query_ethereum_labels_csv return await query_ethereum_labels_csv(address, chain) if source == LabelSource.SOLANA_LABELS_CSV: - from app.domain.labels.sources.solana_labels_csv import query_solana_labels_csv + from app.domains.labels.sources.solana_labels_csv import query_solana_labels_csv return await query_solana_labels_csv(address, chain) if source == LabelSource.CLICKHOUSE: - from app.domain.labels.sources.clickhouse_labels import query_clickhouse_labels + from app.domains.labels.sources.clickhouse_labels import query_clickhouse_labels return await query_clickhouse_labels(address, chain) if source == LabelSource.METASLEUTH: - from app.domain.labels.sources.metasleuth import query_metasleuth + from app.domains.labels.sources.metasleuth import query_metasleuth return await query_metasleuth(address, chain) if source == LabelSource.OPEN_LABELS: - from app.domain.labels.sources.open_labels import query_open_labels + from app.domains.labels.sources.open_labels import query_open_labels return await query_open_labels(address, chain) if source == LabelSource.CHAINBASE: - from app.domain.labels.sources.chainbase import query_chainbase + from app.domains.labels.sources.chainbase import query_chainbase return await query_chainbase(address, chain) if source == LabelSource.INTERNAL: - from app.domain.labels.sources.internal_labels import query_internal + from app.domains.labels.sources.internal_labels import query_internal return await query_internal(address, chain) if source == LabelSource.MBAL: - from app.domain.labels.sources.mbal import query_mbal + from app.domains.labels.sources.mbal import query_mbal return await query_mbal(address, chain) log.warning("federated_source_unknown source=%s", source.value) return [] diff --git a/app/domain/labels/models.py b/app/domains/labels/models.py similarity index 100% rename from app/domain/labels/models.py rename to app/domains/labels/models.py diff --git a/app/domain/labels/router.py b/app/domains/labels/router.py similarity index 97% rename from app/domain/labels/router.py rename to app/domains/labels/router.py index a826d6c..9f8f01e 100644 --- a/app/domain/labels/router.py +++ b/app/domains/labels/router.py @@ -19,7 +19,7 @@ import logging from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel -from app.domain.labels import get_federated_api +from app.domains.labels import get_federated_api log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/__init__.py b/app/domains/labels/sources/__init__.py similarity index 100% rename from app/domain/labels/sources/__init__.py rename to app/domains/labels/sources/__init__.py diff --git a/app/domain/labels/sources/chainbase.py b/app/domains/labels/sources/chainbase.py similarity index 91% rename from app/domain/labels/sources/chainbase.py rename to app/domains/labels/sources/chainbase.py index 599cf71..6ce81be 100644 --- a/app/domain/labels/sources/chainbase.py +++ b/app/domains/labels/sources/chainbase.py @@ -8,7 +8,7 @@ from __future__ import annotations import logging -from app.domain.labels.models import Label +from app.domains.labels.models import Label log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/clickhouse_labels.py b/app/domains/labels/sources/clickhouse_labels.py similarity index 96% rename from app/domain/labels/sources/clickhouse_labels.py rename to app/domains/labels/sources/clickhouse_labels.py index 2b54d71..6084779 100644 --- a/app/domain/labels/sources/clickhouse_labels.py +++ b/app/domains/labels/sources/clickhouse_labels.py @@ -18,7 +18,7 @@ from __future__ import annotations import logging -from app.domain.labels.models import Label +from app.domains.labels.models import Label log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/eth_labels_db.py b/app/domains/labels/sources/eth_labels_db.py similarity index 98% rename from app/domain/labels/sources/eth_labels_db.py rename to app/domains/labels/sources/eth_labels_db.py index 016e388..ad0d9ff 100644 --- a/app/domain/labels/sources/eth_labels_db.py +++ b/app/domains/labels/sources/eth_labels_db.py @@ -12,7 +12,7 @@ import asyncio import logging from datetime import UTC, datetime -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/ethereum_labels_csv.py b/app/domains/labels/sources/ethereum_labels_csv.py similarity index 97% rename from app/domain/labels/sources/ethereum_labels_csv.py rename to app/domains/labels/sources/ethereum_labels_csv.py index c994949..b1d4748 100644 --- a/app/domain/labels/sources/ethereum_labels_csv.py +++ b/app/domains/labels/sources/ethereum_labels_csv.py @@ -5,7 +5,7 @@ import asyncio import logging from datetime import UTC, datetime -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/etherscan_csv.py b/app/domains/labels/sources/etherscan_csv.py similarity index 97% rename from app/domain/labels/sources/etherscan_csv.py rename to app/domains/labels/sources/etherscan_csv.py index eafb5f6..b971cdb 100644 --- a/app/domain/labels/sources/etherscan_csv.py +++ b/app/domains/labels/sources/etherscan_csv.py @@ -5,7 +5,7 @@ import asyncio import logging from datetime import UTC, datetime -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/internal_labels.py b/app/domains/labels/sources/internal_labels.py similarity index 92% rename from app/domain/labels/sources/internal_labels.py rename to app/domains/labels/sources/internal_labels.py index e153b70..51be031 100644 --- a/app/domain/labels/sources/internal_labels.py +++ b/app/domains/labels/sources/internal_labels.py @@ -9,7 +9,7 @@ from __future__ import annotations import logging -from app.domain.labels.models import Label +from app.domains.labels.models import Label log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/mbal.py b/app/domains/labels/sources/mbal.py similarity index 85% rename from app/domain/labels/sources/mbal.py rename to app/domains/labels/sources/mbal.py index 1a89a17..1d3c7a5 100644 --- a/app/domain/labels/sources/mbal.py +++ b/app/domains/labels/sources/mbal.py @@ -10,8 +10,8 @@ Pattern: async function that returns list[Label] """ -from app.domain.labels.models import Label -from app.domain.labels.sources.mbal_source import get_mbalsy_service +from app.domains.labels.models import Label +from app.domains.labels.sources.mbal_source import get_mbalsy_service async def query_mbal(address: str, chain: str) -> list[Label]: diff --git a/app/domain/labels/sources/mbal_source.py b/app/domains/labels/sources/mbal_source.py similarity index 99% rename from app/domain/labels/sources/mbal_source.py rename to app/domains/labels/sources/mbal_source.py index c5b037a..17996f4 100644 --- a/app/domain/labels/sources/mbal_source.py +++ b/app/domains/labels/sources/mbal_source.py @@ -15,7 +15,7 @@ from datetime import datetime import httpx -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource logger = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/metasleuth.py b/app/domains/labels/sources/metasleuth.py similarity index 98% rename from app/domain/labels/sources/metasleuth.py rename to app/domains/labels/sources/metasleuth.py index 93f9fd3..2c4a4ea 100644 --- a/app/domain/labels/sources/metasleuth.py +++ b/app/domains/labels/sources/metasleuth.py @@ -18,7 +18,7 @@ from datetime import UTC, datetime import httpx -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/open_labels.py b/app/domains/labels/sources/open_labels.py similarity index 91% rename from app/domain/labels/sources/open_labels.py rename to app/domains/labels/sources/open_labels.py index 7465f6c..f78215f 100644 --- a/app/domain/labels/sources/open_labels.py +++ b/app/domains/labels/sources/open_labels.py @@ -9,7 +9,7 @@ from __future__ import annotations import logging -from app.domain.labels.models import Label +from app.domains.labels.models import Label log = logging.getLogger(__name__) diff --git a/app/domain/labels/sources/solana_labels_csv.py b/app/domains/labels/sources/solana_labels_csv.py similarity index 97% rename from app/domain/labels/sources/solana_labels_csv.py rename to app/domains/labels/sources/solana_labels_csv.py index e5d553f..7e455a3 100644 --- a/app/domain/labels/sources/solana_labels_csv.py +++ b/app/domains/labels/sources/solana_labels_csv.py @@ -5,7 +5,7 @@ import asyncio import logging from datetime import UTC, datetime -from app.domain.labels.models import Label, LabelSource +from app.domains.labels.models import Label, LabelSource log = logging.getLogger(__name__) diff --git a/app/domain/news/__init__.py b/app/domains/news/__init__.py similarity index 100% rename from app/domain/news/__init__.py rename to app/domains/news/__init__.py diff --git a/app/domain/news/admin_router.py b/app/domains/news/admin_router.py similarity index 87% rename from app/domain/news/admin_router.py rename to app/domains/news/admin_router.py index bd0ab32..13d7419 100644 --- a/app/domain/news/admin_router.py +++ b/app/domains/news/admin_router.py @@ -2,7 +2,7 @@ from fastapi import APIRouter -from app.domain.news.ingest import ingest_all +from app.domains.news.ingest import ingest_all router = APIRouter(prefix="/api/v1/news/_admin", tags=["news-admin"]) diff --git a/app/domain/news/clusterer.py b/app/domains/news/clusterer.py similarity index 100% rename from app/domain/news/clusterer.py rename to app/domains/news/clusterer.py diff --git a/app/domain/news/ingest.py b/app/domains/news/ingest.py similarity index 100% rename from app/domain/news/ingest.py rename to app/domains/news/ingest.py diff --git a/app/domain/news/router.py b/app/domains/news/router.py similarity index 99% rename from app/domain/news/router.py rename to app/domains/news/router.py index 38b3a35..5649afd 100644 --- a/app/domain/news/router.py +++ b/app/domains/news/router.py @@ -228,7 +228,7 @@ async def list_news( # T03: cluster into stories if requested if clustered and items: - from app.domain.news.clusterer import NewsItem, cluster_items, persist_clusters + from app.domains.news.clusterer import NewsItem, cluster_items, persist_clusters cluster_items_list = [ NewsItem( diff --git a/app/domain/reports/__init__.py b/app/domains/reports/__init__.py similarity index 100% rename from app/domain/reports/__init__.py rename to app/domains/reports/__init__.py diff --git a/app/domain/reports/citation_validator.py b/app/domains/reports/citation_validator.py similarity index 100% rename from app/domain/reports/citation_validator.py rename to app/domains/reports/citation_validator.py diff --git a/app/domain/reports/generator.py b/app/domains/reports/generator.py similarity index 99% rename from app/domain/reports/generator.py rename to app/domains/reports/generator.py index 03f6a7c..2f9f69e 100644 --- a/app/domain/reports/generator.py +++ b/app/domains/reports/generator.py @@ -32,7 +32,7 @@ from app.catalog.models import ( ScanReport, utcnow, ) -from app.domain.reports.citation_validator import validate_section +from app.domains.reports.citation_validator import validate_section log = logging.getLogger(__name__) @@ -166,7 +166,7 @@ async def _fetch_news(catalog, subject_id: str, since_hours: int = 720) -> list: subject_id, f"%{subject_id.split(':')[-1][:8]}%", ) - from app.domain.news.router import _adapt_legacy_row as _adapt_news_row + from app.domains.news.router import _adapt_legacy_row as _adapt_news_row return [_adapt_news_row(dict(r)) for r in rows] except Exception as e: diff --git a/app/domain/reports/router.py b/app/domains/reports/router.py similarity index 99% rename from app/domain/reports/router.py rename to app/domains/reports/router.py index 08da467..33abae4 100644 --- a/app/domain/reports/router.py +++ b/app/domains/reports/router.py @@ -20,7 +20,7 @@ from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from app.catalog.service import get_catalog -from app.domain.reports.generator import ( +from app.domains.reports.generator import ( generate_token_report, generate_wallet_report, save_report, diff --git a/app/domain/scanner/__init__.py b/app/domains/scanner/__init__.py similarity index 90% rename from app/domain/scanner/__init__.py rename to app/domains/scanner/__init__.py index d2c6e70..64b7952 100644 --- a/app/domain/scanner/__init__.py +++ b/app/domains/scanner/__init__.py @@ -15,6 +15,6 @@ Architecture: - app/token_scanner.py Backward compatibility shim """ -from app.domain.scanner.service import ScanResult, quick_scan_text, scan_token +from app.domains.scanner.service import ScanResult, quick_scan_text, scan_token __all__ = ["ScanResult", "quick_scan_text", "scan_token"] diff --git a/app/domain/scanner/market_data.py b/app/domains/scanner/market_data.py similarity index 98% rename from app/domain/scanner/market_data.py rename to app/domains/scanner/market_data.py index dc30b6a..4fd7923 100644 --- a/app/domain/scanner/market_data.py +++ b/app/domains/scanner/market_data.py @@ -4,7 +4,7 @@ from typing import Any import httpx -from app.domain.scanner.models import SOLANA_RPC_ENDPOINTS +from app.domains.scanner.models import SOLANA_RPC_ENDPOINTS logger = __import__("app.core.logging", fromlist=["get_logger"]).get_logger(__name__) diff --git a/app/domain/scanner/models.py b/app/domains/scanner/models.py similarity index 100% rename from app/domain/scanner/models.py rename to app/domains/scanner/models.py diff --git a/app/domain/scanner/modules.py b/app/domains/scanner/modules.py similarity index 100% rename from app/domain/scanner/modules.py rename to app/domains/scanner/modules.py diff --git a/app/domain/scanner/service.py b/app/domains/scanner/service.py similarity index 97% rename from app/domain/scanner/service.py rename to app/domains/scanner/service.py index 6750bc1..f149461 100644 --- a/app/domain/scanner/service.py +++ b/app/domains/scanner/service.py @@ -4,7 +4,7 @@ import asyncio from typing import Any from app.core.logging import get_logger -from app.domain.scanner.modules import ( +from app.domains.scanner.modules import ( _get_deployer_info, _get_holder_data, ) @@ -12,9 +12,9 @@ from app.domain.scanner.modules import ( logger = get_logger(__name__) # Re-export ScanResult for backward compatibility -from app.domain.scanner.market_data import fetch_market_data # noqa: E402 -from app.domain.scanner.models import ScanResult # noqa: E402 -from app.domain.scanner.modules import ( # noqa: E402 +from app.domains.scanner.market_data import fetch_market_data # noqa: E402 +from app.domains.scanner.models import ScanResult # noqa: E402 +from app.domains.scanner.modules import ( # noqa: E402 _check_blockscout, _check_defi_scanner, _check_honeypot_is, @@ -297,4 +297,4 @@ async def quick_scan_text(token_address: str, chain: str = "solana") -> str: # Backward compatibility: re-export scan_token -from app.domain.scanner.service import scan_token # noqa: E402, F811 +from app.domains.scanner.service import scan_token # noqa: E402, F811 diff --git a/app/domain/threat/brand_patterns.json b/app/domains/threat/brand_patterns.json similarity index 100% rename from app/domain/threat/brand_patterns.json rename to app/domains/threat/brand_patterns.json diff --git a/app/domain/threat/certstream_listener.py b/app/domains/threat/certstream_listener.py similarity index 98% rename from app/domain/threat/certstream_listener.py rename to app/domains/threat/certstream_listener.py index 47d980d..993476b 100644 --- a/app/domain/threat/certstream_listener.py +++ b/app/domains/threat/certstream_listener.py @@ -13,9 +13,9 @@ seeds. CT logs show new domains BEFORE they go live, giving a 48h lead time to take them down. Usage: - from app.domain.threat.certstream_listener import start_listener + from app.domains.threat.certstream_listener import start_listener asyncio.create_task(start_listener()) # in lifespan - python3 -m app.domain.threat.certstream_listener --duration 300 # CLI + python3 -m app.domains.threat.certstream_listener --duration 300 # CLI """ from __future__ import annotations diff --git a/app/domain/token/__init__.py b/app/domains/token/__init__.py similarity index 79% rename from app/domain/token/__init__.py rename to app/domains/token/__init__.py index 34a73bf..1b9ef6b 100644 --- a/app/domain/token/__init__.py +++ b/app/domains/token/__init__.py @@ -22,8 +22,8 @@ health_mod.register_health_check("token", _health_check) # Public API -from app.domain.token.analyzer import TokenAnalyzer # noqa: E402 -from app.domain.token.models import ( # noqa: E402 +from app.domains.token.analyzer import TokenAnalyzer # noqa: E402 +from app.domains.token.models import ( # noqa: E402 RiskLevel, Token, TokenDetail, @@ -33,8 +33,8 @@ from app.domain.token.models import ( # noqa: E402 TokenScanRequest, TokenScanResult, ) -from app.domain.token.repository import TokenRepository # noqa: E402 -from app.domain.token.service import TokenService # noqa: E402 +from app.domains.token.repository import TokenRepository # noqa: E402 +from app.domains.token.service import TokenService # noqa: E402 __all__ = [ "RiskLevel", diff --git a/app/domain/token/analyzer.py b/app/domains/token/analyzer.py similarity index 98% rename from app/domain/token/analyzer.py rename to app/domains/token/analyzer.py index f6be51a..fa840af 100644 --- a/app/domain/token/analyzer.py +++ b/app/domains/token/analyzer.py @@ -1,7 +1,7 @@ """Pure-Python token risk analysis. No I/O.""" from __future__ import annotations -from app.domain.token.models import ( +from app.domains.token.models import ( RiskLevel, TokenHolder, TokenLiquidity, diff --git a/app/domain/token/models.py b/app/domains/token/models.py similarity index 100% rename from app/domain/token/models.py rename to app/domains/token/models.py diff --git a/app/domain/token/repository.py b/app/domains/token/repository.py similarity index 98% rename from app/domain/token/repository.py rename to app/domains/token/repository.py index f4bcec6..e2936df 100644 --- a/app/domain/token/repository.py +++ b/app/domains/token/repository.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from app.core.logging import get_logger -from app.domain.token.models import ( +from app.domains.token.models import ( TokenDetail, TokenHolder, TokenLiquidity, diff --git a/app/domain/token/service.py b/app/domains/token/service.py similarity index 95% rename from app/domain/token/service.py rename to app/domains/token/service.py index 7f8fc12..70198b1 100644 --- a/app/domain/token/service.py +++ b/app/domains/token/service.py @@ -2,8 +2,8 @@ from __future__ import annotations from app.core.logging import get_logger -from app.domain.token.analyzer import TokenAnalyzer -from app.domain.token.models import ( +from app.domains.token.analyzer import TokenAnalyzer +from app.domains.token.models import ( TokenDetail, TokenHolder, TokenLiquidity, @@ -11,7 +11,7 @@ from app.domain.token.models import ( TokenScanRequest, TokenScanResult, ) -from app.domain.token.repository import TokenRepository +from app.domains.token.repository import TokenRepository log = get_logger(__name__) diff --git a/app/domains/tokens/__init__.py b/app/domains/tokens/__init__.py index 7b7456c..3d1089f 100644 --- a/app/domains/tokens/__init__.py +++ b/app/domains/tokens/__init__.py @@ -6,7 +6,7 @@ Re-exports the canonical public API of the token deployer. Implementation lives in app.tokens.deployer (moved verbatim from app.token_deployer on 2026-07-07). """ -from app.tokens.deployer import ( # noqa: F401 +from app.domains.tokens.deployer import ( # noqa: F401 ChainDeployer, DeployParams, DeploymentStorage, diff --git a/app/domain/x402/__init__.py b/app/domains/x402/__init__.py similarity index 79% rename from app/domain/x402/__init__.py rename to app/domains/x402/__init__.py index 3a62f52..26e08de 100644 --- a/app/domain/x402/__init__.py +++ b/app/domains/x402/__init__.py @@ -3,7 +3,7 @@ T34 from v4.0. Sovereign-first x402 payment layer for AI agents. Re-exports the legacy models (PaymentFacilitator, X402Tier) for backward -compatibility with v1 routers that import from app.domain.x402. +compatibility with v1 routers that import from app.domains.x402. """ from __future__ import annotations @@ -14,7 +14,7 @@ from app.core.health import DomainHealth async def _health_check() -> DomainHealth: """x402 health: catalog + middleware available.""" try: - from app.domain.x402.middleware import PRICING + from app.domains.x402.middleware import PRICING return DomainHealth( name="x402", healthy=True, @@ -28,7 +28,7 @@ health_mod.register_health_check("x402", _health_check) # Re-export models for backward compat with v1 routers and tests -from app.domain.x402.models import ( # noqa: E402 +from app.domains.x402.models import ( # noqa: E402 PaymentFacilitator, PaymentReceipt, ToolCatalog, @@ -38,8 +38,8 @@ from app.domain.x402.models import ( # noqa: E402 ) # Re-export the new T34 router -from app.domain.x402.router import router # noqa: E402 -from app.domain.x402.service import X402Service # noqa: E402 +from app.domains.x402.router import router # noqa: E402 +from app.domains.x402.service import X402Service # noqa: E402 __all__ = [ "PaymentFacilitator", diff --git a/app/domain/x402/middleware.py b/app/domains/x402/middleware.py similarity index 100% rename from app/domain/x402/middleware.py rename to app/domains/x402/middleware.py diff --git a/app/domain/x402/models.py b/app/domains/x402/models.py similarity index 100% rename from app/domain/x402/models.py rename to app/domains/x402/models.py diff --git a/app/domain/x402/router.py b/app/domains/x402/router.py similarity index 99% rename from app/domain/x402/router.py rename to app/domains/x402/router.py index 3ba4c45..564bd04 100644 --- a/app/domain/x402/router.py +++ b/app/domains/x402/router.py @@ -15,7 +15,7 @@ from fastapi import APIRouter, HTTPException, Request from pydantic import BaseModel, Field from app.catalog.service import get_catalog -from app.domain.x402.middleware import ( +from app.domains.x402.middleware import ( PRICING, get_tool_metadata, get_tool_price_usd, diff --git a/app/domain/x402/service.py b/app/domains/x402/service.py similarity index 99% rename from app/domain/x402/service.py rename to app/domains/x402/service.py index ebb96a9..19911b5 100644 --- a/app/domain/x402/service.py +++ b/app/domains/x402/service.py @@ -10,7 +10,7 @@ import asyncio from typing import Any from app.core.logging import get_logger -from app.domain.x402.models import ( +from app.domains.x402.models import ( PaymentFacilitator, ToolCatalog, ToolCatalogEntry, diff --git a/app/domain/x402/tools/__init__.py b/app/domains/x402/tools/__init__.py similarity index 88% rename from app/domain/x402/tools/__init__.py rename to app/domains/x402/tools/__init__.py index 4b5a1dd..e8447d2 100644 --- a/app/domain/x402/tools/__init__.py +++ b/app/domains/x402/tools/__init__.py @@ -15,13 +15,13 @@ Registry shape: } """ -from app.domain.x402.tools.label_tools import get_entity_labels, get_wallet_labels -from app.domain.x402.tools.market_tools import get_market_overview, get_trending -from app.domain.x402.tools.news_tools import get_news, get_news_sentiment -from app.domain.x402.tools.report_tools import generate_report, get_report -from app.domain.x402.tools.scanner_tools import get_scan_result, run_scan -from app.domain.x402.tools.token_tools import get_token_info, get_token_risk -from app.domain.x402.tools.wallet_tools import get_wallet_analysis, get_wallet_history +from app.domains.x402.tools.label_tools import get_entity_labels, get_wallet_labels +from app.domains.x402.tools.market_tools import get_market_overview, get_trending +from app.domains.x402.tools.news_tools import get_news, get_news_sentiment +from app.domains.x402.tools.report_tools import generate_report, get_report +from app.domains.x402.tools.scanner_tools import get_scan_result, run_scan +from app.domains.x402.tools.token_tools import get_token_info, get_token_risk +from app.domains.x402.tools.wallet_tools import get_wallet_analysis, get_wallet_history # Tool registry TOOLS = { diff --git a/app/domain/x402/tools/label_tools.py b/app/domains/x402/tools/label_tools.py similarity index 100% rename from app/domain/x402/tools/label_tools.py rename to app/domains/x402/tools/label_tools.py diff --git a/app/domain/x402/tools/market_tools.py b/app/domains/x402/tools/market_tools.py similarity index 100% rename from app/domain/x402/tools/market_tools.py rename to app/domains/x402/tools/market_tools.py diff --git a/app/domain/x402/tools/news_tools.py b/app/domains/x402/tools/news_tools.py similarity index 100% rename from app/domain/x402/tools/news_tools.py rename to app/domains/x402/tools/news_tools.py diff --git a/app/domain/x402/tools/report_tools.py b/app/domains/x402/tools/report_tools.py similarity index 100% rename from app/domain/x402/tools/report_tools.py rename to app/domains/x402/tools/report_tools.py diff --git a/app/domain/x402/tools/scanner_tools.py b/app/domains/x402/tools/scanner_tools.py similarity index 100% rename from app/domain/x402/tools/scanner_tools.py rename to app/domains/x402/tools/scanner_tools.py diff --git a/app/domain/x402/tools/token_tools.py b/app/domains/x402/tools/token_tools.py similarity index 100% rename from app/domain/x402/tools/token_tools.py rename to app/domains/x402/tools/token_tools.py diff --git a/app/domain/x402/tools/wallet_tools.py b/app/domains/x402/tools/wallet_tools.py similarity index 100% rename from app/domain/x402/tools/wallet_tools.py rename to app/domains/x402/tools/wallet_tools.py diff --git a/app/lifespan.py b/app/lifespan.py index 31a421a..79b9d1f 100644 --- a/app/lifespan.py +++ b/app/lifespan.py @@ -78,7 +78,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: # 7. T12 - CertStream phishing domain monitor (background task) certstream_task = None try: - from app.domain.threat.certstream_listener import start_listener + from app.domains.threat.certstream_listener import start_listener certstream_task = await start_listener() log.info("certstream_started has_task=%s", certstream_task is not None) except Exception as exc: @@ -88,7 +88,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: # Shutdown try: - from app.domain.threat.certstream_listener import stop_listener + from app.domains.threat.certstream_listener import stop_listener await stop_listener(certstream_task) except Exception: pass diff --git a/app/mcp/server.py b/app/mcp/server.py index f7c4342..3c165c7 100644 --- a/app/mcp/server.py +++ b/app/mcp/server.py @@ -509,7 +509,7 @@ async def call_tool(name: str, arguments: dict) -> dict: return {"error": f"news_query_fail: {e}"} if name == "generate_report": - from app.domain.reports.generator import generate_token_report, generate_wallet_report + from app.domains.reports.generator import generate_token_report, generate_wallet_report chain, address = arguments["subject_id"].split(":", 1) try: @@ -517,7 +517,7 @@ async def call_tool(name: str, arguments: dict) -> dict: report = await generate_token_report(catalog, chain, address) else: report = await generate_wallet_report(catalog, chain, address) - from app.domain.reports.generator import save_report + from app.domains.reports.generator import save_report await save_report(catalog, report) return { diff --git a/app/mcp/x402_mcp_server.py b/app/mcp/x402_mcp_server.py index 25715ca..c41ff8d 100644 --- a/app/mcp/x402_mcp_server.py +++ b/app/mcp/x402_mcp_server.py @@ -116,7 +116,7 @@ TOOLS = [ async def handle_mcp_call(tool_name: str, arguments: dict[str, Any]) -> dict: """Route MCP tool calls to the appropriate handler.""" - from app.domain.x402.middleware import create_invoice + from app.domains.x402.middleware import create_invoice from app.routers.x402_enforcement import TOOL_PRICES, _ensure_tool_prices if tool_name == "x402_create_invoice": diff --git a/app/mount.py b/app/mount.py index 5b3169d..11095e2 100644 --- a/app/mount.py +++ b/app/mount.py @@ -38,10 +38,10 @@ ROUTER_MODULES: Final[list[str]] = [ "app.api.v1.catalog", # /api/v1/catalog/* "app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON) # Domain facades (per v4.0 T28-T34) - "app.domain.news", # /api/v1/news/* - "app.domain.news.admin_router", # /api/v1/news/_admin/* - "app.domain.reports", # /api/v1/reports/* - "app.domain.x402", # /api/v1/x402/* + "app.domains.news", # /api/v1/news/* + "app.domains.news.admin_router", # /api/v1/news/_admin/* + "app.domains.reports", # /api/v1/reports/* + "app.domains.x402", # /api/v1/x402/* # x402 MCP + Discovery + Docs "app.routers.x402_mcp_handler", # /mcp/x402, /.well-known/x402 "app.routers.x402_docs", # /x402/docs, /x402/sandbox/* diff --git a/app/routers/x402_docs.py b/app/routers/x402_docs.py index 1b8b7f5..b0e672f 100644 --- a/app/routers/x402_docs.py +++ b/app/routers/x402_docs.py @@ -148,7 +148,7 @@ async def sandbox_create_invoice(tool: str = Query(...), mode: str = Query("test Use ?mode=test to get a fake invoice for integration testing. The invoice includes a mock payment that self-verifies. """ - from app.domain.x402.middleware import create_invoice + from app.domains.x402.middleware import create_invoice invoice = create_invoice(tool, "sandbox_user") if mode == "test": diff --git a/app/token_deployer.py b/app/token_deployer.py index 63f9128..75df8ff 100644 --- a/app/token_deployer.py +++ b/app/token_deployer.py @@ -1,13 +1,19 @@ -"""Backward-compat shim - moved to app.tokens.deployer in P3B.""" -from app.tokens.deployer import * # noqa: F401,F403 -from app.tokens.deployer import ( # noqa: F401 +"""token_deployer.py - DEPRECATED shim. Use app.domains.tokens.deployer. + +Phase 4 of AUDIT-2026-Q3.md moved this to app/domains/tokens/deployer/. +This shim re-exports the public surface for legacy callers. +""" +from app.domains.tokens.deployer import * # noqa: F401,F403 +from app.domains.tokens.deployer import ( # noqa: F401 ChainDeployer, DeployParams, DeploymentStorage, EVMDeployer, SolanaDeployer, - TokenDeployment, TokenDeployerFactory, + TokenDeployment, TronDeployer, get_storage, + logger, + _storage, ) diff --git a/app/token_scanner.py b/app/token_scanner.py index 5aaedbb..d0644b4 100644 --- a/app/token_scanner.py +++ b/app/token_scanner.py @@ -6,6 +6,6 @@ better organization (per architecture standard: NO file > 500 lines). This file re-exports the main API from the new modules. """ -from app.domain.scanner import ScanResult, quick_scan_text, scan_token +from app.domains.scanner import ScanResult, quick_scan_text, scan_token __all__ = ["ScanResult", "quick_scan_text", "scan_token"]