$ sudo teach IT
Модуль 5 · Transfers — переводы между пользователями

Урок 5.2 — Получение и управление переводами

Научимся получать историю переводов, фильтровать их, обрабатывать ошибки и строить практические инструменты для управления выплатами.

После того как мы научились отправлять переводы, возникает логичный вопрос: как увидеть историю переводов, найти конкретный перевод по spend_id, отфильтровать по активу или получить статистику выплат? В этом уроке мы разберём метод get_transfers(), его параметры фильтрации, обработку ошибок и построим полноценный лог переводов.

📋

Метод get_transfers() — история переводов

Метод get_transfers() возвращает список переводов с возможностью фильтрации. Это основной способ получить историю всех исходящих переводов твоего приложения.

Python · Сигнатура метода
async def get_transfers(
    self,
    asset: str | None = None,
    transfer_ids: list[int] | None = None,
    spend_id: str | None = None,
    offset: int | None = None,
    count: int | None = None,
) -> list[Transfer]

Все параметры опциональны. Если вызвать без аргументов — вернутся все переводы приложения.

Python · Таблица параметров get_transfers()
Параметр Тип Описание
asset str | None Фильтр по активу (например "USDT", "TON"). Вернутся только переводы в этом активе.
transfer_ids list[int] | None Фильтр по ID переводов. Передай список ID, и API вернёт только эти переводы.
spend_id str | None Фильтр по spend_id. Позволяет найти перевод по его ID идемпотентности.
offset int | None Смещение для пагинации. Сколько переводов пропустить с начала.
count int | None Количество переводов для возврата. Максимум — 1000.
Python · Различные варианты get_transfers()
from aiosend import CryptoPay

cp = CryptoPay(token="YOUR_TOKEN")

# 1. Все переводы без фильтрации
all_transfers = await cp.get_transfers()
print(f"Всего переводов: {len(all_transfers)}")

# 2. Только USDT-переводы
usdt_transfers = await cp.get_transfers(asset="USDT")
for t in usdt_transfers:
    print(f"#{t.transfer_id}: {t.amount} {t.asset} → user {t.user_id}")

# 3. Конкретные переводы по ID
specific = await cp.get_transfers(transfer_ids=[1, 2, 3])
print(f"Найдено: {len(specific)}")

# 4. Поиск по spend_id
transfer = await cp.get_transfers(spend_id="my-unique-spend-id-123")
# Вернёт список с одним элементом (или пустой)

# 5. С пагинацией
page = await cp.get_transfers(offset=0, count=50)
print(f"Первая страница: {len(page)} переводов")

🔍 Особенности фильтрации

В отличие от get_checks(), у get_transfers() нет фильтра по статусу (переводы всегда completed, если успешны). Зато есть фильтр по spend_id, что очень удобно для проверки статуса перевода по его ID идемпотентности. Если ты сохраняешь spend_id в своей БД, ты всегда можешь найти соответствующий перевод.

🔍

Поиск перевода по spend_id

Параметр spend_id в get_transfers() позволяет найти перевод по его ID идемпотентности. Это особенно полезно, когда:

  • Ты хочешь проверить, был ли выполнен перевод с определённым spend_id
  • Нужно найти перевод, связанный с заказом/транзакцией в твоей системе
  • Ты обрабатываешь вебхук и хочешь получить полную информацию о переводе
Python · Поиск перевода по spend_id
from aiosend import CryptoPay

async def find_transfer_by_spend_id(cp: CryptoPay, spend_id: str):
    """Находит перевод по spend_id."""
    transfers = await cp.get_transfers(spend_id=spend_id)

    if not transfers:
        print(f"Перевод с spend_id '{spend_id}' не найден")
        return None

    transfer = transfers[0]
    print(f"✅ Найден перевод #{transfer.transfer_id}")
    print(f"   Получатель: {transfer.user_id}")
    print(f"   Сумма: {transfer.amount} {transfer.asset}")
    print(f"   Статус: {transfer.status}")
    print(f"   Дата: {transfer.completed_at}")
    return transfer

async def check_transfer_status(cp: CryptoPay, order_id: str):
    """Проверяет статус перевода по ID заказа."""
    spend_id = f"refund_{order_id}"
    transfers = await cp.get_transfers(spend_id=spend_id)

    if transfers:
        t = transfers[0]
        print(f"Возврат по заказу {order_id}: #{t.transfer_id}, {t.status}")
        return t
    else:
        print(f"Возврат по заказу {order_id}: не найден")
        return None

