TON:为合约增加存取功能
🥑

TON:为合约增加存取功能

我将在现有的合约基础上,实现更多不同的命令,创建函数,从合约内部发送消息。
分解一下:
  • 合约对command的要求更详细,会用到新的操作码向合约存入资金。
  • 另一个引入的操作码是资金的提取。在提取过程中,资金实际上是以消息的形式发送到地址,所以需要知道如何从合约内部发送消息。
  • 任何带有未知操作码的资金将返回给发送者。
  • 在存储中引入一个新值——合约的所有者。只有合约所有者能够提取资金。我还将把存储加载和写入逻辑分离到函数中,以保持主代码的整洁。
 

分离存储管理逻辑

首先处理存储的合约所有者数据(在初始化合约时必须将此地址放入存储中)。创建两个新函数——load_datasave_data
(int, slice, slice) load_data() inline { var ds = get_data().begin_parse(); return ( ds~load_uint(32), ;; counter_value ds~load_msg_addr(), ;; the most recent sender ds~load_msg_addr() ;; owner_address ); } () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline { set_data(begin_cell() .store_uint(counter_value, 32) ;; counter_value .store_slice(recent_sender) ;; the most recent sender .store_slice(owner_address) ;; owner_address .end_cell()); }
这里有几点需要注意:
  • inline 说明符。如果一个函数有inline说明符,其代码实际上会在每个调用该函数的地方替换。需要注意的是inline是禁止在内联函数中使用递归调用的。
  • load_data()没有impure函数说明符,是因为这个函数不会影响合约的状态。
 
var (counter_value, recent_sender, owner_address) = load_data(); save_data(counter_value, recent_sender, owner_address);
不必总是读取所有参数,但必须确保将它们全部写回,因为一旦写入数据,cell就会重置,没有写入的数据就会不见了。

新的操作码 OPcode

如果从消息体读取的操作码没有触发任何if语句,我们将抛出错误:
#include "imports/stdlib.fc"; (int, slice, slice) load_data() inline { var ds = get_data().begin_parse(); return ( ds~load_uint(32), ;; counter_value ds~load_msg_addr(), ;; the most recent sender ds~load_msg_addr() ;; owner_address ); } () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline { set_data(begin_cell() .store_uint(counter_value, 32) ;; counter_value .store_slice(recent_sender) ;; the most recent sender .store_slice(owner_address) ;; owner_address .end_cell()); } () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); int op = in_msg_body~load_uint(32); var (counter_value, recent_sender, owner_address) = load_data(); if (op == 1) { save_data(counter_value + 1, sender_address, owner_address); return(); } if (op == 2) { ;; deposit } if (op == 3) { ;; withdrawal } throw(777); } (int, slice, slice) get_contract_storage_data() method_id { var (counter_value, recent_sender, owner_address) = load_data(); return ( counter_value, recent_sender, owner_address ); }
需要注意的几点:
  • 还需更新get_the_latest_sender函数以返回所有者地址。
  • 如果从消息体读取的操作码没有触发任何if语句,将抛出错误代码777。数字可以自由选择,但确保不会与官方错误退出代码冲突。退出码高于1,被视为错误代码,因此使用此代码退出可能会导致事务回滚/反弹。
  • 使用函数return()退出合约执行。
 

存款和取款

对于存款(op == 2),只需成功完成执行资金就被接受了。否则,资金将退还给发送者。
if (op == 2) { return(); }
但是取款的逻辑更复杂一些,首先必须将发送者地址与智能合约所有者地址进行比较,实现这个逻辑需要先完成这些:
  • 比较所有者和发送者的地址,使用FunC standard lib中的函数equal_slice_bits()
  • 使用throw_unless()函数,如果比较结果为假,则抛出错误。还有另一种抛出错误的方法——throw_if(),如果传递给该函数的条件返回为真,则抛出错误。
  • 消息体中包含一个整数,表示请求提取的金额。将要求的金额与合约的实际余额进行比较(标准FunC函数get_balance())。
  • 合约需要始终保留一些资金,以支付租金rent和必要的费用,因此需要设置合约最低金额,如果提取金额后不满足条件,则抛出错误。
  • 最后,通过内部消息从合约中发送资金。
 
