Все статьи
7 мин чтения

История транзакций TON: lt, хэши и архивная глубина

Как устроена история транзакций TON: логическое время (lt), хэши и указатель последней транзакции. Чтение истории агентом через MCP и почему глубина требуе

история транзакций TONlogical timelt hash TONget_transactionsархивная нода TONMCP TON

Ты работаешь ночью, и в 3:47 прилетает сообщение: пользователь оплатил заказ в TON, а бот его не засчитал. Открываешь эксплорер — деньги на месте, входящий перевод виден. Значит, проблема не в блокчейне, а в том, как твой сервис читает историю транзакций. И тут выясняется, что в TON эта история устроена совсем не так, как ты привык по Ethereum.

Мониторинг платежей на TON выглядит обманчиво просто: опрашивай баланс и реагируй на изменение. Но на практике всё разваливается. Два платежа на одинаковую сумму в одну минуту — сколько их было, один или два? Пользователь заплатил USDT, а баланс GRAM не шелохнулся. Нужна выписка за полгода, а лайтсервер отдаёт только последние транзакции и молчит про остальное.

Если ты подключаешь ИИ-агента к TON или пишешь бэкенд, который сверяет платежи, разберёмся по делу: что такое история транзакций TON, зачем нужна пара lt + hash, почему глубина упирается в архивную ноду и как всё это читать агентом через MCP.

Что такое история транзакций в TON и зачем она агенту

В TON у каждого аккаунта — кошелька, контракта, джеттон-кошелька — есть собственная цепочка транзакций. Каждая транзакция — это результат обработки входящего сообщения: приём GRAM, перевод жетона, вызов метода контракта. Для агента (Claude, Cursor, любого MCP-клиента), который принимает платежи или сверяет расчёты, история — единственный достоверный источник: баланс говорит «сколько сейчас», а история — «что именно и когда произошло, от кого и на какую сумму».

Приземлённые задачи, ради которых агенту нужна история:

  • подтвердить, что платёж дошёл («проверь, приходил ли перевод на этот адрес за последний час»);
  • собрать выписку по кошельку;
  • отследить конкретную транзакцию по её идентификатору;
  • сверить входящие джеттоны (например, USDT) с ожидаемой суммой.

Проблема в том, что «дай мне транзакции №100–120» в TON не работает. Здесь нет глобальной сплошной нумерации блоков, по которой можно было бы «дать транзакции номер N». Порядок задаётся иначе — через логическое время.

Логическое время (lt): почему в TON нет обычной нумерации блоков

В Ethereum всё просто: есть блок № 19 000 000, в нём транзакции по порядку. Один глобальный счётчик, монотонно растёт, все ориентируются на один и тот же номер.

TON — многопоточная система: мастерчейн, воркчейны, шарды, которые делятся и сливаются под нагрузкой. Единого «номера блока», по которому можно линейно упорядочить все события сети, тут просто нет. Вместо него TON использует логическое время (lt, logical time) — монотонно растущий счётчик, которым сеть упорядочивает события, сообщения и транзакции. Он гарантирует: если событие A повлияло на событие B, то lt(A) < lt(B).

Отсюда практическое следствие: позиция транзакции задаётся не номером блока, а парой (lt, hash). lt отвечает за порядок (кто раньше), hash — за однозначную идентификацию конкретной транзакции. По отдельности они для точечного запроса бесполезны: у разных объектов могут быть близкие lt, а один только hash не говорит liteserver, откуда начинать чтение.

Хэш транзакции и пара lt + hash: указатель последней транзакции аккаунта

Хэш транзакции — это её криптографический отпечаток; на TON он приходит в base64 или hex. lt говорит «когда по порядку», hash — «какая именно», потому что на одном lt-срезе теоретически возможна неоднозначность, и без хэша liteserver транзакцию не отдаст. Запомни жёстко: пара (lt, hash) обязательна вместе. Только lt или только hash — недостаточно.

