Tất cả bài viết
11 phút đọc

Lịch sử giao dịch TON: lt, hash và độ sâu archive

Lịch sử giao dịch TON hoạt động ra sao: logical time (lt), hash và con trỏ giao dịch cuối. Cách agent đọc lịch sử qua MCP và vì sao độ sâu cần archive node.

lịch sử giao dịch TONlogical timelt hash TONget_transactionsarchive node TONMCP TON

Bạn đang cày đêm, và 3:47 sáng thì có tin nhắn bay tới: người dùng đã thanh toán đơn hàng bằng TON, còn bot thì không ghi nhận. Bạn mở explorer lên — tiền vẫn nằm đó, khoản chuyển đến hiện rõ ràng. Nghĩa là vấn đề không nằm ở blockchain, mà ở cách service của bạn đọc lịch sử giao dịch. Và tới lúc này mới vỡ lẽ: trên TON, lịch sử này được thiết kế hoàn toàn khác với những gì bạn đã quen trên Ethereum.

Việc theo dõi thanh toán trên TON nhìn qua thì tưởng đơn giản: cứ poll số dư rồi phản ứng khi nó thay đổi. Nhưng vào thực tế thì vỡ trận. Hai khoản thanh toán cùng số tiền trong cùng một phút — vậy là một hay hai lần? Người dùng trả USDT, còn số dư GRAM thì chẳng nhúc nhích. Bạn cần sao kê nửa năm, mà liteserver chỉ trả về vài giao dịch gần nhất, phần sâu hơn thì chịu.

Nếu bạn đang nối một AI agent vào TON hoặc đang viết backend đối soát thanh toán, ta đi thẳng vào việc: lịch sử giao dịch TON là gì, vì sao cần cặp lt + hash, vì sao độ sâu lại phụ thuộc vào archive node và làm sao để agent đọc được tất cả những thứ đó qua MCP.

Lịch sử giao dịch trong TON là gì và vì sao agent cần nó

Trên TON, mỗi tài khoản — ví, contract, jetton wallet — đều có chuỗi giao dịch của riêng nó. Mỗi giao dịch là kết quả của việc xử lý một message đến: nhận GRAM, chuyển jetton, gọi method của contract. Với một agent (Claude, Cursor, bất kỳ MCP client nào) đang nhận thanh toán hoặc đối soát sổ sách, lịch sử là nguồn đáng tin cậy duy nhất: số dư cho biết "bây giờ có bao nhiêu", còn lịch sử cho biết "chuyện gì đã xảy ra, vào lúc nào, từ ai và với số tiền bao nhiêu".

Những bài toán rất đời thường khiến agent cần tới lịch sử:

  • xác nhận rằng khoản thanh toán đã tới ("kiểm tra xem trong một giờ qua có giao dịch nào chuyển vào địa chỉ này không");
  • dựng sao kê cho một ví;
  • truy vết một giao dịch cụ thể theo định danh của nó;
  • đối chiếu các jetton đến (ví dụ USDT) với số tiền mong đợi.

Vấn đề là trên TON, kiểu yêu cầu "cho tôi các giao dịch số 100–120" không hoạt động. Ở đây không có một hệ đánh số block liên tục toàn cục để dựa vào đó mà "đưa ra giao dịch số N". Thứ tự được xác lập theo cách khác — bằng logical time.

Logical time (lt): vì sao TON không có cách đánh số block thông thường

Trên Ethereum thì mọi thứ đơn giản: có block số 19 000 000, bên trong là các giao dịch xếp theo thứ tự. Một bộ đếm toàn cục duy nhất, tăng đơn điệu, ai cũng nhìn vào cùng một con số.

