| """T27A β Canonical entity models for the RMI data catalog. |
| |
| Pydantic v2 (per ADR-0002). Every entity is persisted in exactly one primary |
| store, with cross-store references (string IDs) to related entities in other |
| stores. CatalogService resolves these references transparently. |
| |
| Cross-store ID conventions: |
| Token, Wallet, Contract: f"{chain.value}:{address}" |
| Entity, Alert, NewsItem, RAGFinding, ScanReport: UUID4 hex |
| Qdrant point_id: 16-byte UUID hex (matches RAG engine) |
| |
| Persistence (per v4.0 Β§T27 "Why each store exists"): |
| Redis: hot token data, rate limits, alert state, cron locks |
| Postgres: users, api_keys, subscriptions, x402 receipts, audit |
| alerts, news_items, scan_reports |
| Neo4j: entities, wallets, deployers, contracts, entity labels |
| (graph traversal, Cypher) |
| Qdrant: RAG embeddings, token similarity, news embeddings |
| MinIO: news raw HTML, report markdown, RAG source docs |
| Filesystem: ingest tmp, log rotation (transient) |
| """ |
| from __future__ import annotations |
|
|
| from datetime import UTC, datetime |
| from enum import StrEnum |
| from typing import Any, Literal |
|
|
| from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator |
|
|
|
|
| |
| class Chain(StrEnum): |
| """All chains the platform indexes. Per v4.0 Β§T27.""" |
|
|
| SOLANA = "solana" |
| ETHEREUM = "ethereum" |
| BASE = "base" |
| ARBITRUM = "arbitrum" |
| OPTIMISM = "optimism" |
| POLYGON = "polygon" |
| BSC = "bsc" |
| TRON = "tron" |
| BITCOIN = "bitcoin" |
| AVALANCHE = "avalanche" |
| FANTOM = "fantom" |
| GNOSIS = "gnosis" |
| |
| SEPOLIA = "sepolia" |
| LINEA = "linea" |
| SCROLL = "scroll" |
| ZKSYNC = "zksync" |
| BLAST = "blast" |
| MANTLE = "mantle" |
|
|
|
|
| |
| |
| |
| CHAIN_REGISTRY: dict[str, dict[str, str]] = { |
| "solana": {"name": "Solana", "type": "svm", "native": "SOL", "explorer": "https://solscan.io"}, |
| "ethereum": {"name": "Ethereum", "type": "evm", "native": "ETH", "explorer": "https://etherscan.io"}, |
| "base": {"name": "Base", "type": "evm", "native": "ETH", "explorer": "https://basescan.org"}, |
| "arbitrum": {"name": "Arbitrum One", "type": "evm", "native": "ETH", "explorer": "https://arbiscan.io"}, |
| "optimism": {"name": "Optimism", "type": "evm", "native": "ETH", "explorer": "https://optimistic.etherscan.io"}, |
| "polygon": {"name": "Polygon", "type": "evm", "native": "MATIC", "explorer": "https://polygonscan.com"}, |
| "bsc": {"name": "BNB Smart Chain", "type": "evm", "native": "BNB", "explorer": "https://bscscan.com"}, |
| "tron": {"name": "TRON", "type": "tvm", "native": "TRX", "explorer": "https://tronscan.org"}, |
| "bitcoin": {"name": "Bitcoin", "type": "utxo", "native": "BTC", "explorer": "https://mempool.space"}, |
| "avalanche": {"name": "Avalanche C-Chain", "type": "evm", "native": "AVAX", "explorer": "https://snowtrace.io"}, |
| "fantom": {"name": "Fantom Opera", "type": "evm", "native": "FTM", "explorer": "https://ftmscan.com"}, |
| "gnosis": {"name": "Gnosis Chain", "type": "evm", "native": "xDAI", "explorer": "https://gnosisscan.io"}, |
| } |
|
|
|
|
| class RiskTier(StrEnum): |
| LOW = "low" |
| MEDIUM = "medium" |
| HIGH = "high" |
| CRITICAL = "critical" |
|
|
|
|
| |
| class Entity(BaseModel): |
| """A logical entity resolved across chains. Neo4j primary.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| entity_id: str = Field(..., description="UUID, Neo4j primary key") |
| label: str | None = None |
| aliases: list[str] = Field(default_factory=list) |
| first_seen: datetime |
| last_seen: datetime |
| risk_score: int | None = Field(None, ge=0, le=100) |
| tags: list[str] = Field(default_factory=list) |
| notes: str | None = None |
| store: Literal["neo4j"] = "neo4j" |
|
|
|
|
| class EntityLabel(BaseModel): |
| """A label attached to an entity. Neo4j primary.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| entity_id: str |
| label: str |
| source: Literal["manual", "heuristic", "third_party"] |
| confidence: float = Field(ge=0.0, le=1.0) |
| added_at: datetime |
| added_by: str |
| store: Literal["neo4j"] = "neo4j" |
|
|
|
|
| |
| class Wallet(BaseModel): |
| """An on-chain wallet. Neo4j primary; linked to Entity.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| wallet_id: str = Field(..., description='"chain:address", e.g. "solana:7Np41..."') |
| chain: Chain |
| address: str |
| entity_id: str | None = None |
| first_seen: datetime |
| last_seen: datetime |
| tx_count: int = 0 |
| total_volume_usd: float = 0.0 |
| is_deployer: bool = False |
| is_known_exchange: bool = False |
| is_suspicious: bool = False |
| reputation_score: int | None = Field(None, ge=0, le=100) |
| store: Literal["neo4j"] = "neo4j" |
|
|
| @field_validator("wallet_id") |
| @classmethod |
| def _check_id_format(cls, v: str) -> str: |
| if ":" not in v: |
| raise ValueError("wallet_id must be 'chain:address'") |
| chain, _ = v.split(":", 1) |
| if chain not in {c.value for c in Chain}: |
| |
| pass |
| return v |
|
|
|
|
| class Deployer(Wallet): |
| """A wallet that has deployed at least one token. Extends Wallet.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| deployments: list[str] = Field(default_factory=list, description="token_ids") |
| rug_count: int = 0 |
| legit_count: int = 0 |
| avg_token_lifetime_days: float = 0.0 |
| reputation_score: int | None = Field( |
| None, |
| ge=0, |
| le=100, |
| description="Weighted: legit_count * 1.0 - rug_count * 3.0 + age_bonus - news_penalty. Cached in Redis TTL 1h.", |
| ) |
|
|
|
|
| |
| class Token(BaseModel): |
| """A token contract. Postgres primary; references Deployer in Neo4j.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| token_id: str = Field(..., description='"chain:address"') |
| chain: Chain |
| address: str |
| symbol: str |
| name: str |
| decimals: int |
| deployer_wallet_id: str | None = Field( |
| None, description="Cross-store ref to Wallet (Neo4j)" |
| ) |
| deployed_at: datetime |
| initial_supply: int |
| current_supply: int | None = None |
| is_honeypot: bool | None = None |
| is_mintable: bool | None = None |
| is_proxy: bool | None = None |
| tax_buy_bps: int | None = None |
| tax_sell_bps: int | None = None |
| risk_tier: RiskTier | None = None |
| risk_score: int | None = Field(None, ge=0, le=100) |
| risk_factors: list[str] = Field(default_factory=list) |
| rag_embedding_id: str | None = Field( |
| None, description="Cross-store ref to Qdrant point (16-byte hex)" |
| ) |
| store: Literal["postgres"] = "postgres" |
|
|
|
|
| |
| class Alert(BaseModel): |
| """A risk alert. Postgres primary.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| alert_id: str = Field(..., description="UUID") |
| token_id: str | None = None |
| wallet_id: str | None = None |
| chain: Chain | None = None |
| alert_type: Literal[ |
| "rug_detected", |
| "deployer_history", |
| "liquidity_drain", |
| "honeypot_detected", |
| "high_tax_change", |
| "whale_dump", |
| ] |
| severity: Literal["info", "warning", "critical"] |
| title: str |
| description: str |
| evidence: dict[str, Any] = Field(default_factory=dict) |
| created_at: datetime |
| resolved_at: datetime | None = None |
| store: Literal["postgres"] = "postgres" |
|
|
|
|
| |
| class NewsItem(BaseModel): |
| """A news article from RSS. Postgres primary; embeddings in Qdrant.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| news_id: str = Field(..., description="UUID") |
| url: HttpUrl |
| title: str |
| summary: str |
| body_markdown: str | None = None |
| source: str |
| published_at: datetime |
| ingested_at: datetime |
| chains_mentioned: list[Chain] = Field(default_factory=list) |
| tokens_mentioned: list[str] = Field(default_factory=list) |
| wallets_mentioned: list[str] = Field(default_factory=list) |
| sentiment_score: float | None = Field(None, ge=-1.0, le=1.0) |
| ai_analysis: str | None = None |
| rag_embedding_id: str | None = None |
| store: Literal["postgres"] = "postgres" |
|
|
|
|
| |
| class RAGFinding(BaseModel): |
| """A fact extracted by RAG. Qdrant primary (vector); metadata in Postgres.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| finding_id: str = Field(..., description="UUID") |
| source_type: Literal["news", "onchain", "audit", "social", "manual"] |
| source_url: HttpUrl | None = None |
| source_token_id: str | None = None |
| source_wallet_id: str | None = None |
| claim: str |
| confidence: float = Field(ge=0.0, le=1.0) |
| extracted_at: datetime |
| qdrant_point_id: str |
| store: Literal["qdrant"] = "qdrant" |
|
|
|
|
| |
| class ScanReport(BaseModel): |
| """A research report. Postgres primary; markdown in MinIO.""" |
|
|
| model_config = ConfigDict(extra="ignore") |
|
|
| report_id: str = Field(..., description="UUID") |
| subject_type: Literal["token", "wallet", "deployer"] |
| subject_id: str |
| generated_at: datetime |
| generated_by_model: str |
| risk_score: int = Field(ge=0, le=100) |
| risk_tier: RiskTier |
| sections: dict[str, str] = Field(default_factory=dict) |
| markdown_url: HttpUrl | None = None |
| paid_via_x402: str | None = None |
| store: Literal["postgres", "minio"] = "postgres" |
|
|
| def to_markdown(self) -> str: |
| """Render report sections to a single Markdown document.""" |
| parts = [ |
| f"# Research Report: {self.subject_type.title()} `{self.subject_id}`", |
| "", |
| f"**Generated:** {self.generated_at.isoformat()}", |
| f"**Generated by:** {self.generated_by_model}", |
| f"**Risk score:** {self.risk_score}/100 ({self.risk_tier.value.upper()})", |
| "", |
| ] |
| for section, body in self.sections.items(): |
| parts.append(f"## {section.replace('_', ' ').title()}") |
| parts.append("") |
| parts.append(body) |
| parts.append("") |
| parts.append("---") |
| parts.append(f"*Report ID: {self.report_id}*") |
| return "\n".join(parts) |
|
|
|
|
| |
| def utcnow() -> datetime: |
| """Timezone-aware UTC now. Pydantic serializes to ISO 8601.""" |
| return datetime.now(UTC) |
|
|
|
|
| |
| COLLECTIONS: list[str] = [ |
| |
| |
| "scam_intel", |
| "deployer_history", |
| "wallet_labels", |
| "contract_audit", |
| "phishing_db", |
| "defi_hacks", |
| "rug_timeline", |
| "vuln_patterns", |
| "crime_reports", |
| "transaction_patterns", |
| "known_scams", |
| "token_analysis", |
| "market_intel", |
| ] |
|
|