Розробка індексатора TON blockchain
TON — одна з найскладніших блокчейн-платформ для індексації. Причина в архітектурі: TON використовує infinite sharding — кількість шардів динамічно змінюється залежно від навантаження. Транзакції всередину одного шарду финалізуються швидко, але транзакція між аккаунтами в різних шардах це ланцюг сообщень, яку потрібно відслідковувати через кілька блоків та шардів. Стандартний підхід "слухаємо блоки по одному" тут не працює.
Архітектура TON: що потрібно зрозуміти перед індексацією
TON складається з трьох рівнів:
- Masterchain — головна цепь, финалізує стан усіх workcheins
- Basechain (workchain 0) — основна користувальницька цепь
- Shardchains — шарди workchain-а, їх може бути від 1 до 256
Кожний masterchain блок ссилається на останні блоки всіх шардів (ShardStateUnsplit). Для повної індексації потрібно:
- Отримати masterchain блок
- З нього витягти список актуальних шард-блоків
- З кожного шард-блока витягти транзакції
- Для кожної транзакції — відследити дочірні сообщення (out messages)
MasterBlock[N]
└─ Shard(0:0..7fff)[blockX]
├─ tx1 → out_msg → [інший шард або аккаунт]
└─ tx2 → out_msg → ...
└─ Shard(0:8000..ffff)[blockY]
└─ tx3 ...
TON API vs запуск своєї ноди
Hosted API (швидкий старт)
- TonAPI (tonapi.io) — багатий REST + WebSocket, підтримує events, transactions, traces; free tier з rate limits
-
TON Center (
toncenter.com/api/v2) — офіційний публічний API, платний тариф без rate limits - GetBlock.io, Chainbase — enterprise RPC
Hosted API — правильний вибір для MVP та середніх навантажень.
Своя нода
Повний TON архів (~2 TB, зростає ~500 GB/рік) потрібен при:
- Необхідність трасування транзакцій (
runGetMethodпо історичних блокам) - Rate limits стають bottleneck
- Вимоги до SLA
# TON node (C++ реалізація) — складна сборка з вихідних текстів
git clone --recurse-submodules https://github.com/ton-blockchain/ton
cd ton && mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc) validator-engine
# Або tonutils-go для Go-інструментів
Простіший шлях — mytonctrl (скрипт Ton Foundation для встановлення ноди):
wget https://raw.githubusercontent.com/ton-blockchain/mytonctrl/master/scripts/install.sh
sudo bash install.sh -m full
Реалізація індексера: основний цикл
import { TonClient4, Cell, loadTransaction } from '@ton/ton';
class TonIndexer {
private client: TonClient4;
private db: Pool;
private lastMasterBlock: number;
async indexLoop(): Promise<void> {
while (true) {
try {
// Отримуємо актуальний masterchain блок
const masterInfo = await this.client.getLastBlock();
const currentSeqno = masterInfo.last.seqno;
if (currentSeqno <= this.lastMasterBlock) {
await this.sleep(2000); // Чекаємо новий блок (~5 сек у TON)
continue;
}
// Обробляємо усі пропущені мастер-блоки
for (let seqno = this.lastMasterBlock + 1; seqno <= currentSeqno; seqno++) {
await this.processMasterBlock(seqno);
}
this.lastMasterBlock = currentSeqno;
} catch (e) {
console.error('Indexer error:', e);
await this.sleep(5000);
}
}
}
private async processMasterBlock(seqno: number): Promise<void> {
const masterBlock = await this.client.getBlock(seqno);
// Обробляємо мастер-блок транзакції
await this.processShardTransactions(
-1, // workchain -1 = masterchain
masterBlock.shards
.filter(s => s.workchain === -1)
.flatMap(s => s.transactions)
);
// Отримуємо та обробляємо кожний шард
for (const shard of masterBlock.shards.filter(s => s.workchain === 0)) {
const transactions = await this.getShardTransactions(
shard.workchain,
shard.shard,
shard.seqno
);
await this.processShardTransactions(shard.workchain, transactions);
}
}
}
Транзакції TON: структура та парсинг
Транзакція у TON має:
-
in_msg— вхідне сообщение (причина виконання) -
out_msgs— вихідні сообщения (результат виконання) -
compute_ph— фаза вичислень (gas, exit code) -
action_ph— фаза дій (відправка вихідних сообщений)
interface IndexedTransaction {
hash: string;
lt: bigint; // logical time
account: string;
inMsgHash?: string;
value?: bigint; // nanoTON
opcode?: number; // перші 4 байти in_msg body
exitCode: number;
computeFee: bigint;
timestamp: number;
blockSeqno: number;
}
function parseTransaction(rawTx: RawTransaction): IndexedTransaction {
const inMsg = rawTx.inMessage;
let opcode: number | undefined;
if (inMsg?.body) {
// Перші 4 байти body — op code для Jetton/NFT/DEX протоколів
const slice = inMsg.body.beginParse();
if (slice.remainingBits >= 32) {
opcode = slice.loadUint(32);
}
}
return {
hash: rawTx.hash().toString('hex'),
lt: rawTx.lt,
account: rawTx.address.toString(),
value: inMsg?.info.type === 'internal' ? inMsg.info.value.coins : undefined,
opcode,
exitCode: rawTx.description.computePhase?.exitCode ?? 0,
computeFee: rawTx.totalFees.coins,
timestamp: rawTx.now,
blockSeqno: rawTx.blockSeqno,
};
}
Op-коди Jetton: індексація переводів токенів
Jetton Transfer має стандартний op-код 0x0f8a7ea5. Для індексації всіх Jetton переводів моніторимо транзакції на JettonWallet контрактах з цим op-кодом:
const JETTON_TRANSFER_OP = 0x0f8a7ea5;
const JETTON_TRANSFER_NOTIFICATION_OP = 0x7362d09c;
function isJettonTransfer(tx: IndexedTransaction): boolean {
return tx.opcode === JETTON_TRANSFER_OP;
}
function parseJettonTransferNotification(body: Cell): {
amount: bigint;
sender: Address;
forwardPayload: Cell | null;
} {
const slice = body.beginParse();
slice.loadUint(32); // op
slice.loadUint(64); // query_id
const amount = slice.loadCoins();
const sender = slice.loadAddress();
const hasPayload = slice.loadBit();
const forwardPayload = hasPayload ? slice.loadRef() : null;
return { amount, sender, forwardPayload };
}
Трасування транзакцій (Trace)
Для DEX аналітики, бот-детекції та аудиту потрібна повна трасування: одна вхідна транзакція породжує дерево викликів. TonAPI надає /v2/traces/{hash} — граф всіх пов'язаних транзакцій.
async function getTransactionTrace(txHash: string) {
const response = await fetch(
`https://tonapi.io/v2/traces/${txHash}`,
{ headers: { 'Authorization': `Bearer ${TONAPI_KEY}` } }
);
const trace = await response.json();
// trace.transaction + trace.children[] — рекурсивне дерево
return trace;
}
База даних: схема для TON індексера
CREATE TABLE ton_transactions (
hash CHAR(64) PRIMARY KEY,
lt BIGINT NOT NULL,
account VARCHAR(66) NOT NULL,
block_seqno INTEGER NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
in_msg_hash CHAR(64),
value NUMERIC(38,0), -- nanoTON
opcode INTEGER,
exit_code SMALLINT NOT NULL,
compute_fee NUMERIC(38,0),
raw_data BYTESEA -- оригінальний BOC для повторного парсинга
);
CREATE INDEX idx_ton_tx_account ON ton_transactions(account);
CREATE INDEX idx_ton_tx_timestamp ON ton_transactions(timestamp DESC);
CREATE INDEX idx_ton_tx_opcode ON ton_transactions(opcode) WHERE opcode IS NOT NULL;
-- Для Jetton індексації
CREATE TABLE jetton_transfers (
id BIGSERIAL PRIMARY KEY,
tx_hash CHAR(64) REFERENCES ton_transactions(hash),
jetton_master VARCHAR(66) NOT NULL,
from_address VARCHAR(66) NOT NULL,
to_address VARCHAR(66) NOT NULL,
amount NUMERIC(38,0) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL
);
Моніторинг індексера
Ключові метрики:
-
Отставання від masterchain —
current_seqno - indexed_seqnoмає бути < 5 - Кількість транзакцій в очереді — якщо растає, обработчик не встигає
- Помилки парсинга — некоректний BOC у raw_data
- RPC latency — час ответа TonAPI/TonCenter
Реорганізації у TON рідкісні, але можливі для свіжих блоків. Зберігаємо raw_data (BOC) кожної транзакції — при обнаруженні розбіжності перепарсимо з сирих даних без повторного звернення до мережи.







