理解eosio.token合约

我必须承认,学习eosio一直没有闲庭信步的感觉,我可以看到为什么很多人说它有一个陡峭的学习曲线。随着eosio软件继续经历大量快速发展,文档数量有限,很少有工作实例可供参考。我已经被困了好几次,也希望帮助改善下一个开发人员的体验。在本文中,我将通过将其分解为单独的部分来讨论eosio.token合约。

什么是eosio.token合约?

eosio.token合约允许创建许多不同的代币。这使任何人都能够创建和发送代币。每个代币必须由issuer帐户发行。由于帐户可以包含多方,因此你可以使用具有所有者和活动权限的普通帐户或自定义配置帐户来创建和管理代币。每个代币都是asset类型,如下所示:

1
2
3
4
5
1000000000.0000 SYS

1.0000 SYMBOL

0.10 SYS

asset类型是一个数字(如果我没记错的话可以达到18位小数)和一个可以在1-7个大写字母之间的符号。此合约有三个操作可用于与之交互。它们是:创建,发布和转账。

创建用于定义新代币的特征。这包括代币asset符号,最大供应量以及允许发出代币的帐户。创建还会将新代币配置保留在区块链上。这意味着新代币配置的存储必须由某人放置。正如你稍后将看到的,部署此合约的帐户(在我们的案例中为’eosio.token’)也将支付代币配置存储。

发布用于增加代币的有效供应。可以持续发出代币,直到达到最大供应量。在代币创建期间定义的issuer必须批准此操作才能使其成功。

转账让一个帐户将代币转移到另一个帐户。

部署合约

你应该知道的第一件事是每个eosio智能合约都属于一个eosio帐户。合约基本上是其他帐户可以与之交互的对象。合约包含在区块链上执行代码的操作actions。合约可以直接访问区块链上的存储,删除和更新数据。

将一个action推送到合约需要至少一个帐户的授权。根据合约的复杂性,可能需要进一步的帐户和权限。帐户可以由基于权限的配置中设置的单个或多个个人组成。智能合约只能由一个帐户运行,而一个帐户只能拥有一个智能合约。最佳做法是为帐户和合约提供相同(小写)的名称。

在你与eosio.token合约进行交互之前,你需要创建一个具有相同名称的帐户,并将合约部署到该帐户。

首先创建一个帐户

1
$cleos create account eosio eosio.token <OWNER-KEY> <ACTIVE-KEY>

然后编译合约

1
2
3
$cd eos/contract/eosio.token

$eosiocpp -o eosio.token.wast eosio.token.cpp

最后将合约部署到帐户上

1
$cleos set contract eosio.token ../eosio.token

你可以验证合约是否已部署

1
$cleos get code eosio.token

合约架构

合约分为两个文件eosio.token.cppeosio.token.hpp.hpp文件定义合约类,操作和表,而.cpp文件实现操作逻辑。让我们首先看一下将用于实例化合约对象的合约类。(我从eosio.token.hpp中删除了一些遗留代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* @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()。这些将由转移操作调用。

定义的两个表是accountsstataccounts表由不同的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对象,用于他持有的每个代币,SYSEOS

还有一个名为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添加到supplyissuer也将此supply添加到他们的余额中,以便初始供应可以追溯到他们的帐户。

紧接着,通过SEND_INLINE_ACTION()调用transfer函数,这会将资金进行转移。论点如下:

  • 1.*this是行动所属的合约代码。
  • 2.transfer操作的名称。
  • 3.{st.issuer, N(active)}操作所需的权限。
  • 4.{st.issuer, to, quantity, memo}操作本身的参数。

这将我们带到最后的转账操作。转账操作将从fromtoquantitymemo获取四个输入参数。from是谁将发送代币,因此quantity将从他们的余额中减去。to是谁将收到代币,因此quantity将被添加到他们的余额。quantity是要发送的代币数量,memo是一个可以与交易一起发送的字符串。memo未在本合约中使用或存储。

该操作首先要求from accounts帐户权限并对fromto帐户执行发布处理。该符号从quantity提取并用于获取代币的currency_stats

require_recipient()函数将在操作完成时通知发送方和接收方。

完成更多发布处理,最后调用两个私有函数sub_balance()add_balance()以从发送add_balance()减去代币余额并增加接收方的代币余额。

这是完整的’eosio.token.cpp’文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* @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) )

示例命令:

1
2
3
4
5
$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

表命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$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合约