完善 recv_internal 功能
在 contracts/main.fc中已经有了一个处理传入消息的函数。
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { }
我们已经可以看到传入 recv_internal 函数的三个参数:
- msg_value - 这个参数显示收到了多少TONcoin
- in_msg - 这是一条完整信息,包含发送者等所有信息。它的类型是 Cell,信息正文作为一个单元格存储在 TVM 上,有一个完整的单元格专门用于存储信息及其所有数据。
- in_msg_body - 这是收到的消息的实际 "可读 "部分。它是单元格的一部分,具有片段slice类型,指示了如果要读取这个片段参数,应该从单元格的哪个部分开始读取的 "地址"。
msg_value 和 in_msg_body 都可以从 in_msg 派生,但为了便于使用,我们将它们作为参数接收到 receive_internal 函数中。
函数说明符
在传入函数的参数后面有一个 impure 字样。这是三种可能的函数指令之一:
- impure
- inline/inline_ref
- method_id
函数声明可以包含一个、几个或任何一个,但必须按照正确的顺序排列。例如,不允许将 impure 放在 inline 之后。
impure 指定符意味着函数可能会产生一些不能忽略的副作用。例如,如果函数可以修改合约存储、发送信息或在某些数据无效时抛出异常,而函数的目的是验证这些数据,那么就应该使用 impure 。
如果未指定 impure,且函数调用的结果未被使用,那么 FunC 编译器可能会删除该函数调用。
导入 stdlib.fc
为了在合约中操作数据和编写其他逻辑,我们需要导入 FunC 标准库。目前,该库只是 TVM 命令中最常用的汇编程序的封装器,而 TVM 命令并不是内置的。库中使用的每条 TVM 命令说明都可以在文档中找到。
在 contracts 文件夹中创建一个 imports 文件夹。然后,创建一个文件 stdlib.fc,并将官方 stdlib.fc 库的内容填入其中。
接下来在 main.fc 文件的开头导入:
#include "imports/stdlib.fc"; () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { }
解析in_msg
最后,来了解一下能用传给 recv_internal 函数的参数做些什么。
每当我们要处理内部消息,在开始读取有意义的in_msg_body 部分之前,首先需要了解我们收到的是哪种内部邮件。
例如,因为合约之前向某人发送了一些消息,但接收方无法接受,所以消息被 "弹回 "了。
我们收到的每条报文都有标记。标志基本上是一个 4 位整数。
这些标志实际上会告诉我们关于msg的一些信息,例如 "此报文被接收方退回,现在实际上又返回了"。
因此,当合约接收到内部信息时,要做的第一件事就是解析它。解析 in_msg:
来分析一下这段代码中到底发生了什么:
你需要记住这样一个概念:片段slice是一个 "地址",一个指针。因此,当我们解析时,是从某个地方开始解析的。在本例中,begin_parse() 告诉我们应该从哪里开始解析,它给了我们指向 in_msg 单元第一位的指针。
然后,我们通过调用 load_uint(4) 来解析一个 4 位整数,并将结果赋值给一个 int 变量 flags。
一旦我们要在 cs 变量上调用更多 ~load_{},我们将从上一次 ~load_{} 完成的地方继续解析。
如果我们试图解析单元格中并不存在的内容,我们的合约将以代码 9 退出。有关标准代码错误的更多信息,请点击此处。
in_msg 单元中还有一些更有价值的信息,即发件人地址,让我们继续解析:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); }
当我们要创建一个存储地址的变量时总是使用片段类型,因此我们只存储指针,一旦需要就从内存读取地址。
智能合约的还有另外 2 项功能:
- 智能合约有一个持久存储空间(persistent storage,称为 c4 storage)
- 智能合约有一个 getter method,可以让外部其他人从合约中获取一些数据。
利用这两项新功能,我们可以在存储空间中存储发送者地址,并创建一个 getter 方法,在调用时返回。
换句话说, getter 将始终返回最近向我们的合约发送消息的合约地址。
使用持久存储( persistent storage)
为了在 c4 persistent storage 中存储数据,我们将使用 FunC 标准函数 set_data。该函数接受并存储一个 Cell。
如果想在一个 Cell 中存储更多数据,可以很容易地在第一个 Cell 中写入一个指向另一个 Cell 的 "链接"。这种链接称为 ref。我们最多可以在一个 Cell 中写入 4 个 ref。
用 set_data 函数更新代码,并学习如何将 Cell 传递到该函数中。
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); set_data(begin_cell().end_cell()); }
要在 set_data 中传递一个单元格,我们需要先构建它。这可以通过两个函数 begin_cell() 和 end_cell() 实现。
目前我们传递的是一个空单元格,要将消息发送者的地址写入存储空间应该如下:
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); set_data(begin_cell().store_slice(sender_address).end_cell()); }
使用method .store_slice() 来存储指针。
这样就有了一个能够将发件人地址写入持久化存储的智能合约,每当合约接收到一条内部信息时,它就会用一个新的单元格替换存储在 c4 中的单元格,新的单元格中将包含一个新的发件人地址。
使用getter method
如果不想仅仅只存储发件人地址,而是希望任何人都能读取最新的发件人地址,就要从 TVM 外部访问此类数据,所以合约还需要一个特殊函数。
slice get_the_latest_sender() method_id { slice ds = get_data().begin_parse(); return ds~load_msg_addr(); }
先使用 begin_parse() 来获取一个指针,从中解析存储在 c4 storage中的 Cell,再使用 ~load_msg_addr 来载入地址。
编译合约
main.fc最终代码应该是这样的:
#include "imports/stdlib.fc"; () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); set_data(begin_cell().store_slice(sender_address).end_cell()); } slice get_the_latest_sender() method_id { slice ds = get_data().begin_parse(); return ds~load_msg_addr(); }
如果一切顺利,终端应该会出现如下提示:
================================================================= Compile script is running, let's find some FunC code to compile... - Compilation successful! - Compiled code saved to build/main.compiled.json ✨ Done in 1.40s.
检查 build/main.compiled.json,它的内容发生了变化,因为改动增加了一段 FunC 代码,这次的十六进制值合约还会更显得更长。