Skip to content
Learning Platform

Troubleshooting TON

База знаний типичных ошибок при разработке в экосистеме TON -- от компиляции Tact/Tolk до настройки Blueprint, работы с TON Connect и развёртывания смарт-контрактов. Используйте фильтры или Cmd+K для поиска.

Back to course

Фильтр по модулю

Фильтр по категории

Showing 30 of 30 errors

Symptoms

  • Контракт деплоится, но при первом вызове возвращает exit code 128
  • В Sandbox тест падает сразу после деплоя при отправке первого сообщения
  • Транзакция на testnet завершается с exit code 128 в compute phase

Cause

Контракт не был корректно инициализирован. В Tact каждый контракт должен иметь функцию `init()`, которая задаёт начальное состояние. Если `init()` отсутствует или StateInit не передан при деплое, контракт останется неинициализированным.

Solution

  1. Убедитесь, что контракт имеет функцию init(): contract MyContract { init() { ... } }
  2. При деплое обязательно передавайте StateInit: contractProvider.internal(via, { value: toNano('0.05'), body: beginCell().endCell() })
  3. В Blueprint убедитесь, что wrapper корректно формирует StateInit через MyContract.fromInit(...) перед деплоем

Symptoms

  • Контракт отклоняет входящее сообщение с exit code 129
  • Сообщение отправлено, но контракт не выполняет ожидаемый receiver
  • В логах Sandbox видно, что opcode сообщения не совпадает с ожидаемым

Cause

Контракт получил сообщение с opcode, для которого нет подходящего receiver. Это может быть вызвано несовпадением имени сообщения между отправителем и получателем, либо коллизией opcode при использовании пользовательских типов сообщений.

Solution

  1. Проверьте, что имя сообщения в send() точно совпадает с receive() в контракте: receive(msg: MyMessage) ожидает MyMessage{...}
  2. Убедитесь, что оба контракта (отправитель и получатель) используют одну и ту же версию .tact файла с определением сообщения
  3. Проверьте opcode в сгенерированном коде: npx blueprint build и осмотрите ABI файл на предмет коллизий

Symptoms

  • Сообщение доставлено, но контракт отвергает его с exit code 130
  • Отправка на контракт работает из одного кошелька, но не из другого
  • В Sandbox тест проходит, но на testnet сообщение отклоняется

Cause

Контракт ожидает bounceable-сообщение, но получил non-bounceable (или наоборот). Адреса с префиксом EQ (bounceable) и UQ (non-bounceable) определяют поведение bounce-флага. Некоторые контракты строго проверяют этот флаг.

Solution

  1. Используйте правильный формат адреса: EQ-адрес для контрактов (bounceable), UQ-адрес для кошельков (non-bounceable)
  2. При отправке из кода явно укажите bounce-флаг: send(SendParameters{ bounce: true, to: contractAddress, ... })
  3. В тестах Sandbox проверьте формат адреса: address.toString({ bounceable: true }) для контрактов

Symptoms

  • Вызов функции контракта возвращает exit code 132
  • Только определённые операции завершаются с ошибкой, остальные работают
  • Ошибка возникает при попытке административных действий (withdraw, transfer ownership)

Cause

Контракт проверяет права доступа через `require(sender() == self.owner, "Access denied")` или `self.requireOwner()`. Вызывающий адрес не совпадает с owner-адресом, записанным в состоянии контракта при инициализации.

Solution

  1. Убедитесь, что отправляете транзакцию с того же адреса, который указан как owner при деплое контракта
  2. Проверьте owner через get-метод: const owner = await contract.getOwner()
  3. В тестах Sandbox используйте правильный wallet: const owner = await blockchain.treasury('owner') и отправляйте от его имени

Symptoms

  • Компиляция Tact контракта завершается с ошибкой несовпадения типов
  • Поля структуры принимаются как Int, но ожидается сериализованный тип
  • Ошибка появляется при использовании struct в message body

Cause

В Tact поля структур и сообщений требуют явных аннотаций сериализации для определения формата хранения в Cell. Без аннотации компилятор использует полный 257-bit Int, что может конфликтовать с ожиданиями на стороне получателя.

Solution

  1. Добавьте аннотацию сериализации к полям: amount: Int as coins вместо amount: Int
  2. Для целых чисел используйте конкретные размеры: id: Int as uint32, timestamp: Int as uint64
  3. Проверьте, что структура на стороне отправителя и получателя имеет идентичные аннотации
  4. Список доступных аннотаций: uint8, uint16, uint32, uint64, uint128, uint256, int8...int257, coins