TON là một hệ thống đa luồng: masterchain, các workchain, các shard tự chia tách và hợp nhất theo tải. Ở đây đơn giản là không tồn tại một "số block" duy nhất để sắp xếp tuyến tính mọi sự kiện trong mạng. Thay vào đó TON dùng logical time (lt, thời gian logic) — một bộ đếm tăng đơn điệu mà mạng lưới dùng để sắp thứ tự các sự kiện, message và giao dịch. Nó đảm bảo: nếu sự kiện A có ảnh hưởng tới sự kiện B thì lt(A) < lt(B).

Từ đó rút ra hệ quả thực tế: vị trí của một giao dịch không được xác định bằng số block, mà bằng cặp (lt, hash). lt lo phần thứ tự (cái nào trước), còn hash lo phần định danh chính xác một giao dịch cụ thể. Đứng riêng lẻ thì cả hai đều vô dụng khi cần truy vấn đúng một giao dịch: các đối tượng khác nhau có thể có lt gần nhau, còn nếu chỉ có mỗi hash thì liteserver không biết phải bắt đầu đọc từ đâu.

Hash giao dịch và cặp lt + hash: con trỏ tới giao dịch cuối của tài khoản

Hash của giao dịch là dấu vân tay mật mã của nó; trên TON nó được trả về ở dạng base64 hoặc hex. lt cho biết "đứng thứ mấy theo thứ tự", hash cho biết "chính xác là cái nào", bởi vì trên cùng một mốc lt, về lý thuyết vẫn có thể xảy ra nhập nhằng, và thiếu hash thì liteserver sẽ không trả giao dịch về. Hãy nhớ thật kỹ: cặp (lt, hash) bắt buộc phải đi cùng nhau. Chỉ lt hoặc chỉ hash là chưa đủ.

Lấy điểm khởi đầu ở đâu? Trạng thái tài khoản có lưu con trỏ tới giao dịch mới nhất — last_transaction_id, chính là cặp lt + hash đó (các trường last_trans_lt / last_trans_hash). Đây là "đầu" của danh sách, nơi bắt đầu hành trình duyệt ngược lịch sử.

Trong bộ tool MCP của TONNode, việc này do get_account_state đảm nhiệm — nó trả về trạng thái tài khoản, các cờ và con trỏ tới giao dịch cuối cùng.

get_transactions lật lịch sử ra sao: lt + hash và chuỗi prev_trans

Giờ tới điểm mấu chốt khiến việc lật lịch sử trở nên khả thi. Ngoài lt và hash của chính mình, mỗi giao dịch còn chứa hai trường: prev_trans_ltprev_trans_hash — tham chiếu tới giao dịch trước đó của cùng tài khoản này. Nghĩa là các giao dịch tạo thành một danh sách liên kết đơn chạy ngược về quá khứ; phần đầu chính là last_transaction_id lấy từ trạng thái.

get_transactions nhận vào:

  • account — địa chỉ tài khoản;
  • lthash khởi đầu — điểm bắt đầu đọc;
  • count — kích thước một lô.

Nó trả về một lô giao dịch, đi ngược từ điểm đã chỉ định. Logic duyệt theo trang:

  1. Lấy last_trans_lt / last_trans_hash từ get_account_state — đó là phần đầu.
  2. Gọi get_transactions(account, lt, hash, count) — nhận về một lô.
  3. Lấy prev_trans_ltprev_trans_hash của giao dịch cuối cùng trong lô.
  4. Gọi lại get_transactions với đúng cặp đó — đây chính là trang tiếp theo.
  5. Lặp lại cho tới khi prev_trans_lt bằng 0 (thời điểm tài khoản bắt đầu tồn tại) hoặc cho tới khi bạn chạm tới độ sâu cần thiết.

Phân trang trên TON hoạt động như vậy: không phải "trang 2", mà là "hãy đi tiếp từ cặp (lt, hash) này".

Block mới nhất so với lịch sử sâu: vì sao cần archive node

