Try   HackMD

本文我们来学习如何使用 FunC 编写 TON 上的 Jetton 代币合约。在开始之前,希望读者已经阅读过之前的关于 Tact 编写 Jetton 的文章。其中介绍的一些 Jetton 特性,本文不再重复。

我们这里使用 TON 官方提供的 FunC 代码,来学习 Jetton 的编写。

代码分析

与 Tact 相同,FunC 的 Jetton 也遵守相同的架构,即每个 Jetton 都分为 Master 合约和用户独立的 Jetton 钱包合约。不过在 FunC 这边,Master 被称为 Minter 合约,这应该是历史命名习惯,大家了解即可。

官方代码中,主要包含下面这些文件:

  • jetton-minter.fc
  • jetton-wallet.fc
  • jetton-utils.fc
  • op-codes.fc
  • params.fc

另外还有一些相关拓展合约,不过本文我们主要来学习上面的内容。上述文件中,前两个分别是 Minter 和 Jetton wallet 合约,下面三个是一些辅助工具方法。

我们从简单的内容看起,先看这几个工具方法的内容。

op-codes.fc

int op::transfer() asm "0xf8a7ea5 PUSHINT";
int op::transfer_notification() asm "0x7362d09c PUSHINT";
int op::internal_transfer() asm "0x178d4519 PUSHINT";
int op::excesses() asm "0xd53276db PUSHINT";
int op::burn() asm "0x595f07bc PUSHINT";
int op::burn_notification() asm "0x7bdd97de PUSHINT";

;; Minter
int op::mint() asm "21 PUSHINT";

该文件中包含的是 Jetton 中需要用到的 opcode,对于每个动作,都定义了对应的 opcode。合约会根据 opcode 的不同来处理不同的消息。

asm 表示前面定义的方法使用后面的汇编操作码,PUSHINT 表示将整数压入栈中。在 FunC 中常在定义常量时会使用到该操作码。

params.fc

int workchain() asm "0 PUSHINT";

() force_chain(slice addr) impure {
  (int wc, _) = parse_std_addr(addr);
  throw_unless(333, wc == workchain());
}

TON 中存在主链和工作链,我们的合约一般都是部署在工作链上。force_chain 方法的作用是校验地址必须为 0 工作链的地址,该工作链的标识符为 0,主链的标识符为 -1。

jetton-utils.fc

该文件中包含四个方法:

  • pack_jetton_wallet_data
  • calculate_jetton_wallet_state_init
  • calculate_jetton_wallet_address
  • calculate_user_jetton_wallet_address

这几个方法目的主要是为了计算合约的初始化状态和地址。

合约中向其它合约发送消息时,如果目标合约有可能还没有被部署,需要在消息中附带合约的代码和初始化状态。在 Tact 中,该消息可以这样编写:

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 中,同样的场景也需要附带 codedata

pack_jetton_wallet_data

cell pack_jetton_wallet_data(
    int balance,
    slice owner_address,
    slice jetton_master_address,
    cell jetton_wallet_code
) inline {
   return begin_cell()
              .store_coins(balance)
              .store_slice(owner_address)
              .store_slice(jetton_master_address)
              .store_ref(jetton_wallet_code)
              .end_cell();
}

TEP-74 中规定,Jetton wallet 的数据中需要包含如下的这四个字段:

  • balance
  • owner_address
  • jetton_master_address
  • jetton_wallet_code

所以需要将其打包起来并放入 Cell 中,该方法就是用于打包初始化的数据。

在编写自己的代码时,初始化需要什么数据,就类比该方法将其打包进 Cell 即可。

calculate_jetton_wallet_state_init

cell calculate_jetton_wallet_state_init(
    slice owner_address,
    slice jetton_master_address,
    cell jetton_wallet_code
) inline {
  return begin_cell()
            .store_uint(0, 2)
            .store_dict(jetton_wallet_code)
            .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code))
            .store_uint(0, 1)
            .end_cell();
}

该方法接收的参数为初始化的数据,并调用 pack_jetton_wallet_data 将其打包,最后生成初始化合约的状态。类似于 Tact 中的:

