English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french

简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE คนไทย Français русский

回答

收藏

4.3 带有存款/提款逻辑的合同

开源社区 开源社区 9847 人阅读 | 0 人回复 | 2025-03-08

在这一课中,我们将练习执行更多不同的命令、创建函数,甚至从合约内部发送信息。我们将在现有合约的基础上完成所有这些工作。
我们的计划是什么?让我们来分析一下:
  • 我们的合约对指令的要求将变得更加严格,我们将引入一个新的操作码,用于向合约存入资金的逻辑。
  • 我们要引入的另一个操作代码是提取资金。在提取资金过程中,资金实际上是以消息的形式发送到地址的,因此我们将学习如何在合约中发送消息
  • 任何以未知操作代码到达的资金都将返回给发送方
  • 我们将在存储中引入一个新值--合约所有者。只有合约所有者才能提取资金。我们还将在函数中分离存储加载和写入逻辑,以保持主代码的整洁
    3 n: |4 i" g5 |+ V# I
准备好了吗?就绪。开始!
分离存储管理逻辑
首先,让我们来处理合约所有者存储中的数据(我们需要记住,我们必须在启动合约时将该地址放入存储中)。我们还将创建两个新函数-load_data 和 save_data:
  1. (int, slice, slice) load_data() inline {& W% z2 \5 A+ {$ `! G
  2.   var ds = get_data().begin_parse();$ M: L9 D0 X4 q* l* Q" j+ v
  3.   return (6 g; ~# ]! P0 I6 ~9 w0 Y
  4.           ds~load_uint(32), ;; counter_value
    9 ~/ ?4 @: N( Y2 T, {9 b
  5.            ds~load_msg_addr(), ;; the most recent sender: f& p" R& b0 |  ?1 \  s1 C- j, h
  6.             ds~load_msg_addr() ;; owner_address
    % t2 T0 p' B( y$ q
  7.   );
    7 Y6 ^" ]% q$ ]0 s
  8. }" Y9 v4 _( G6 ], ]) K, O3 e

  9. 1 [2 {* I' E4 @+ N/ N$ _
  10. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {( p  k. L* K4 v
  11.   set_data(begin_cell()) }) `* t3 ^: s" e8 t
  12.     .store_uint(counter_value, 32) ;; counter_value8 ^% K+ Z2 b& ]$ E6 Z
  13.     .store_slice(recent_sender) ;; the most recent sender
    # Y) m/ t; r% J. T
  14.     .store_slice(owner_address) ;; owner_address
    ; T* G  J: c) j+ p: O+ p5 `
  15.     .end_cell());. r: O0 K& J/ V3 d
  16. }
复制代码
这里有几点需要注意:
  • inline 指定符。你已经对指定符有所了解。如果一个函数有内联指定符,那么在调用该函数的每一个地方,它的代码实际上都会被替换。禁止在内联函数中使用递归调用。
  • 为什么 load_data() 没有 impure 函数指定符?我们在第 3 章中已经讨论过这个问题,现在让我们来复习一下。答案是,因为这个函数不会影响合约的状态。它是只读的。
    5 g8 t+ W- Z" G! Y& |5 t( W' R3 @& E
我们将在代码中进一步使用这些函数:
  1. var (counter_value, recent_sender, owner_address) = load_data();2 ?7 i" |" \6 P5 e
  2. + W5 Z5 e* d" ?
  3. save_data(counter_value,recent_sender,owner_address);
复制代码
有几点需要注意:
  • 我们还更新了 get_the_latest_sender 函数,以返回所有者地址
  • 如果从信息正文中读取的操作码没有触发任何 if 语句 - 我们会抛出一个代码为 777 的错误。这个数字可以自己选择,但要确保它不会与 official error exit codes 相同. (https://ton.org/docs/learn/tvm-instructions/tvm-exit-codes) exit_code 如果大于 1,则视为错误代码,因此退出时如果出现这样的代码,可能会导致交易回退/反弹。
  • 我们使用函数 return() 成功退出合约执行
    - `* N5 m7 D6 z9 @& r
