跳至主要内容
此页面是从英文翻译而来的。请注意,与原始页面相比,可能会出现错误或差异。真实的文档来源应始终是英文版本。

“创建代币”

代币是一种可拥有的数字资产,例如虚拟收藏品或游戏内货币。它只不过是一个数据结构 它存储在区块链上。

代币合约定义了构成代币的数据结构,这些结构的存储, 以及为操纵代币可以采取的行动。

区块链代币有两种广泛使用的类型: -可替代代币可以互换,每个代币彼此相等,就像游戏中的金币一样 -不可替代的代币是独一无二的,比如收藏卡或一块土地

在本教程中,您将创建一种名为GOLD的游戏内货币,这是一种可替代的代币

创建新合约

首先,让我们搭建一个基本的合约脚手架。

创建一个 token.cpp 文件并添加以下代码:

#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
#include <eosio/singleton.hpp>
using namespace eosio;

CONTRACT token : public contract {

public:
using contract::contract;

// TODO: Add actions
};

创建动作

我们的代币合约将有三个动作:

    ACTION issue(name to, asset quantity){

}

ACTION burn(name owner, asset quantity){

}

ACTION transfer(name from, name to, asset quantity, std::string memo){

}

将它们添加到您的合约中,然后让我们深入研究每个操作,看看它们做了什么,以及它们采用了哪些参数。

问题

那个 issue action 会创建新的代币并将其添加到账户余额和总供应量中。

它需要两个参数: -to:将向其发放代币的账户 -数量:要发行的代币数量

Burn

那个 burn action 会从账户余额和总供应量中移除代币。

它需要两个参数: -所有者:将烧掉代币的账户 -数量:要销毁的代币数量

转账

那个 transfer action 将代币从一个账户转移到另一个账户。

它需要四个参数: -from:发送代币的账户 -to:接收代币的账户 -数量:要转移的代币数量 -备忘录:转账时要包含的备忘录

设置符号和精度

每个可替代代币都有一个符号和一个精度

符号是代币的标识符(比如EOS、BTC,或者在我们的例子中为GOLD),而精度是代币支持的小数位数。 我们将在合约中添加一个常量变量来定义 symbolprecision 我们的代币。

在上面加上这个 issue 动作:

    const symbol TOKEN_SYMBOL = symbol(symbol_code("GOLD"), 4);

ACTION issue ...

以上几行意味着我们将使用该符号创建代币 GOLD 而且精度为 4

看起来会像 100.0000 GOLD 要么 0.0001 GOLD

添加数据结构

现在您已经定义了操作,接下来让我们添加将用于存储令牌数据的数据结构。

把这个放在下面 TOKEN_SYMBOL 你刚刚添加了。

    const symbol TOKEN_SYMBOL = symbol(symbol_code("GOLD"), 4);

TABLE balance {
name owner;
asset balance;

uint64_t primary_key()const {
return owner.value;
}
};

using balances_table = multi_index<"balances"_n, balance>;

你刚刚创建了一个 balance 结构,它定义了将存储在 balances 桌子。 然后,你定义了 balances_table type,这是表的定义,该表将存储表的行 balance 模型。

稍后您将使用 balances_table 键入以实例化对的引用 balances 表,然后使用该引用 在区块链中存储和检索数据。

那个 owner 属性的类型是 name (EOS 账户名称),并将用于识别拥有代币的账户。 那个 name type 是一种高效地将字符串打包成 64 位整数的方法。它仅限于 a-z、1-5 和一个句点,并且可以 长度不超过 12 个字符。

那个 balance 属性的类型是 asset 并将用于存储账户拥有的代币数量。 那个 asset type 是一种特殊类型,包括符号、精度和金额。它有 asset.symbol 属性 还有 asset.amount 属性(其类型为 int64_t)。

那个 primary_key 结构中的函数用于唯一标识每行以进行索引。在这种情况下, 我们正在使用 owner 字段作为主键,但使用 uint64_t 取而代之的是代表以提高效率。

接下来,您需要另一张表来存储代币的总供应量。在下面加上这个 balances_table 你刚刚添加了:

    using supply_table = singleton<"supply"_n, asset>;

我们在这里使用的是另一种类型的表格,a singleton。A singleton 是每个作用域只有一行的表。 这非常适合存储配置之类的东西。我们将使用它来存储代币的总供应量,就像你一样 你的合约中只有一个代币。

你可以看到,我们也没有定义要存储的自定义结构,因为我们只需要 asset 键入要存储 总供应量。