Symptoms

  • Компилятор Tact не находит функцию send()
  • Код скопирован из примера на FunC, но не компилируется в Tact
  • Ошибка при попытке отправить сообщение из контракта

Cause

В Tact нет глобальной функции `send()` в стиле FunC. Для отправки сообщений используются методы контракта: `self.reply()`, `self.forward()`, `self.notify()` или глобальная `send(SendParameters{...})` с явной структурой параметров.

Solution

  1. Для ответа отправителю используйте: self.reply("Response".asComment())
  2. Для пересылки другому контракту: send(SendParameters{ to: address, value: ton("0.05"), body: MyMessage{...}.toCell() })
  3. Для уведомления (без body): self.notify("Done".asComment())
  4. Не копируйте FunC-паттерны напрямую -- Tact имеет собственный API для отправки сообщений

Symptoms

  • Blueprint не может задеплоить контракт -- ошибка при сборке wrapper
  • Контракт компилируется, но деплой через Blueprint падает
  • В wrapper-файле вызывается Deploy, но контракт его не принимает

Cause

Blueprint по умолчанию отправляет сообщение Deploy при деплое контракта. Если контракт не импортирует trait Deployable или не имеет receiver для сообщения Deploy, деплой завершится ошибкой.

Solution

  1. Добавьте trait Deployable в контракт: contract MyContract with Deployable { ... }
  2. Или добавьте явный receiver: receive(msg: Deploy) { self.notify("deployed".asComment()) }
  3. Не забудьте импорт: import "@stdlib/deploy"

Symptoms

  • Арифметическая операция в контракте завершается с exit code 4
  • Умножение или сложение больших чисел приводит к краху контракта
  • Ошибка появляется при работе с балансами в nanoTON (10^9 порядки)

Cause

TVM использует 257-битные знаковые целые числа. Exit code 4 возникает при переполнении диапазона (-2^256 до 2^256-1) или при делении на ноль. В контексте Tact это часто происходит при перемножении больших значений (например, price * amount) без промежуточной проверки.

Solution

  1. Добавьте проверки перед арифметикой: require(a <= MAX_SAFE_VALUE, "Value too large")
  2. Для финансовых расчётов используйте nanoTON (Int) и проверяйте границы: require(amount > 0 && amount <= ton("1000000"), "Invalid amount")
  3. Избегайте деления на ноль: require(divisor != 0, "Division by zero")
  4. Тестируйте граничные значения в Sandbox: toNano('1000000000') для проверки переполнения

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

  1. Проверьте версию Node.js: node -v (должна быть >= 18)
  2. Установите зависимости: npm install в корне проекта
  3. Если проблема сохраняется, удалите node_modules и package-lock.json: rm -rf node_modules package-lock.json && npm install

Symptoms

  • Jest тест завершается по таймауту при работе с контрактом
  • Тест с цепочкой сообщений между контрактами не успевает завершиться
  • Первый тест проходит, но сложные сценарии падают по timeout

Cause

По умолчанию Jest даёт 5 секунд на тест. Сложные сценарии с несколькими контрактами, цепочками сообщений или большими данными могут превысить этот лимит в Sandbox.

Solution

  1. Увеличьте таймаут для конкретного теста: it('complex test', async () => { ... }, 30000)
  2. Или глобально в jest.config.ts: testTimeout: 30000
  3. Оптимизируйте тест: деплойте контракты в beforeAll(), а не в каждом it()

Symptoms

  • Тест проходил раньше, но после изменения контракта начал падать
  • Hash кода контракта в wrapper не совпадает с скомпилированным
  • Sandbox создаёт контракт со старой версией кода

Cause

После изменения .tact файла необходимо перекомпилировать контракт. Wrapper использует скомпилированный code cell (hex), который кешируется после `npx blueprint build`. Без пересборки wrapper ссылается на устаревший код.

Solution

  1. Перекомпилируйте контракт: npx blueprint build
  2. Убедитесь, что wrapper обновлён: проверьте файл build/MyContract/tact_MyContract.code.boc
  3. В CI/CD добавьте build перед тестами: npx blueprint build && npx jest

Symptoms

  • Dev-сервер не запускается -- порт уже занят
  • После некорректного завершения предыдущего процесса порт заблокирован
  • Несколько проектов конфликтуют по порту

Cause

Другой процесс уже слушает порт 3000. Это может быть незавершённый dev-сервер, другой проект или системный процесс.

Solution

  1. Найдите процесс на порту: lsof -i :3000 (macOS/Linux) или netstat -ano | findstr :3000 (Windows)
  2. Завершите процесс: kill -9
  3. Или запустите на другом порту: измените PORT=3001 в конфигурации или используйте --port 3001

Symptoms

  • Команда npx jest падает с ошибкой preset not found
  • Проект Blueprint создан, но тесты не запускаются
  • TypeScript тесты не компилируются через Jest

Cause

Пакет ts-jest не установлен в проекте. Blueprint-проекты используют TypeScript для тестов, и ts-jest необходим как preset для Jest для компиляции .ts файлов.

Solution

  1. Установите ts-jest: npm install --save-dev ts-jest
  2. Убедитесь, что jest.config.ts содержит: preset: 'ts-jest'
  3. Проверьте совместимость версий: ts-jest должен соответствовать версии Jest (обычно jest@29 + ts-jest@29)

Symptoms

  • npx blueprint build не может найти компилятор Tact
  • Ошибка после обновления зависимостей проекта
  • Новый проект компилировался, но после npm update перестал

Cause

Пакет @tact-lang/compiler не установлен или установлена несовместимая версия. Blueprint ищет компилятор Tact в node_modules, и при отсутствии пакета или конфликте версий сборка невозможна.

Solution

  1. Установите компилятор: npm install @tact-lang/compiler
  2. Проверьте версию в package.json: "@tact-lang/compiler": "^1.x.x"
  3. При конфликте версий: npm ls @tact-lang/compiler для диагностики, затем npm dedupe

Symptoms

  • Транзакция завершается с exit code 13 в compute phase
  • Контракт не выполняет все операции, несмотря на корректный код
  • В Sandbox тест проходит, но на testnet транзакция падает

Cause

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

Solution

  1. Проверьте msg_value входящего сообщения: для простых операций достаточно 0.05 TON, для сложных (с пересылкой) -- 0.1-0.5 TON
  2. В Sandbox включите логирование газа: blockchain.verbosity = { vmLogs: 'vm_logs_full' }
  3. Оптимизируйте контракт: избегайте циклов по всему map, используйте пагинацию для больших коллекций
  4. Убедитесь, что отправляете достаточно TON с сообщением: send(SendParameters{ value: ton("0.1"), ... })

Symptoms

  • Попытка записать данные в Cell завершается с exit code 8
  • Контракт падает при сохранении большого объёма данных в storage
  • Сериализация структуры с множеством полей вызывает ошибку

Cause

Одна Cell в TON может хранить максимум 1023 бита данных и 4 ссылки на другие Cell. При попытке записать больше данных или добавить более 4 ссылок TVM выбрасывает exit code 8.

Solution

  1. Разделите данные на несколько Cell: используйте ссылки (refs) для больших структур
  2. Проверьте размер данных: builder.bits не должен превышать 1023, builder.refs не более 4
  3. В Tact используйте map для коллекций данных вместо хранения всего в одной Cell
  4. Для строк используйте String тип в Tact -- он автоматически разбивается на snake-cells

Symptoms

  • Чтение данных из Cell завершается с exit code 9
  • Get-метод контракта падает при попытке десериализации
  • Контракт работал, но после обновления формата данных начал падать

Cause

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

Solution

  1. Проверьте порядок чтения полей: он должен точно соответствовать порядку записи
  2. При обновлении контракта убедитесь, что формат данных совместим или реализуйте миграцию
  3. Используйте slice.remainingBits и slice.remainingRefs для проверки перед чтением

Symptoms

  • Сообщение отправлено, но вернулось обратно (bounced) без обработки
  • Средства вернулись за вычетом комиссии, но целевое действие не выполнено
  • В explorer транзакция отмечена как bounced

Cause

Контракт-получатель отклонил сообщение (вернул ненулевой exit code), и так как сообщение было отправлено с bounce: true, оно было возвращено отправителю. Если отправитель не обрабатывает bounced-сообщения, средства могут быть потеряны.

Solution

  1. Реализуйте обработку bounced-сообщений: bounced(msg: bounced) { ... }
  2. Проверьте причину отклонения на стороне получателя через explorer (tonscan.org или tonviewer.com)
  3. Для некритичных уведомлений используйте bounce: false в SendParameters
  4. Учитывайте, что bounced-сообщение содержит только первые 224 бита тела оригинального сообщения

Symptoms

  • TVM останавливается с exit code 11 при выполнении контракта
  • Контракт задеплоен, но любой вызов завершается этой ошибкой
  • Ошибка появляется после деплоя с неправильным code cell

Cause

TVM встретил невалидную инструкцию. Это обычно означает повреждённый code cell, деплой с неправильной версией скомпилированного кода или попытку выполнить данные как код.

Solution

  1. Перекомпилируйте контракт: npx blueprint build и убедитесь, что используете свежий .boc файл
  2. Проверьте, что code cell в StateInit соответствует ожидаемому контракту
  3. Убедитесь, что версия компилятора совместима с версией TVM на целевой сети

Symptoms

  • Пользователь начинает подписание транзакции, но приложение получает ошибку
  • После открытия кошелька и нажатия 'Отмена' приложение падает
  • Необработанное исключение при закрытии модального окна кошелька

Cause

Пользователь отклонил транзакцию в кошельке (нажал 'Cancel' или закрыл приложение). TON Connect SDK выбрасывает UserRejectsError, которое необходимо обработать в UI.

Solution

  1. Оберните вызов sendTransaction в try/catch: try { await tonConnectUI.sendTransaction(tx) } catch (e) { if (e instanceof UserRejectsError) { /* показать сообщение */ } }
  2. Покажите пользователю дружелюбное сообщение: 'Транзакция отменена' вместо стектрейса
  3. Не блокируйте UI при ожидании подписания -- добавьте кнопку 'Отмена' в вашем интерфейсе

Symptoms

  • Кошелёк не подключается -- в консоли браузера CORS ошибка
  • Manifest.json доступен по URL, но TON Connect не может его загрузить
  • Подключение работает локально, но не на продакшене

Cause

Файл tonconnect-manifest.json должен быть доступен по публичному URL с корректными CORS-заголовками. Кошельки загружают его напрямую, и если сервер не отдаёт Access-Control-Allow-Origin, запрос блокируется.

Solution

  1. Разместите manifest.json в публичной директории (public/) и убедитесь, что сервер отдаёт CORS-заголовки
  2. Для Vercel/Netlify добавьте в конфиг: { "headers": [{ "source": "/tonconnect-manifest.json", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }] }] }
  3. Проверьте URL в конфигурации TonConnect: manifestUrl должен быть абсолютным HTTPS-URL

Symptoms

  • Кошелёк подключён, но транзакции отправляются в неправильную сеть
  • Баланс контракта не меняется после отправки транзакции
  • Адрес контракта существует в testnet, но кошелёк работает в mainnet

Cause

Кошелёк пользователя подключён к другой сети (mainnet вместо testnet или наоборот). TON Connect не переключает сеть автоматически -- dApp должен проверять network при подключении.

Solution

  1. Проверяйте network после подключения: const wallet = tonConnectUI.wallet; if (wallet.account.chain !== CHAIN.TESTNET) { /* предупреждение */ }
  2. Покажите пользователю инструкцию по переключению сети в настройках кошелька
  3. В Tonkeeper: Настройки > Разработка > переключить testnet/mainnet

Symptoms

  • sendTransaction() завершился успешно, но состояние контракта не изменилось
  • UI показывает 'транзакция отправлена', но результата нет
  • Нет callback или события о подтверждении транзакции

Cause

sendTransaction() в TON Connect возвращает boc (bag of cells) отправленного сообщения, но не ждёт подтверждения в блокчейне. Необходимо самостоятельно отслеживать статус транзакции через API.

Solution

  1. После sendTransaction() извлеките hash из boc: const hash = Cell.fromBase64(result.boc).hash().toString('hex')
  2. Отслеживайте транзакцию через TonCenter API: GET /api/v2/getTransactions?address=...&hash=...
  3. Реализуйте polling с экспоненциальной задержкой: проверяйте каждые 3-5 секунд в течение 60 секунд

Symptoms

  • Перевод Jetton инициирован, но получатель не получил токены
  • Баланс отправителя уменьшился, но баланс получателя не увеличился
  • В explorer видно, что часть цепочки сообщений не выполнилась

Cause

Jetton-перевод в TON -- это цепочка из минимум 3 сообщений: (1) владелец -> jetton-wallet отправителя, (2) wallet отправителя -> wallet получателя (internal_transfer), (3) wallet получателя -> получатель (transfer_notification). Если на каком-то этапе не хватает TON для газа, цепочка прерывается.

Solution

  1. Убедитесь, что forward_ton_amount достаточен для всей цепочки: минимум 0.05 TON
  2. Проверьте все сообщения в цепочке через explorer: каждый шаг должен завершиться успешно
  3. Для тестирования в Sandbox проверяйте balanceOf для обоих wallet-контрактов после трансфера

Symptoms

  • После успешного mint баланс показывает 0
  • get_wallet_data возвращает 0, хотя mint-транзакция прошла
  • Запрос баланса по адресу master-контракта возвращает ошибку