总的来说,代码看起来很简洁,不是吗?
存款和取款
对我们来说,存款 (op == 2) 非常简单,因为在这种情况下,我们只需成功完成执行,使得资金被接受。否则,资金将退回给发件人。
  1. if (op == 2) {8 J9 V' U- g9 b% s4 Y
  2.     return();
    2 X0 o- F  N( P- b
  3. }
复制代码
不过,取款时情况会变得复杂一些。我们必须将发件人地址与智能合约的所有者地址进行比较。让我们来看看,为了实现这个逻辑,我们需要知道哪些事情:
  • 要比较所有者地址和发送者地址,我们使用 FunC 标准函数 equal_slice_bits()
  • 我们使用 throw_unless() 函数在比较结果为 false. 还有另一种通过错误的方法-throw_if(),如果传入该函数的条件返回 true,该函数就会抛出错误.
  • 带有此操作的消息正文还需要有一个整数,说明要求提取的金额。我们将此金额与合约的实际余额进行比较(标准 FunC 函数 get_balance())。
  • 我们希望我们的合约总是有一些资金,能够支付租金和必要的费用(在第 1 章中了解更多关于费用的信息),因此我们需要设置合约中必须保留的最低金额,如果请求的金额不允许,则会抛出错误。
  • 最后,我们需要学习如何通过内部信息从合约内部发送代币
    ) r. G6 d- H8 R" s$ s  K

# Q  f2 d# Y! p) Y1 g! I
我们看看这一切是如何实现的。
首先,我们设置最小所需存储空间的常数:
  1. const const::min_tons_for_storage = 10000000; ;; 0.01 TON
    / J9 c% q" f5 |; H1 s3 P4 c
复制代码
然后,我们实现提款的逻辑。它看起来是这样的:

+ g$ P) X# X( S' \( k% B
  1. if (op == 3) {0 _8 T  l9 [- h5 b# D; p
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));* {4 Z9 A4 w+ z

  3. 0 N' O& L$ Y+ J8 j7 l
  4.     int withdraw_amount = in_msg_body~load_coins();
    ( m/ H  w5 ]- P! a/ G) F
  5.     var [balance, _] = get_balance();
    0 R, u$ Q/ n& z) c/ r& j
  6.     throw_unless(104, balance >= withdraw_amount);
    & ~/ L3 X8 P3 v( X" J& C; Z
  7. - w  k- y* ^1 i' \3 K
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);- r. e3 m# C) X( k. X. `
  9. 3 N7 V4 k/ t7 b2 t, X5 J# \. J) X9 W; V
  10.     $ U& S1 S/ J8 w  W3 `1 g: L% H# E; Z. G
  11.     ;; TODO: Sending internal message with funds
      f) H# V3 \; |3 h
  12.     " B4 M, L1 C9 J9 q( `
  13.     return();
    9 o$ @) _1 K% L$ e) l
  14. }
复制代码
如您所见,我们正在读取提款的代币数量(将存储在我们的 in_msg_body 在操作代码之后,我们将在下一课中完成) 检查余额是否大于或等于提款金额。
我们还使用了一种很棒的技术,确保在合约中保留最低存储金额。
让我们来详细谈谈这个发送实际资金的逻辑。
发送内部信息
send_raw_message 是一个标准函数,它接受一个带有信息的Cell 和一个包含 mode 和 flag 的 integer(整数). 目前有 3 种模式和 3 个信息标志。您可以将单个模式与多个(也可以一个都没有)标志相结合,以获得所需的模式。简单地说,组合就是获得它们的值之和。关于模式和标志的说明,请参阅下面的表格  this documentation part.
在我们的示例中,我们要发送常规消息并单独支付转账费用,因此我们使用模式 0 和标志 +1 得到 mode = 1.
我们在将其传递到 send_raw_message() 之前编写的信息Cell,可能是目前你需要了解的最高级的内容。
我们将花一点时间来确保这部分内容变得清晰明了。不过,在开始之前,请允许我给你们两个建议:
  • 首先--习惯数据在Cell中的存储方式。 这就是 serialization. 此时,您需要熟悉并习惯这一点,因为您会经常将数据序列化到Cell中,因为所有数据都存储在Cell中。
  • 第二-适应 TON documentation. 这几乎是处理所有这些序列化结构的唯一方法,因为要记住它们非常困难。

    , x  V, j+ V+ g+ a) |9 B; U3 R