填写动作

现在您已经定义了数据结构,让我们填写操作。

问题

首先,我们将从 issue action,它将创建新的代币并将其添加到账户的余额中。

我们只希望部署合约的账户能够调用 issue 动作,所以我们将添加 一种断言,用于确保调用操作的账户与部署合约的账户相同。

    ACTION issue(name to, asset quantity){
check(has_auth(get_self()), "only contract owner can issue new GOLD");
}

接下来,我们要确保我们将发行代币的账户存在于区块链上。我们不想要那样 游戏内甜蜜的金币要浪费掉了!

    ...
check(is_account(to), "the account you are trying to issue GOLD to does not exist");

接下来,我们要确保 quantity 参数是正数,并且是正确的 symbolprecision.

    ...
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must issue a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");

Shwew!这是很多支票,但重要的是要确保我们在保护游戏中的金币!

现在让我们开始处理余额表。

    ...
balances_table balances(get_self(), get_self().value);

我们拿了 balances_table 我们之前定义的类型并实例化了一个新的 balances_table 对象。我们通过了 get_self() 函数作为第一个参数( code 参数),返回合约账户的名称。我们通过了 get_self().value 作为第二个参数( scope 参数),它返回 uint64_t 表示合约账户的名称。

作用域:作用域是一种将表格中的行分组在一起的方法。你可以把它想象成一个文件夹 包含表中的所有行。在这种情况下,我们使用合约账户的名称作为范围,所以所有 中的行 balances 表格将在合约账户的名称下分组。如果你愿意 想了解更多关于瞄准镜的信息,请查看 智能合约入门指南

接下来,我们需要检查是否 to 账户已经有余额。我们可以使用以下方法来做到这一点 find 上的函数 balances 桌子。

    ...
auto to_balance = balances.find(to.value);

那个 find 函数返回指向表中与主键匹配的行的迭代器。如果 to 账户确实如此 没有余额,那么 find 函数将返回指向表末尾的迭代器。请记住,主键 因为这张桌子是 uint64_t,所以我们需要使用 to.value 要获得 uint64_t 的代表 to 账户。

如果已经有余额,那么我们需要将新代币添加到现有余额中。我们可以使用以下方法来做到这一点 modify 上的函数 balances 桌子。我们会检查一下是否 to_balance 迭代器不等于的结尾 表,如果不是,我们将修改该行。

    ...
if(to_balance != balances.end()){
balances.modify(to_balance, get_self(), [&](auto& row){
row.balance += quantity;
});
}

那个 modify 函数需要三个参数: -迭代器:指向要修改的行的迭代器 -payer:为存储修改后的行支付内存费用的账户 -lambda:一个 lambda 函数,它提供对要修改的行的引用

lambda 函数是您实际修改行的地方。在这种情况下,我们将新代币添加到现有代币中 平衡。

如果还没有余额,那么我们需要创建一个新的余额 to 账户。我们可以通过使用来做到这一点 这 emplace 上的函数 balances 桌子。

    ...
else{
balances.emplace(get_self(), [&](auto& row){
row.owner = to;
row.balance = quantity;
});
}

那个 emplace 函数有两个参数: -付款人:为存储新行支付内存费用的账户 -lambda:一个提供对新行的引用的 lambda 函数

lambda 函数是您实际初始化新行的地方。在本例中,我们正在设置 ownerto 账户,以及 balancequantity

最后,我们需要更新代币的总供应量。我们可以通过获取 supply 桌子。

    ...
supply_table supply(get_self(), get_self().value);
auto current_supply = supply.get_or_default(asset(0, TOKEN_SYMBOL));

我们拿了 supply_table 我们之前定义并实例化了一个新的 supply_table 对象。和以前一样,我们通过了 在 get_self() 第一个和第二个参数的函数(分别为: code,以及 scope)。

接下来,我们使用了 get_or_default 在单例上使用函数来获取代币的当前供应量,或者创建一个新的代币 如果这是本合约中发行的第一批代币。那个 get_or_default 函数需要一个参数, 如果还不存在任何值,则为要创建的值。在我们的例子中,该默认值是一个新值 asset 那我们 初始化时值为 0 还有 TOKEN_SYMBOL 我们之前定义的常量。这看起来像 0.0000 GOLD

现在我们有了当前的供应量,我们可以将新的代币添加到当前供应量中,并将价值保存到区块链中。 既然两个 current_supplyquantity 属于类型 asset,我们可以使用 + 运算符将它们相加。

