refactor(domains): rename app/domain/ to app/domains/ + consolidate (P4.7)
Some checks failed
CI / build (push) Failing after 2s

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)
This commit is contained in:
Crypto Rug Munch 2026-07-06 23:08:17 +02:00
parent 56075cfffa
commit 3b7ef428a9
72 changed files with 122 additions and 625 deletions

View file

@ -49,7 +49,7 @@ from app.api.v1.public.scanner import router as scanner_router # noqa: E402
api_v1_router.append(scanner_router) 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 # 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 from app.api.v1.rag.search import router as rag_v2_router # noqa: E402

View file

@ -37,7 +37,7 @@ async def scan(req: ScanRequest) -> ScanResult:
"""Queue a token/wallet scan.""" """Queue a token/wallet scan."""
raise HTTPException( raise HTTPException(
status_code=501, 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+)",
) )

View file

@ -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')

View file

@ -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",
]

View file

@ -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

View file

@ -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)

View file

@ -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"),
)

View file

@ -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

View file

@ -57,15 +57,15 @@ health_mod.register_health_check("alerts", _health_check)
# Public API # Public API
from app.domain.alerts.broadcaster import AlertBroadcaster # noqa: E402 from app.domains.alerts.broadcaster import AlertBroadcaster # noqa: E402
from app.domain.alerts.models import ( # noqa: E402 from app.domains.alerts.models import ( # noqa: E402
AlertEvent, AlertEvent,
AlertSubscription, AlertSubscription,
AlertType, AlertType,
CreateAlertRequest, CreateAlertRequest,
) )
from app.domain.alerts.repository import AlertRepository # noqa: E402 from app.domains.alerts.repository import AlertRepository # noqa: E402
from app.domain.alerts.service import AlertService # noqa: E402 from app.domains.alerts.service import AlertService # noqa: E402
__all__ = [ __all__ = [
"AlertBroadcaster", "AlertBroadcaster",

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from app.core.logging import get_logger from app.core.logging import get_logger
from app.core.websocket import broadcast_alert 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__) log = get_logger(__name__)

View file

@ -11,7 +11,7 @@ from __future__ import annotations
from app.core.logging import get_logger from app.core.logging import get_logger
from app.core.redis import get_redis_async 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__) log = get_logger(__name__)

View file

@ -9,14 +9,14 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from app.core.logging import get_logger from app.core.logging import get_logger
from app.domain.alerts.broadcaster import AlertBroadcaster from app.domains.alerts.broadcaster import AlertBroadcaster
from app.domain.alerts.models import ( from app.domains.alerts.models import (
AlertEvent, AlertEvent,
AlertSubscription, AlertSubscription,
AlertType, AlertType,
CreateAlertRequest, CreateAlertRequest,
) )
from app.domain.alerts.repository import AlertRepository from app.domains.alerts.repository import AlertRepository
log = get_logger(__name__) log = get_logger(__name__)

View file

@ -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. 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 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 path = request.url.path

View file

