Understanding TON Transactions: How to Track Transaction Results and Utilize TonClient

Allen Dev Blog
Coinmonks
4 min readJun 24, 2024

--

In recent discussions on the Telegram Dev Channel, a common question has been raised: “How can I determine the outcome of a transaction sent on the TON network?” Additionally, there are queries about the significance of the Boc returned by TonClient after sending a transaction and how to extract more useful information from it. This article aims to address these questions, helping you better understand TON.

TON Transactions

To begin, let’s understand what a transaction in TON entails. A TON transaction includes the following components:

• An initial inbound message that triggers the contract (with special trigger methods).

• Actions caused by the inbound message, such as updates to contract storage (optional).

• Generated outbound messages sent to other participants (optional).

As illustrated above, initiating a TON transfer transaction essentially involves sending a message to a wallet contract, which then performs the corresponding transfer operation based on the message content.

Preparing the Transaction Message

Before sending a transaction using TonConnect, we need to define a valid message. When using the JavaScript version of the TonConnect SDK, the transaction parameters are defined as follows:

export declare interface SendTransactionRequest {
validUntil: number; // Sending transaction deadline in unix epoch seconds.
network?: CHAIN; // The network (mainnet or testnet) where DApp intends to send the transaction.
from?: string; // The sender address in '<wc>:<hex>' format.
messages: {
address: string; // Receiver's address.
amount: string; // Amount to send in nanoTon.
stateInit?: string; // Contract specific data to add to the transaction.
payload?: string; // Contract specific data to add to the transaction.
}[];
}

To create a simple transfer transaction, we define the transaction parameters as shown below:

const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 600, // The transaction is valid for 10 minutes from now.
messages: [
{
address: "0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F", // destination address
amount: "20000000", // Toncoin in nanotons
},
],
};

For a transaction with a comment, TON requires a specific payload, which must be a Base64-encoded string composed of cells:

import { beginCell } from "@ton/ton";

const body = beginCell()
.storeUint(0, 32) // Write 32 zero bits to indicate a text comment will follow
.storeStringTail("Hello, TON!") // Write the text comment
.endCell();

const transaction = {
validUntil: Math.floor(Date.now() / 1000) + 600, // The transaction is valid for 10 minutes from now.
messages: [
{
address: "0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F", // destination address
amount: "20000000", // Toncoin in nanotons,
payload: body.toBoc().toString("base64"), // Payload with comment in body
},
],
};

For more message types and references, see [Message Build].

Result After Sending a Transaction

Now, let’s discuss how to wait for the transaction to complete and obtain the final transaction result.

Using TonConnect to send a transaction returns the following result:

const [tonConnectUi] = useTonConnectUI();
const result = await tonConnectUi.sendTransaction(transaction);

The sendTransaction function returns a SendTransactionResponse object containing a boc string:

export declare interface SendTransactionResponse {
boc: string; // Signed boc
}

The boc is the initial content that triggers the transaction. This boc is sent to the TON network for validation and execution. To wait for the transaction to complete, we use TonClient to query the transaction status and parse the latest transaction’s inbound message in Base64 format. If it matches our sent boc, the transaction is confirmed, and we can retrieve its details, including the TxHash.

const waitForTransaction = async (options, client) => {
const { hash, refetchInterval = 1000, refetchLimit, address } = options;

return new Promise((resolve) => {
let refetches = 0;
const walletAddress = Address.parse(address);
const interval = setInterval(async () => {
refetches += 1;
console.log("waiting transaction...");

const state = await client.getContractState(walletAddress);
if (!state || !state.lastTransaction) {
clearInterval(interval);
resolve(null);
return;
}

const { lt: lastLt, hash: lastHash } = state.lastTransaction;
const lastTx = await client.getTransaction(walletAddress, lastLt, lastHash);

if (lastTx && lastTx.inMessage) {
const msgCell = beginCell().store(storeMessage(lastTx.inMessage)).endCell();
const inMsgHash = msgCell.hash().toString("base64");
console.log("InMsgHash", inMsgHash);

if (inMsgHash === hash) {
clearInterval(interval);
resolve(lastTx);
}
}

if (refetchLimit && refetches >= refetchLimit) {
clearInterval(interval);
resolve(null);
}
}, refetchInterval);
});
};

In this code, getContractState retrieves the latest txHash and txLt, and getTransaction queries the latest transaction details. We parse the inbound message and compare its hash with our sent transaction’s hash to confirm the transaction completion. Other methods, such as getTransactions, can also be used to monitor on-chain transaction changes and match them with our sent transactions.

Conclusion

For relevant code examples, visit [Github], where I modified the official tonconnect SDK examples. You can access the demo [here].

TON, as a message-based asynchronous blockchain, requires a different approach to dApp development compared to Ethereum. This article aims to help you better understand TON’s transaction mechanism and how to obtain transaction results. If you found this article helpful, please give me some applause. If you enjoyed it, feel free to share and follow my latest articles, and follow me on X: https://x.com/0xAllenDev.

--

--

Allen Dev Blog
Coinmonks

Experienced software engineer in mobile dev, now in the Web3 industry. Sharing insights on AI, Web3, and more development topics X: https://x.com/0xAllenDev