上篇[文章](https://hackmd.io/@xyymeeth/SJnUDOwwkg)我们介绍了 FunC 的一些基础内容,这篇文章来学习如何使用 FunC 来发送消息。这部分内容有许多细节需要注意,因此值得单独写篇文章记录一下。 ## 消息体代码模板 TON 的文档为我们提供了一个发送消息的模板代码: ```func= cell msg = begin_cell() .store_uint(0x18, 6) .store_slice(addr) .store_coins(amount) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_slice(message_body) .end_cell(); ``` 与 FunC 的 `recv_internal()` 方法签名对照来看: ```func= recv_internal(int my_ton_balance, int msg_value, cell in_msg_full, slice `in_msg_body`) ``` 其中 `msg` 就是 `in_msg_full`,`message_body` 就是 `in_msg_body`。 与 Tact 的消息发送方法对比: ```tact! send(SendParameters{ to: self.owner, value: msg.forward_ton_amount, mode: SendPayGasSeparately, bounce: false, body: JettonTransferNotification { query_id: msg.query_id, amount: msg.amount, sender: msg.from, forward_payload: msg.forward_payload }.toCell() }); ``` 其中 `msg` 是 `send` 方法包含的所有内容,`message_body` 是 `body` 部分,这里即 `JettonTransferNotification`。 上面这部分模板代码,我们目前阶段不需要深究它为什么这么写。当前的任务是学会灵活使用这段代码,例如 `mode` 怎么指定,`bounce` 怎么指定。 如果对于消息体的具体结构感兴趣,可以看[这里](https://docs.ton.org/v3/documentation/smart-contracts/message-management/sending-messages),不过这部分内容比较深入,我们在本文不会过多提及,当大家有了一部分开发经验之后,再去学习,会有更深入的理解。 ## 消息体字段 我们先来简单学习一个这几个字段的意义。 `store_uint(0x18, 6)` 是在消息体头部设置一些信息,常用的改写是和 `bounce` 相关的。如果希望设置 `bounce = true`,则使用: ```func! store_uint(0x18, 6) ``` 如果希望设置 `bounce = false`,则使用: ```func! store_uint(0x10, 6) ``` `store_slice(addr)` 存放的是消息接收者的地址,要将该消息发送给谁,就填写谁的地址,相当于 Tact 消息体中的 `to` 字段。 `store_coins(amount)` 存放的是该条消息附带的 TON Gas 数量,相当于 Tact 消息体中的 `value` 字段。 `store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)` 存放的是消息体的一些信息,例如消息时间等。这部分在编写时会根据场景的不同,值也不同。我们后面会详细介绍各个场景。 `store_slice(message_body)` 存放的是实际消息体,也就是 Tact 中的 `body`。 ## 消息体编写 这里我将根据几个具体的场景来讲解消息体该怎么编写,我们这里默认所有消息设置为 `bounce = true`,这样就直接使用 `store_uint(0x18, 6)`。 ### 空消息体 当发送空消息体时候,不需要 `body` 部分,写法如下: ```func! cell msg = begin_cell() .store_uint(0x18, 6) .store_slice(addr) .store_coins(amount) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .end_cell(); ``` 没有 `body`,也就不需要最后的 `store_slice(message_body)`,同时,最后一个 `store_uint()` 的第一个参数为 0,当发送空消息体的时候就使用 0。 我们编写一个发送空消息的合约: ```func! #include "imports/stdlib.fc"; () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages return (); } slice to_address = in_msg_body~load_msg_addr(); var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_address) .store_coins(0) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1); send_raw_message(msg.end_cell(), 64); } ``` 首先构建出消息体 `msg`,随后通过 `send_raw_message` 方法发送消息,该方法是 FunC 的底层方法,所有的消息都通过该方法发送。其中第一个参数是消息体 Cell,第二个参数是消息模式(Message mode),也就是 Tact 中的 `SendRemainingValue`、`SendRemainingBalance` 等等那些常数。对消息模式不了解的可以看看我之前写的 Tact [文章](https://hackmd.io/@xyymeeth/HkOEnF8rye#%E6%B6%88%E6%81%AF%E6%A8%A1%E5%BC%8F),里面有相关介绍。 随后编写出接收合约: ```func! #include "imports/stdlib.fc"; () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages ~dump(1); return (); } } ``` 运行单元测试发现空消息会在这部分直接返回,说明消息体确实为空。 ### 非空消息体 发送非空消息体有两种方法,一种是在消息体最后将实际消息体引用进来,一种是直接在当前 Cell 将消息体的内容包含进来。 我们假设消息体中存放了两个整数,分别是 32 位的 0x12345678 和 64 位的 123。 #### 方法一 将实际消息体引用进来,例如下面的写法: ```func! #include "imports/stdlib.fc"; () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages return (); } slice to_address = in_msg_body~load_msg_addr(); var msg_body = begin_cell() .store_uint(0x12345678, 32) .store_uint(123, 64) .end_cell(); var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_address) .store_coins(0) .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_ref(msg_body); send_raw_message(msg.end_cell(), 64); } ``` 在构建出 `msg_body` 之后,将其作为引用放在了 `msg` 的最后部分。此时,最后一个 `store_uint` 的内容为: ```func! store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) ``` #### 方法二 直接在当前 Cell 将消息体的内容包含进来,例如下面的写法: ```func! var msg_body = begin_cell() .store_uint(0x12345678, 32) .store_uint(123, 64) .end_cell().begin_parse(); var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_address) .store_coins(0) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_slice(msg_body); send_raw_message(msg.end_cell(), 64); ``` 此时 `msg_body` 不是引用的形式,而是跟在 `msg` 后面。还有一种更加简单粗暴的形式: ```func! var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_address) .store_coins(0) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_uint(0x12345678, 32) .store_uint(123, 64); send_raw_message(msg.end_cell(), 64); ``` 直接将 `body` 的内容在 `msg` 中原地解包,此时使用的模式是: ```func! store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ``` 第一个参数为 0。 两者对比,我们可以得出结论,如果真实消息体是被单独引用进来,则使用 `1`,如果是直接在跟在 `msg` 后面,则使用 `0`。 我们再来编写接收合约: ```func! #include "imports/stdlib.fc"; () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages ~dump(1); return (); } int a = in_msg_body~load_uint(32); int b = in_msg_body~load_uint(64); ~dump(a); ~dump(b); } ``` 运行单元测试,两种模式都成功打印出 `a` 和 `b` 的值,说明消息体已经成功发送并被接收合约解析。 ### 带有初始化状态的消息 我们在之前学习 Jetton 时遇到一种消息,它会附带接收合约的初始化状态,以便在该合约未部署的情况下,部署该合约,例如: ```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 中在实现该逻辑时,是将初始化数据作为引用放在了消息体的最后,例如: ```func! var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_wallet_address) .store_coins(0) .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) .store_ref(state_init); var msg_body = begin_cell() .store_uint(op::internal_transfer(), 32) .store_uint(query_id, 64) .store_coins(jetton_amount) .store_slice(owner_address) .store_slice(response_address) .store_coins(forward_ton_amount) .store_slice(either_forward_payload) .end_cell(); msg = msg.store_ref(msg_body); send_raw_message(msg.end_cell(), 64); ``` 这是官方 Jetton FunC 合约中的一段代码,可以看到消息体的最后一个 `store_uint` 的内容为: ```func! store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) ``` 与前面的写法对比: ```func! store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ``` 可以看到,第一个参数为 `4 + 2 + 1`,第二个参数最后多了一个 `+ 1`。我们目前阶段不需要知道它为什么这么写,只需记住即可。 最后将合约的初始化状态引用到了消息体中,这个 `state_init` 变量的构成我们会在后续的文章中介绍。 来编写一个带有初始化状态消息的合约: ```func! #include "imports/stdlib.fc"; cell load_data() inline { slice ds = get_data().begin_parse(); return ( ds~load_ref() ;; recv_code ); } const WORKCHAIN = 0; cell pack_recv_data(cell recv_code) inline { return begin_cell() .store_ref(recv_code) .end_cell(); } cell calculate_recv_state_init(cell recv_code) inline { return begin_cell() .store_uint(0, 2) .store_dict(recv_code) .store_dict(pack_recv_data(recv_code)) .store_uint(0, 1) .end_cell(); } slice calculate_recv_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(); } () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages return (); } ~dump(1); var addr = in_msg_body~load_msg_addr(); cell recv_code = load_data(); cell state_init = calculate_recv_state_init(recv_code); slice to_address = calculate_recv_address(state_init); var msg_body = begin_cell() .store_uint(0x12345678, 32) .store_uint(123, 64) .end_cell(); var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_address) .store_coins(0) .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) .store_ref(state_init) .store_ref(msg_body); send_raw_message(msg.end_cell(), 64); ~dump(3); } ``` 我们分别将初始化状态 `state_init` 和 body 内容放在 `msg` 后面的引用中。 然后编写接收合约: ```func! #include "imports/stdlib.fc"; () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { if (in_msg_body.slice_empty?()) { ;; ignore all empty messages ~dump(5); return (); } slice cs = in_msg_full.begin_parse(); int flags = cs~load_uint(4); if (flags & 1) { return (); } int op = in_msg_body~load_uint(32); int query_id = in_msg_body~load_uint(64); ~dump(op); ~dump(query_id); } ``` 注意这个例子中,由于我们需要通过合约发送消息来部署合约,因此在单元测试中,就无需部署接收合约,只部署发送合约即可。 运行单元测试后,也能正常打印出 `op` 和 `query_id`,说明通过消息部署合约成功。 还有一个场景是在发送带有初始化状态的消息中,不额外引用 body,那么此时的消息内容需要改为: ```func! store_uint(4 + 2, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) ``` 第一个参数变为 `4 + 2`,第二个参数不变。这时表示没有额外 body,即可以是空 body,也可以将 body 直接解包跟在当前消息体之后。这个场景大家可以自行编写代码验证。 目前 FunC 中常用的几个消息发送逻辑就是上面这些,掌握了这些内容,在编写和阅读 FunC 代码时,对于消息发送的逻辑,就能够得心应手了。 ## 代码 相关代码在[这里](https://github.com/xyyme/FunCSendMessage),大家可以参考 ## 总结 本文总结了使用 FunC 发送消息的方法,场景不同,写法也略有不同。希望这篇文章能够帮助大家更快掌握这部分内容。 ## 关于我 欢迎[和我交流](https://linktr.ee/xyymeeth)