💡 Зачем хранить spend_id?

Храни spend_id в своей базе данных вместе с ID заказа или транзакции. Это позволяет: проверить статус перевода, избежать двойных выплат (идемпотентность), найти перевод для аудита, восстановить информацию после сбоя. spend_id — это твой ключ к истории переводов.

📄

Пагинация истории переводов

Как и с чеками, для переводов работает пагинация через offset и count. Это необходимо, когда переводов много (сотни и тысячи).

Параметры Страница Результат
offset=0, count=100 Первая Переводы 0-99
offset=100, count=100 Вторая Переводы 100-199
offset=0, count=1000 Максимум Переводы 0-999 (лимит API)
Python · Получение всех переводов через пагинацию
from aiosend import CryptoPay

async def get_all_transfers(cp: CryptoPay) -> list:
    """Получает все переводы приложения через пагинацию."""
    all_transfers = []
    offset = 0
    page_size = 500

    while True:
        page = await cp.get_transfers(offset=offset, count=page_size)
        if not page:
            break
        all_transfers.extend(page)
        offset += len(page)
        print(f"Загружено {len(all_transfers)} переводов...")

    print(f"Всего загружено: {len(all_transfers)} переводов")
    return all_transfers

async def get_transfers_by_asset(cp: CryptoPay, asset: str) -> list:
    """Получает все переводы в указанном активе."""
    transfers = []
    offset = 0
    page_size = 500

    while True:
        page = await cp.get_transfers(
            asset=asset,
            offset=offset,
            count=page_size,
        )
        if not page:
            break
        transfers.extend(page)
        offset += len(page)

    print(f"Переводов в {asset}: {len(transfers)}")
    return transfers
🛡️

Обработка ошибок при работе с переводами

При работе с переводами могут возникать различные ошибки. Рассмотрим основные и способы их обработки.

Недостаточный баланс (Insufficient Balance)

Самая частая ошибка при отправке переводов — недостаточно средств на балансе приложения. Всегда проверяй баланс перед отправкой.

Python · Проверка баланса перед переводом
from aiosend import CryptoPay
from aiosend.exceptions import APIError

async def transfer_with_balance_check(
    cp: CryptoPay,
    user_id: int,
    asset: str,
    amount: float,
    spend_id: str,
):
    """Безопасный перевод с проверкой баланса."""
    # 1. Проверяем баланс
    balances = await cp.get_balances()
    balance_map = {b.code: float(b.available) for b in balances}

    if asset not in balance_map:
        print(f"❌ Актив {asset} не найден на балансе")
        return None

    available = balance_map[asset]
    if available < amount:
        print(f"❌ Недостаточно {asset}: есть {available}, нужно {amount}")
        return None

    # 2. Выполняем перевод
    try:
        transfer = await cp.transfer(
            user_id=user_id,
            asset=asset,
            amount=amount,
            spend_id=spend_id,
        )
        print(f"✅ Перевод #{transfer.transfer_id} выполнен")
        return transfer

    except APIError as e:
        print(f"❌ Ошибка API: {e}")
        # Анализируем ошибку
        if "insufficient" in str(e).lower():
            print("   Причина: недостаточно средств")
        elif "not found" in str(e).lower():
            print("   Причина: пользователь не в @CryptoBot")
        elif "rate limit" in str(e).lower():
            print("   Причина: слишком много запросов")
        return None

Rate Limiting

Crypto Pay API имеет ограничение на количество запросов в минуту. При превышении возвращается ошибка TooManyRequestsError.

Python · Обработка rate limiting
import asyncio
from aiosend.exceptions import TooManyRequestsError

async def transfer_with_retry(cp, user_id, asset, amount, spend_id, max_retries=3):
    """Перевод с повторными попытками при rate limit."""
    for attempt in range(max_retries):
        try:
            return await cp.transfer(
                user_id=user_id,
                asset=asset,
                amount=amount,
                spend_id=spend_id,
            )
        except TooManyRequestsError:
            if attempt < max_retries - 1:
                wait = 2 ** (attempt + 1)  # 2, 4, 8 секунд
                print(f"Rate limit, ждём {wait}с...")
                await asyncio.sleep(wait)
            else:
                print("❌ Превышен лимит запросов")
                raise
    return None

Пользователь не найден

Если пользователь никогда не запускал @CryptoBot, API вернёт ошибку. Это нужно учитывать при проектировании логики выплат.

Python · Полная обработка ошибок перевода
import uuid
from aiosend import CryptoPay
from aiosend.exceptions import (
    APIError,
    TooManyRequestsError,
    MethodValuesError,
)

