本文我们来学习如何使用 FunC 编写 TON 上的 Jetton 代币合约。在开始之前,希望读者已经阅读过之前的关于 Tact 编写 Jetton 的[文章](https://hackmd.io/@xyymeeth/Hy6UfF9rye)。其中介绍的一些 Jetton 特性,本文不再重复。
我们这里使用 TON 官方提供的 FunC [代码](https://github.com/ton-blockchain/token-contract),来学习 Jetton 的编写。
## 代码分析
与 Tact 相同,FunC 的 Jetton 也遵守相同的架构,即每个 Jetton 都分为 Master 合约和用户独立的 Jetton 钱包合约。不过在 FunC 这边,Master 被称为 Minter 合约,这应该是历史命名习惯,大家了解即可。
官方代码中,主要包含下面这些文件:
+ jetton-minter.fc
+ jetton-wallet.fc
+ jetton-utils.fc
+ op-codes.fc
+ params.fc
另外还有一些相关拓展合约,不过本文我们主要来学习上面的内容。上述文件中,前两个分别是 Minter 和 Jetton wallet 合约,下面三个是一些辅助工具方法。
我们从简单的内容看起,先看这几个工具方法的内容。
### op-codes.fc
```func!
int op::transfer() asm "0xf8a7ea5 PUSHINT";
int op::transfer_notification() asm "0x7362d09c PUSHINT";
int op::internal_transfer() asm "0x178d4519 PUSHINT";
int op::excesses() asm "0xd53276db PUSHINT";
int op::burn() asm "0x595f07bc PUSHINT";
int op::burn_notification() asm "0x7bdd97de PUSHINT";
;; Minter
int op::mint() asm "21 PUSHINT";
```
该文件中包含的是 Jetton 中需要用到的 opcode,对于每个动作,都定义了对应的 opcode。合约会根据 opcode 的不同来处理不同的消息。
`asm` 表示前面定义的方法使用后面的汇编操作码,`PUSHINT` 表示将整数压入栈中。在 FunC 中常在定义常量时会使用到该操作码。
### params.fc
```func!
int workchain() asm "0 PUSHINT";
() force_chain(slice addr) impure {
(int wc, _) = parse_std_addr(addr);
throw_unless(333, wc == workchain());
}
```
TON 中存在主链和工作链,我们的合约一般都是部署在工作链上。`force_chain` 方法的作用是校验地址必须为 0 工作链的地址,该工作链的标识符为 0,主链的标识符为 -1。
### jetton-utils.fc
该文件中包含四个方法:
+ pack_jetton_wallet_data
+ calculate_jetton_wallet_state_init
+ calculate_jetton_wallet_address
+ calculate_user_jetton_wallet_address
这几个方法目的主要是为了计算合约的初始化状态和地址。
合约中向其它合约发送消息时,如果目标合约有可能还没有被部署,需要在消息中附带合约的代码和初始化状态。在 Tact 中,该消息可以这样编写:
```tact!
send(SendParameters{
to: receiver,
value: 0,
bounce: true,
mode: SendRemainingValue,
body: JettonInternalTransfer{
query_id: msg.query_id,
amount: msg.amount,
response_address: msg.response_destination,
from: self.owner,
forward_ton_amount: msg.forward_ton_amount,
forward_payload: msg.forward_payload
}.toCell(),
code: init.code,
data: init.data
});
```
其中,最后的 `code` 和 `data` 就是目标合约的代码和初始化状态。在 FunC 中,同样的场景也需要附带 `code` 和 `data`。
#### pack_jetton_wallet_data
```func!
cell pack_jetton_wallet_data(
int balance,
slice owner_address,
slice jetton_master_address,
cell jetton_wallet_code
) inline {
return begin_cell()
.store_coins(balance)
.store_slice(owner_address)
.store_slice(jetton_master_address)
.store_ref(jetton_wallet_code)
.end_cell();
}
```
TEP-74 中规定,Jetton wallet 的数据中需要包含如下的这四个字段:
+ balance
+ owner_address
+ jetton_master_address
+ jetton_wallet_code
所以需要将其打包起来并放入 Cell 中,该方法就是用于打包初始化的数据。
在编写自己的代码时,初始化需要什么数据,就类比该方法将其打包进 Cell 即可。
#### calculate_jetton_wallet_state_init
```func!
cell calculate_jetton_wallet_state_init(
slice owner_address,
slice jetton_master_address,
cell jetton_wallet_code
) inline {
return begin_cell()
.store_uint(0, 2)
.store_dict(jetton_wallet_code)
.store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code))
.store_uint(0, 1)
.end_cell();
}
```
该方法接收的参数为初始化的数据,并调用 `pack_jetton_wallet_data` 将其打包,最后生成初始化合约的状态。类似于 Tact 中的:
```tact!
fun calculate_jetton_wallet_init(owner_address: Address): StateInit {
return initOf ExampleJettonWallet(
owner_address,
self.jetton_master
);
}
```
`pack_jetton_wallet_data` 的第一个参数为 0,代表初始化的钱包余额为 0。
该方法的内容可能比较难懂,例如 `store_uint(0, 2)`、`store_uint(0, 1)` 等,我们目前先不用纠结,只需知道这是固定形式即可。在编写自己的代码时,第一个 `store_dict` 放入目标合约的代码,第二个 `store_dict` 放入目标合约的初始化数据即可。
#### calculate_jetton_wallet_address
```func!
slice calculate_jetton_wallet_address(cell state_init) inline {
return begin_cell().store_uint(4, 3)
.store_int(workchain(), 8)
.store_uint(cell_hash(state_init), 256)
.end_cell()
.begin_parse();
}
```
该方法通过上一步生成的合约初始化状态,计算出其地址。同样,目前不需要理解细节,这种写法是固定形式。类似于 Tact 中的:
```tact=
contractAddress(initCode)
```
#### calculate_user_jetton_wallet_address
```func!
slice calculate_user_jetton_wallet_address(
slice owner_address,
slice jetton_master_address,
cell jetton_wallet_code
) inline {
return calculate_jetton_wallet_address(
calculate_jetton_wallet_state_init(
owner_address,
jetton_master_address,
jetton_wallet_code
)
);
}
```
这个就很简单了,接收初始化数据,并返回目标合约的地址。
看完了这几个辅助方法,我们接下来学习 Minter 和 Jetton wallet 合约。
### jetton-minter.fc
#### load_data
```func=
(int, slice, cell, cell) load_data() inline {
slice ds = get_data().begin_parse();
return (
ds~load_coins(), ;; total_supply
ds~load_msg_addr(), ;; admin_address
ds~load_ref(), ;; content
ds~load_ref() ;; jetton_wallet_code
);
}
```
读取合约 c4 寄存器中的数据,数据类型由 TEP-74 定义。
#### save_data
```func=
() save_data(
int total_supply,
slice admin_address,
cell content,
cell jetton_wallet_code
) impure inline {
set_data(begin_cell()
.store_coins(total_supply)
.store_slice(admin_address)
.store_ref(content)
.store_ref(jetton_wallet_code)
.end_cell()
);
}
```
遵循 TEP-74 定义的规则,与 `load_data` 读取的数据需一致。
#### recv_internal
合约的入口方法,该方法前面的一些内容都比较常规,这里不再重复介绍,不理解的读者可以先学些这篇 FunC 的[入门文章](https://hackmd.io/@xyymeeth/SJnUDOwwkg)。
其中 `slice sender_address = cs~load_msg_addr();` 是从消息中获取 `sender` 地址。
随后通过 `load_data` 方法获取合约中存储的数据:
```func=
(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
```
接下来的部分就是根据 opcode 的不同,处理不同的消息内容。总共包括下面四个消息:
+ op::mint()
+ op::burn_notification()
+ 3 -> 修改管理员
+ 4 -> 修改 Metadata
其中,`3` 和 `4` 也是 opcode,它俩处理的逻辑比较简单,都是从消息体中获取相关数据,并将其存储到 c4 寄存器中:
```func=
if (op == 3) { ;; change admin
throw_unless(73, equal_slices(sender_address, admin_address));
slice new_admin_address = in_msg_body~load_msg_addr();
save_data(total_supply, new_admin_address, content, jetton_wallet_code);
return ();
}
if (op == 4) { ;; change content, delete this for immutable tokens
throw_unless(73, equal_slices(sender_address, admin_address));
save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code);
return ();
}
```
其中,`equal_slices` 用于判断两个 slice(这里是两个地址)是否相同,如果 `sender` 不是管理员地址,则抛出异常。
打开 `wrappers/JettonMinter.ts` 文件,找到它们的消息体构成:
```ts=
static changeAdminMessage(newOwner: Address) {
return beginCell().storeUint(Op.change_admin, 32).storeUint(0, 64) // op, queryId
.storeAddress(newOwner)
.endCell();
}
static changeContentMessage(content: Cell) {
return beginCell().storeUint(Op.change_content, 32).storeUint(0, 64) // op, queryId
.storeRef(content)
.endCell();
}
```
可以看到它们的消息体数据结构与合约中的解析部分相同。
`op::burn_notification()` 消息来自于 Jetton wallet 合约,这部分放到后面我们再做分析。
来看 `op::mint()` 部分,回忆一下 mint 的逻辑如下:

代码如下:
```func=
if (op == op::mint()) {
throw_unless(73, equal_slices(sender_address, admin_address));
slice to_address = in_msg_body~load_msg_addr();
int amount = in_msg_body~load_coins();
cell master_msg = in_msg_body~load_ref();
slice master_msg_cs = master_msg.begin_parse();
master_msg_cs~skip_bits(32 + 64); ;; op + query_id
int jetton_amount = master_msg_cs~load_coins();
mint_tokens(to_address, jetton_wallet_code, amount, master_msg);
save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code);
return ();
}
```
首先判断消息发送者是否是 admin,随后从 `body` 中解析数据,我们查看 `wrappers/JettonMinter.ts` 中的 mint 消息结构:
```ts=
static mintMessage(from: Address, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint, query_id: number | bigint = 0) {
const mintMsg = beginCell().storeUint(Op.internal_transfer, 32)
.storeUint(0, 64)
.storeCoins(jetton_amount)
.storeAddress(null)
.storeAddress(from) // Response addr
.storeCoins(forward_ton_amount)
.storeMaybeRef(null)
.endCell();
return beginCell().storeUint(Op.mint, 32).storeUint(query_id, 64) // op, queryId
.storeAddress(to)
.storeCoins(total_ton_amount)
.storeCoins(jetton_amount)
.storeRef(mintMsg)
.endCell();
}
```
首先看 return 的 Cell,包含的数据如下:
+ mint opcode
+ query_id
+ to
+ total_ton_amount
+ jetton_amount
+ mintMsg
对应于合约中,`if` 语句之前已经解析了 `opcode` 和 `query_id`,随后又解析出 `to_address`、`total_ton_amount`、`jetton_amount`、`mintMsg`。
`to_address` 为 mint 接收者的地址,注意是接收者的 TON 钱包地址,而不是 Jetton 钱包地址。
`total_ton_amount` 为发送给 Jetton wallet 消息附带的 TON 数量。
`jetton_amount` 为 mint 的 Jetton 数量。
`mintMsg` 为发送给 Jetton wallet 的实际消息体,包含的数据如下:
+ internal transfer opcode
+ query_id
+ jetton_amount
+ internal transfer 消息来源地址(这里由 minter 合约发送,可以为 null)
+ response address,即剩余 Gas 返还地址
+ forward ton amount
+ forward payload
在上述代码部分,`mintMsg` 被解析为 `master_msg`,随后跳过前面两个数据,即 `internal transfer opcode` 和 `query_id`,读取第三个数据 `jetton_amount`。
随后,调用 `mint_tokens` 发送消息,最后使用 `save_data` 将 `total_supply` 数据更新。
#### mint_tokens
```func=
() mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure {
cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code);
slice to_wallet_address = calculate_jetton_wallet_address(state_init);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(to_wallet_address)
.store_coins(amount)
.store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
.store_ref(state_init)
.store_ref(master_msg);
send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors
}
```
前面两行通过调用之前介绍的辅助方法,获取目标合约的初始状态和地址,随后构造消息体内容,在消息体最后附上初始化状态和实际 body 消息体。对于这一堆参数不理解的读者可以阅读这篇[文章](https://hackmd.io/@xyymeeth/r15Ok8qD1g)。
最后使用 `send_raw_message` 方法发送消息,这里使用的 mode 是 `1`,也就是 pay transfer fees separately,即发送消息的 Gas 需要额外支付。
消息被 Jetton wallet 合约接收,接收部分我们放在后面介绍。
#### Getter
还剩余两个 Getter 方法:
```func
(int, int, slice, cell, cell) get_jetton_data() method_id {
(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
return (total_supply, -1, admin_address, content, jetton_wallet_code);
}
slice get_wallet_address(slice owner_address) method_id {
(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code);
}
```
它们被链下调用,第一个方法用于获取合约中存储的数据,第二个方法用于获取用户的 Jetton wallet 地址。
### jetton-wallet.fc
接着来看 Jetton wallet 合约,用户转账以及销毁代币的入口都在该合约中。
首先定义了两个常量:
```func
int min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON
int gas_consumption() asm "15000000 PUSHINT"; ;; 0.015 TON
```
由于 TON 的合约需要向系统缴纳租金,`min_tons_for_storage()` 就是合约中必须维持的最低 TON 数量。`gas_consumption()` 是转账中的 Gas 消耗数量。这两个值都是经过测试之后的一个比较均衡的数量,你也可以将其变更,不过如果减少了,可能会导致一些问题。
#### load_data 与 save_data
```func
(int, slice, slice, cell) load_data() inline {
slice ds = get_data().begin_parse();
;; balance, owner address, jetton master address, jetton wallet code
return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
}
() save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline {
set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code));
}
```
分别用于加载与存储合约中的数据,他们的数据类型也是 TEP-74 中定义的,注意它们也与前面 mint 时构造的目标合约初始化数据一一对应:
```func
pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)
```
#### recv_internal
先来看消息入口方法,这里与 Minter 合约的入口相比,有一些不同:
```func
if (flags & 1) {
on_bounce(in_msg_body);
return ();
}
```
对于 bounced 消息,这里使用了 `on_bounce` 方法进行处理,该方法的内容我们后面再看。
```func
slice sender_address = cs~load_msg_addr();
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs
```
接着从 `in_msg_full` 中解析出 sender 地址,并跳过一系列数据,获取该消息体的 `fwd_fee`,这部分需要参考消息的[具体构成](https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages#message-layout)。
一个消息的实际构成如下所示:
```func
var msg = begin_cell()
.store_uint(0, 1) ;; tag
.store_uint(1, 1) ;; ihr_disabled
.store_uint(1, 1) ;; allow bounces
.store_uint(0, 1) ;; not bounced itself
.store_slice(source)
.store_slice(destination)
.store_coins(amount)
.store_dict(extra_currencies)
.store_coins(0) ;; ihr_fee
.store_coins(fwd_value) ;; fwd_fee
.store_uint(cur_lt(), 64) ;; lt of transaction
.store_uint(now(), 32) ;; unixtime of transaction
.store_uint(0, 1) ;; no init-field flag (Maybe)
.store_uint(0, 1) ;; inplace message body flag (Either)
.store_slice(msg_body)
.end_cell();
```
其中前 4 个 `store_uint` 的内容就是最开始的 `flags`,随后跳过 `destination`、`amount`、`extra_currencies`、`ihr_fee`。最后通过 `cs~load_coins()` 解析出 `fwd_value`,并通过:
```func
int fwd_fee = muldiv(cs~load_coins(), 3, 2);
```
计算出 `fwd_fee`,可以将其简单理解成每次发送消息时所需的费用,即***邮费***,而不包含附带的 TON 数量。这部分的计算逻辑目前官方文档也没有给出一个特别清晰的说明,应该是和验证节点有关,大家目前暂时记住即可。
随后从 `in_msg_body` 中解析出 `op`,并根据 `op` 的不同处理不同逻辑:
```func
int op = in_msg_body~load_uint(32);
if (op == op::transfer()) { ;; outgoing transfer
send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
if (op == op::internal_transfer()) { ;; incoming transfer
receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
return ();
}
if (op == op::burn()) { ;; burn
burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
throw(0xffff);
```
#### send_tokens
代码比较多,我们就挑一些重要的点来说。一些简单的数据解析等,就不再重复。
```func
force_chain(to_owner_address);
```
校验接收者的地址必须是 0 workchain 链上的地址。
```func
cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
slice to_wallet_address = calculate_jetton_wallet_address(state_init);
```
转账这里与 mint 类似,由于接收者可能并不拥有 Jetton wallet,因此需要构造出目标合约的状态与地址,并将其附带在消息的后面。
```func
slice response_address = in_msg_body~load_msg_addr();
cell custom_payload = in_msg_body~load_dict();
int forward_ton_amount = in_msg_body~load_coins();
throw_unless(708, slice_bits(in_msg_body) >= 1);
slice either_forward_payload = in_msg_body;
```
重点来看看这几行代码,首先是从 `in_msg_body` 中解析出了 `response_address`、`custom_payload`、`forward_ton_amount`。根据 TEP-74 的规范,此时后面应该还跟有 `forward_payload`,也就是说 `in_msg_body` 的解析指针目前是在`forward_payload` 的起始位置。
我们查看 TEP-74 的数据类型[规定](https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md#1-transfer):
```txt
forward_payload:(Either Cell ^Cell)
```
它的意思是 `forward_payload` 的内容要么直接在当前 Cell 中原地解包,要么存储在另一个 Cell 中,并被引用进来。
对于前者,接下来的一个 bit 需要存储 0,后面再跟上 `forward_payload` 的实际内容。如果 `forward_payload` 为空,该 bit 也需要存储 0。对于后者,接下来的一个 bit 需要存储 1,后面跟上 `forward_payload` 的引用。也就是说,不论 `forward_payload` 是以何种形式存储,接下来的一个 bit 都是有数据的,0 或 1([参考](https://docs.ton.org/v3/documentation/data-formats/tlb/tl-b-language))。
我们可以查看[文档](https://docs.ton.org/v3/guidelines/ton-connect/guidelines/preparing-messages)中给出的 Jetton Transfer 的消息构成的例子进行验证:
```ts
const forwardPayload = beginCell()
.storeUint(0, 32) // 0 opcode means we have a comment
.storeStringTail('Hello, TON!')
.endCell();
const body = beginCell()
.storeUint(0xf8a7ea5, 32) // opcode for jetton transfer
.storeUint(0, 64) // query id
.storeCoins(toNano("5")) // Jetton amount for transfer (decimals = 6 - USDT, 9 - default). Function toNano use decimals = 9 (remember it)
.storeAddress(destinationAddress) // TON wallet destination address
.storeAddress(destinationAddress) // response excess destination
.storeBit(0) // no custom payload
.storeCoins(toNano("0.02")) // forward amount (if >0, will send notification message)
.storeBit(1) // we store forwardPayload as a reference
.storeRef(forwardPayload)
.endCell();
```
`forwardPayload` 作为引用放在最后,在它之前,存储的是 1。
```python
data = {
'address': jetton_wallet_address,
'amount': str(transfer_fee),
'payload': urlsafe_b64encode(
begin_cell()
.store_uint(0xf8a7ea5, 32) # op code for jetton transfer message
.store_uint(0, 64) # query_id
.store_coins(jettons_amount) # Jetton amount for transfer (decimals = 6 - USDT, 9 - default). Exapmple: 1 USDT = 1 * 10**6 and 1 TON = 1 * 10**9
.store_address(recipient_address) # destination address
.store_address(response_address or recipient_address) # address send excess to
.store_uint(0, 1) # custom payload
.store_coins(1) # forward amount
.store_uint(0, 1) # forward payload
.end_cell() # end cell
.to_boc() # convert it to boc
)
.decode() # encode it to urlsafe base64
}
```
`forward payload` 实际为空,但最后仍需要存储一位 0。
我们再来查看 `wrappers/JettonWallet.ts` 中 Jetton 转账消息的构成:
```ts
return beginCell().storeUint(0xf8a7ea5, 32).storeUint(0, 64) // op, queryId
.storeCoins(jetton_amount)
.storeAddress(to)
.storeAddress(responseAddress)
.storeMaybeRef(customPayload)
.storeCoins(forward_ton_amount)
.storeMaybeRef(forwardPayload)
.endCell();
```
对于 `forwardPayload` 的存储,使用了 `storeMaybeRef`。查看一个 golang 的[实现](https://github.com/tonft-app/backend/blob/bcde84c3d586/lib/tvm/cell/builder.go#L387):
```golang
func (b *Builder) StoreMaybeRef(ref *Cell) error {
if ref == nil {
return b.StoreUInt(0, 1)
}
// we need early checks to do 2 stores atomically
if len(b.refs) >= 4 {
return ErrTooMuchRefs
}
if b.bitsSz+1 >= 1024 {
return ErrNotFit1023
}
b.MustStoreUInt(1, 1).MustStoreRef(ref)
return nil
}
```
如果 `ref` 为空,则需要存储一位 0,否则存储一位 1,并将 `ref` 作为引用存储。
看完了这些代码,我们再回头看 Jetton wallet 的代码:
```func
int forward_ton_amount = in_msg_body~load_coins();
throw_unless(708, slice_bits(in_msg_body) >= 1);
slice either_forward_payload = in_msg_body;
```
`forward_ton_amount` 接下来的一位必然是有数据的,要么是 0,要么是 1。不管是什么,都是有数据的,因此能够通过 `708` 的校验,如果没有数据,则说明发送的消息体不符合 Jetton 的规范。
随后构造消息体,并将目标合约的 `state_init` 附带在消息体中。
```func
throw_unless(709, msg_value >
forward_ton_amount +
;; 3 messages: wal1->wal2, wal2->owner, wal2->response
;; but last one is optional (it is ok if it fails)
fwd_count * fwd_fee +
(2 * gas_consumption() + min_tons_for_storage()));
```
这里是要求用户传入的 Gas 足够处理完整个流程。这部分的计算在之前的 Tact [文章](https://hackmd.io/@xyymeeth/Hy6UfF9rye#JettonWallet)中讲过,这里不再重复。
最后,发送消息,并将余额更新到合约数据中。
#### receive_tokens
该方法的整体逻辑都比较简单,参考之前 Tact 的文章很容易理解。不过其中有两部分代码需要关注:
```func
var msg = begin_cell()
.store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
.store_slice(owner_address)
.store_coins(forward_ton_amount)
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(msg_body);
```
这里使用了 `0x10`,即 non-bounceable 消息。这条消息是给接收者的地址发送 `transfer_notification` 消息,但是接收者的钱包可能还没有被部署。这种场景是可能存在的,例如 A 给 B 转 100 个 USDT,但是 B 刚刚生成了主网地址,还没有部署它自己的合约钱包。这条消息我们并不在乎它是否成功,因为主逻辑已经完成。此时如果使用了 bounceable 消息,会使整个逻辑变得更复杂,也可能会出错。
TON 中规定,如果向一个可能未被部署的合约发送消息,要么需附带 state init,要么需是 non-bounceable 消息。
```func
if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
.store_slice(response_address)
.store_coins(msg_value)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::excesses(), 32)
.store_uint(query_id, 64);
send_raw_message(msg.end_cell(), 2);
}
```
`preload_uint` 用于预加载整数,也就是指针不会移动。在 Cell 中,空地址使用 2 位 0 表示,如果这里预加载的 2 位数据不是 0,则说明
`response_address` 不为空。如果该地址不为空,并且剩余的 Gas 大于 0,则将剩余的 Gas 返还给 `response_address`。
这条消息同样为 non-bounceable 消息,因为返还 Gas 的地址可能未被部署。
#### burn_tokens
burn 的逻辑更加简单,最后会向 Minter 合约发送消息。Minter 中同样使用了 `preload_uint` 来预加载地址,如果非空,则将剩余的 Gas 返还给 `response_address`。
#### on_bounce
最后来看看这个比较新鲜的方法,它用于处理接收到的 bounced 回弹消息:
```func
() on_bounce (slice in_msg_body) impure {
in_msg_body~skip_bits(32); ;; 0xFFFFFFFF
(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
int op = in_msg_body~load_uint(32);
throw_unless(709, (op == op::internal_transfer()) | (op == op::burn_notification()));
int query_id = in_msg_body~load_uint(64);
int jetton_amount = in_msg_body~load_coins();
balance += jetton_amount;
save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
}
```
它的参数为 `in_msg_body`,但是这个 body 并不是我们最初发送的 body。当初始 bounceable 消息变成 bounced 消息时,bounced 的消息体 body 将以 `0xFFFFFFFF` 开头([参考](https://docs.ton.org/v3/documentation/smart-contracts/message-management/non-bounceable-messages))。随后跟随的是原始消息体 body 的前 256 位数据,多余的数据将被截掉。
在 `on_bounce` 方法中,对于 `in_msg_body`,先跳过前面的 `0xFFFFFFFF`,随后从消息体中解析出 `op`、`query_id` 和 `jetton_amount`,它们分别占据 32 位、64 位、120 位的数据,加起来小于 256 位。方法中限制,返回的 `op` 必须是 `op::internal_transfer()` 或 `op::burn_notification()`。由于这两条消息的前三个字段,都是 `op`、`query_id` 和 `jetton_amount`,因此可以放在一起处理。
这两条消息之前均是减少余额的行为,如果这两条消息被回弹,则将消息中的 `jetton_amount` 加回来。
这里的 bounced 消息,由于它的结构比较简单,最重要的字段就是 `jetton_amount`,也恰好可以放在 bounced 消息体中,因此使用 bounced 消息来处理很合适。但是在实际业务开发中,并不推荐使用 bounced 消息,因为它的信息内容有限。如果需要有类似的错误处理,更推荐在接收合约中直接编写错误处理逻辑并回发消息。
#### get_wallet_data
最后还剩余一个 Get 方法,用于返回该钱包合约存储的数据,即用户的 Jetton 余额,用户的地址,Jetton minter 的合约地址,Jetton wallet 的合约代码。
至此,我们就完成了 FunC Jetton 合约的编写。
## 总结
本文详细介绍了 FunC Jetton 合约的编写,并对一些代码细节进行了详细的讲解,如果对其都能够掌握,那么对于 FunC 的编写,将会更加得心应手。在学习完 FunC 版本的 Jetton 代码之后,其实会深切体会到 FunC 对于 Cell 数据的处理更加精确,因此如果想要在 TON 生态深入,FunC 是必须要掌握的。
## 关于我
欢迎[和我交流](https://linktr.ee/xyymeeth)