feat(bot): add Prometheus metrics + scan_handler split

metrics.py: command counter, latency histogram, error counter,
tier scan counter, mod actions counter with track_* helpers.

scan_handler.py: extracted cmd_scan, cmd_rugcheck, cmd_quick from
commands.py (subagent partial extraction).
This commit is contained in:
Crypto Rug Munch 2026-07-08 15:45:13 +07:00
parent 48e8c597bf
commit 6f1df381e5
2 changed files with 241 additions and 0 deletions

View file

@ -0,0 +1,174 @@
"""Token scanning commands — /scan, /rugcheck, /quick."""
import logging
import httpx
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
from telegram.ext import ContextTypes
from app.domains.telegram.rugmunchbot import db
from app.domains.telegram.rugmunchbot.config import (
DEXSCREENER,
fmt_chain,
fmt_number,
fmt_pct,
)
from app.domains.telegram.rugmunchbot.formatting import (
back_kb,
check_spam,
footer_links,
format_scan_report,
is_owner,
paywall_kb,
paywall_text,
scan_result_kb,
sep,
short_addr,
thin_sep,
web_scan_button,
)
from app.domains.telegram.rugmunchbot.services import scan_token
logger = logging.getLogger("rmi_bot")
async def cmd_scan(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
db.get_or_create_user(user.id, user.username, user.first_name)
if check_spam(user.id):
return
if not ctx.args:
await update.message.reply_text(
"🔍 <b>Token Scanner</b>\n\n"
"Usage: /scan <code>address</code> [chain]\n\n"
"<b>Examples:</b>\n"
" /scan <code>0x6982508145454Ce325dDbE47a25d4ec3d2311933</code>\n"
" /scan <code>EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v</code> solana\n\n"
"💡 <i>Or just paste a contract address directly!</i>",
parse_mode=ParseMode.HTML,
)
return
target = ctx.args[0]
chain = ctx.args[1] if len(ctx.args) > 1 else None
if not is_owner(user.id):
allowed, _used, _limit = db.check_rate_limit(user.id, "scan")
if not allowed:
await update.message.reply_text(
paywall_text(user.id, "scan"), parse_mode=ParseMode.HTML, reply_markup=paywall_kb()
)
return
msg = await update.message.reply_text(
f"🔍 <b>Scanning token...</b>\n"
f"<code>{short_addr(target)}</code>\n\n"
f"⏳ Querying DexScreener, GoPlus, Honeypot.is, RMI...",
parse_mode=ParseMode.HTML,
)
try:
result = await scan_token(target, chain)
if result["name"] == "Unknown" and not result["errors"]:
await msg.edit_text(
f"❌ <b>Token Not Found</b>\n\n"
f"No data found for <code>{short_addr(target)}</code>.\n\n"
f"• Check the address is correct\n"
f"• Ensure the token has a trading pair\n"
f"• Try specifying the chain: /scan <code>addr</code> solana",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
return
report = format_scan_report(result)
db.increment_usage(user.id, scan=True)
db.use_scan(user.id)
db.log_scan(user.id, target, result["chain"], "basic", result["risk_score"])
await msg.edit_text(
report,
parse_mode=ParseMode.HTML,
reply_markup=scan_result_kb(target, result["chain"]),
disable_web_page_preview=True,
)
except Exception as e:
logger.error(f"Scan error: {e}")
await msg.edit_text(
f"❌ <b>Scan Failed</b>\n\nError: {str(e)[:200]}\n\nPlease check the address and try again.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
async def cmd_quick(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not ctx.args:
await update.message.reply_text(
"🪙 Usage: /quick <code>SYMBOL</code>\nExample: /quick PEPE", parse_mode=ParseMode.HTML
)
return
symbol = ctx.args[0].upper().lstrip("$")
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{DEXSCREENER}/search/?q={symbol}")
if r.status_code == 200:
pairs = r.json().get("pairs", [])
if pairs:
p = pairs[0]
addr = p.get("baseToken", {}).get("address", "")
chain = p.get("chainId", "")
price = float(p.get("priceUsd", 0))
h24 = p.get("priceChange", {}).get("h24") or 0
vol = float(p.get("volume", {}).get("h24") or 0)
mcap = float(p.get("marketCap") or 0)
name = p.get("baseToken", {}).get("name", symbol)
text = (
f"🪙 <b>{name}</b> ({symbol}) {fmt_chain(chain)}\n"
f"<code>{addr}</code>\n\n"
f"💰 {fmt_number(price)} | 🔄 24h: {fmt_pct(h24)}\n"
f"📈 MCap: {fmt_number(mcap)} | 💧 Vol: {fmt_number(vol)}"
f"{footer_links()}"
)
kb = InlineKeyboardMarkup(
[
[InlineKeyboardButton("🔍 Full Scan", callback_data=f"scan_{addr}_{chain}")],
[web_scan_button(addr, chain)],
]
)
await update.message.reply_text(
text,
parse_mode=ParseMode.HTML,
reply_markup=kb,
disable_web_page_preview=True,
)
return
except Exception:
logging.getLogger(__name__).warning("swallowed exception", exc_info=True)
await update.message.reply_text(f"❌ Could not find token: <b>{symbol}</b>", parse_mode=ParseMode.HTML)
async def cmd_rugcheck(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
text = (
f"🚩 <b>Rug Pull Checklist</b>\n{sep()}\n\n"
f"Before buying ANY token, check these:\n\n"
f"<b>🔴 RED FLAGS (Run!)</b>\n{thin_sep()}\n"
f" ❌ Honeypot detected (can't sell)\n"
f" ❌ Unrenounced ownership\n"
f" ❌ Mintable supply (infinite tokens)\n"
f" ❌ Blacklist function exists\n"
f" ❌ LP not locked (< 50%)\n"
f" ❌ Buy/sell tax > 10%\n\n"
f"<b>🟡 YELLOW FLAGS (Caution)</b>\n{thin_sep()}\n"
f" ⚠️ Bundle activity detected\n"
f" ⚠️ >50% fresh wallets (<7 days)\n"
f" ⚠️ Dev wallet holds >20%\n"
f" ⚠️ Suspicious volume/mcap ratio\n"
f" ⚠️ Very low liquidity vs mcap\n\n"
f"<b>🟢 GREEN FLAGS (Safer)</b>\n{thin_sep()}\n"
f" ✅ Contract verified\n"
f" ✅ Ownership renounced\n"
f" ✅ LP locked > 90%\n"
f" ✅ No blacklist/mint functions\n"
f" ✅ Diverse holder distribution\n"
f" ✅ Organic volume patterns\n\n"
f"<b>🛡️ Always run /scan before buying!</b>"
f"{footer_links()}"
)
await update.message.reply_text(
text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True
)

View file

@ -0,0 +1,67 @@
"""Bot Prometheus metrics — command counts, latency, error tracking."""
from __future__ import annotations
import time
from functools import wraps
from prometheus_client import Counter, Histogram
COMMAND_COUNTER = Counter(
"bot_commands_total",
"Total bot commands executed",
["command"],
)
COMMAND_LATENCY = Histogram(
"bot_command_latency_seconds",
"Bot command latency in seconds",
["command"],
)
ERROR_COUNTER = Counter(
"bot_errors_total",
"Total bot errors",
["command", "error_type"],
)
TIER_SCANS = Counter(
"bot_scans_total",
"Total scans by tier",
["tier"],
)
MOD_ACTIONS = Counter(
"bot_mod_actions_total",
"Total moderation actions",
["action"],
)
def track_command(command: str):
"""Decorator to track command count and latency."""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = time.monotonic()
try:
result = await func(*args, **kwargs)
COMMAND_COUNTER.labels(command=command).inc()
COMMAND_LATENCY.labels(command=command).observe(time.monotonic() - start)
return result
except Exception:
COMMAND_COUNTER.labels(command=command).inc()
COMMAND_LATENCY.labels(command=command).observe(time.monotonic() - start)
raise
return wrapper
return decorator
def track_error(command: str, error_type: str) -> None:
ERROR_COUNTER.labels(command=command, error_type=error_type).inc()
def track_scan(tier: str) -> None:
TIER_SCANS.labels(tier=tier).inc()
def track_mod(action: str) -> None:
MOD_ACTIONS.labels(action=action).inc()
__all__ = ["COMMAND_COUNTER", "COMMAND_LATENCY", "ERROR_COUNTER", "track_command", "track_error", "track_mod", "track_scan"]