Кейс · P2P-биржа и крипто-инфраструктура · 2026

QTest P2P

P2P-биржа, где пользователи обменивают USDT, TON и Jetton-токены на рубли напрямую друг с другом, а платформа держит крипто-часть в собственном кастоди, сверяет активы с обязательствами каждые полчаса и не отпускает вывод, пока ledger не сойдётся до копейки.

23 377строк PHP-кода
64контроллера
38сервисов
45миграций
18artisan-команд
34теста

Задача

P2P-обмен — это всегда доверие между двумя людьми, которые никогда не встретятся, плюс крипто-часть, которую нельзя ни потерять, ни задвоить. Пользователь заводит USDT на TRC-20, TON или Jetton, выставляет объявление, договаривается со встречным в чате и ждёт, что после подтверждения оплаты платформа отпустит крипту — без ошибки на копейку и без второго списания при повторном запросе.

При этом сама биржа не хранит деньги в одном кошельке «на всякий случай»: TON и Jetton приходят и уходят через отдельный кастодиальный микросервис GRAM, а сайт должен доверять его событиям ровно настолько, насколько может их проверить — подписью, идемпотентностью и регулярной сверкой остатков.

Путь одной P2P-сделки

  1. Объявление и встречное предложение

    Продавец публикует ордер по своему курсу — фиксированному или плавающему от биржевого; покупатель откликается, и для пары создаётся сделка с зафиксированной на момент отклика ценой.

    Market · курсы онлайн
  2. Резерв и чат сделки

    Крипто-часть продавца замораживается на балансе сразу, чтобы её нельзя было увести в другую сделку. Стороны договариваются в чате в реальном времени, прикладывают чек оплаты.

    Deal · WebSocket через Reverb
  3. Тайминги, которые не дают сделке зависнуть

    Неоплаченная сделка автоматически отменяется по таймауту, неподтверждённая после оплаты — эскалируется, а фоновые джобы следят за этим независимо от того, открыт ли у сторон браузер.

    CancelUnpaidDeal / CancelUnconfirmedDeal / AutoRejectDeal
  4. Спор

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

    AdminDisputeController · причины из справочника
  5. Разморозка и зачисление

    Подтверждение оплаты снимает заморозку и переводит крипто-часть покупателю одной атомарной операцией — промежуточного состояния, в котором деньги «нигде», не существует.

    BalanceService · транзакционно

Три актива, три разных пути в кошелёк

USDT на TRON, TON и Jetton-токены заходят на платформу не одинаково. У TRON-переводов своя логика аренды энергии и мониторинга адресов, а TON и Jetton вообще не трогают блокчейн напрямую с основного сайта — за это отвечает микросервис GRAM, который сайт вызывает через подписанный HMAC-API.

USDT · TRC-20

Собственный TRON-кошелёк на mnemonic-ключах, мониторинг входящих переводов и аренда энергии под исходящий TRC-20 перевод, чтобы не платить полную комиссию в TRX за каждый вывод.

TON

Депозиты приходят подтверждёнными событиями от GRAM в crypto_deposits, отдельно от legacy-баланса USDT. Минимальная сумма и сеть (testnet/mainnet) проверяются на приёме, а не постфактум.

Jetton

Только allowlisted токены, отдельный endpoint приёма и неизменяемый jetton_ledger_entries. Вывод резервирует сумму и комиссию сразу, а списывает — только после подтверждения из GRAM.

Что держит систему на ногах

  • Повторная доставка не задваивает баланс — каждое событие от GRAM несёт Idempotency-Key и external_id; повторный вызов возвращает тот же результат с credited=false, а не создаёт вторую операцию.
  • Активы и обязательства сверяются самиgram:custody:audit каждые 30 минут сравнивает управляемые TON-кошельки GRAM с суммой обязательств перед пользователями и держит допуск в 0.1 TON на сетевые комиссии — расхождение сверх допуска уходит администратору алертом.
  • API GRAM закрыт со всех сторон — HMAC-подпись, метка времени, одноразовый nonce, allowlist по IP и обязательный HTTPS — вместе, а не по отдельности. Клиент для приёма депозитов не совпадает с клиентом, которым сайт сам вызывает GRAM.
  • Testnet и mainnet физически не пересекаются — сеть входящего события сверяется с настроенной сетью на приёме; отдельная команда безопасно обнуляет testnet-учёт, не трогая ни одной реальной операции.
  • Вывод крипты — под ручным контролем, а не по кнопке — заявки на вывод USDT и Jetton уходят на проверку администратору; для Jetton резерв снимается атомарно и освобождается обратно, если заявка отклонена или не прошла.
  • Вход в аккаунт и в вывод — разные пороги — Google 2FA на входе в чувствительные действия и отдельный PIN на подтверждение вывода: компрометация пароля сама по себе не даёт доступа к выводу средств.

Архитектура

Технологии
Бэкенд — Laravel 12, MySQL, Redis, очереди
Фронтенд — Inertia.js, Vue 3, Tailwind
Реалтайм — Laravel Reverb (WebSocket)
TRON — собственный TRC-20 USDT кошелёк, аренда энергии
TON / Jetton — кастодиальный микросервис GRAM, HMAC-API
Безопасность и интеграции
Безопасность — Google 2FA, PIN на вывод, IP allowlist, идемпотентность
Интеграции — Telegram-бот и mini app, Web Push, PDF-восстановление доступа
Инфраструктура — Docker: nginx, app, queue, scheduler, reverb, db, redis

Результат

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

← Об этой платформе на странице услуг

Laravel 12 · MySQL · Redis · Laravel Reverb · Vue 3 · Inertia.js · TRC-20 · TON · Jetton · GRAM


Сделать заказ

| необходим для связи с вами
В кротчайшие сроки я свяжусь с вами.

Также вы можетете связать со мной:
telegram: @ifwcom