$ sudo teach IT
Модуль 6 · Polling, Webhook & Filters

Урок 6.2 — Check Polling

Освоим механизм polling для отслеживания статуса крипто-чеков: декораторы @cp.check_activated() и @cp.check_expired(), метод check.poll() и полный жизненный цикл чека.

Крипто-чеки (Crypto Checks) — это предоплаченные ваучеры, которые можно создавать и отправлять пользователям. Как и инвойсы, чеки имеют жизненный цикл: создание, ожидание активации и истечение срока. В aiosend для отслеживания чеков используется polling с декораторами @cp.check_activated() и @cp.check_expired(), а также метод check.poll().

🪙

Что такое Check Polling

Check polling — это механизм, аналогичный invoice polling, но предназначенный для отслеживания статуса крипто-чеков. Чек может быть в одном из трёх состояний:

🟡
ACTIVE
Ожидает активации
🟢
ACTIVATED
Активирован получателем
🔴
EXPIRED
Истёк срок

В aiosend polling для чеков работает через те же механизмы, что и для инвойсов. Разница лишь в названиях декораторов и внутренних методах API (getChecks вместо getInvoices).

💡 Invoice Polling vs Check Polling

По сути, это один и тот же PollingManager, который внутри обрабатывает разные типы сущностей. start_polling() автоматически проверяет и инвойсы, и чеки, если зарегистрированы соответствующие обработчики. Вам не нужно запускать разные polling-циклы для инвойсов и чеков.

✅

Декоратор @cp.check_activated()

Декоратор @cp.check_activated() регистрирует обработчик, который вызывается, когда крипто-чек активирован (получатель забрал средства). Это аналог @cp.invoice_paid() для чеков.

Сигнатура обработчика:

Python · Сигнатура обработчика check_activated
@cp.check_activated()
async def on_check_activated(check: Check, **kwargs) -> None:
    """Обработчик активированного чека.
    
    Аргументы:
        check: объект Check с обновлённым статусом ACTIVATED
        **kwargs: дополнительные аргументы от фильтров
    """
    pass

Объект Check содержит поля:

Поле Тип Описание
check_id int Уникальный ID чека
amount str Сумма чека (строка для точности)
asset str Криптовалюта чека
status str Статус: "active", "activated", "expired"
payload str | None Внутренние данные (до 4096 символов)
created_at str Дата создания чека
activated_at str | None Дата активации (если активирован)
expires_at str | None Дата истечения срока
Python · Пример с check_activated
import asyncio
from aiosend import CryptoPay

cp = CryptoPay(token="YOUR_TOKEN")

@cp.check_activated()
async def on_activated(check, **kwargs):
    print(f"✅ Чек #{check.check_id} активирован!")
    print(f"   Сумма: {check.amount} {check.asset}")
    print(f"   Кто активировал: {check.activated_by_user_id}")
    print(f"   Время: {check.activated_at}")
    print(f"   Payload: {check.payload}")

async def main():
    # Создаём чек
    check = await cp.create_check(
        amount=10,
        asset="USDT",
        payload="bonus_for_user_123",
    )
    print(f"Чек создан: {check.bot_check_url}")
    
    # Запускаем polling
    await cp.start_polling()

asyncio.run(main())
⏰

Декоратор @cp.check_expired()

Декоратор @cp.check_expired() вызывается, когда срок действия чека истёк, и он не был активирован получателем. Это важно для возврата средств или пересоздания чека.

Python · Сигнатура обработчика check_expired
@cp.check_expired()
async def on_check_expired(check: Check, **kwargs) -> None:
    """Обработчик истёкшего чека.
    
    Аргументы:
        check: объект Check со статусом EXPIRED
        **kwargs: дополнительные аргументы от фильтров
    """
    pass

Типичные сценарии использования check_expired:

  • Возврат средств: если чек не был активирован, вернуть средства отправителю
  • Уведомление: отправить сообщение создателю чека об истечении срока
  • Пересоздание: автоматически создать новый чек с продлённым сроком
  • Логирование: записать статистику неиспользованных чеков

Важно понимать: средства чека не «сгорают» при истечении срока. Они возвращаются на баланс создателя чека. check_expired просто уведомляет вас, что срок истёк.

Python · Полный пример с check_activated и check_expired
import asyncio
import logging
from aiosend import CryptoPay
from aiosend.polling import PollingConfig

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

