上篇[文章](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)