Усі статті
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 зап/хв, картка не потрібна.