在上一课中,我们创建了一个编译脚本,现在是时候开始编写 FunC 代码了。你知道最酷的是什么吗?我们现在可以看看我们的 FunC 代码是否真的有效。一旦我们编写了一些 FunC 代码,我们只需运行 yarn compile 并确保我们的代码能在 TVM 上运行。
- w% y: B/ i! @3 o) }+ M3 j# M6 H
- }) x6 y6 L) k5 P$ L+ B8 L! {
, y0 X% H4 F& L0 [
本课中的智能合约将非常非常简单,但这足以让我们熟悉基本的 FunC 语法和结构。 : H2 L5 [" n5 W* s/ K/ W
我们通过内部信息处理程序 recv_internal 获得的参数让我们动手编写代码吧。在上一课中,我们已经创建了一个 contracts/main.fc 文件。让我们打开它,看看里面有什么: - () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {2 p+ y. T; ]* W# x
0 Q8 _- x. y# T" `3 k- y+ M) E- }
复制代码我们已经有了一个处理传入信息的函数。在第一章中我们已经知道,TON 上的任何事务都被称为 messages. 那么,这条信息会给我们带来什么呢?我们已经可以看到传入 recv_internal 函数的三个参数: msg_value - 该参数将告诉我们该报文收到了多少TON币(或gram)。 in_msg - 这是我们收到的一条完整信息,包含发送者等所有信息。我们可以看到它的类型是 Cell。这意味着什么呢?信息正文作为一个cell存储在 TVM 上,因此有一个完整的Cell专门用于存储我们的信息及其所有数据。 in_msg_body - 这是我们收到的信息中实际 "可读 "的部分。它有一个片段类型,因为它是Cell的一部分,它指出了如果我们要读取这个片段参数,应该从Cell的哪个部分开始读取的 "地址"。 6 e$ Y& q; ~% X8 v9 k
待办事项:详细说明 in_msg Cell的内容。 如您所见,msg_value 和 in_msg_body 都可以从 in_msg 派生,但为了便于使用,我们将它们作为参数接收到 receive_internal 函数中。 功能说明我相信你已经注意到,在传入函数的参数后面有一个 impure (不纯)字样。这是三种可能的函数指令之一: impure inline/inline_ref method_id 1 Z1 Y- I* p5 J* M4 f
函数声明中可以包含一个、几个或任何一个,但目前必须按照正确的顺序排列。例如,不允许将 impure 放在 inline 之后。 目前,我们只对不纯规范符感兴趣,但只要其他规范符开始出现在我们的代码中,我们就会涉及它们。 impure 指定符意味着函数可能会产生一些不可忽略的副作用。例如,如果函数可以修改合约存储空间、发送信息或在某些数据无效时抛出异常,而函数的目的是验证这些数据,那么我们就应该使用 impure 指定符。 如果未指定 impure,且函数调用的结果未被使用,那么 FunC 编译器可能会删除该函数调用。 导入 stdlib.fc为了在合约中操作数据和编写其他逻辑,我们还需要做一件重要的事情。我们需要导入 FunC 标准库。目前,该库只是 TVM 命令最常用汇编程序的封装器,而 TVM 命令并不是内置的。库中使用的每条 TVM 命令说明都可以在 documentation. 为了导入 stdlib.fc,让我们在 contracts 文件夹中创建一个 imports 文件夹。然后,创建一个文件 stdlib.fc,并将官方 stdlib.fc 库的内容填入其中 here. 现在,我们需要在 main.fc 的开头插入实际的导入: - #include "imports/stdlib.fc";
4 d& R/ X6 |: \( U - ' d1 o( t& V3 b" Y
- () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {. Q5 _% M& q, d& y4 Q" y
1 u. E; o6 _8 V( z% s7 J- }
复制代码很好,现在我们可以开始下一步了 解析 in_msg最后,让我们来学习一下如何使用传给 recv_internal 函数的参数。 每当我们要处理内部信息时,在开始读取有意义的 in_msg_body 部分之前,我们首先需要了解我们收到的是哪种内部信息。可能会有不同的情况。例如,我们可能会收到这条信息,因为我们的合约之前向某人发送了一些信息,但接收方无法接受,所以消息被 "弹回 "了。有时我们并不想处理这类信息。我们将在本课程的稍后部分讨论这种情况。 我们收到的每条信息都有标记。标志基本上是一个 4 位整数,其中每一位......(TODO:详细说明标志的结构。 int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool). 这些标记会告诉我们关于信息的宝贵信息,比如 "此信息被接收方退回,现在它正在返回"。 因此,当我们的合约接收到内部信息时,它要做的第一件事就是解析它。让我们来解析 in_msg: - () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {" g9 T, T" b2 _6 g3 J& {' q2 z
- slice cs = in_msg.begin_parse();
3 j$ }7 `9 i* B5 L% L7 A; e" H - int flags = cs~load_uint(4);
3 \$ E# v" r, v - }
复制代码让我们来分析一下这段代码的具体内容: 你需要记住这个概念,即片段是一个 "地址",一个指针。因此,当我们解析时,我们是从某个地方开始解析的。在本例中,begin_parse() 告诉我们应该从哪里开始解析,它给了我们指向 in_msg 单元第一位的指针。 然后,我们通过调用 load_uint(4) 来解析一个 4 位整数,并将结果赋值给一个 int 变量 flags。 一旦我们在 cs 变量上调用更多 ~load_{*},我们将从上一次 ~load_{*} 完成的地方继续解析。 如果我们试图解析Cell中实际上不存在的内容,我们的合约将以代码 9 退出。您可以阅读有关标准代码错误的更多信息 here in_msg Cell中还有一些更有价值的信息,即发件人地址,让我们继续解析: - () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {! |. T8 |, G1 Y+ @& @, i
- slice cs = in_msg.begin_parse();
1 r- i+ c7 s+ ~6 Z - int flags = cs~load_uint(4);
5 _! n+ R& c: U6 g _ - slice sender_address = cs~load_msg_addr();- i% y9 |" `" j
- }
复制代码当我们要创建一个存储地址的变量时,总是使用 slice 类型,因此我们只存储 pointer 一旦需要,内存应从哪里读取地址。 们该如何处理已有的变量呢?让我先向你介绍智能合约的另外两项功能: 利用这两项新功能,我们可以做以下事情。我们可以在存储空间中存储发送者地址,并创建一个getter方法,在调用该方法时返回。 换句话说,我们的 getter 将始终返回最近向我们的合约发送信息的合约地址。 使用持续存储为了在 c4 持续存储中存储相同的数据,我们将使用 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 {, b( M1 B. t3 }! i8 h, e% }
- slice cs = in_msg.begin_parse();
1 y, `# G; A3 b; M0 d) C - int flags = cs~load_uint(4);9 x/ e0 t5 `) Z: k: k$ L5 P2 _" n
- slice sender_address = cs~load_msg_addr();
; l6 q6 f$ t, P0 l5 F S" t - . _& I4 d" D. y' c$ Y6 q
- set_data(begin_cell().end_cell());3 B; b5 w2 R) O: N0 Y Q4 _
- }
复制代码要在 set_data 中传递一个Cell,我们需要先构建它。这可以通过两个函数 begin_cell() 和 end_cell() 轻松实现。 目前我们传递的是一个空Cell,这在技术上是没有问题的,但我们希望将消息发送者的地址写入存储空间,因此我们应该用它来更新我们的Cell: - () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
6 A) i. o5 ^; ^ - slice cs = in_msg.begin_parse();; I' ^: q U0 n2 @0 B( ^! ]
- int flags = cs~load_uint(4);& K" e' ?! [% o# X% I- T
- slice sender_address = cs~load_msg_addr();
1 k" @3 x( }- A# v6 j
9 p5 D0 c; p8 W: X- set_data(begin_cell().store_slice(sender_address).end_cell());! ^9 z9 I3 [3 c# H6 k/ G2 Y
- }
复制代码为此,我们使用方法 .store_slice()。 这样,我们就有了一个能够将发件人地址写入持续存储的智能合约。每当我们的合约接收到一条内部信息时,它就会用一个新的Cell替换存储在 c4 中的Cell,新的Cell中将包含一个新的发件人地址。就这么简单。 使用获取(getter)方法大家都记得,我们不想只存储发件人地址。我们希望任何人都能读取最新的发件人地址。要从 TVM 外部访问此类数据,我们的合约需要一个特殊函数。 我们最近一直在讨论函数指定符。为了使我们的数据能从 TVM 外部访问,我们将创建一个函数,并使用说明符 method_id。如果一个函数设置了这个指定符,那么它就可以在 lite-client 或 ton-explorer 中以 get-method 的形式调用。 我们创建一个: - slice get_the_latest_sender() method_id {! Q( r5 e: k+ Q8 N' u" H
- , c! v+ m* f( B+ m$ c
- }
复制代码获取函数被置于 recv_internal 函数之外 正如您所看到的,我们定义了函数应该返回的时间、函数名称和 method_id 指定符。 现在我们来编写从持续存储中读取数据并返回其值的逻辑。为此,我们使用 FunC 标准 get_data 函数: - slice get_the_latest_sender() method_id {2 N# O( {) z/ v6 |$ i
- slice ds = get_data().begin_parse();
@& q5 V$ U+ c) ~7 s& L - return ds~load_msg_addr();3 u6 _& }9 {0 R1 r( l6 A2 V
- }
复制代码正如你所看到的,我们再次使用 begin_parse() 来获取一个指针,并从中解析存储在 c4 存储器中的 Cell。 为了载入存储的地址,我们使用 ~load_msg_addr 来载入地址。 编译我们的合约我们的最终代码是这样的: - #include "imports/stdlib.fc";5 s5 {) ]0 R. F. \' h& n
- 6 l" u! a) ~* H1 [2 t T: Q% o" _
- () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
- i/ F' g/ B$ @' s* Z - slice cs = in_msg.begin_parse();6 |; h! p# X6 k0 M5 G- [
- int flags = cs~load_uint(4);: |7 R/ P; d2 J( m& x
- slice sender_address = cs~load_msg_addr();& P M( O6 x/ B
- $ ?5 q$ x: H/ [" F" z) g0 h: p
- set_data(begin_cell().store_slice(sender_address).end_cell());
- \4 J' T1 `( d( W& b3 X - }
3 n4 p; A- s& c8 F* A/ C
3 o$ _+ q! Y+ d7 b. t, O- slice get_the_latest_sender() method_id {) M' n8 B9 q Y; W, ?: m+ V
- slice ds = get_data().begin_parse();
H+ E1 L5 E4 N1 N0 ` - return ds~load_msg_addr();
: q* V/ Q9 r6 a L/ f - }
复制代码这是一份相当简单的合约,但我们在编程过程中学到了很多,不是吗? 既然我们已经写好了所有计划中的代码,让我们运行可爱的 yarn compile 在我们的终端。 如果你跟着我一步步完成了所有操作,你应该会在终端看到以下结果: - =================================================================+ [5 V" U0 v2 `& h
- Compile script is running, let's find some FunC code to compile...% O7 e- U* U5 }5 s, d+ }
- - Compilation successful!
1 S1 K$ S2 K I$ _! [ O$ j! Z% R l - - Compiled code saved to build/main.compiled.json
. O; d# c4 i7 i6 ?; |4 h; q* ^8 [ - ✨ Done in 1.40s.
复制代码
`( b, A! b+ j5 ~) ?9 D# G" {+ F& \* H, }# q
|