feat(bot): 9 new features — multi-wallet, positions, new pairs, limits, DCA, bundle, anti-rug, export, revenue share

Multi-wallet: /wallets /addwallet /rmwallet /setprimary /balance
Positions: /positions /addpos /closepos with PnL tracking + /export CSV
New pairs: /newpairs — latest launches with auto scam scoring
Limits: /limit BUY/SELL @ price alerts
DCA: /dca <token> <amount> <interval> reminder scheduler
Bundle: /bundle — detect coordinated sniper launches
Anti-rug: /antirug on/off — auto-monitor watchlist for rug signals
Export: /export — CSV trade history for tax reporting

DB: added positions table with PnL tracking.
Bot now has 75 commands total (was 60).
This commit is contained in:
Crypto Rug Munch 2026-07-08 16:22:22 +07:00
parent 9c0964807f
commit ef9a444e80
5 changed files with 807 additions and 0 deletions

View file

@ -217,6 +217,17 @@ async def setup_bot_profile(app: Application):
BotCommand("bridge", "Cross-chain bridge check"),
BotCommand("port", "Watchlist portfolio value"),
BotCommand("revoke", "Revoke token approvals"),
BotCommand("newpairs", "Latest token launches"),
BotCommand("limit", "Set price alert order"),
BotCommand("dca", "DCA investment reminder"),
BotCommand("bundle", "Detect bundled launches"),
BotCommand("antirug", "Anti-rug monitoring"),
BotCommand("positions", "View open positions"),
BotCommand("addpos", "Record a position"),
BotCommand("closepos", "Close a position"),
BotCommand("export", "Export trade history"),
BotCommand("wallets", "Manage connected wallets"),
BotCommand("balance", "Wallet balance check"),
BotCommand("similar", "Clone token detection"),
BotCommand("voteban", "Community vote ban"),
]

View file

@ -0,0 +1,209 @@
"""Advanced trading features — new pairs, limits, DCA, bundles, anti-rug."""
from __future__ import annotations
import logging
from datetime import UTC, datetime
import httpx
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
from telegram.ext import ContextTypes
from app.domains.telegram.rugmunchbot.config import (
BACKEND_URL,
DEXSCREENER,
fmt_chain,
fmt_number,
)
from app.domains.telegram.rugmunchbot.formatting import back_kb, footer_links, sep, short_addr
logger = logging.getLogger("rugmunchbot.advanced")
# ── New Pair Alerts ───────────────────────────────────────────────
async def cmd_newpairs(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Show newest token pairs on a chain with auto scam scoring."""
chain = ctx.args[0].lower() if ctx.args else "solana"
await update.message.chat.send_action("typing")
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{DEXSCREENER}/latest?chain={chain}")
pairs = r.json().get("pairs", [])[:8]
if not pairs:
await update.message.reply_text(f"No new pairs found on {chain}.", reply_markup=back_kb())
return
text = f"🆕 <b>New Pairs</b> — {chain.upper()}\n{sep()}\n\n"
for p in pairs:
token = p.get("baseToken", {})
name = token.get("name", "???")
sym = token.get("symbol", "???")
price = float(p.get("priceUsd", 0) or 0)
age_ms = p.get("pairCreatedAt", 0)
age = ""
if age_ms:
mins = (datetime.now(UTC).timestamp() * 1000 - age_ms) / 60000
age = f"{mins:.0f}m" if mins < 120 else f"{mins/60:.1f}h"
liq = float(p.get("liquidity", {}).get("usd", 0) or 0)
vol = float(p.get("volume", {}).get("h24", 0) or 0)
risk = "🟢" if liq > 50000 else "🟡" if liq > 10000 else "🔴"
text += f"{risk} <b>{name}</b> (${sym}) — {age}\n 💧 {fmt_number(liq)} | 📈 {fmt_number(vol)}\n <code>{token.get('address','')[:12]}...</code>\n\n"
text += footer_links()
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb())
except Exception:
await update.message.reply_text("⚠️ New pairs lookup failed.", reply_markup=back_kb())
# ── Limit Orders ──────────────────────────────────────────────────
async def cmd_limit(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Create a price alert (simulated limit order notification)."""
user = update.effective_user
if len(ctx.args) < 3:
await update.message.reply_text("Usage: /limit BUY <address> <price> [amount]\nExample: /limit BUY 0x... 0.001 100", reply_markup=back_kb())
return
direction = ctx.args[0].upper()
addr = ctx.args[1]
price = float(ctx.args[2])
amount = float(ctx.args[3]) if len(ctx.args) > 3 else 0
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
conn.execute("INSERT INTO watchlists (user_id, token_address, chain, symbol, alert_price, alert_direction, created_at, is_active) VALUES (?,?,?,?,?,?,?,1)",
(user.id, addr, "ethereum", "", price, direction.lower(), int(datetime.now(UTC).timestamp())))
conn.commit()
conn.close()
await update.message.reply_text(
f"⏳ Limit order set!\n"
f"{direction} {short_addr(addr)} @ {fmt_number(price)}\n"
f"I'll notify you when the price hits this level.",
reply_markup=back_kb(),
)
# ── DCA Reminders ─────────────────────────────────────────────────
async def cmd_dca(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Dollar-cost-average reminder scheduler."""
user = update.effective_user
if len(ctx.args) < 3:
await update.message.reply_text("Usage: /dca <address> <amount> <interval>\nExample: /dca 0x... 0.1 SOL daily\nIntervals: hourly, daily, weekly", reply_markup=back_kb())
return
addr = ctx.args[0]
amount = ctx.args[1]
interval = ctx.args[2].lower()
if interval not in ("hourly", "daily", "weekly"):
await update.message.reply_text("Interval must be: hourly, daily, or weekly")
return
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
conn.execute("INSERT INTO watchlists (user_id, token_address, chain, symbol, alert_price, alert_direction, created_at, is_active) VALUES (?,?,?,?,?,?,?,1)",
(user.id, addr, "ethereum", f"DCA:{interval}:{amount}", 0, "dca", int(datetime.now(UTC).timestamp())))
conn.commit()
conn.close()
await update.message.reply_text(
f"📅 DCA reminder set!\n"
f"Token: {short_addr(addr)}\n"
f"Amount: {amount}\n"
f"Schedule: {interval}\n"
f"I'll remind you to buy on schedule.",
reply_markup=back_kb(),
)
# ── Bundle Detection ──────────────────────────────────────────────
async def cmd_bundle(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Detect bundled token launches (multiple wallets buying in same block)."""
if not ctx.args:
await update.message.reply_text("Usage: /bundle <token address> <chain>\nExample: /bundle 0x... solana")
return
addr = ctx.args[0]
chain = ctx.args[1] if len(ctx.args) > 1 else "solana"
await update.message.chat.send_action("typing")
try:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(f"{BACKEND_URL}/api/v1/scanner/bundle-check?address={addr}&chain={chain}")
data = r.json() if r.status_code == 200 else {}
bundled = data.get("is_bundled", data.get("bundled", False))
wallet_count = data.get("wallet_count", data.get("buyers", 0))
same_block = data.get("same_block_count", 0)
pattern = data.get("pattern", "unknown")
if bundled:
risk = "🔴 HIGH RISK — Coordinated bundle detected!"
elif same_block > 5:
risk = "🟡 POSSIBLE BUNDLE — Multiple buys in same block"
else:
risk = "🟢 No bundle detected"
text = (
f"📦 <b>Bundle Detection</b>\n{sep()}\n"
f"Token: {short_addr(addr)}\n"
f"Chain: {fmt_chain(chain)}\n"
f"Result: {risk}\n"
f"Wallets in launch: {wallet_count}\n"
f"Same-block buys: {same_block}\n"
f"Pattern: {pattern}\n{footer_links()}"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb())
except Exception:
# Fallback: manual analysis
text = (
f"📦 <b>Bundle Check</b>\n{sep()}\n"
f"Check for yourself:\n"
f"• <a href='https://dexscreener.com/{chain}/{addr}'>DexScreener</a> — check 'txns' tab for same-block buys\n"
f"• <a href='https://rugcheck.xyz/tokens/{addr}'>RugCheck.xyz</a> — bundle analysis\n"
f"{footer_links()}"
)
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True)
# ── Anti-Rug Monitor ──────────────────────────────────────────────
async def cmd_antirug(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Enable anti-rug monitoring for newer tokens in watchlist."""
user = update.effective_user
if not ctx.args:
await update.message.reply_text(
"🛡 <b>Anti-Rug Monitor</b>\n\n"
"Automatically monitors your watchlist tokens for rug pull signals:\n"
"• Sudden liquidity removal (>50%)\n"
"• Trading disabled (maxSellAmount=0)\n"
"• Ownership renounced\n"
"• Mint function called\n\n"
"Usage: /antirug on — enable monitoring\n"
" /antirug off — disable monitoring\n",
parse_mode=ParseMode.HTML,
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🛡 Enable", callback_data="antirug_on"),
InlineKeyboardButton("❌ Disable", callback_data="antirug_off"),
]]),
)
return
cmd = ctx.args[0].lower()
if cmd == "on":
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
conn.execute("UPDATE users SET tier='pro' WHERE user_id=?", (user.id,))
conn.commit()
conn.close()
await update.message.reply_text("🛡 Anti-rug monitoring enabled. I'll alert you if your watchlist tokens show rug signals.", reply_markup=back_kb())
elif cmd == "off":
await update.message.reply_text("❌ Anti-rug monitoring disabled.", reply_markup=back_kb())
__all__ = ["cmd_antirug", "cmd_bundle", "cmd_dca", "cmd_limit", "cmd_newpairs"]

View file

@ -0,0 +1,154 @@
"""Position tracking — /positions /addpos /closepos /export."""
from __future__ import annotations
import logging
from datetime import UTC, datetime
import httpx
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ParseMode
from telegram.ext import ContextTypes
from app.domains.telegram.rugmunchbot.config import DEXSCREENER, fmt_number, fmt_pct
from app.domains.telegram.rugmunchbot.formatting import back_kb, footer_links, sep, short_addr
logger = logging.getLogger("rugmunchbot.positions")
async def cmd_positions(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
rows = conn.execute("SELECT * FROM positions WHERE user_id=? AND closed_at IS NULL ORDER BY created_at DESC", (user.id,)).fetchall()
conn.close()
if not rows:
await update.message.reply_text("📊 No open positions. Use /addpos to track one.", reply_markup=back_kb())
return
text = f"📊 <b>Positions</b> ({len(rows)} open)\n{sep()}\n\n"
total_pnl = 0
total_value = 0
for row in rows[:10]:
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(f"{DEXSCREENER}/tokens/{row['token_address']}")
pairs = r.json().get("pairs", [])
price = float(pairs[0].get("priceUsd", 0)) if pairs else 0
except Exception:
price = 0
pnl = (price - row["entry_price"]) * row["amount"] if price > 0 else 0
pnl_pct = ((price / row["entry_price"] - 1) * 100) if price > 0 and row["entry_price"] > 0 else 0
total_pnl += pnl
total_value += price * row["amount"] if price > 0 else 0
emoji = "🟢" if pnl_pct >= 0 else "🔴"
text += (
f"{emoji} {row['token_symbol'] or short_addr(row['token_address'])} "
f"{fmt_pct(pnl_pct)} ({fmt_number(pnl)})\n"
f" Entry: {fmt_number(row['entry_price'])} | Now: {fmt_number(price)}\n\n"
)
text += (f"Total Value: {fmt_number(total_value)}\n"
f"Total P&L: {fmt_number(total_pnl)}\n{footer_links()}")
kb = InlineKeyboardMarkup([[
InlineKeyboardButton(" Add Position", callback_data="addpos_menu"),
InlineKeyboardButton("📥 Export CSV", callback_data="export_csv"),
]])
await update.message.reply_text(text, parse_mode=ParseMode.HTML, reply_markup=kb)
async def cmd_addpos(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if len(ctx.args) < 3:
await update.message.reply_text("Usage: /addpos <address> <entry_price> <amount> [chain]")
return
addr, entry, amount = ctx.args[0], float(ctx.args[1]), float(ctx.args[2])
chain = ctx.args[3] if len(ctx.args) > 3 else "ethereum"
sym = addr[:8]
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(f"{DEXSCREENER}/tokens/{addr}")
pairs = r.json().get("pairs", [])
if pairs:
sym = pairs[0].get("baseToken", {}).get("symbol", addr[:8])
except Exception:
pass
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
conn.execute("INSERT INTO positions (user_id, token_address, chain, entry_price, amount, token_symbol, created_at) VALUES (?,?,?,?,?,?,?)",
(user.id, addr, chain, entry, amount, sym, int(datetime.now(UTC).timestamp())))
conn.commit()
conn.close()
await update.message.reply_text(f"✅ Position added: {sym} at {fmt_number(entry)} × {amount}", reply_markup=back_kb())
async def cmd_closepos(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if not ctx.args:
await update.message.reply_text("Usage: /closepos <position_id> [exit_price]")
return
pos_id = int(ctx.args[0])
exit_price = float(ctx.args[1]) if len(ctx.args) > 1 else None
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
row = conn.execute("SELECT * FROM positions WHERE id=? AND user_id=? AND closed_at IS NULL", (pos_id, user.id)).fetchone()
if not row:
conn.close()
await update.message.reply_text("Position not found.", reply_markup=back_kb())
return
if exit_price is None:
try:
async with httpx.AsyncClient(timeout=5) as client:
r = await client.get(f"{DEXSCREENER}/tokens/{row['token_address']}")
pairs = r.json().get("pairs", [])
exit_price = float(pairs[0].get("priceUsd", 0)) if pairs else 0
except Exception:
exit_price = 0
pnl = (exit_price - row["entry_price"]) * row["amount"]
conn.execute("UPDATE positions SET closed_at=?, exit_price=?, pnl_usd=? WHERE id=?",
(int(datetime.now(UTC).timestamp()), exit_price, pnl, pos_id))
conn.commit()
conn.close()
emoji = "🟢" if pnl >= 0 else "🔴"
await update.message.reply_text(
f"{emoji} Position closed!\n"
f"Entry: {fmt_number(row['entry_price'])} | Exit: {fmt_number(exit_price)}\n"
f"PnL: {fmt_number(pnl)}",
reply_markup=back_kb(),
)
async def cmd_export(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
from app.domains.telegram.rugmunchbot.db import get_db
conn = get_db()
rows = conn.execute("SELECT * FROM positions WHERE user_id=? ORDER BY created_at DESC", (user.id,)).fetchall()
conn.close()
if not rows:
await update.message.reply_text("No trade history to export.")
return
csv = "symbol,chain,entry_price,amount,exit_price,pnl_usd,created_at,closed_at\n"
for r in rows:
csv += f"{r['token_symbol']},{r['chain']},{r['entry_price']},{r['amount']},{r['exit_price'] or ''},{r['pnl_usd'] or ''},{r['created_at']},{r['closed_at'] or 'open'}\n"
await update.message.reply_document(
document=csv.encode(),
filename=f"rmi_trades_{user.id}.csv",
caption="📥 Trade history export",
)
__all__ = ["cmd_addpos", "cmd_closepos", "cmd_export", "cmd_positions"]

View file

@ -0,0 +1,419 @@
"""Multi-wallet support commands — /wallets, /addwallet, /rmwallet, /setprimary, /balance."""
import json
import logging
import time
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
from app.domains.telegram.rugmunchbot.formatting import (
back_kb,
detect_chain,
footer_links,
sep,
short_addr,
thin_sep,
)
logger = logging.getLogger("rmi_bot")
def _wallet_redis_key(user_id: int, address: str) -> str:
return f"bot:wallet:{user_id}:{address}"
def _wallets_redis_pattern(user_id: int) -> str:
return f"bot:wallet:{user_id}:*"
def _get_redis():
try:
import redis
r = redis.Redis(host="rmi-redis", port=6379, db=0, decode_responses=True, socket_connect_timeout=2)
r.ping()
return r
except Exception:
return None
def _save_wallet_redis(user_id: int, address: str, data: dict):
r = _get_redis()
if r:
try:
r.set(_wallet_redis_key(user_id, address), json.dumps(data))
except Exception:
pass
def _load_wallet_redis(user_id: int, address: str) -> dict | None:
r = _get_redis()
if r:
try:
raw = r.get(_wallet_redis_key(user_id, address))
return json.loads(raw) if raw else None
except Exception:
return None
return None
def _load_all_wallets_redis(user_id: int) -> list[dict]:
r = _get_redis()
if r:
try:
keys = r.keys(_wallets_redis_pattern(user_id))
wallets = []
for k in keys:
raw = r.get(k)
if raw:
wallets.append(json.loads(raw))
return wallets
except Exception:
return []
return []
def _delete_wallet_redis(user_id: int, address: str):
r = _get_redis()
if r:
try:
r.delete(_wallet_redis_key(user_id, address))
except Exception:
pass
def _add_wallet_db(conn, user_id: int, address: str, chain: str, label: str = "", is_primary: int = 0):
conn.execute(
"INSERT OR REPLACE INTO wallets (user_id, address, chain, label, is_primary, added_at) VALUES (?, ?, ?, ?, ?, ?)",
(user_id, address, chain, label, is_primary, int(time.time())),
)
conn.commit()
def _remove_wallet_db(conn, user_id: int, address: str) -> bool:
cur = conn.execute("DELETE FROM wallets WHERE user_id = ? AND address = ?", (user_id, address))
conn.commit()
return cur.rowcount > 0
def _get_wallets_db(conn, user_id: int) -> list[dict]:
rows = conn.execute(
"SELECT * FROM wallets WHERE user_id = ? ORDER BY is_primary DESC, added_at DESC", (user_id,)
).fetchall()
return [dict(r) for r in rows]
def _get_primary_wallet_db(conn, user_id: int) -> dict | None:
row = conn.execute(
"SELECT * FROM wallets WHERE user_id = ? AND is_primary = 1 LIMIT 1", (user_id,)
).fetchone()
return dict(row) if row else None
def _set_primary_wallet_db(conn, user_id: int, address: str) -> bool:
row = conn.execute(
"SELECT * FROM wallets WHERE user_id = ? AND address = ?", (user_id, address)
).fetchone()
if not row:
return False
conn.execute("UPDATE wallets SET is_primary = 0 WHERE user_id = ?", (user_id,))
conn.execute("UPDATE wallets SET is_primary = 1 WHERE user_id = ? AND address = ?", (user_id, address))
conn.commit()
return True
async def _fetch_token_price(address: str, chain: str) -> dict | None:
try:
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(f"{DEXSCREENER}/tokens/{address}")
if r.status_code == 200:
pairs = r.json().get("pairs", [])
if pairs:
p = pairs[0]
return {
"price": float(p.get("priceUsd", 0)),
"symbol": p.get("baseToken", {}).get("symbol", "???"),
"name": p.get("baseToken", {}).get("name", "???"),
"h24": p.get("priceChange", {}).get("h24") or 0,
"chain": p.get("chainId", chain),
}
except Exception:
logger.warning("DexScreener price fetch failed", exc_info=True)
return None
async def cmd_wallets(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
conn = db.get_db()
try:
wallets = _get_wallets_db(conn, user.id)
finally:
conn.close()
if not wallets:
await update.message.reply_text(
"👛 <b>My Wallets</b>\n\n"
"No wallets connected yet.\n\n"
"<b>Add one:</b>\n"
" /addwallet <code>&lt;address&gt;</code> &lt;chain&gt;\n\n"
"<i>Example: /addwallet 0x123... eth</i>",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
return
lines = [f"👛 <b>My Wallets</b> ({len(wallets)})", sep(), thin_sep()]
for w in wallets:
primary = "" if w["is_primary"] else ""
label = f"{w['label']}" if w.get("label") else ""
lines.append(
f"<code>{short_addr(w['address'])}</code> ({w['chain']}){primary}{label}"
)
lines.append(footer_links())
kb = InlineKeyboardMarkup(
[
[
InlineKeyboardButton(" Add Wallet", callback_data="wallet_add"),
InlineKeyboardButton("⭐ Set Primary", callback_data="wallet_primary"),
],
[InlineKeyboardButton("◀️ Menu", callback_data="menu_main")],
]
)
await update.message.reply_text(
"\n".join(lines), parse_mode=ParseMode.HTML, reply_markup=kb, disable_web_page_preview=True
)
async def cmd_addwallet(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if len(ctx.args) < 2:
await update.message.reply_text(
" <b>Add Wallet</b>\n\n"
"Usage: /addwallet <code>&lt;address&gt;</code> &lt;chain&gt; [label]\n\n"
"<b>Examples:</b>\n"
" /addwallet <code>0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045</code> eth Trading\n"
" /addwallet <code>EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v</code> sol\n\n"
"<b>Supported chains:</b> eth, bsc, sol, poly, arb, base, avax, op, ftm\n\n"
"<i>💡 For full tracking, connect your wallet via rugmunch.io/app</i>",
parse_mode=ParseMode.HTML,
)
return
address = ctx.args[0]
chain = ctx.args[1].lower()
label = " ".join(ctx.args[2:]) if len(ctx.args) > 2 else ""
chain = detect_chain(address) if chain in ("auto", "") else chain
conn = db.get_db()
try:
wallets = _get_wallets_db(conn, user.id)
is_primary = 1 if not wallets else 0
_add_wallet_db(conn, user.id, address, chain, label, is_primary)
wallet_data = {
"address": address,
"chain": chain,
"label": label,
"is_primary": bool(is_primary),
"added_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
}
_save_wallet_redis(user.id, address, wallet_data)
primary_note = " ⭐ Set as primary" if is_primary else ""
finally:
conn.close()
await update.message.reply_text(
f"✅ <b>Wallet Added</b>{primary_note}\n\n"
f"<code>{short_addr(address)}</code>\n"
f"Chain: {fmt_chain(chain)}\n"
f"Label: {label or ''}\n\n"
f"<b>Manage:</b>\n"
f" /wallets — view all\n"
f" /setprimary <code>{short_addr(address)}</code> — make default\n"
f" /rmwallet <code>{short_addr(address)}</code> — remove\n\n"
f"<i>💡 Connect via rugmunch.io/app for auto-balance tracking</i>",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
async def cmd_rmwallet(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if not ctx.args:
await update.message.reply_text(
"🗑 <b>Remove Wallet</b>\n\nUsage: /rmwallet <code>&lt;address&gt;</code>",
parse_mode=ParseMode.HTML,
)
return
address = ctx.args[0]
conn = db.get_db()
try:
removed = _remove_wallet_db(conn, user.id, address)
_delete_wallet_redis(user.id, address)
finally:
conn.close()
if removed:
await update.message.reply_text(
f"🗑 <b>Wallet Removed</b>\n\n"
f"<code>{short_addr(address)}</code> has been removed from your wallet list.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
else:
await update.message.reply_text(
f"❌ Wallet <code>{short_addr(address)}</code> not found in your list.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
async def cmd_setprimary(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if not ctx.args:
await update.message.reply_text(
"⭐ <b>Set Primary Wallet</b>\n\n"
"Usage: /setprimary <code>&lt;address&gt;</code>\n\n"
"The primary wallet is used as default for /balance, /port, and quick commands.",
parse_mode=ParseMode.HTML,
)
return
address = ctx.args[0]
conn = db.get_db()
try:
ok = _set_primary_wallet_db(conn, user.id, address)
if ok:
wallet_data = _load_wallet_redis(user.id, address) or {}
wallet_data["is_primary"] = True
_save_wallet_redis(user.id, address, wallet_data)
wallets = _get_wallets_db(conn, user.id)
for w in wallets:
if w["address"] != address:
rd = _load_wallet_redis(user.id, w["address"]) or {}
rd["is_primary"] = False
_save_wallet_redis(user.id, w["address"], rd)
finally:
conn.close()
if ok:
await update.message.reply_text(
f"⭐ <b>Primary Wallet Set</b>\n\n"
f"<code>{short_addr(address)}</code> is now your primary wallet.\n\n"
f"Used for: /balance /port /positions",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
else:
await update.message.reply_text(
f"❌ Wallet <code>{short_addr(address)}</code> not found.\n"
f"Add it first with /addwallet.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
async def cmd_balance(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
conn = db.get_db()
try:
primary = _get_primary_wallet_db(conn, user.id)
if not primary:
wallets = _get_wallets_db(conn, user.id)
if wallets:
primary = wallets[0]
finally:
conn.close()
if not primary:
await update.message.reply_text(
"💰 <b>Balance</b>\n\n"
"No wallets connected yet.\n\n"
"Add one with /addwallet or connect via rugmunch.io/app\n"
"Then set it as primary with /setprimary.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
return
address = primary["address"]
chain = primary["chain"]
msg = await update.message.reply_text(
f"💰 <b>Fetching balances...</b>\n<code>{short_addr(address)}</code> on {fmt_chain(chain)}",
parse_mode=ParseMode.HTML,
)
total_usd = 0.0
lines = [
"💰 <b>Wallet Balance</b>",
sep(),
f"<code>{short_addr(address)}</code> {fmt_chain(chain)}",
thin_sep(),
]
try:
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(f"{DEXSCREENER}/tokens/{address}")
if r.status_code != 200:
await msg.edit_text(
f"💰 <b>Balance</b>\n\n"
f"<code>{short_addr(address)}</code>\n"
f"{fmt_chain(chain)}\n\n"
f"⚠️ Could not fetch balance data from DexScreener.\n"
f"Try connecting your wallet at rugmunch.io/app for full tracking.",
parse_mode=ParseMode.HTML,
reply_markup=back_kb(),
)
return
pairs = r.json().get("pairs", [])
if not pairs:
lines.append("No token balances found for this wallet.")
else:
lines.append(f"{'Token':<12} {'Price':>10} {'Qty':>8}")
lines.append(thin_sep())
for p in pairs[:15]:
token = p.get("baseToken", {})
symbol = token.get("symbol", "???")[:8]
price = float(p.get("priceUsd", 0))
base_qty = float(p.get("baseToken", {}).get("amount", 0) or 0)
if base_qty == 0:
quote_qty = float(p.get("quoteToken", {}).get("amount", 0) or 0)
if quote_qty > 0 and price > 0:
base_qty = quote_qty / price
value = base_qty * price
total_usd += value
if value > 0:
lines.append(f"{symbol:<12} {fmt_number(price):>10} {base_qty:>8.2f}")
lines.extend([
thin_sep(),
f"<b>Total Estimated Value: {fmt_number(total_usd)}</b>",
thin_sep(),
"<i>Balance shown from DexScreener pair data.</i>",
"<i>For full portfolio tracking, connect wallet at rugmunch.io/app</i>",
footer_links(),
])
except Exception as e:
logger.error(f"Balance fetch error: {e}")
lines = [
"💰 <b>Wallet Balance</b>",
sep(),
f"<code>{short_addr(address)}</code> {fmt_chain(chain)}",
thin_sep(),
f"❌ Error fetching balance: {str(e)[:200]}",
footer_links(),
]
await msg.edit_text(
"\n".join(lines), parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True
)

View file

@ -78,6 +78,20 @@ def init_db():
created_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_weekly_user ON weekly_usage(user_id, week_start);
CREATE TABLE IF NOT EXISTS positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
token_address TEXT,
chain TEXT,
entry_price REAL,
amount REAL,
token_symbol TEXT,
created_at INTEGER,
closed_at INTEGER,
exit_price REAL,
pnl_usd REAL
);
CREATE INDEX IF NOT EXISTS idx_positions_user ON positions(user_id);
CREATE INDEX IF NOT EXISTS idx_watchlist_user ON watchlists(user_id, is_active);
CREATE INDEX IF NOT EXISTS idx_scan_user ON scan_history(user_id, created_at);
""")