feat(withdraw): add user withdrawal flow using CryptoPay
This commit is contained in:
+39
-14
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user