您可能会发现一个非常有用的东西 old documentation portal, 因为它有一些主题是以一种非常基本且良好的方式进行阐述。
让我们来分析一下要传入 send_raw_message 的信息Cell的结构。正如你已经理解的那样,我们需要在Cell中放入若干位bits,而哪个位bits负责哪项工作是有一定逻辑的。
信息Cell以 1 位前缀 0 开始,然后是三个 1 位标志,即是否禁用即时超立方路由(目前始终为 true)、如果处理过程中出现错误是否应跳转消息、消息本身是否是跳转的结果。然后,源地址和目标地址被序列化,接着是信息值和四个与信息转发费用和时间有关的整数。
如果从智能合约发送信息,其中一些字段将被改写为正确的值。特别是,验证器将重写 bounced, src, ihr_fee, fwd_fee, created_lt 和 created_at. 这意味着两件事:第一,另一个智能合约在处理信息时可以信任这些字段(发件人可以不伪造源地址、退回标志等);第二,在序列化过程中,我们可以在这些字段中加入任何有效值(无论如何,这些值都会被覆盖).
信息的直接序列化如下(摘自 documentation portal):
  1. var msg = begin_cell()! U  N1 M4 x" }# f3 Q
  2.     .store_uint(0, 1) ;; tag
    " u4 A9 ?' t0 P# h* v* B
  3.     .store_uint(1, 1) ;; ihr_disabled. o" v' s8 R  g6 ?( U
  4.     .store_uint(1, 1) ;; allow bounces: w+ G7 @( h  _9 e5 `; ~
  5.     .store_uint(0, 1) ;; not bounced itself
    ' G0 Y" }) k1 a) l# v6 H: X
  6.     .store_slice(source) ;; ex. addr_none, N3 S8 g' Q6 f' {3 N2 N
  7.     .store_slice(destination)+ T7 e) N0 T4 h5 J! I
  8.     ;; serialize CurrencyCollection (see below)0 t5 v+ W, L+ O; w& Q
  9.     .store_coins(amount)% k' y9 |- g. H% p3 {; l4 V
  10.     .store_dict(extra_currencies)
    1 M/ e! i) k0 \7 d' |
  11.     .store_coins(0) ;; ihr_fee9 o! s; k3 {. u8 c
  12.     .store_coins(fwd_value) ;; fwd_fee
    $ S/ ]& n1 r2 `5 c$ C
  13.     .store_uint(cur_lt(), 64) ;; lt of transaction- Y9 ~: S0 [) ~; s; |
  14.     .store_uint(now(), 32) ;; unixtime of transaction
      ]; w7 Z9 k5 \& |
  15.     .store_uint(0,  1) ;; no init-field flag (Maybe)8 \, S, ?3 A! k5 D$ a
  16.     .store_uint(0,  1) ;; inplace message body flag (Either), |1 D) v$ P) N, \! B
  17.     .store_slice(msg_body)) {  \2 ]4 I' Z2 }8 X( Y% ^" r
  18.   .end_cell();
