Кейс · TON / crypto-инфраструктура · 2026
Два отдельных сервиса — тестовый и боевой — генерируют TON-кошельки: 24-словная мнемоника, ed25519-пара, адрес V5R1. Сид-фраза попадает в ответ ровно один раз за всю жизнь кошелька и никогда больше не появляется — ни в API, ни в панели, ни в базе.
Внутренним сервисам нужны настоящие TON-кошельки для тестов — без похода в реальный кошелёк и без риска смешать тестовые ключи с боевыми. А там, где на другом конце уже реальные деньги, нужен кошелёк с той же логикой, но без права на ошибку: скомпрометированная мнемоника — это не баг, а прямой убыток.
Решение — два физически разных сервиса на соседних доменах с одинаковой архитектурой, но без единого разделяемого секрета: ton.ifreework.com для тестов и mainnet.ifreework.com для боевых кошельков. Компрометация одного не открывает доступ ко второму.
Генерация никогда не происходит внутри веб-запроса — только через очередь, так, чтобы медленный внешний вызов не мог занять воркер и положить сайт.
Каждый вызов несёт HMAC-SHA256 подпись по канонической строке (метод, путь, время, nonce, хэш тела) и обязательный Idempotency-Key — повтор того же ключа возвращает уже созданный кошелёк, а не плодит второй.
POST отвечает мгновенно, ничего не ожидая: кошелька ещё нет, есть только id со статусом pending и адрес для опроса.
Отдельный воркер очереди вызывает Node-сервис с TON SDK — вся криптография изолирована туда, потому что TON SDK написан на JavaScript, а не на PHP. Сайдкар слушает только внутреннюю docker-сеть и требует общий токен.
Node 22 · @ton/crypto · @ton/tonКлиент опрашивает тот же id, пока статус не станет ready или failed — обычно меньше секунды. Кошелёк из чужого запроса на этот id недоступен: чужой id возвращает такой же 404, как и несуществующий.
Мнемоника есть только в первом успешном ответе на опрос. Следующий запрос — хоть через секунду — вернёт тот же кошелёк с mnemonic: null. Не забрали за 15 минут — фраза стирается из временного кэша безвозвратно, адрес остаётся, ключ — нет.
Боевой сервис — не режим тестового, а полный клон с перепрошитой сетью: свой контейнер, своя база, свой сайдкар, свой домен. Ничего не переиспользуется.
-3, зашит в сайдкаре намертво0Q… / kQ…-239, зашит в сайдкаре намертвоUQ… / EQ…Клиент на каждой стороне ещё и сам себя перепроверяет: если ответ вдруг содержит адрес не того формата — UQ… там, где должно быть только 0Q…, — сервис отказывается его принять, а не молча продолжает работу с смешанными сетями.
Настоящая причина — синхронный вызов Node-сайдкара внутри php-fpm-воркера при пуле в 5 воркеров: один медленный запрос вместе со сканер-ботом, долбившим сайт в это же время, забрал все воркеры разом — и лёг весь сайт, включая создание кошельков.
Вскоре после первого деплоя создание кошельков начало падать. Логи молчали — но не потому, что всё было в порядке: директива access_log $ton_log_path … в конфиге nginx проверяет на буквальный текст директивы, а не на то, во что раскрывается переменная, поэтому nginx подставлял перед каждым путём /etc/nginx/ и не мог никуда писать. Логирование молча ломалось с момента деплоя и маскировало настоящую проблему.
Настоящей проблемой был синхронный вызов сайдкара внутри веб-запроса при дефолтном pm.max_children: 5. Исправление — не просто увеличить пул, а убрать генерацию из пути веб-запроса: она переехала в отдельный процесс очереди, который нельзя истощить веб-трафиком, независимо от размера пула php-fpm.
X-Forwarded-*; приложение не доверяет прокси вслепую.created_by./api/* для публичного домена, nginx повторяет тот же отказ независимо, и приложение проверяет это третий раз само.Внутренние сервисы получают одноразовые тестовые кошельки без риска зацепить боевые ключи, а там, где на кону настоящие деньги, — тот же проверенный механизм на полностью изолированном сервисе: своя сеть, своя база, свои секреты. Сид-фраза существует в открытом виде ровно один ответ одного запроса — до и после него её просто нет.
Laravel 13 · PHP 8.4 · Node 22 · Redis · MySQL · TON SDK · HMAC-SHA256