Где взять стартовую точку? Состояние аккаунта хранит указатель на самую последнюю транзакцию — last_transaction_id, это та самая пара lt + hash (поля last_trans_lt / last_trans_hash). Это «голова» списка, с которой начинается обход истории назад.

В MCP-инструментарии TONNode это делает get_account_state — он возвращает статус аккаунта, флаги и указатель на последнюю транзакцию.

Как get_transactions листает историю: lt + hash и цепочка prev_trans

Теперь ключевой момент, из-за которого история вообще листается. Каждая транзакция, кроме собственных lt и hash, содержит два поля: prev_trans_lt и prev_trans_hash — ссылку на предыдущую транзакцию этого же аккаунта. То есть транзакции образуют односвязный список, идущий назад во времени; голова — last_transaction_id из состояния.

get_transactions принимает:

  • account — адрес аккаунта;
  • стартовые lt и hash — точку, от которой читать;
  • count — размер пачки.

Он возвращает пачку транзакций, идя назад от указанной точки. Логика постраничного обхода:

  1. Взять last_trans_lt / last_trans_hash из get_account_state — это голова.
  2. Вызвать get_transactions(account, lt, hash, count) — получить пачку.
  3. У последней транзакции в пачке взять prev_trans_lt и prev_trans_hash.
  4. Снова вызвать get_transactions уже с этой парой — это следующая страница.
  5. Повторять, пока prev_trans_lt не станет 0 (начало жизни аккаунта) или пока не дойдёшь до нужной глубины.

Так работает пагинация в TON: не «страница 2», а «продолжи от этой пары (lt, hash)».

Свежие блоки против глубокой истории: зачем нужна архивная нода

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

Это не баг, а дизайн: держать полный путь каждого аккаунта с генезиса дорого. Глубокую историю хранит архивная нода — узел, который не сбрасывает старые блоки, а держит полное состояние сети за всю историю.

Практический вывод:

  • детект платежа, свежая выписка, «пришло ли за последний час» — хватает обычного liteserver;
  • полный аудит кошелька с самого первого дня — нужна архивная нода.

Отдельная боль — публичные лайтсерверы из глобального конфига TON. Они общие и лимитированные: под нагрузкой часто отвечают not ready или уходят в ADNL-таймаут, и глубокой истории на них тоже нет. Если ты словил именно not ready, это симптом перегруженного общего лайтсервера — разбор в заметке почему liteserver отвечает not ready и как это чинить.

Честно про TONNode: архивная нода в роадмапе и сейчас синхронизируется — архивную глубину как готовую функцию я тебе не обещаю. На сегодня это чтение свежей и недавней истории через управляемый эндпоинт, без лотереи с публичными лайтсерверами. Для полного архива с самого генезиса жди отдельного анонса.

Как читать историю TON ИИ-агентом через MCP

TONNode — это hosted MCP-сервер для TON, ровно 16 инструментов, и get_transactions входит в блок чтения. MCP (Model Context Protocol) — стандарт, по которому агент вызывает инструменты сам, без того чтобы ты руками собирал ADNL-запросы. Не нужно ставить SDK, поднимать лайтсервер и разбирать TL-B — агент дёргает инструмент напрямую.

Бесплатное локальное подключение

Полный набор чтения, публичный конфиг, пакет @tonnode/mcp — open source, MIT:

{
  "mcpServers": {
    "ton": {
      "command": "npx",
      "args": ["-y", "@tonnode/mcp"]
    }
  }
}

Пакет работает по нативному ADNL-протоколу TON, без HTTP-прослоек.

Hosted-эндпоинт со своим ключом

Для гарантированной пропускной способности берётся hosted-эндпоинт со своим ключом:

{
  "mcpServers": {
    "ton": {
      "type": "http",
      "url": "https://mcp.tonnode.io/mcp",
      "headers": { "Authorization": "Bearer tn_live_…" }
    }
  }
}