class TransferError(Exception):
    """Базовое исключение для ошибок перевода."""

class InsufficientBalanceError(TransferError):
    """Недостаточно средств."""

class UserNotFoundError(TransferError):
    """Пользователь не найден в @CryptoBot."""

async def robust_transfer(cp: CryptoPay, user_id: int, asset: str, amount: float):
    """Надёжный перевод с полной обработкой ошибок."""
    try:
        return await cp.transfer(
            user_id=user_id,
            asset=asset,
            amount=amount,
            spend_id=str(uuid.uuid4()),
        )

    except MethodValuesError as e:
        raise TransferError(f"Неверные параметры: {e}") from e

    except TooManyRequestsError as e:
        raise TransferError("Слишком много запросов. Попробуйте позже.") from e

    except APIError as e:
        error_text = str(e).lower()
        if "insufficient" in error_text:
            raise InsufficientBalanceError(
                f"Недостаточно {asset} на балансе"
            ) from e
        elif "user" in error_text and "not found" in error_text:
            raise UserNotFoundError(
                f"Пользователь {user_id} не найден в @CryptoBot"
            ) from e
        else:
            raise TransferError(f"Ошибка API: {e}") from e
📊

Практический пример: лог переводов

Давай создадим полноценный инструмент для логирования и анализа переводов — TransferLogger.

Python · Класс для логирования переводов
from collections import Counter, defaultdict
from datetime import datetime
from aiosend import CryptoPay

class TransferLogger:
    """Логирование и анализ переводов."""

    def __init__(self, cp: CryptoPay):
        self.cp = cp

    async def show_all(self, asset: str | None = None):
        """Выводит все переводы в читаемом формате."""
        transfers = await self.cp.get_transfers(asset=asset)

        if not transfers:
            print("Нет переводов.")
            return

        print(f"{'ID':<8} {'Пользователь':<12} {'Сумма':<16} {'Актив':<8} {'Дата':<24} {'Комментарий'}")
        print("-" * 100)

        for t in transfers:
            comment = (t.comment[:30] + "..") if t.comment and len(t.comment) > 30 else (t.comment or "")
            print(f"{t.transfer_id:<8} {t.user_id:<12} {t.amount:<16} {t.asset:<8} {t.completed_at or '':<24} {comment}")

        print(f"\nВсего: {len(transfers)} переводов")

    async def stats(self):
        """Показывает статистику по переводам."""
        all_t = await self.cp.get_transfers()

        print("📊 Статистика переводов")
        print(f"   Всего переводов: {len(all_t)}")

        # По активам
        by_asset = Counter(t.asset for t in all_t)
        print("\n   По активам:")
        for asset, count in by_asset.most_common():
            total = sum(float(t.amount) for t in all_t if t.asset == asset)
            print(f"     {asset}: {count} переводов, сумма {total:.4f}")

        # По пользователям (топ-10)
        by_user = Counter(t.user_id for t in all_t)
        print("\n   Топ-10 получателей:")
        for user_id, count in by_user.most_common(10):
            total = sum(float(t.amount) for t in all_t if t.user_id == user_id)
            print(f"     user #{user_id}: {count} раз, всего {total:.4f}")

    async def export_csv(self, filename: str = "transfers.csv"):
        """Экспортирует переводы в CSV."""
        import csv

        transfers = await self.cp.get_transfers()
        with open(filename, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["ID", "User ID", "Asset", "Amount", "Status",
                           "Completed At", "Comment", "Spend ID"])
            for t in transfers:
                writer.writerow([
                    t.transfer_id, t.user_id, t.asset, t.amount,
                    t.status, t.completed_at, t.comment, t.spend_id,
                ])
        print(f"✅ Экспортировано {len(transfers)} переводов в {filename}")


async def main():
    cp = CryptoPay(token="YOUR_TOKEN")
    logger = TransferLogger(cp)

    await logger.show_all()
    await logger.stats()
    # await logger.export_csv("my_transfers.csv")


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

Мониторинг и оповещения

Используя get_transfers(), ты можешь построить систему мониторинга, которая отслеживает все переводы и оповещает о проблемах.

Python · Мониторинг переводов
from aiosend import CryptoPay

