Try   HackMD

上篇文章我们介绍了 FunC 的一些基础内容,这篇文章来学习如何使用 FunC 来发送消息。这部分内容有许多细节需要注意,因此值得单独写篇文章记录一下。

消息体代码模板

TON 的文档为我们提供了一个发送消息的模板代码:

cell msg = begin_cell() .store_uint(0x18, 6) .store_slice(addr) .store_coins(amount) .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_slice(message_body) .end_cell();

与 FunC 的 recv_internal() 方法签名对照来看:

recv_internal(int my_ton_balance, int msg_value, cell in_msg_full, slice `in_msg_body`)

其中 msg 就是 in_msg_fullmessage_body 就是 in_msg_body

与 Tact 的消息发送方法对比:

send(SendParameters{
    to: self.owner,
    value: msg.forward_ton_amount,
    mode: SendPayGasSeparately,
    bounce: false,
    body: JettonTransferNotification {
        query_id: msg.query_id,
        amount: msg.amount,
        sender: msg.from,
        forward_payload: msg.forward_payload
    }.toCell()
});

其中 msgsend 方法包含的所有内容,message_bodybody 部分,这里即 JettonTransferNotification

上面这部分模板代码,我们目前阶段不需要深究它为什么这么写。当前的任务是学会灵活使用这段代码,例如 mode 怎么指定,bounce 怎么指定。

如果对于消息体的具体结构感兴趣,可以看这里,不过这部分内容比较深入,我们在本文不会过多提及,当大家有了一部分开发经验之后,再去学习,会有更深入的理解。

消息体字段

我们先来简单学习一个这几个字段的意义。

store_uint(0x18, 6) 是在消息体头部设置一些信息,常用的改写是和 bounce 相关的。如果希望设置 bounce = true,则使用:

store_uint(0x18, 6)

如果希望设置 bounce = false,则使用:

store_uint(0x10, 6)

store_slice(addr) 存放的是消息接收者的地址,要将该消息发送给谁,就填写谁的地址,相当于 Tact 消息体中的 to 字段。

store_coins(amount) 存放的是该条消息附带的 TON Gas 数量,相当于 Tact 消息体中的 value 字段。

store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 存放的是消息体的一些信息,例如消息时间等。这部分在编写时会根据场景的不同,值也不同。我们后面会详细介绍各个场景。

store_slice(message_body) 存放的是实际消息体,也就是 Tact 中的 body

消息体编写

这里我将根据几个具体的场景来讲解消息体该怎么编写,我们这里默认所有消息设置为 bounce = true,这样就直接使用 store_uint(0x18, 6)

空消息体

当发送空消息体时候,不需要 body 部分,写法如下:

cell msg = begin_cell()
    .store_uint(0x18, 6)
    .store_slice(addr)
    .store_coins(amount)
    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.end_cell();

没有 body,也就不需要最后的 store_slice(message_body),同时,最后一个 store_uint() 的第一个参数为 0,当发送空消息体的时候就使用 0。

我们编写一个发送空消息的合约:

#include "imports/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        return ();
    }

    slice to_address = in_msg_body~load_msg_addr();

    var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(to_address)
      .store_coins(0)
      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
    send_raw_message(msg.end_cell(), 64);
}

首先构建出消息体 msg,随后通过 send_raw_message 方法发送消息,该方法是 FunC 的底层方法,所有的消息都通过该方法发送。其中第一个参数是消息体 Cell,第二个参数是消息模式(Message mode),也就是 Tact 中的 SendRemainingValueSendRemainingBalance 等等那些常数。对消息模式不了解的可以看看我之前写的 Tact 文章,里面有相关介绍。

随后编写出接收合约:

#include "imports/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        ~dump(1);
        return ();
    }
}

运行单元测试发现空消息会在这部分直接返回,说明消息体确实为空。

非空消息体

发送非空消息体有两种方法,一种是在消息体最后将实际消息体引用进来,一种是直接在当前 Cell 将消息体的内容包含进来。

我们假设消息体中存放了两个整数,分别是 32 位的 0x12345678 和 64 位的 123。

方法一

将实际消息体引用进来,例如下面的写法:

#include "imports/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        return ();
    }

    slice to_address = in_msg_body~load_msg_addr();

    var msg_body = begin_cell()
        .store_uint(0x12345678, 32)
        .store_uint(123, 64)
        .end_cell();
        
    var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(to_address)
      .store_coins(0)
      .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
      .store_ref(msg_body);
      
    send_raw_message(msg.end_cell(), 64);
}

在构建出 msg_body 之后,将其作为引用放在了 msg 的最后部分。此时,最后一个 store_uint 的内容为:

store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)

方法二

直接在当前 Cell 将消息体的内容包含进来,例如下面的写法:

var msg_body = begin_cell()
    .store_uint(0x12345678, 32)
    .store_uint(123, 64)
    .end_cell().begin_parse();

var msg = begin_cell()
  .store_uint(0x18, 6)
  .store_slice(to_address)
  .store_coins(0)
  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
  .store_slice(msg_body);

send_raw_message(msg.end_cell(), 64);

此时 msg_body 不是引用的形式,而是跟在 msg 后面。还有一种更加简单粗暴的形式:

var msg = begin_cell()
  .store_uint(0x18, 6)
  .store_slice(to_address)
  .store_coins(0)
  .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
  .store_uint(0x12345678, 32)
  .store_uint(123, 64);
send_raw_message(msg.end_cell(), 64);

直接将 body 的内容在 msg 中原地解包,此时使用的模式是:

store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)

第一个参数为 0。

两者对比,我们可以得出结论,如果真实消息体是被单独引用进来,则使用 1,如果是直接在跟在 msg 后面,则使用 0

我们再来编写接收合约:

#include "imports/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        ~dump(1);
        return ();
    }
    int a = in_msg_body~load_uint(32);
    int b = in_msg_body~load_uint(64);
    ~dump(a);
    ~dump(b);
}

运行单元测试,两种模式都成功打印出 ab 的值,说明消息体已经成功发送并被接收合约解析。

带有初始化状态的消息

我们在之前学习 Jetton 时遇到一种消息,它会附带接收合约的初始化状态,以便在该合约未部署的情况下,部署该合约,例如:

send(SendParameters{
    to: receiver,
    value: 0,
    bounce: true,
    mode: SendRemainingValue,
    body: JettonInternalTransfer{
        query_id: msg.query_id,
        amount: msg.amount,
        response_address: msg.response_destination,
        from: self.owner,
        forward_ton_amount: msg.forward_ton_amount,
        forward_payload: msg.forward_payload
    }.toCell(),
    code: init.code,
    data: init.data
});

最后的 codedata 就是目标合约的代码和初始化数据。FunC 中在实现该逻辑时,是将初始化数据作为引用放在了消息体的最后,例如:

var msg = begin_cell()
    .store_uint(0x18, 6)
    .store_slice(to_wallet_address)
    .store_coins(0)
    .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
    .store_ref(state_init);

var msg_body = begin_cell()
    .store_uint(op::internal_transfer(), 32)
    .store_uint(query_id, 64)
    .store_coins(jetton_amount)
    .store_slice(owner_address)
    .store_slice(response_address)
    .store_coins(forward_ton_amount)
    .store_slice(either_forward_payload)
    .end_cell();

msg = msg.store_ref(msg_body);

send_raw_message(msg.end_cell(), 64);

这是官方 Jetton FunC 合约中的一段代码,可以看到消息体的最后一个 store_uint 的内容为:

store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)

与前面的写法对比:

store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)

可以看到,第一个参数为 4 + 2 + 1,第二个参数最后多了一个 + 1。我们目前阶段不需要知道它为什么这么写,只需记住即可。

最后将合约的初始化状态引用到了消息体中,这个 state_init 变量的构成我们会在后续的文章中介绍。

来编写一个带有初始化状态消息的合约:

#include "imports/stdlib.fc";

cell load_data() inline {
  slice ds = get_data().begin_parse();
  return (
      ds~load_ref()  ;; recv_code
  );
}

const WORKCHAIN = 0;

cell pack_recv_data(cell recv_code) inline {
  return begin_cell()
    .store_ref(recv_code)
  .end_cell();
}

cell calculate_recv_state_init(cell recv_code) inline {
  return begin_cell()
    .store_uint(0, 2)
    .store_dict(recv_code)
    .store_dict(pack_recv_data(recv_code))
    .store_uint(0, 1)
  .end_cell();
}

slice calculate_recv_address(cell state_init) inline {
  return begin_cell().store_uint(4, 3)
    .store_int(WORKCHAIN, 8)
    .store_uint(cell_hash(state_init), 256)
  .end_cell().begin_parse();
}

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        return ();
    }
    ~dump(1);
    var addr = in_msg_body~load_msg_addr();
    cell recv_code = load_data();
    cell state_init = calculate_recv_state_init(recv_code);
    slice to_address = calculate_recv_address(state_init);
    var msg_body = begin_cell()
        .store_uint(0x12345678, 32)
        .store_uint(123, 64)
        .end_cell();
    var msg = begin_cell()
      .store_uint(0x18, 6)
      .store_slice(to_address)
      .store_coins(0)
      .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
      .store_ref(state_init)
      .store_ref(msg_body);
    send_raw_message(msg.end_cell(), 64);
    ~dump(3);
}

我们分别将初始化状态 state_init 和 body 内容放在 msg 后面的引用中。

然后编写接收合约:

#include "imports/stdlib.fc";

() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
    if (in_msg_body.slice_empty?()) { ;; ignore all empty messages
        ~dump(5);
        return ();
    }

    slice cs = in_msg_full.begin_parse();
    int flags = cs~load_uint(4);
    if (flags & 1) {
        return ();
    }

    int op = in_msg_body~load_uint(32);
    int query_id = in_msg_body~load_uint(64);
    ~dump(op);
    ~dump(query_id);
}

注意这个例子中,由于我们需要通过合约发送消息来部署合约,因此在单元测试中,就无需部署接收合约,只部署发送合约即可。

运行单元测试后,也能正常打印出 opquery_id,说明通过消息部署合约成功。

还有一个场景是在发送带有初始化状态的消息中,不额外引用 body,那么此时的消息内容需要改为:

store_uint(4 + 2, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)

第一个参数变为 4 + 2,第二个参数不变。这时表示没有额外 body,即可以是空 body,也可以将 body 直接解包跟在当前消息体之后。这个场景大家可以自行编写代码验证。

目前 FunC 中常用的几个消息发送逻辑就是上面这些,掌握了这些内容,在编写和阅读 FunC 代码时,对于消息发送的逻辑,就能够得心应手了。

代码

相关代码在这里,大家可以参考

总结

本文总结了使用 FunC 发送消息的方法,场景不同,写法也略有不同。希望这篇文章能够帮助大家更快掌握这部分内容。

关于我

欢迎和我交流