fun calculate_jetton_wallet_init(owner_address: Address): StateInit {
    return initOf ExampleJettonWallet(
        owner_address,
        self.jetton_master
    );
}

pack_jetton_wallet_data 的第一个参数为 0,代表初始化的钱包余额为 0。

该方法的内容可能比较难懂,例如 store_uint(0, 2)store_uint(0, 1) 等,我们目前先不用纠结,只需知道这是固定形式即可。在编写自己的代码时,第一个 store_dict 放入目标合约的代码,第二个 store_dict 放入目标合约的初始化数据即可。

calculate_jetton_wallet_address

slice calculate_jetton_wallet_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();
}

该方法通过上一步生成的合约初始化状态,计算出其地址。同样,目前不需要理解细节,这种写法是固定形式。类似于 Tact 中的:

contractAddress(initCode)

calculate_user_jetton_wallet_address

slice calculate_user_jetton_wallet_address(
    slice owner_address,
    slice jetton_master_address,
    cell jetton_wallet_code
) inline {
    return calculate_jetton_wallet_address(
              calculate_jetton_wallet_state_init(
                  owner_address,
                  jetton_master_address,
                  jetton_wallet_code
              )
    );
}

这个就很简单了,接收初始化数据,并返回目标合约的地址。

看完了这几个辅助方法,我们接下来学习 Minter 和 Jetton wallet 合约。

jetton-minter.fc

load_data

(int, slice, cell, cell) load_data() inline { slice ds = get_data().begin_parse(); return ( ds~load_coins(), ;; total_supply ds~load_msg_addr(), ;; admin_address ds~load_ref(), ;; content ds~load_ref() ;; jetton_wallet_code ); }

读取合约 c4 寄存器中的数据,数据类型由 TEP-74 定义。

save_data

() save_data( int total_supply, slice admin_address, cell content, cell jetton_wallet_code ) impure inline { set_data(begin_cell() .store_coins(total_supply) .store_slice(admin_address) .store_ref(content) .store_ref(jetton_wallet_code) .end_cell() ); }

遵循 TEP-74 定义的规则,与 load_data 读取的数据需一致。

recv_internal

合约的入口方法,该方法前面的一些内容都比较常规,这里不再重复介绍,不理解的读者可以先学些这篇 FunC 的入门文章

其中 slice sender_address = cs~load_msg_addr(); 是从消息中获取 sender 地址。

随后通过 load_data 方法获取合约中存储的数据:

(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();

接下来的部分就是根据 opcode 的不同,处理不同的消息内容。总共包括下面四个消息:

  • op::mint()
  • op::burn_notification()
  • 3 -> 修改管理员
  • 4 -> 修改 Metadata

其中,34 也是 opcode,它俩处理的逻辑比较简单,都是从消息体中获取相关数据,并将其存储到 c4 寄存器中:

if (op == 3) { ;; change admin throw_unless(73, equal_slices(sender_address, admin_address)); slice new_admin_address = in_msg_body~load_msg_addr(); save_data(total_supply, new_admin_address, content, jetton_wallet_code); return (); } if (op == 4) { ;; change content, delete this for immutable tokens throw_unless(73, equal_slices(sender_address, admin_address)); save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code); return (); }

其中,equal_slices 用于判断两个 slice(这里是两个地址)是否相同,如果 sender 不是管理员地址,则抛出异常。

打开 wrappers/JettonMinter.ts 文件,找到它们的消息体构成:

static changeAdminMessage(newOwner: Address) { return beginCell().storeUint(Op.change_admin, 32).storeUint(0, 64) // op, queryId .storeAddress(newOwner) .endCell(); } static changeContentMessage(content: Cell) { return beginCell().storeUint(Op.change_content, 32).storeUint(0, 64) // op, queryId .storeRef(content) .endCell(); }

可以看到它们的消息体数据结构与合约中的解析部分相同。

op::burn_notification() 消息来自于 Jetton wallet 合约,这部分放到后面我们再做分析。

来看 op::mint() 部分,回忆一下 mint 的逻辑如下:

image

代码如下:

if (op == op::mint()) { throw_unless(73, equal_slices(sender_address, admin_address)); slice to_address = in_msg_body~load_msg_addr(); int amount = in_msg_body~load_coins(); cell master_msg = in_msg_body~load_ref(); slice master_msg_cs = master_msg.begin_parse(); master_msg_cs~skip_bits(32 + 64); ;; op + query_id int jetton_amount = master_msg_cs~load_coins(); mint_tokens(to_address, jetton_wallet_code, amount, master_msg); save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); return (); }