cp = CryptoPay(
    token="YOUR_TOKEN",
    polling_config=PollingConfig(timeout=300, delay=2),
)

# Статистика
stats = {"created": 0, "activated": 0, "expired": 0}

@cp.check_activated()
async def handle_activated(check, **kwargs):
    stats["activated"] += 1
    logger.info(f"🎉 Чек #{check.check_id} активирован!")
    logger.info(f"   Сумма: {check.amount} {check.asset}")
    
    # Бизнес-логика: начисление бонуса, запись в БД
    if check.payload:
        user_id = check.payload.replace("user_", "")
        logger.info(f"   Пользователь #{user_id} получил {check.amount} {check.asset}")
        await award_bonus(int(user_id), check)

@cp.check_expired()
async def handle_expired(check, **kwargs):
    stats["expired"] += 1
    logger.warning(f"⏰ Чек #{check.check_id} истёк!")
    logger.info(f"   Сумма: {check.amount} {check.asset} возвращена на баланс")
    
    if check.payload:
        user_id = check.payload.replace("user_", "")
        logger.info(f"   Уведомление пользователю #{user_id} об истечении чека")
        await notify_expired(int(user_id), check)

async def award_bonus(user_id: int, check):
    await asyncio.sleep(0.3)
    logger.info(f"   ✅ Бонус {check.amount} {check.asset} начислен user #{user_id}")

async def notify_expired(user_id: int, check):
    logger.info(f"   📧 Уведомление отправлено user #{user_id}")

async def main():
    # Создаём несколько чеков
    for i in range(3):
        check = await cp.create_check(
            amount=5 + i * 5,
            asset="USDT",
            payload=f"user_{100 + i}",
        )
        stats["created"] += 1
        logger.info(f"📄 Создан чек #{check.check_id} на {check.amount} {check.asset}")
    
    logger.info(f"\nСоздано чеков: {stats['created']}")
    logger.info("Запуск polling...")
    
    try:
        await cp.start_polling()
    except KeyboardInterrupt:
        logger.info("\nPolling остановлен пользователем")
    
    logger.info(f"Итог: активировано {stats['activated']}, истекло {stats['expired']}")

if __name__ == "__main__":
    asyncio.run(main())
⏳

Метод check.poll() — ручное ожидание активации

Как и для инвойсов, для чеков есть метод check.poll(), который позволяет дождаться активации конкретного чека вручную.

Python · Сигнатура check.poll()
async def poll(
    self,
    timeout: int | None = None,
    delay: float | None = None,
) -> Check:
    """Ожидает активации или истечения чека.
    
    Аргументы:
        timeout: максимальное время ожидания
        delay: задержка между проверками
    
    Возвращает:
        Check с обновлённым статусом (ACTIVATED или EXPIRED)
    """
    ...

Внутри poll() использует тот же механизм, что и invoice.poll() — циклический вызов get_checks(check_ids=[...]).

Python · Использование check.poll()
import asyncio
from aiosend import CryptoPay

async def main():
    cp = CryptoPay(token="YOUR_TOKEN")
    
    # Создаём чек
    check = await cp.create_check(
        amount=25,
        asset="USDT",
        payload="prize_for_user_456",
    )
    print(f"Чек создан: {check.bot_check_url}")
    print("Ожидаем активации чека...")
    
    # Ждём активации
    result = await check.poll(timeout=300, delay=2)
    
    if result.status == "activated":
        print(f"✅ Чек #{result.check_id} активирован!")
        print(f"   Кем: {result.activated_by_user_id}")
        print(f"   Когда: {result.activated_at}")
    else:
        print(f"❌ Чек просрочен")

asyncio.run(main())

💡 Когда использовать check.poll()?

Метод удобен для простых скриптов и тестирования. Например, вы создали чек и хотите дождаться его активации в консольной утилите. Для продакшена лучше использовать декораторы, так как они не блокируют выполнение и масштабируются.

🔁

Жизненный цикл чека с polling

Проследим полный путь чека от создания до финального статуса:

Диаграмма жизненного цикла чека

           ┌─────────────────────────────────────────────┐
           │        1. Создание чека                       │
           │  cp.create_check(amount, asset, ...)         │
           │         → status = ACTIVE                    │
           └────────────────┬────────────────────────────┘
                            │
                            ▼
           ┌─────────────────────────────────────────────┐
           │        2. Polling (декораторы или poll())    │
           │  cp.start_polling()  или  check.poll()      │
           │  → периодический опрос getChecks()           │
           └────────────────┬────────────────────────────┘
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
   ┌─────────────────────┐   ┌─────────────────────┐
   │ 3a. Активирован      │   │ 3b. Истёк срок      │
   │ Получатель забрал    │   │ Срок действия истёк │
   │ → status = ACTIVATED│   │ → status = EXPIRED  │
   │ Вызов on_activated() │   │ Вызов on_expired()  │
   │ check_activated      │   │ check_expired       │
   └─────────────────────┘   └─────────────────────┘
              ▼                           ▼
   ┌─────────────────────┐   ┌─────────────────────┐
   │ 4a. Обработка        │   │ 4b. Обработка       │
   │ Начисление средств   │   │ Возврат на баланс   │
   │ Запись в БД         │   │ Уведомление         │
   │ Благодарность       │   │ Пересоздание чека   │
   └─────────────────────┘   └─────────────────────┘

Важные отличия от invoice polling:

  • Чек создаётся вами (вы отдаёте деньги), инвойс создаётся для вас (вам платят)
  • При активации чека деньги списываются с вашего баланса и зачисляются получателю
  • При истечении чека деньги возвращаются на ваш баланс
  • Чек не может быть «оплачен частично» — только полностью активирован или истёк
🧩

Полный пример: система бонусных чеков

Напишем систему, которая создаёт бонусные чеки для пользователей и отслеживает их активацию. Если чек не активирован за 24 часа — создаётся новый с таким же номиналом.

Python · Система бонусных чеков с авто-продлением
import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime
from aiosend import CryptoPay
from aiosend.polling import PollingConfig

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class BonusSystem:
    """Система управления бонусными чеками."""
    cp: CryptoPay
    pending_users: dict[int, str] = field(default_factory=dict)
    
    def register_handlers(self):
        @self.cp.check_activated()
        async def on_activated(check, **kwargs):
            if check.payload and check.payload.startswith("bonus_"):
                user_id = int(check.payload.split("_")[1])
                logger.info(f"🎉 Пользователь #{user_id} активировал бонус!")
                logger.info(f"   Получено: {check.amount} {check.asset}")
                if user_id in self.pending_users:
                    del self.pending_users[user_id]
        
        @self.cp.check_expired()
        async def on_expired(check, **kwargs):
            if check.payload and check.payload.startswith("bonus_"):
                user_id = int(check.payload.split("_")[1])
                logger.warning(f"⏰ Бонус для #{user_id} истёк. Создаём новый...")
                # Автоматически пересоздаём чек
                await self.issue_bonus(user_id, float(check.amount), check.asset)
    
    async def issue_bonus(self, user_id: int, amount: float, asset: str):
        """Создаёт бонусный чек для пользователя."""
        check = await self.cp.create_check(
            amount=amount,
            asset=asset,
            payload=f"bonus_{user_id}",
        )
        self.pending_users[user_id] = check.bot_check_url
        logger.info(f"📄 Бонус #{check.check_id} для user #{user_id}: {check.amount} {check.asset}")
        return check
    
    async def run(self):
        self.register_handlers()
        logger.info("🚀 Запуск check polling...")
        await self.cp.start_polling()

async def main():
    cp = CryptoPay(
        token="YOUR_TOKEN",
        polling_config=PollingConfig(timeout=86400, delay=5),
    )
    
    system = BonusSystem(cp)
    
    # Создаём бонусы для нескольких пользователей
    await system.issue_bonus(101, 10, "USDT")
    await system.issue_bonus(102, 25, "USDT")
    await system.issue_bonus(103, 50, "TON")
    
    print(f"Создано бонусов: {len(system.pending_users)}")
    print("Ожидание активации или истечения...")
    
    try:
        await system.run()
    except KeyboardInterrupt:
        logger.info("Система остановлена")

if __name__ == "__main__":
    asyncio.run(main())
⚠️

Ошибки и граничные случаи

Ошибка: Недостаточно средств для создания чека

При создании чека баланс вашего приложения должен быть достаточным. Иначе API вернёт ошибку.

try:
    check = await cp.create_check(amount=10000, asset="USDT")
except APIError as e:
    print(f"Недостаточно средств: {e}")
    
# Решение: проверяйте баланс перед созданием чека
balance = await cp.get_balance()
usdt_balance = next(b for b in balance if b.currency_code == "USDT")
if float(usdt_balance.available) >= 10000:
    check = await cp.create_check(amount=10000, asset="USDT")

