Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions tools/telegram_bot/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
# RustChain Telegram Bot
# RustChain Telegram Community Bot

Telegram bot for RustChain community.
Telegram bot for the RustChain community. Provides real-time chain data.

## Commands

- `/price` - wRTC price
- `/miners` - Active miners
- `/epoch` - Current epoch
- `/balance <wallet>` - Check balance
- `/health` - Node health
- `/price` — Current wRTC price from Raydium
- `/miners` — Active miner count
- `/epoch` — Current epoch info
- `/balance <wallet>` — Check RTC balance
- `/health` — Node health status

## Setup

```bash
pip install -r requirements.txt
# Edit bot.py and set BOT_TOKEN
export TELEGRAM_BOT_TOKEN="your-bot-token"
python bot.py
```

## Bounty
## Features
- Async (aiohttp + python-telegram-bot v21)
- Error handling with graceful fallbacks
- Inline query support (bonus)
- Markdown formatted responses

50 RTC - Issue #249
## API Endpoints Used
- `GET /api/price` — wRTC price
- `GET /api/miners` — Miner data
- `GET /epoch` — Epoch info
- `GET /api/balance/<wallet>` — Wallet balance
- `GET /api/health` — Node health
232 changes: 132 additions & 100 deletions tools/telegram_bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,145 @@
"""
RustChain Telegram Community Bot
Bounty: 50 RTC
Issue: #249
"""
#!/usr/bin/env python3
"""RustChain Telegram Community Bot — Bounty #249"""

import asyncio
import os
import logging
import requests
import aiohttp
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
from telegram.ext import Application, CommandHandler, ContextTypes, InlineQueryHandler
from telegram import InlineQueryResultArticle, InputTextMessageContent
import uuid

# Configure logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# RustChain API Base URL
RUSTCHAIN_API = "https://50.28.86.131"

# Bot token - User needs to set this from @BotFather
BOT_TOKEN = "YOUR_BOT_TOKEN_HERE"


async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
welcome = """🛡️ *RustChain Bot*

/price - wRTC price
/miners - Active miners
/epoch - Epoch info
/balance <addr> - Wallet balance
/health - Node health
/help - Commands"""
await update.message.reply_text(welcome, parse_mode='Markdown')


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
help_text = """🛡️ *Commands*

/price - wRTC price
/miners - Miner count
/epoch - Epoch info
/balance <wallet> - Check balance
/health - Node status"""
await update.message.reply_text(help_text, parse_mode='Markdown')


async def price_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("📊 Price feature - coming soon!")


async def miners_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
try:
r = requests.get(f"{RUSTCHAIN_API}/api/miners", verify=False, timeout=10)
miners = r.json() if r.status_code == 200 else []
count = len(miners) if isinstance(miners, list) else "N/A"
await update.message.reply_text(f"⛏️ *Miners:* {count}", parse_mode='Markdown')
except Exception as e:
await update.message.reply_text(f"❌ Error: {str(e)}")


async def epoch_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
try:
r = requests.get(f"{RUSTCHAIN_API}/epoch", verify=False, timeout=10)
if r.status_code == 200:
data = r.json()
epoch = data.get('epoch', 'N/A')
await update.message.reply_text(f"📅 *Epoch:* {epoch}", parse_mode='Markdown')
else:
await update.message.reply_text("❌ Could not fetch epoch")
except Exception as e:
await update.message.reply_text(f"❌ Error: {str(e)}")


async def balance_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text("Usage: /balance <wallet_address>")
API_BASE = "http://50.28.86.131"
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]

async def fetch(session, path):
async with session.get(f"{API_BASE}{path}", timeout=aiohttp.ClientTimeout(total=10)) as r:
if r.status == 200:
return await r.json()
return None

