应用内token
很多应用都会存在内置代币,用于应用内消费使用场景。如果使用通用的Coin代币标准,有可能会被用户无许可添加到交易所流通,也会造成困扰。一个非常值得被了解的技术方案是Token标准。Token可以被限定在应用内的使用方式,比如是否可以被转移、被消费、以及将来可否被转换成Coin, 非常适合作为应用内代币。
Token 基础概念
Token
, Coin
, Balance
的数据结构可以互相转换。
flowchart RL
subgraph " Balance has store { u64 } "
A["Balance<T>
Supply<T>"];
end
subgraph " Coin has key, store { Balance } "
B["Coin<T>
TreasuryCap<T>"];
end
subgraph " Token has key { Balance } "
C["Token<T>
TreasuryCap<T>"];
end
B-- to_balance -->A;
C-- to_coin -->B;
flowchart LR
subgraph " Balance has store { u64 } "
A["Balance<T>
Supply<T>"];
end
subgraph " Coin has key, store { Balance } "
B["Coin<T>
TreasuryCap<T>"];
end
subgraph " Token has key { Balance } "
C["Token<T>
TreasuryCap<T>"];
end
A-- from_balance -->B;
B-- from_coin -->C;
Coin
的数据结构在定义时包含key
和store
能力,在上一节课程中属于可以随意被转移和交易的Object, 而Token
的数据结构定义只包含key
能力,因此不能随意被转移,可以被限定使用场景。
// defined in `sui::coin`
struct Coin<phantom T> has key, store { id: UID, balance: Balance<T> }
// defined in `sui::token`
struct Token<phantom T> has key { id: UID, balance: Balance<T> }
在Token
中,有四种操作是被限定的:
token::transfer
- 将Token
转移到其他地址token::to_coin
- 将Token
转变为Coin
token::from_coin
- 将Coin
转变为Token
token::spend
- 将Token
消费掉
这四种操作的函数,都会产生ActionRequest<T>
的数据结构,比如看token::spend
函数的定义。
public fun spend<T>(t: Token<T>, ctx: &mut TxContext): ActionRequest<T>
其中,ActionRequest<T>
是属于Hot Potato模式的数据结构,必须要被后续的函数分解,否则会阻塞函数运行并回退。
在token
模块中,提供了TreasuryCap
, TokenPolicy
和 TokenPolicyCap
三个Object以及对应的函数方法去分解ActionRequest<T>
.
它们的使用方法有很多,支持了很多灵活的自定义方式。本小节教程提供一个简单的代码示例。
示例讲解
完整代码在这里。
引入模块
当我们的项目工程变得复杂时,需要将代码分在不同模块。这个示例代码是本系列课程第一次出现的分模块代码。/sources
目录下,将代码分为了两个模块。
sources
├── app_token.move
├── profile.move
其中,profile.move
使用的是之前的示例代码,包含创建Profile
和每个epoch打卡一次的功能,需要在app_token.move
模块内被用到。
那么,在app_token.move
模块内引用Profile.move
的方法是,使用use
声明。
use app_token::profile::{Self, Profile};
表示引入了app_token
package下的profile
module的Self
和Profile
数据结构,其中Self
表示整个profile
模块。后续使用profile
模块的click
函数时,用profile::click(arg, ctx);
的格式去调用。
要使用token
功能,还需要从sui标准库中引入一些相关模块。
use sui::{
coin::{Self, TreasuryCap},
token::{Self, TokenPolicy, Token}
};
初始化
const DECIMALS: u8 = 0;
const SYMBOLS: vector<u8> = b"APP";
const NAME: vector<u8> = b"App";
const DESCRIPTION: vector<u8> = b"Token for Application";
const ICON_URL: vector<u8> = b"https://"; // Coin / Token Icon
public struct APP has drop {}
public struct AdminCap has key, store {
id: UID,
}
public struct AppTokenCap has key {
id: UID,
cap: TreasuryCap<APP>,
}
fun init(otw: APP, ctx: &mut TxContext) {
let deployer = ctx.sender();
let admin_cap = AdminCap { id: object::new(ctx) };
transfer::public_transfer(admin_cap, deployer);
let (treasury_cap, metadata) = coin::create_currency<APP>(
otw,
DECIMALS,
SYMBOLS,
NAME,
DESCRIPTION,
option::some(new_unsafe_from_bytes(ICON_URL)),
ctx
);
let (mut policy, cap) = token::new_policy<APP>(
&treasury_cap, ctx
);
let token_cap = AppTokenCap {
id: object::new(ctx),
cap: treasury_cap,
};
token::allow(&mut policy, &cap, token::spend_action(), ctx);
token::share_policy<APP>(policy);
transfer::share_object(token_cap);
transfer::public_transfer(cap, deployer);
transfer::public_freeze_object(metadata);
}
TreasuryCap
来自sui::coin
模块,直接使用coin::create_currency<T>
方法创建。返回的metadata是记录代币图标、Tick等信息的数据,如果不再改动可以冻结起来。
let (treasury_cap, metadata) = coin::create_currency<APP>(
otw,
DECIMALS,
SYMBOLS,
NAME,
DESCRIPTION,
option::some(new_unsafe_from_bytes(ICON_URL)),
ctx
);
transfer::public_freeze_object(metadata);
在示例代码中,提供了获取和消费token
的两种场景。
先使用TreasuryCap
来创建TokenPolicy
和TokenPolicyCap
, 添加支持消费spend
的许可。然后共享TokenPolicy
, 把TokenPolicyCap
发送给部署合约地址。
let (mut policy, cap) = token::new_policy<APP>(
&treasury_cap, ctx
);
token::allow(&mut policy, &cap, token::spend_action(), ctx);
token::share_policy<APP>(policy);
transfer::public_transfer(cap, deployer);
定义一个AppTokenCap
结构,将TreasuryCap
封装起来,这是避免任何人都能直接使用TreasuryCap
去执行任意操作,只能按照本模块合约内定义的函数方法去调用。最后将AppTokenCap
共享。
public struct AppTokenCap has key {
id: UID,
cap: TreasuryCap<APP>,
}
let token_cap = AppTokenCap {
id: object::new(ctx),
cap: treasury_cap,
};
transfer::share_object(token_cap);
定义一个管理员权限AdminCap
, 并且定义一个可以通过AdminCap
, 从AppTokenCap
中获取TreasuryCap
可变引用的函数。因为,将来如果需要定期将被消费过的Token
清除,还需要通过&mut TreasuryCap
调用token::flush
函数。
public struct AdminCap has key, store {
id: UID,
}
public fun treasury_borrow_mut(
_admin: &AdminCap,
app_token_cap: &mut AppTokenCap,
): &mut TreasuryCap<APP> {
&mut app_token_cap.cap
}
获得token
这是获得token
的函数实现click2earn
.
public fun click2earn(
profile: &mut Profile,
token_cap: &mut AppTokenCap,
ctx: &mut TxContext
) {
profile::click(profile, ctx);
let app_token: Token<APP> = token::mint(&mut token_cap.cap, profile::points(profile), ctx);
emit(Click2EarnEvent {
user: ctx.sender(),
amount: token::value<APP>(&app_token),
});
let req: ActionRequest<APP> = token::transfer<APP>(app_token, ctx.sender(), ctx);
token::confirm_with_treasury_cap<APP>(
&mut token_cap.cap,
req,
ctx
);
}
先要成功执行profile::click(profile, ctx);
函数,接着使用token::mint
方法创建了app_token: Token<APP>
. app_token
是变量名,Token<APP>
是数据格式。
当使用token::transfer
方法将app_token: Token<APP>
转移给用户地址时,会产生req: ActionRequest<APP>
.
最后使用token::confirm_with_treasury_cap
将req: ActionRequest<APP>
分解,函数就会成功执行。调用req: ActionRequest<APP>
的第一个变量是&mut TreasuryCap<APP>
, 可以输入&mut token_cap.cap
, 表示AppTokenCap
的cap
属性,也就是被封装在AppTokenCap
里的TreasuryCap<APP>
.
消费token
这是消费token
的函数实现buy
.
public fun buy(
payment: Token<APP>,
price_record: &PriceRecord,
purchased_record: &mut PurchasedRecord,
item: String,
token_prolicy: &mut TokenPolicy<APP>,
ctx: &mut TxContext
) {
let price = &price_record.prices[item];
assert!(token::value<APP>(&payment) == *price, EWrongAmount);
let req = token::spend(payment, ctx);
token::confirm_request_mut(token_prolicy, req, ctx);
table::add<String, bool>(&mut purchased_record.record, item, true);
emit(BuyEvent {
buyer: ctx.sender(),
item,
price: *price,
});
}
输入参数说明:
payment: Token<APP>
- 要被消费的Token<APP>
price_record: &PriceRecord
- 记录所有商品价格的数据,是Shared Objectpurchased_record: &mut PurchasedRecord
- 每个用户独有的记录已购买产品信息的Objectitem: String
- 购买的项目的名称,String类型token_prolicy: &mut TokenPolicy<APP>
- 之前在初始化时被共享的TokenPolicy
, 已经添加了spend
许可
获取商品的价格并与传入Token<APP>
的面值对比,如果不一致会报错。
let price = &price_record.prices[item];
assert!(token::value<APP>(&payment) == *price, EWrongAmount);
let req: ActionRequest<APP> = token::spend(payment, ctx);
token::confirm_request_mut(token_prolicy, req, ctx);
使用token::spend
方法消费Token<APP>
会产生包含spend
和数量信息的req: ActionRequest<APP>
.
由于token_prolicy: &mut TokenPolicy<APP>
在初始化函数里已经被添加了spend
许可,在这里可以用token::confirm_request_mut
方法将ActionRequest<APP>
分解掉。
最后,在PurchasedRecord
中添加购买记录。如果之前已经购买过,table
会阻止重复添加相同的Key
, 就会阻止函数执行并且回退。
table::add<String, bool>(&mut purchased_record.record, item, true);
其他功能
申领 PurchasedRecord
public fun get_purchased_record(ctx: &mut TxContext) {
let record = PurchasedRecord {
id: object::new(ctx),
record: table::new<String, bool>(ctx),
};
transfer::transfer(record, ctx.sender());
}
管理员设置或删除商品价格
public fun set_item_price(
_admin: &AdminCap,
price_record: &mut PriceRecord,
item: String,
price: u64,
) {
if (table::contains<String, u64>(&price_record.prices, item)) {
let item_price = &mut price_record.prices[item];
*item_price = price;
} else {
table::add<String, u64>(&mut price_record.prices, item, price);
};
}
public fun remove_item_price(
_admin: &AdminCap,
price_record: &mut PriceRecord,
item: String,
) {
table::remove<String, u64>(&mut price_record.prices, item);
}
作业
阅读Token
标准与sui::token
源代码。
在本小节示例代码的基础上,新建一个可以支持token::to_coin
的TokenPolicy
, 当Profile
的点数到达100时,可以支持将所有持有的Token<APP>
转为Coin<APP>
, 并且每次转换,都会将Profile
的点数清零。
app_token.move
和profile.move
模块都需要添加新代码,支持token::to_coin
的TokenPolicy
需要被封装起来。