Cause

Баланс Jetton хранится не в master-контракте, а в индивидуальном wallet-контракте каждого владельца. Адрес wallet вычисляется детерминистически из адреса владельца + адреса master. Запрос баланса по неправильному адресу возвращает 0.

Solution

  1. Получите адрес jetton-wallet через master-контракт: const walletAddress = await jettonMaster.getWalletAddress(ownerAddress)
  2. Запрашивайте баланс у wallet-контракта, а не у master: const data = await jettonWallet.getJettonData()
  3. В Sandbox: const walletContract = blockchain.openContract(JettonWallet.fromAddress(walletAddress))

Symptoms

  • Деплой NFT-коллекции завершается с exit code 132
  • Контракт коллекции задеплоен, но mint нового NFT падает
  • Административные операции (mint, change_owner) не работают

Cause

Exit code 132 означает отказ в доступе. При деплое NFT-коллекции необходимо корректно задать owner в initial data. Если owner не совпадает с адресом, с которого отправляются административные операции, контракт отклоняет запросы.

Solution

  1. Убедитесь, что owner в StateInit совпадает с адресом деплоера: NftCollection.fromInit({ owner: deployer.address, ... })
  2. Проверьте, что metadata (collection_content) корректно сериализована в Cell формате
  3. Для mint укажите корректный next_item_index: начните с 0 для новой коллекции

Symptoms

  • Попытка перевести SBT другому пользователю завершается ошибкой
  • Транзакция transfer для SBT отклоняется контрактом
  • В отличие от обычного NFT, transfer не работает

Cause

Soulbound Token (SBT) по стандарту TEP-85 является непередаваемым. Контракт SBT намеренно отклоняет все операции transfer. Только authority (выпустивший SBT) может отозвать (revoke) или уничтожить (destroy) токен.

Solution

  1. SBT нельзя передать -- это ожидаемое поведение по дизайну стандарта TEP-85
  2. Для выпуска нового SBT другому пользователю: mint новый SBT с нужным owner адресом
  3. Для отзыва: вызовите revoke() от имени authority-адреса, указанного при деплое коллекции

Symptoms

  • Приложение не может подключиться к TON API
  • Локальная нода TON не отвечает на запросы
  • Скрипт деплоя или тестирования падает с ошибкой подключения

Cause

Локальный эндпоинт TON API (MyLocalTon, tonlib) не запущен или Docker-контейнер с нодой не активен. Если проект настроен на локальную ноду, а она не запущена, все сетевые запросы завершатся с ECONNREFUSED.

Solution

  1. Проверьте, запущен ли Docker-контейнер: docker ps | grep ton
  2. Переключитесь на публичный testnet RPC: замените endpoint на https://testnet.toncenter.com/api/v2/jsonRPC
  3. Для Sandbox тестов используйте blockchain = await Blockchain.create() -- он не требует внешнего подключения

Symptoms

  • Запрос тестовых TON из faucet возвращает ошибку rate limit
  • Бот @testgiver_ton_bot не отвечает или отказывает в выдаче
  • Невозможно получить тестовые TON для деплоя на testnet

Cause

Testnet faucet имеет ограничение на частоту запросов для предотвращения злоупотреблений. Обычно лимит составляет 1 запрос в 1-2 минуты на один адрес.

Solution

  1. Подождите 1-2 минуты между запросами к faucet
  2. Используйте альтернативный faucet: бот @testgiver_ton_bot в Telegram
  3. Для множественных деплоев: запросите больше TON за раз и распределите между кошельками

Symptoms

  • Сообщение отправлено на неправильный адрес (bounceable вместо non-bounceable)
  • Контракт получает bounced-сообщения неожиданно
  • Адрес из explorer не совпадает с адресом в коде

Cause

В TON существуют два формата friendly-адресов: EQ-адрес (bounceable, для контрактов) и UQ-адрес (non-bounceable, для кошельков). Это один и тот же адрес, но с разным bounce-флагом. Путаница приводит к неожиданным bounced-сообщениям или их отсутствию.

Solution

  1. Используйте EQ-адрес (bounceable) при отправке на смарт-контракты -- если контракт откажет, TON вернёт средства
  2. Используйте UQ-адрес (non-bounceable) при отправке на кошельки -- средства не вернутся при ошибке
  3. Для конвертации: address.toString({ bounceable: true }) для EQ, address.toString({ bounceable: false }) для UQ
  4. Raw-адрес (0:abc...) не содержит bounce-флага -- он устанавливается при отправке сообщения

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.