Troubleshooting TON
База знаний типичных ошибок при разработке в экосистеме TON -- от компиляции Tact/Tolk до настройки Blueprint, работы с TON Connect и развёртывания смарт-контрактов. Используйте фильтры или Cmd+K для поиска.
Back to courseФильтр по модулю
Фильтр по категории
Symptoms
- Контракт деплоится, но при первом вызове возвращает exit code 128
- В Sandbox тест падает сразу после деплоя при отправке первого сообщения
- Транзакция на testnet завершается с exit code 128 в compute phase
Cause
Контракт не был корректно инициализирован. В Tact каждый контракт должен иметь функцию `init()`, которая задаёт начальное состояние. Если `init()` отсутствует или StateInit не передан при деплое, контракт останется неинициализированным.
Solution
- Убедитесь, что контракт имеет функцию
init():contract MyContract { init() { ... } } - При деплое обязательно передавайте StateInit:
contractProvider.internal(via, { value: toNano('0.05'), body: beginCell().endCell() }) - В Blueprint убедитесь, что wrapper корректно формирует StateInit через
MyContract.fromInit(...)перед деплоем
Related lessons:
Symptoms
- Контракт отклоняет входящее сообщение с exit code 129
- Сообщение отправлено, но контракт не выполняет ожидаемый receiver
- В логах Sandbox видно, что opcode сообщения не совпадает с ожидаемым
Cause
Контракт получил сообщение с opcode, для которого нет подходящего receiver. Это может быть вызвано несовпадением имени сообщения между отправителем и получателем, либо коллизией opcode при использовании пользовательских типов сообщений.
Solution
- Проверьте, что имя сообщения в
send()точно совпадает сreceive()в контракте:receive(msg: MyMessage)ожидаетMyMessage{...} - Убедитесь, что оба контракта (отправитель и получатель) используют одну и ту же версию .tact файла с определением сообщения
- Проверьте opcode в сгенерированном коде:
npx blueprint buildи осмотрите ABI файл на предмет коллизий
Related lessons:
Symptoms
- Сообщение доставлено, но контракт отвергает его с exit code 130
- Отправка на контракт работает из одного кошелька, но не из другого
- В Sandbox тест проходит, но на testnet сообщение отклоняется
Cause
Контракт ожидает bounceable-сообщение, но получил non-bounceable (или наоборот). Адреса с префиксом EQ (bounceable) и UQ (non-bounceable) определяют поведение bounce-флага. Некоторые контракты строго проверяют этот флаг.
Solution
- Используйте правильный формат адреса: EQ-адрес для контрактов (bounceable), UQ-адрес для кошельков (non-bounceable)
- При отправке из кода явно укажите bounce-флаг:
send(SendParameters{ bounce: true, to: contractAddress, ... }) - В тестах Sandbox проверьте формат адреса:
address.toString({ bounceable: true })для контрактов
Related lessons:
Symptoms
- Вызов функции контракта возвращает exit code 132
- Только определённые операции завершаются с ошибкой, остальные работают
- Ошибка возникает при попытке административных действий (withdraw, transfer ownership)
Cause
Контракт проверяет права доступа через `require(sender() == self.owner, "Access denied")` или `self.requireOwner()`. Вызывающий адрес не совпадает с owner-адресом, записанным в состоянии контракта при инициализации.
Solution
- Убедитесь, что отправляете транзакцию с того же адреса, который указан как owner при деплое контракта
- Проверьте owner через get-метод:
const owner = await contract.getOwner() - В тестах Sandbox используйте правильный wallet:
const owner = await blockchain.treasury('owner')и отправляйте от его имени
Related lessons:
Symptoms
- Компиляция Tact контракта завершается с ошибкой несовпадения типов
- Поля структуры принимаются как Int, но ожидается сериализованный тип
- Ошибка появляется при использовании struct в message body
Cause
В Tact поля структур и сообщений требуют явных аннотаций сериализации для определения формата хранения в Cell. Без аннотации компилятор использует полный 257-bit Int, что может конфликтовать с ожиданиями на стороне получателя.
Solution
- Добавьте аннотацию сериализации к полям:
amount: Int as coinsвместоamount: Int - Для целых чисел используйте конкретные размеры:
id: Int as uint32,timestamp: Int as uint64 - Проверьте, что структура на стороне отправителя и получателя имеет идентичные аннотации
- Список доступных аннотаций:
uint8,uint16,uint32,uint64,uint128,uint256,int8...int257,coins
Related lessons:
Symptoms
- Компилятор Tact не находит функцию send()
- Код скопирован из примера на FunC, но не компилируется в Tact
- Ошибка при попытке отправить сообщение из контракта
Cause
В Tact нет глобальной функции `send()` в стиле FunC. Для отправки сообщений используются методы контракта: `self.reply()`, `self.forward()`, `self.notify()` или глобальная `send(SendParameters{...})` с явной структурой параметров.
Solution
- Для ответа отправителю используйте:
self.reply("Response".asComment()) - Для пересылки другому контракту:
send(SendParameters{ to: address, value: ton("0.05"), body: MyMessage{...}.toCell() }) - Для уведомления (без body):
self.notify("Done".asComment()) - Не копируйте FunC-паттерны напрямую -- Tact имеет собственный API для отправки сообщений
Related lessons:
Symptoms
- Blueprint не может задеплоить контракт -- ошибка при сборке wrapper
- Контракт компилируется, но деплой через Blueprint падает
- В wrapper-файле вызывается Deploy, но контракт его не принимает
Cause
Blueprint по умолчанию отправляет сообщение Deploy при деплое контракта. Если контракт не импортирует trait Deployable или не имеет receiver для сообщения Deploy, деплой завершится ошибкой.
Solution
- Добавьте trait Deployable в контракт:
contract MyContract with Deployable { ... } - Или добавьте явный receiver:
receive(msg: Deploy) { self.notify("deployed".asComment()) } - Не забудьте импорт:
import "@stdlib/deploy"
Related lessons:
Symptoms
- Арифметическая операция в контракте завершается с exit code 4
- Умножение или сложение больших чисел приводит к краху контракта
- Ошибка появляется при работе с балансами в nanoTON (10^9 порядки)
Cause
TVM использует 257-битные знаковые целые числа. Exit code 4 возникает при переполнении диапазона (-2^256 до 2^256-1) или при делении на ноль. В контексте Tact это часто происходит при перемножении больших значений (например, price * amount) без промежуточной проверки.
Solution
- Добавьте проверки перед арифметикой:
require(a <= MAX_SAFE_VALUE, "Value too large") - Для финансовых расчётов используйте nanoTON (Int) и проверяйте границы:
require(amount > 0 && amount <= ton("1000000"), "Invalid amount") - Избегайте деления на ноль:
require(divisor != 0, "Division by zero") - Тестируйте граничные значения в Sandbox:
toNano('1000000000')для проверки переполнения
Related lessons:
Symptoms
- Команда npx blueprint build падает с ошибкой Cannot find module
- Проект создан через npm create ton@latest, но зависимости не найдены
- Node.js запускается, но пакеты TON не резолвятся
Cause
Зависимости проекта не установлены или используется устаревшая версия Node.js (ниже 18). Blueprint требует Node.js >= 18 и корректной установки всех зависимостей через npm install.
Solution
- Проверьте версию Node.js:
node -v(должна быть >= 18) - Установите зависимости:
npm installв корне проекта - Если проблема сохраняется, удалите node_modules и package-lock.json:
rm -rf node_modules package-lock.json && npm install
Related lessons:
Symptoms
- Jest тест завершается по таймауту при работе с контрактом
- Тест с цепочкой сообщений между контрактами не успевает завершиться
- Первый тест проходит, но сложные сценарии падают по timeout
Cause
По умолчанию Jest даёт 5 секунд на тест. Сложные сценарии с несколькими контрактами, цепочками сообщений или большими данными могут превысить этот лимит в Sandbox.
Solution
- Увеличьте таймаут для конкретного теста:
it('complex test', async () => { ... }, 30000) - Или глобально в jest.config.ts:
testTimeout: 30000 - Оптимизируйте тест: деплойте контракты в
beforeAll(), а не в каждомit()
Related lessons:
Symptoms
- Тест проходил раньше, но после изменения контракта начал падать
- Hash кода контракта в wrapper не совпадает с скомпилированным
- Sandbox создаёт контракт со старой версией кода
Cause
После изменения .tact файла необходимо перекомпилировать контракт. Wrapper использует скомпилированный code cell (hex), который кешируется после `npx blueprint build`. Без пересборки wrapper ссылается на устаревший код.
Solution
- Перекомпилируйте контракт:
npx blueprint build - Убедитесь, что wrapper обновлён: проверьте файл
build/MyContract/tact_MyContract.code.boc - В CI/CD добавьте build перед тестами:
npx blueprint build && npx jest
Related lessons:
Symptoms
- Dev-сервер не запускается -- порт уже занят
- После некорректного завершения предыдущего процесса порт заблокирован
- Несколько проектов конфликтуют по порту
Cause
Другой процесс уже слушает порт 3000. Это может быть незавершённый dev-сервер, другой проект или системный процесс.
Solution
- Найдите процесс на порту:
lsof -i :3000(macOS/Linux) илиnetstat -ano | findstr :3000(Windows) - Завершите процесс:
kill -9 - Или запустите на другом порту: измените
PORT=3001в конфигурации или используйте--port 3001
Related lessons:
Symptoms
- Команда npx jest падает с ошибкой preset not found
- Проект Blueprint создан, но тесты не запускаются
- TypeScript тесты не компилируются через Jest
Cause
Пакет ts-jest не установлен в проекте. Blueprint-проекты используют TypeScript для тестов, и ts-jest необходим как preset для Jest для компиляции .ts файлов.
Solution
- Установите ts-jest:
npm install --save-dev ts-jest - Убедитесь, что jest.config.ts содержит:
preset: 'ts-jest' - Проверьте совместимость версий: ts-jest должен соответствовать версии Jest (обычно jest@29 + ts-jest@29)
Related lessons:
Symptoms
- npx blueprint build не может найти компилятор Tact
- Ошибка после обновления зависимостей проекта
- Новый проект компилировался, но после npm update перестал
Cause
Пакет @tact-lang/compiler не установлен или установлена несовместимая версия. Blueprint ищет компилятор Tact в node_modules, и при отсутствии пакета или конфликте версий сборка невозможна.
Solution
- Установите компилятор:
npm install @tact-lang/compiler - Проверьте версию в package.json:
"@tact-lang/compiler": "^1.x.x" - При конфликте версий:
npm ls @tact-lang/compilerдля диагностики, затемnpm dedupe
Related lessons:
Symptoms
- Транзакция завершается с exit code 13 в compute phase
- Контракт не выполняет все операции, несмотря на корректный код
- В Sandbox тест проходит, но на testnet транзакция падает
Cause
Контракту не хватает газа для завершения вычислений. В Sandbox лимиты газа могут отличаться от реальной сети. Каждая операция TVM потребляет газ: хранение данных, отправка сообщений, итерации по map -- всё это суммируется.
Solution
- Проверьте msg_value входящего сообщения: для простых операций достаточно 0.05 TON, для сложных (с пересылкой) -- 0.1-0.5 TON
- В Sandbox включите логирование газа:
blockchain.verbosity = { vmLogs: 'vm_logs_full' } - Оптимизируйте контракт: избегайте циклов по всему map, используйте пагинацию для больших коллекций
- Убедитесь, что отправляете достаточно TON с сообщением:
send(SendParameters{ value: ton("0.1"), ... })
Related lessons:
Symptoms
- Попытка записать данные в Cell завершается с exit code 8
- Контракт падает при сохранении большого объёма данных в storage
- Сериализация структуры с множеством полей вызывает ошибку
Cause
Одна Cell в TON может хранить максимум 1023 бита данных и 4 ссылки на другие Cell. При попытке записать больше данных или добавить более 4 ссылок TVM выбрасывает exit code 8.
Solution
- Разделите данные на несколько Cell: используйте ссылки (refs) для больших структур
- Проверьте размер данных:
builder.bitsне должен превышать 1023,builder.refsне более 4 - В Tact используйте map для коллекций данных вместо хранения всего в одной Cell
- Для строк используйте
Stringтип в Tact -- он автоматически разбивается на snake-cells
Related lessons:
Symptoms
- Чтение данных из Cell завершается с exit code 9
- Get-метод контракта падает при попытке десериализации
- Контракт работал, но после обновления формата данных начал падать
Cause
Попытка прочитать больше бит или ссылок, чем содержится в Slice. Это часто происходит при изменении формата хранения контракта без миграции данных -- новый код пытается читать поля, которых нет в старом формате.
Solution
- Проверьте порядок чтения полей: он должен точно соответствовать порядку записи
- При обновлении контракта убедитесь, что формат данных совместим или реализуйте миграцию
- Используйте
slice.remainingBitsиslice.remainingRefsдля проверки перед чтением
Related lessons:
Symptoms
- Сообщение отправлено, но вернулось обратно (bounced) без обработки
- Средства вернулись за вычетом комиссии, но целевое действие не выполнено
- В explorer транзакция отмечена как bounced
Cause
Контракт-получатель отклонил сообщение (вернул ненулевой exit code), и так как сообщение было отправлено с bounce: true, оно было возвращено отправителю. Если отправитель не обрабатывает bounced-сообщения, средства могут быть потеряны.
Solution
- Реализуйте обработку bounced-сообщений:
bounced(msg: bounced) { ... } - Проверьте причину отклонения на стороне получателя через explorer (tonscan.org или tonviewer.com)
- Для некритичных уведомлений используйте
bounce: falseв SendParameters - Учитывайте, что bounced-сообщение содержит только первые 224 бита тела оригинального сообщения
Related lessons:
Symptoms
- TVM останавливается с exit code 11 при выполнении контракта
- Контракт задеплоен, но любой вызов завершается этой ошибкой
- Ошибка появляется после деплоя с неправильным code cell
Cause
TVM встретил невалидную инструкцию. Это обычно означает повреждённый code cell, деплой с неправильной версией скомпилированного кода или попытку выполнить данные как код.
Solution
- Перекомпилируйте контракт:
npx blueprint buildи убедитесь, что используете свежий .boc файл - Проверьте, что code cell в StateInit соответствует ожидаемому контракту
- Убедитесь, что версия компилятора совместима с версией TVM на целевой сети
Related lessons:
Symptoms
- Пользователь начинает подписание транзакции, но приложение получает ошибку
- После открытия кошелька и нажатия 'Отмена' приложение падает
- Необработанное исключение при закрытии модального окна кошелька
Cause
Пользователь отклонил транзакцию в кошельке (нажал 'Cancel' или закрыл приложение). TON Connect SDK выбрасывает UserRejectsError, которое необходимо обработать в UI.
Solution
- Оберните вызов sendTransaction в try/catch:
try { await tonConnectUI.sendTransaction(tx) } catch (e) { if (e instanceof UserRejectsError) { /* показать сообщение */ } } - Покажите пользователю дружелюбное сообщение: 'Транзакция отменена' вместо стектрейса
- Не блокируйте UI при ожидании подписания -- добавьте кнопку 'Отмена' в вашем интерфейсе
Related lessons:
Symptoms
- Кошелёк не подключается -- в консоли браузера CORS ошибка
- Manifest.json доступен по URL, но TON Connect не может его загрузить
- Подключение работает локально, но не на продакшене
Cause
Файл tonconnect-manifest.json должен быть доступен по публичному URL с корректными CORS-заголовками. Кошельки загружают его напрямую, и если сервер не отдаёт Access-Control-Allow-Origin, запрос блокируется.
Solution
- Разместите manifest.json в публичной директории (public/) и убедитесь, что сервер отдаёт CORS-заголовки
- Для Vercel/Netlify добавьте в конфиг:
{ "headers": [{ "source": "/tonconnect-manifest.json", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }] }] } - Проверьте URL в конфигурации TonConnect:
manifestUrlдолжен быть абсолютным HTTPS-URL
Related lessons:
Symptoms
- Кошелёк подключён, но транзакции отправляются в неправильную сеть
- Баланс контракта не меняется после отправки транзакции
- Адрес контракта существует в testnet, но кошелёк работает в mainnet
Cause
Кошелёк пользователя подключён к другой сети (mainnet вместо testnet или наоборот). TON Connect не переключает сеть автоматически -- dApp должен проверять network при подключении.
Solution
- Проверяйте network после подключения:
const wallet = tonConnectUI.wallet; if (wallet.account.chain !== CHAIN.TESTNET) { /* предупреждение */ } - Покажите пользователю инструкцию по переключению сети в настройках кошелька
- В Tonkeeper: Настройки > Разработка > переключить testnet/mainnet
Related lessons:
Symptoms
- sendTransaction() завершился успешно, но состояние контракта не изменилось
- UI показывает 'транзакция отправлена', но результата нет
- Нет callback или события о подтверждении транзакции
Cause
sendTransaction() в TON Connect возвращает boc (bag of cells) отправленного сообщения, но не ждёт подтверждения в блокчейне. Необходимо самостоятельно отслеживать статус транзакции через API.
Solution
- После sendTransaction() извлеките hash из boc:
const hash = Cell.fromBase64(result.boc).hash().toString('hex') - Отслеживайте транзакцию через TonCenter API:
GET /api/v2/getTransactions?address=...&hash=... - Реализуйте polling с экспоненциальной задержкой: проверяйте каждые 3-5 секунд в течение 60 секунд
Related lessons:
Symptoms
- Перевод Jetton инициирован, но получатель не получил токены
- Баланс отправителя уменьшился, но баланс получателя не увеличился
- В explorer видно, что часть цепочки сообщений не выполнилась
Cause
Jetton-перевод в TON -- это цепочка из минимум 3 сообщений: (1) владелец -> jetton-wallet отправителя, (2) wallet отправителя -> wallet получателя (internal_transfer), (3) wallet получателя -> получатель (transfer_notification). Если на каком-то этапе не хватает TON для газа, цепочка прерывается.
Solution
- Убедитесь, что forward_ton_amount достаточен для всей цепочки: минимум 0.05 TON
- Проверьте все сообщения в цепочке через explorer: каждый шаг должен завершиться успешно
- Для тестирования в Sandbox проверяйте balanceOf для обоих wallet-контрактов после трансфера
Related lessons:
Symptoms
- После успешного mint баланс показывает 0
- get_wallet_data возвращает 0, хотя mint-транзакция прошла
- Запрос баланса по адресу master-контракта возвращает ошибку
Cause
Баланс Jetton хранится не в master-контракте, а в индивидуальном wallet-контракте каждого владельца. Адрес wallet вычисляется детерминистически из адреса владельца + адреса master. Запрос баланса по неправильному адресу возвращает 0.
Solution
- Получите адрес jetton-wallet через master-контракт:
const walletAddress = await jettonMaster.getWalletAddress(ownerAddress) - Запрашивайте баланс у wallet-контракта, а не у master:
const data = await jettonWallet.getJettonData() - В Sandbox:
const walletContract = blockchain.openContract(JettonWallet.fromAddress(walletAddress))
Related lessons:
Symptoms
- Деплой NFT-коллекции завершается с exit code 132
- Контракт коллекции задеплоен, но mint нового NFT падает
- Административные операции (mint, change_owner) не работают
Cause
Exit code 132 означает отказ в доступе. При деплое NFT-коллекции необходимо корректно задать owner в initial data. Если owner не совпадает с адресом, с которого отправляются административные операции, контракт отклоняет запросы.
Solution
- Убедитесь, что owner в StateInit совпадает с адресом деплоера:
NftCollection.fromInit({ owner: deployer.address, ... }) - Проверьте, что metadata (collection_content) корректно сериализована в Cell формате
- Для mint укажите корректный next_item_index: начните с 0 для новой коллекции
Related lessons:
Symptoms
- Попытка перевести SBT другому пользователю завершается ошибкой
- Транзакция transfer для SBT отклоняется контрактом
- В отличие от обычного NFT, transfer не работает
Cause
Soulbound Token (SBT) по стандарту TEP-85 является непередаваемым. Контракт SBT намеренно отклоняет все операции transfer. Только authority (выпустивший SBT) может отозвать (revoke) или уничтожить (destroy) токен.
Solution
- SBT нельзя передать -- это ожидаемое поведение по дизайну стандарта TEP-85
- Для выпуска нового SBT другому пользователю: mint новый SBT с нужным owner адресом
- Для отзыва: вызовите revoke() от имени authority-адреса, указанного при деплое коллекции
Related lessons:
Symptoms
- Приложение не может подключиться к TON API
- Локальная нода TON не отвечает на запросы
- Скрипт деплоя или тестирования падает с ошибкой подключения
Cause
Локальный эндпоинт TON API (MyLocalTon, tonlib) не запущен или Docker-контейнер с нодой не активен. Если проект настроен на локальную ноду, а она не запущена, все сетевые запросы завершатся с ECONNREFUSED.
Solution
- Проверьте, запущен ли Docker-контейнер:
docker ps | grep ton - Переключитесь на публичный testnet RPC: замените endpoint на
https://testnet.toncenter.com/api/v2/jsonRPC - Для Sandbox тестов используйте
blockchain = await Blockchain.create()-- он не требует внешнего подключения
Related lessons:
Symptoms
- Запрос тестовых TON из faucet возвращает ошибку rate limit
- Бот @testgiver_ton_bot не отвечает или отказывает в выдаче
- Невозможно получить тестовые TON для деплоя на testnet
Cause
Testnet faucet имеет ограничение на частоту запросов для предотвращения злоупотреблений. Обычно лимит составляет 1 запрос в 1-2 минуты на один адрес.
Solution
- Подождите 1-2 минуты между запросами к faucet
- Используйте альтернативный faucet: бот @testgiver_ton_bot в Telegram
- Для множественных деплоев: запросите больше TON за раз и распределите между кошельками
Related lessons:
Symptoms
- Сообщение отправлено на неправильный адрес (bounceable вместо non-bounceable)
- Контракт получает bounced-сообщения неожиданно
- Адрес из explorer не совпадает с адресом в коде
Cause
В TON существуют два формата friendly-адресов: EQ-адрес (bounceable, для контрактов) и UQ-адрес (non-bounceable, для кошельков). Это один и тот же адрес, но с разным bounce-флагом. Путаница приводит к неожиданным bounced-сообщениям или их отсутствию.
Solution
- Используйте EQ-адрес (bounceable) при отправке на смарт-контракты -- если контракт откажет, TON вернёт средства
- Используйте UQ-адрес (non-bounceable) при отправке на кошельки -- средства не вернутся при ошибке
- Для конвертации:
address.toString({ bounceable: true })для EQ,address.toString({ bounceable: false })для UQ - Raw-адрес (0:abc...) не содержит bounce-флага -- он устанавливается при отправке сообщения
Related lessons:
Error not found? Use Cmd+K (Ctrl+K) to search for error text in the knowledge base. Also refer to module lessons for deeper understanding of mechanisms and problem diagnosis.