首先判断消息发送者是否是 admin,随后从 body 中解析数据,我们查看 wrappers/JettonMinter.ts 中的 mint 消息结构:

static mintMessage(from: Address, to: Address, jetton_amount: bigint, forward_ton_amount: bigint, total_ton_amount: bigint, query_id: number | bigint = 0) { const mintMsg = beginCell().storeUint(Op.internal_transfer, 32) .storeUint(0, 64) .storeCoins(jetton_amount) .storeAddress(null) .storeAddress(from) // Response addr .storeCoins(forward_ton_amount) .storeMaybeRef(null) .endCell(); return beginCell().storeUint(Op.mint, 32).storeUint(query_id, 64) // op, queryId .storeAddress(to) .storeCoins(total_ton_amount) .storeCoins(jetton_amount) .storeRef(mintMsg) .endCell(); }

首先看 return 的 Cell,包含的数据如下:

  • mint opcode
  • query_id
  • to
  • total_ton_amount
  • jetton_amount
  • mintMsg

对应于合约中,if 语句之前已经解析了 opcodequery_id,随后又解析出 to_addresstotal_ton_amountjetton_amountmintMsg

to_address 为 mint 接收者的地址,注意是接收者的 TON 钱包地址,而不是 Jetton 钱包地址。

total_ton_amount 为发送给 Jetton wallet 消息附带的 TON 数量。

jetton_amount 为 mint 的 Jetton 数量。

mintMsg 为发送给 Jetton wallet 的实际消息体,包含的数据如下:

  • internal transfer opcode
  • query_id
  • jetton_amount
  • internal transfer 消息来源地址(这里由 minter 合约发送,可以为 null)
  • response address,即剩余 Gas 返还地址
  • forward ton amount
  • forward payload

在上述代码部分,mintMsg 被解析为 master_msg,随后跳过前面两个数据,即 internal transfer opcodequery_id,读取第三个数据 jetton_amount

随后,调用 mint_tokens 发送消息,最后使用 save_datatotal_supply 数据更新。

mint_tokens

() mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); slice to_wallet_address = calculate_jetton_wallet_address(state_init); var msg = begin_cell() .store_uint(0x18, 6) .store_slice(to_wallet_address) .store_coins(amount) .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) .store_ref(state_init) .store_ref(master_msg); send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors }

前面两行通过调用之前介绍的辅助方法,获取目标合约的初始状态和地址,随后构造消息体内容,在消息体最后附上初始化状态和实际 body 消息体。对于这一堆参数不理解的读者可以阅读这篇文章

最后使用 send_raw_message 方法发送消息,这里使用的 mode 是 1,也就是 pay transfer fees separately,即发送消息的 Gas 需要额外支付。

消息被 Jetton wallet 合约接收,接收部分我们放在后面介绍。

Getter

还剩余两个 Getter 方法:

(int, int, slice, cell, cell) get_jetton_data() method_id {
    (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
    return (total_supply, -1, admin_address, content, jetton_wallet_code);
}

slice get_wallet_address(slice owner_address) method_id {
    (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
    return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code);
}

它们被链下调用,第一个方法用于获取合约中存储的数据,第二个方法用于获取用户的 Jetton wallet 地址。

jetton-wallet.fc

接着来看 Jetton wallet 合约,用户转账以及销毁代币的入口都在该合约中。

首先定义了两个常量:

int min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON

int gas_consumption() asm "15000000 PUSHINT"; ;; 0.015 TON

由于 TON 的合约需要向系统缴纳租金,min_tons_for_storage() 就是合约中必须维持的最低 TON 数量。gas_consumption() 是转账中的 Gas 消耗数量。这两个值都是经过测试之后的一个比较均衡的数量,你也可以将其变更,不过如果减少了,可能会导致一些问题。

load_data 与 save_data

(int, slice, slice, cell) load_data() inline {
  slice ds = get_data().begin_parse();
  ;; balance, owner address, jetton master address, jetton wallet code
  return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
}

() save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline {
  set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code));
}

分别用于加载与存储合约中的数据,他们的数据类型也是 TEP-74 中定义的,注意它们也与前面 mint 时构造的目标合约初始化数据一一对应:

pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)

recv_internal

先来看消息入口方法,这里与 Minter 合约的入口相比,有一些不同:

if (flags & 1) {
    on_bounce(in_msg_body);
    return ();
}

对于 bounced 消息,这里使用了 on_bounce 方法进行处理,该方法的内容我们后面再看。

slice sender_address = cs~load_msg_addr();
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs

接着从 in_msg_full 中解析出 sender 地址,并跳过一系列数据,获取该消息体的 fwd_fee,这部分需要参考消息的具体构成

一个消息的实际构成如下所示:

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)
    .store_slice(destination)
    .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();

其中前 4 个 store_uint 的内容就是最开始的 flags,随后跳过 destinationamountextra_currenciesihr_fee。最后通过 cs~load_coins() 解析出 fwd_value,并通过:

int fwd_fee = muldiv(cs~load_coins(), 3, 2);

计算出 fwd_fee,可以将其简单理解成每次发送消息时所需的费用,即邮费,而不包含附带的 TON 数量。这部分的计算逻辑目前官方文档也没有给出一个特别清晰的说明,应该是和验证节点有关,大家目前暂时记住即可。

随后从 in_msg_body 中解析出 op,并根据 op 的不同处理不同逻辑:

int op = in_msg_body~load_uint(32);

if (op == op::transfer()) { ;; outgoing transfer
    send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
    return ();
}

if (op == op::internal_transfer()) { ;; incoming transfer
    receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
    return ();
}

if (op == op::burn()) { ;; burn
    burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
    return ();
}

throw(0xffff);

send_tokens

代码比较多,我们就挑一些重要的点来说。一些简单的数据解析等,就不再重复。

force_chain(to_owner_address);

校验接收者的地址必须是 0 workchain 链上的地址。

cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
slice to_wallet_address = calculate_jetton_wallet_address(state_init);

转账这里与 mint 类似,由于接收者可能并不拥有 Jetton wallet,因此需要构造出目标合约的状态与地址,并将其附带在消息的后面。

slice response_address = in_msg_body~load_msg_addr();
cell custom_payload = in_msg_body~load_dict();
int forward_ton_amount = in_msg_body~load_coins();
throw_unless(708, slice_bits(in_msg_body) >= 1);
slice either_forward_payload = in_msg_body;

重点来看看这几行代码,首先是从 in_msg_body 中解析出了 response_addresscustom_payloadforward_ton_amount。根据 TEP-74 的规范,此时后面应该还跟有 forward_payload,也就是说 in_msg_body 的解析指针目前是在forward_payload 的起始位置。

我们查看 TEP-74 的数据类型规定

forward_payload:(Either Cell ^Cell)

它的意思是 forward_payload 的内容要么直接在当前 Cell 中原地解包,要么存储在另一个 Cell 中,并被引用进来。

对于前者,接下来的一个 bit 需要存储 0,后面再跟上 forward_payload 的实际内容。如果 forward_payload 为空,该 bit 也需要存储 0。对于后者,接下来的一个 bit 需要存储 1,后面跟上 forward_payload 的引用。也就是说,不论 forward_payload 是以何种形式存储,接下来的一个 bit 都是有数据的,0 或 1(参考)。

我们可以查看文档中给出的 Jetton Transfer 的消息构成的例子进行验证:

const forwardPayload = beginCell()
    .storeUint(0, 32) // 0 opcode means we have a comment
    .storeStringTail('Hello, TON!')
    .endCell();

const body = beginCell()
    .storeUint(0xf8a7ea5, 32) // opcode for jetton transfer
    .storeUint(0, 64) // query id
    .storeCoins(toNano("5")) // Jetton amount for transfer (decimals = 6 - USDT, 9 - default). Function toNano use decimals = 9 (remember it)
    .storeAddress(destinationAddress) // TON wallet destination address
    .storeAddress(destinationAddress) // response excess destination
    .storeBit(0) // no custom payload
    .storeCoins(toNano("0.02")) // forward amount (if >0, will send notification message)
    .storeBit(1) // we store forwardPayload as a reference
    .storeRef(forwardPayload)
    .endCell();

forwardPayload 作为引用放在最后,在它之前,存储的是 1。

data = {
    'address': jetton_wallet_address,
    'amount': str(transfer_fee),
    'payload': urlsafe_b64encode(
        begin_cell()
        .store_uint(0xf8a7ea5, 32)  # op code for jetton transfer message
        .store_uint(0, 64)  # query_id
        .store_coins(jettons_amount) # Jetton amount for transfer (decimals = 6 - USDT, 9 - default). Exapmple: 1 USDT = 1 * 10**6 and 1 TON = 1 * 10**9
        .store_address(recipient_address)  # destination address
        .store_address(response_address or recipient_address)  # address send excess to
        .store_uint(0, 1)  # custom payload
        .store_coins(1)  # forward amount
        .store_uint(0, 1)  # forward payload
        .end_cell()  # end cell
        .to_boc()  # convert it to boc
    )
    .decode()  # encode it to urlsafe base64
}

forward payload 实际为空,但最后仍需要存储一位 0。

我们再来查看 wrappers/JettonWallet.ts 中 Jetton 转账消息的构成:

return beginCell().storeUint(0xf8a7ea5, 32).storeUint(0, 64) // op, queryId
          .storeCoins(jetton_amount)
          .storeAddress(to)
          .storeAddress(responseAddress)
          .storeMaybeRef(customPayload)
          .storeCoins(forward_ton_amount)
          .storeMaybeRef(forwardPayload)
       .endCell();

对于 forwardPayload 的存储,使用了 storeMaybeRef。查看一个 golang 的实现

func (b *Builder) StoreMaybeRef(ref *Cell) error {
    if ref == nil {
        return b.StoreUInt(0, 1)
    }

    // we need early checks to do 2 stores atomically
    if len(b.refs) >= 4 {
        return ErrTooMuchRefs
    }
    if b.bitsSz+1 >= 1024 {
        return ErrNotFit1023
    }

    b.MustStoreUInt(1, 1).MustStoreRef(ref)
    return nil
}

如果 ref 为空,则需要存储一位 0,否则存储一位 1,并将 ref 作为引用存储。

看完了这些代码,我们再回头看 Jetton wallet 的代码:

int forward_ton_amount = in_msg_body~load_coins();
throw_unless(708, slice_bits(in_msg_body) >= 1);
slice either_forward_payload = in_msg_body;

forward_ton_amount 接下来的一位必然是有数据的,要么是 0,要么是 1。不管是什么,都是有数据的,因此能够通过 708 的校验,如果没有数据,则说明发送的消息体不符合 Jetton 的规范。

随后构造消息体,并将目标合约的 state_init 附带在消息体中。

throw_unless(709, msg_value >
                 forward_ton_amount +
                 ;; 3 messages: wal1->wal2,  wal2->owner, wal2->response
                 ;; but last one is optional (it is ok if it fails)
                 fwd_count * fwd_fee +
                 (2 * gas_consumption() + min_tons_for_storage()));

这里是要求用户传入的 Gas 足够处理完整个流程。这部分的计算在之前的 Tact 文章中讲过,这里不再重复。

最后,发送消息,并将余额更新到合约数据中。

receive_tokens

该方法的整体逻辑都比较简单,参考之前 Tact 的文章很容易理解。不过其中有两部分代码需要关注:

var msg = begin_cell()
    .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
    .store_slice(owner_address)
    .store_coins(forward_ton_amount)
    .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
    .store_ref(msg_body);

这里使用了 0x10,即 non-bounceable 消息。这条消息是给接收者的地址发送 transfer_notification 消息,但是接收者的钱包可能还没有被部署。这种场景是可能存在的,例如 A 给 B 转 100 个 USDT,但是 B 刚刚生成了主网地址,还没有部署它自己的合约钱包。这条消息我们并不在乎它是否成功,因为主逻辑已经完成。此时如果使用了 bounceable 消息,会使整个逻辑变得更复杂,也可能会出错。

TON 中规定,如果向一个可能未被部署的合约发送消息,要么需附带 state init,要么需是 non-bounceable 消息。

if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
    var msg = begin_cell()
      .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
      .store_slice(response_address)
      .store_coins(msg_value)
      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
      .store_uint(op::excesses(), 32)
      .store_uint(query_id, 64);
    send_raw_message(msg.end_cell(), 2);
}

preload_uint 用于预加载整数,也就是指针不会移动。在 Cell 中,空地址使用 2 位 0 表示,如果这里预加载的 2 位数据不是 0,则说明
response_address 不为空。如果该地址不为空,并且剩余的 Gas 大于 0,则将剩余的 Gas 返还给 response_address

这条消息同样为 non-bounceable 消息,因为返还 Gas 的地址可能未被部署。

burn_tokens

burn 的逻辑更加简单,最后会向 Minter 合约发送消息。Minter 中同样使用了 preload_uint 来预加载地址,如果非空,则将剩余的 Gas 返还给 response_address

on_bounce

最后来看看这个比较新鲜的方法,它用于处理接收到的 bounced 回弹消息:

() on_bounce (slice in_msg_body) impure {
    in_msg_body~skip_bits(32); ;; 0xFFFFFFFF
    (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
    int op = in_msg_body~load_uint(32);
    throw_unless(709, (op == op::internal_transfer()) | (op == op::burn_notification()));
    int query_id = in_msg_body~load_uint(64);
    int jetton_amount = in_msg_body~load_coins();
    balance += jetton_amount;
    save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
}

它的参数为 in_msg_body,但是这个 body 并不是我们最初发送的 body。当初始 bounceable 消息变成 bounced 消息时,bounced 的消息体 body 将以 0xFFFFFFFF 开头(参考)。随后跟随的是原始消息体 body 的前 256 位数据,多余的数据将被截掉。

on_bounce 方法中,对于 in_msg_body,先跳过前面的 0xFFFFFFFF,随后从消息体中解析出 opquery_idjetton_amount,它们分别占据 32 位、64 位、120 位的数据,加起来小于 256 位。方法中限制,返回的 op 必须是 op::internal_transfer()op::burn_notification()。由于这两条消息的前三个字段,都是 opquery_idjetton_amount,因此可以放在一起处理。

这两条消息之前均是减少余额的行为,如果这两条消息被回弹,则将消息中的 jetton_amount 加回来。

这里的 bounced 消息,由于它的结构比较简单,最重要的字段就是 jetton_amount,也恰好可以放在 bounced 消息体中,因此使用 bounced 消息来处理很合适。但是在实际业务开发中,并不推荐使用 bounced 消息,因为它的信息内容有限。如果需要有类似的错误处理,更推荐在接收合约中直接编写错误处理逻辑并回发消息。

get_wallet_data

最后还剩余一个 Get 方法,用于返回该钱包合约存储的数据,即用户的 Jetton 余额,用户的地址,Jetton minter 的合约地址,Jetton wallet 的合约代码。

至此,我们就完成了 FunC Jetton 合约的编写。

总结

本文详细介绍了 FunC Jetton 合约的编写,并对一些代码细节进行了详细的讲解,如果对其都能够掌握,那么对于 FunC 的编写,将会更加得心应手。在学习完 FunC 版本的 Jetton 代码之后,其实会深切体会到 FunC 对于 Cell 数据的处理更加精确,因此如果想要在 TON 生态深入,FunC 是必须要掌握的。

关于我

欢迎和我交流