我必须承认,学习 eosio 一直没有闲庭信步的感觉,我可以看到为什么很多人说它有一个陡峭的学习曲线。随着 eosio 软件继续经历大量快速发展,文档数量有限,很少有工作实例可供参考。我已经被困了好几次,也希望帮助改善下一个开发人员的体验。在本文中,我将通过将其分解为单独的部分来讨论 eosio.token 合约。
什么是 eosio.token 合约?
eosio.token 合约允许创建许多不同的代币。这使任何人都能够创建和发送代币。每个代币必须由 issuer 帐户发行。由于帐户可以包含多方,因此你可以使用具有所有者和活动权限的普通帐户或自定义配置帐户来创建和管理代币。每个代币都是 asset 类型,如下所示:
1000000000.0000 SYS
1.0000 SYMBOL
0.10 SYS
asset 类型是一个数字(如果我没记错的话可以达到 18 位小数)和一个可以在 1 - 7 个大写字母之间的符号。此合约有三个操作可用于与之交互。它们是:创建,发布和转账。
创建用于定义新代币的特征。这包括代币 asset 符号,最大供应量以及允许发出代币的帐户。创建还会将新代币配置保留在区块链上。这意味着新代币配置的存储必须由某人放置。正如你稍后将看到的,部署此合约的帐户(在我们的案例中为 ’eosio.token’)也将支付代币配置存储。
发布用于增加代币的有效供应。可以持续发出代币,直到达到最大供应量。在代币创建期间定义的 issuer 必须批准此操作才能使其成功。
转账让一个帐户将代币转移到另一个帐户。
部署合约
你应该知道的第一件事是每个 eosio 智能合约都属于一个 eosio 帐户。合约基本上是其他帐户可以与之交互的对象。合约包含在区块链上执行代码的操作 actions。合约可以直接访问区块链上的存储,删除和更新数据。
将一个 action 推送到合约需要至少一个帐户的授权。根据合约的复杂性,可能需要进一步的帐户和权限。帐户可以由基于权限的配置中设置的单个或多个个人组成。智能合约只能由一个帐户运行,而一个帐户只能拥有一个智能合约。最佳做法是为帐户和合约提供相同(小写)的名称。
在你与 eosio.token 合约进行交互之前,你需要创建一个具有相同名称的帐户,并将合约部署到该帐户。
首先创建一个帐户
$cleos create account eosio eosio.token <OWNER-KEY> <ACTIVE-KEY>
然后编译合约
$cd eos/contract/eosio.token
$eosiocpp -o eosio.token.wast eosio.token.cpp
最后将合约部署到帐户上
$cleos set contract eosio.token ../eosio.token
你可以验证合约是否已部署
$cleos get code eosio.token
合约架构
合约分为两个文件 eosio.token.cpp 和 eosio.token.hpp。.hpp 文件定义合约类,操作和表,而.cpp 文件实现操作逻辑。让我们首先看一下将用于实例化合约对象的合约类。(我从 eosio.token.hpp 中删除了一些遗留代码)
/**
* @file
* @copyright defined in eos/LICENSE.txt
*/
#pragma once
#include <eosiolib/asset.hpp>
#include <eosiolib/eosio.hpp>
#include <string>
namespace eosiosystem {
class system_contract;
}
namespace eosio {
using std::string;
class token : public contract {
public:
token(account_name self):contract(self){}
void create(account_name issuer,
asset maximum_supply);
void issue(account_name to, asset quantity, string memo);
void transfer(account_name from,
account_name to,
asset quantity,
string memo );
private:
friend eosiosystem::system_contract;
inline asset get_supply(symbol_name sym)const;
inline asset get_balance(account_name owner, symbol_name sym)const;
struct account {
asset balance;
uint64_t primary_key()const { return balance.symbol.name(); }
};
struct currency_stats {
asset supply;
asset max_supply;
account_name issuer;
uint64_t primary_key()const { return supply.symbol.name(); }
};
typedef eosio::multi_index<N(accounts), account> accounts;
typedef eosio::multi_index<N(stat), currency_stats> stats;
void sub_balance(account_name owner, asset value);
void add_balance(account_name owner, asset value, account_name ram_payer);
};
asset token::get_supply(symbol_name sym)const
{
stats statstable(_self, sym);
const auto& st = statstable.get(sym);
return st.supply;
}
asset token::get_balance(account_name owner, symbol_name sym)const
{
accounts accountstable(_self, owner);
const auto& ac = accountstable.get(sym);
return ac.balance;
}
} /// namespace eosio
构造函数和操作被定义为公共成员函数。构造函数采用帐户名称(将是部署合约的帐户,也就是 eosio.token)并将其设置为 contract 变量。请注意,此类继承自 eosio::contract。
表和 helper 函数作为私有成员提供。两个内联函数在底部定义但从未使用过。这给我们留下了重要的函数 sub_balance() 和 add_balance()。这些将由转移操作调用。
表
定义的两个表是 accounts 和 stat。accounts 表由不同的 account 对象组成,每个 account 对象持有不同代币的余额。stat 表由持有供应,max_supply 和发行者的 currency_stats 对象(由 struct currency_stats 定义)组成。在继续之前,重要的是要知道该合约将数据保存到两个不同的范围。accounts 表的范围限定为 eosio 帐户,stat 表的范围限定为代币符号名称。
根据 eosio::multi_index 定义,code 是具有写权限的帐户的名称,scope 是存储数据的帐户。
范围本质上是一种在合约中划分数据的方法,以便只能在定义的空间内访问。在代币合约中,每个 eosio 帐户都用作 accounts 表的范围。accounts 表是一个包含多个 account 对象的多索引容器。每个 account 对象都由其代币符号编制索引,并包含代币余额。使用其范围查询用户的 accounts 表时,将返回用户具有现有余额的所有代币的列表。
这是我如何想象它。
在上图中,有一个名为 tom 的 eosio 帐户,他有自己的范围。在他的范围内是一个名为 accounts 的表。在该表中是一个单独的 account 对象,用于他持有的每个代币,SYS 和 EOS。
还有一个名为 stat 的第二个表。此表将包含现有代币的状态。新标记在其自己的符号名称范围内创建。范围内是一个包含 currency_stats 对象的 stat 表。与包含许多不同 account 对象的 accounts 表不同,stat 表仅包含给定标记符号的单个 currency_stats 对象。
操作
操作在.cpp 文件中实现。要创建新代币,必须发送创建操作。Create 有两个参数:发行者,以及新代币的最大供应量。issuer 是唯一允许增加新代币供应的人。issuer 不能超过最高供应量发行。
第一行代码只需要合约帐户本身的授权。这可以在推动操作时使用命令行标志 -p eosio.token 给出。
接下来的几行提取传入的 maximum_supply 资产的符号并执行一些错误处理。如果任何 eosio_assert 失败,那么所有代码都将被回滚,并且交易不会被推送到区块链。
一个 stat 表使用符号名称(标记符号)作为其范围构造为 statstable。代码检查代币是否已存在。如果没有,则创建新代币状态并将其保存到区块链中。emplace 函数中的第一个参数_self 意味着此合约帐户 eosio.token 将支付投注存储。
请注意,保存 supply 的符号,因为它用作定位表行的键,但供应量尚未发出。
你现在可以执行下一个操作,发布。发布将采用将收到已发行代币的帐户,发出的代币数量和备忘录。发布操作在一个中执行两个操作,因为它将修改创建的代币供应并调用转账操作以发送已发布的代币。同样,前几行提取代币符号并执行错误检查。
以下代码部分将使用符号名称作为范围构造 stat 表。这用作查找先前使用 create action 创建的代币的键。
请注意,从 statstable.find() 返回的 existing currency_stat 是一个指向找到的对象的迭代器。为简洁起见,声明了一个名为 st 的新变量,并将其设置为 existing 迭代器指向的实际对象。这让我们可以使用. 运算符访问成员变量而不是指针表示法 ->。
创建代币的 issuer 者需要签署此交易,并执行更多错误处理。
最后,修改现有代币的 currency_stats st,并将已发放的 quantity 添加到 supply。issuer 也将此 supply 添加到他们的余额中,以便初始供应可以追溯到他们的帐户。
紧接着,通过 SEND_INLINE_ACTION() 调用 transfer 函数,这会将资金进行转移。论点如下:
1.*this 是行动所属的合约代码。
2.transfer 操作的名称。
3.{st.issuer, N(active)} 操作所需的权限。
4.{st.issuer, to, quantity, memo} 操作本身的参数。
这将我们带到最后的转账操作。转账操作将从 from,to,quantity 和 memo 获取四个输入参数。from 是谁将发送代币,因此 quantity 将从他们的余额中减去。to 是谁将收到代币,因此 quantity 将被添加到他们的余额。quantity 是要发送的代币数量,memo 是一个可以与交易一起发送的字符串。memo 未在本合约中使用或存储。
该操作首先要求 from accounts 帐户权限并对 from 和 to 帐户执行发布处理。该符号从 quantity 提取并用于获取代币的 currency_stats。
require_recipient() 函数将在操作完成时通知发送方和接收方。
完成更多发布处理,最后调用两个私有函数 sub_balance() 和 add_balance() 以从发送 add_balance() 减去代币余额并增加接收方的代币余额。
这是完整的 ’eosio.token.cpp’ 文件。
/**
* @file
* @copyright defined in eos/LICENSE.txt
*/
#include “eosio.token.hpp”
namespace eosio {
void token::create(account_name issuer,
asset maximum_supply )
{
require_auth(_self);
auto sym = maximum_supply.symbol;
eosio_assert(sym.is_valid(), “invalid symbol name” );
eosio_assert(maximum_supply.is_valid(), “invalid supply”);
eosio_assert(maximum_supply.amount > 0, “max-supply must be positive”);
stats statstable(_self, sym.name() );
auto existing = statstable.find(sym.name() );
eosio_assert(existing == statstable.end(), “token with symbol already exists” );
statstable.emplace(_self, [&](auto& s) {
s.supply.symbol = maximum_supply.symbol;
s.max_supply = maximum_supply;
s.issuer = issuer;
});
}
void token::issue(account_name to, asset quantity, string memo)
{
auto sym = quantity.symbol;
eosio_assert(sym.is_valid(), “invalid symbol name” );
eosio_assert(memo.size() <= 256, “memo has more than 256 bytes” );
auto sym_name = sym.name();
stats statstable(_self, sym_name);
auto existing = statstable.find(sym_name);
eosio_assert(existing != statstable.end(), “token with symbol does not exist, create token before issue” );
const auto& st = *existing;
require_auth(st.issuer);
eosio_assert(quantity.is_valid(), “invalid quantity” );
eosio_assert(quantity.amount > 0, “must issue positive quantity”);
eosio_assert(quantity.symbol == st.supply.symbol, “symbol precision mismatch”);
eosio_assert(quantity.amount <= st.max_supply.amount – st.supply.amount, “quantity exceeds available supply”);
statstable.modify(st, 0, [&](auto& s) {
s.supply += quantity;
});
add_balance(st.issuer, quantity, st.issuer);
if(to != st.issuer) {
SEND_INLINE_ACTION(*this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );
}
}
void token::transfer(account_name from,
account_name to,
asset quantity,
string memo )
{
eosio_assert(from != to, “cannot transfer to self”);
require_auth(from);
eosio_assert(is_account( to), “to account does not exist”);
auto sym = quantity.symbol.name();
stats statstable(_self, sym);
const auto& st = statstable.get(sym);
require_recipient(from);
require_recipient(to);
eosio_assert(quantity.is_valid(), “invalid quantity” );
eosio_assert(quantity.amount > 0, “must transfer positive quantity”);
eosio_assert(quantity.symbol == st.supply.symbol, “symbol precision mismatch”);
eosio_assert(memo.size() <= 256, “memo has more than 256 bytes” );
sub_balance(from, quantity);
add_balance(to, quantity, from);
}
void token::sub_balance(account_name owner, asset value) {
accounts from_acnts(_self, owner);
const auto& from = from_acnts.get(value.symbol.name(), “no balance object found” );
eosio_assert(from.balance.amount >= value.amount, “overdrawn balance”);
if(from.balance.amount == value.amount) {
from_acnts.erase(from);
} else {
from_acnts.modify(from, owner, [&](auto& a) {
a.balance -= value;
});
}
}
void token::add_balance(account_name owner, asset value, account_name ram_payer)
{
accounts to_acnts(_self, owner);
auto to = to_acnts.find(value.symbol.name() );
if(to == to_acnts.end() ) {
to_acnts.emplace(ram_payer, [&](auto& a){
a.balance = value;
});
} else {
to_acnts.modify(to, 0, [&](auto& a) {
a.balance += value;
});
}
}
} /// namespace eosio
EOSIO_ABI(eosio::token, (create)(issue)(transfer) )
示例命令:
$cleos push action eosio.token create ‘[“usera”,”21000000.0000 DEMO”]’ -p eosio.token usera
$cleos push action eosio.token issue ‘[“usera”,”21000000.0000 DEMO”,”issuance”]’ -p usera
$cleos push action eosio.token tranfser ‘[“usera”,”userb”,”1000000.0000 DEMO”,”here you go”]’ -p usera
表命令:
$cleos get table eosio.token DEMO stat
{
“rows”: [{
“supply”: “21000000.0000 DEMO”
“max_supply”: “2100000000.0000 DEMO”
“issuer”: “usera”
}
],
“more”: false
}
$cleos get table eosio.token usera accounts
{
“rows”: [{
“balance”: “20000000.0000 DEMO”
}
],
“more”: false
}
$cleos get table eosio.token userb accounts
{
“rows”: [{
“balance”: “10000000.0000 DEMO”
}
],
“more”: false
}
注意:本文是在 Dawn4.1 代码发布时编写的。
======================================================================
分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:
EOS 教程,本课程帮助你快速入门 EOS 区块链去中心化应用的开发,内容涵盖 EOS 工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签 DApp 的开发。
java 以太坊开发教程,主要是针对 java 和 android 程序员进行区块链以太坊开发的 web3j 详解。
python 以太坊,主要是针对 python 工程师使用 web3.py 进行区块链以太坊开发的详解。
php 以太坊,主要是介绍使用 php 进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
以太坊入门教程,主要介绍智能合约与 dapp 应用开发,适合入门。
以太坊开发进阶教程,主要是介绍使用 node.js、mongodb、区块链、ipfs 实现去中心化电商 DApp 实战,适合进阶。
C#以太坊,主要讲解如何使用 C# 开发基于.Net 的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
java 比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与 UTXO 等,同时也详细讲解如何在 Java 代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是 Java 工程师不可多得的比特币开发学习课程。
php 比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与 UTXO 等,同时也详细讲解如何在 Php 代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是 Php 工程师不可多得的比特币开发学习课程。
tendermint 区块链开发详解,本课程适合希望使用 tendermint 进行区块链开发的工程师,课程内容即包括 tendermint 应用开发模型中的核心概念,例如 ABCI 接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是 go 语言工程师快速入门区块链开发的最佳选择。
汇智网原创翻译,转载请标明出处。这里是原文理解 eosio.token 合约