首先,设置存储所需的最低常数:
const const::min_tons_for_storage = 10000000; ;; 0.01 TON
然后,实现取款的逻辑:
if (op == 3) { throw_unless(103, equal_slice_bits(sender_address, owner_address)); int withdraw_amount = in_msg_body~load_coins(); var [balance, _] = get_balance(); throw_unless(104, balance >= withdraw_amount); int return_value = min(withdraw_amount, balance - const::min_tons_for_storage); ;; TODO: Sending internal message with funds return(); }
读取请求提取的金额(它存储在in_msg_body中,在操作码之后),检查余额是否大于或等于请求的提取金额。
 

发送内部消息

send_raw_message是一个标准函数,它需要接受一个包含了消息的Cell和一个包含模式和标志之和的整数
 
目前有3种modes模式和3个flags。你可以将一个模式与几个(可能没有)flags标志组合以获得所需的模式。这部分的详情参考这个链接
在这个例子中,我想发送一条普通消息并单独支付转账费用,因此选择模式0和标志+1以获得模式=1。
在传递给send_raw_message()之前,刚刚组合的消息单元格可能是到目前为止需要理解的最复杂的内容。
 
  • 首先——习惯于数据是存储在单元格中的。这叫序列化。需要熟悉并习惯这一点,因为你将经常序列化数据到一个单元格中,因为一切都存储在单元格中。
  • 其次——熟悉TON文档。这几乎是掌握所有这些序列化结构的唯一方法,因为记住它们是极其困难的。
旧的TON doc门户网站非常有用,它以非常基础和简洁的方式阐述了一些问题。
 
分解一下传递给send_raw_message的消息单元格的结构。现在我需要将多个bits放入单元格中,并且每个位都有其特定的逻辑。
消息单元格以1比特前缀0开始,然后是三个1比特标志,即是否禁用即时超立方体路由Instant Hypercube Routing (目前总是为真)、是否在处理过程中出现错误时应反弹消息消息本身是否为反弹消息。通常只有在处理过程中发生错误时,我们才会使用反弹消息。
 
如果消息是从智能合约发送的,那么这些字段中的一些fields将被重写为正确的值。特别是,验证器会重写bounced、src、ihr_fee、fwd_fee、created_lt和created_at这些字段。这意味着两件事:
第一,另一个智能合约在处理消息时可以信任这些字段(发送者不能伪造源地址、bounced标志等);
第二,在序列化时,我可以向这些字段放入任何有效值(这些值无论如何都会被重写)。
直接序列化消息的方法如下(取自文档):
var msg = begin_cell() .store_uint(0, 1) ;; tag .store_uint(1, 1) ;; ihr_disabled .store_uint(1, 1) ;; allow bounces .store_uint(0, 1) ;; not bounced itself .store_slice(source) ;; ex. addr_none .store_slice(destination) ;; serialize CurrencyCollection (see below) .store_coins(amount) .store_dict(extra_currencies) .store_coins(0) ;; ihr_fee .store_coins(fwd_value) ;; fwd_fee .store_uint(cur_lt(), 64) ;; lt of transaction .store_uint(now(), 32) ;; unixtime of transaction .store_uint(0, 1) ;; no init-field flag (Maybe) .store_uint(0, 1) ;; inplace message body flag (Either) .store_slice(msg_body) .end_cell();
这段代码中的 .store_uint(cur_lt(), 64) 是在将当前交易的生命周期标签(Logical Time,简称 LT)存储到一个单元(cell)中,存储的大小为 64 位。
  • cur_lt() 是一个函数,用来获取当前交易的生命周期标签(LT)。
  • 生命周期标签(LT)通常是一个单调递增的 64 位整数,用于标识和排序账户的交易。

.store_uint(now(), 32):

  • 存储内容: 当前交易的 Unix 时间戳。
  • 位数: 32 位。
  • 用途: Unix 时间戳是一个表示从1970年1月1日00:00:00 UTC(协调世界时)起经过的秒数的整数值。它通常用于标识交易的发生时间。
  • 使用场景: 记录交易的具体发生时间,可能用于验证交易是否在规定的时间窗口内发生,或用于时间敏感的逻辑,比如防止某些交易在特定时间之后执行。
 
然而,开发者通常会使用捷径而不是逐步序列化所有字段。因此通过一个示例来看看如何从智能合约发送消息:
var msg = begin_cell() .store_uint(0x18, 6) .store_slice(addr) .store_coins(grams) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_uint(op_code, 32) .store_uint(query_id, 64); send_raw_message(msg.end_cell(), mode);
让我们更仔细地看看这里发生了什么。
.store_uint(0x18, 6)
我们通过将0x18值放入6its来开始组合一个单元格Cell,即从十六进制转换,它是0b011000。
 
第一位是0——1bit前缀,表示这是int_msg_info(内部消息信息)。然后是3位1、1和0,意味着:
  • 禁用即时超立方体路由
  • 消息可以被反弹
  • 消息本身不是反弹的结果。
接下来应该是发送者地址,但由于它无论如何都会被重写(当验证器会将实际的发送者地址放在这里时,如上所述),任何有效的地址都可以存储在那里。最短的有效地址序列化是addr_none,它序列化为一个两位字符串00。因此,.store_uint(0x18, 6)是序列化标记和前4个字段的优化方式。
.store_slice(addr) - 这行序列化目标地址。
.store_coins(grams) - 这行只是序列化coins金额(grams只是一个包含硬币数量的变量)。
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) - 这一行从技术上讲,只是向单元格中写入大量的零(即零的数量等于1 + 4 + 4 + 64 + 32 + 1 + 1)。这些值的消费有明确的结构,这意味着我们放入的每个0都有某种原因。
这些零以两种方式工作——其中一些作为0,因为验证器无论如何都会重写值,其中一些作为0,因为这个功能尚不支持(例如额外的货币)。
为了确保我们理解为什么有这么多的零,让我们分解一下它的预期结构:
  • 第一位表示空的额外货币字典。
  • 然后有两个4位长的字段。由于ihr_fee和fwd_fee将被重写,也可以放置零。
  • 然后将零放入created_lt和created_at字段。这些字段也将被重写;然而,与费用不同,这些字段具有固定长度,因此编码为64位和32位长的字符串。
  • 接下来的零位表示没有init字段。
  • 最后一位零表示msg_body将被就地序列化。这基本上表明是否有带有自定义布局的msg_body。
.store_uint(op_code, 32).store_uint(query_id, 64); 这一部分最简单——我们传递一个带有自定义布局的消息体,这意味着可以放入任何数据,只要接收者知道如何处理它即可。
合并在一起实现资金提取代码的话:
if (op == 3) { throw_unless(103, equal_slice_bits(sender_address, owner_address)); int withdraw_amount = in_msg_body~load_coins(); var [balance, _] = get_balance(); throw_unless(104, balance >= withdraw_amount); int return_value = min(withdraw_amount, balance - const::min_tons_for_storage); int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value) var msg = begin_cell() .store_uint(0x18, 6) .store_slice(sender_address) .store_coins(return_value) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1); send_raw_message(msg.end_cell(), msg_mode); return(); }
最后,为合约代码添加一个新的getter方法,返回合约余额:
int balance() method_id { var [balance, _] = get_balance(); return balance; }
再看一遍最终代码:
#include "imports/stdlib.fc"; const const::min_tons_for_storage = 10000000; ;; 0.01 TON (int, slice, slice) load_data() inline { var ds = get_data().begin_parse(); return ( ds~load_uint(32), ;; counter_value ds~load_msg_addr(), ;; the most recent sender ds~load_msg_addr() ;; owner_address ); } () save_data(int counter_value, slice recent_sender, slice owner_address) impure inline { set_data(begin_cell() .store_uint(counter_value, 32) ;; counter_value .store_slice(recent_sender) ;; the most recent sender .store_slice(owner_address) ;; owner_address .end_cell()); } () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); slice sender_address = cs~load_msg_addr(); int op = in_msg_body~load_uint(32); var (counter_value, recent_sender, owner_address) = load_data(); if (op == 1) { save_data(counter_value + 1, sender_address, owner_address); return(); } if (op == 2) { return(); } if (op == 3) { throw_unless(103, equal_slice_bits(sender_address, owner_address)); int withdraw_amount = in_msg_body~load_coins(); var [balance, _] = get_balance(); throw_unless(104, balance >= withdraw_amount); int return_value = min(withdraw_amount, balance - const::min_tons_for_storage); int msg_mode = 1; ;; 0 (Ordinary message) + 1 (Pay transfer fees separately from the message value) var msg = begin_cell() .store_uint(0x18, 6) .store_slice(sender_address) .store_coins(return_value) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1); send_raw_message(msg.end_cell(), msg_mode); return(); } throw(777); } (int, slice, slice) get_contract_storage_data() method_id { var (counter_value, recent_sender, owner_address) = load_data(); return ( counter_value, recent_sender, owner_address ); } int balance() method_id { var [balance, _] = get_balance(); return balance; }
检查代码是否通过yarn compile编译,并继续为更新的合约编写测试。