Đây chính là chỗ đa số vấp ngã. Một liteserver thông thường chỉ trả về các block mới và lịch sử gần đây của tài khoản: nó chỉ giữ một cửa sổ trạng thái có giới hạn — một số lượng block gần nhất nhất định — và khi mạng phình to dần, dữ liệu cũ bị đẩy dần ra khỏi đó. Cứ đi theo chuỗi prev_trans đủ sâu, tới một lúc nào đó liteserver sẽ đơn giản là ngừng trả giao dịch về: về mặt vật lý, chúng không còn nằm trong cửa sổ đó nữa. Khoản thanh toán vừa mới đến thì bạn thấy, còn giao dịch từ nửa năm trước thì không.

Đây không phải bug mà là thiết kế: giữ trọn vẹn đường đi của từng tài khoản kể từ genesis là chuyện tốn kém. Lịch sử sâu được lưu bởi archive node — node không xóa bỏ các block cũ mà giữ toàn bộ trạng thái mạng suốt chiều dài lịch sử.

Kết luận thực tế:

  • detect thanh toán, sao kê gần đây, "một giờ qua có tiền về không" — một liteserver thông thường là đủ;
  • audit toàn bộ một ví từ ngày đầu tiên — cần archive node.

Một chuyện nhức đầu riêng: các liteserver công cộng trong config toàn cục của TON. Chúng dùng chung và bị giới hạn: khi tải cao thường trả về not ready hoặc rơi vào timeout ADNL, và lịch sử sâu thì chúng cũng không có. Nếu bạn dính đúng lỗi not ready, đó là triệu chứng của một liteserver dùng chung đang quá tải — phân tích chi tiết có trong bài vì sao liteserver trả về not ready và cách khắc phục.

Nói thẳng về TONNode: archive node đang nằm trong roadmap và hiện đang đồng bộ — tôi không hứa với bạn rằng độ sâu archive là một tính năng đã sẵn sàng. Tính tới hôm nay, đây là việc đọc lịch sử mới và gần đây qua một endpoint được quản lý, chứ không phải chơi xổ số với các liteserver công cộng. Còn muốn archive đầy đủ từ genesis thì hãy chờ một thông báo riêng.

Cách để AI agent đọc lịch sử TON qua MCP

TONNode là một MCP server hosted dành cho TON, đúng 16 tool, và get_transactions nằm trong nhóm đọc dữ liệu. MCP (Model Context Protocol) là chuẩn để agent tự gọi tool mà bạn không phải ngồi ráp tay từng request ADNL. Không cần cài SDK, không cần dựng liteserver và cũng chẳng phải bóc tách TL-B — agent gọi thẳng tool.

Kết nối local miễn phí

Đầy đủ bộ tool đọc dữ liệu, config công khai, gói @tonnode/mcp — open source, MIT:

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

Gói này chạy trên giao thức ADNL gốc của TON, không qua lớp trung gian HTTP nào.

Endpoint hosted với key riêng

Muốn throughput được đảm bảo thì dùng endpoint hosted với key riêng:

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

Để đọc lịch sử, những tool sau sẽ có ích:

  • get_account_state — điểm khởi đầu của hành trình duyệt (last_trans_lt / last_trans_hash), trạng thái và các cờ của tài khoản;
  • get_transactions — chính các giao dịch, theo từng trang, dựa trên cặp (lt, hash);
  • parse_address — chuyển đổi địa chỉ offline giữa EQ / UQ / raw;
  • get_jetton_infoget_jetton_balance — cho lịch sử theo jetton.

Thực hành: detect thanh toán và duyệt lịch sử theo trang

Hãy dựng một kịch bản điển hình — "tiền đã về chưa và về bao nhiêu".

Prompt gửi cho agent

Qua tool ton, hãy lấy get_account_state cho EQC…myshop, sau đó gọi get_transactions từ last_trans_lt / last_trans_hash của nó, count 20. Tìm giao dịch chuyển đến 5 GRAM trong 10 phút gần nhất. Địa chỉ người gửi thì đưa về dạng EQ qua parse_address trước khi so sánh. Nếu cần lịch sử sâu hơn — lấy prev_trans_lt / prev_trans_hash của giao dịch cuối cùng rồi lặp lại get_transactions.