@ -20,11 +20,11 @@ Per RMIV5 v4.0 §T28 (P1): biggest moat - 5K internal labels → 200K+
via federation. via federation.
""" """
from app.domain.labels.federated import ( from app.domains.labels.federated import (
FederatedLabelAPI, FederatedLabelAPI,
get_federated_api, get_federated_api,
) )
from app.domain.labels.models import Label, LabelSource from app.domains.labels.models import Label, LabelSource
__all__ = [ __all__ = [
"FederatedLabelAPI", "FederatedLabelAPI",

View file

@ -17,7 +17,7 @@ import asyncio
import logging import logging
from typing import Any from typing import Any
from app.domain.labels.models import Label, LabelSource from app.domains.labels.models import Label, LabelSource
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -189,34 +189,34 @@ class FederatedLabelAPI:
) -> list[Label]: ) -> list[Label]:
"""Dispatch to the correct source adapter. Each adapter handles its own logic.""" """Dispatch to the correct source adapter. Each adapter handles its own logic."""
if source == LabelSource.ETH_LABELS_DB: 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) return await query_eth_labels_db(address, chain)
if source == LabelSource.ETHERSCAN_CSV: 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) return await query_etherscan_csv(address, chain)
if source == LabelSource.ETHEREUM_LABELS_CSV: 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) return await query_ethereum_labels_csv(address, chain)
if source == LabelSource.SOLANA_LABELS_CSV: 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) return await query_solana_labels_csv(address, chain)
if source == LabelSource.CLICKHOUSE: 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) return await query_clickhouse_labels(address, chain)
if source == LabelSource.METASLEUTH: 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) return await query_metasleuth(address, chain)
if source == LabelSource.OPEN_LABELS: 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) return await query_open_labels(address, chain)
if source == LabelSource.CHAINBASE: 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) return await query_chainbase(address, chain)
if source == LabelSource.INTERNAL: 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) return await query_internal(address, chain)
if source == LabelSource.MBAL: 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) return await query_mbal(address, chain)
log.warning("federated_source_unknown source=%s", source.value) log.warning("federated_source_unknown source=%s", source.value)
return [] return []

View file

@ -19,7 +19,7 @@ import logging
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
from app.domain.labels import get_federated_api from app.domains.labels import get_federated_api
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -8,7 +8,7 @@ from __future__ import annotations
import logging import logging
from app.domain.labels.models import Label from app.domains.labels.models import Label
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -18,7 +18,7 @@ from __future__ import annotations
import logging import logging
from app.domain.labels.models import Label from app.domains.labels.models import Label
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -12,7 +12,7 @@ import asyncio
import logging import logging
from datetime import UTC, datetime 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__) log = logging.getLogger(__name__)

View file

@ -5,7 +5,7 @@ import asyncio
import logging import logging
from datetime import UTC, datetime 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__) log = logging.getLogger(__name__)

View file

@ -5,7 +5,7 @@ import asyncio
import logging import logging
from datetime import UTC, datetime 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__) log = logging.getLogger(__name__)

View file

@ -9,7 +9,7 @@ from __future__ import annotations
import logging import logging
from app.domain.labels.models import Label from app.domains.labels.models import Label
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -10,8 +10,8 @@ Pattern: async function that returns list[Label]
""" """
from app.domain.labels.models import Label from app.domains.labels.models import Label
from app.domain.labels.sources.mbal_source import get_mbalsy_service from app.domains.labels.sources.mbal_source import get_mbalsy_service
async def query_mbal(address: str, chain: str) -> list[Label]: async def query_mbal(address: str, chain: str) -> list[Label]:

View file

@ -15,7 +15,7 @@ from datetime import datetime
import httpx import httpx
from app.domain.labels.models import Label, LabelSource from app.domains.labels.models import Label, LabelSource
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -18,7 +18,7 @@ from datetime import UTC, datetime
import httpx import httpx
from app.domain.labels.models import Label, LabelSource from app.domains.labels.models import Label, LabelSource
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -9,7 +9,7 @@ from __future__ import annotations
import logging import logging
from app.domain.labels.models import Label from app.domains.labels.models import Label
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -5,7 +5,7 @@ import asyncio
import logging import logging
from datetime import UTC, datetime 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__) log = logging.getLogger(__name__)

View file

@ -2,7 +2,7 @@
from fastapi import APIRouter 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"]) router = APIRouter(prefix="/api/v1/news/_admin", tags=["news-admin"])

View file