Для чтения истории пригодятся:

  • get_account_state — стартовая точка обхода (last_trans_lt / last_trans_hash), статус и флаги аккаунта;
  • get_transactions — сами транзакции постранично, по паре (lt, hash);
  • parse_address — офлайн-конвертация адресов между EQ / UQ / raw;
  • get_jetton_info и get_jetton_balance — для истории по жетонам.

Практика: детект платежа и постраничный обход истории

Соберём типовой сценарий — «пришёл ли платёж и на какую сумму».

Промпт агенту

Через инструмент ton возьми get_account_state для EQC…myshop, потом get_transactions от его last_trans_lt / last_trans_hash, count 20. Найди входящий перевод на 5 GRAM за последние 10 минут. Адреса отправителей приводи через parse_address к EQ перед сравнением. Если нужна история глубже — бери prev_trans_lt / prev_trans_hash последней транзакции и повторяй get_transactions.

Агент сам сходит за головой цепочки, пролистнёт пачку и сверит суммы.

Псевдокод постраничного обхода

Тот же обход в псевдокоде (вызовы — это MCP-инструменты, которые дёргает агент):

state = get_account_state(account)
lt    = state.last_trans_lt
hash  = state.last_trans_hash

while lt != 0:
    batch = get_transactions(account, lt, hash, count=20)
    for tx in batch:
        if matches_expected_payment(tx):   # входящее сообщение с нужной суммой/комментом
            return tx
    last = batch[-1]
    lt   = last.prev_trans_lt
    hash = last.prev_trans_hash   # без hash следующая страница не запросится

Детали, экономящие часы отладки

  • Адреса в полях транзакций приходят в raw-форме (0:abcd…). Прежде чем сравнивать отправителя или получателя с привычным EQ…-адресом, приведи оба к одному формату через parse_address — иначе сравнение строк не сойдётся, хотя адрес один и тот же. Подробнее про форматы — EQ, UQ и raw в адресах TON.
  • Платежи в USDT не видны на цепочке кошелька GRAM. Жетоны живут на отдельном джеттон-кошельке, чей адрес вычисляется он-чейн (get_jetton_balance делает это за тебя и даёт текущий баланс). Для истории по жетонам обходи транзакции джеттон-кошелька, а не основного.
  • Пересчитывай raw-суммы через decimals. Суммы жетонов приходят в минимальных единицах: чтобы превратить 5000000 в человеческие 5 USDT, возьми decimals из get_jetton_info — у USDT это 6, у большинства джеттонов 9. Перепутаешь — ошибёшься в 1000 раз. Полный разбор — как ловить входящий платёж USDT на TON.
  • Дедуп по (lt, hash), а не по сумме. Два одинаковых платежа различаются только парой lt+hash — это и есть ключ идемпотентности.

Если помимо истории агенту нужно дёргать текущее состояние контракта, это делает run_get_method — как звать get-методы без SDK, показано в чтении TON без SDK через get-метод.

Коротко

  • В TON нет номеров блоков как в Ethereum — позиция транзакции задаётся парой (lt, hash): lt — порядок, hash — идентификация, и они обязательны вместе.
  • Транзакции аккаунта — связный список через prev_trans_lt / prev_trans_hash; листаешь назад, начиная с last_transaction_id из get_account_state.
  • Обычный liteserver отдаёт свежую и недавнюю историю; полная глубина — задача архивной ноды.
  • Для джеттонов не забудь про decimals из get_jetton_info (USDT = 6) и raw-адреса, которые надо прогнать через parse_address.

Не хочешь возиться с публичными лайтсерверами, которые отвечают not ready? get_transactions доступен из коробки на бесплатном тарифе Hobby — 60 запросов/мин, навсегда, без карты, ключ выдаётся сразу после входа.

Подключи чтение истории TON за минуту → tonnode.io/dashboard?plan=hobby

А что ещё умеет агент на TON помимо чтения истории — все 16 инструментов на странице инструментов TONNode.

Дайте вашему агенту доступ к TON

16 MCP-инструментов: чтение, некастодиальные свапы, кроссчейн и кошельки. Бесплатный тариф — 60 запр/мин, карта не нужна.