在这一课中,我们将练习执行更多不同的命令、创建函数,甚至从合约内部发送信息。我们将在现有合约的基础上完成所有这些工作。 我们的计划是什么?让我们来分析一下: 我们的合约对指令的要求将变得更加严格,我们将引入一个新的操作码,用于向合约存入资金的逻辑。 我们要引入的另一个操作代码是提取资金。在提取资金过程中,资金实际上是以消息的形式发送到地址的,因此我们将学习如何在合约中发送消息 任何以未知操作代码到达的资金都将返回给发送方 我们将在存储中引入一个新值--合约所有者。只有合约所有者才能提取资金。我们还将在函数中分离存储加载和写入逻辑,以保持主代码的整洁 3 \; S" x6 {- {$ |! x
准备好了吗?就绪。开始! 分离存储管理逻辑首先,让我们来处理合约所有者存储中的数据(我们需要记住,我们必须在启动合约时将该地址放入存储中)。我们还将创建两个新函数-load_data 和 save_data: - (int, slice, slice) load_data() inline {7 Z# `' P* D4 s# C+ _1 |8 q2 Z, z
- var ds = get_data().begin_parse();
, i' p4 L& ^/ f& W: T- {4 { - return (
, U' V! M& N, Z, ^9 J - ds~load_uint(32), ;; counter_value/ M! M# _4 K6 m5 ?: d9 [
- ds~load_msg_addr(), ;; the most recent sender1 f/ `+ X! h. O _8 K& B" K, X! M
- ds~load_msg_addr() ;; owner_address
0 V( Z" t6 H% b; J; x) v/ [ - );* Z, @8 I/ Q8 \
- }3 w, {8 o, n8 o' g8 D* |0 b1 n7 n
- x- ^+ @4 u7 z4 U4 s- () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {5 g# Y& U$ r9 |6 }7 X
- set_data(begin_cell()8 i% N$ P* ~1 P$ Z/ `4 L( U1 B7 ?
- .store_uint(counter_value, 32) ;; counter_value% Q/ V) b9 }' u9 s; Z3 w
- .store_slice(recent_sender) ;; the most recent sender
o8 O9 l& E" {) Q3 R - .store_slice(owner_address) ;; owner_address
$ a- |3 t3 C' b' `3 y4 }, L5 J - .end_cell());& {( u0 }* L# A& n+ n0 t
- }
复制代码这里有几点需要注意: 我们将在代码中进一步使用这些函数: - var (counter_value, recent_sender, owner_address) = load_data();0 q% {! l2 v, ^# |1 G* H, e- u3 B
- $ Y4 ]- r$ z% Y1 E
- save_data(counter_value,recent_sender,owner_address);
复制代码有几点需要注意: 总的来说,代码看起来很简洁,不是吗? 存款和取款对我们来说,存款 (op == 2) 非常简单,因为在这种情况下,我们只需成功完成执行,使得资金被接受。否则,资金将退回给发件人。 - if (op == 2) {
8 W5 s; a. d2 I. ?5 g - return();
7 ?/ V6 B d% B - }
复制代码不过,取款时情况会变得复杂一些。我们必须将发件人地址与智能合约的所有者地址进行比较。让我们来看看,为了实现这个逻辑,我们需要知道哪些事情: 要比较所有者地址和发送者地址,我们使用 FunC 标准函数 equal_slice_bits() 我们使用 throw_unless() 函数在比较结果为 false. 还有另一种通过错误的方法-throw_if(),如果传入该函数的条件返回 true,该函数就会抛出错误. 带有此操作的消息正文还需要有一个整数,说明要求提取的金额。我们将此金额与合约的实际余额进行比较(标准 FunC 函数 get_balance())。 我们希望我们的合约总是有一些资金,能够支付租金和必要的费用(在第 1 章中了解更多关于费用的信息),因此我们需要设置合约中必须保留的最低金额,如果请求的金额不允许,则会抛出错误。 最后,我们需要学习如何通过内部信息从合约内部发送代币 ( f8 X; c8 X, T$ V9 }
. H/ y% O- z$ M我们看看这一切是如何实现的。 首先,我们设置最小所需存储空间的常数: - const const::min_tons_for_storage = 10000000; ;; 0.01 TON" Z5 E8 W, C/ V. E9 V
复制代码然后,我们实现提款的逻辑。它看起来是这样的:
9 `7 |) r% \( _9 [( c- if (op == 3) {; U) T; Q) R, G% _9 d! X
- throw_unless(103, equal_slice_bits(sender_address, owner_address));
7 V* S: w5 ]1 V0 q { C - ' C$ _; B9 B# ^
- int withdraw_amount = in_msg_body~load_coins();
& F3 f& h1 f# P' d+ o) n - var [balance, _] = get_balance();9 U8 d' C- s, x0 E$ Z+ U0 x
- throw_unless(104, balance >= withdraw_amount);2 H& m3 e7 G9 {( k6 \1 r
4 x' W% }5 \; E) c2 F* i- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
5 u5 ?( f- c6 M1 w G! l- \
; Q$ Z8 g" \' u, q7 [, w0 q9 N N- ; x5 s6 o7 m; c: o+ K
- ;; TODO: Sending internal message with funds- p4 H8 j% S# v
-
, [- r! w. m, g - return();2 l$ Q4 a1 o m ?; S+ n
- }
复制代码如您所见,我们正在读取提款的代币数量(将存储在我们的 in_msg_body 在操作代码之后,我们将在下一课中完成) 检查余额是否大于或等于提款金额。 我们还使用了一种很棒的技术,确保在合约中保留最低存储金额。 让我们来详细谈谈这个发送实际资金的逻辑。 发送内部信息send_raw_message 是一个标准函数,它接受一个带有信息的Cell 和一个包含 mode 和 flag 的 integer(整数). 目前有 3 种模式和 3 个信息标志。您可以将单个模式与多个(也可以一个都没有)标志相结合,以获得所需的模式。简单地说,组合就是获得它们的值之和。关于模式和标志的说明,请参阅下面的表格 this documentation part. 在我们的示例中,我们要发送常规消息并单独支付转账费用,因此我们使用模式 0 和标志 +1 得到 mode = 1. 我们在将其传递到 send_raw_message() 之前编写的信息Cell,可能是目前你需要了解的最高级的内容。 我们将花一点时间来确保这部分内容变得清晰明了。不过,在开始之前,请允许我给你们两个建议: 让我们来分析一下要传入 send_raw_message 的信息Cell的结构。正如你已经理解的那样,我们需要在Cell中放入若干位bits,而哪个位bits负责哪项工作是有一定逻辑的。 信息Cell以 1 位前缀 0 开始,然后是三个 1 位标志,即是否禁用即时超立方路由(目前始终为 true)、如果处理过程中出现错误是否应跳转消息、消息本身是否是跳转的结果。然后,源地址和目标地址被序列化,接着是信息值和四个与信息转发费用和时间有关的整数。 如果从智能合约发送信息,其中一些字段将被改写为正确的值。特别是,验证器将重写 bounced, src, ihr_fee, fwd_fee, created_lt 和 created_at. 这意味着两件事:第一,另一个智能合约在处理信息时可以信任这些字段(发件人可以不伪造源地址、退回标志等);第二,在序列化过程中,我们可以在这些字段中加入任何有效值(无论如何,这些值都会被覆盖). - var msg = begin_cell()3 c8 d. g. @7 m
- .store_uint(0, 1) ;; tag
/ k* ~! F6 J) d/ s# o - .store_uint(1, 1) ;; ihr_disabled7 Y6 t5 {3 ?5 n" M7 l3 g( D _) i
- .store_uint(1, 1) ;; allow bounces
# d9 g# [8 F7 _8 u& ~; ? - .store_uint(0, 1) ;; not bounced itself: j' }" R/ h7 t3 R- w" {
- .store_slice(source) ;; ex. addr_none
/ S9 X5 P `8 w1 E' ^ - .store_slice(destination)
% X2 G9 P# }( x" Y1 \! c. i - ;; serialize CurrencyCollection (see below)- f* M0 N1 c b( B0 G) h
- .store_coins(amount)# {( Q/ B. Z6 j& y
- .store_dict(extra_currencies)
( Y) R% h6 n7 P - .store_coins(0) ;; ihr_fee
% }) N6 T" H- I' m- j0 g* G: k+ J - .store_coins(fwd_value) ;; fwd_fee 8 b/ s' n& T1 k
- .store_uint(cur_lt(), 64) ;; lt of transaction
& r7 Z, y! o E* o4 H# X - .store_uint(now(), 32) ;; unixtime of transaction8 e4 u1 T) j% n. P( T+ g! m
- .store_uint(0, 1) ;; no init-field flag (Maybe). Y' m& N# l& E6 d' u
- .store_uint(0, 1) ;; inplace message body flag (Either)6 p- p0 w3 W5 y' F7 m. L
- .store_slice(msg_body)
$ @, S+ y* K2 q0 h9 C- t4 d - .end_cell();
复制代码不过,开发人员通常会使用快捷方式,而不是逐步序列化所有字段。因此,让我们举例说明如何从智能合约中发送信息: - Z( @7 l4 N1 V" B/ r, D! v1 C* Y
- var msg = begin_cell()
, f. d0 Q @2 w8 } t4 v4 g3 { - .store_uint(0x18, 6)% b% g+ E% v2 d- K: }; }0 b
- .store_slice(addr)
4 b- n; m( P/ `# X; i - .store_coins(grams)) E$ z+ G/ m- n. J. P
- .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) Z+ h! E0 _% `
- .store_uint(op_code, 32)
" B+ v; C b" V. D - .store_uint(query_id, 64);
' W! N# L0 l# i9 h4 g2 r6 M - 2 S$ u* u" f- b+ L5 }3 \
- send_raw_message(msg.end_cell(), mode);
复制代码我们仔细看看到底发生了什么。 .store_uint(0x18, 6)1 g/ N) `/ o6 D) r) l4 T9 `
我们开始组成一个Cell时,将 0x18 值转换成 6 位,即 0b011000 如果我们从十六进制翻译过来。这是什么?2 N% L6 L& I! M; {. [: f9 l2 v
0b011000 因此,.store_uint(0x18, 6) 是对标签和前 4 个字段进行序列化的优化方法。 .store_slice(addr) - 这一行将目标地址序列化。 .store_coins(grams) - 这一行只是将代币的数量序列化(grams 只是一个带有代币数量的变量)。 .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - 这个问题很有趣。从技术上讲,我们只是在Cell中写入了大量的零(即零的数量等于 1 + 4 + 4 + 64 + 32 + 1 + 1)。为什么会这样?我们知道,这些值的消耗有一个清晰的结构,这意味着我们放在这里的每一个 0 都是有原因的。; k! N3 U8 c8 j7 f
有趣的是,这些零有两种工作方式--其中一些被设置为 0,因为无论如何验证器都会重写值;另一些被设置为 0,因为目前还不支持该功能(如额外货币)。
; _) R [) `- l) E为了确保我们理解为什么会有这么多零,让我们来分析一下它的预期结构: 第一位代表空的额外货币字典。 这样我们就有了两个 4 位长的字段。因为 ihr_fee 和 fwd_fee 将被覆盖,因此我们不妨将其归零。 然后,我们把 0 添加到 created_lt 和 created_at 字段。 这些字段也将被覆盖;不过,与费用相比,这些字段的长度是固定的,因此被编码为 64 位和 32 位长字符串。 下一个零位表示没有初始字段。 最后一个零位表示 msg_body 将就地序列化。 这基本上表示 msg_body 是否带有自定义布局。
2 O( V/ ~8 C* r* p& Q n' x4 |3 e& R
.store_uint(op_code, 32).store_uint(query_id, 64); 这部分是最简单的--我们传递的是一个具有自定义布局的信息正文,这意味着只要接收方知道如何处理,我们就可以在其中放置任何数据。
+ C" F% _: b" E. g; K8 `" ~3 c _+ S
" x: a* u* w: [' G我们看看如何将其应用到我们的提款代码中:
4 J/ L8 `: D; R* E9 g* H- if (op == 3) { c5 \( r+ l2 f3 J7 f+ N
- throw_unless(103, equal_slice_bits(sender_address, owner_address));
) h" D4 F0 d- D4 P' `1 J. J - 0 i; e& |# M* z7 i# _, S+ e0 ? t
- int withdraw_amount = in_msg_body~load_coins();# [3 i: l' n* \4 ?. @/ z
- var [balance, _] = get_balance();
2 f u3 h1 ^! ^0 g: k$ g! ~ - throw_unless(104, balance >= withdraw_amount);
* Q4 [* z; b( t1 J" x. s' f
_! U ^, M' U6 N: s% m- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);3 d- {" c( e. p7 ~0 _4 i; G
-
4 j4 t; \2 ]. v! n- u1 b( g - int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)
0 @; b2 v) O. h: k' z -
8 k8 @( C G+ x7 @' S$ t. w - var msg = begin_cell()
) l9 K/ U$ P6 \ - .store_uint(0x18, 6)
7 D5 c& O: o6 N' p6 J2 v- l& _ - .store_slice(sender_address)
. [! _! T( t q9 o$ @$ _ - .store_coins(return_value)) f. h, R9 H4 B/ E! u2 I) _
- .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
: r, g% k6 E, k- B. y8 X) o -
3 y) {4 p( _$ E3 U6 ?5 m2 N - send_raw_message(msg.end_cell(), msg_mode);
* E8 T; d1 E. I0 q -
; ^: k- g$ Y. Y& e! y+ l. |9 y9 v - return();! V& ]% I6 \# T, f+ ?
- }
复制代码 敲定我们的合约代码最后一件事是添加一个新的 getter 方法,用于返回合约余额: - int balance() method_id {
! c. a( E' J" \, Q - var [balance, _] = get_balance();
" y6 E5 O! Q$ F6 H* P. E - return balance;
6 R# _: X2 I/ ]' `+ T( B9 \5 Q - }
复制代码 我们再来看看最终代码的样子:9 u6 K- I/ ~5 p7 [
- #include "imports/stdlib.fc";
" S' f. V4 X( Z7 R* E2 V
/ j7 ?9 T4 j; |- const const::min_tons_for_storage = 10000000; ;; 0.01 TON1 ]7 P3 J3 @+ @' B2 A- R0 C3 H; |
+ `% g& z4 K3 n4 k4 u" I- (int, slice, slice) load_data() inline {
3 ~2 d0 Y$ e4 z% |1 K1 Z - var ds = get_data().begin_parse();
& D* c/ k1 x. j1 m& ? - return (& d2 G# L' A4 B4 J
- ds~load_uint(32), ;; counter_value
2 i; j" ^8 X! g$ } - ds~load_msg_addr(), ;; the most recent sender b% `6 y+ m- k- _! M5 ~( `9 k3 D
- ds~load_msg_addr() ;; owner_address
5 _) j) u) M/ i; N1 p - );
) S# S9 H- w" U/ B) O+ | - }5 \/ D5 p( k$ h
- + s+ \. d% ^ C+ C( l6 w5 f
- () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {1 @% x& j5 I" r
- set_data(begin_cell()
' n2 j, F! C# } - .store_uint(counter_value, 32) ;; counter_value a7 r4 C8 j, Q* ]0 C
- .store_slice(recent_sender) ;; the most recent sender
, g* J) v: o, s' d - .store_slice(owner_address) ;; owner_address
, r( B5 K* j5 o! W( a+ X* ?4 R - .end_cell());
" J. {7 @- f1 e. } - }+ D- {- f: e* Q0 w
- 2 W( O5 {& g# i% I" `" {
- () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {& A* [: P6 @ J) K2 T$ O ^$ _. |
- slice cs = in_msg.begin_parse();8 q7 [/ B G7 r) [/ \ `" \0 A
- int flags = cs~load_uint(4);
7 [" F) E e# z, x. I+ l* G" R; A - slice sender_address = cs~load_msg_addr();3 N, F( o- `% U c" `% v7 A1 i
- 8 D* ^" g9 z8 q' G
- int op = in_msg_body~load_uint(32);) O: o p6 D% O# W- _# O: h& \9 V
9 `+ A$ |- F0 Q& Q; m/ Z: a5 M. j* A- var (counter_value, recent_sender, owner_address) = load_data();$ F' A# P* b/ a
-
* `1 o: m a& G- ^ - if (op == 1) {
3 B( t: H& q, S7 w" h1 l& |+ d - save_data(counter_value + 1, sender_address, owner_address);
9 f$ S+ I/ I% z1 q, w, l9 G - return();7 J+ r7 D6 U7 L% u& X; m8 S. ^
- }
' V# v. r$ T; {$ I" z6 D/ O* |+ G
* |6 r! `2 D, D" u0 S- if (op == 2) {
, O6 d. ]% {' o% _8 n8 t - return();
& {) } G7 m& z+ S' o& \ - }, S+ R& Q2 s+ m4 w# l$ r8 ^
- & k# i+ m6 W9 t
- if (op == 3) {
2 {+ r5 V7 @3 d" ^ - throw_unless(103, equal_slice_bits(sender_address, owner_address));+ a6 R$ `5 C) f
- + ~8 u4 k8 U+ S4 I }8 y
- int withdraw_amount = in_msg_body~load_coins();9 Y' k5 ~' |$ L/ ^ V T) `$ ^
- var [balance, _] = get_balance();' j6 f1 A; t2 [- ]0 V5 J/ Y& A4 k
- throw_unless(104, balance >= withdraw_amount);" W$ U* }3 D( L' f, j6 }6 J8 \/ L
, [7 Z+ W0 i Q$ G- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);2 z. A* q, X) R Q6 D6 u9 n
/ I0 @5 F3 m4 D/ ]- q7 |; P- int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)
8 n" M& [. N! q" i
' r- Z7 S) P* ?* F& D+ L- var msg = begin_cell()
) @. _5 t- G6 K3 j6 B7 U - .store_uint(0x18, 6)" w5 H$ T- X9 K# _7 H
- .store_slice(sender_address)
5 K- ?0 [; |# ] v; a+ @$ d - .store_coins(return_value), |& ]5 W# ^' N/ k. z
- .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);' r& y4 }6 s: ]+ `2 D5 P. f1 u
( M6 j: c# y8 W, `+ \) d/ [% V$ }- send_raw_message(msg.end_cell(), msg_mode);% z9 w$ N8 y N4 ?
! G: D1 r; w1 t) |% y2 j1 B6 [- return();- f0 H9 E8 v( t2 O" G$ @. y7 O
- }
$ e9 ~5 o( g* A3 ~- _
+ `6 o* C% F: t+ I; |5 M" \3 A- throw(777);4 n' ~' o8 S) f# T! n) j/ C& p
- }
) V- {# Z8 G$ l/ H% ^ s" F
: Z. B% i4 w! n; Z! z0 V6 J- (int, slice, slice) get_contract_storage_data() method_id {" j' M) V$ G+ ^% |) Z
- var (counter_value, recent_sender, owner_address) = load_data();& `2 d) d5 G5 ]5 g3 ]2 F$ Y
- return (
3 o/ Z6 C/ m+ N - counter_value,2 W! }! U+ A5 S4 e! I4 r* O
- recent_sender,
8 C3 C" w! g8 A2 p5 b% ]; t - owner_address: i# V1 L9 ?5 W: J! S
- );
7 W6 A' n' j( v3 u - }; ?: M2 q2 o( s) C
- 6 S5 B9 |$ d" v1 D7 x4 w v
- int balance() method_id {
0 y3 G1 \8 U# V2 U( ~! K9 n# E' ^ - var [balance, _] = get_balance(); v) J, T5 M& y
- return balance;
- @3 w9 e) e# L, ] w( h/ p - }
复制代码 我们检查一下我们的代码是否能用 yarn compile 然后 让我们开始为更新后的合约编写测试。
) l% y! l- \9 E& @) K% M2 s5 ?$ z4 R% o4 ~$ B5 F" ?7 x: l. m
& M- H& Y p; f3 u2 L8 o
|