@ -228,7 +228,7 @@ async def list_news(
# T03: cluster into stories if requested # T03: cluster into stories if requested
if clustered and items: 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 = [ cluster_items_list = [
NewsItem( NewsItem(

View file

@ -32,7 +32,7 @@ from app.catalog.models import (
ScanReport, ScanReport,
utcnow, utcnow,
) )
from app.domain.reports.citation_validator import validate_section from app.domains.reports.citation_validator import validate_section
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -166,7 +166,7 @@ async def _fetch_news(catalog, subject_id: str, since_hours: int = 720) -> list:
subject_id, subject_id,
f"%{subject_id.split(':')[-1][:8]}%", 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] return [_adapt_news_row(dict(r)) for r in rows]
except Exception as e: except Exception as e:

View file

@ -20,7 +20,7 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.catalog.service import get_catalog from app.catalog.service import get_catalog
from app.domain.reports.generator import ( from app.domains.reports.generator import (
generate_token_report, generate_token_report,
generate_wallet_report, generate_wallet_report,
save_report, save_report,

View file

@ -15,6 +15,6 @@ Architecture:
- app/token_scanner.py Backward compatibility shim - 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"] __all__ = ["ScanResult", "quick_scan_text", "scan_token"]

View file

@ -4,7 +4,7 @@ from typing import Any
import httpx 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__) logger = __import__("app.core.logging", fromlist=["get_logger"]).get_logger(__name__)

View file

@ -4,7 +4,7 @@ import asyncio
from typing import Any from typing import Any
from app.core.logging import get_logger from app.core.logging import get_logger
from app.domain.scanner.modules import ( from app.domains.scanner.modules import (
_get_deployer_info, _get_deployer_info,
_get_holder_data, _get_holder_data,
) )
@ -12,9 +12,9 @@ from app.domain.scanner.modules import (
logger = get_logger(__name__) logger = get_logger(__name__)
# Re-export ScanResult for backward compatibility # Re-export ScanResult for backward compatibility
from app.domain.scanner.market_data import fetch_market_data # noqa: E402 from app.domains.scanner.market_data import fetch_market_data # noqa: E402
from app.domain.scanner.models import ScanResult # noqa: E402 from app.domains.scanner.models import ScanResult # noqa: E402
from app.domain.scanner.modules import ( # noqa: E402 from app.domains.scanner.modules import ( # noqa: E402
_check_blockscout, _check_blockscout,
_check_defi_scanner, _check_defi_scanner,
_check_honeypot_is, _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 # 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

View file

@ -13,9 +13,9 @@ seeds. CT logs show new domains BEFORE they go live, giving a 48h lead
time to take them down. time to take them down.
Usage: 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 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 from __future__ import annotations

View file

@ -22,8 +22,8 @@ health_mod.register_health_check("token", _health_check)
# Public API # Public API
from app.domain.token.analyzer import TokenAnalyzer # noqa: E402 from app.domains.token.analyzer import TokenAnalyzer # noqa: E402
from app.domain.token.models import ( # noqa: E402 from app.domains.token.models import ( # noqa: E402
RiskLevel, RiskLevel,
Token, Token,
TokenDetail, TokenDetail,
@ -33,8 +33,8 @@ from app.domain.token.models import ( # noqa: E402
TokenScanRequest, TokenScanRequest,
TokenScanResult, TokenScanResult,
) )
from app.domain.token.repository import TokenRepository # noqa: E402 from app.domains.token.repository import TokenRepository # noqa: E402
from app.domain.token.service import TokenService # noqa: E402 from app.domains.token.service import TokenService # noqa: E402
__all__ = [ __all__ = [
"RiskLevel", "RiskLevel",

View file

@ -1,7 +1,7 @@
"""Pure-Python token risk analysis. No I/O.""" """Pure-Python token risk analysis. No I/O."""
from __future__ import annotations from __future__ import annotations
from app.domain.token.models import ( from app.domains.token.models import (
RiskLevel, RiskLevel,
TokenHolder, TokenHolder,
TokenLiquidity, TokenLiquidity,

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from app.core.logging import get_logger from app.core.logging import get_logger
from app.domain.token.models import ( from app.domains.token.models import (
TokenDetail, TokenDetail,
TokenHolder, TokenHolder,
TokenLiquidity, TokenLiquidity,

View file

@ -2,8 +2,8 @@
from __future__ import annotations from __future__ import annotations
from app.core.logging import get_logger from app.core.logging import get_logger
from app.domain.token.analyzer import TokenAnalyzer from app.domains.token.analyzer import TokenAnalyzer
from app.domain.token.models import ( from app.domains.token.models import (
TokenDetail, TokenDetail,
TokenHolder, TokenHolder,
TokenLiquidity, TokenLiquidity,
@ -11,7 +11,7 @@ from app.domain.token.models import (
TokenScanRequest, TokenScanRequest,
TokenScanResult, TokenScanResult,
) )
from app.domain.token.repository import TokenRepository from app.domains.token.repository import TokenRepository
log = get_logger(__name__) log = get_logger(__name__)

View file

@ -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 lives in app.tokens.deployer (moved verbatim from app.token_deployer on
2026-07-07). 2026-07-07).
""" """
from app.tokens.deployer import ( # noqa: F401 from app.domains.tokens.deployer import ( # noqa: F401
ChainDeployer, ChainDeployer,
DeployParams, DeployParams,
DeploymentStorage, DeploymentStorage,

View file

@ -3,7 +3,7 @@
T34 from v4.0. Sovereign-first x402 payment layer for AI agents. T34 from v4.0. Sovereign-first x402 payment layer for AI agents.
Re-exports the legacy models (PaymentFacilitator, X402Tier) for backward 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 from __future__ import annotations
@ -14,7 +14,7 @@ from app.core.health import DomainHealth
async def _health_check() -> DomainHealth: async def _health_check() -> DomainHealth:
"""x402 health: catalog + middleware available.""" """x402 health: catalog + middleware available."""
try: try:
from app.domain.x402.middleware import PRICING from app.domains.x402.middleware import PRICING
return DomainHealth( return DomainHealth(
name="x402", name="x402",
healthy=True, 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 # 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, PaymentFacilitator,
PaymentReceipt, PaymentReceipt,
ToolCatalog, ToolCatalog,
@ -38,8 +38,8 @@ from app.domain.x402.models import ( # noqa: E402
) )
# Re-export the new T34 router # Re-export the new T34 router
from app.domain.x402.router import router # noqa: E402 from app.domains.x402.router import router # noqa: E402
from app.domain.x402.service import X402Service # noqa: E402 from app.domains.x402.service import X402Service # noqa: E402
__all__ = [ __all__ = [
"PaymentFacilitator", "PaymentFacilitator",

View file

@ -15,7 +15,7 @@ from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.catalog.service import get_catalog from app.catalog.service import get_catalog
from app.domain.x402.middleware import ( from app.domains.x402.middleware import (
PRICING, PRICING,
get_tool_metadata, get_tool_metadata,
get_tool_price_usd, get_tool_price_usd,

View file

@ -10,7 +10,7 @@ import asyncio
from typing import Any from typing import Any
from app.core.logging import get_logger from app.core.logging import get_logger
from app.domain.x402.models import ( from app.domains.x402.models import (
PaymentFacilitator, PaymentFacilitator,
ToolCatalog, ToolCatalog,
ToolCatalogEntry, ToolCatalogEntry,

View file

@ -15,13 +15,13 @@ Registry shape:
} }
""" """
from app.domain.x402.tools.label_tools import get_entity_labels, get_wallet_labels from app.domains.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.domains.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.domains.x402.tools.news_tools import get_news, get_news_sentiment
from app.domain.x402.tools.report_tools import generate_report, get_report from app.domains.x402.tools.report_tools import generate_report, get_report
from app.domain.x402.tools.scanner_tools import get_scan_result, run_scan from app.domains.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.domains.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.wallet_tools import get_wallet_analysis, get_wallet_history
# Tool registry # Tool registry
TOOLS = { TOOLS = {

View file

@ -78,7 +78,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
# 7. T12 - CertStream phishing domain monitor (background task) # 7. T12 - CertStream phishing domain monitor (background task)
certstream_task = None certstream_task = None
try: try:
from app.domain.threat.certstream_listener import start_listener from app.domains.threat.certstream_listener import start_listener
certstream_task = await start_listener() certstream_task = await start_listener()
log.info("certstream_started has_task=%s", certstream_task is not None) log.info("certstream_started has_task=%s", certstream_task is not None)
except Exception as exc: except Exception as exc:
@ -88,7 +88,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
# Shutdown # Shutdown
try: try:
from app.domain.threat.certstream_listener import stop_listener from app.domains.threat.certstream_listener import stop_listener
await stop_listener(certstream_task) await stop_listener(certstream_task)
except Exception: except Exception:
pass pass

View file

@ -509,7 +509,7 @@ async def call_tool(name: str, arguments: dict) -> dict:
return {"error": f"news_query_fail: {e}"} return {"error": f"news_query_fail: {e}"}
if name == "generate_report": 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) chain, address = arguments["subject_id"].split(":", 1)
try: try:
@ -517,7 +517,7 @@ async def call_tool(name: str, arguments: dict) -> dict:
report = await generate_token_report(catalog, chain, address) report = await generate_token_report(catalog, chain, address)
else: else:
report = await generate_wallet_report(catalog, chain, address) 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) await save_report(catalog, report)
return { return {

View file

@ -116,7 +116,7 @@ TOOLS = [
async def handle_mcp_call(tool_name: str, arguments: dict[str, Any]) -> dict: async def handle_mcp_call(tool_name: str, arguments: dict[str, Any]) -> dict:
"""Route MCP tool calls to the appropriate handler.""" """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 from app.routers.x402_enforcement import TOOL_PRICES, _ensure_tool_prices
if tool_name == "x402_create_invoice": if tool_name == "x402_create_invoice":

View file

@ -38,10 +38,10 @@ ROUTER_MODULES: Final[list[str]] = [
"app.api.v1.catalog", # /api/v1/catalog/* "app.api.v1.catalog", # /api/v1/catalog/*
"app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON) "app.api.v1.mcp", # /mcp/* (JSON-RPC + plain JSON)
# Domain facades (per v4.0 T28-T34) # Domain facades (per v4.0 T28-T34)
"app.domain.news", # /api/v1/news/* "app.domains.news", # /api/v1/news/*
"app.domain.news.admin_router", # /api/v1/news/_admin/* "app.domains.news.admin_router", # /api/v1/news/_admin/*
"app.domain.reports", # /api/v1/reports/* "app.domains.reports", # /api/v1/reports/*
"app.domain.x402", # /api/v1/x402/* "app.domains.x402", # /api/v1/x402/*
# x402 MCP + Discovery + Docs # x402 MCP + Discovery + Docs
"app.routers.x402_mcp_handler", # /mcp/x402, /.well-known/x402 "app.routers.x402_mcp_handler", # /mcp/x402, /.well-known/x402
"app.routers.x402_docs", # /x402/docs, /x402/sandbox/* "app.routers.x402_docs", # /x402/docs, /x402/sandbox/*

View file

@ -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. Use ?mode=test to get a fake invoice for integration testing.
The invoice includes a mock payment that self-verifies. 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") invoice = create_invoice(tool, "sandbox_user")
if mode == "test": if mode == "test":

View file

@ -1,13 +1,19 @@
"""Backward-compat shim - moved to app.tokens.deployer in P3B.""" """token_deployer.py - DEPRECATED shim. Use app.domains.tokens.deployer.
from app.tokens.deployer import * # noqa: F401,F403
from app.tokens.deployer import ( # noqa: F401 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, ChainDeployer,
DeployParams, DeployParams,
DeploymentStorage, DeploymentStorage,
EVMDeployer, EVMDeployer,
SolanaDeployer, SolanaDeployer,
TokenDeployment,
TokenDeployerFactory, TokenDeployerFactory,
TokenDeployment,
TronDeployer, TronDeployer,
get_storage, get_storage,
logger,
_storage,
) )

View file

@ -6,6 +6,6 @@ better organization (per architecture standard: NO file > 500 lines).
This file re-exports the main API from the new modules. 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"] __all__ = ["ScanResult", "quick_scan_text", "scan_token"]