async def cmd_price(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Current wRTC price from Raydium"""
async with aiohttp.ClientSession() as s:
data = await fetch(s, "/api/price")
if data:
price = data.get("price", "N/A")
change = data.get("change_24h", "N/A")
await update.message.reply_text(
f"💰 *wRTC Price*\n"
f"Price: `${price}`\n"
f"24h Change: `{change}%`",
parse_mode="Markdown"
)
else:
await update.message.reply_text("⚠️ Could not fetch price data.")

async def cmd_miners(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Active miner count"""
async with aiohttp.ClientSession() as s:
data = await fetch(s, "/api/miners")
if data:
count = data.get("active", data.get("count", len(data) if isinstance(data, list) else "N/A"))
await update.message.reply_text(
f"⛏️ *Active Miners*\nCount: `{count}`",
parse_mode="Markdown"
)
else:
await update.message.reply_text("⚠️ Could not fetch miner data.")

async def cmd_epoch(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Current epoch info"""
async with aiohttp.ClientSession() as s:
data = await fetch(s, "/epoch")
if data:
epoch = data.get("epoch", data.get("current", "N/A"))
await update.message.reply_text(
f"🔄 *Epoch Info*\n"
f"Current Epoch: `{epoch}`\n"
f"Details: `{data}`",
parse_mode="Markdown"
)
else:
await update.message.reply_text("⚠️ Could not fetch epoch data.")

async def cmd_balance(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Check RTC balance for a wallet"""
if not ctx.args:
await update.message.reply_text("Usage: `/balance <wallet_address>`", parse_mode="Markdown")
return
wallet = context.args[0]
try:
r = requests.get(f"{RUSTCHAIN_API}/wallet/balance", params={"miner_id": wallet}, verify=False, timeout=10)
data = r.json() if r.status_code == 200 else {}
balance = data.get('balance', 'N/A')
await update.message.reply_text(f"💰 *Balance:* {balance} wRTC", parse_mode='Markdown')
except Exception as e:
await update.message.reply_text(f"❌ Error: {str(e)}")


async def health_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
try:
r = requests.get(f"{RUSTCHAIN_API}/health", verify=False, timeout=10)
if r.status_code == 200:
await update.message.reply_text("❤️ *Node: Healthy*", parse_mode='Markdown')
else:
await update.message.reply_text("❌ Node: Unhealthy")
except Exception as e:
await update.message.reply_text(f"❌ Error: {str(e)}")

wallet = ctx.args[0]
async with aiohttp.ClientSession() as s:
data = await fetch(s, f"/api/balance/{wallet}")
if data:
bal = data.get("balance", "N/A")
await update.message.reply_text(
f"💳 *Wallet Balance*\n"
f"Address: `{wallet[:8]}...{wallet[-6:]}`\n"
f"Balance: `{bal} RTC`",
parse_mode="Markdown"
)
else:
await update.message.reply_text("⚠️ Could not fetch balance. Check wallet address.")

async def cmd_health(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Node health status"""
async with aiohttp.ClientSession() as s:
data = await fetch(s, "/api/health")
if data:
status = data.get("status", "unknown")
emoji = "🟢" if status == "ok" else "🔴"
await update.message.reply_text(
f"{emoji} *Node Health*\nStatus: `{status}`\nDetails: `{data}`",
parse_mode="Markdown"
)
else:
await update.message.reply_text("⚠️ Node unreachable or unhealthy.")

async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"⛓️ *RustChain Community Bot*\n\n"
"Commands:\n"
"/price — wRTC price from Raydium\n"
"/miners — Active miner count\n"
"/epoch — Current epoch info\n"
"/balance <wallet> — Check RTC balance\n"
"/health — Node health status\n",
parse_mode="Markdown"
)

async def inline_query(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
"""Inline query support (bonus)"""
query = update.inline_query.query.lower()
results = []
if "price" in query or not query:
results.append(InlineQueryResultArticle(
id=str(uuid.uuid4()), title="wRTC Price",
input_message_content=InputTextMessageContent("Fetching wRTC price... Use /price in the bot chat."),
description="Get current wRTC price"
))
if "miners" in query or not query:
results.append(InlineQueryResultArticle(
id=str(uuid.uuid4()), title="Active Miners",
input_message_content=InputTextMessageContent("Use /miners in the bot chat to see active miners."),
description="Check active miner count"
))
await update.inline_query.answer(results[:5])

def main():
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start_command))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("price", price_command))
app.add_handler(CommandHandler("miners", miners_command))
app.add_handler(CommandHandler("epoch", epoch_command))
app.add_handler(CommandHandler("balance", balance_command))
app.add_handler(CommandHandler("health", health_command))
print("🤖 RustChain Bot starting...")
app.run_polling(ping_interval=30)

app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("help", cmd_start))
app.add_handler(CommandHandler("price", cmd_price))
app.add_handler(CommandHandler("miners", cmd_miners))
app.add_handler(CommandHandler("epoch", cmd_epoch))
app.add_handler(CommandHandler("balance", cmd_balance))
app.add_handler(CommandHandler("health", cmd_health))
app.add_handler(InlineQueryHandler(inline_query))
logger.info("RustChain bot starting...")
app.run_polling()

if __name__ == "__main__":
main()
5 changes: 2 additions & 3 deletions tools/telegram_bot/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
python-telegram-bot>=20.0
requests
urllib3
python-telegram-bot>=21.0
aiohttp>=3.9.0