Граничный случай: Чек активирован мгновенно

Получатель может активировать чек сразу после получения ссылки. В этом случае обработчик check_activated будет вызван при ближайшем цикле polling. Задержка между созданием чека и обработкой активации — максимум delay секунд (по умолчанию 2).

Совет: Всегда проверяйте payload

При обработке активации чека всегда проверяйте, что payload соответствует ожидаемому формату. Чек может быть активирован кем угодно, поэтому payload — ваш единственный способ идентифицировать получателя. Используйте PayloadData (урок 6.5) для структурированных данных.

📎

Дополнительные примеры с чеками

Рассмотрим ещё несколько сценариев использования check polling в реальных проектах.

Пример: Массовая рассылка чеков с отслеживанием

Представьте, что нужно разослать бонусные чеки 100 пользователям и отследить, кто активировал, а кто нет:

Python · Массовая рассылка чеков
import asyncio
import logging
from dataclasses import dataclass, field
from aiosend import CryptoPay
from aiosend.polling import PollingConfig

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass
class BulkCheckSender:
    """Массовая рассылка чеков с отслеживанием."""
    cp: CryptoPay
    sent: dict[int, str] = field(default_factory=dict)  # check_id -> payload
    activated: set[int] = field(default_factory=set)
    expired: set[int] = field(default_factory=set)
    
    def register_handlers(self):
        @self.cp.check_activated()
        async def on_activated(check, **kwargs):
            self.activated.add(check.check_id)
            logger.info(f"✅ Чек #{check.check_id} активирован!")
            if check.check_id in self.sent:
                payload = self.sent[check.check_id]
                logger.info(f"   Payload: {payload}, сумма: {check.amount} {check.asset}")
        
        @self.cp.check_expired()
        async def on_expired(check, **kwargs):
            self.expired.add(check.check_id)
            logger.warning(f"⏰ Чек #{check.check_id} истёк!")
    
    async def send_bonuses(self, users: list[dict]):
        """Создаёт чеки для списка пользователей."""
        for user in users:
            check = await self.cp.create_check(
                amount=user["amount"],
                asset=user.get("asset", "USDT"),
                payload=f"bulk_bonus_{user['user_id']}",
            )
            self.sent[check.check_id] = f"user_{user['user_id']}"
            logger.info(f"📄 Создан чек #{check.check_id} для user #{user['user_id']}")
            await asyncio.sleep(0.5)  # небольшая пауза между созданиями
    
    def print_stats(self):
        total = len(self.sent)
        act = len(self.activated)
        exp = len(self.expired)
        logger.info(f"\n{'='*40}")
        logger.info(f"Статистика рассылки:")
        logger.info(f"  Отправлено: {total}")
        logger.info(f"  Активировано: {act}")
        logger.info(f"  Истекло: {exp}")
        logger.info(f"  Ожидают: {total - act - exp}")

async def main():
    cp = CryptoPay(
        token="YOUR_TOKEN",
        polling_config=PollingConfig(timeout=86400, delay=3),
    )
    
    sender = BulkCheckSender(cp)
    sender.register_handlers()
    
    # Создаём тестовых пользователей
    test_users = [
        {"user_id": 101, "amount": 10},
        {"user_id": 102, "amount": 25},
        {"user_id": 103, "amount": 50, "asset": "TON"},
    ]
    
    await sender.send_bonuses(test_users)
    logger.info("Запуск polling...")
    
    try:
        await cp.start_polling()
    except KeyboardInterrupt:
        logger.info("Остановлено пользователем")
    
    sender.print_stats()

if __name__ == "__main__":
    asyncio.run(main())

Пример: Создание чека с последующим ожиданием через poll()

Для простых сценариев можно использовать check.poll() в отдельной задаче:

Python · Ожидание активации через poll()
import asyncio
from aiosend import CryptoPay
from aiosend.polling import PollingConfig

async def create_and_wait_check(cp: CryptoPay, amount: float,
                                  asset: str, payload: str):
    """Создаёт чек и ожидает его активации."""
    check = await cp.create_check(
        amount=amount,
        asset=asset,
        payload=payload,
    )
    print(f"Чек создан: {check.bot_check_url}")
    print(f"Ожидание активации (таймаут: 5 минут)...")
    
    try:
        result = await check.poll(timeout=300, delay=2)
        if result.status == "activated":
            print(f"✅ Чек #{result.check_id} активирован!")
            print(f"   Кем: {result.activated_by_user_id}")
            print(f"   Когда: {result.activated_at}")
            return result
        else:
            print(f"❌ Чек истёк (status={result.status})")
    except asyncio.TimeoutError:
        print(f"⏰ Таймаут ожидания активации")
    
    return None

