- Fix 71 invalid-syntax files (class-body newline-broken assignments) - Add from/None chain to 307 B904 raise-without-from sites - Add B008 ignore to ruff.toml (already in pyproject.toml) - Noqa F401 on __init__.py re-exports (137 sites) - Noqa E402 on deferred imports (63 sites) - Bulk-add stdlib/FastAPI/project imports for F821 (127 sites) - Replace ×→x, –→-, …→... in docstrings (4093 chars) - Manual refactor of 5 SIM103/SIM116 patterns Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py) Co-authored-by: opencode <opencode@rugmunch.io>
161 lines
5.3 KiB
Python
161 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""#17 - MBAL 10M-Label Market Maker. Public searchable database of the MBAL dataset
|
|
(10M labeled addresses). Free search, paid API. Kaggle: cryptorugmuncher."""
|
|
|
|
import contextlib
|
|
import os
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
router = APIRouter(prefix="/api/v1/mbal", tags=["mbal-market-maker"])
|
|
|
|
MBAL_DB = os.environ.get("MBAL_DB", str(Path.home() / "rmi/backend/data/mbal.db"))
|
|
|
|
|
|
def _get_db() -> sqlite3.Connection | None:
|
|
"""Open MBAL database connection."""
|
|
db_path = Path(MBAL_DB)
|
|
if not db_path.exists():
|
|
return None
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
@router.get("/search/{address}")
|
|
async def search_mbal(address: str):
|
|
"""Search MBAL database for an address. Returns label if found."""
|
|
conn = _get_db()
|
|
if not conn:
|
|
return {
|
|
"address": address,
|
|
"found": False,
|
|
"database": "unavailable",
|
|
"note": "MBAL database not found. Visit: https://www.kaggle.com/datasets/cryptorugmuncher/mbal-10m-labeled-addresses",
|
|
}
|
|
|
|
try:
|
|
cursor = conn.execute(
|
|
"SELECT address, label, category, confidence, source FROM addresses WHERE address = ? LIMIT 1",
|
|
(address.lower(),),
|
|
)
|
|
row = cursor.fetchone()
|
|
if row:
|
|
return {
|
|
"address": address,
|
|
"found": True,
|
|
"label": row["label"],
|
|
"category": row["category"],
|
|
"confidence": row["confidence"],
|
|
"source": row["source"],
|
|
"dataset": "MBAL (Multi-chain Blockchain Address Labels)",
|
|
}
|
|
return {"address": address, "found": False, "database": "available"}
|
|
except sqlite3.Error:
|
|
return {"address": address, "found": False, "database": "error"}
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
@router.get("/stats")
|
|
async def mbal_stats():
|
|
"""MBAL dataset statistics."""
|
|
conn = _get_db()
|
|
stats = {
|
|
"dataset": "MBAL - Multi-chain Blockchain Address Labels",
|
|
"total_addresses": 0,
|
|
"categories": [],
|
|
"chains_covered": [],
|
|
"kaggle_url": "https://www.kaggle.com/datasets/cryptorugmuncher/mbal-10m-labeled-addresses",
|
|
"database_available": conn is not None,
|
|
}
|
|
if conn:
|
|
try:
|
|
stats["total_addresses"] = conn.execute("SELECT COUNT(*) FROM addresses").fetchone()[0]
|
|
stats["categories"] = [
|
|
r[0]
|
|
for r in conn.execute(
|
|
"SELECT DISTINCT category FROM addresses WHERE category IS NOT NULL LIMIT 20"
|
|
).fetchall()
|
|
]
|
|
with contextlib.suppress(sqlite3.Error):
|
|
stats["chains_covered"] = [
|
|
r[0]
|
|
for r in conn.execute(
|
|
"SELECT DISTINCT chain FROM addresses WHERE chain IS NOT NULL LIMIT 20"
|
|
).fetchall()
|
|
]
|
|
except sqlite3.Error:
|
|
pass
|
|
finally:
|
|
conn.close()
|
|
|
|
return stats
|
|
|
|
|
|
class BulkSearchRequest(BaseModel):
|
|
addresses: list[str]
|
|
max_results: int = 100
|
|
|
|
|
|
@router.post("/bulk-search")
|
|
async def bulk_search_mbal(request: BulkSearchRequest):
|
|
"""Search multiple addresses in MBAL. Paid tier (x402)."""
|
|
conn = _get_db()
|
|
if not conn:
|
|
raise HTTPException(503, "MBAL database unavailable")
|
|
|
|
results: list[dict] = []
|
|
try:
|
|
for addr in request.addresses[: request.max_results]:
|
|
cursor = conn.execute(
|
|
"SELECT address, label, category, confidence FROM addresses WHERE address = ? LIMIT 1", (addr.lower(),)
|
|
)
|
|
row = cursor.fetchone()
|
|
if row:
|
|
results.append(
|
|
{
|
|
"address": addr,
|
|
"found": True,
|
|
"label": row["label"],
|
|
"category": row["category"],
|
|
"confidence": row["confidence"],
|
|
}
|
|
)
|
|
else:
|
|
results.append({"address": addr, "found": False})
|
|
except sqlite3.Error:
|
|
raise HTTPException(500, "Database query error") from None
|
|
finally:
|
|
conn.close()
|
|
|
|
return {
|
|
"searched": len(request.addresses[: request.max_results]),
|
|
"found": sum(1 for r in results if r.get("found")),
|
|
"results": results,
|
|
}
|
|
|
|
|
|
@router.get("/category/{category}")
|
|
async def search_by_category(category: str, limit: int = Query(20, le=100)):
|
|
"""Search MBAL addresses by category (e.g., 'exchange', 'scam', 'mixer')."""
|
|
conn = _get_db()
|
|
if not conn:
|
|
raise HTTPException(503, "MBAL database unavailable")
|
|
|
|
try:
|
|
rows = conn.execute(
|
|
"SELECT address, label, category, confidence FROM addresses WHERE category = ? LIMIT ?", (category, limit)
|
|
).fetchall()
|
|
results = [
|
|
{"address": r["address"], "label": r["label"], "category": r["category"], "confidence": r["confidence"]}
|
|
for r in rows
|
|
]
|
|
return {"category": category, "count": len(results), "addresses": results}
|
|
except sqlite3.Error:
|
|
raise HTTPException(500, "Database query error") from None
|
|
finally:
|
|
conn.close()
|