上篇文章我们介绍了 FunC 的一些基础内容,这篇文章来学习如何使用 FunC 来发送消息。这部分内容有许多细节需要注意,因此值得单独写篇文章记录一下。
TON 的文档为我们提供了一个发送消息的模板代码:
与 FunC 的 recv_internal()
方法签名对照来看:
其中 msg
就是 in_msg_full
,message_body
就是 in_msg_body
。
与 Tact 的消息发送方法对比:
其中 msg
是 send
方法包含的所有内容,message_body
是 body
部分,这里即 JettonTransferNotification
。
上面这部分模板代码,我们目前阶段不需要深究它为什么这么写。当前的任务是学会灵活使用这段代码,例如 mode
怎么指定,bounce
怎么指定。
如果对于消息体的具体结构感兴趣,可以看这里,不过这部分内容比较深入,我们在本文不会过多提及,当大家有了一部分开发经验之后,再去学习,会有更深入的理解。
我们先来简单学习一个这几个字段的意义。
store_uint(0x18, 6)
是在消息体头部设置一些信息,常用的改写是和 bounce
相关的。如果希望设置 bounce = true
,则使用:
如果希望设置 bounce = false
,则使用:
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
部分,写法如下:
没有 body
,也就不需要最后的 store_slice(message_body)
,同时,最后一个 store_uint()
的第一个参数为 0,当发送空消息体的时候就使用 0。
我们编写一个发送空消息的合约:
首先构建出消息体 msg
,随后通过 send_raw_message
方法发送消息,该方法是 FunC 的底层方法,所有的消息都通过该方法发送。其中第一个参数是消息体 Cell,第二个参数是消息模式(Message mode),也就是 Tact 中的 SendRemainingValue
、SendRemainingBalance
等等那些常数。对消息模式不了解的可以看看我之前写的 Tact 文章,里面有相关介绍。
随后编写出接收合约:
运行单元测试发现空消息会在这部分直接返回,说明消息体确实为空。
发送非空消息体有两种方法,一种是在消息体最后将实际消息体引用进来,一种是直接在当前 Cell 将消息体的内容包含进来。
我们假设消息体中存放了两个整数,分别是 32 位的 0x12345678 和 64 位的 123。
将实际消息体引用进来,例如下面的写法:
在构建出 msg_body
之后,将其作为引用放在了 msg
的最后部分。此时,最后一个 store_uint
的内容为:
直接在当前 Cell 将消息体的内容包含进来,例如下面的写法:
此时 msg_body
不是引用的形式,而是跟在 msg
后面。还有一种更加简单粗暴的形式:
直接将 body
的内容在 msg
中原地解包,此时使用的模式是:
第一个参数为 0。
两者对比,我们可以得出结论,如果真实消息体是被单独引用进来,则使用 1
,如果是直接在跟在 msg
后面,则使用 0
。
我们再来编写接收合约:
运行单元测试,两种模式都成功打印出 a
和 b
的值,说明消息体已经成功发送并被接收合约解析。
我们在之前学习 Jetton 时遇到一种消息,它会附带接收合约的初始化状态,以便在该合约未部署的情况下,部署该合约,例如:
最后的 code
和 data
就是目标合约的代码和初始化数据。FunC 中在实现该逻辑时,是将初始化数据作为引用放在了消息体的最后,例如:
这是官方 Jetton FunC 合约中的一段代码,可以看到消息体的最后一个 store_uint
的内容为:
与前面的写法对比:
可以看到,第一个参数为 4 + 2 + 1
,第二个参数最后多了一个 + 1
。我们目前阶段不需要知道它为什么这么写,只需记住即可。
最后将合约的初始化状态引用到了消息体中,这个 state_init
变量的构成我们会在后续的文章中介绍。
来编写一个带有初始化状态消息的合约:
我们分别将初始化状态 state_init
和 body 内容放在 msg
后面的引用中。
然后编写接收合约:
注意这个例子中,由于我们需要通过合约发送消息来部署合约,因此在单元测试中,就无需部署接收合约,只部署发送合约即可。
运行单元测试后,也能正常打印出 op
和 query_id
,说明通过消息部署合约成功。
还有一个场景是在发送带有初始化状态的消息中,不额外引用 body,那么此时的消息内容需要改为:
第一个参数变为 4 + 2
,第二个参数不变。这时表示没有额外 body,即可以是空 body,也可以将 body 直接解包跟在当前消息体之后。这个场景大家可以自行编写代码验证。
目前 FunC 中常用的几个消息发送逻辑就是上面这些,掌握了这些内容,在编写和阅读 FunC 代码时,对于消息发送的逻辑,就能够得心应手了。
相关代码在这里,大家可以参考
本文总结了使用 FunC 发送消息的方法,场景不同,写法也略有不同。希望这篇文章能够帮助大家更快掌握这部分内容。
欢迎和我交流