在这一课中,我们将练习执行更多不同的命令、创建函数,甚至从合约内部发送信息。我们将在现有合约的基础上完成所有这些工作。 我们的计划是什么?让我们来分析一下: 我们的合约对指令的要求将变得更加严格,我们将引入一个新的操作码,用于向合约存入资金的逻辑。 我们要引入的另一个操作代码是提取资金。在提取资金过程中,资金实际上是以消息的形式发送到地址的,因此我们将学习如何在合约中发送消息 任何以未知操作代码到达的资金都将返回给发送方 我们将在存储中引入一个新值--合约所有者。只有合约所有者才能提取资金。我们还将在函数中分离存储加载和写入逻辑,以保持主代码的整洁
4 ~2 @6 @' m1 W/ }% D" E( D: a ]5 R
准备好了吗?就绪。开始! 分离存储管理逻辑首先,让我们来处理合约所有者存储中的数据(我们需要记住,我们必须在启动合约时将该地址放入存储中)。我们还将创建两个新函数-load_data 和 save_data: - (int, slice, slice) load_data() inline {
) Y$ q/ P4 a) [# s* Y3 J5 ~! k - var ds = get_data().begin_parse();3 r1 t& J: f3 x- G8 b l, e8 @
- return (5 e) [" d# q, ?4 W" s: d
- ds~load_uint(32), ;; counter_value* f6 k2 D* b5 A4 x
- ds~load_msg_addr(), ;; the most recent sender
5 L/ U" E6 j3 E( ]5 [ - ds~load_msg_addr() ;; owner_address2 V) b' {/ D6 S
- );1 ] F0 L. ?6 z4 s7 |7 H% z
- }
- t, S. @4 V9 y: k$ ? M" ?9 \ - ; w7 c3 }8 S% ^. y0 t
- () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {
4 O0 M1 ^, c. E/ Q - set_data(begin_cell(); D. @: n- g; a: ?0 L O6 b
- .store_uint(counter_value, 32) ;; counter_value% W a& G) V3 v2 }2 h# D. O
- .store_slice(recent_sender) ;; the most recent sender
& ~0 D) z1 D( t) w1 K' F: O, f - .store_slice(owner_address) ;; owner_address
- a. I7 J( F8 e4 _; K - .end_cell());' e- N5 W0 P4 M X Y4 Q% H) Y
- }
复制代码这里有几点需要注意: 我们将在代码中进一步使用这些函数: - var (counter_value, recent_sender, owner_address) = load_data();0 G4 G* h+ n) Q( D' D
, r ~0 G& }, D; F# Q) @' J8 t- save_data(counter_value,recent_sender,owner_address);
复制代码有几点需要注意: 总的来说,代码看起来很简洁,不是吗? 存款和取款对我们来说,存款 (op == 2) 非常简单,因为在这种情况下,我们只需成功完成执行,使得资金被接受。否则,资金将退回给发件人。 - if (op == 2) {3 D9 q/ [5 O/ ?* a: Z" u- m
- return();
6 g* p, Q: Q/ p* \- ^5 |; Q - }
复制代码不过,取款时情况会变得复杂一些。我们必须将发件人地址与智能合约的所有者地址进行比较。让我们来看看,为了实现这个逻辑,我们需要知道哪些事情: 要比较所有者地址和发送者地址,我们使用 FunC 标准函数 equal_slice_bits() 我们使用 throw_unless() 函数在比较结果为 false. 还有另一种通过错误的方法-throw_if(),如果传入该函数的条件返回 true,该函数就会抛出错误. 带有此操作的消息正文还需要有一个整数,说明要求提取的金额。我们将此金额与合约的实际余额进行比较(标准 FunC 函数 get_balance())。 我们希望我们的合约总是有一些资金,能够支付租金和必要的费用(在第 1 章中了解更多关于费用的信息),因此我们需要设置合约中必须保留的最低金额,如果请求的金额不允许,则会抛出错误。 最后,我们需要学习如何通过内部信息从合约内部发送代币 ; W; G3 w3 @6 H& P, n3 j. K0 ?9 `
3 y" ]; ~. ~7 y* V; M1 a5 U/ a: f我们看看这一切是如何实现的。 首先,我们设置最小所需存储空间的常数: - const const::min_tons_for_storage = 10000000; ;; 0.01 TON
* p. c8 z+ w* X9 q% T$ U
复制代码然后,我们实现提款的逻辑。它看起来是这样的: $ |4 {8 I7 K, x
- if (op == 3) {, g6 |0 i& d# B) v0 E a/ f3 d
- throw_unless(103, equal_slice_bits(sender_address, owner_address));' L: R% `# R" n# i: i- V. N
- 3 c- E. c. p+ T3 U# i2 L9 A9 u
- int withdraw_amount = in_msg_body~load_coins();
$ y7 \+ U' d \7 G - var [balance, _] = get_balance();$ e0 B4 O: }6 C
- throw_unless(104, balance >= withdraw_amount);
2 b. G, O) V& ^7 T9 O4 X8 V - & G, U5 g5 v( E \2 h
- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);- A8 }$ o6 H9 m. Q/ K* O6 ]2 V
) X# R; B! x. N- $ k2 X, O9 S) H+ x) N5 z/ o, ]
- ;; TODO: Sending internal message with funds
& H( z' r6 L) {/ c/ \: D& F - , k) D5 i; l3 h T+ y& a7 ]! K
- return();; V4 W: S E8 [. u, j
- }
复制代码如您所见,我们正在读取提款的代币数量(将存储在我们的 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(); n. ]% K0 L% O) U
- .store_uint(0, 1) ;; tag
+ H; K/ @; A7 l( u - .store_uint(1, 1) ;; ihr_disabled- U% @. ^; P3 n- e& x
- .store_uint(1, 1) ;; allow bounces
. M/ |. E" G% ^, h( Q - .store_uint(0, 1) ;; not bounced itself8 m- h- u8 [$ f% w% l# m3 A
- .store_slice(source) ;; ex. addr_none& Q0 k, p/ h% V# D( L$ O
- .store_slice(destination)
! x6 f& I! a$ n8 a - ;; serialize CurrencyCollection (see below)
7 P+ ?2 Q% l- P$ x9 i - .store_coins(amount)
$ g* ?! T% v2 \* x - .store_dict(extra_currencies)3 s+ G7 W, v& h9 V
- .store_coins(0) ;; ihr_fee
# h+ @ h- C7 a, w - .store_coins(fwd_value) ;; fwd_fee
3 r- J w, l! I0 r+ ` - .store_uint(cur_lt(), 64) ;; lt of transaction) x- e2 i+ c. o$ K. c2 \5 i
- .store_uint(now(), 32) ;; unixtime of transaction0 T- O; o: {+ F
- .store_uint(0, 1) ;; no init-field flag (Maybe)
' U0 j5 S3 c" i- ]+ t, L - .store_uint(0, 1) ;; inplace message body flag (Either)
( l; D* W5 p9 t% o m: `/ ]% o9 t - .store_slice(msg_body)! Q p% r8 m. T! a1 b \% L" t
- .end_cell();
复制代码不过,开发人员通常会使用快捷方式,而不是逐步序列化所有字段。因此,让我们举例说明如何从智能合约中发送信息:
1 J, r& C( g$ x1 I9 f; B% m2 @- var msg = begin_cell()
& f2 S& i, y. j - .store_uint(0x18, 6)2 _3 a2 Q1 \4 a3 G) F
- .store_slice(addr)
/ o8 i! c" t- M: r8 _* b. L - .store_coins(grams)
/ U: r9 m A4 V# n& V3 s - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)( s* S6 a/ Z5 j) }1 U
- .store_uint(op_code, 32)
9 u/ {' i+ D4 e6 T) i* W - .store_uint(query_id, 64);
; T* {% {# [6 `2 E3 f! a -
! p$ A0 k! \0 r9 u - send_raw_message(msg.end_cell(), mode);
复制代码我们仔细看看到底发生了什么。 .store_uint(0x18, 6)! }( ]# d" j* ]
我们开始组成一个Cell时,将 0x18 值转换成 6 位,即 0b011000 如果我们从十六进制翻译过来。这是什么?
- q4 s% L U; e9 p9 N( ] ^) k& t0b011000 因此,.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 都是有原因的。- T4 ] n q/ M% c; m) i
有趣的是,这些零有两种工作方式--其中一些被设置为 0,因为无论如何验证器都会重写值;另一些被设置为 0,因为目前还不支持该功能(如额外货币)。
5 z& j2 A0 H3 ]2 n% n6 x) b为了确保我们理解为什么会有这么多零,让我们来分析一下它的预期结构: 第一位代表空的额外货币字典。 这样我们就有了两个 4 位长的字段。因为 ihr_fee 和 fwd_fee 将被覆盖,因此我们不妨将其归零。 然后,我们把 0 添加到 created_lt 和 created_at 字段。 这些字段也将被覆盖;不过,与费用相比,这些字段的长度是固定的,因此被编码为 64 位和 32 位长字符串。 下一个零位表示没有初始字段。 最后一个零位表示 msg_body 将就地序列化。 这基本上表示 msg_body 是否带有自定义布局。
M5 t( }% X! p( _
.store_uint(op_code, 32).store_uint(query_id, 64); 这部分是最简单的--我们传递的是一个具有自定义布局的信息正文,这意味着只要接收方知道如何处理,我们就可以在其中放置任何数据。
3 m% g2 G. T N
" K* E0 p! D" c a$ r# b) o3 ^我们看看如何将其应用到我们的提款代码中:
" k+ k$ o9 e1 t g; q% ^% V- if (op == 3) {
; r1 r: `7 v, _7 [. b+ {+ ?" _0 i7 l6 f - throw_unless(103, equal_slice_bits(sender_address, owner_address));
# g* `2 c+ \ |6 w2 M( q% r8 b - # W( S L9 }- B; l
- int withdraw_amount = in_msg_body~load_coins();
& E% d2 J7 ~0 r" Y) }6 J+ H7 F9 A9 J - var [balance, _] = get_balance();7 C% N! R, f" x6 z- `
- throw_unless(104, balance >= withdraw_amount);
( m+ |" Q5 @5 p5 n; X, ` - 4 U: j' B! L: K" @, m
- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage); S' l, Z. n1 d! _
- 0 y6 P; h" b7 X" R- W
- int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)3 C5 V/ c& L% S' ]2 ~! f3 u
- # k$ i' R! h8 W8 n
- var msg = begin_cell()/ A9 n2 Q6 |: s9 ?- w8 }
- .store_uint(0x18, 6)2 T4 E2 j9 ]. \5 ^, B
- .store_slice(sender_address)
4 ^: X1 |4 }, N; ~: E; x$ Q' y9 R - .store_coins(return_value)
4 c4 t% I' z9 Y o7 l9 c9 z - .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
( q1 \" H3 O! ~7 T0 `* r( Y' } -
k& B7 c% e1 {" c6 ^7 ^! Y' q6 z- Y - send_raw_message(msg.end_cell(), msg_mode);
! D# T8 B6 ]% X3 X8 ` -
" h; n$ X& M5 N5 k/ w - return();& n, K: A! ?& i" P. P7 V# T* \
- }
复制代码 敲定我们的合约代码最后一件事是添加一个新的 getter 方法,用于返回合约余额: - int balance() method_id {
( D2 n, u% C) Z8 Q9 o - var [balance, _] = get_balance();
( \/ N7 o0 J, i' r j6 g+ m0 | - return balance;
# Z- D- c: V8 q! ? - }
复制代码 我们再来看看最终代码的样子:! ]# }. l2 a h2 P* F
- #include "imports/stdlib.fc";/ m/ `( O ^* W' z* f1 |0 Y) f
- 2 d7 D* ^, `3 y( V
- const const::min_tons_for_storage = 10000000; ;; 0.01 TON6 e1 @6 d% Z2 C% y; I1 L( L' w6 T
- % t1 Z e7 @3 \% Y! i/ s; _- r
- (int, slice, slice) load_data() inline {) |9 d& b3 b; s$ p
- var ds = get_data().begin_parse(); U% S0 [ w) h$ u
- return (
5 _6 P) R- T; @7 U/ N1 n - ds~load_uint(32), ;; counter_value
% ~3 e4 F6 C( o- V! u' t9 n5 P3 Q - ds~load_msg_addr(), ;; the most recent sender$ x. z5 y6 G; A5 D0 k, O
- ds~load_msg_addr() ;; owner_address3 A. x$ F( z& j% U
- );* k8 x1 S" H" V
- }( c9 @$ |# r% j
- 3 F' T3 ^4 k' Z1 @- R7 Q
- () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {4 ?* G0 R) O# L' }0 b4 n
- set_data(begin_cell()
5 P1 V, q' C% W- O, P3 F. b - .store_uint(counter_value, 32) ;; counter_value
) o( A+ t4 f) z. G- A$ e - .store_slice(recent_sender) ;; the most recent sender
' f0 n" S; P0 L - .store_slice(owner_address) ;; owner_address
; s1 z2 i" A' C3 l; \ K - .end_cell());; f! k- c! O. P5 W7 X0 K: U
- }
4 {3 F j2 T( f; }# Q* n
, e* U# b, ^" R! _9 c, U; W- () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
+ L, T1 S$ |4 V - slice cs = in_msg.begin_parse();
" T# U5 Q& U/ C8 O9 b1 Q - int flags = cs~load_uint(4);
3 f7 d" c' @+ G+ R - slice sender_address = cs~load_msg_addr();
7 `2 E/ n# ]. l' _2 | - . _( ]3 h: P5 c- J8 k; ^5 D3 q8 J
- int op = in_msg_body~load_uint(32);
' X8 H7 Q) R4 A
+ c% z2 }5 O! T7 S+ G. X- var (counter_value, recent_sender, owner_address) = load_data();2 u" m7 \1 _1 [: V4 r1 D: n7 w
-
4 L5 E( A+ }: _/ h! O$ c. z$ e - if (op == 1) {
7 o6 d8 o& q# E- z+ S - save_data(counter_value + 1, sender_address, owner_address);1 P- _5 u5 N, n; G
- return();- P4 D; \% T7 b+ I# Q
- }
' f. ~/ n" N) W9 z2 [
( W! q- Q) Z, Z; k' X Q0 w% N- if (op == 2) {
( I/ L: Q8 J' I7 s - return();
0 A4 w. M L" }& b: L& Y7 \ - }2 i% N% d' \6 j. f
- 2 U2 `$ ^! ?2 R4 n# W4 E
- if (op == 3) {
! O2 u) S, d" T3 J9 t( |. u - throw_unless(103, equal_slice_bits(sender_address, owner_address));
4 k; h$ R! Z# `$ A9 E+ l6 @ - " f3 m2 w& X$ x" ^
- int withdraw_amount = in_msg_body~load_coins();
+ S/ M( i) c- w" Z$ O - var [balance, _] = get_balance();3 r0 q" q% R& M
- throw_unless(104, balance >= withdraw_amount);
% r/ |9 ^0 \% h2 V6 S N: i( W% M# ^ - 5 J+ e( n5 Y$ C1 b4 X( [
- int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
* Y1 h0 y+ [: n5 i% _1 A+ O* _ - 7 _3 h! Z. J/ A/ D3 S# ^7 Q/ b
- int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)7 q. v( x! A, v! O' ]5 l
3 d) k+ b8 a$ Q @7 x- var msg = begin_cell()1 G* P: k' M( ]' A$ D
- .store_uint(0x18, 6)
: |% ~5 i1 p% u. |. ]. o9 ? - .store_slice(sender_address)8 X/ \2 V, a& ]2 ?: @, y2 m4 |
- .store_coins(return_value)7 | O! z( x' f5 K/ ?$ Y& p4 k& Z
- .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);& b! U3 K" X0 F" \
- 0 u3 c7 S: h& J5 H" b% e/ h
- send_raw_message(msg.end_cell(), msg_mode);- q) n; s! {7 k/ ]" y! I* y" h' D
0 D' [ ?/ J' d! p- V- return();
/ W" K' x8 @. W5 [ - }
( ]. ~# t, c( [4 m5 ` - 7 Y# n+ G/ }2 K* R' G
- throw(777);
+ m" P- ~0 e6 L6 ` - }; P$ o1 ^7 ~/ g' s& v B
. f- X" N- E% L- H) J- (int, slice, slice) get_contract_storage_data() method_id {+ l" t$ V! M: u" I& r
- var (counter_value, recent_sender, owner_address) = load_data();# J! _) Z% ~8 C: {' ?
- return (& Y7 K$ P0 ?: Y/ @% V( u
- counter_value,6 w; B2 D4 ~+ l& ?' D8 D
- recent_sender,7 J! P/ T( l/ f1 K
- owner_address: T1 \% N4 G# C ~
- );- Y% R5 U' e/ R% l5 U2 E1 G. s) m9 j
- }# z& q' _% W; N; W
- 6 [# N. E( A& |+ s$ S
- int balance() method_id {
+ C9 y2 i5 n T1 ^ z; \ - var [balance, _] = get_balance();4 U1 t* w; O, |) q- D( P
- return balance;
! o! \" B8 D# i - }
复制代码 我们检查一下我们的代码是否能用 yarn compile 然后 让我们开始为更新后的合约编写测试。
/ x8 }( w: x4 D% Q) y8 i8 S5 ^4 v6 ~+ }- W3 {0 v# p
2 D5 _6 r2 y' ]- _7 o
|