class TransferMonitor:
    """Мониторинг переводов с оповещениями."""

    def __init__(self, cp: CryptoPay):
        self.cp = cp
        self.last_id = 0

    async def check_new(self) -> list:
        """Проверяет новые переводы (последние 10)."""
        transfers = await self.cp.get_transfers(count=10)
        new_ones = [t for t in transfers if t.transfer_id > self.last_id]

        if new_ones:
            self.last_id = max(t.transfer_id for t in new_ones)

        return new_ones

    async def check_large_transfers(self, threshold: float = 1000):
        """Находит переводы больше указанной суммы."""
        transfers = await self.cp.get_transfers()
        large = [t for t in transfers if float(t.amount) >= threshold]

        if large:
            print(f"⚠️ Крупные переводы (>{threshold} USD):")
            for t in large:
                print(f"   #{t.transfer_id}: {t.amount} {t.asset} → {t.user_id}")
        return large

    async def summary_report(self) -> str:
        """Генерирует текстовый отчёт."""
        transfers = await self.cp.get_transfers()
        total = len(transfers)
        total_usdt = sum(float(t.amount) for t in transfers if t.asset == "USDT")
        unique_users = len(set(t.user_id for t in transfers))

        return (
            f"📋 Отчёт по переводам:\n"
            f"   Всего переводов: {total}\n"
            f"   Сумма USDT: {total_usdt:.2f}\n"
            f"   Уникальных получателей: {unique_users}\n"
            f"   Последний ID: {max((t.transfer_id for t in transfers), default=0)}"
        )


async def main():
    cp = CryptoPay(token="YOUR_TOKEN")
    monitor = TransferMonitor(cp)

    # Проверяем новые переводы
    new = await monitor.check_new()
    if new:
        print(f"Новых переводов: {len(new)}")

    # Проверяем крупные переводы
    await monitor.check_large_transfers(threshold=500)

    # Отчёт
    print(await monitor.summary_report())


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

💡 Как использовать TransferMonitor?

Ты можешь запускать check_new() периодически (например, раз в минуту через cron или asyncio-задачу) и отправлять оповещения в Telegram при появлении крупных переводов или ошибок. Это полноценная система мониторинга финансовых операций.

⚡

Массовые операции с переводами

Комбинируя get_transfers() с другими методами, можно выполнять массовые операции: проверять статус всех переводов, группировать, экспортировать.

Python · Массовые операции
from aiosend import CryptoPay

async def verify_transfers(cp: CryptoPay, spend_ids: list[str]) -> dict:
    """Проверяет статус нескольких переводов по spend_id."""
    results = {}
    for sid in spend_ids:
        transfers = await cp.get_transfers(spend_id=sid)
        results[sid] = transfers[0] if transfers else None
    return results

async def get_user_transfer_history(
    cp: CryptoPay,
    user_id: int,
) -> list:
    """Получает все переводы конкретного пользователя."""
    all_t = await cp.get_transfers()
    return [t for t in all_t if t.user_id == user_id]

async def total_spent_on_user(
    cp: CryptoPay,
    user_id: int,
    asset: str = "USDT",
) -> float:
    """Считает общую сумму выплат пользователю."""
    user_transfers = await get_user_transfer_history(cp, user_id)
    return sum(
        float(t.amount)
        for t in user_transfers
        if t.asset == asset
    )

async def main():
    cp = CryptoPay(token="YOUR_TOKEN")

    # Проверяем несколько переводов
    statuses = await verify_transfers(cp, ["spend_1", "spend_2", "spend_3"])
    for sid, transfer in statuses.items():
        if transfer:
            print(f"{sid}: #{transfer.transfer_id} — {transfer.status}")
        else:
            print(f"{sid}: не найден")

    # История пользователя
    history = await get_user_transfer_history(cp, user_id=123456789)
    print(f"Переводов пользователю: {len(history)}")

    # Общая сумма
    total = await total_spent_on_user(cp, user_id=123456789)
    print(f"Всего отправлено: {total:.2f} USDT")


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

Граничные случаи и частые ошибки

Пустой список при get_transfers()

Если переводов нет, API возвращает пустой список [], а не ошибку. Всегда проверяй длину списка перед итерацией, чтобы избежать логических ошибок. Это нормальное поведение — у нового приложения может не быть переводов.

spend_id не уникален в пределах приложения

Хотя spend_id должен быть уникальным, при повторном вызове с тем же spend_id get_transfers() вернёт список с одним элементом. Если spend_id не был указан при создании перевода, в поле spend_id будет None, и фильтрация по нему не сработает.

Лимит истории переводов

Crypto Pay API хранит историю переводов. Точный лимит не документирован, но предполагается, что хранятся все переводы за всё время. Используй пагинацию для получения больших объёмов данных. При достижении лимита старые переводы могут автоматически удаляться.

