feat(withdraw): add user withdrawal flow using CryptoPay

This commit is contained in:
2026-07-20 20:29:36 +05:00
parent 8d4d75f7fd
commit 8ae14f415f
11 changed files with 269 additions and 137 deletions
+3 -2
View File
@@ -4,14 +4,15 @@ ADMIN_ID=
CRYPTO_PAY_TOKEN=
CRYPTO_PAY_NETWORK=
CRYPTO_PAY_ASSET=USDT
# Бот для логгинга (логи будут в этом боте, можно указать один и тот же бот на главный и логгинга)
LOG_BOT_TOKEN=
# Данные шопа
SHOP_NAME=KILL UNONY MOM
SHOP_CHANNEL=https://t.me/username
BOT_USERNAME=username
SHOP_CHANNEL=
BOT_USERNAME=
# Директории и файлы
DATABASE_DIR=Users
+9 -5
View File
@@ -5,8 +5,8 @@ from dotenv import load_dotenv
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from models.config import BotConfig
from models.stats import BotStats
from models.config import BotConfig
BASE_DIR = Path(__file__).resolve().parent
@@ -22,6 +22,7 @@ def _env_int(name: str, default: int) -> int:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return int(raw)
@@ -29,6 +30,7 @@ def _env_float(name: str, default: float) -> float:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return float(raw)
@@ -36,21 +38,23 @@ def _env_bool(name: str, default: bool) -> bool:
raw = os.getenv(name)
if raw is None or raw == "":
return default
return raw.strip().lower() in {"1", "true", "yes", "on"}
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN", "")
LOG_BOT_TOKEN = os.getenv("LOG_BOT_TOKEN", "") or None
LOG_BOT_TOKEN = os.getenv("LOG_BOT_TOKEN", BOT_TOKEN) or None
ADMIN_ID = _env_int("ADMIN_ID", 0)
CRYPTO_PAY_TOKEN = os.getenv("CRYPTO_PAY_TOKEN", "")
CRYPTO_PAY_NETWORK = os.getenv("CRYPTO_PAY_NETWORK", "MAINNET")
CRYPTO_PAY_ASSET = os.getenv("CRYPTO_PAY_ASSET", "USDT")
SHOP_NAME = os.getenv("SHOP_NAME", "KILL UNONY MOM")
SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "https://t.me/bot399_start_bot")
BOT_USERNAME = os.getenv("BOT_USERNAME", "bot399_start_bot")
SHOP_CHANNEL = os.getenv("SHOP_CHANNEL", "")
BOT_USERNAME = os.getenv("BOT_USERNAME", "")
DATABASE_DIR = _resolve_path("DATABASE_DIR", "Users")
COOKIE_FILES_DIR = _resolve_path("COOKIE_FILES_DIR", "Cookies")
+24 -7
View File
@@ -11,7 +11,7 @@ from aiogram.types import (
Message,
)
from config import ADMIN_ID, ROBSEC_FILE
from config import ADMIN_ID, CRYPTO_PAY_ASSET, ROBSEC_FILE
from db.storage import load_config
from db.users import get
from keyboards.admin import admin_menu_kb
@@ -37,7 +37,10 @@ from states.admin import Form
from runtime import get_bot
from utils.logging import safe_edit
from runtime import get_cp
router = Router(name="admin")
cp = get_cp()
def _admin(user_id: int) -> bool:
@@ -78,7 +81,9 @@ def _user_card(uid: int) -> tuple[str, InlineKeyboardMarkup] | None:
],
[
InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку",
text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}",
)
],
@@ -184,7 +189,9 @@ async def admin_robsec_clear_yes(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
clear_robsec()
await safe_edit(callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb())
await safe_edit(
callback, "✅ <b>robsec.txt очищен</b>", reply_markup=admin_menu_kb()
)
await callback.answer("Очищено")
@@ -315,7 +322,8 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
"❌ Не удалось создать счет CryptoBot.", reply_markup=admin_menu_kb()
)
return
pay_url, invoice_id, amount_usdt = invoice
pay_url, invoice_id, amount_asset = invoice
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", url=pay_url)],
@@ -328,7 +336,7 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
]
)
await message.answer(
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} ₽</b> (~{amount_usdt} USDT)\n\nПосле оплаты нажмите «Проверить оплату».",
f"🏦 Счет на пополнение казны\n\n💰 Сумма: <b>{amount:.2f} ₽</b> (~{amount_asset} {CRYPTO_PAY_ASSET})\n\nПосле оплаты нажмите «Проверить оплату».",
parse_mode="HTML",
reply_markup=keyboard,
)
@@ -338,16 +346,21 @@ async def admin_treasury_amount(message: Message, state: FSMContext):
async def treasury_check(callback: CallbackQuery):
if not _admin(callback.from_user.id):
return
_, _, invoice_id, amount = callback.data.split("_", 3)
try:
paid = await check_treasury_invoice(int(invoice_id))
amount_rub = float(amount)
except (ValueError, TypeError):
paid = False
amount_rub = 0.0
if not paid:
await callback.answer("Оплата еще не поступила", show_alert=True)
return
balance = add_treasury(amount_rub)
await safe_edit(
callback,
@@ -392,7 +405,9 @@ async def admin_users(callback: CallbackQuery, state: FSMContext):
return
await state.set_state(Form.admin_find_user)
await safe_edit(
callback, "Отправьте ID пользователя или @username.", reply_markup=back_kb("admin_back")
callback,
"Отправьте ID пользователя или @username.",
reply_markup=back_kb("admin_back"),
)
@@ -489,7 +504,9 @@ async def u_ban(callback: CallbackQuery, state: FSMContext):
await state.update_data(target_uid=uid)
await state.set_state(Form.admin_user_ban_reason)
await safe_edit(
callback, "Отправьте причину блокировки:", reply_markup=back_kb("admin_back")
callback,
"Отправьте причину блокировки:",
reply_markup=back_kb("admin_back"),
)
return
card = _user_card(uid)
+47 -14
View File
@@ -36,7 +36,9 @@ async def _blocked(message: Message) -> bool:
register(message.from_user.id, message.from_user.username)
profile = get(message.from_user.id)
if profile.is_banned:
await message.answer(f"❌ Вы заблокированы. Причина: {profile.ban_reason or ''}")
await message.answer(
f"❌ Вы заблокированы. Причина: {profile.ban_reason or ''}"
)
return True
cfg = load_config()
if not cfg.bot_enabled and message.from_user.id != ADMIN_ID:
@@ -51,7 +53,9 @@ async def _blocked(message: Message) -> bool:
@router.message(StateFilter(None), F.document)
async def handle_document(message: Message):
if is_admin(message.from_user.id):
await message.answer("👑 Администраторы не могут продавать cookie. Используйте /admin.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return
if await _blocked(message):
return
@@ -60,7 +64,10 @@ async def handle_document(message: Message):
await message.answer("❌ Отправьте файл формата .txt.")
return
path = COOKIE_FILES_DIR / f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
path = (
COOKIE_FILES_DIR
/ f"{message.from_user.id}_{random.randint(100000, 999999)}.txt"
)
try:
await message.bot.download(message.document, destination=path)
text = path.read_text(encoding="utf-8", errors="ignore")
@@ -72,14 +79,22 @@ async def handle_document(message: Message):
@router.message(StateFilter(None), F.text & ~F.text.startswith("/"))
async def handle_text(message: Message):
if is_admin(message.from_user.id):
await message.answer("👑 Администраторы не могут продавать cookie. Используйте /admin.")
await message.answer(
"👑 Администраторы не могут продавать cookie. Используйте /admin."
)
return
if await _blocked(message):
return
text = message.text or ""
if "_|WARNING:-DO-NOT-SHARE-THIS." not in text:
await message.answer("👋 Используйте меню ниже или отправьте текст с cookie.", reply_markup=main_menu_kb())
await message.answer(
"👋 Используйте меню ниже или отправьте текст с cookie.",
reply_markup=main_menu_kb(),
)
return
await process_cookies(message, text)
@@ -99,7 +114,9 @@ async def process_cookies(message: Message, raw_text: str):
valid = []
async with aiohttp.ClientSession() as session:
tasks = [check_cookie_with_retry(session, cookie, proxies) for cookie in cookies]
tasks = [
check_cookie_with_retry(session, cookie, proxies) for cookie in cookies
]
for index, task in enumerate(asyncio.as_completed(tasks), start=1):
try:
result = await task
@@ -117,15 +134,22 @@ async def process_cookies(message: Message, raw_text: str):
pass
if not valid:
await progress.edit_text(f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}")
await progress.edit_text(
f"❌ Валидные cookie не найдены. Дубликатов: {duplicates}"
)
return
await progress.edit_text(f"⏳ <b>Проверяю донаты</b>\nВалидных cookie: {len(valid)}", parse_mode="HTML")
await progress.edit_text(
f"⏳ <b>Проверяю донаты</b>\nВалидных cookie: {len(valid)}", parse_mode="HTML"
)
donations = []
async with aiohttp.ClientSession() as session:
async def get_donation(result):
proxy = random.choice(proxies) if proxies else None
all_time = await get_all_time_donate(session, result.cookie, result.user_id, proxy)
all_time = await get_all_time_donate(
session, result.cookie, result.user_id, proxy
)
year = await get_year_donate(result.cookie, result.user_id, proxy)
return CookieDonationInfo(
cookie=result.cookie,
@@ -152,14 +176,16 @@ async def process_cookies(message: Message, raw_text: str):
fresh_ok = []
fresh_failed = 0
for offset in range(0, len(priced), FRESHER_BATCH_SIZE):
batch = priced[offset:offset + FRESHER_BATCH_SIZE]
batch = priced[offset : offset + FRESHER_BATCH_SIZE]
async def refresh(item):
proxy = random.choice(proxies) if proxies else None
ok, refreshed = await fresh_cookie_async(item.cookie, proxy)
return item, ok, refreshed
for item, ok, refreshed in await asyncio.gather(*(refresh(item) for item in batch)):
for item, ok, refreshed in await asyncio.gather(
*(refresh(item) for item in batch)
):
if ok and isinstance(refreshed, str) and refreshed.startswith("_|WARNING"):
fresh_ok.append((item, refreshed))
else:
@@ -184,8 +210,13 @@ async def process_cookies(message: Message, raw_text: str):
with ROBSEC_FILE.open("a", encoding="utf-8") as output:
output.writelines(f"{cookie}\n" for _, cookie in fresh_ok)
user_file = COOKIE_FILES_DIR / f"robsec_{message.from_user.id}_{random.randint(100000, 999999)}.txt"
user_file.write_text("".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8")
user_file = (
COOKIE_FILES_DIR
/ f"robsec_{message.from_user.id}_{random.randint(100000, 999999)}.txt"
)
user_file.write_text(
"".join(f"{cookie}\n" for _, cookie in fresh_ok), encoding="utf-8"
)
payout = sum(item.price for item, _ in fresh_ok)
profile = get(message.from_user.id)
@@ -202,7 +233,9 @@ async def process_cookies(message: Message, raw_text: str):
stats.total_paid_direct += payout
save_stats(stats)
bot = get_bot() or message.bot
await apply_referral_payout(bot, profile.user_id, profile.username, payout, cfg.referral_percent)
await apply_referral_payout(
bot, profile.user_id, profile.username, payout, cfg.referral_percent
)
method = "📊 AVG-система" if avg_used else "📋 Поштучный"
report = (
+64 -19
View File
@@ -1,11 +1,12 @@
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery
from aiogram.types import LinkPreviewOptions, CallbackQuery, Message
from aiogram.fsm.state import State, StatesGroup
from config import ADMIN_ID, BOT_USERNAME
from db.storage import load_config, load_stats
from db.storage import load_config, load_stats, save_config
from db.users import get, register, save
from keyboards.user import back_kb
from keyboards.user import back_kb, activate_kb
from runtime import get_bot, get_log_bot
from services.withdrawals import create_crypto_check
from utils.logging import log_admin, safe_edit
@@ -13,6 +14,10 @@ from utils.logging import log_admin, safe_edit
router = Router(name="user")
class WithdrawState(StatesGroup):
waiting_for_amount = State()
def _get_profile(callback: CallbackQuery):
profile = get(callback.from_user.id)
if profile is None:
@@ -77,6 +82,7 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
await state.clear()
profile = _get_profile(callback)
cfg = load_config()
if profile.balance < cfg.min_withdraw:
await safe_edit(
callback,
@@ -87,37 +93,76 @@ async def cb_withdraw(callback: CallbackQuery, state: FSMContext):
)
await callback.answer()
return
if profile.balance > cfg.treasury:
await safe_edit(
callback,
f"⚠️ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>.",
f"💳 <b>Вывод средств</b>\n\n"
f"Ваш баланс: <b>{profile.balance:.2f} ₽</b>\n"
f"Минимум: <b>{cfg.min_withdraw:.2f} ₽</b>\n\n"
f"Введите сумму для вывода:",
reply_markup=back_kb(),
)
await state.set_state(WithdrawState.waiting_for_amount)
await callback.answer()
@router.message(WithdrawState.waiting_for_amount)
async def process_withdraw_amount(message: Message, state: FSMContext):
profile = _get_profile(message)
cfg = load_config()
try:
amount = float(message.text)
except ValueError:
await message.answer("❌ Введите корректную сумму числом.")
return
await callback.answer("⏳ Создаю чек...")
await safe_edit(callback, " Создаю CryptoBot-чек, подождите...")
amount = profile.balance
check_url = await create_crypto_check(amount)
if amount <= 0:
await message.answer(" Сумма должна быть больше нуля.")
return
if amount < cfg.min_withdraw:
await message.answer(f"❌ Минимум для вывода: <b>{cfg.min_withdraw:.2f} ₽</b>.")
return
if amount > profile.balance:
await message.answer(
f"❌ Недостаточно средств. Ваш баланс: <b>{profile.balance:.2f} ₽</b>."
)
return
if amount > cfg.treasury:
await message.answer(
f"❌ Недостаточно средств в казне: <b>{cfg.treasury:.2f} ₽</b>."
)
return
await state.clear()
await message.answer("⏳ Создаю CryptoBot-чек, подождите...")
check_url, check_image_url = await create_crypto_check(amount)
if not check_url:
await safe_edit(callback, "❌ Не удалось создать чек. Попробуйте позже.", reply_markup=back_kb())
await message.answer(
"❌ Не удалось создать чек. Попробуйте позже.",
reply_markup=back_kb(),
)
return
profile.balance = 0.0
profile.balance -= amount
save(profile.user_id, profile)
cfg.treasury = max(0.0, cfg.treasury - amount)
from db.storage import save_config
save_config(cfg)
await safe_edit(
callback,
f" Чек создан на сумму <b>{amount:.2f} ₽</b>.\n"
f"<a href='{check_url}'>Активировать чек</a>",
reply_markup=back_kb(),
disable_web_page_preview=True,
await message.answer(
text=f"🦋 Чек на <b>{amount:.2f} ₽</b>.",
reply_markup=activate_kb(url=check_url, amount=amount),
link_preview_options=LinkPreviewOptions(
url=check_image_url, show_above_text=True
),
)
bot = get_bot() or callback.message.bot
bot = get_bot() or message.bot
await log_admin(
bot,
ADMIN_ID,
+11
View File
@@ -19,6 +19,17 @@ def main_menu_kb() -> InlineKeyboardMarkup:
)
def activate_kb(url: str, amount: float) -> InlineKeyboardMarkup:
if not url:
return back_kb()
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text=f"Получить {amount:.2f}", url=url)]
]
)
def back_kb(callback_data: str = "back_main") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
+9 -8
View File
@@ -4,22 +4,23 @@ from datetime import datetime
from aiosend import CryptoPay, TESTNET, MAINNET
from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
from config import (
ADMIN_ID,
BOT_TOKEN,
CRYPTO_PAY_NETWORK,
CRYPTO_PAY_TOKEN,
LOG_BOT_TOKEN,
ADMIN_ID,
SHOP_NAME,
CRYPTO_PAY_TOKEN,
CRYPTO_PAY_NETWORK,
)
from db.storage import load_config, load_stats
from handlers import admin, start, upload, user
from runtime import set_bots
from runtime import set_bots, set_cp
from utils.logging import log_admin
@@ -28,11 +29,13 @@ async def main() -> None:
bot = Bot(token=BOT_TOKEN, default=defaults)
log_bot = Bot(token=LOG_BOT_TOKEN, default=defaults) if LOG_BOT_TOKEN else None
set_bots(bot, log_bot)
cp_net = TESTNET if CRYPTO_PAY_NETWORK.lower() == "testnet" else MAINNET
cp = CryptoPay(token=CRYPTO_PAY_TOKEN, network=cp_net)
set_cp(cp)
set_bots(bot, log_bot)
dp = Dispatcher(storage=MemoryStorage())
dp.include_router(start.router)
@@ -52,13 +55,11 @@ async def main() -> None:
try:
await dp.start_polling(bot)
await cp.start_polling(bot)
finally:
if log_bot:
await log_bot.session.close()
await cp.session.close()
await bot.session.close()
+11
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
from aiogram import Bot
from aiosend import CryptoPay
_bot: Bot | None = None
_log_bot: Bot | None = None
_cp: CryptoPay | None = None
def set_bots(bot: Bot, log_bot: Bot | None = None) -> None:
@@ -18,3 +20,12 @@ def get_bot() -> Bot | None:
def get_log_bot() -> Bot | None:
return _log_bot
def set_cp(cp: CryptoPay | None = None) -> None:
global _cp
_cp = cp
def get_cp() -> CryptoPay | None:
return _cp
+39 -14
View File
@@ -1,23 +1,22 @@
from __future__ import annotations
import asyncio
import os
import asyncio
from aiogram.enums import ParseMode
from aiogram.types import FSInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from config import ADMIN_ID, BOT_USERNAME, ROBSEC_FILE
from db.storage import load_config, save_config, load_stats, save_stats
from config import ADMIN_ID, ROBSEC_FILE
from utils.logging import safe_edit
from db.storage import load_config, save_config
from db.users import all_users, get, save, find_by_username
from keyboards.admin import admin_menu_kb
from models.user import UserProfile
from runtime import get_log_bot
from utils.logging import log_admin, log_admin_document, safe_edit
def is_admin(user_id: int) -> bool:
if user_id == ADMIN_ID:
return True
profile = get(user_id)
return bool(profile and profile.is_admin)
@@ -25,6 +24,7 @@ def is_admin(user_id: int) -> bool:
def toggle_shop() -> bool:
cfg = load_config()
cfg.shop_enabled = not cfg.shop_enabled
save_config(cfg)
return cfg.shop_enabled
@@ -32,6 +32,7 @@ def toggle_shop() -> bool:
def toggle_bot() -> bool:
cfg = load_config()
cfg.bot_enabled = not cfg.bot_enabled
save_config(cfg)
return cfg.bot_enabled
@@ -52,6 +53,7 @@ def update_min_withdraw(value: float) -> None:
def add_treasury(amount_rub: float) -> float:
cfg = load_config()
cfg.treasury += amount_rub
save_config(cfg)
return cfg.treasury
@@ -59,6 +61,7 @@ def add_treasury(amount_rub: float) -> float:
def subtract_treasury(amount_rub: float) -> float:
cfg = load_config()
cfg.treasury = max(0.0, cfg.treasury - amount_rub)
save_config(cfg)
return cfg.treasury
@@ -99,8 +102,12 @@ async def send_user_card(target, uid: int) -> None:
kb = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_add_{uid}"),
InlineKeyboardButton(text=" Баланс", callback_data=f"u_bal_sub_{uid}"),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_add_{uid}"
),
InlineKeyboardButton(
text=" Баланс", callback_data=f"u_bal_sub_{uid}"
),
],
[
InlineKeyboardButton(
@@ -110,7 +117,9 @@ async def send_user_card(target, uid: int) -> None:
],
[
InlineKeyboardButton(
text="👑 Убрать админку" if profile.is_admin else "👑 Выдать админку",
text=(
"👑 Убрать админку" if profile.is_admin else "👑 Выдать админку"
),
callback_data=f"u_admin_{uid}",
)
],
@@ -123,15 +132,24 @@ async def send_user_card(target, uid: int) -> None:
await safe_edit(target, text, reply_markup=kb)
async def broadcast(bot, text: str | None = None, photo_id: str | None = None, caption: str | None = None) -> tuple[int, int]:
async def broadcast(
bot,
text: str | None = None,
photo_id: str | None = None,
caption: str | None = None,
) -> tuple[int, int]:
ok = 0
fail = 0
for uid in all_users():
try:
if photo_id:
await bot.send_photo(uid, photo_id, caption=caption or "", parse_mode=ParseMode.HTML)
await bot.send_photo(
uid, photo_id, caption=caption or "", parse_mode=ParseMode.HTML
)
else:
await bot.send_message(uid, text or caption or "-", parse_mode=ParseMode.HTML)
await bot.send_message(
uid, text or caption or "-", parse_mode=ParseMode.HTML
)
ok += 1
except Exception:
fail += 1
@@ -143,6 +161,7 @@ def find_user(query: str) -> int | None:
if query.isdigit():
uid = int(query)
return uid if get(uid) else None
return find_by_username(query)
@@ -150,6 +169,7 @@ def set_balance(uid: int, value: float) -> None:
profile = get(uid)
if not profile:
return
profile.balance = value
save(uid, profile)
@@ -158,6 +178,7 @@ def add_balance(uid: int, amount: float) -> None:
profile = get(uid)
if not profile:
return
profile.balance += amount
save(uid, profile)
@@ -166,6 +187,7 @@ def sub_balance(uid: int, amount: float) -> None:
profile = get(uid)
if not profile:
return
profile.balance = max(0.0, profile.balance - amount)
save(uid, profile)
@@ -174,6 +196,7 @@ def toggle_user_admin(uid: int) -> bool:
profile = get(uid)
if not profile:
return False
profile.is_admin = not profile.is_admin
save(uid, profile)
return profile.is_admin
@@ -183,6 +206,7 @@ def ban_user(uid: int, reason: str) -> None:
profile = get(uid)
if not profile:
return
profile.is_banned = True
profile.ban_reason = reason
save(uid, profile)
@@ -192,6 +216,7 @@ def unban_user(uid: int) -> None:
profile = get(uid)
if not profile:
return
profile.is_banned = False
profile.ban_reason = ""
save(uid, profile)
+38 -57
View File
@@ -1,82 +1,63 @@
from __future__ import annotations
import aiohttp
import logging
from runtime import get_cp
from config import CRYPTO_PAY_TOKEN
async def _get_usdt_rub_rate(session: aiohttp.ClientSession) -> float:
rate = 90.0
async with session.get(
"https://testnet-pay.crypt.bot/api/getExchangeRates",
headers={"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN},
) as response:
data = await response.json()
if data.get("ok"):
for item in data.get("result", []):
if item.get("source") == "USDT" and item.get("target") == "RUB":
rate = float(item["rate"])
break
return rate
from config import CRYPTO_PAY_ASSET
async def create_crypto_check(amount_rub: float) -> str | None:
try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
async with aiohttp.ClientSession() as session:
usdt_rub = await _get_usdt_rub_rate(session)
amount_usdt = round(amount_rub / usdt_rub, 4)
async with session.post(
"https://testnet-pay.crypt.bot/api/createCheck",
headers=headers,
json={"asset": "USDT", "amount": str(amount_usdt)},
) as response:
data = await response.json()
if data.get("ok"):
return data["result"]["bot_check_url"]
cp = get_cp()
amount_asset = await cp.exchange(
amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET
)
createdCheck = await cp.create_check(
amount=amount_asset, asset=CRYPTO_PAY_ASSET
)
check_image_url = await createdCheck.get_image(fiat="RUB")
return createdCheck.bot_check_url, check_image_url
except Exception as exc:
logging.error("crypto check: %s", exc)
return None
async def create_treasury_invoice(amount_rub: float) -> tuple[str, int, float] | None:
try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
async with aiohttp.ClientSession() as session:
usdt_rub = await _get_usdt_rub_rate(session)
amount_usdt = round(amount_rub / usdt_rub, 4)
async with session.post(
"https://testnet-pay.crypt.bot/api/createInvoice",
headers=headers,
json={
"asset": "USDT",
"amount": str(amount_usdt),
"description": f"Treasury +{amount_rub} RUB",
"payload": f"treasury_{amount_rub}",
},
) as response:
data = await response.json()
if data.get("ok"):
result = data["result"]
return result["pay_url"], int(result["invoice_id"]), amount_usdt
cp = get_cp()
createdInvoice = await cp.create_invoice(
amount=amount_rub,
currency_type="fiat",
accepted_assets=[CRYPTO_PAY_ASSET],
fiat="RUB",
)
amount_asset = round(
await cp.exchange(amount=amount_rub, source="RUB", target=CRYPTO_PAY_ASSET),
2,
)
return (createdInvoice.pay_url, int(createdInvoice.invoice_id), amount_asset)
except Exception as exc:
logging.error("create_treasury_invoice: %s", exc)
return None
async def check_treasury_invoice(invoice_id: int) -> bool:
try:
headers = {"Crypto-Pay-API-Token": CRYPTO_PAY_TOKEN}
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://testnet-pay.crypt.bot/api/getInvoices?invoice_ids={invoice_id}",
headers=headers,
) as response:
data = await response.json()
if data.get("ok"):
items = data.get("result", {}).get("items", [])
return bool(items and items[0].get("status") == "paid")
cp = get_cp()
paidInvoice = await cp.get_invoice(invoice_id)
return paidInvoice.status == "paid"
except Exception as exc:
logging.error("check_treasury_invoice: %s", exc)
+4 -1
View File
@@ -8,11 +8,14 @@ async def log_admin(bot, admin_id: int, text: str, log_bot=None) -> None:
try:
target = log_bot or bot
await target.send_message(admin_id, text, parse_mode=ParseMode.HTML)
except Exception as exc:
logging.error("log_admin: %s", exc)
async def log_admin_document(bot, admin_id: int, path: str, caption: str | None = None, log_bot=None) -> None:
async def log_admin_document(
bot, admin_id: int, path: str, caption: str | None = None, log_bot=None
) -> None:
try:
target = log_bot or bot
await target.send_document(