自动溢出保护

那个 asset 类会自动处理溢出/下溢。如果有溢出 它会抛出一个错误并自动中止交易。你不必做任何事情 使用时的特殊检查 asset。但是,如果使用,则需要这样做 uint64_t 或任何其他基本类型。

    ...
auto new_supply = current_supply + quantity;
supply.set(new_supply, get_self());

我们使用了 set 在单例上使用函数,将新的供应保存到区块链。

那个 set 函数有两个参数: -value:要保存到区块链的新价值 -付款人:为存储新值支付内存费用的账户

Burn

那个 burn 动作非常类似于 issue 行动。唯一的区别是我们从中减去代币 这 owner 账户和供应量,而不是增加它们。

让我们像以前一样从检查开始,然后进入逻辑。

    ACTION burn(name owner, asset quantity){
check(has_auth(owner), "only the owner of these tokens can burn them");
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must burn a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");
}

我们正在做与在... 相同的检查 issue 操作,除了 is_account 检查一下,因为我们已经是 测试看看是否 owner 有平衡的 balances 桌子。

    ...
balances_table balances(get_self(), get_self().value);
auto owner_balance = balances.find(owner.value);
check(owner_balance != balances.end(), "account does not have any GOLD");

现在让我们来看看是否 owner 账户有足够的代币可以烧掉。

    ...
check(owner_balance->balance.amount >= quantity.amount, "owner doesn't have enough GOLD to burn");

让我们计算一个新的余额 owner 账户。

    ...
auto new_balance = owner_balance->balance - quantity;

我们不需要检查是否 new_balance 低于零,因为我们已经检查过是否 owner 账户有足够的代币 烧掉。

让我们从中减去代币 owner 账户。如果 new_balance 为零,那么我们就可以抹掉了 一行来自 balances 要保存的表 RAM

    ...
if(new_balance.amount == 0){
balances.erase(owner_balance);
}

如果 new_balance 不为零,那么我们需要修改中的行 balances 桌子。

    ...
else {
balances.modify(owner_balance, get_self(), [&](auto& row){
row.balance -= quantity;
});
}

我们还需要从总供应量中移除代币。

    ...
supply_table supply(get_self(), get_self().value);
supply.set(supply.get() - quantity, get_self());

瞧,现在我们可以烧掉虚拟金币了。

转账

那个 transfer 动作要比动作复杂一点 issueburn 行动。我们需要从中转移代币 一个账户转到另一个账户,并确保 from 账户有足够的代币可以转移。

最重要的是,我们希望做到这一点,以便其他合约可以与我们的代币进行交互,这样他们就可以 在它之上建造东西。

再说一遍,让我们从检查开始,然后进入逻辑。

    ACTION transfer(name from, name to, asset quantity, string memo){
check(has_auth(from), "only the owner of these tokens can transfer them");
check(is_account(to), "to account does not exist");
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must transfer a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");
}

我们所做的检查与以前基本相同,但这次我们要确保 from 账户(发件人)就是那个 那就是授权转移,我们正在确保 to 账户已存在。

接下来,我们需要获得 balances 表并检查是否 from 账户有余额。

    ...
balances_table balances(get_self(), get_self().value);
auto from_balance = balances.find(from.value);
check(from_balance != balances.end(), "account does not have any GOLD");

让我们来看看是否 from 账户有足够的代币可以转移。

    ...
check(from_balance->balance.amount >= quantity.amount, "owner doesn't have enough GOLD to transfer");

我们需要检查一下是否 to 账户有余额在 balances 桌子。

    ...
auto to_balance = balances.find(to.value);

如果 to 账户没有余额,那么我们需要在里面新建一行 balances 桌子。

    ...
if(to_balance == balances.end()){
balances.emplace(get_self(), [&](auto& row){
row.owner = to;
row.balance = quantity;
});
}

如果 to 账户确实有余额,那么我们需要修改该行 balances 桌子。

    ...
else {
balances.modify(to_balance, get_self(), [&](auto& row){
row.balance += quantity;
});
}

现在我们需要检查一下是否 from 账户的余额等于 quantity 我们正在转移。如果 确实如此,然后我们可以从中删除该行 balances 表,然后再次保存RAM

    ...
if(from_balance->balance.amount == quantity.amount){
balances.erase(from_balance);
}

如果 from 账户的余额大于 quantity 我们正在转移,那我们需要 修改中的行 balances 桌子。

    ...
else {
balances.modify(from_balance, get_self(), [&](auto& row){
row.balance -= quantity;
});
}

