跳到主要内容

TON 开发手册

在产品开发过程中,关于如何与不同的合约进行交互,常常会出现各种问题。

此文档旨在收集所有开发者的最佳实践,并与大家分享。

如何转换(用户友好型 <-> 原始格式)、组装和从字符串提取地址?

在 TON 上,根据服务的不同,地址可以以两种格式出现:用户友好型原始格式

用户友好型: EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
原始格式: 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

用户友好型地址采用 base64 编码,而原始格式地址采用 hex 编码。在原始格式中,地址所在的工作链单独写在“:”字符之前,字符的大小写不重要。

要从字符串中获取地址,可以使用以下代码:

import { Address } from "@ton/core";


const address1 = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');
const address2 = Address.parse('0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e');

// toStrings 参数:urlSafe, bounceable, testOnly
// 默认值:true, true, false

console.log(address1.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address1.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

console.log(address2.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
console.log(address2.toRawString()); // 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

如何获得不同类型的地址并确定地址类型?

地址有三种格式:可弹回的(bounceable)不可弹回的(non-bounceable)测试网络的(testnet)。可以通过查看地址的第一个字母来轻松理解,因为它是第一个字节(8位)包含的标志根据 TEP-2

字母二进制形式可弹回测试网络
E00010001
U01010001
k10010001
011010001

值得注意的是,在 base64 编码中,每个字符代表了 6位 的信息。正如你所观察到的,在所有情况下,最后 2 位保持不变,所以在这种情况下,我们可以关注第一个字母。如果它们改变了,会影响地址中的下一个字符。

此外,在某些库中,你可能会注意到一个称为“url safe”的字段。事实是,base64 格式不是 url 安全的,这意味着在链接中传输这个地址时可能会出现问题。当 urlSafe = true 时,所有的 + 符号被替换为 -,所有的 / 符号被替换为 _。您可以使用以下代码获得这些地址格式:

import { Address } from "@ton/core";

const address = Address.parse('EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF');

// toStrings 参数:urlSafe, bounceable, testOnly
// 默认值:true, true, false

console.log(address.toString()); // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHFэ
console.log(address.toString({urlSafe: false})) // EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff+W72r5gqPrHF
console.log(address.toString({bounceable: false})) // UQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPuwA
console.log(address.toString({testOnly: true})) // kQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPgpP
console.log(address.toString({bounceable: false, testOnly: true})) // 0QDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPleK

如何发送标准 TON 转账消息?

要发送标准TON转账消息,首先需要打开您的钱包合约,之后获取您的钱包序列号(seqno)。只有完成这些步骤之后,您才能发送您的TON转账。请注意,如果您使用的是非V4版本的钱包,您需要将WalletContractV4重命名为WalletContract{您的钱包版本},例如,WalletContractV3R2。

import { TonClient, WalletContractV4, internal } from "@ton/ton";
import { mnemonicNew, mnemonicToPrivateKey } from "@ton/crypto";

const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
});

// 将助记词转换成私钥
let mnemonics = "word1 word2 ...".split(" ");
let keyPair = await mnemonicToPrivateKey(mnemonics);

// 创建钱包合约
let workchain = 0; // 通常你需要一个workchain 0
let wallet = WalletContractV4.create({ workchain, publicKey: keyPair.publicKey });
let contract = client.open(wallet);

// 创建转账
let seqno: number = await contract.getSeqno();
await contract.sendTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [internal({
value: '1',
to: 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N',
body: '转账示例内容',
})]
});

如何计算用户的 Jetton 钱包地址?

为了计算用户的Jetton钱包地址,我们需要调用jetton主合约的"get_wallet_address" get方法,并实际传入用户地址。对于这项任务,我们可以轻松使用JettonMaster的getWalletAddress方法或者自行调用主合约。

const { Address, beginCell } = require("@ton/core")
const { TonClient, JettonMaster } = require("@ton/ton")

const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});

const jettonMasterAddress = Address.parse('...') // 例如 EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE
const userAddress = Address.parse('...')

const jettonMaster = client.open(JettonMaster.create(jettonMasterAddress))
console.log(await jettonMaster.getWalletAddress(userAddress))

如何构建带有评论的 jetton 转账消息?

为了理解如何构建 token 转账消息,我们使用 TEP-74,该标准描述了 token 标准。需要注意的是,每个 token 可以有自己的 decimals,默认值为 9。因此,在下面的示例中,我们将数量乘以 10^9。如果小数位数不同,您需要乘以不同的值

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const jettonWalletAddress = Address.parse('put your jetton wallet address');
const destinationAddress = Address.parse('put destination wallet address');

const forwardPayload = beginCell()
.storeUint(0, 32) // 0 opcode 意味着我们有一个评论
.storeStringTail('Hello, TON!')
.endCell();

const messageBody = beginCell()
.storeUint(0x0f8a7ea5, 32) // jetton 转账的 opcode
.storeUint(0, 64) // query id
.storeCoins(toNano(5)) // jetton 数量,数量 * 10^9
.storeAddress(destinationAddress)
.storeAddress(destinationAddress) // 响应目的地
.storeBit(0) // 无自定义有效负载
.storeCoins(toNano('0.02')) // 转发金额
.storeBit(1) // 我们将 forwardPayload 作为引用存储
.storeRef(forwardPayload)
.endCell();

const internalMessage = internal({
to: jettonWalletAddress,
value: toNano('0.1'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

为了表示我们想要包含一个评论,我们指定了 32 个零位,然后写下我们的评论。我们还指定了响应目的地,这意味着关于成功转账的响应将发送到这个地址。如果我们不想要响应,我们可以指定 2 个零位而不是一个地址。

如何向 DEX(DeDust)发送交换(swap)信息?

DEX使用不同的协议来进行交易。在这个例子中,我们将与DeDust交互。

DeDust有两种交换路径:jetton <-> jetton 或 toncoin <-> jetton。每种都有不同的方案。要进行交换,您需要将jettons(或toncoin)发送到特定的vault并提供特殊的有效负载。以下是将jetton交换为jetton或jetton交换为toncoin的方案:

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

此方案显示了您的jettons转账消息(transfer#0f8a7ea5)的forward_payload中应包含的内容。

以及toncoin到jetton交换的方案:

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;
step#_ pool_addr:MsgAddressInt params:SwapStepParams = SwapStep;
step_params#_ kind:SwapKind limit:Coins next:(Maybe ^SwapStep) = SwapStepParams;
swap_params#_ deadline:Timestamp recipient_addr:MsgAddressInt referral_addr:MsgAddress
fulfill_payload:(Maybe ^Cell) reject_payload:(Maybe ^Cell) = SwapParams;

这是向toncoin vault转账的方案。

首先,您需要知道您要交换的jettons的vault地址或toncoin vault地址。可以通过合约Factoryget_vault_address方法来完成。您需要按照下面的方案传递一个切片:

native$0000 = Asset; // 用于ton
jetton$0001 workchain_id:int8 address:uint256 = Asset; // 用于jetton

此外,对于交换本身,我们需要pool地址 - 可以通过get方法get_pool_address获得。至于参数 - 根据上述方案的资产切片。作为响应,这两个方法都将返回所请求的vault / pool地址的切片。

这足以构建消息。

DEX使用不同的协议来执行它们的工作,我们需要了解关键概念和一些重要组件,还需要知道涉及我们正确执行交换过程的TL-B模式。在这个教程中,我们处理DeDust,TON中已构建完成的著名DEX之一。 在DeDust中,我们有一个抽象的Asset概念,它包括任何可交换的资产类型。对资产类型的抽象化简化了交换过程,因为资产类型无关紧要,即使是来自其他链的额外代币或资产,在这种方法中也能轻松覆盖。

以下是DeDust为Asset概念引入的TL-B模式。

native$0000 = Asset; // 用于ton

jetton$0001 workchain_id:int8 address:uint256 = Asset; // 用于任何jetton,地址指的是jetton主地址

// 即将推出,尚未实现。
extra_currency$0010 currency_id:int32 = Asset;

接下来,DeDust引入了三个组件,Vault,Pool和Factory。这些组件是合约或合约组,并且负责j交换过程的部分。Factory充当寻找其他组件地址(如vault和pool)的角色,并且还构建其他组件。 Vault负责接收转账消息,持有资产,只是通知相应的Pool,"用户A想要将100 X换成Y"。

另一方面,Pool负责根据预定义公式计算交换金额,通知负责资产Y的其他Vault,并告诉它支付给用户计算出的金额。 交换金额的计算基于数学公式,这意味着到目前为止我们有两种不同的pool,一种被称为Volatile,它基于常用的“恒定产品”公式运作:x y = k,另一种被称为Stable-Swap - 为近等值资产(例如USDT / USDC,TON / stTON)优化。它使用公式:x3 y + y3 * x = k。 所以对于每次交换,我们需要相应的Vault,它只需要实现一个为与特定资产类型交互而定制的特定API。DeDust有三种Vault的实现,Native Vault - 处理原生代币(Toncoin)。Jetton Vault - 管理jettons和Extra-Currency Vault(即将推出)- 为TON额外代币设计。

DeDust提供了一个特殊的SDk来处理合约、组件和API,它是用typescript编写的。 足够的理论,让我们设置环境以交换一个jetton和TON。

npm install --save @ton/core @ton/ton @ton/crypt

我们还需要引入DeDust SDK。

npm install --save @dedust/sdk

现在我们需要初始化一些对象。

import { Factory, MAINNET_FACTORY_ADDR } from "@dedust/sdk";
import { Address, TonClient4 } from "@ton/ton";

const tonClient = new TonClient4({
endpoint: "https://mainnet-v4.tonhubapi.com",
});
const factory = tonClient.open(Factory.createFromAddress(MAINNET_FACTORY_ADDR));
//Factory合约用于定位其他合约。

交换过程有一些步骤,例如,要用Jetton交换一些TON,我们首先需要找到相应的Vault和Pool 然后确保它们已部署。对于我们的示例TON和SCALE,代码如下:

import { Asset, VaultNative } from "@dedust/sdk";

//Native vault是用于TON的
const tonVault = tonClient.open(await factory.getNativeVault());
//我们使用factory来找到我们的原生代币(Toncoin)Vault。

下一步是找到相应的Pool,这里是(TON和SCALE)

import { PoolType } from "@dedust/sdk";

const SCALE_ADDRESS = Address.parse(
"EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE",
);
// SCALE jetton的主地址
const TON = Asset.native();
const SCALE = Asset.jetton(SCALE_ADDRESS);

const pool = tonClient.open(
await factory.getPool(PoolType.VOLATILE, [TON, SCALE]),
);

现在我们应该确保这些合约存在,因为向一个未激活的合约发送资金可能导致无法找回的损失。

import { ReadinessStatus } from "@dedust/sdk";

// 检查pool是否存在:
if ((await pool.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Pool (TON, SCALE) 不存在。");
}

// 检查vault是否存在:
if ((await tonVault.getReadinessStatus()) !== ReadinessStatus.READY) {
throw new Error("Vault (TON) 不存在。");
}

之后,我们可以发送带有TON数量的转账消息

import { toNano } from "@ton/core";
import { mnemonicToPrivateKey } from "@ton/crypto";

if (!process.env.MNEMONIC) {
throw new Error("需要环境变量MNEMONIC。");
}

const mnemonic = process.env.MNEMONIC.split(" ");

const keys = await mnemonicToPrivateKey(mnemonic);
const wallet = tonClient.open(
WalletContractV3R2.create({
workchain: 0,
publicKey: keys.publicKey,
}),
);

const sender = wallet.sender(keys.secretKey);

const amountIn = toNano("5"); // 5 TON

await tonVault.sendSwap(sender, {
poolAddress: pool.address,
amount: amountIn,
gasAmount: toNano("0.25"),
});

要用Y交换Token X,流程相同,例如,我们向Vault X发送X token的数量,Vault X接收我们的资产,持有它,并通知(X,Y)pool这个地址请求交换,然后Pool根据计算通知另一个Vault,这里Vault Y向请求交换的用户释放等价的Y。

资产之间的差异只是关于转账方法的问题,例如,对于jettons,我们使用转账消息将它们转入Vault,并附加特定的forward_payload,但对于原生代币,我们发送交换消息到Vault,附加相应数量的TON。

这是TON和jetton的模式:

swap#ea06185d query_id:uint64 amount:Coins _:SwapStep swap_params:^SwapParams = InMsgBody;

因此,每个vault和相应的Pool都针对特定的交换设计,并具有为特定资产量身定做的特殊API。

这是使用jetton SCALE交换TON的过程。jetton与jetton交换的过程是相同的,唯一的区别是我们应提供TL-B模式中描述的有效负载。

swap#e3a0d482 _:SwapStep swap_params:^SwapParams = ForwardPayload;
//寻找Vault
const scaleVault = tonClient.open(await factory.getJettonVault(SCALE_ADDRESS));
//寻找jetton地址
import { JettonRoot, JettonWallet } from '@dedust/sdk';

const scaleRoot = tonClient.open(JettonRoot.createFromAddress(SCALE_ADDRESS));
const scaleWallet = tonClient.open(await scaleRoot.getWallet(sender.address);

// 将jettons转移到Vault(SCALE)并附上相应的有效负载

const amountIn = toNano('50'); // 50 SCALE

await scaleWallet.sendTransfer(sender, toNano("0.3"), {
amount: amountIn,
destination: scaleVault.address,
responseAddress: sender.address, // 将gas返回给用户
forwardAmount: toNano("0.25"),
forwardPayload: VaultJetton.createSwapPayload({ poolAddress }),
});

如何使用 NFT 批量部署?

集合的智能合约允许在单个交易中部署多达250个NFT。但是,实际上,由于1ton的计算费用限制,这个最大数量在100到130个NFT之间。为此,我们需要在字典中存储有关新NFT的信息。

import { Address, Cell, Dictionary, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";
import { TonClient } from "@ton/ton";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const nftMinStorage = '0.05';
const client = new TonClient({
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC' // 对于Testnet
});
const ownersAddress = [
Address.parse('EQBbQljOpEM4Z6Hvv8Dbothp9xp2yM-TFYVr01bSqDQskHbx'),
Address.parse('EQAUTbQiM522Y_XJ_T98QPhPhTmb4nV--VSPiha8kC6kRfPO'),
Address.parse('EQDWTH7VxFyk_34J1CM6wwEcjVeqRQceNwzPwGr30SsK43yo')
];
const nftsMeta = [
'0/meta.json',
'1/meta.json',
'2/meta.json'
];

const getMethodResult = await client.runMethod(collectionAddress, 'get_collection_data');
let nextItemIndex = getMethodResult.stack.readNumber();

首先,我们假设每存储费用的TON最小金额为0.05。这意味着部署一个NFT后,集合的智能合约将向其余额发送这么多TON。接下来,我们获取新NFT所有者和内容的数组。之后,我们通过GET方法get_collection_data获取next_item_index

    let counter = 0;
const nftDict = Dictionary.empty<number, Cell>();
for (let index = 0; index < 3; index++) {
const metaCell = beginCell()
.storeStringTail(nftsMeta[index])
.endCell();
const nftContent = beginCell()
.storeAddress(ownersAddress[index])
.storeRef(metaCell)
.endCell();
nftDict.set(nextItemIndex, nftContent);
nextItemIndex++;
counter++;
}

/*
我们需要编写自定义的序列化和反序列化
函数来正确地在字典中存储数据,因为库中的
内置函数不适合我们的案例。
*/
const messageBody = beginCell()
.storeUint(2, 32)
.storeUint(0, 64)
.storeDict(nftDict, Dictionary.Keys.Uint(64), {
serialize: (src, builder) => {
builder.storeCoins(toNano(nftMinStorage));
builder.storeRef(src);
},
parse: (src) => {
return beginCell()
.storeCoins(src.loadCoins())
.storeRef(src.loadRef())
.endCell();
}
})
.endCell();

const totalValue = String(
(counter * parseFloat(nftMinStorage) + 0.015 * counter).toFixed(6)
);

const internalMessage = internal({
to: collectionAddress,
value: totalValue,
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

接下来,我们需要正确计算总交易费用。通过测试得知0.015值,但每个案例可能会有所不同。主要取决于NFT的内容,内容的增加导致更高的转发费(交付费)。

如何更改集合的智能合约所有者?

更改集合的所有者非常简单。要做到这一点,你需要指定 opcode = 3,任何 query_id,以及新所有者的地址:

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newOwnerAddress = Address.parse('put new owner wallet address');

const messageBody = beginCell()
.storeUint(3, 32) // 改变所有者的opcode
.storeUint(0, 64) // query id
.storeAddress(newOwnerAddress)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});
const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

如何更改集合智能合约中的内容?

要更改智能合约集合的内容,我们需要了解它是如何存储的。集合将所有内容存储在一个单一的cell中,其中包含两个cell:集合内容NFT 通用内容。第一个cell包含集合的元数据,而第二个cell包含NFT元数据的基本URL。

通常,集合的元数据存储格式类似于 0.json 并且继续递增,而这个文件之前的地址保持不变。正是这个地址应该存储在NFT通用内容中。

import { Address, beginCell, internal, storeMessageRelaxed, toNano } from "@ton/core";

async function main() {
const collectionAddress = Address.parse('put your collection address');
const newCollectionMeta = 'put url fol collection meta';
const newNftCommonMeta = 'put common url for nft meta';
const royaltyAddress = Address.parse('put royalty address');

const collectionMetaCell = beginCell()
.storeUint(1, 8) // 我们拥有链下元数据
.storeStringTail(newCollectionMeta)
.endCell();
const nftCommonMetaCell = beginCell()
.storeUint(1, 8) // 我们拥有链下元数据
.storeStringTail(newNftCommonMeta)
.endCell();

const contentCell = beginCell()
.storeRef(collectionMetaCell)
.storeRef(nftCommonMetaCell)
.endCell();

const royaltyCell = beginCell()
.storeUint(5, 16) // factor
.storeUint(100, 16) // base
.storeAddress(royaltyAddress) // 该地址将接收每次销售金额的5%
.endCell();

const messageBody = beginCell()
.storeUint(4, 32) // 更改内容的 opcode
.storeUint(0, 64) // query id
.storeRef(contentCell)
.storeRef(royaltyCell)
.endCell();

const internalMessage = internal({
to: collectionAddress,
value: toNano('0.05'),
bounce: true,
body: messageBody
});

const internalMessageCell = beginCell()
.store(storeMessageRelaxed(internalMessage))
.endCell();
}

main().finally(() => console.log("Exiting..."));

另外,我们需要在消息中包含版权信息,因为使用这个 opcode 时,它们也会改变。需要注意的是,不是一定要在所有地方指定新值。例如,如果只需要更改NFT通用内容,则所有其他值可以按照之前的指定。

处理蛇形Cells

有时候,在cell最多可以包含 1023位 的情况下,需要存储长字符串(或其他大型信息)。这种情况下,我们可以使用 蛇形cells。蛇形cells 是包含对另一个cell的引用的cell,而该cell又包含对另一个cell的引用,依此类推。

const TonWeb = require("tonweb");

function writeStringTail(str, cell) {
const bytes = Math.floor(cell.bits.getFreeBits() / 8); // 1字符 = 8位
if(bytes < str.length) { // 如果我们不能写下所有字符串
cell.bits.writeString(str.substring(0, bytes)); // 写入字符串的一部分
const newCell = writeStringTail(str.substring(bytes), new TonWeb.boc.Cell()); // 创建新cell
cell.refs.push(newCell); // 将新cell添加到当前cell的引用中
} else {
cell.bits.writeString(str); // 写下所有字符串
}

return cell;
}

function readStringTail(cell) {
const slice = cell.beginParse(); // 将cell转换为切片
if(cell.refs.length > 0) {
const str = new TextDecoder('ascii').decode(slice.array); // 解码 uint8array 为字符串
return str + readStringTail(cell.refs[0]); // 读取下一个cell
} else {
return new TextDecoder('ascii').decode(slice.array);
}
}

let cell = new TonWeb.boc.Cell();
const str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In euismod, ligula vel lobortis hendrerit, lectus sem efficitur enim, vel efficitur nibh dui a elit. Quisque augue nisi, vulputate vitae mauris sit amet, iaculis lobortis nisi. Aenean molestie ultrices massa eu fermentum. Cras rhoncus ipsum mauris, et egestas nibh interdum in. Maecenas ante ipsum, sodales eget suscipit at, placerat ut turpis. Nunc ac finibus dui. Donec sit amet leo id augue tempus aliquet. Vestibulum eu aliquam ex, sit amet suscipit odio. Vestibulum et arcu dui.";
cell = writeStringTail(str, cell);
const text = readStringTail(cell);
console.log(text);

这个示例将帮助你了解如何使用递归来处理这类cell。

如何解析账户的交易记录(转账、Jettons、NFTs)?

通过 getTransactions API方法可以获取到一个账户上的交易记录列表。它返回一个Transaction对象的数组,其中每个项都有很多属性。然而,最常用的字段有:

  • 初始化这笔交易的消息的Sender, Body和Value
  • 交易的哈希和逻辑时间(LT)

SenderBody 字段可用于确定消息的类型(常规转账、jetton转账、nft转账等等)。

以下是一个例子,展示了如何获取任何区块链账户上最近的5笔交易,根据类型解析它们,并在循环中打印出来。

import { Address, TonClient, beginCell, fromNano } from '@ton/ton';

async function main() {
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: '1b312c91c3b691255130350a49ac5a0742454725f910756aff94dfe44858388e',
});

const myAddress = Address.parse('EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN'); // 你想要从中获取交易记录的地址

const transactions = await client.getTransactions(myAddress, {
limit: 5,
});

for (const tx of transactions) {
const inMsg = tx.inMessage;

if (inMsg?.info.type == 'internal') {
// 我们在这里只处理内部消息,因为它们最常用
// 对于外部消息,一些字段是空的,但主要结构是相似的
const sender = inMsg?.info.src;
const value = inMsg?.info.value.coins;

const originalBody = inMsg?.body.beginParse();
let body = originalBody.clone();
if (body.remainingBits < 32) {
// 如果正文没有操作码:这是一条没有评论的简单消息
console.log(`Simple transfer from ${sender} with value ${fromNano(value)} TON`);
} else {
const op = body.loadUint(32);
if (op == 0) {
// 如果操作码是0:这是一条有评论的简单消息
const comment = body.loadStringTail();
console.log(
`Simple transfer from ${sender} with value ${fromNano(value)} TON and comment: "${comment}"`
);
} else if (op == 0x7362d09c) {
// 如果操作码是0x7362d09c:这是一个Jetton转账通知

body.skip(64); // 跳过query_id
const jettonAmount = body.loadCoins();
const jettonSender = body.loadAddressAny();
const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body;
let forwardPayload = originalForwardPayload.clone();

// 重要:我们必须验证这条消息的来源,因为它可能被伪造
const runStack = (await client.runMethod(sender, 'get_wallet_data')).stack;
runStack.skip(2);
const jettonMaster = runStack.readAddress();
const jettonWallet = (
await client.runMethod(jettonMaster, 'get_wallet_address', [
{ type: 'slice', cell: beginCell().storeAddress(myAddress).endCell() },
])
).stack.readAddress();
if (!jettonWallet.equals(sender)) {
// 如果发送者不是我们真正的JettonWallet:这条消息被伪造了
console.log(`FAKE Jetton transfer`);
continue;
}

if (forwardPayload.remainingBits < 32) {
// 如果forward payload没有操作码:这是一个简单的Jetton转账
console.log(`Jetton transfer from ${jettonSender} with value ${fromNano(jettonAmount)} Jetton`);
} else {
const forwardOp = forwardPayload.loadUint(32);
if (forwardOp == 0) {
// 如果forward payload的操作码是0:这是一次带有评论的简单Jetton转账
const comment = forwardPayload.loadStringTail();
console.log(
`Jetton transfer from ${jettonSender} with value ${fromNano(
jettonAmount
)} Jetton and comment: "${comment}"`
);
} else {
// 如果forward payload的操作码是其他:这是一条具有任意结构的消息
// 如果你知道其他操作码,你可以手动解析它,或者直接以十六进制形式打印
console.log(
`Jetton transfer with unknown payload structure from ${jettonSender} with value ${fromNano(
jettonAmount
)} Jetton and payload: ${originalForwardPayload}`
);
}

console.log(`Jetton Master: ${jettonMaster}`);
}
} else if (op == 0x05138d91) {
// 如果操作码是0x05138d91:这是一个NFT转账通知

body.skip(64); // 跳过query_id
const prevOwner = body.loadAddress();
const originalForwardPayload = body.loadBit() ? body.loadRef().beginParse() : body;
let forwardPayload = originalForwardPayload.clone();

// 重要:我们必须验证这条消息的来源,因为它可能被伪造
const runStack = (await client.runMethod(sender, 'get_nft_data')).stack;
runStack.skip(1);
const index = runStack.readBigNumber();
const collection = runStack.readAddress();
const itemAddress = (
await client.runMethod(collection, 'get_nft_address_by_index', [{ type: 'int', value: index }])
).stack.readAddress();

if (!itemAddress.equals(sender)) {
console.log(`FAKE NFT Transfer`);
continue;
}

if (forwardPayload.remainingBits < 32) {
// 如果forward payload没有操作码:这是一个简单的NFT转账
console.log(`NFT transfer from ${prevOwner}`);
} else {
const forwardOp = forwardPayload.loadUint(32);
if (forwardOp == 0) {
// 如果forward payload的操作码是0:这是一次带有评论的简单NFT转账
const comment = forwardPayload.loadStringTail();
console.log(`NFT transfer from ${prevOwner} with comment: "${comment}"`);
} else {
// 如果forward payload的操作码是其他:这是一条具有任意结构的消息
// 如果你知道其他操作码,你可以手动解析它,或者直接以十六进制形式打印
console.log(
`NFT transfer with unknown payload structure from ${prevOwner} and payload: ${originalForwardPayload}`
);
}
}

console.log(`NFT Item: ${itemAddress}`);
console.log(`NFT Collection: ${collection}`);
} else {
// 如果操作码是其他的:这是一条具有任意结构的消息
// 如果你知道其他操作码,你可以手动解析它,或者直接以十六进制形式打印
console.log(
`Message with unknown structure from ${sender} with value ${fromNano(
value
)} TON and body: ${originalBody}`
);
}
}
}
console.log(`Transaction Hash: ${tx.hash().toString('hex')}`);
console.log(`Transaction LT: ${tx.lt}`);
console.log();
}
}

main().finally(() => console.log('Exiting...'));

请注意,这个示例只涵盖了入站消息的最简单情况,其中只需在单个账户上获取交易记录即可。如果你想进一步深入并处理更复杂的交易和消息链,你应该考虑tx.outMessages字段。它包含了这笔交易所产生的输出消息的列表。为了更好地理解整个逻辑,你可以阅读这些文章: