merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
346
app/community_badges.py
Normal file
346
app/community_badges.py
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
"""
|
||||
Community Reputation Badges — Verified Sleuth System
|
||||
=====================================================
|
||||
Users vote on whether a token is clean or malicious. If their vote aligns
|
||||
with the ultimate outcome (rug confirmed / token survives), they earn a
|
||||
verified sleuth badge. Builds engagement without requiring financial rewards.
|
||||
|
||||
Architecture:
|
||||
- SQLite-backed vote tracking (lightweight, no extra infra)
|
||||
- Badge tiers: Bronze (3+ correct), Silver (10+), Gold (25+), Diamond (50+)
|
||||
- API endpoint for voting and badge queries
|
||||
- Token outcome resolution via manual/cron trigger
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "community_badges.db")
|
||||
|
||||
# ── Badge tiers ──────────────────────────────────────────────────
|
||||
BADGE_TIERS = [
|
||||
{"name": "Sleuth", "min_correct": 3, "emoji": "🔍", "color": "#9B59B6"},
|
||||
{"name": "Detective", "min_correct": 10, "emoji": "🕵️", "color": "#3498DB"},
|
||||
{"name": "Veteran", "min_correct": 25, "emoji": "🎖️", "color": "#F39C12"},
|
||||
{"name": "Oracle", "min_correct": 50, "emoji": "🔮", "color": "#E74C3C"},
|
||||
{"name": "Legend", "min_correct": 100, "emoji": "👑", "color": "#FFD700"},
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserBadge:
|
||||
user_id: str
|
||||
total_votes: int = 0
|
||||
correct_votes: int = 0
|
||||
current_tier: str = "none"
|
||||
next_tier: str = "Sleuth"
|
||||
votes_needed_for_next: int = 3
|
||||
badges_earned: list[str] = field(default_factory=list)
|
||||
accuracy_pct: float = 0.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenVote:
|
||||
token_address: str
|
||||
chain: str
|
||||
voter_id: str
|
||||
vote: str # "clean" or "malicious"
|
||||
voted_at: str
|
||||
outcome: str | None = None # None=pending, "clean", "malicious", "rug_confirmed"
|
||||
resolved_at: str | None = None
|
||||
earned_badge: bool = False
|
||||
|
||||
|
||||
def _get_db() -> sqlite3.Connection:
|
||||
"""Get or create SQLite DB with tables."""
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS votes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token_address TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
voter_id TEXT NOT NULL,
|
||||
vote TEXT NOT NULL CHECK(vote IN ('clean', 'malicious')),
|
||||
voted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
outcome TEXT CHECK(outcome IN ('clean', 'malicious', 'rug_confirmed', NULL)),
|
||||
resolved_at TEXT,
|
||||
earned_badge INTEGER DEFAULT 0,
|
||||
UNIQUE(token_address, chain, voter_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS badges (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
total_votes INTEGER DEFAULT 0,
|
||||
correct_votes INTEGER DEFAULT 0,
|
||||
current_tier TEXT DEFAULT 'none',
|
||||
earned_tiers TEXT DEFAULT '[]',
|
||||
last_updated TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_votes_token ON votes(token_address, chain);
|
||||
CREATE INDEX IF NOT EXISTS idx_votes_voter ON votes(voter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_votes_outcome ON votes(outcome);
|
||||
""")
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
def cast_vote(token_address: str, chain: str, voter_id: str, vote: str) -> dict[str, Any]:
|
||||
"""Cast a vote on a token. One vote per token per user."""
|
||||
if vote not in ("clean", "malicious"):
|
||||
return {"error": "Vote must be 'clean' or 'malicious'", "success": False}
|
||||
|
||||
conn = _get_db()
|
||||
try:
|
||||
# Upsert: replace if exists, insert if new
|
||||
conn.execute(
|
||||
"""INSERT INTO votes (token_address, chain, voter_id, vote)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(token_address, chain, voter_id)
|
||||
DO UPDATE SET vote = excluded.vote, voted_at = datetime('now')""",
|
||||
(token_address, chain, voter_id, vote),
|
||||
)
|
||||
|
||||
# Update badge stats
|
||||
total = conn.execute("SELECT COUNT(*) FROM votes WHERE voter_id = ?", (voter_id,)).fetchone()[0]
|
||||
|
||||
# Recalculate correct votes using normalized outcome matching
|
||||
correct = conn.execute(
|
||||
"""SELECT COUNT(*) FROM votes WHERE voter_id = ?
|
||||
AND outcome IS NOT NULL
|
||||
AND vote = CASE
|
||||
WHEN outcome = 'rug_confirmed' THEN 'malicious'
|
||||
ELSE outcome
|
||||
END""",
|
||||
(voter_id,),
|
||||
).fetchone()[0]
|
||||
|
||||
tier = _compute_tier(correct)
|
||||
earned_tiers = _get_earned_tiers(correct)
|
||||
|
||||
conn.execute(
|
||||
"""INSERT INTO badges (user_id, total_votes, correct_votes, current_tier, earned_tiers)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
total_votes = excluded.total_votes,
|
||||
correct_votes = excluded.correct_votes,
|
||||
current_tier = excluded.current_tier,
|
||||
earned_tiers = excluded.earned_tiers,
|
||||
last_updated = datetime('now')""",
|
||||
(voter_id, total, correct, tier, json.dumps(earned_tiers)),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"token_address": token_address,
|
||||
"voter_id": voter_id,
|
||||
"vote": vote,
|
||||
"total_votes": total,
|
||||
"correct_votes": correct,
|
||||
"current_tier": tier,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Vote failed: {e}")
|
||||
return {"error": str(e), "success": False}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def resolve_token_outcome(token_address: str, chain: str, outcome: str) -> dict[str, Any]:
|
||||
"""Resolve a token's outcome and update voter badges. Call when token rugs or survives."""
|
||||
if outcome not in ("clean", "malicious", "rug_confirmed"):
|
||||
return {
|
||||
"error": "Outcome must be 'clean', 'malicious', or 'rug_confirmed'",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
# Normalize: rug_confirmed = malicious for matching purposes
|
||||
match_outcome = "malicious" if outcome == "rug_confirmed" else outcome
|
||||
|
||||
conn = _get_db()
|
||||
try:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
|
||||
# Update outcome on all votes for this token
|
||||
conn.execute(
|
||||
"UPDATE votes SET outcome = ?, resolved_at = ? WHERE token_address = ? AND chain = ?",
|
||||
(outcome, now, token_address, chain),
|
||||
)
|
||||
|
||||
# Mark correct voters
|
||||
conn.execute(
|
||||
"UPDATE votes SET earned_badge = 1 WHERE token_address = ? AND chain = ? AND vote = ?",
|
||||
(token_address, chain, match_outcome),
|
||||
)
|
||||
|
||||
# Recalculate badges for all voters of this token
|
||||
voters = conn.execute(
|
||||
"SELECT DISTINCT voter_id FROM votes WHERE token_address = ? AND chain = ?",
|
||||
(token_address, chain),
|
||||
).fetchall()
|
||||
|
||||
for row in voters:
|
||||
vid = row[0]
|
||||
total = conn.execute("SELECT COUNT(*) FROM votes WHERE voter_id = ?", (vid,)).fetchone()[0]
|
||||
correct = conn.execute(
|
||||
"""SELECT COUNT(*) FROM votes WHERE voter_id = ?
|
||||
AND outcome IS NOT NULL
|
||||
AND vote = CASE
|
||||
WHEN outcome = 'rug_confirmed' THEN 'malicious'
|
||||
ELSE outcome
|
||||
END""",
|
||||
(vid,),
|
||||
).fetchone()[0]
|
||||
tier = _compute_tier(correct)
|
||||
earned_tiers = _get_earned_tiers(correct)
|
||||
conn.execute(
|
||||
"""INSERT INTO badges (user_id, total_votes, correct_votes, current_tier, earned_tiers)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
total_votes = excluded.total_votes,
|
||||
correct_votes = excluded.correct_votes,
|
||||
current_tier = excluded.current_tier,
|
||||
earned_tiers = excluded.earned_tiers,
|
||||
last_updated = datetime('now')""",
|
||||
(vid, total, correct, tier, json.dumps(earned_tiers)),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"outcome": outcome,
|
||||
"voters_updated": len(voters),
|
||||
"resolved_at": now,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Resolve failed: {e}")
|
||||
return {"error": str(e), "success": False}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_user_badge(user_id: str) -> UserBadge | None:
|
||||
"""Get a user's badge profile."""
|
||||
conn = _get_db()
|
||||
try:
|
||||
row = conn.execute("SELECT * FROM badges WHERE user_id = ?", (user_id,)).fetchone()
|
||||
if not row:
|
||||
return UserBadge(user_id=user_id)
|
||||
|
||||
earned = json.loads(row["earned_tiers"]) if row["earned_tiers"] else []
|
||||
accuracy = (row["correct_votes"] / row["total_votes"] * 100) if row["total_votes"] > 0 else 0.0
|
||||
|
||||
next_tier, needed = _next_tier(row["correct_votes"])
|
||||
|
||||
return UserBadge(
|
||||
user_id=user_id,
|
||||
total_votes=row["total_votes"],
|
||||
correct_votes=row["correct_votes"],
|
||||
current_tier=row["current_tier"],
|
||||
next_tier=next_tier,
|
||||
votes_needed_for_next=needed,
|
||||
badges_earned=earned,
|
||||
accuracy_pct=round(accuracy, 1),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Badge lookup failed: {e}")
|
||||
return UserBadge(user_id=user_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_token_votes(token_address: str, chain: str) -> dict[str, Any]:
|
||||
"""Get all votes for a token with summary."""
|
||||
conn = _get_db()
|
||||
try:
|
||||
votes = conn.execute(
|
||||
"SELECT * FROM votes WHERE token_address = ? AND chain = ? ORDER BY voted_at DESC",
|
||||
(token_address, chain),
|
||||
).fetchall()
|
||||
|
||||
clean = sum(1 for v in votes if v["vote"] == "clean")
|
||||
malicious = sum(1 for v in votes if v["vote"] == "malicious")
|
||||
total = len(votes)
|
||||
|
||||
consensus = clean / total * 100 if total > 0 else 50
|
||||
|
||||
return {
|
||||
"token_address": token_address,
|
||||
"chain": chain,
|
||||
"total_votes": total,
|
||||
"clean_votes": clean,
|
||||
"malicious_votes": malicious,
|
||||
"community_consensus": round(consensus, 1),
|
||||
"consensus_label": "clean" if consensus > 60 else ("malicious" if consensus < 40 else "divided"),
|
||||
"outcome": votes[0]["outcome"] if total > 0 else None,
|
||||
"votes": [dict(v) for v in votes],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Token votes lookup failed: {e}")
|
||||
return {"error": str(e), "token_address": token_address, "total_votes": 0}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_leaderboard(limit: int = 20) -> list[dict[str, Any]]:
|
||||
"""Get top sleuths by correct votes."""
|
||||
conn = _get_db()
|
||||
try:
|
||||
rows = conn.execute("SELECT * FROM badges ORDER BY correct_votes DESC LIMIT ?", (limit,)).fetchall()
|
||||
return [
|
||||
{
|
||||
"user_id": r["user_id"],
|
||||
"tier": r["current_tier"],
|
||||
"correct": r["correct_votes"],
|
||||
"total": r["total_votes"],
|
||||
"accuracy": round(r["correct_votes"] / r["total_votes"] * 100, 1) if r["total_votes"] > 0 else 0,
|
||||
"badges": json.loads(r["earned_tiers"]) if r["earned_tiers"] else [],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Leaderboard failed: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ── Internal helpers ──────────────────────────────────────────────
|
||||
|
||||
|
||||
def _compute_tier(correct: int) -> str:
|
||||
"""Compute badge tier from correct vote count."""
|
||||
tier = "none"
|
||||
for t in reversed(BADGE_TIERS):
|
||||
if correct >= t["min_correct"]:
|
||||
tier = t["name"]
|
||||
break
|
||||
return tier
|
||||
|
||||
|
||||
def _get_earned_tiers(correct: int) -> list[str]:
|
||||
"""Get list of all badge names earned."""
|
||||
return [t["name"] for t in BADGE_TIERS if correct >= t["min_correct"]]
|
||||
|
||||
|
||||
def _next_tier(correct: int) -> tuple:
|
||||
"""Get the next tier and votes needed."""
|
||||
for t in BADGE_TIERS:
|
||||
if correct < t["min_correct"]:
|
||||
return t["name"], t["min_correct"] - correct
|
||||
return BADGE_TIERS[-1]["name"], 0 # Max tier
|
||||
Loading…
Add table
Add a link
Reference in a new issue