Created
March 3, 2026 01:49
-
-
Save refet-crypto/3bd816b8ac771b6855e8123e9a860812 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Greengo Exchange Telegram Bot — с балансом пользователей | |
| Требования: pip install aiogram aiohttp | |
| Запуск: python bot.py | |
| """ | |
| import asyncio | |
| import logging | |
| import sqlite3 | |
| import aiohttp | |
| from aiogram import Bot, Dispatcher, F | |
| from aiogram.filters import CommandStart | |
| from aiogram.fsm.context import FSMContext | |
| from aiogram.fsm.state import State, StatesGroup | |
| from aiogram.fsm.storage.memory import MemoryStorage | |
| from aiogram.types import ( | |
| Message, CallbackQuery, | |
| InlineKeyboardMarkup, InlineKeyboardButton | |
| ) | |
| # ─── Настройки ─────────────────────────────────────────────────────────────── | |
| BOT_TOKEN = "8552275284:AAEcVri96NGxtDzt7DlBg9Nm0EWmWokJmto" | |
| API_SECRET = "GG4qggukKiUIR2Iq6p9DSsQFdUDcfb2XYG4mgbQY9upClA00Z8Vl1bZQl" | |
| GREENGO_API = "https://api.greengo.cc/api/v2" | |
| POLL_INTERVAL = 30 # секунд между автопроверками | |
| # Специальный «технический» адрес для заявок пополнения баланса | |
| # Greengo требует wallet в запросе — используем минимально валидный BTC-адрес | |
| # Реальные рубли клиента оседают на твоём балансе в Greengo | |
| DEPOSIT_WALLET = "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf6a" # Genesis block address (публичный, только для формата) | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| bot = Bot(token=BOT_TOKEN) | |
| dp = Dispatcher(storage=MemoryStorage()) | |
| # ─── База данных ───────────────────────────────────────────────────────────── | |
| def db_connect(): | |
| conn = sqlite3.connect("bot.db") | |
| conn.row_factory = sqlite3.Row | |
| return conn | |
| def db_init(): | |
| with db_connect() as conn: | |
| conn.executescript(""" | |
| CREATE TABLE IF NOT EXISTS users ( | |
| user_id INTEGER PRIMARY KEY, | |
| username TEXT, | |
| balance REAL DEFAULT 0, | |
| created_at TEXT DEFAULT (datetime('now')) | |
| ); | |
| CREATE TABLE IF NOT EXISTS transactions ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER, | |
| type TEXT, | |
| amount REAL, | |
| order_id TEXT, | |
| note TEXT, | |
| created_at TEXT DEFAULT (datetime('now')) | |
| ); | |
| CREATE TABLE IF NOT EXISTS orders ( | |
| order_id TEXT PRIMARY KEY, | |
| user_id INTEGER, | |
| type TEXT, | |
| amount_rub REAL, | |
| amount_crypto TEXT, | |
| crypto TEXT, | |
| payment_method TEXT, | |
| wallet TEXT, | |
| status TEXT DEFAULT 'unconfirmed', | |
| created_at TEXT DEFAULT (datetime('now')) | |
| ); | |
| """) | |
| def get_or_create_user(user_id: int, username: str = ""): | |
| with db_connect() as conn: | |
| user = conn.execute("SELECT * FROM users WHERE user_id=?", (user_id,)).fetchone() | |
| if not user: | |
| conn.execute("INSERT INTO users (user_id,username) VALUES (?,?)", (user_id, username or "")) | |
| conn.commit() | |
| return dict(user) if user else {"balance": 0.0} | |
| def get_balance(user_id: int) -> float: | |
| with db_connect() as conn: | |
| row = conn.execute("SELECT balance FROM users WHERE user_id=?", (user_id,)).fetchone() | |
| return float(row["balance"]) if row else 0.0 | |
| def update_balance(user_id: int, delta: float, order_id: str, tx_type: str, note: str = ""): | |
| with db_connect() as conn: | |
| conn.execute("UPDATE users SET balance=balance+? WHERE user_id=?", (delta, user_id)) | |
| conn.execute( | |
| "INSERT INTO transactions (user_id,type,amount,order_id,note) VALUES (?,?,?,?,?)", | |
| (user_id, tx_type, delta, order_id, note) | |
| ) | |
| conn.commit() | |
| def get_history(user_id: int, limit: int = 10): | |
| with db_connect() as conn: | |
| return conn.execute( | |
| "SELECT * FROM transactions WHERE user_id=? ORDER BY created_at DESC LIMIT ?", | |
| (user_id, limit) | |
| ).fetchall() | |
| def save_order(order_id, user_id, order_type, amount_rub, amount_crypto, crypto, payment_method, wallet): | |
| with db_connect() as conn: | |
| conn.execute( | |
| "INSERT OR REPLACE INTO orders (order_id,user_id,type,amount_rub,amount_crypto,crypto,payment_method,wallet) VALUES (?,?,?,?,?,?,?,?)", | |
| (order_id, user_id, order_type, amount_rub, amount_crypto, crypto, payment_method, wallet) | |
| ) | |
| conn.commit() | |
| def update_order_status(order_id: str, status: str): | |
| with db_connect() as conn: | |
| conn.execute("UPDATE orders SET status=? WHERE order_id=?", (status, order_id)) | |
| conn.commit() | |
| def get_order(order_id: str): | |
| with db_connect() as conn: | |
| row = conn.execute("SELECT * FROM orders WHERE order_id=?", (order_id,)).fetchone() | |
| return dict(row) if row else None | |
| # ─── FSM ───────────────────────────────────────────────────────────────────── | |
| class DepositStates(StatesGroup): | |
| choosing_payment = State() | |
| entering_amount = State() | |
| class ExchangeStates(StatesGroup): | |
| choosing_direction = State() | |
| choosing_payment = State() | |
| entering_amount = State() | |
| entering_wallet = State() | |
| # ─── Greengo API ───────────────────────────────────────────────────────────── | |
| def greengo_headers(): | |
| return {"Api-Secret": API_SECRET, "Content-Type": "application/json"} | |
| async def get_directions(): | |
| async with aiohttp.ClientSession() as s: | |
| async with s.get(f"{GREENGO_API}/directions", headers=greengo_headers()) as r: | |
| data = await r.json() | |
| return [d for d in data if d.get("status") == 1] | |
| async def create_order(payment_method: str, wallet: str, amount: str): | |
| async with aiohttp.ClientSession() as s: | |
| async with s.post( | |
| f"{GREENGO_API}/order/create", | |
| headers=greengo_headers(), | |
| json={"payment_method": payment_method, "wallet": wallet, "from_amount": amount} | |
| ) as r: | |
| return await r.json() | |
| async def check_order_api(order_id: str): | |
| async with aiohttp.ClientSession() as s: | |
| async with s.post( | |
| f"{GREENGO_API}/order/check", | |
| headers=greengo_headers(), | |
| json={"order_id": [order_id]} | |
| ) as r: | |
| return await r.json() | |
| async def cancel_order_api(order_id: str): | |
| async with aiohttp.ClientSession() as s: | |
| async with s.post( | |
| f"{GREENGO_API}/order/cancel", | |
| headers=greengo_headers(), | |
| json={"order_id": [order_id]} | |
| ) as r: | |
| return await r.json() | |
| # ─── Автопроверка статусов ─────────────────────────────────────────────────── | |
| active_orders: dict = {} | |
| async def poll_orders(): | |
| while True: | |
| await asyncio.sleep(POLL_INTERVAL) | |
| if not active_orders: | |
| continue | |
| order_ids = list(active_orders.keys()) | |
| try: | |
| async with aiohttp.ClientSession() as s: | |
| async with s.post( | |
| f"{GREENGO_API}/order/check", | |
| headers=greengo_headers(), | |
| json={"order_id": order_ids} | |
| ) as r: | |
| result = await r.json() | |
| for order in result.get("data", {}).get("orders", []): | |
| oid = order["order_id"] | |
| status = order["order_status"] | |
| if oid not in active_orders: | |
| continue | |
| info = active_orders[oid] | |
| user_id = info["user_id"] | |
| prev_status = info.get("status") | |
| order_type = info.get("type", "exchange") | |
| if status == prev_status: | |
| continue | |
| active_orders[oid]["status"] = status | |
| update_order_status(oid, status) | |
| # Пополнение баланса | |
| if order_type == "deposit": | |
| if status == "payed": | |
| await bot.send_message(user_id, | |
| f"✅ *Оплата получена!* Зачисляем *{order['amount_payable']} ₽*...", | |
| parse_mode="Markdown") | |
| elif status == "completed": | |
| amt = float(order["amount_payable"]) | |
| update_balance(user_id, amt, oid, "deposit", f"Пополнение #{oid}") | |
| bal = get_balance(user_id) | |
| await bot.send_message(user_id, | |
| f"🎉 *Баланс пополнен на {amt:,.0f} ₽!*\n💰 Текущий баланс: *{bal:,.2f} ₽*", | |
| parse_mode="Markdown") | |
| del active_orders[oid] | |
| elif status in ("autocanceled", "canceled"): | |
| await bot.send_message(user_id, | |
| f"❌ *Пополнение отменено*\nЗаявка `{oid}`\n\n/start", | |
| parse_mode="Markdown") | |
| del active_orders[oid] | |
| # Обмен | |
| else: | |
| crypto = info.get("crypto", "") | |
| if status == "payed": | |
| await bot.send_message(user_id, | |
| f"✅ *Оплата получена!* Отправляем {crypto}...", | |
| parse_mode="Markdown") | |
| elif status == "completed": | |
| await bot.send_message(user_id, | |
| f"🎉 *Обмен завершён!*\n📦 {order['amount_receivable']} {crypto} отправлены!\n\n/start", | |
| parse_mode="Markdown") | |
| del active_orders[oid] | |
| elif status in ("autocanceled", "canceled"): | |
| db_order = get_order(oid) | |
| if db_order and db_order["type"] == "exchange_balance": | |
| refund = db_order["amount_rub"] | |
| update_balance(user_id, refund, oid, "refund", f"Возврат за #{oid}") | |
| bal = get_balance(user_id) | |
| await bot.send_message(user_id, | |
| f"❌ *Заявка отменена*\n💰 Возвращено: *{refund:,.0f} ₽*\nБаланс: *{bal:,.2f} ₽*", | |
| parse_mode="Markdown") | |
| else: | |
| await bot.send_message(user_id, | |
| f"❌ Заявка `{oid}` отменена.\n/start", parse_mode="Markdown") | |
| del active_orders[oid] | |
| except Exception as e: | |
| logger.error(f"poll_orders: {e}") | |
| # ─── Константы ─────────────────────────────────────────────────────────────── | |
| PAYMENT_LABELS = { | |
| "card": "💳 Карта РФ", | |
| "sbp": "📱 СБП", | |
| "sbp_alpha": "🏦 СБП → Альфа-Банк", | |
| "sbp_sber": "🏦 СБП → Сбербанк", | |
| "sbp_ozon": "🏦 СБП → Ozon Банк", | |
| "sbp_gazprom": "🏦 СБП → Газпромбанк", | |
| "sbp_psb": "🏦 СБП → ПСБ", | |
| "sbp_sovkom": "🏦 СБП → Совкомбанк", | |
| "sbp_otp": "🏦 СБП → ОТП Банк", | |
| "sbp_transgran": "🌍 СБП трансграничный", | |
| "kzt": "🇰🇿 Карта Казахстана", | |
| "mobile": "📞 Мобильный счёт", | |
| "qr_code": "🔲 QR-код", | |
| } | |
| STATUS_LABELS = { | |
| "unconfirmed": "⏳ Создана", | |
| "awaiting": "⌛ Ожидает оплаты", | |
| "payed": "✅ Оплачена, обрабатывается", | |
| "completed": "🎉 Выполнена", | |
| "autocanceled": "❌ Отменена автоматически", | |
| "canceled": "🚫 Отменена", | |
| } | |
| TX_LABELS = {"deposit": "➕", "exchange": "💱", "exchange_balance": "💱", "refund": "↩️"} | |
| def parse_crypto(fullname: str) -> str: | |
| return fullname.split(" на ")[-1] if " на " in fullname else fullname | |
| def main_kb(): | |
| return InlineKeyboardMarkup(inline_keyboard=[ | |
| [InlineKeyboardButton(text="💰 Мой баланс", callback_data="my_balance")], | |
| [InlineKeyboardButton(text="➕ Пополнить баланс", callback_data="deposit")], | |
| [InlineKeyboardButton(text="💱 Обменять → крипта", callback_data="start_exchange")], | |
| [InlineKeyboardButton(text="📋 История", callback_data="history")], | |
| [InlineKeyboardButton(text="📊 Курсы", callback_data="show_rates")], | |
| ]) | |
| # ─── /start ────────────────────────────────────────────────────────────────── | |
| @dp.message(CommandStart()) | |
| async def cmd_start(message: Message, state: FSMContext): | |
| await state.clear() | |
| get_or_create_user(message.from_user.id, message.from_user.username) | |
| bal = get_balance(message.from_user.id) | |
| await message.answer( | |
| f"👋 Привет, *{message.from_user.first_name}*!\n\n" | |
| f"💰 Баланс: *{bal:,.2f} ₽*\n\nВыбери действие:", | |
| parse_mode="Markdown", reply_markup=main_kb() | |
| ) | |
| @dp.callback_query(F.data == "back_start") | |
| async def back_start(call: CallbackQuery, state: FSMContext): | |
| await state.clear() | |
| await call.answer() | |
| bal = get_balance(call.from_user.id) | |
| await call.message.edit_text( | |
| f"💰 Баланс: *{bal:,.2f} ₽*\n\nВыбери действие:", | |
| parse_mode="Markdown", reply_markup=main_kb() | |
| ) | |
| # ─── Баланс ────────────────────────────────────────────────────────────────── | |
| @dp.callback_query(F.data == "my_balance") | |
| async def show_balance(call: CallbackQuery): | |
| await call.answer() | |
| bal = get_balance(call.from_user.id) | |
| kb = InlineKeyboardMarkup(inline_keyboard=[ | |
| [InlineKeyboardButton(text="➕ Пополнить", callback_data="deposit")], | |
| [InlineKeyboardButton(text="💱 Обменять → крипта", callback_data="start_exchange")], | |
| [InlineKeyboardButton(text="🔙 Назад", callback_data="back_start")], | |
| ]) | |
| await call.message.edit_text( | |
| f"💰 *Твой баланс*\n\n*{bal:,.2f} ₽*", | |
| parse_mode="Markdown", reply_markup=kb | |
| ) | |
| # ─── История ───────────────────────────────────────────────────────────────── | |
| @dp.callback_query(F.data == "history") | |
| async def show_history(call: CallbackQuery): | |
| await call.answer() | |
| txs = get_history(call.from_user.id) | |
| if not txs: | |
| text = "📋 *История транзакций*\n\nПока пусто." | |
| else: | |
| text = "📋 *Последние транзакции:*\n\n" | |
| for tx in txs: | |
| icon = TX_LABELS.get(tx["type"], "•") | |
| sign = "+" if tx["amount"] > 0 else "" | |
| date = tx["created_at"][:16] | |
| text += f"{icon} {sign}{tx['amount']:,.0f} ₽ {date}\n" | |
| kb = InlineKeyboardMarkup(inline_keyboard=[ | |
| [InlineKeyboardButton(text="🔙 Назад", callback_data="back_start")] | |
| ]) | |
| await call.message.edit_text(text, parse_mode="Markdown", reply_markup=kb) | |
| # ─── Курсы ─────────────────────────────────────────────────────────────────── | |
| @dp.callback_query(F.data == "show_rates") | |
| async def show_rates(call: CallbackQuery): | |
| await call.answer() | |
| await call.message.edit_text("⏳ Загружаю...") | |
| try: | |
| directions = await get_directions() | |
| text = "📊 *Актуальные курсы:*\n\n" | |
| seen = {} | |
| for d in directions: | |
| crypto = parse_crypto(d["fullname"]) | |
| if crypto not in seen: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment