TON (Blockchain Platform)
Table of Contents
1. 简介
TON (The Open Network) 是一个支持 sharding 的区块链,它的签名算法采用 Ed25519,它的原生币为 TON,其精度为 9。
TON 采用 Actor model 来构建智能合约,一个智能合约就是一个 Actor,通过向智能合约(Actor)发送消息来改变智能合约(Actor)的状态。
1.1. RPC
TON 有好几套 RPC:
- TonCenter HTTP API (v2 API): https://toncenter.com/api/v2/
- TonCenter TON Index API (v3 API): https://toncenter.com/api/v3/
- TonHub HTTP API (v4 API): https://github.com/ton-community/ton-api-v4
- TON API (source): https://docs.tonconsole.com/tonapi/api-v2
2. 帐户
2.1. 地址推导
TON 中每个帐户都是一个智能合约(TON 中没有类似 Ethereum 中 EOA 帐户的概念),帐户地址就是智能合约的地址。智能合约地址是“智能合约代码及初始数据”的哈希值。
帐户合约有不同的版本,比如 V3R2/V4R2/V5R1 等等,参考:https://docs.ton.org/participate/wallets/contracts 。节 9.2.1, 9.2.2 中有 V4R2/V5R1 帐户合约地址的推导代码。
2.2. 地址类型
TON 有两种风格的地址:
- Raw 格式,由
workchain_id:contract_hash
组成,比如 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e - Base64 编码的 User-Friendly 格式,比如 EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrHF
User-Friendly 格式地址是 36 bytes(如表 1 所示)数据的 Base64 编码。
说明 | 占用字节数 |
---|---|
flag | 1 byte |
workchain_id | 1 byte |
smart-contract address inside the workchain | 32 bytes |
CRC16-CCITT of the previous 34 bytes | 2 bytes |
关于 User-Friendly 地址的第一个字节 flag 的含义如表 2 所示。
Binary form | Testnet-only | Bounceable | Address beginning |
---|---|---|---|
00010001 | no | yes | E... |
01010001 | no | no | U... |
10010001 | yes | yes | k... |
11010001 | yes | no | 0... |
注 1:使用工具 https://ton.org/address/ 可以对不同类型的地址进行转换。它们一般只有前缀和后面几个 CRC16-CCITT 字符不一样,节 9.2.1 有不同类型地址的例子。
注 2:Bounceable 地址的意思是如果出错(目标地址不存在也算是错误)了就返回资产给发送者。对于个人使用的钱包来说,推荐使用 U 开头的 Non-Bounceable 地址,它不会出现目标地址链上不存在而无法收款的情况。关于 Bounceable 和 Non-Bounceable 区别可以参考:https://docs.ton.org/develop/smart-contracts/guidelines/non-bouncable-messages
2.2.1. 地址实例
图 1 是私钥 4d31991a68e96f59e7da117c7de41a3d275837d7942ba1fa94e0dfba3d9cc7ca 对应的 9 个 V4R2 地址。
Figure 1: 私钥 4d31991a68e96f59e7da117c7de41a3d275837d7942ba1fa94e0dfba3d9cc7ca 对应的 9 个 V4R2 地址
3. 内存部署
3.1. Cell
在 TON 区块中,所有数据对象(比如 message, block, whole blockchain state, contract code and data 等)都保存在被称为 Cell 的结构中。一个 Cell 最多保存 1023 比特位数据,最多引用其它 4 个 Cell,如图 2 所示。 这意味着编程时的数据结构是受限的,比如无法分配一个任意大小的数组,又如 Hash 等数据结构其性能也会在数据量大时变得较差(因为它是基于 Cell 实现的,数据量大时不得不访问 Cell 树状结构的很多层才能找到数据)。
Figure 2: Cell 结构
Cell 的循环引用是被禁止的:对于任何 Cell,其后代 Cell 都不能将该 Cell 作为引用。因此,所有 Cell 构成了一个有向无环图 (DAG)。 如图 3(摘自:https://docs.ton.org/learn/overviews/cells )所示。
Figure 3: Cell 构成有向无环图,其中没有循环引用
3.1.1. Cell 的 refs descriptor
Cell 的 refs descriptor
已知某 Cell 的 refs descriptor 为
如果不考虑 Exotic Cell,那么 refs descriptor
3.1.2. Cell 序列化结果不唯一
Cell 序列化结果是不唯一的,在计算 Cell 的 Hash 时,需要计算 Standard Cell representation 的哈希。
3.1.3. Cell 的 bits descriptor
假设 Cell 有
由于 Cell 是比特数据,我们在序列化 Cell 时,会把它补齐到字节,规则是: 如果不足字节,则补 1 个比特位 1,再补零个或者多个(最多 6 个)比特位 0。 比如:
b= 1 bits_descriptor=1 data=0b1xxxxxxx serialized=0b11000000 b= 7 bits_descriptor=1 data=0b1111100x serialized=0b11111001 b= 8 bits_descriptor=2 data=0b10110001 serialized=0b10110001 (not changed) b= 9 bits_descriptor=3 data=0b11110111 1xxxxxxx serialized=0b11110111 11000000 b=15 bits_descriptor=3 data=0b01011100 1001001x serialized=0b01011100 10010011 b=16 bits_descriptor=4 data=0b01011100 10010010 serialized=0b01011100 10010010 (not changed) b=17 bits_descriptor=5 data=0b01011100 10010010 1xxxxxxx serialized=0b01011100 10010010 11000000
已经某 Cell 的 bits descriptor 为
- 根据
的奇偶性可以知道在序列化时是否有补齐操作。具体来说就是: 为奇数时有补齐操作,为偶数时没有补齐操作。所以当 是奇数时,在反序列化 Cell 时需要去掉这个补齐处理。比如, 上面例子中的 1/3/5(奇数),反序列化时需要从后往前删除一些比特位,直到遇到第 1 个比特位 1,把这个比特位 1 删除后就得到原 Cell 数据了。 - 序列化 Cell 后占用的字节数的计算公式为:
比如,前面 的例子,可以算出序列化这个 Cell 后占用了 3 字节。
4. 智能合约
前面提到过,在 TON 中,帐户本身就是智能合约。用户的帐户地址就是智能合约的地址。不过,用户不用直接编写部署帐户合约,而是通过钱包 App 来生成创建帐户,所以也不用关心智能合约。如果是开发者,往往需要更加深入地了解 TON 的智能合约。
TON 智能合约采用 FunC 语言来编写,然后会编译为 Fift 语言,最后会编译为 TVM 字节码。 链上部署时使用的是 TVM 字节码。
4.1. 最简单的合约(recv_internal 介绍)
TON 中智能合约之间所传递的消息被称为 Internal Message。要实现一个最简单的智能合约,我们实现函数 recv_internal
即可,它表示当前智能合约收到其它智能合约(比如用户的钱包合约)发来的 Internal Message 后如何处理。
下面是 TON 中最简单的智能合约实现:
;; simplest smart contract 最简单的智能合约 () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { ;; contract body }
它的三个参数的说明如下:
msg_value
表示这个消息中传来了多少原生币 TON。in_msg
是原始的 cell 类型,表示合约接收到的完整消息,包含消息发送者等信息。in_msg_body
是 slice 类型,这是一种更易读的类型,我们往往从in_msg_body
中读取消息。
其中修饰符 impure
的意思是这个函数不是一个只读函数,它会 1. 修改合约的存储数据;2. 或者会向其它合约发送消息;3. 或者会抛出异常。
4.1.0.1. recv_internal VS. recv_external
除前面介绍的 recv_internal
函数外,有时我们会看到 recv_external
函数,它们分别用于处理 Internal Message/External Message。
智能合约与智能合约之间传递的消息属于 Internal Message,而“外部”传给智能合约的消息属于 External Message。
总结:
- 只有 Wallet 合约(比如 V4R2/V5R1 等)才需要实现
recv_external
函数。注:Wallet 合约往往由 TON 核心团队开发; 对于非 Wallet 合约(比如前面介绍的 simplest smart contract),我们只需要实现
recv_internal
函数。因为我们都是通过 Wallet 合约和它交互的,所以根本不会直接发送 External Message 消息给它,所以不需要实现recv_external
函数。下面是和非 Wallet 合约(simplest smart contract)交互的过程:
user ---(external message)---> user's wallet smart contract ---(internal message)---> simplest smart contract
如果用户使用的是 TonKeeper App,那么 External Message 是 TonKeeper App 负责编码的。
4.2. 合约的存储(get_data/set_data)
每个合约都分配了一个 data cell,我们可以通过函数 get_data/set_data
来读取(或写入)data cell 的数据。需要说明的是:尽管一个 cell 的存储数据有限,但每个 cell 可以引用其它 4 个 cell,所以合约的存储可以认为是无限的。
节 4.3.1 会介绍 get_data/set_data
的具体用法。
4.3. 合约开发实践
4.3.1. 编写合约(Counter 合约)
下面介绍一个简单的 Counter 合约,合约的 FunC 源码为:
;; file counter.fc #include "imports/stdlib.fc"; ;; get_data/set_data 是在 stdlib.fc 中定义的 (int) load_data() inline { var ds = get_data().begin_parse(); ;; 使用 get_data() 读取合约的存储数据 var counter = ds~load_uint(64); ;; 关于 ~ 的说明可以参考 https://docs.ton.org/develop/func/statements#modifying-methods return (counter); } () save_data(int counter) impure inline { set_data(begin_cell() ;; 使用 set_data() 设置合约的存储数据 .store_uint(counter, 64) .end_cell()); } () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { var op = in_msg_body~load_uint(32); var (counter) = load_data(); if (op == 1) { save_data(counter + 1); } } int counter() method_id { var (counter) = load_data(); return counter; }
TVM 每个函数都有一个整数 Id 来进行标记,常规函数会从 1 开始连续编号。对于那些读取合约状态的 getter 函数,我们往往使用 method_id(<some_number>)
修饰符来改变这个整数 Id,当不指定 some_number
时,默认会把 (crc16(<function_name>) & 0xffff) | 0x10000
作为 getter 函数的整数 Id,参考 method_id 。
4.3.2. 编译合约
FunC 源码不能被直接编译为 TVM bytecode,需要先编译为 Fift 后,再从 Fift 编译为 TVM bytecode:
FunC --> Fift --> TVM bytecode
不过,使用 func-js 的话,用户不用关心上面细节,一个命令就可以从 FunC 源码编译到 TVM bytecode。
下面是把源码 counter.fc 编译为 counter.cell 的过程:
$ mkdir imports $ curl -o imports/stdlib.fc https://raw.githubusercontent.com/ton-blockchain/ton/master/crypto/smartcont/stdlib.fc $ yarn add @ton-community/func-js # 安装编译器 $ npx func-js counter.fc --boc ./counter.cell # 编译 counter.fc 为 counter.cell
注:由于上一节的源码 counter.fc 中引用了 imports/stdlib.fc,我们在编译它之前先要下载 https://github.com/ton-blockchain/ton/blob/master/crypto/smartcont/stdlib.fc ,并把它保存到子目录 imports 中。
4.3.3. 部署合约
上一步编译出来的 boc 文件 counter.cell 只是 code,再准备一个初始的 data,就可以部署合约了。
运行下面代码中的 runDeploy 方法可以部署 Counter 合约:
// yarn add tsx // npx tsx index.ts import { Contract, ContractProvider, Sender, Address, Cell, contractAddress, beginCell } from "@ton/core"; import { mnemonicToWalletKey } from "@ton/crypto"; import { TonClient, WalletContractV4 } from "@ton/ton"; import * as fs from "fs"; import { getHttpEndpoint } from "@orbs-network/ton-access"; const mnemonic = "fade ocean garage transfer extend tone bar arrest brick fever camp lucky correct bitter stem move output ability degree tomato miracle exhibit sheriff coil"; // your 24 secret words function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } class Counter implements Contract { static createForDeploy(code: Cell, initialCounterValue: number): Counter { const data = beginCell() .storeUint(initialCounterValue, 64) .endCell(); const workchain = 0; // deploy to workchain 0 const address = contractAddress(workchain, { code, data }); return new Counter(address, { code, data }); } constructor(readonly address: Address, readonly init?: { code: Cell, data: Cell }) {} // 部署合约 async sendDeploy(provider: ContractProvider, via: Sender) { await provider.internal(via, { value: "0.01", // send 0.01 TON to contract for rent bounce: false }); } // 读取合约中的状态(调用合约中的 counter 函数) async getCounter(provider: ContractProvider) { const { stack } = await provider.get("counter", []); // 调用合约中名为 counter 的函数 return stack.readBigNumber(); } // 通过指定 op = 1 来执行合约中增加计数器的逻辑 async sendIncrement(provider: ContractProvider, via: Sender) { const messageBody = beginCell() .storeUint(1, 32) // op (op #1 = increment) .storeUint(0, 64) // query id .endCell(); await provider.internal(via, { value: "0.002", // send 0.002 TON for gas body: messageBody }); } } async function runDeploy() { // initialize ton rpc client on testnet const endpoint = await getHttpEndpoint({ network: "testnet" }); console.log("endpoint:", endpoint) const client = new TonClient({ endpoint }); // prepare Counter's initial code and data cells for deployment const counterCode = Cell.fromBoc(fs.readFileSync("counter.cell"))[0]; // compilation output from step 6 const initialCounterValue = 1718007636746; // Date.now(); // initial value const counter = Counter.createForDeploy(counterCode, initialCounterValue); // exit if contract is already deployed console.log("contract address:", counter.address.toString()); if (await client.isContractDeployed(counter.address)) { return console.log("Counter already deployed"); } // open wallet v4 (notice the correct wallet version here) const key = await mnemonicToWalletKey(mnemonic.split(" ")); const wallet = WalletContractV4.create({ publicKey: key.publicKey, workchain: 0 }); console.log("wallet address:", wallet.address.toString()); if (!await client.isContractDeployed(wallet.address)) { return console.log("wallet is not deployed"); } // open wallet and read the current seqno of the wallet const walletContract = client.open(wallet); const walletSender = walletContract.sender(key.secretKey); const seqno = await walletContract.getSeqno(); console.log("seqno:", seqno); // send the deploy transaction const counterContract = client.open(counter); await counterContract.sendDeploy(walletSender); // wait until confirmed let currentSeqno = seqno; while (currentSeqno == seqno) { console.log("waiting for deploy transaction to confirm..."); await sleep(1500); currentSeqno = await walletContract.getSeqno(); } console.log("deploy transaction confirmed!"); return counter.address.toString() } // 调用 Counter 合约的只读方法,读取合约当前的计数器值 async function queryCounter(counterAddress: string) { // initialize ton rpc client on testnet const endpoint = await getHttpEndpoint({ network: "testnet" }); console.log("endpoint:", endpoint) const client = new TonClient({ endpoint }); // open Counter instance by address const counter = new Counter(Address.parse(counterAddress)); const counterContract = client.open(counter); // call the getter on chain const counterValue = await counterContract.getCounter(); return counterValue.toString() } // 向 Counter 合约发送消息,增加合约中的计数器 async function incrementCounter(counterAddress: string, mnemonic: string) { // initialize ton rpc client on testnet const endpoint = await getHttpEndpoint({ network: "testnet" }); console.log("endpoint:", endpoint) const client = new TonClient({ endpoint }); // open wallet v4 (notice the correct wallet version here) const key = await mnemonicToWalletKey(mnemonic.split(" ")); const wallet = WalletContractV4.create({ publicKey: key.publicKey, workchain: 0 }); if (!await client.isContractDeployed(wallet.address)) { return console.log("wallet is not deployed"); } // open wallet and read the current seqno of the wallet const walletContract = client.open(wallet); const walletSender = walletContract.sender(key.secretKey); const seqno = await walletContract.getSeqno(); // open Counter instance by address const counter = new Counter(Address.parse(counterAddress)); const counterContract = client.open(counter); // send the increment transaction await counterContract.sendIncrement(walletSender); // wait until confirmed let currentSeqno = seqno; while (currentSeqno == seqno) { console.log("waiting for transaction to confirm..."); await sleep(1500); currentSeqno = await walletContract.getSeqno(); } console.log("transaction confirmed!"); } // 部署 Counter 合约 await runDeploy() // 成功部署 Counter 合约后的合约地址 let counterAddress = "EQAY6oEpDbfwOiFS_fcgKYn0jhGtlDiixnwItkVcd-EiDB4G"; // 调用 Counter 合约的只读方法,读取合约当前的计数器值 let counterQuery = await queryCounter(counterAddress) console.log("counter value:", counterQuery) // 向 Counter 合约发送消息,增加合约中的计数器 await incrementCounter(counterAddress, mnemonic)
通过抓包我们可以知道,部署 Counter 合约时发送给节点的消息为:
{ "id": "1", "jsonrpc": "2.0", "method": "sendBoc", "params": { "boc": "te6cckEBBwEA7wAB4YgBGeJu2exoYFt42/TgJPcX9no4HRV4uZ4CasTxapumMGgA3nzCBhWMXKoNPwdZSiPcBld/m2M5+J9eU7D/CTZf7nhWlaedGfOUBnnLPFsnOrBn5CM9eTlU/ShTqNN5UQMYEU1NGLszNby4AAAAIAAMAQJnQgAMdUCUhtv4HRCpfvuQFMT6RwjWyhxRYz4EWyKuO/CRBhzEtAAAAAAAAAAAAAAAAAACMgIGART/APSkE/S88sgLAwIBYgQFADTQbCHTHzDtRNDTPzABwAGXpMjLP8ntVJEw4gARoTQx2omhpn5hABAAAAGQATwjClm2l7k=" } }
4.3.4. 修改合约状态(发送消息给合约)
通过节 4.3.3 中的 incrementCounter 方法,可以发送消息给合约,以修改合约状态。
通过抓包我们可以知道,修改 Counter 状态时发送给节点的消息为:
{ "id": "1", "jsonrpc": "2.0", "method": "sendBoc", "params": { "boc": "te6cckEBAgEAtQAB4YgBGeJu2exoYFt42/TgJPcX9no4HRV4uZ4CasTxapumMGgFUFPRxVFfIivqCdFwt6aEZ/73QQdsznZ24b2rk8xW0ON7XIDzMFqFfmyWYW9O7CSAfylcblTEJV0x/FDN+1sQIU1NGLszNcWIAAAAKAAMAQB+YgAMdUCUhtv4HRCpfvuQFMT6RwjWyhxRYz4EWyKuO/CRBhj0JAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAgfTvLQ==" } }
4.3.5. 调用合约的只读方法(runGetMethod)
通过节 4.3.3 中的 queryCounter 方法,可以调用合约的只读方法,它的本质上是调用 rpc runGetMethod。比如上面例子是调用合约 EQAY6oEpDbfwOiFS_fcgKYn0jhGtlDiixnwItkVcd-EiDB4G 的 counter 方法,它对应的 RPC 请求是:
$ curl -X POST -H "Content-Type: application/json" https://testnet.toncenter.com/api/v2/jsonRPC -d '{ "id": "1", "jsonrpc": "2.0", "method": "runGetMethod", "params": { "address": "EQAY6oEpDbfwOiFS_fcgKYn0jhGtlDiixnwItkVcd-EiDB4G", "method": "counter", "stack": [] } }' # 通过 rpc runGetMethod 可以调用合约中的只读方法 { "ok": true, "result": { "@type": "smc.runResult", "gas_used": 479, "stack": [ [ "num", "0x190013c230a" ] ], "exit_code": 0, "@extra": "1718007932.3977787:14:0.9130411615833036" }, "id": "1", "jsonrpc": "2.0" }
5. 消息管理
TON 是一个异步区块链,所谓合约调用就是向合约(Actor)发送 Message。
一个重要的事实: 一次合约调用不一定可以在一个区块中处理完,可能会涉及到多个区块!
In an asynchronous system you can't get a response from the destination smart contract in the same transaction. A contract call may take a few blocks to be processed, depending on the length of the route between source and destination.
摘自:https://docs.ton.org/develop/smart-contracts/guidelines/message-delivery-guarantees
5.1. Message Hash VS. Tx Hash
当我们通过节点 RPC sendBocReturnHash 向合约发送消息时,只能得到 Message Hash,这时还无法得到 Tx Hash。等链上处理完这个消息后,通过 RPC /api/v3/transactionsByMessage 可以查询指定 Message Hash 所关联的 Tx Hash。这个 Tx Hash(父 Tx Hash)又可能往其它合约发送 Message,从而产生新的 Tx Hash(子 Tx Hash)。通过 RPC /api/v3/adjacentTransactions 可以查询指定 Tx Hash 的子 Tx Hash 或者父 Tx Hash。
5.1.1. Message Hash 不唯一
我们发往 Basic Wallet 的 Message 中,都包含了 seqno 信息,由于它是递增的,所以一般来说 Message Hash 不会重复。
但其实 seqno 在某种情况(即使用 send mode 128+32 销毁当前帐户)可能重新从 0 开始,这时 Message Hash 就不唯一了:
1. Account 1 receive TON from other account 2. Account 1 send TON to account 2 (seqno == 1) 3. Account 1 use send mode (128+32) to send TON to other account, send mode (128+32) leads to account 1 be destroyed 4. Account 1 receive TON from other account 5. Account 1 send TON to account 2 (seqno == 1)
上面场景中,如果 step2 和 step 5 的交易详情是一模一样的,那么它们的 Message Hash 将相同。
6. 交易
TON 采用的是 Actor model 来构建智能合约,通过往合约发送消息来改变合约状态,交易在执行时也可以往其它的合约发送消息,如图 4 所示。
Figure 4: 往合约发送消息来改变合约状态
External Message 和 Internal Message(参考节 4.1.0.1)有个不同: External Message 无法附带 TON;Internal Message 往往附带 TON。
6.1. 交易的 5 个执行阶段
TON 交易执行时有 5 phases:
- Storage phase。在这个阶段里,区块链会向合约收取其存在期间内所欠缴的所有租金。所需要缴纳的租金,是以每比特、以及每秒的价格进行计算的。在两笔交易之间,所有这段时间以及所有这些比特都会被相乘。Storage fee 的公式为
storage_fee = (cells_count * cell_price + bits_count * bit_price) * time_delta / 2^16
。在交易开始执行之前,区块链就会开始从合约的余额里扣除 Storage fee,如果此时合约上的 TON 用完了,那么剩下的执行就会中止,合约就会被冻结。这就意味着,如果你碰到的合约已经超过了它的租金,那么它就不会再处理任何数据,只会处于闲置状态,直到它的所有者解冻,它的余额得到补充。一旦 Storage phase 结束,合约成功支付 Storage fee,就进入第二个阶段,即贷记阶段(Credit phase)。 - Credit phase。在这个阶段,收到的信息所附带的 TON 代币会被记入合约的余额。显然,这不适用于 External Message,因为 External Message 是数据,不可能附带 TON 代币。但 Internal Message 大多总是附带有 TON 代币,这些代币会记入余额。这个阶段很简单,下面讨论第三个阶段,即计算阶段(Compute phase)。
- Compute phase。这个阶段,TVM 会执行代码并验证每个操作,同时记录 Gas 的使用情况。因此,每次操作都会消耗一定量的 Gas,而作为开发者,你可以按照自己的意愿限制 Gas 的使用量。例如您可以将其限制在第二个阶段所附带的代币数量,这意味着所有 Gas 都将由向您发送信息的人支付。或者您也可以提高这个限额。假设你验证了这个消息来自一个经过验证的来源,然后你可以说,把我的 Gas 限额增加到我合约的全部余额,然后我可以随心所欲地花钱了。所以作为开发者,你的工作就是确保代码不会把钱花完。同时,TVM 虚拟机在执行代码并收取 Gas 费用的过程当中,会产生“输出操作”。如果你的钱没有用完,也没有违反 TVM 的规则,例如没有进行无效的算术运算,那么 TVM 就会完成执行,你就会得到一个“操作列表”。接下来是第四个阶段,即行动阶段(Action phase)。
- Action phase。行动阶段,处理第三个阶段得到的“操作列表”,同时记录新的状态并可能发送信息给其它合约。
- Bounce phase。反弹阶段,如果合约失败,而接收到的信息有一个标记显示“我是反弹回来的信息",就会发生这种情况。这意味着,在这个阶段,如果出现任何故障,而收到的信息中还有剩余的钱,那么合约就会创建一条发回给发件人的信息,将钱退回。这是一个安全功能,这个功能可以让人们在合约出现任何错误或故障的情况下取回大部分的资金。
6.2. 交易是否成功
TON 中判断交易是否成功是比较麻烦的。因为一个交易往往会关联到其它好几个交易(一起可称为交易树),当我们说交易是否成功时,往往是指“它们是否完成了既定目标”。
我们无法通过检测单一交易来判断是否成功,甚至我们也无法通过检测交易树每个交易来判断是否成功。
比如,交易 eabf7613ff86d3e4a9544354cdff1ed6bd59fe3211479139838753c29650f75b、还有交易 8faf2a89337e460a139edf0e003dc9716134bea20acbc9cb5dd4cabe8057121a,通过 RPC 查询所返回的 compute_ph/action 的 success 字段都是 true:
curl 'https://toncenter.com/api/v3/transactions?hash=eabf7613ff86d3e4a9544354cdff1ed6bd59fe3211479139838753c29650f75b' curl 'https://toncenter.com/api/v3/transactions?hash=8faf2a89337e460a139edf0e003dc9716134bea20acbc9cb5dd4cabe8057121a'
这并不能说明它们成功了,事实上这两个交易都是失败的,它们并没有正常完成转移代币这个操作。
那应该如何判断交易是否成功呢?
- 首先,你需要知道这个交易的功能是什么;
- 还需要知道,正常情况下这个交易对应的交易树中会涉及哪一些关键的子交易;
- 这些关键子交易是否都在交易树中,而且都是执行成功的。
7. Jetton 代币(类似 ERC20)
和 Ethereum 的 ERC20 类似,TON 也有自己的代币标准,即 TEP74(Jetton)。
在 TON 的 Jetton 方案中,每个人的 Jetton 代币余额不保存在某一个合约中,而是保存在每个人各自的 Jetton Wallet 中。 如果一个 Jetton 代币(假设名为 SHIB)有 2 个持有者(比如 Alison 和 Becky),那么一共会有 3 合约被部署, 如图 5 所示。
Figure 5: Jetton 代币 SHIB 的余额保存在每个 owner 的 Jetton Wallet 中
转帐 Jetton 代币的流程如图 6 所示。Tx dec72fda2925880602cd6b9a30f679d2a9aadbc2a30cbd15821cc54b5eb229f2 是一个真实的 USDT 转帐交易。
Figure 6: 转帐 Jetton 代币的流程
为什么 TON 不像 Ethereum ERC20 一样,把所以用户的代币余额放在一个合约中呢?这是因为:TON 是一个支持 Sharding 的区块链,如果把所有帐户的代币余额全部放在一个代币合约中(注:Ethereum 就是这样做的),当代币合约的帐户很多时数据量会很大,这不利于 Sharding。而且,由于 TON 的存储结构是 Cell,它并不适合保存大量的帐户代币余额信息(一个 Cell 最多只有 2023 比特位和 4 个引用,很难利用 Cell 构造出高效的能存储大量数据的 Hash 结构来保存帐户的代币余额)。
7.1. Transfer Notification 应用(STON.fi)
STON.fi 利用 Jetton 转帐时的发出的 Transfer Notification 消息实现了 SWAP 功能,如图 7(摘自 https://docs.ston.fi/docs/developer-section/architecture )所示。它演示了 Alice 想通过 USDT 换取 TOKEN 的过程,Alice 首先会把 USDT 转给 USDT Router Wallet,然后通过 Transfer Notification 消息告诉 Pool 合约进行 SWAP 操作,最后会把一定数量的 TOKEN 转移到 Alice 代币帐户中。
Figure 7: STON.fi 架构图
Tx 3aef38a6cbe5cc972c7254229c1edb203b24750717e1cdd93d7950d9b9113cef 是 UQCCb7r0UPYmYrj2M1M4BCsWUvBH6RzD3ExAu4Lk_ik-HkUF 通过 STON.fi 使用 1232.11 USDT 换取 165200 NOT 的实例。
7.2. Mintless Jettons
Mintless Jettons 是 Jettons 的扩展,它的主要作用是: 把使用 Merkle tree 空投 Jettons 的方法进行了标准化。 而且,用户都不用专门提交一个 claim/withdrawal 交易来获得空投,用户可以在往外转帐 Jettons 前,顺带先接收 Mintless Jettons 空投。
给别人发送 Mintless Jettons 时,如果带上了 custom_payload,那么会把 custom_payload 当作 merkle proof,校验通过后,会把你 Mintless Jettons 的余额加上 airdrop 数量,参考 send_jettons:
() send_jettons(slice in_msg_body, slice sender_address, int my_ton_balance, int msg_value, int fwd_fee) impure inline_ref { ;; see transfer TL-B layout in jetton.tlb int query_id = in_msg_body~load_query_id(); int jetton_amount = in_msg_body~load_coins(); slice to_owner_address = in_msg_body~load_msg_addr(); check_same_workchain(to_owner_address); (int status, int balance, slice owner_address, slice jetton_master_address) = load_data(); throw_unless(error::not_owner, equal_slices_bits(owner_address, sender_address)); cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, my_code(), merkle_root); builder to_wallet_address = calculate_jetton_wallet_address(state_init); slice response_address = in_msg_body~load_msg_addr(); cell custom_payload = in_msg_body~load_maybe_ref(); int mode = SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL; ifnot(custom_payload.null?()) { ;; 如果转帐 Jettons 时,传入了 custom_payload,就准备接收空投 slice cps = custom_payload.begin_parse(); int cp_op = cps~load_op(); throw_unless(error::unknown_custom_payload, cp_op == op::merkle_airdrop_claim); ;; 确保 custom_payload 正确(Merkle Proof 正确) throw_if(error::airdrop_already_claimed, status); ;; 确保之前没有领取过空投 cell airdrop_proof = cps~load_ref(); (int airdrop_amount, int start_from, int expired_at) = get_airdrop_params(airdrop_proof, merkle_root, owner_address); int time = now(); throw_unless(error::airdrop_not_ready, time >= start_from); ;; 确保领取空投时,已经过了开始时间 throw_unless(error::airdrop_finished, time <= expired_at); ;; 确保领取空投时,没有超过过期时间 balance += airdrop_amount; ;; 增加余额(即领取空投) status |= 1; ;; 标记空投已经领取过了 ......
7.2.1. 空投领取实例(给别人转帐)
下面以 HMSTR 为例,介绍一下用户 UQAA0V_U10jjuBYLTohBT82kLbhcXSVlG0i01GnS-uclEDb9 如何领取空投。
第 1 步、通过 RPC 查询 HMSTR 的基本信息,比如:
$ curl 'https://toncenter.com/api/v3/jetton/masters?address=EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo' { "jetton_masters": [ { "address": "0:09F2E59DEC406AB26A5259A45D7FF23EF11F3E5C7C21DE0B0D2A1CBE52B76B3D", "total_supply": "100000000000000000000", "mintable": true, "admin_address": "0:BBF13A702490AE3B853FDE3849B8E7DC1B5F548807A9158753046496A4AED324", "jetton_content": { "uri": "https://api.hamsterkombat.io/public/token/metadata.json" }, "jetton_wallet_code_hash": "Qx4UhbMmocYAuvAtw+TLYzmhU4qS68Tjzl2qcQD2x5s=", "code_hash": "hNCucX79I+7M6zpWXiX8X7LgZvA7zzZyp91prvo4nbI=", "data_hash": "0oF1KMYipn4Q6Y3gUfuIW/twRpsU517XUi3v1ZUYPPo=", "last_transaction_lt": "49511386000003" } ], "address_book": { "0:09F2E59DEC406AB26A5259A45D7FF23EF11F3E5C7C21DE0B0D2A1CBE52B76B3D": { "user_friendly": "EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo" }, "0:BBF13A702490AE3B853FDE3849B8E7DC1B5F548807A9158753046496A4AED324": { "user_friendly": "EQC78TpwJJCuO4U_3jhJuOfcG19UiAepFYdTBGSWpK7TJBCk" } } }
第 2 步、访问上一步输出中的 jetton_content.uri 字段,得到 HMSTR 的 Metadata,比如:
$ curl https://api.hamsterkombat.io/public/token/metadata.json { "name": "Hamster Kombat", "description": "Unleash your inner CEO", "symbol": "HMSTR", "decimals": "9", "image": "https://token.hamsterkombatgame.io/token/icon.png", "mintles_merkle_dump_uri": "", "custom_payload_api_uri":"https://proof.hamsterkombatgame.io/jettons/EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo/" }
第 3 步、如果发现第 2 步输出中有 custom_payload_api_uri,则它就是 Mintless Jetton,在后面加上 wallet/YOUR_ADDRESS 来查询 YOUR_ADDRESS 是否有领取空格的资格。比如查询 UQAA0V_U10jjuBYLTohBT82kLbhcXSVlG0i01GnS-uclEDb9 是否可以领取 HMSTR 空投:
$ curl 'https://proof.hamsterkombatgame.io/jettons/EQAJ8uWd7EBqsmpSWaRdf_I-8R8-XHwh3gsNKhy-UrdrPcUo/wallet/UQAA0V_U10jjuBYLTohBT82kLbhcXSVlG0i01GnS-uclEDb9' { "owner": "0:00d15fd4d748e3b8160b4e88414fcda42db85c5d25651b48b4d469d2fae72510", "jetton_wallet": "0:02b3f83ac2ae0d7c62cb0a7ca768e94657a98bd3e3d90188306ff4a3636b0740", "custom_payload": "te6ccgECMwEABGoAAQgN9gLWAQlGA0I0rXIU3k/T5YpIO61lji2qYaoATvD6VByERzLig6MRAB4CIgWBcAIDBCIBIAUGKEgBAcIMRlHGnrpvTT66fLjpTmtZBobfDAkqpOnhamrcwA44AB0iASAHCChIAQHu0h9XcUyOdXiieUpNhLvU+QkK3OZhIebPrsObEB0tGwAcIgEgCQooSAEBRRb2TYrfc+fFIkHZlKtfx/PNOEVE9E7K+6cxp4zCDpgAGiIBIAsMKEgBAf24vO/qWLtpYpESTatgDBB4IWaA8DUwott1Eezj5N7qABkiASANDihIAQHT2eI9+POS5jcnaIDaeVyYg5NFooH/R4RLFzr0L284SAAZIgEgDxAoSAEBKZjwA8vElMkIVUomgPPnhyqMXdtGFzG3lcBlPR8CC5sAFyIBIBESKEgBAZvdIzPBJ3b6DwCvtmdsC5oT7WhYQ2T+VJnLxTRBlXxVABYiASATFChIAQHOYSgKE72sIvhxqzrYKbvuH3xUtv0/dDngtrkfaauuvwAUKEgBAVP4eK5IF/Q0AQJar00+8f+pwAFTSulQfLZsECOEq5wMABMiASAVFihIAQF+7aKQ+6247T002Lox6jocxm0Ui/irflIIDkBFZHtEMwASIgEgFxgiASAZGihIAQH//INj5SjYFVzhoiiWISey+sz9J6ewPVMi6Uo4V249TwAQKEgBAemBygrPfYedlhIrEyJMbwvxDD8OIXCRve19XLT2ub4hAA8iASAbHCIBIB0eKEgBAcz7fixySseJ7zoFC/uyUwWLa1fj6TTLK+peIkPBAKY6AA8iASAfIChIAQGKZswtl9zvEuzwk8Qzf4f75ihb6aeOD+jNVQP1Jbgq3wAOIgEgISIoSAEBF7fS+FdTm4YZjW9vZcSZ5j9uf1uA5ZyaNxhfFX7PORIADChIAQEkcc3zomNXd7QhNeaxmk1s/8cRySTx9Ht8Pyo4oi5dfQAKIgEgIyQiASAlJihIAQGO11ksmuGq4iZ1QArXJpmbTxXQQyvhNbNOKCLtaGBKrAAJKEgBAQVjTYWreWWobP+VGHYiCXB+lBcsmdkLvS+4kpfu4QKwAAgiASAnKCIBICkqKEgBAbKs/0w9Ka4pQt8PIV691LFGbzGxFvEQM1QP17MQcZwTAAUoSAEBd0NEBJV9bjkxoOUZyK/qvvXWQaZ/y91vOuOU8sDzzYoABSIBICssKEgBAQ6EZBOlGiWsnM2+Smtjng9myfdQVj9BrfClvuAqhBm3AAMiASAtLihIAQHE5wMWDZ8fdU946XCDTQL9+/Gt32LRHBvSm8n2R1v3fQACIgEgLzAoSAEBuHxPfLRVcKPZuVpFWNJoqbOWpWqAyMs7cR+wtCvCTL0AASIBIDEyKEgBAckieAf17secQqQ9SJJSYKga8AMy/42o93TmsS499suvAAAAYbo1NdI47gWC06IQU/NpC24XF0lZRtItNRp0vrnJRBgI+KmlwdQAAZvUwoAAAbJjLII=", "state_init": "te6ccgEBAwEAjwACATQBAghCAg8a09ika9KDMh3eY5GV+3JgLpsxsXJ/7MJeLtwQlm30AMoAgAAaK/qa6Rx3AsFp0Qgp+bSFtwuLpKyjaRaajTpfXOSiEAAny5Z3sQGqyalJZpF1/8j7xHz5cfCHeCw0qHL5St2s9QjStchTeT9Plikg7rWWOLaphqgBO8PpUHIRHMuKDoxEHQ==", "compressed_info": { "amount": "2466022781045", "start_from": "1727344800", "expired_at": "1821952800" } }
在转帐 HMSTR(即构造 Jetton Transfer 消息)时,把 custom_payload 字段设置为上面链接中获得的 custom_payload 内容,即可领取空投。
Tx 104fb93b7ce8673936675702bb71c7b0135321cc56b91f8b582096c42f9a2bbc 是 UQAA0V_U10jjuBYLTohBT82kLbhcXSVlG0i01GnS-uclEDb9 往 UQAWA0M6Y4AzY8VN37gLBxIuYaWN4l_D3AZrRxoCkff8Cff7 转帐 500 HMSTR 的同时领取 HMSTR 空投(注:它领取的空投数量约 2466 HMSTR)的例子。
注:要判断某帐户是否领取过空投,可以检查这个帐户的 Jetton Wallet 合约的 is_claimed 方法。
7.2.2. 空投领取实例(给自己转帐数量零)
Tx 548c29c8d5bafcd0b5b0f48d2e11e07afc8e3f9a55aab5de8093354fe830b236 是 UQCh41gQP1A4I0lnAn6yAfitDAIYpXG6UFIXqeSz1TVxNOJ_ 领取 Mintless Points 空投的例子,它是通过给自己转帐数量为零(其它数量也行,只要不多于空投数量就行)的 Points 来实现的。
7.3. 部署 Jetton Wallet 的时机
User A 给 User B 转帐 Jetton 时,当 User B 的 Jetton Wallet 没有部署时,会部署 User B 的 Jetton Wallet。这个逻辑是 User A 的 Jetton Wallet 在发送 Jetton Internal Transfer 消息时处理的,参考:https://github.com/ton-blockchain/token-contract/blob/21e7844fa6dbed34e0f4c70eb5f0824409640a30/ft/jetton-wallet.fc#L61
对于 Mintless Jetton 来说,User A 给 User B 转帐 Jetton 时,可能 User A 的 Jetton Wallet 也没有部署。这是因为 Mintless Jetton 的空投领取和转帐是可以合二为一的,也就是在转帐前自动先领取空投。对于这种情况,部署 User A 的 Jetton Wallet 所需要的 State Init 信息,可以从节 7.2.1 第 3 步中返回数据中的 state_init 字段得到。即在构造 Jetton Transfer(不是 Jetton Internal Transfer)消息时,需要带上 state init,这样才可以部署 User A 的 Jetton Wallet。
8. Sharding
分片概念起源于 Database 设计,它的思路是将一个逻辑数据集拆分并分布到多个不共享任何内容且可以部署在多个服务器上的数据库中。简而言之,分片允许水平扩展:将数据拆分为可以并行处理的不同独立部分。
在 TON 中,当负载高时,会自动按照 Account 的前缀进行 Splitting(相同前缀的 Account 位于同一个 Sharding);如果负载正常了,会自动 Merging 以减少分片数量。
9. 附录
9.1. TON 助记词(非 BIP39)
TON 既没有采用 BIP39 方案来生成助记词和推导 Seed;也没有采用 BIP32 方案从 Seed 推导私钥。TON 有自己的方案,下面将介绍它们。
9.1.1. 生成助记词
TON 默认使用 24 个助记词,尽管它的生成规则和 BIP39 不一样,但它的单词表和 BIP39 的单词表相同。
TON 的助记词是直接从单词表中随机产生的,其流程如图 8 所示。
Figure 8: TON Mnemonics 生成规则
其中 isBasicSeed/isPasswordSeed 的伪代码如下:
function mnemonicToEntropy(mnemonicArray, password) { return hmac_sha512(mnemonicArray.join(' '), password && password.length > 0 ? password : ''); } function isBasicSeed(entropy) { const seed = pbkdf2_sha512(key = entropy, salt = 'TON seed version', iteration = 390, keylen = 64); // 390: Math.floor(100000 / 256) return seed[0] == 0; } function isPasswordSeed(entropy) { const seed = pbkdf2_sha512(key = entropy, salt = 'TON fast seed version', iteration = 1, keylen = 64); return seed[0] == 0; }
9.1.1.1. 优点
TON 助记词和 BIP39 标准的助记词相比,有两个优点:
- 从 TON 助记词本身,就能知道它是否关联了 Password;
- 如果 TON 助记词关联了 Password,能检查 Password 是否和助记词匹配。
9.1.2. 从助记词推导 Seed
从 TON 助记词推导 Seed 的伪代码如下:
// TON 助记词推导 Seed entropy = hmac_sha512(mnemonicArray.join(' '), password && password.length > 0 ? password : '') seed = pbkdf2_sha512(key = entropy, salt = 'TON default seed', iteration = 100000, keylen = 64);
和 BIP39 相比,从 TON 助记词推导 Seed 这个步骤有两个不同点:
- TON 助记词推导 Seed 时多了一个 sha512 计算步骤;
- pdkdf2 的参数不同。
BIP39: Mnemonic -> pdkdf2 -> seed pdkdf2 的参数:iteration = 2048, keylen = 64, salt = 'mnemonic' + '{passkey}' // passkey may be empty TON: Mnemonic -> sha512 -> pdkdf2 -> seed // pdkdf2 之前多了 sha512 过程 pdkdf2 的参数:iteration = 100000, keylen = 64, salt = 'TON default seed'
9.1.3. 从 Seed 推导私钥
TON 中,从 Seed 推导私钥时,没有使用 Derivation Path。直接把 Seed 的前 32 字节(即 BIP32 中的 Master private key)作为 Ed25519 私钥。
9.2. 推导帐户合约地址
9.2.1. V4R2 帐户合约地址
下面 Javascript 代码演示了如何推导 V4R2 帐户合约的地址:
import core_1 from "@ton/core"; // "@ton/core": "^0.56.3", import { mnemonicToSeed, mnemonicToPrivateKey } from "@ton/crypto"; // "@ton/crypto": "^3.2.0" import { storeStateInit } from "@ton/core/dist/types/StateInit.js"; import { Address } from "@ton/core/dist/address/Address.js"; // Convert mnemonics to private key,TON 推荐使用 24 个的助记词 let mnemonics = "reveal pair october art leave weekend setup boss image manual side enact flee enter hazard pride history lazy under fox beauty supply allow goat".split(" "); console.log("mnemonics", mnemonics); const seed = (await mnemonicToSeed(mnemonics, 'TON default seed', "")).slice(0, 32); // TON SDK 使用 TON default seed,且未使用 Path,直接把 Master private key 的前 32 字节为 Ed25519 私钥 console.log("private key", seed.toString('hex')); // d734d92abb327736828db428b4792242eeeb3418689e61c50f08cffff106ab7a let keyPair = await mnemonicToPrivateKey(mnemonics); // keyPair.secretKey 的前 32 字节为 Ed25519 私钥,后 32 字节为 Ed25519 公钥 console.log("private key and public key", keyPair.secretKey.toString('hex')); // d734d92abb327736828db428b4792242eeeb3418689e61c50f08cffff106ab7af62575b800be30f1ca0435fbb470be915deb5bd698debb5a29b19cd58ccff975 console.log("public key", keyPair.publicKey.toString('hex')); // f62575b800be30f1ca0435fbb470be915deb5bd698debb5a29b19cd58ccff975 let workchain = 0; // Usually you need a workchain 0 let walletId = 698983191 + workchain // walletId: any integer since multiple wallets can be deployed by the same key, the value 698983191 is used by standard wallets as the primary ID, using it will allow seamless import of the wallet into a third-party wallet app in the future let publicKey = keyPair.publicKey // 智能合约地址(帐户地址)完全由智能合约 code,以及初始化时使用的 data 决定 // The WalletV4R2 contract code, from https://github.com/toncenter/tonweb/blob/master/src/contract/wallet/WalletSources.md let code = core_1.Cell.fromBoc(Buffer.from('B5EE9C72410214010002D4000114FF00F4A413F4BCF2C80B010201200203020148040504F8F28308D71820D31FD31FD31F02F823BBF264ED44D0D31FD31FD3FFF404D15143BAF2A15151BAF2A205F901541064F910F2A3F80024A4C8CB1F5240CB1F5230CBFF5210F400C9ED54F80F01D30721C0009F6C519320D74A96D307D402FB00E830E021C001E30021C002E30001C0039130E30D03A4C8CB1F12CB1FCBFF1011121302E6D001D0D3032171B0925F04E022D749C120925F04E002D31F218210706C7567BD22821064737472BDB0925F05E003FA403020FA4401C8CA07CBFFC9D0ED44D0810140D721F404305C810108F40A6FA131B3925F07E005D33FC8258210706C7567BA923830E30D03821064737472BA925F06E30D06070201200809007801FA00F40430F8276F2230500AA121BEF2E0508210706C7567831EB17080185004CB0526CF1658FA0219F400CB6917CB1F5260CB3F20C98040FB0006008A5004810108F45930ED44D0810140D720C801CF16F400C9ED540172B08E23821064737472831EB17080185005CB055003CF1623FA0213CB6ACB1FCB3FC98040FB00925F03E20201200A0B0059BD242B6F6A2684080A06B90FA0218470D4080847A4937D29910CE6903E9FF9837812801B7810148987159F31840201580C0D0011B8C97ED44D0D70B1F8003DB29DFB513420405035C87D010C00B23281F2FFF274006040423D029BE84C600201200E0F0019ADCE76A26840206B90EB85FFC00019AF1DF6A26840106B90EB858FC0006ED207FA00D4D422F90005C8CA0715CBFFC9D077748018C8CB05CB0222CF165005FA0214CB6B12CCCCC973FB00C84014810108F451F2A7020070810108D718FA00D33FC8542047810108F451F2A782106E6F746570748018C8CB05CB025006CF165004FA0214CB6A12CB1FCB3FC973FB0002006C810108D718FA00D33F305224810108F459F2A782106473747270748018C8CB05CB025005CF165003FA0213CB6ACB1F12CB3FC973FB00000AF400C9ED54696225E5', 'hex'))[0]; let data = (0, core_1.beginCell)() .storeUint(0, 32) // Seqno .storeUint(walletId, 32) .storeBuffer(publicKey) // Public key,用户的 Ed25519 公钥在初始化合约的 data 中 .storeBit(0) // Empty plugins dict .endCell(); // 这个初始化 data 的结构可参考 https://github.com/ton-blockchain/wallet-contract/blob/4111fd9e3313ec17d99ca9b5b1656445b5b49d8f/func/wallet-v4-code.fc#L78 let init = { code, data }; let walletContract = (0, core_1.beginCell)() .store(storeStateInit(init)) .endCell(); let rawAddress = `${workchain}` + ":" + walletContract.hash().toString('hex') console.log("TON raw address", rawAddress) // 0:794b27615f061ba5cd6a3fc45e77fa5fe17ba23a436fb4d88a2d9ce1c79e285c let address = Address.parseRaw(rawAddress) // 可读地址是 base64 编码,它所编码的数据固定为 36 bytes: 1 byte flag + 1 byte workchain + 32 bytes hash + 2 byte crc16 console.log("TON address", address.toString({ urlSafe: true, bounceable: true, testOnly: true })) // kQB5SydhXwYbpc1qP8Red_pf4XuiOkNvtNiKLZzhx54oXCRw console.log("TON address", address.toString({ urlSafe: true, bounceable: true, testOnly: false })) // EQB5SydhXwYbpc1qP8Red_pf4XuiOkNvtNiKLZzhx54oXJ_6 console.log("TON address", address.toString({ urlSafe: true, bounceable: false, testOnly: true })) // 0QB5SydhXwYbpc1qP8Red_pf4XuiOkNvtNiKLZzhx54oXHm1 console.log("TON address", address.toString({ urlSafe: true, bounceable: false, testOnly: false })) // UQB5SydhXwYbpc1qP8Red_pf4XuiOkNvtNiKLZzhx54oXMI_ console.log("TON address", address.toString({ urlSafe: false, bounceable: true, testOnly: true })) // kQB5SydhXwYbpc1qP8Red/pf4XuiOkNvtNiKLZzhx54oXCRw console.log("TON address", address.toString({ urlSafe: false, bounceable: true, testOnly: false })) // EQB5SydhXwYbpc1qP8Red/pf4XuiOkNvtNiKLZzhx54oXJ/6 console.log("TON address", address.toString({ urlSafe: false, bounceable: false, testOnly: true })) // 0QB5SydhXwYbpc1qP8Red/pf4XuiOkNvtNiKLZzhx54oXHm1 console.log("TON address", address.toString({ urlSafe: false, bounceable: false, testOnly: false })) // UQB5SydhXwYbpc1qP8Red/pf4XuiOkNvtNiKLZzhx54oXMI/
9.2.2. V5R1 帐户合约地址
下面 Javascript 代码演示了如何推导主网 V5R1 帐户合约的地址(如果要生成测试网 V5R1 帐户合约的地址,则需要修改里面的 networkGlobalId 为 -3):
import core_1 from "@ton/core"; // "@ton/core": "^0.56.3", import {mnemonicToSeed, mnemonicToPrivateKey, mnemonicNew} from "@ton/crypto"; // "@ton/crypto": "^3.2.0" import { storeStateInit } from "@ton/core/dist/types/StateInit.js"; import { Address } from "@ton/core/dist/address/Address.js"; // Convert mnemonics to private key,TON 推荐使用 24 个的助记词 //let mnemonics = await mnemonicNew(); let mnemonics = "reveal pair october art leave weekend setup boss image manual side enact flee enter hazard pride history lazy under fox beauty supply allow goat".split(" "); const seed = (await mnemonicToSeed(mnemonics, 'TON default seed', "")).slice(0, 32); // TON SDK 使用 TON default seed,且未使用 Path,直接把 Master private key 的前 32 字节为 Ed25519 私钥 console.log("private key", seed.toString('hex')); // d734d92abb327736828db428b4792242eeeb3418689e61c50f08cffff106ab7a let keyPair = await mnemonicToPrivateKey(mnemonics); // keyPair.secretKey 的前 32 字节为 Ed25519 私钥,后 32 字节为 Ed25519 公钥 console.log("private key and public key", keyPair.secretKey.toString('hex')); // d734d92abb327736828db428b4792242eeeb3418689e61c50f08cffff106ab7af62575b800be30f1ca0435fbb470be915deb5bd698debb5a29b19cd58ccff975 console.log("public key", keyPair.publicKey.toString('hex')); // f62575b800be30f1ca0435fbb470be915deb5bd698debb5a29b19cd58ccff975 let workchain = 0; // Usually you need a workchain 0 let publicKey = keyPair.publicKey function walletIdV5R1(networkGlobalId, workchain, subWalletNumber) { // * schema: // * wallet_id -- int32 // * wallet_id = global_id ^ context_id // * context_id_client$1 = wc:int8 wallet_version:uint8 counter:uint15 // * context_id_backoffice$0 = counter:uint31 let contextId = (0, core_1.beginCell)() .storeUint(1, 1) // first bit 1: means choose context_id_client$1, rather than context_id_backoffice$0 .storeInt(workchain, 8) // workchain .storeUint(0, 8) // walletVersion, 0 means v5r1. see https://github.com/ton-org/ton/blob/f9842909ac0e7d6f66d055dd18a4c41ec3416c02/src/wallets/v5r1/WalletV5R1WalletId.ts#L59 .storeUint(subWalletNumber, 15) // subWalletNumber .endCell().beginParse().loadInt(32); return Number(BigInt(networkGlobalId) ^ BigInt(contextId)) } // V4R2 帐户合约,从技术上说,可能遭受主网和测试之间的 Tx 重放攻击,因为它的 wallet_id 是固定的,而且打包 Tx 时没有包含网络特定的数据 // V5R1 帐户合约,主网的 wallet_id 和测试网的 wallet_id 是不同的,因为计算 wallet_id 时包含了 networkGlobalId let networkGlobalId = -239 // -239 for TON Mainnet, -3 for TON Testnet。注:如果是生成测试网的地址,这里请修改为 -3 let subWalletNumber = 0 let walletId = walletIdV5R1(networkGlobalId, workchain, subWalletNumber) // 智能合约地址(帐户地址)完全由智能合约 code,以及初始化时使用的 data 决定 // The WalletV5R1 contract code, from https://github.com/ton-blockchain/wallet-contract-v5/blob/88557ebc33047a95207f6e47ac8aadb102dff744/build/wallet_v5.compiled.json let code = core_1.Cell.fromBoc(Buffer.from('b5ee9c7241021401000281000114ff00f4a413f4bcf2c80b01020120020d020148030402dcd020d749c120915b8f6320d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e011e20d70b1f82107369676ebaf2e08a7f0f01e68ef0eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109a28945f0adb31e1f2c087df02b35007b0f2d0845125baf2e0855036baf2e086f823bbf2d0882292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde70db3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0b4d6c35e', 'hex'))[0]; let data = (0, core_1.beginCell)() .storeBit(1) // is signature auth allowed .storeUint(0, 32) // Seqno .storeUint(walletId, 32) // https://github.com/tonkeeper/tonkeeper-ton/blob/f8146946d7e83de6300d384ed399fd739f9e652b/src/wallets/v5r1/WalletV5R1WalletId.ts#L21 .storeBuffer(publicKey) // Public key,用户的 Ed25519 公钥在初始化合约的 data 中 .storeBit(0) // Empty plugins dict .endCell(); let init = { code, data }; let walletContract = (0, core_1.beginCell)() .store(storeStateInit(init)) .endCell(); let rawAddress = `${workchain}` + ":" + walletContract.hash().toString('hex') console.log("TON raw address", rawAddress) // 0:46d8bc093ed54a5e5507a17106988164b1b2da09982b043b440c3d0cc65dbea1 let address = Address.parseRaw(rawAddress) // 可读地址是 base64 编码,它所编码的数据固定为 36 bytes: 1byte tag + 1byte workchain + 32 bytes hash + 2 byte crc16 console.log("TON address (V5R1), urlSafe(true), bounceable(true):", address.toString({ urlSafe: true, bounceable: true, testOnly: false })) // EQBG2LwJPtVKXlUHoXEGmIFksbLaCZgrBDtEDD0Mxl2-oWSC console.log("TON address (V5R1), urlSafe(true), bounceable(false):", address.toString({ urlSafe: true, bounceable: false, testOnly: false })) // UQBG2LwJPtVKXlUHoXEGmIFksbLaCZgrBDtEDD0Mxl2-oTlH console.log("TON address (V5R1), urlSafe(false), bounceable(true):", address.toString({ urlSafe: false, bounceable: true, testOnly: false })) // EQBG2LwJPtVKXlUHoXEGmIFksbLaCZgrBDtEDD0Mxl2+oWSC console.log("TON address (V5R1), urlSafe(false), bounceable(false):", address.toString({ urlSafe: false, bounceable: false, testOnly: false })) // UQBG2LwJPtVKXlUHoXEGmIFksbLaCZgrBDtEDD0Mxl2+oTlH