在 TON 如何獲取發送交易後的結果 - 使用 TonConnect 為範例

最近在 Telegram Dev Channel 中蠻常看到有人提出這幾種問題,如果我在 TON 上發送了一筆交易,那麼我要如何知道這筆交易的結果呢?或者是使用 TonClient 發送交易後,TonClient 回傳的結果裡面的 Boc 又是什麼,我如何取得更有用的資訊呢?這篇文章就來解答這些問題,讓你更好的了解 TON。

TON 的 Transaction

在一開始我們先理解在 TON 的 Transaction 是什麼,TON 的 Transaction 包含了以下幾個部分:

  • 觸發合約的初始傳入訊息(存在特殊的觸發方式)
  • 傳入訊息引起的合約行為,例如對合約存儲的更新(可選)
  • 發送給其他參與者的生成輸出訊息(可選)

img

從上圖可以得知,如果我要發起一筆 TON 轉帳交易,本質上就是傳送一個 Message 給錢包合約,錢包合約根據這個 Message 內容做出相應的轉帳操作。

準備交易的 Message 內容

使用 TonConnect 發送交易前,我們需要定義有效的 Message 內容,使用 JS 版本的 TonConnect SDK 時,可以看到傳入的交易參數如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export declare interface SendTransactionRequest {
/**
* Sending transaction deadline in unix epoch seconds.
*/
validUntil: number;
/**
* The network (mainnet or testnet) where DApp intends to send the transaction. If not set, the transaction is sent to the network currently set in the wallet, but this is not safe and DApp should always strive to set the network. If the network parameter is set, but the wallet has a different network set, the wallet should show an alert and DO NOT ALLOW TO SEND this transaction.
*/
network?: CHAIN;
/**
* The sender address in '<wc>:<hex>' format from which DApp intends to send the transaction. Current account.address by default.
*/
from?: string;
/**
* Messages to send: min is 1, max is 4.
*/
messages: {
/**
* Receiver's address.
*/
address: string;
/**
* Amount to send in nanoTon.
*/
amount: string;
/**
* Contract specific data to add to the transaction.
*/
stateInit?: string;
/**
* Contract specific data to add to the transaction.
*/
payload?: string;
}[];
}

所以當我今天想要做一筆簡單的傳送交易時,我只需要構建下列的交易參數即可

1
2
3
4
5
6
7
8
9
10
11
const transaction = {
// The transaction is valid for 10 minutes from now, in unix epoch seconds.
validUntil: Math.floor(Date.now() / 1000) + 600,
messages: [
{
address:
"0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F", // destination address
amount: "20000000", //Toncoin in nanotons
},
],
};

如果我需要一個附上 Comment 的交易,在 TON 中,對於特定的 Custom Transaction,必須定義特定的 Payload,這個 Payload 必須是一個由 Cell 組成,並轉成 Base64 格式的字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { beginCell } from "@ton/ton";

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

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

更多的 Message 類型定義與參考,可以參考 Message Build

發送交易後的結果

接下來進入我們的主題,發送交易後,我要怎麼等待交易完成,得到最後的 Transaction Result 呢?
首先我們先看一下我們使用 TonConnect 發送交易後,回傳的結果

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

這個 sendTransaction 函數會回傳一個 SendTransactionResponse 物件,這個物件包含了一個 boc 字串

1
2
3
4
5
6
export declare interface SendTransactionResponse {
/**
* Signed boc
*/
boc: string;
}

這個 boc 其實就是我們前面提到的,他是一個 觸發交易傳入的初始內容,這個 boc 會被用來發送到 TON 網路上,進行交易的驗證與執行。在這中間我們需要等待交易完成,這時候我們可以使用 TonClient 來進行交易的查詢。並且需要去解析目前最新 Transaction 中他的 InMessage 的 Base64 是否與我們發送的 boc 相同,如果相同,則代表這筆交易是我們發送的交易,代表這筆交易已經完成,我們也可以取得這筆交易的內容,包含 TxHash 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const waitForTransaction = async (
options: WaitForTransactionOptions,
client: TonClient
): Promise<Transaction | null> => {
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 lastLt = state.lastTransaction.lt;
const lastHash = state.lastTransaction.hash;
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);
});
};

首先裡面的 getContractState 是為了獲取最後一筆的 txHash 以及 txLt,接著使用 getTransaction 去查詢最後一筆的交易內容,並且解析這筆交易的 InMessage,取得 InMessage 的 Hash,最後比對這個 Hash 是否與我們發送的交易相同,如果相同,則代表這筆交易已經完成,我們可以取得這筆交易的內容。當然你也可以使用其他方法,像是 getTransactions 等方式,去監聽鏈上交易的變化,然後根據最新結果的 InMessage 去比對是否是我們發送的交易。

結論

相關代碼範例可以參考 Github,這個是我 fork 官方 tonconnect sdk 的範例之上做了修改,Demo 可以訪問 Demo

Ton 身為一個以消息通訊為基礎的區塊鏈,且是異步的,所以我們在實作相關 dApp 的功能時,需要以一個不同於之前開發以太方的思維去思考,這篇文章希望能夠幫助你更好的了解 TON 的交易機制,以及如何獲取交易後的結果。