作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。
掘金:https://juejin.im/user/1785262612681997
GitHub : https://github.com/af913337456/
出版的书籍:
Ton 区块链的官方 类ERC20-Token 智能合约代码-Transfer部分解析
最近在学习 Ton 链的智能合约,由于我之前的经验思维主要是集中在以太坊这条链的,即Solidity那套,所以带着长久偏向的思维去阅读 Ton 的合约时发现格格不入,Ton 的合约设计与EVM体系
的属于天壤之别。
首先 Ton 的合约是分片的,遵循 Parent-Child 的规则,这里详细了解见:
其次是合约开发的语言,Ton 有三种,用得最多的是 FunC
,这是一种完全的非主流语言,在 GitHub 上都没有特点标识的那种。
按照最快了解 Token 智能合约的方式,寻找到官方的合约代码项目。由于Ton 的经济 Token 代码目前还没有类似以太坊的各种模型协议,只能把对应以太坊ERC-20的那部分取下来进行阅读。
下面我将结合Token的转账核心操作
的源码来对其整个调用链路
进行细致的分析讲解,所选代码片段也有注释。
先了解合约模式
- Ton 的合约是分片的,拿 Token 类型的合约做例子,其做法是将一份主合约,被称为 Master 或 Minter 的合约独一份进行部署,再将和 User 的子合约在转账进行时进行新建形式的一一对应部署。
- 比如说,发布一份名叫
NOT
的 Token 合约,它的 Master 合约将被部署在链上,然后对于后续每一位收到 NOT token 的用户地址,若不存在就都会创建一份与该地址对应的子合约,称为 Wallet 合约。 - 在 Token 类型的合约中,Master 合约中存储了 Token 的公共信息,比如 Name,Metaurl,Supply 等,而Transfer 转账行为却都发生在各自的 Wallet 合约里面。
- 为 User 创建 Wallet 合约都要经过 Master 进行。
- 合约允许各自内部调用,A 合约调用 B 合约的函数。
客户端-发起转账 Token 的流程
例子取于 Golang 客户端项目代码。
func main() {
...
// 初始化自己的钱包
w, _ := wallet.FromSeed(api, words, wallet.V3R2)
// 根据该 Token 的 Master 合约地址初始化 Token
token := jetton.NewJettonMasterClient(api, address.MustParseAddr("EQD0vdS......"))
// 调用 Master 的合约函数获取转账者的 Wallet 合约
tokenWallet, _ := token.GetJettonWallet(ctx, w.WalletAddress())
tokenBalance, _ := tokenWallet.GetBalance(ctx)
amountTokens := tlb.MustFromDecimal("0.1", 9)
// 转账附带的信息
comment, _ := wallet.CreateCommentCell("Hello from tonutils-go!")
// 初始化收款者的地址,这不是 Wallet 地址
to := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")
// 在 BuildTransferPayloadV2 里指定了 OP = Transfer
transferPayload, _ := tokenWallet.BuildTransferPayloadV2(to, to, amountTokens, tlb.ZeroCoins, comment, nil)
// 构造链上请求的消息结构
msg := wallet.SimpleMessage(tokenWallet.Address(), tlb.MustFromTON("0.05"), transferPayload)
// 发送转账交易,然后结束
tx, _, _ := w.SendWaitTransaction(ctx, msg)
log.Println("transaction confirmed, hash:", base64.StdEncoding.EncodeToString(tx.Hash))
}
上述代码可以看到在发起转账的时候,收款地址并不是 User 的钱包地址,而是其对应的 Wallet 合约地址。这一点就和包括以太坊在内的绝大部分公链都不一样。
合约端对应的转账入口代码解析
内部消息的入口函数,根据 op 参数指定调用入口。
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; ignore empty messages
return ();
}
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) {
on_bounce(in_msg_body);
return ();
}
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
int op = in_msg_body~load_uint(32);
if (op == op::transfer()) { ;; outgoing transfer
;; sender_address 是一开始的转账者
;; msg_value 是改次转账中的 Ton 数额
send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
return ();
}
if (op == op::internal_transfer()) { ;; incoming transfer
;; my_balance 是当前所执行的合约所有者的 Ton 余额
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);
}
recv_internal
是系统内置的函数入口,相当于 main;- 系统函数还有:
load_data
与save_data
,加载的是当前合约的数据,存储也是存储到当前合约。代码中的变量jetton_master_address
地址永远是父合约地址
。 - 转账发起时,指定 op 是 transfer,走到代码处理点
op == op::transfer
,进入到send_tokens
; send_tokens
函数源码及其解析注释内容见下 标签:body,slice,Transfer,jetton,Token,Ton,address,msg,store From: https://www.cnblogs.com/linguanh/p/18243703