async def main():
    cp = CryptoPay(
        token="YOUR_TOKEN",
        polling_config=PollingConfig(timeout=600, delay=2),
    )
    
    # Создаём и ждём несколько чеков параллельно
    tasks = [
        create_and_wait_check(cp, 10, "USDT", "bonus_user_1"),
        create_and_wait_check(cp, 25, "USDT", "bonus_user_2"),
    ]
    
    results = await asyncio.gather(*tasks)
    activated = sum(1 for r in results if r is not None)
    print(f"\nИтого активировано: {activated} из {len(tasks)}")

if __name__ == "__main__":
    asyncio.run(main())

Пример: Обработка check и invoice в одном polling

Один клиент может одновременно обрабатывать и инвойсы, и чеки. Все обработчики регистрируются на одном cp и запускаются одним start_polling():

Python · Единый polling для всего
import asyncio
from aiosend import CryptoPay
from aiosend.polling import PollingConfig

cp = CryptoPay(
    token="YOUR_TOKEN",
    polling_config=PollingConfig(timeout=600, delay=2),
)

# Регистрируем все обработчики вместе
@cp.invoice_paid()
async def on_invoice_paid(invoice, **kwargs):
    print(f"💵 Инвойс #{invoice.invoice_id} оплачен: {invoice.amount} {invoice.asset}")

@cp.invoice_expired()
async def on_invoice_expired(invoice, **kwargs):
    print(f"⏰ Инвойс #{invoice.invoice_id} просрочен")

@cp.check_activated()
async def on_check_activated(check, **kwargs):
    print(f"🎉 Чек #{check.check_id} активирован: {check.amount} {check.asset}")

@cp.check_expired()
async def on_check_expired(check, **kwargs):
    print(f"⏰ Чек #{check.check_id} истёк")

async def main():
    # Создаём и инвойс, и чек
    invoice = await cp.create_invoice(amount=15, asset="USDT")
    check = await cp.create_check(amount=5, asset="USDT")
    print(f"Инвойс: {invoice.bot_invoice_url}")
    print(f"Чек: {check.bot_check_url}")
    
    # Один polling обработает всё
    await cp.start_polling()

asyncio.run(main())
📌

Что важно запомнить

1️⃣
Check polling = Invoice polling. Тот же PollingManager, те же принципы, другие декораторы: @cp.check_activated() и @cp.check_expired().
2️⃣
Один start_polling() для всего. Обработчики и инвойсов, и чеков запускаются одним вызовом cp.start_polling().
3️⃣
Чек — это вы отдаёте деньги. В отличие от инвойса, где вам платят, чек — это предоплаченный ваучер, который вы дарите.
4️⃣
check.poll() — ручное ожидание. Как и для инвойсов, можно дождаться активации конкретного чека через check.poll(timeout, delay).
5️⃣
Проверяйте баланс. Перед созданием чека убедитесь, что на балансе достаточно средств. При истечении чека средства возвращаются автоматически.
💻

Практическая задача

Задача: Система розыгрыша призов через чеки

Напишите скрипт prize_system.py, который:

  1. Создаёт 5 чеков на разные суммы (10, 20, 30, 40, 50 USDT) с разными payload.
  2. Регистрирует обработчик @cp.check_activated(), который выводит какой приз выигран и кем.
  3. Регистрирует обработчик @cp.check_expired(), который логирует невостребованные призы.
  4. После активации всех чеков (или истечения) выводит статистику: сколько активировано, сколько истекло, общая сумма выданных призов.
  5. Использует кастомный PollingConfig с таймаутом 1 час и задержкой 3 секунды.

Ожидаемый вывод:

🎉 Приз 10 USDT активирован пользователем #12345!
🎉 Приз 30 USDT активирован пользователем #67890!
⏰ Приз 20 USDT истёк — не востребован
═══════════════════════════════════════
Статистика розыгрыша:
  Всего чеков: 5
  Активировано: 2
  Истекло: 3
  Выдано: 40 USDT
  Невостребовано: 60 USDT

Урок 6.2: Check Polling

8 вопросов