最后,我们需要发出一个其他合约可以监听的事件。我们将发出两个事件,其中一个事件有 from 账户作为收件人,另一个账户拥有 to 账户为收件人。这允许任何一方倾听 如果他们向该账户部署了合同,就去活动然后用它做点什么。

    ...
require_recipient(from);
require_recipient(to);

完整合同

如果您想复制完整的合同,并将其与您的合同进行匹配,可以在下面找到。

点击这里查看完整代码
#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
#include <eosio/singleton.hpp>
using namespace eosio;

CONTRACT token : public contract {
public:
using contract::contract;

const symbol TOKEN_SYMBOL = symbol(symbol_code("GOLD"), 4);

TABLE balance {
name owner;
asset balance;

uint64_t primary_key()const {
return owner.value;
}
};

using balances_table = multi_index<"balances"_n, balance>;

using supply_table = singleton<"supply"_n, asset>;




ACTION issue(name to, asset quantity){
check(has_auth(get_self()), "only contract owner can issue new GOLD");
check(is_account(to), "the account you are trying to issue GOLD to does not exist");
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must issue a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");

balances_table balances(get_self(), get_self().value);

auto to_balance = balances.find(to.value);

if(to_balance != balances.end()){
balances.modify(to_balance, get_self(), [&](auto& row){
row.balance += quantity;
});
}
else{
balances.emplace(get_self(), [&](auto& row){
row.owner = to;
row.balance = quantity;
});
}

supply_table supply(get_self(), get_self().value);

auto current_supply = supply.get_or_default(asset(0, TOKEN_SYMBOL));

supply.set(current_supply + quantity, get_self());
}

ACTION burn(name owner, asset quantity){
check(has_auth(owner), "only the owner of these tokens can burn them");
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must burn a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");

balances_table balances(get_self(), get_self().value);
auto owner_balance = balances.find(owner.value);
check(owner_balance != balances.end(), "account does not have any GOLD");
check(owner_balance->balance.amount >= quantity.amount, "owner doesn't have enough GOLD to burn");

auto new_balance = owner_balance->balance - quantity;
check(new_balance.amount >= 0, "quantity exceeds available supply");

if(new_balance.amount == 0){
balances.erase(owner_balance);
}
else {
balances.modify(owner_balance, get_self(), [&](auto& row){
row.balance -= quantity;
});
}

supply_table supply(get_self(), get_self().value);
supply.set(supply.get() - quantity, get_self());
}

ACTION transfer(name from, name to, asset quantity, std::string memo){
check(has_auth(from), "only the owner of these tokens can transfer them");
check(is_account(to), "to account does not exist");
check(quantity.is_valid(), "invalid quantity");
check(quantity.amount > 0, "must transfer a positive quantity");
check(quantity.symbol == TOKEN_SYMBOL, "symbol precision and/or ticker mismatch");

balances_table balances(get_self(), get_self().value);
auto from_balance = balances.find(from.value);
check(from_balance != balances.end(), "from account does not have any GOLD");
check(from_balance->balance.amount >= quantity.amount, "from account doesn't have enough GOLD to transfer");

auto to_balance = balances.find(to.value);
if(to_balance == balances.end()){
balances.emplace(get_self(), [&](auto& row){
row.owner = to;
row.balance = quantity;
});
}
else {
balances.modify(to_balance, get_self(), [&](auto& row){
row.balance += quantity;
});
}

if(from_balance->balance.amount == quantity.amount){
balances.erase(from_balance);
}
else {
balances.modify(from_balance, get_self(), [&](auto& row){
row.balance -= quantity;
});
}

require_recipient(from);
require_recipient(to);
}
};

Grab 经过战斗测试的源代码

如果你只想使用 EOS Network 上大多数可替代代币中使用的源代码,你可以前往 esio.token 存储库来获取它。这场代码战不仅经过了测试,而且还为底层的EOS代币提供了动力。

请注意,标准 eosio.token 合约与本教程有很大不同。它更复杂 合约,它允许使用更高级的功能,例如允许用户与合约进行交互以购买自己的内存, 或者在单个合约中创建多个代币。

你需要 create 用它换一个新代币,然后 issue 这些代币在转账之前将其存入账户。 你还需要 open 账户余额,然后才能将代币转移到该账户。

挑战

这个代币没有 MAXIMUM_SUPPLY。你怎么能在合约中添加一个常量来定义最大供应量 令牌并确保 issue 动作没有超过这个最大供应量?