复制代码
不过,开发人员通常会使用快捷方式,而不是逐步序列化所有字段。因此,让我们举例说明如何从智能合约中发送信息:
/ X7 M) h* w( l
  1. var msg = begin_cell()* y0 V$ k0 ]+ Q* a3 f) q
  2.     .store_uint(0x18, 6)5 C3 G: w2 _# H+ o5 @6 @# j
  3.     .store_slice(addr)9 i, l% I' U& ~2 n
  4.     .store_coins(grams)
    9 _) H- Z& ?8 @/ A) \( j( W0 u
  5.     .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    # T* r$ `+ x- M
  6.     .store_uint(op_code, 32)
    . w4 M  X) a+ U; p6 d; r1 K* ?4 J
  7.     .store_uint(query_id, 64);
    , S$ s, b# ?9 \6 q; P. \8 B/ e& q
  8.    
    - @1 d! _# u: k! I9 L; J
  9. send_raw_message(msg.end_cell(), mode);
复制代码
我们仔细看看到底发生了什么。
  • .store_uint(0x18, 6)! x+ M  }% o( e4 u
    我们开始组成一个Cell时,将 0x18 值转换成 6 位,即 0b011000 如果我们从十六进制翻译过来。这是什么?
    2 R' x1 U# ~1 e1 s0b011000
    • 第一位是 0-1 位前缀,表示它是 int_msg_info (内部信息)。
    • 然后有 3 个位 1、1 和 0,即
      • 禁用即时超立方路由(我们将不详细介绍它是什么,因为它太基础,您在编写合约时暂时用不上)
      • 信息可能被退回
      • 信息本身不是退回的结果。
        8 H2 l1 g4 w9 h2 ?
    • 然后应该有发件人地址,但由于它无论如何都会被重写(当验证器将发件人的实际地址放在这里时,就像我们上面讨论的那样),因此任何有效地址都可以存储在这里。最短的有效地址序列化是 addr_none,它序列化为一个两位字符串 00。

      " @7 h4 n" W" i' _' l; {8 |
  • 因此,.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 都是有原因的。% j5 {$ K. r  o! C
    有趣的是,这些零有两种工作方式--其中一些被设置为 0,因为无论如何验证器都会重写值;另一些被设置为 0,因为目前还不支持该功能(如额外货币)。
    9 P# T2 \) P7 S/ j3 |为了确保我们理解为什么会有这么多零,让我们来分析一下它的预期结构:
    • 第一位代表空的额外货币字典。
    • 这样我们就有了两个 4 位长的字段。因为 ihr_fee 和 fwd_fee 将被覆盖,因此我们不妨将其归零。
    • 然后,我们把 0 添加到 created_lt 和 created_at 字段。 这些字段也将被覆盖;不过,与费用相比,这些字段的长度是固定的,因此被编码为 64 位和 32 位长字符串。
    • 下一个零位表示没有初始字段。
    • 最后一个零位表示 msg_body 将就地序列化。 这基本上表示 msg_body 是否带有自定义布局。
      : s6 W2 q6 ?, T5 j3 u
  • .store_uint(op_code, 32).store_uint(query_id, 64); 这部分是最简单的--我们传递的是一个具有自定义布局的信息正文,这意味着只要接收方知道如何处理,我们就可以在其中放置任何数据。

    1 }' O, h1 t3 a) r& ?7 f

, P  p% L5 }0 [我们看看如何将其应用到我们的提款代码中:
! u+ G8 ]' J& A/ D
  1. if (op == 3) {
    : h8 s1 J* P8 w4 b* c
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));" o! O, W4 F0 g1 ~; I) R, I( m

  3. * }( ^: h; {$ s( m4 `& R
  4.     int withdraw_amount = in_msg_body~load_coins();% _% u7 N( r$ _# F7 ~% e$ R* |
  5.     var [balance, _] = get_balance();
    : [9 Q5 q( b0 g: o3 W6 K
  6.     throw_unless(104, balance >= withdraw_amount);
    5 b3 M. _$ |6 ?' X1 s
  7. - h2 W$ D! x5 D$ y
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);: x# b* T$ @3 S5 l
  9.     - E+ X5 ^# B' d& T! `  U( w, G; r
  10.     int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)$ H' C' w* @  k2 w: P$ p5 H5 [5 B
  11.    
    9 v* j6 W: c6 F4 C5 l$ H
  12.     var msg = begin_cell()
    4 @( y& W3 Z: U, E- I
  13.         .store_uint(0x18, 6)
    6 V, X) v8 }0 T* g
  14.         .store_slice(sender_address); x$ [& v- F+ b# Z6 r
  15.         .store_coins(return_value)1 G5 G; {$ j9 r: Q$ D
  16.         .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
    - p2 f: `) e6 e* x7 R( _
  17.    
    / R1 M* t) @4 l7 p' H
  18.     send_raw_message(msg.end_cell(), msg_mode);
    2 M3 {. a! _# s" r4 N
  19.    
    # |* |9 ?* b9 c, d# }6 h1 R# T# I1 R
  20.     return();
    , I' p% x0 f9 C5 G6 U. h
  21. }
复制代码
敲定我们的合约代码
最后一件事是添加一个新的 getter 方法,用于返回合约余额:
  1. int balance() method_id {
    8 C  z( l. V. ^  j. L9 G( m* R
  2.   var [balance, _] = get_balance();
    ' K/ A1 _6 o& r: I
  3.   return balance;
    3 d2 p4 L* Z2 W( ~
  4. }
复制代码
我们再来看看最终代码的样子:
# O' D- n. I0 A
  1. #include "imports/stdlib.fc";3 A! G( `& K" y8 u3 h8 }* q

  2. . }' j! B7 u9 c  m2 N. x
  3. const const::min_tons_for_storage = 10000000; ;; 0.01 TON6 z3 i+ \8 s, k$ @* G* W, Z9 `

  4. 0 `& d0 a2 B6 b* M/ O- T
  5. (int, slice, slice) load_data() inline {! `2 `& q0 q# l
  6.   var ds = get_data().begin_parse();
    " U0 I1 B5 S  x! a( a, G
  7.   return (2 e7 R7 }. e, w1 S5 j; ]) x
  8.     ds~load_uint(32), ;; counter_value" t$ E8 u* `; f9 H; S6 X0 F3 h
  9.     ds~load_msg_addr(), ;; the most recent sender
    # W( e% H* y' f8 n" y1 k- P( _
  10.     ds~load_msg_addr() ;; owner_address5 z. g) y% h( {4 z0 T: ?
  11.   );
    " @# N* S8 J7 f- D4 Q
  12. }
    6 K- ^6 T$ g& q; i& O7 g7 A& y
  13. - K1 p; X! u+ G
  14. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {
    4 c2 z* V: k; C2 L7 |
  15.   set_data(begin_cell()8 l& P/ R% ^. K, d8 J2 s! D6 o
  16.     .store_uint(counter_value, 32) ;; counter_value. L6 j& ?& q: _! [: n/ P! I
  17.     .store_slice(recent_sender) ;; the most recent sender5 Q/ Z. O( u2 D
  18.     .store_slice(owner_address) ;; owner_address
    - m0 @4 s9 e" J
  19.     .end_cell());* t2 q$ l" U9 t! ^
  20. }0 B/ j  ~+ F& U

  21. 5 l# `* p& X7 F! d: U
  22. () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {1 e& w% D, D, v4 j
  23.   slice cs = in_msg.begin_parse();  w/ j' b3 m# R, y* v2 t
  24.   int flags = cs~load_uint(4);( M# D6 D5 o8 B8 @! a) R. Z
  25.   slice sender_address = cs~load_msg_addr();
      @9 T6 K. O& p. c: N/ ~, Q

  26. $ s+ m8 T3 v5 D& O' z4 V  `" j
  27.   int op = in_msg_body~load_uint(32);! C& U) e- l9 E- o

  28. & X8 X" i' i9 `. P& B
  29.   var (counter_value, recent_sender, owner_address) = load_data();6 m3 C. V" h: b' c  T" F
  30.         9 x$ ]. v: l/ I: I" B; T8 w
  31.   if (op == 1) {/ P% A& K  k& p* J! \
  32.     save_data(counter_value + 1, sender_address, owner_address);) e/ D' M" n6 u' a' G
  33.     return();
    7 ^$ ^; d2 }" s( n6 O& K6 H
  34.   }* A* c. l' Z6 d( K& e

  35. 4 W- `5 ^& Q5 ^2 E9 z
  36.   if (op == 2) {
    8 Y) S) p( K, N$ D7 \
  37.     return();0 Z4 t1 W3 ~7 M& Y& |0 u; d5 o5 s2 I, y
  38.   }
    0 Y; q3 Q% v/ [0 b7 z
  39. . G  s! ~. i- w8 }! W
  40.   if (op == 3) {. ]$ d" a; ~$ T/ l
  41.         throw_unless(103, equal_slice_bits(sender_address, owner_address));
    ) W# Z$ e1 q8 d1 K  r9 `0 P
  42. : q% s8 Y5 P9 Z( f7 U/ D5 R/ l
  43.         int withdraw_amount = in_msg_body~load_coins();
    4 b2 `" \% G' S* ^6 E
  44.         var [balance, _] = get_balance();
    1 j' i+ I9 x- v9 \4 ]
  45.         throw_unless(104, balance >= withdraw_amount);+ v' `" [0 x0 d/ H4 c

  46.   M$ q7 F: L4 q3 P9 O1 |
  47.         int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);: J( Z. z$ q; r
  48. * i- e' x; x! \
  49.         int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)
    : Z3 K. y! T) q1 b. w

  50. . p$ t: P6 d* N9 P$ r  H9 ]. q
  51.         var msg = begin_cell()
    1 J& P8 Q4 e8 A, I6 S5 W
  52.             .store_uint(0x18, 6)& _8 |1 }$ _# h  f# I* W; k
  53.             .store_slice(sender_address)
      w* y1 `3 q: ?9 l1 S: z! Z
  54.             .store_coins(return_value)' Q% {$ H: @% e" A8 }
  55.             .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);4 I1 t; A3 @$ P4 u9 H% H( O
  56. ! y5 y0 A1 K/ Y! v  c
  57.         send_raw_message(msg.end_cell(), msg_mode);3 w) _" J' ?% Y+ K! U

  58. ! ^8 I5 r6 Y3 E
  59.         return();
    " h# v( l8 B! z( r4 L3 L+ j: H
  60.     }$ k8 @1 X4 O* S# }8 g0 y; l+ g4 e

  61. # T+ h7 L7 T- T' _; A
  62.   throw(777);
    $ ]2 ~5 R8 e/ h( |; |3 p
  63. }
    . H5 d% N2 H. W7 B' g
  64. 3 J' t: o% L! j& }* a
  65. (int, slice, slice) get_contract_storage_data() method_id {
    9 w5 q! Q" {2 s* `4 Q) Y" B6 ^
  66.   var (counter_value, recent_sender, owner_address) = load_data();
    % V  o2 N, K3 v; x) e2 ]
  67.   return (
    . F  J7 ]! J. d( ^
  68.     counter_value,
    2 m1 q, k, i- x
  69.     recent_sender,
    4 {1 u6 @& G8 }4 n
  70.     owner_address+ V4 p! X/ R% A7 C/ P
  71.   );1 D; h$ e2 O* }' [2 P6 \
  72. }
    8 X7 u! U2 f3 c6 ~2 a& ^
  73. ) H: X" u: l7 z& p; ^; l
  74. int balance() method_id {
    : C3 c1 W! ?. A9 y" I( R
  75.   var [balance, _] = get_balance();% n  [6 \  {! a; e. ?: ^
  76.   return balance;3 M% @9 W. S4 x/ v2 q) E& I2 T
  77. }
复制代码
我们检查一下我们的代码是否能用 yarn compile 然后 让我们开始为更新后的合约编写测试。
3 a& V0 G# O. Z
0 \( |$ G$ A4 O# m( P6 _) K, p( \- L; ~
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则