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

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

回答

收藏

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

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

在这一课中,我们将练习执行更多不同的命令、创建函数,甚至从合约内部发送信息。我们将在现有合约的基础上完成所有这些工作。
我们的计划是什么?让我们来分析一下:
  • 我们的合约对指令的要求将变得更加严格,我们将引入一个新的操作码,用于向合约存入资金的逻辑。
  • 我们要引入的另一个操作代码是提取资金。在提取资金过程中,资金实际上是以消息的形式发送到地址的,因此我们将学习如何在合约中发送消息
  • 任何以未知操作代码到达的资金都将返回给发送方
  • 我们将在存储中引入一个新值--合约所有者。只有合约所有者才能提取资金。我们还将在函数中分离存储加载和写入逻辑,以保持主代码的整洁
    3 \; S" x6 {- {$ |! x
准备好了吗?就绪。开始!
分离存储管理逻辑
首先,让我们来处理合约所有者存储中的数据(我们需要记住,我们必须在启动合约时将该地址放入存储中)。我们还将创建两个新函数-load_data 和 save_data:
  1. (int, slice, slice) load_data() inline {7 Z# `' P* D4 s# C+ _1 |8 q2 Z, z
  2.   var ds = get_data().begin_parse();
    , i' p4 L& ^/ f& W: T- {4 {
  3.   return (
    , U' V! M& N, Z, ^9 J
  4.           ds~load_uint(32), ;; counter_value/ M! M# _4 K6 m5 ?: d9 [
  5.            ds~load_msg_addr(), ;; the most recent sender1 f/ `+ X! h. O  _8 K& B" K, X! M
  6.             ds~load_msg_addr() ;; owner_address
    0 V( Z" t6 H% b; J; x) v/ [
  7.   );* Z, @8 I/ Q8 \
  8. }3 w, {8 o, n8 o' g8 D* |0 b1 n7 n

  9. - x- ^+ @4 u7 z4 U4 s
  10. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {5 g# Y& U$ r9 |6 }7 X
  11.   set_data(begin_cell()8 i% N$ P* ~1 P$ Z/ `4 L( U1 B7 ?
  12.     .store_uint(counter_value, 32) ;; counter_value% Q/ V) b9 }' u9 s; Z3 w
  13.     .store_slice(recent_sender) ;; the most recent sender
      o8 O9 l& E" {) Q3 R
  14.     .store_slice(owner_address) ;; owner_address
    $ a- |3 t3 C' b' `3 y4 }, L5 J
  15.     .end_cell());& {( u0 }* L# A& n+ n0 t
  16. }
复制代码
这里有几点需要注意:
  • inline 指定符。你已经对指定符有所了解。如果一个函数有内联指定符,那么在调用该函数的每一个地方,它的代码实际上都会被替换。禁止在内联函数中使用递归调用。
  • 为什么 load_data() 没有 impure 函数指定符?我们在第 3 章中已经讨论过这个问题,现在让我们来复习一下。答案是,因为这个函数不会影响合约的状态。它是只读的。
    - H+ c7 h  |) Z  }/ x! M
我们将在代码中进一步使用这些函数:
  1. var (counter_value, recent_sender, owner_address) = load_data();0 q% {! l2 v, ^# |1 G* H, e- u3 B
  2. $ Y4 ]- r$ z% Y1 E
  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() 成功退出合约执行
    0 E; p8 V2 `: ]2 A! {, C. o
总的来说,代码看起来很简洁,不是吗?
存款和取款
对我们来说,存款 (op == 2) 非常简单,因为在这种情况下,我们只需成功完成执行,使得资金被接受。否则,资金将退回给发件人。
  1. if (op == 2) {
    8 W5 s; a. d2 I. ?5 g
  2.     return();
    7 ?/ V6 B  d% B
  3. }
复制代码
不过,取款时情况会变得复杂一些。我们必须将发件人地址与智能合约的所有者地址进行比较。让我们来看看,为了实现这个逻辑,我们需要知道哪些事情:
  • 要比较所有者地址和发送者地址,我们使用 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
我们看看这一切是如何实现的。
首先,我们设置最小所需存储空间的常数:
  1. const const::min_tons_for_storage = 10000000; ;; 0.01 TON" Z5 E8 W, C/ V. E9 V
复制代码
然后,我们实现提款的逻辑。它看起来是这样的:

9 `7 |) r% \( _9 [( c
  1. if (op == 3) {; U) T; Q) R, G% _9 d! X
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));
    7 V* S: w5 ]1 V0 q  {  C
  3. ' C$ _; B9 B# ^
  4.     int withdraw_amount = in_msg_body~load_coins();
    & F3 f& h1 f# P' d+ o) n
  5.     var [balance, _] = get_balance();9 U8 d' C- s, x0 E$ Z+ U0 x
  6.     throw_unless(104, balance >= withdraw_amount);2 H& m3 e7 G9 {( k6 \1 r

  7. 4 x' W% }5 \; E) c2 F* i
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);
    5 u5 ?( f- c6 M1 w  G! l- \

  9. ; Q$ Z8 g" \' u, q7 [, w0 q9 N  N
  10.     ; x5 s6 o7 m; c: o+ K
  11.     ;; TODO: Sending internal message with funds- p4 H8 j% S# v
  12.    
    , [- r! w. m, g
  13.     return();2 l$ Q4 a1 o  m  ?; S+ n
  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. 这几乎是处理所有这些序列化结构的唯一方法,因为要记住它们非常困难。
    : P: v0 m  z3 e
您可能会发现一个非常有用的东西 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()3 c8 d. g. @7 m
  2.     .store_uint(0, 1) ;; tag
    / k* ~! F6 J) d/ s# o
  3.     .store_uint(1, 1) ;; ihr_disabled7 Y6 t5 {3 ?5 n" M7 l3 g( D  _) i
  4.     .store_uint(1, 1) ;; allow bounces
    # d9 g# [8 F7 _8 u& ~; ?
  5.     .store_uint(0, 1) ;; not bounced itself: j' }" R/ h7 t3 R- w" {
  6.     .store_slice(source) ;; ex. addr_none
    / S9 X5 P  `8 w1 E' ^
  7.     .store_slice(destination)
    % X2 G9 P# }( x" Y1 \! c. i
  8.     ;; serialize CurrencyCollection (see below)- f* M0 N1 c  b( B0 G) h
  9.     .store_coins(amount)# {( Q/ B. Z6 j& y
  10.     .store_dict(extra_currencies)
    ( Y) R% h6 n7 P
  11.     .store_coins(0) ;; ihr_fee
    % }) N6 T" H- I' m- j0 g* G: k+ J
  12.     .store_coins(fwd_value) ;; fwd_fee 8 b/ s' n& T1 k
  13.     .store_uint(cur_lt(), 64) ;; lt of transaction
    & r7 Z, y! o  E* o4 H# X
  14.     .store_uint(now(), 32) ;; unixtime of transaction8 e4 u1 T) j% n. P( T+ g! m
  15.     .store_uint(0,  1) ;; no init-field flag (Maybe). Y' m& N# l& E6 d' u
  16.     .store_uint(0,  1) ;; inplace message body flag (Either)6 p- p0 w3 W5 y' F7 m. L
  17.     .store_slice(msg_body)
    $ @, S+ y* K2 q0 h9 C- t4 d
  18.   .end_cell();
复制代码
不过,开发人员通常会使用快捷方式,而不是逐步序列化所有字段。因此,让我们举例说明如何从智能合约中发送信息:
- Z( @7 l4 N1 V" B/ r, D! v1 C* Y
  1. var msg = begin_cell()
    , f. d0 Q  @2 w8 }  t4 v4 g3 {
  2.     .store_uint(0x18, 6)% b% g+ E% v2 d- K: }; }0 b
  3.     .store_slice(addr)
    4 b- n; m( P/ `# X; i
  4.     .store_coins(grams)) E$ z+ G/ m- n. J. P
  5.     .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)  Z+ h! E0 _% `
  6.     .store_uint(op_code, 32)
    " B+ v; C  b" V. D
  7.     .store_uint(query_id, 64);
    ' W! N# L0 l# i9 h4 g2 r6 M
  8.     2 S$ u* u" f- b+ L5 }3 \
  9. 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
    • 第一位是 0-1 位前缀,表示它是 int_msg_info (内部信息)。
    • 然后有 3 个位 1、1 和 0,即
      • 禁用即时超立方路由(我们将不详细介绍它是什么,因为它太基础,您在编写合约时暂时用不上)
      • 信息可能被退回
      • 信息本身不是退回的结果。

        ( m7 c3 p5 a- W4 f5 z: {
    • 然后应该有发件人地址,但由于它无论如何都会被重写(当验证器将发件人的实际地址放在这里时,就像我们上面讨论的那样),因此任何有效地址都可以存储在这里。最短的有效地址序列化是 addr_none,它序列化为一个两位字符串 00。
      # v$ ^5 [2 Z' j
  • 因此,.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
  1. if (op == 3) {  c5 \( r+ l2 f3 J7 f+ N
  2.     throw_unless(103, equal_slice_bits(sender_address, owner_address));
    ) h" D4 F0 d- D4 P' `1 J. J
  3. 0 i; e& |# M* z7 i# _, S+ e0 ?  t
  4.     int withdraw_amount = in_msg_body~load_coins();# [3 i: l' n* \4 ?. @/ z
  5.     var [balance, _] = get_balance();
    2 f  u3 h1 ^! ^0 g: k$ g! ~
  6.     throw_unless(104, balance >= withdraw_amount);
    * Q4 [* z; b( t1 J" x. s' f

  7.   _! U  ^, M' U6 N: s% m
  8.     int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);3 d- {" c( e. p7 ~0 _4 i; G
  9.    
    4 j4 t; \2 ]. v! n- u1 b( g
  10.     int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)
    0 @; b2 v) O. h: k' z
  11.    
    8 k8 @( C  G+ x7 @' S$ t. w
  12.     var msg = begin_cell()
    ) l9 K/ U$ P6 \
  13.         .store_uint(0x18, 6)
    7 D5 c& O: o6 N' p6 J2 v- l& _
  14.         .store_slice(sender_address)
    . [! _! T( t  q9 o$ @$ _
  15.         .store_coins(return_value)) f. h, R9 H4 B/ E! u2 I) _
  16.         .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
    : r, g% k6 E, k- B. y8 X) o
  17.    
    3 y) {4 p( _$ E3 U6 ?5 m2 N
  18.     send_raw_message(msg.end_cell(), msg_mode);
    * E8 T; d1 E. I0 q
  19.    
    ; ^: k- g$ Y. Y& e! y+ l. |9 y9 v
  20.     return();! V& ]% I6 \# T, f+ ?
  21. }
复制代码
敲定我们的合约代码
最后一件事是添加一个新的 getter 方法,用于返回合约余额:
  1. int balance() method_id {
    ! c. a( E' J" \, Q
  2.   var [balance, _] = get_balance();
    " y6 E5 O! Q$ F6 H* P. E
  3.   return balance;
    6 R# _: X2 I/ ]' `+ T( B9 \5 Q
  4. }
复制代码
我们再来看看最终代码的样子:9 u6 K- I/ ~5 p7 [
  1. #include "imports/stdlib.fc";
    " S' f. V4 X( Z7 R* E2 V

  2. / j7 ?9 T4 j; |
  3. const const::min_tons_for_storage = 10000000; ;; 0.01 TON1 ]7 P3 J3 @+ @' B2 A- R0 C3 H; |

  4. + `% g& z4 K3 n4 k4 u" I
  5. (int, slice, slice) load_data() inline {
    3 ~2 d0 Y$ e4 z% |1 K1 Z
  6.   var ds = get_data().begin_parse();
    & D* c/ k1 x. j1 m& ?
  7.   return (& d2 G# L' A4 B4 J
  8.     ds~load_uint(32), ;; counter_value
    2 i; j" ^8 X! g$ }
  9.     ds~load_msg_addr(), ;; the most recent sender  b% `6 y+ m- k- _! M5 ~( `9 k3 D
  10.     ds~load_msg_addr() ;; owner_address
    5 _) j) u) M/ i; N1 p
  11.   );
    ) S# S9 H- w" U/ B) O+ |
  12. }5 \/ D5 p( k$ h
  13. + s+ \. d% ^  C+ C( l6 w5 f
  14. () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline {1 @% x& j5 I" r
  15.   set_data(begin_cell()
    ' n2 j, F! C# }
  16.     .store_uint(counter_value, 32) ;; counter_value  a7 r4 C8 j, Q* ]0 C
  17.     .store_slice(recent_sender) ;; the most recent sender
    , g* J) v: o, s' d
  18.     .store_slice(owner_address) ;; owner_address
    , r( B5 K* j5 o! W( a+ X* ?4 R
  19.     .end_cell());
    " J. {7 @- f1 e. }
  20. }+ D- {- f: e* Q0 w
  21. 2 W( O5 {& g# i% I" `" {
  22. () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {& A* [: P6 @  J) K2 T$ O  ^$ _. |
  23.   slice cs = in_msg.begin_parse();8 q7 [/ B  G7 r) [/ \  `" \0 A
  24.   int flags = cs~load_uint(4);
    7 [" F) E  e# z, x. I+ l* G" R; A
  25.   slice sender_address = cs~load_msg_addr();3 N, F( o- `% U  c" `% v7 A1 i
  26. 8 D* ^" g9 z8 q' G
  27.   int op = in_msg_body~load_uint(32);) O: o  p6 D% O# W- _# O: h& \9 V

  28. 9 `+ A$ |- F0 Q& Q; m/ Z: a5 M. j* A
  29.   var (counter_value, recent_sender, owner_address) = load_data();$ F' A# P* b/ a
  30.        
    * `1 o: m  a& G- ^
  31.   if (op == 1) {
    3 B( t: H& q, S7 w" h1 l& |+ d
  32.     save_data(counter_value + 1, sender_address, owner_address);
    9 f$ S+ I/ I% z1 q, w, l9 G
  33.     return();7 J+ r7 D6 U7 L% u& X; m8 S. ^
  34.   }
    ' V# v. r$ T; {$ I" z6 D/ O* |+ G

  35. * |6 r! `2 D, D" u0 S
  36.   if (op == 2) {
    , O6 d. ]% {' o% _8 n8 t
  37.     return();
    & {) }  G7 m& z+ S' o& \
  38.   }, S+ R& Q2 s+ m4 w# l$ r8 ^
  39. & k# i+ m6 W9 t
  40.   if (op == 3) {
    2 {+ r5 V7 @3 d" ^
  41.         throw_unless(103, equal_slice_bits(sender_address, owner_address));+ a6 R$ `5 C) f
  42. + ~8 u4 k8 U+ S4 I  }8 y
  43.         int withdraw_amount = in_msg_body~load_coins();9 Y' k5 ~' |$ L/ ^  V  T) `$ ^
  44.         var [balance, _] = get_balance();' j6 f1 A; t2 [- ]0 V5 J/ Y& A4 k
  45.         throw_unless(104, balance >= withdraw_amount);" W$ U* }3 D( L' f, j6 }6 J8 \/ L

  46. , [7 Z+ W0 i  Q$ G
  47.         int return_value = min(withdraw_amount, balance - const::min_tons_for_storage);2 z. A* q, X) R  Q6 D6 u9 n

  48. / I0 @5 F3 m4 D/ ]- q7 |; P
  49.         int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value)
    8 n" M& [. N! q" i

  50. ' r- Z7 S) P* ?* F& D+ L
  51.         var msg = begin_cell()
    ) @. _5 t- G6 K3 j6 B7 U
  52.             .store_uint(0x18, 6)" w5 H$ T- X9 K# _7 H
  53.             .store_slice(sender_address)
    5 K- ?0 [; |# ]  v; a+ @$ d
  54.             .store_coins(return_value), |& ]5 W# ^' N/ k. z
  55.             .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);' r& y4 }6 s: ]+ `2 D5 P. f1 u

  56. ( M6 j: c# y8 W, `+ \) d/ [% V$ }
  57.         send_raw_message(msg.end_cell(), msg_mode);% z9 w$ N8 y  N4 ?

  58. ! G: D1 r; w1 t) |% y2 j1 B6 [
  59.         return();- f0 H9 E8 v( t2 O" G$ @. y7 O
  60.     }
    $ e9 ~5 o( g* A3 ~- _

  61. + `6 o* C% F: t+ I; |5 M" \3 A
  62.   throw(777);4 n' ~' o8 S) f# T! n) j/ C& p
  63. }
    ) V- {# Z8 G$ l/ H% ^  s" F

  64. : Z. B% i4 w! n; Z! z0 V6 J
  65. (int, slice, slice) get_contract_storage_data() method_id {" j' M) V$ G+ ^% |) Z
  66.   var (counter_value, recent_sender, owner_address) = load_data();& `2 d) d5 G5 ]5 g3 ]2 F$ Y
  67.   return (
    3 o/ Z6 C/ m+ N
  68.     counter_value,2 W! }! U+ A5 S4 e! I4 r* O
  69.     recent_sender,
    8 C3 C" w! g8 A2 p5 b% ]; t
  70.     owner_address: i# V1 L9 ?5 W: J! S
  71.   );
    7 W6 A' n' j( v3 u
  72. }; ?: M2 q2 o( s) C
  73. 6 S5 B9 |$ d" v1 D7 x4 w  v
  74. int balance() method_id {
    0 y3 G1 \8 U# V2 U( ~! K9 n# E' ^
  75.   var [balance, _] = get_balance();  v) J, T5 M& y
  76.   return balance;
    - @3 w9 e) e# L, ]  w( h/ p
  77. }
复制代码
我们检查一下我们的代码是否能用 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
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则