Transfer IDs не гарантируют последовательность

Не полагайся на то, что transfer_id всегда возрастают. Хотя обычно это так, API не гарантирует строгой последовательности. Используй поле completed_at для сортировки по времени.

📌

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

1️⃣
get_transfers() — получение истории с фильтрацией по asset, transfer_ids, spend_id, offset, count.
2️⃣
spend_id — мощный инструмент поиска. Храни его в своей БД для быстрого доступа к переводам.
3️⃣
Пагинация — offset и count, максимум 1000 за раз. Используй цикл для получения всех.
4️⃣
Обработка ошибок — проверяй баланс перед отправкой, обрабатывай rate limiting, проверяй существование пользователя.
5️⃣
Фильтры — asset, transfer_ids, spend_id. Нет фильтра по статусу (все переводы completed).
6️⃣
Мониторинг — используй TransferMonitor для отслеживания новых и крупных переводов.
7️⃣
Аналитика — TransferLogger считает статистику по активам, пользователям и суммам. Экспорт в CSV.
🎯

Практическое задание

Задание: Создай AuditTransfer система аудита

Напиши класс TransferAudit, который предоставляет возможности аудита переводов. Класс должен:

  • Иметь метод find_by_spend_id(spend_id) — поиск по spend_id с полной информацией
  • Иметь метод find_by_user(user_id) — все переводы конкретному пользователю
  • Иметь метод daily_report(date) — отчёт за конкретный день (суммы, количество)
  • Иметь метод verify_transfer(transfer_id, expected_amount, expected_user) — верификация перевода
  • Иметь метод export_to_csv(filename) — экспорт всей истории
  • Каждый метод должен обрабатывать ошибки и логировать действия

Подсказка:

Python · Шаблон решения
import csv
from datetime import datetime
from aiosend import CryptoPay
from aiosend.exceptions import APIError

class TransferAudit:
    def __init__(self, cp: CryptoPay):
        self.cp = cp

    async def find_by_spend_id(self, spend_id: str):
        transfers = await self.cp.get_transfers(spend_id=spend_id)
        if not transfers:
            print(f"Spend ID {spend_id} не найден")
            return None
        return transfers[0]

    async def find_by_user(self, user_id: int) -> list:
        all_t = await self.cp.get_transfers()
        return [t for t in all_t if t.user_id == user_id]

    async def daily_report(self, date_str: str):
        """date_str: '2024-01-15'"""
        transfers = await self.cp.get_transfers()
        day_transfers = [
            t for t in transfers
            if t.completed_at and t.completed_at.startswith(date_str)
        ]
        total = sum(float(t.amount) for t in day_transfers)
        print(f"📅 {date_str}: {len(day_transfers)} переводов, сумма {total:.4f}")
        return day_transfers

    async def verify_transfer(self, transfer_id: int,
                              expected_amount: float,
                              expected_user: int) -> bool:
        try:
            transfers = await self.cp.get_transfers(
                transfer_ids=[transfer_id]
            )
            if not transfers:
                print(f"❌ Перевод #{transfer_id} не найден")
                return False
            t = transfers[0]
            ok = (float(t.amount) == expected_amount and
                  t.user_id == expected_user)
            print(f"{'✅' if ok else '❌'} Перевод #{transfer_id}: "
                  f"{'совпадает' if ok else 'НЕ совпадает'}")
            return ok
        except APIError as e:
            print(f"❌ Ошибка: {e}")
            return False

    async def export_to_csv(self, filename: str):
        transfers = await self.cp.get_transfers()
        with open(filename, "w", newline="") as f:
            w = csv.writer(f)
            w.writerow(["ID", "User", "Asset", "Amount", "Status",
                        "Completed", "Comment", "SpendID"])
            for t in transfers:
                w.writerow([t.transfer_id, t.user_id, t.asset,
                           t.amount, t.status, t.completed_at,
                           t.comment, t.spend_id])
        print(f"✅ Экспортировано: {filename}")


async def main():
    cp = CryptoPay(token="YOUR_TOKEN")
    audit = TransferAudit(cp)

    t = await audit.find_by_spend_id("refund_ORD-123")
    if t:
        print(f"Найден: #{t.transfer_id}, {t.amount} {t.asset}")

    await audit.daily_report("2024-06-01")
    await audit.verify_transfer(42, 10.0, 123456789)
    # await audit.export_to_csv("audit.csv")


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

Урок 5.2: Получение и управление переводами

10 вопросов