本文我们来学习如何使用 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 的逻辑如下: ![image](https://hackmd.io/_uploads/BJ2AqlhHJe.png) 代码如下: ```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)