这是TON Panda 上第一篇面向开发者的入门教学,会用我们自己的实验专案Trump Vs Harris 为例子和大家一起讨论TON 智能合约的开发体验和心得,设计一款带有特殊功能的Jetton合约。这个合约也会部署在TON主网上,完成后大家可以一起体验一下。
快速检索
以下是这合约的一些功能:
- Jetton会通过投票来MINT
- 投票需要支付TON
- 每一票要支付的TON会愈来愈多
- 当大选结果公布前的1小时前投票会被禁止
- 当美国大选公布后,输的一方的票会全部作废,投入的TON为当成池子加入胜出的一方
在开始之前,我们要先了解Jetton和ERC20的智能合约的区别。和以太坊不同,TON的代币标准名为Jetton,并且在智能合约上有着一币一地址的特性,这是由于TON的合约有储存上限,不可以像以太坊一样由一个合约控制所有持有人的余额,Jetton 主智能合约上只存储了有关代币的常见信息(包括总供应量、元数据链接或元数据本身)。所以在智能合约的写法上,Jetton和ERC20 有着显著的差异。
开发流程
參考TON官方的 TON 文档:https://docs.ton.org/mandarin/develop/overview 们会使用Blueprint SDK 和 Tact 语言来进行开发。TON的智能合约一般都用FunC或Tact来开发,FunC 是一种针对那些想深入了解 TON 架构的开发者的底层语言,由于 FunC 是较底层的语言,使用上有一定难度,也不容易上手。而 Tact 的设计类似于流行的编程语言,如 JavaScript、Python 和 Solidity。这显著简化了对语言的学习和理解,用 Tact 来开发,你可以更专注于写代码,而不必太担心区块链的底层细节。
使用Blueprint SDK 快速开始
npm create ton@latest
这样便可以生成一个完整的智能合约开发环境了,内容包含了开发,测试及部署。
Jetton主智能合约
基本的Jetton可参考 Howard Pen 的例子(https://github.com/howardpen9/jetton-implementation-in-tact/tree/main),𥚃面的代码包括了:
- Jetton 主智能合约 (jetton.tact)
- Jetton 钱包智能合约(jetton.tact)
- Jetton 讯息 (messages.tact)
中文区其中一位技术大佬LaDoger Bark,曾指出TON 其实是一个通讯工具,TON其实是Telegram的去中心化版本。TON上面的所有”交易”其实本质上都是智能合约彼此之间”传讯”而已。别忘了TON的”钱包”本身也是一个智能合约。 - 我们的代币合约 (contract.tact)
jetton.tact (1)
import "@stdlib/ownable";
import "./messages";
// ============================================================================================================ //
@interface("org.ton.jetton.master")
trait Jetton with Ownable {
total_supply: Int;
mintable: Bool;
owner: Address;
content: Cell;
receive(msg: TokenUpdateContent){
self.requireOwner(); // Allow changing content only by owner
self.content = msg.content; // Update content
}
receive(msg: TokenBurnNotification){
self.requireSenderAsWalletOwner(msg.sender); // Check wallet
self.total_supply = (self.total_supply - msg.amount); // Update supply
if (msg.response_destination != null) {
// Cashback
send(SendParameters{
to: msg.response_destination!!,
value: 0,
bounce: false,
mode: SendRemainingValue,
body: TokenExcesses{query_id: msg.query_id}.toCell()
}
);
}
}
// https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md
receive(msg: ProvideWalletAddress){
// 0x2c76b973
require(context().value >= ton("0.0061"), "Insufficient gas");
let init: StateInit = initOf JettonDefaultWallet(msg.owner_address, myAddress());
if (msg.include_address) {
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingValue,
body: TakeWalletAddress{
query_id: msg.query_id,
wallet_address: contractAddress(init),
owner_address: beginCell().storeBool(true).storeAddress(msg.owner_address).endCell().asSlice()
}.toCell()
}
);
} else {
send(SendParameters{
to: sender(),
value: 0,
mode: SendRemainingValue,
body: TakeWalletAddress{ // 0xd1735400
query_id: msg.query_id,
wallet_address: contractAddress(init),
owner_address: beginCell().storeBool(false).endCell().asSlice()
}.toCell()
}
);
}
}
// Private Methods //
// @to The Address receive the Jetton token after minting
// @amount The amount of Jetton token being minted
// @response_destination The previous owner address
fun mint(to: Address, amount: Int, response_destination: Address) {
require(self.mintable, "Can't Mint Anymore");
self.total_supply = (self.total_supply + amount); // Update total supply
let winit: StateInit = self.getJettonWalletInit(to); // Create message
send(SendParameters{
to: contractAddress(winit),
value: 0,
bounce: true,
mode: SendRemainingValue,
body: TokenTransferInternal{
query_id: 0,
amount: amount,
from: myAddress(),
response_destination: response_destination,
forward_ton_amount: 0,
forward_payload: beginCell().endCell().asSlice()
}.toCell(),
code: winit.code,
data: winit.data
}
);
}
fun requireSenderAsWalletOwner(owner: Address) {
let ctx: Context = context();
let winit: StateInit = self.getJettonWalletInit(owner);
require(contractAddress(winit) == ctx.sender, "Invalid sender");
}
virtual fun getJettonWalletInit(address: Address): StateInit {
return initOf JettonDefaultWallet(address, myAddress());
}
// ====== Get Methods ====== //
get fun get_jetton_data(): JettonData {
return
JettonData{
total_supply: self.total_supply,
mintable: self.mintable,
owner: self.owner,
content: self.content,
wallet_code: initOf JettonDefaultWallet(self.owner, myAddress()).code
};
}
get fun get_wallet_address(owner: Address): Address {
return contractAddress(initOf JettonDefaultWallet(owner, myAddress()));
}
}
导入ownable 和 messages,ownable属于标准的trait,可以使合约可以设置拥有者角色,拥有者拥有比其他用户更高的权限。例如,如果你的合约支持升级,那么将升级权限限制为只有拥有者可以执行就非常合理,否则任何人都有可能破坏合约。而messages 则是我们自定义的讯息标准。上面的代码我们可以直接引用,也可以重写,但要留意必须要跟从 TEP74 的标准,不然的话其他钱包/交易所/区块链浏览器就有可能不支援你的代币了。上面的代碼中,total_supply是Howard 自定义的,本身不在TEP74的标准中。
Jetton 钱包智能合约
jetton.tact (2)
// ============================================================ //
@interface("org.ton.jetton.wallet")
contract JettonDefaultWallet
{
const minTonsForStorage: Int = ton("0.019");
const gasConsumption: Int = ton("0.013");
balance: Int as coins = 0;
owner: Address;
master: Address;
init(owner: Address, master: Address){
self.balance = 0;
self.owner = owner;
self.master = master;
}
receive(msg: TokenTransfer){
// 0xf8a7ea5
let ctx: Context = context(); // Check sender
require(ctx.sender == self.owner, "Invalid sender");
let final: Int =
(((ctx.readForwardFee() * 2 + 2 * self.gasConsumption) + self.minTonsForStorage) + msg.forward_ton_amount); // Gas checks, forward_ton = 0.152
require(ctx.value > final, "Invalid value");
// Update balance
self.balance = (self.balance - msg.amount);
require(self.balance >= 0, "Invalid balance");
let init: StateInit = initOf JettonDefaultWallet(msg.sender, self.master);
let wallet_address: Address = contractAddress(init);
send(SendParameters{
to: wallet_address,
value: 0,
mode: SendRemainingValue,
bounce: true,
body: TokenTransferInternal{ // 0x178d4519
query_id: msg.query_id,
amount: msg.amount,
from: self.owner,
response_destination: msg.response_destination,
forward_ton_amount: msg.forward_ton_amount,
forward_payload: msg.forward_payload
}.toCell(),
code: init.code,
data: init.data
}
);
}
receive(msg: TokenTransferInternal){
// 0x178d4519
let ctx: Context = context();
if (ctx.sender != self.master) {
let sinit: StateInit = initOf JettonDefaultWallet(msg.from, self.master);
require(contractAddress(sinit) == ctx.sender, "Invalid sender!");
}
// Update balance
self.balance = (self.balance + msg.amount);
require(self.balance >= 0, "Invalid balance");
// Get value for gas
let msg_value: Int = self.msg_value(ctx.value);
let fwd_fee: Int = ctx.readForwardFee();
if (msg.forward_ton_amount > 0) {
msg_value = ((msg_value - msg.forward_ton_amount) - fwd_fee);
send(SendParameters{
to: self.owner,
value: msg.forward_ton_amount,
mode: SendPayGasSeparately,
bounce: false,
body: TokenNotification{ // 0x7362d09c -- Remind the new Owner
query_id: msg.query_id,
amount: msg.amount,
from: msg.from,
forward_payload: msg.forward_payload
}.toCell()
}
);
}
// 0xd53276db -- Cashback to the original Sender
if (msg.response_destination != null && msg_value > 0) {
send(SendParameters{
to: msg.response_destination!!,
value: msg_value,
bounce: false,
body: TokenExcesses{query_id: msg.query_id}.toCell(),
mode: SendPayGasSeparately
}
);
}
}
receive(msg: TokenBurn){
let ctx: Context = context();
require(ctx.sender == self.owner, "Invalid sender"); // Check sender
self.balance = (self.balance - msg.amount); // Update balance
require(self.balance >= 0, "Invalid balance");
let fwd_fee: Int = ctx.readForwardFee(); // Gas checks
require(ctx.value > ((fwd_fee + 2 * self.gasConsumption) + self.minTonsForStorage), "Invalid value - Burn");
// Burn tokens
send(SendParameters{
to: self.master,
value: 0,
mode: SendRemainingValue,
bounce: true,
body: TokenBurnNotification{
query_id: msg.query_id,
amount: msg.amount,
sender: self.owner,
response_destination: msg.response_destination
}.toCell()
}
);
}
fun msg_value(value: Int): Int {
let msg_value: Int = value;
let ton_balance_before_msg: Int = (myBalance() - msg_value);
let storage_fee: Int = (self.minTonsForStorage - min(ton_balance_before_msg, self.minTonsForStorage));
msg_value = (msg_value - (storage_fee + self.gasConsumption));
return msg_value;
}
bounced(msg: bounced<TokenTransferInternal>){
self.balance = (self.balance + msg.amount);
}
bounced(msg: bounced<TokenBurnNotification>){
self.balance = (self.balance + msg.amount);
}
get fun get_wallet_data(): JettonWalletData {
return
JettonWalletData{
balance: self.balance,
owner: self.owner,
master: self.master,
code: initOf JettonDefaultWallet(self.owner, self.master).code
};
}
}
我们可以看到很多 receive() 开头的代码,这是因为所有交易都是讯息,智能合约中的代码则会判断收到讯息后会执行什么逻辑,同时更新链上储存的数据。有几点非常特别的地方,TON链上储存数据是需要费用的,这个费用在这里minTonsForStorage 定义了,据文档资料显示,0.1TON可以在链上储存 10KB 资料 2.5年。另外TON 异步链,我们在编写智能合约的时候,也需要注意这一点,一个交易当中可能包含数个不同的讯息,每个讯息都不会同步完成的,所以我们不能理所当然的在不同合约上加入线性逻辑。以 receive(msg: TokenTransfer) 和 receive(msg: TokenTransferInternal) 为例,假设 BOB 要转一笔Jetton给ALICE,BOB是通过自己的钱包地址(其实也是合约),向属于BOB的JettonWallet 发送TokenTransfer 的讯息,BOB的JettonWallet在收到讯息后,便扣减BOB在JettonWallet 中的余额,然后再向ALICE 的JettonWallet发送 TokenTransferInternal 的讯息,ALICE 的JettonWallet在收到讯息后再更新ALICE的余额,这样便完成转帐。(Excess的部分我们先忽略)
你可以看到,按照这个写法,代币的转账是完全不需要经过Jetton的主合约的,只需要两个钱包合约之间来回沟通便可以完成。下一篇我们会尝试在这Jetton 范本的基础上加入自定义的MINT 功能,欢迎一起参与讨论。
如果你只需要一个简单的代币合约,只需到 https://minter.ton.org/ 上输入表单即可。这个方法无需任何代码!