Кошелёк на один показ

Кейс · TON / crypto-инфраструктура · 2026

Два отдельных сервиса — тестовый и боевой — генерируют TON-кошельки: 24-словная мнемоника, ed25519-пара, адрес V5R1. Сид-фраза попадает в ответ ровно один раз за всю жизнь кошелька и никогда больше не появляется — ни в API, ни в панели, ни в базе.

2 сетиtestnet и mainnet
24 словамнемоника кошелька
1 показсид-фраза только в первом ответе
0 секретовобщих между сервисами

Коротко: мнемонику можно забрать в течение 15 минут, но увидеть — только один раз. Testnet и mainnet работают как два изолированных сервиса, поэтому компрометация одного не открывает доступ ко второму.

Задача

Внутренним сервисам нужны настоящие TON-кошельки для тестов — без похода в реальный кошелёк и без риска смешать тестовые ключи с боевыми. А там, где на другом конце уже реальные деньги, нужен кошелёк с той же логикой, но без права на ошибку: скомпрометированная мнемоника — это не баг, а прямой убыток.

Решение — два физически разных сервиса на соседних доменах с одинаковой архитектурой, но без единого разделяемого секрета: ton.ifreework.com для тестов и mainnet.ifreework.com для боевых кошельков. Компрометация одного не открывает доступ ко второму.

Путь одного кошелька

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

  1. Подписанный запрос

    Каждый вызов несёт HMAC-SHA256 подпись по канонической строке (метод, путь, время, nonce, хэш тела) и обязательный Idempotency-Key — повтор того же ключа возвращает уже созданный кошелёк, а не плодит второй.

    X-Signature · X-Nonce · X-Timestamp
  2. Постановка в очередь

    POST отвечает мгновенно, ничего не ожидая: кошелька ещё нет, есть только id со статусом pending и адрес для опроса.

    202 Accepted · queue: ton-wallets
  3. Генерация в Node-сайдкаре

    Отдельный воркер очереди вызывает Node-сервис с TON SDK — вся криптография изолирована туда, потому что TON SDK написан на JavaScript, а не на PHP. Сайдкар слушает только внутреннюю docker-сеть и требует общий токен.

    Node 22 · @ton/crypto · @ton/ton
  4. Опрос статуса

    Клиент опрашивает тот же id, пока статус не станет ready или failed — обычно меньше секунды. Кошелёк из чужого запроса на этот id недоступен: чужой id возвращает такой же 404, как и несуществующий.

    GET /api/.../{id}
  5. Раскрытие — и всё

    Мнемоника есть только в первом успешном ответе на опрос. Следующий запрос — хоть через секунду — вернёт тот же кошелёк с mnemonic: null. Не забрали за 15 минут — фраза стирается из временного кэша безвозвратно, адрес остаётся, ключ — нет.

    RevealCache · TON_REVEAL_TTL

Testnet и mainnet — близнецы без общих секретов

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

ton.ifreework.com — testnet

  • Глобальный id сети -3, зашит в сайдкаре намертво
  • Адреса начинаются с 0Q… / kQ…
  • Хранение мнемоники: по умолчанию не хранится, опционально — зашифровано
  • Очистка просроченных записей — раз в 5 минут, хранение до года

mainnet.ifreework.com — боевой

  • Глобальный id сети -239, зашит в сайдкаре намертво
  • Адреса начинаются с UQ… / EQ…
  • Хранение мнемоники запрещено на уровне конфигурации — не просто выключено по умолчанию
  • Очистка просроченных записей — раз в минуту, хранение 180 дней

Клиент на каждой стороне ещё и сам себя перепроверяет: если ответ вдруг содержит адрес не того формата — UQ… там, где должно быть только 0Q…, — сервис отказывается его принять, а не молча продолжает работу с смешанными сетями.

Инцидент: как сайт клали пять воркеров

Настоящая причина — синхронный вызов Node-сайдкара внутри php-fpm-воркера при пуле в 5 воркеров: один медленный запрос вместе со сканер-ботом, долбившим сайт в это же время, забрал все воркеры разом — и лёг весь сайт, включая создание кошельков.

Вскоре после первого деплоя создание кошельков начало падать. Логи молчали — но не потому, что всё было в порядке: директива access_log $ton_log_path … в конфиге nginx проверяет на буквальный текст директивы, а не на то, во что раскрывается переменная, поэтому nginx подставлял перед каждым путём /etc/nginx/ и не мог никуда писать. Логирование молча ломалось с момента деплоя и маскировало настоящую проблему.

Настоящей проблемой был синхронный вызов сайдкара внутри веб-запроса при дефолтном pm.max_children: 5. Исправление — не просто увеличить пул, а убрать генерацию из пути веб-запроса: она переехала в отдельный процесс очереди, который нельзя истощить веб-трафиком, независимо от размера пула php-fpm.

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

Сид-фразы нет в логах

Объект секрета ничего не хранит в своих полях, отдельный обработчик вычищает канал, а API-эндпоинт не логирует тело запроса.

Нет опасной истории запросов

Telescope и Pulse на этих сервисах не установлены в принципе, а не просто выключены в конфигурации.

Чужой ID не перебрать

ID проверяется после HMAC- или сессионной аутентификации, поэтому неавторизованный ответ не раскрывает существование кошелька.

IP нельзя подделать

Nginx восстанавливает реальный адрес через Cloudflare и docker-хопы и обнуляет входящие X-Forwarded-*.

Именной доступ

Учётки создаются вручную одной командой, общего пароля нет, у каждого кошелька фиксируется created_by.

Публичного API нет

Caddy, nginx и само приложение независимо возвращают 404 для внешних запросов к /api/*.

Архитектура

Технологии

  • Backend — Laravel 13, PHP 8.4
  • Очередь — Redis, отдельный queue:work-процесс
  • Сайдкар — Node 22, @ton/crypto, @ton/ton
  • Хранилище — MySQL, метаданные кошельков и аудит

Безопасность и эксплуатация

  • Аутентификация — HMAC-SHA256, nonce, окно времени, идемпотентность
  • Сеть — приватный канал через 127.0.0.1 / VPN
  • Панель — именной логин, QR-код, экспорт CSV
  • Обслуживание — плановая очистка просроченных записей

Результат

Внутренние сервисы получают одноразовые тестовые кошельки без риска зацепить боевые ключи, а там, где на кону настоящие деньги, — тот же проверенный механизм на полностью изолированном сервисе: своя сеть, своя база, свои секреты. Сид-фраза существует в открытом виде ровно один ответ одного запроса — до и после него её просто нет.

Подробнее об услуге создания и привязки TON-кошельков →

Laravel 13 · PHP 8.4 · Node 22 · Redis · MySQL · TON SDK · HMAC-SHA256