| |
| """ |
| Ingest ChainAbuse reports into RAG known_scams collection. |
| API: https://api.chainabuse.com/v1/reports |
| """ |
|
|
| import asyncio |
| import hashlib |
| import logging |
| import os |
| import sys |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") |
| logger = logging.getLogger("ingest_chainabuse") |
|
|
| CHAINABUSE_API = "https://api.chainabuse.com/v1/reports" |
| RAG_API = "http://localhost:8000/api/v1/rag/ingest" |
| COLLECTION = "known_scams" |
|
|
|
|
| def make_doc_id(address: str) -> str: |
| return hashlib.sha256(f"known_scams:{address.lower()}:chainabuse".encode()).hexdigest()[:16] |
|
|
|
|
| async def ingest_via_api(content: str, metadata: dict) -> bool: |
| try: |
| import httpx |
|
|
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.post(RAG_API, json={"collection": COLLECTION, "content": content, "metadata": metadata}) |
| return resp.status_code == 200 |
| except Exception as e: |
| logger.debug(f"API ingest failed: {e}") |
| return False |
|
|
|
|
| async def main(): |
| import httpx |
|
|
| |
| all_reports = [] |
| for page in range(0, 5): |
| url = f"{CHAINABUSE_API}?limit=100&offset={page * 100}" |
| try: |
| logger.info(f"Fetching page {page + 1} from ChainAbuse...") |
| async with httpx.AsyncClient(timeout=30, headers={"Accept": "application/json"}) as client: |
| resp = await client.get(url) |
| if resp.status_code == 200: |
| data = resp.json() |
| reports = data if isinstance(data, list) else data.get("data", data.get("reports", [])) |
| if not reports: |
| logger.info(f"No more reports at page {page + 1}") |
| break |
| all_reports.extend(reports) |
| logger.info(f"Got {len(reports)} reports (total: {len(all_reports)})") |
| elif resp.status_code == 401: |
| logger.warning("ChainAbuse API requires authentication — trying public feed") |
| break |
| else: |
| logger.warning(f"ChainAbuse returned {resp.status_code}") |
| break |
| except Exception as e: |
| logger.warning(f"ChainAbuse fetch failed: {e}") |
| break |
|
|
| if not all_reports: |
| |
| alt_urls = [ |
| "https://api.chainabuse.com/v0/reports?limit=100", |
| "https://api.chainabuse.com/reports?limit=100", |
| "https://chainabuse.com/api/reports", |
| ] |
| for url in alt_urls: |
| try: |
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.get(url) |
| if resp.status_code == 200: |
| data = resp.json() |
| reports = data if isinstance(data, list) else data.get("data", data.get("reports", [])) |
| if reports: |
| all_reports.extend(reports) |
| logger.info(f"Got {len(reports)} reports from alt endpoint") |
| break |
| except Exception: |
| pass |
|
|
| if not all_reports: |
| logger.warning("No ChainAbuse reports accessible via API. Ingestion skipped.") |
| logger.info("ChainAbuse may require API key — visit https://chainabuse.com for access") |
| return |
|
|
| logger.info(f"Processing {len(all_reports)} ChainAbuse reports") |
|
|
| ingested = 0 |
| errors = 0 |
|
|
| for i, report in enumerate(all_reports): |
| if not isinstance(report, dict): |
| continue |
|
|
| address = report.get("address", report.get("targetAddress", "")) |
| category = report.get("category", report.get("type", "unknown")) |
| description = report.get("description", report.get("report", "")) |
| status = report.get("status", "unknown") |
| reporter = report.get("reporter", "") |
|
|
| if not address: |
| continue |
|
|
| content = f"ChainAbuse report: {address}. Category: {category}. Status: {status}. Description: {description}. Reporter: {reporter}." |
| metadata = { |
| "source": "chainabuse", |
| "address": address.lower(), |
| "category": category, |
| "status": status, |
| "severity": "high", |
| } |
|
|
| if await ingest_via_api(content, metadata): |
| ingested += 1 |
| else: |
| errors += 1 |
|
|
| if (i + 1) % 50 == 0: |
| logger.info(f"Progress: {i + 1}/{len(all_reports)} | ingested: {ingested}") |
|
|
| logger.info(f"COMPLETE: ingested={ingested}, errors={errors}") |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|