Agent sẽ tự đi lấy phần đầu của chuỗi, lật qua lô giao dịch và đối chiếu số tiền.

Pseudocode duyệt theo trang

Vẫn hành trình đó nhưng ở dạng pseudocode (các lời gọi chính là những MCP tool mà agent gọi):

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):   # message đến với số tiền/comment cần tìm
            return tx
    last = batch[-1]
    lt   = last.prev_trans_lt
    hash = last.prev_trans_hash   # thiếu hash thì không lấy được trang sau

Những chi tiết giúp bạn tiết kiệm hàng giờ debug

  • Địa chỉ trong các trường của giao dịch được trả về ở dạng raw (0:abcd…). Trước khi so sánh người gửi hay người nhận với địa chỉ EQ… quen thuộc, hãy đưa cả hai về cùng một định dạng qua parse_address — nếu không thì phép so sánh chuỗi sẽ không khớp, dù thực chất vẫn là cùng một địa chỉ. Chi tiết hơn về các định dạng — EQ, UQ và raw trong địa chỉ TON.
  • Thanh toán bằng USDT không hiện ra trên chuỗi giao dịch của ví GRAM. Jetton sống trên một jetton wallet riêng, địa chỉ của nó được tính on-chain (get_jetton_balance làm việc đó thay bạn và đưa ra số dư hiện tại). Muốn có lịch sử theo jetton thì hãy duyệt giao dịch của jetton wallet, chứ không phải của ví chính.
  • Hãy quy đổi số tiền raw qua decimals. Số tiền jetton được trả về theo đơn vị nhỏ nhất: muốn biến 5000000 thành 5 USDT như cách người ta vẫn đọc, hãy lấy decimals từ get_jetton_info — với USDT thì đó là 6, còn đa số jetton là 9. Nhầm một cái là lệch 1000 lần. Phân tích đầy đủ — cách bắt khoản thanh toán USDT đến trên TON.
  • Dedup theo (lt, hash), chứ không theo số tiền. Hai khoản thanh toán giống hệt nhau chỉ khác nhau ở cặp lt+hash — và đó chính là khóa idempotency.

Nếu ngoài lịch sử, agent còn cần lấy trạng thái hiện tại của contract thì đã có run_get_method — cách gọi get-method mà không cần SDK được trình bày trong bài đọc dữ liệu TON không cần SDK qua get-method.

Tóm lại

  • TON không có số block như Ethereum — vị trí của giao dịch được xác định bằng cặp (lt, hash): lt lo thứ tự, hash lo định danh, và cả hai bắt buộc phải đi cùng nhau.
  • Các giao dịch của một tài khoản là một danh sách liên kết qua prev_trans_lt / prev_trans_hash; bạn lật ngược về sau, bắt đầu từ last_transaction_id lấy trong get_account_state.
  • Liteserver thông thường trả về lịch sử mới và gần đây; còn độ sâu đầy đủ là việc của archive node.
  • Với jetton, đừng quên decimals lấy từ get_jetton_info (USDT = 6) và các địa chỉ raw cần chạy qua parse_address.

Không muốn vật lộn với những liteserver công cộng cứ trả về not ready? get_transactions có sẵn ngay từ đầu trên gói Hobby miễn phí — 60 request/phút, vĩnh viễn, không cần thẻ, key được cấp ngay sau khi đăng nhập.

Kết nối đọc lịch sử TON chỉ trong một phút → tonnode.io/dashboard?plan=hobby

Còn agent trên TON làm được gì nữa ngoài đọc lịch sử — cả 16 tool đều có trên trang công cụ của TONNode.

Cho agent của bạn quyền truy cập TON

16 công cụ MCP: đọc, swap phi lưu ký, cross-chain và ví. Gói miễn phí — 60 req/phút, không cần thẻ.