eos的资源利用机制:
比特币和以太坊中的交易手续费机制,其目的就是防止大量 垃圾交易使得系统拥堵。
eos通过增发,完全取消手续费,(根据账户中EOS通证的数量来分配系统资源。)
CPU与带宽:按抵押的EOS通证比例分配CPU与带宽。例如,如果你持有全网1%的EOS通证,那就可以抵押这些通证来获得全网1%的CPU和带宽。 这样就可以隔离开所有的DAPP,防止资源竞争和恶意的DDOS供给,无论其他的DAPP如何拥堵, 你自己的带宽都不受影响。
当不再需要CPU与带宽时,抵押的EOS通证可以赎回,在赎回的时候,存在三天的赎回期。
与CPU和带宽不同,要将数据存储在区块链中,你需要基于当前的RAM市场价格,用EOS通证买入 RAM,才能获得一定数量的存储字节。当你不再需要内存时,也可以将内存以当前的RAM市场价格 卖出得到EOS通证
RAM的价格是基于班科(Bancor)算法,也就是说是由市场供需调节的:如果 RAM供不应求,则买入RAM时就需要更多的EOS通证,而这时卖出RAM也能获得更多的EOS通证。
内存是消耗资源,不可赎回,只能买卖。以EOS上发币为例,目前发币需要200K的内存,一个 EOS可买20KB,按目前的存储价格发一个币需要消耗10个EOS。这是EOS内存消耗的刚需来源
nodeos:核心程序,用于启动eos节点服务,在后台运行,可以配置不同 插件。该进程负责账户管理、区块生成、共识建立,并提供智能合约的运行环境
keosd:钱包管理程序,负责钱包、密钥的管理和交易的签名
cleos:与nodeos和keosd交互的命令行工具,cleos通过RPC API 访问nodeos和keosd
nodeos启动节点服务器
Ctrl+C或者pkill nodeos退出nodeos终端
nodes的运行依赖于配置文件config.ini,在linux下,其默认位置为: ~/.local/share/eosio/nodeos/config/config.ini
enable-stale-production = true # 启用不稳定出块
producer-name = eosio # 出块节点名
http-validate-host = false # 是否验证http头信息
plugin = eosio::history_api_plugin # 启用history api插件
plugin = eosio::chain_api_plugin # 启用chain_api插件
access-control-allow-origin = * # CORS
http-server-address = 0.0.0.0:8888 # 监听地址
如果nodeos没有正常关闭,那么再次启动nodeos就会提示如下错误:
error: database dirty flag set. replay required.
如果你需要保留之前的数据,可以清除节点中的可逆块,然后重放 交易来重新生成:
~ nodeos --replay-blockchain
当然,你也可以完全删除整个数据目录,如果不再需要其中的账户和交易:
~ nodeos
钱包服务器的作用是管理存储私钥的钱包,并负责对交易进行签名。
~$ keosd 启动钱包服务
Ctrl+C或者pkill keosd结束keosd的运行
keosd的运行也依赖于配置文件,在linux下其默认路径为:~/eosio-wallet/config.ini
重要工具cleos,例如 创建钱包、创建密钥对、创建账户、部署合约、发送交易等等,都是 使用它。
在执行cleos命令之前,别忘了启动nodeos和keosd。
EOS使用非对称密钥对来鉴别交易的来源:在发送 交易时,用户需要使用私钥签名交易,然后节点就可以使用其公钥来 确认该交易来源与声称的相符。因此密钥是区块链身份识别的基础
钱包则是用来保存私钥的,一个钱包中可以保存多个私钥。由于钱包持有 私钥,因此它的另一个作用是对数据进行签名 —— 私钥不用出钱包就可以 完成交易的签名,这对资产安全很有好处。
与其他区块链不同,以太坊的账户是建立在区块链上的,每个账户都对应 于链上的账户表中的一条记录;另一点区别在于,EOS的一个账户需要 两组密钥。
钱包服务器负责钱包的管理,一个钱包服务器可以管理多个钱包
使用cleos的wallet creat子命令创建钱包
~$ cleos wallet create
PW5K8TVxnEH2ue5CFdr54NTZRzkyYxSpfg7VrMR49CsCdZavE9rhc
需要记住这个密码,因为后面还要用它来解锁钱包 —— 只有解锁的钱包,才可以 用来签名交易
可以使用-n选项来声明钱包的名称,以便创建额外的钱包。例如:
~$ cleos wallet create -n mytest
上面命令对应的钱包文件为~/eosio-wallet/mytest.wallet。同样, 别忘了记录下生成的钱包密码。
~$ cleos wallet list查看钱包列表
钱包后面带*号的表示当前处于解锁状态
重启keosd或者15分钟之内无操作,钱包都会关闭,需要重新解锁unlock
当然锁定时间可以在keosd的配置文件中修改,例如,将其延长至一小时
unlock-timeout = 3600
我们可以重启keosd来关闭所有钱包,或者执行wallet lock_all来锁定所有钱包:
~ cleos wallet unlock
解锁指定钱包$ cleos wallet unlock -n mywallet
按照命令提示输入钱包密码即可解锁成功PW5JQdyKRCdH1AS51AYKm8VAVTgoh5PiaTQ2GUtQYKFR8AwvejSmr
在发送 交易时,用户需要使用私钥签名交易,然后节点就可以使用其公钥来 确认该交易来源与声称的相符。因此密钥是区块链身份识别的基础
使用create key子命令创建一对新的密钥
cleos create key
由于从私钥可以推算出公钥,因此只需要把私钥导入钱包就可以了。下面的命令 执行后会显示导入成功的私钥所对应的公钥
~$ cleos wallet import 5JoKhub5Fb3tGNv2bfDP1yz2mbrWmAa3DeMJRKu4YujTwM98m4H(这是私钥)
在EOS中建立一个账户需要两个私钥,分别对应于Onwer权限和Active权限。Owner权限可以做 任何的事情,不过平时只是供着,只有在Active私钥丢失或被盗时才用来重置Active权限。 Active权限则用来进行日常的交易签名等操作。
因此在创建账户之前我们需要首先创建两组密钥并导入钱包,例如:
第一组,将其用于账户的Owner权限:
~$ cleos create key
Private key: 5Jmw8TexQMCqderdzNBjAVYn4Tj7z9c8aBTEYou3DBrCumwnpB8
Public key: EOS7xUuBw134uoURifmSCSSScJksxok6Pp6CSCujr4AVabDGTNVit
第二组,将其用于账户的Active权限:
~$ cleos create key
Private key: 5JZ8LhMULSijNtaS5E8VZ4dKMwAmQpARwBk4Z4Fo2CLw7oSefqM
Public key: EOS75K9RtMbjsr5WpT1md3fgeSwbYkqUF8MwgQbZtXpGWtVCVjS8i
将两个私钥导入默认钱包:
~ cleos wallet import 5JZ8LhMULSijNtaS5E8VZ4dKMwAmQpARwBk4Z4Fo2CLw7oSefqM
然后使用create account子命令来创建一个新账号mary,并依次输入上面的 两个公钥:
~$ cleos create account eosio mary
EOS7xUuBw134uoURifmSCSSScJksxok6Pp6CSCujr4AVabDGTNVit
EOS75K9RtMbjsr5WpT1md3fgeSwbYkqUF8MwgQbZtXpGWtVCVjS8i
eosio是系统合约代码托管账户,现在可以简单地理解为,在账户eosio那里存放 着用来创建账户的系统代码,因此在执行create account命令时,始终需要首先 指定该账户。
方便的脚本:
init-wallet.sh的作用是重新初始化默认钱包并导入eosio系统账号的私钥。它会自动 删除之前创建的默认钱包文件,同时也会在脚本目录下的artifacts子目录保存默认钱包 的密码,以便后续使用。该脚本也会同时将eosio账户的公钥导入默认钱包。
unlock-wallet.sh脚本的作用是使用脚本目录中预先保存的钱包密码解锁默认钱包
new-account.sh脚本的作用是创建两组私钥、导入默认钱包并创建指定名称的账号,同时 在脚本目录的artifacts子目录中保存账号的私钥和公钥,以便后续使用。
例如,下面的代码创建账号demo:
~$ new-account.sh demo
两组密钥对保存在~/repo/tools/artifacts/demo子目录中。
上述脚本采用明文保存钱包密码和私钥,因此只能用于开发环境。
EOS智能合约就是运行在EOS节点虚拟机上具有特定编程模式的应用程序
在EOS中,一个交易(Transaction)由一个或多个动作(Action)组成。 当用户使用cleos向nodeos提交一个交易后,其包含的动作经过分发,由对应 的智能合约对象负责处理。
智能合约有其特定的编程模式,一般采用状态机模型。一个智能合约通常 会包含两个核心部件:动作处理器和状态。动作处理器是合约为外部提供的 用来更新状态的接口,而合约状态只有在外部动作的激发下才会发生变化。 例如,一个账户的代币余额就可以视为一个状态,只有当发生转账交易时, 该账户的余额才会发生变化。
EOS的智能合约运行在WASM虚拟机之上,目前只支持采用C++语言开发智能合约, 不过据称有支持solidity的计划。
EOS之所以选择C++是因为它的模板 系统能保证合约更加安全,同时运行速度更快,而且通常也需要像使用C语言那样 担心内存管理问题。
编写counter.cpp EOS智能合约
首先建立一个~/repo/counter文件夹,并在该文件夹下创建文件counter.cpp:
~ touch ~/repo/counter/counter.cpp
~$ cd ~/repo/counter
在EOS中,一个智能合约类总是需要继承eosio库的contract类
contract类的_self成员用来记录合约的部署账户,因此继承类需要调用 contract类的构造函数来初始化该成员变量。
include <eosiolib/eosio.hpp>
class counter_contract:public eosio::contract {
public:
//account_name 是uint64_t类型在EOS开发包中的别名,表示部署合约
//的账户名称经过base32编码后的结果。因此在EOS中,一个账户名最长为12个字符。
counter_contract(account_name self):eosio::contract(self){}
//@abi action
void increase() {
//在cleos客户端输出显示指定的字符串
eosio::print("INCREASE => ",value++);
}
//@abi action
void decrease() {
eosio::print("DECREASE => ",value--);
}
private:
uint64_t value;
};
//合约入口
EOSIO_ABI(counter_contract, (increase)(decrease))
和其他C++程序一样,我们需要先引入eosio库的 头文件。由于EOS开发包中的大多数类型和api都定义在eosio命名空间, 因此在后续的代码中,需要使用eosio::前缀来声明其命名空间, 例如eosio::contract用来指代eosio命名空间中的contract类。
increase()方法上的@abi action注解用来告诉abi生成器,这个方法 是一个action处理函数,如果缺少该注解,生成的abi中可能不会出现该 方法的信息。
宏EOSIO_ABI是EOS开发包中用来声明合约的ABI信息的一个预定义宏, 每一个合约类都需要使用这个宏来声明其所动作处理能力。我们将在下一 节详细讲解。
EOSIO_ABI这个预定义宏的作用有两个:
引导abi生成器发现合约的abi信息
展开生成合约模块的入口调用。
EOSIO_ABI(counter_contract,(increase)(decrease))
宏调用声明了counter_contract类中定义了increase和decrease 这两个动作,abi生成器将根据这些线索继续搜索相应的动作实现 定义,例如参数列表和类型。
EOSIO_ABI宏展开后其实是一个apply()函数的实现。类似于标准C程序中 的main()函数,apply()函数是EOS中智能合约的入口函数,该函数将负责 处理分发至本合约模块的动作
从上面的代码容易看出,对应nodeos转发来的 动作,该函数将创建一个新的counter_contract实例,然后调用该实例的对应 方法。
EOSIO_ABI既是合约执行的入口,可以引导abi生成器发现合约的abi信息
nodeos每次执行合约方法,都会创建一个新的合约实例
C++编写的合约代码需要先进行编译才能在EOS虚拟机上运行。不过 和通常的C++编译目标不同,EOS的编译输出是WebAssembly格式的指令模块 —— WASM模块。可以把WebAssembly视为一种汇编语言,只是它不依赖于特定的硬件架构, 而是运行在WebAssembly虚拟机之上
wasm是二进制格式,与之对应的wast则是文本格式的,使用wast便于开发人员 编辑与调试,这两种格式之间可以互相转换。
EOS提供了一个命令行工具eosiocpp用来处理合约代码。这个工具有两个作用: 编译生成wasm/wast、提取abi信息。
使用-o或--outname选项来执行eosiocpp命令,即可生成wasm模块及wast文件。 例如,下面的代码为counter.cpp生成wast及wasm模块:
~/repo/counter$ eosiocpp -o counter.wast counter.cpp`
注意,只需要指定输出的wast文件名即可,eosiocpp会自动生成对应的wasm模块
除了编译出wasm模块,EOS合约的构建过程还包括另一个环节:抽取合约的ABI信息。
ABI(Application Binary Interface),即应用程序二进制接口, 是JSON格式的合约接口描述文件,它支持使用不同的开发语言访问智能合约。 如果需要从区块链外部访问合约,就必须利用这个描述文件。
使用-g或--genabi参数来执行eosiocpp命令,就可以生成指定合约类对应的ABI 接口文件。例如:
~/repo/counter$ eosiocpp -g counter.abi counter.cpp`
一旦我们生成了合约的wasm/wast文件和abi文件,就可以部署到指定的账户了 —— 在EOS中,合约只有部署到账户才可以使用,而且一个账户最多只能部署一个合约, 当你将一个新的合约部署到一个账户,该账户之前部署的合约将不再可用。
因此,我们可以将部署了合约代码的账户称为代码托管账户,同时在EOS的文档中, 也经常使用code来表示部署了合约的账户。
在执行以下操作之前,别忘了启动nodeos和keosd!
首先我们使用方便脚本生成一个账户sodfans,你知道,它包含了密钥创建、密钥 导入、账户创建等多个环节:
~/repo/counter cleos set contract sodfans ../counter
需要指出的是,合约目录名需要与wasm/wast以及abi文件名一致,例如,在上面的 代码中我们指定合约目录名为../counter,那么set contract子命令就会在该 目录下寻找counter.wasm、counter.wast以及counter.abi。
合约的部署是由账户eosio中的代码完成的,交易包含两个动作: setcode和setabi,分别用于将合约的wasm字节码和abi信息写入授权执行的 账户sodfans
使用cleos的push action子命令向指定的合约发送动作。例如:
~/repo/counter$ cleos push action sodfans increase '[]' -p sodfans
容易理解,我们需要在push action子命令中首先指定目标合约, 这等价于指定部署该合约的账户,即sodfans;然后指定希望在 该合约上执行的动作increase;由于increase动作不需要参数, 因此使用[]表示一个空的参数清单。
所有的交易动作都需要一个执行账户的授权,使用-p选项来指定授权账户。 在上面的调用中,我们使用sodfans账户进行交易授权,这意味着该交易 将使用sodfans的私钥进行签名。
build-contract.sh脚本用来编译指定的合约CPP代码文件同时提取abi信息,并将 结果wasm/wast和abi文件保存在当前目录下的build子目录。
下面的命令执行后,将在当前目录的build/counter子目录下生成 counter.wasm、counter.wast和counter.abi文件:
~/repo/counter ls build/counter
counter.abi counter.wasm counter.wast
deploy-contrat.sh
deploy-contract.sh脚本用来部署指定的合约,参数为部署账户和合约目录。 例如,下面的命令将build/counter目录下的合约代码及abi部署到账户sodfans:
~/repo/counter$ deploy-contract.sh sodfans build/counter
以下合约代码,通过eosio::require_auth(actor);校验提交动作时输入的为一个真实的账号;通过print(eosio::name{actor}," INCREASE => ",value++)来显示的打印出是谁提交的动作。
void increase(account_name actor){
eosio::require_auth(actor);
eosio::print(eosio::name{actor}," INCREASE => ",value++);
}
我们的计数器合约的另一个瑕疵,是它其实计不了数!
无论你提交多少次increase动作,你会看到cleos终端显示的计数值始终为0。 这是因为我们试图使用对象成员变量value来记录之前的值,但是,每次 处理nodeos分发过来的动作时,合约模块总是会重新实例化一个新的合约对象!
所以我们需要将计数器的值持久化到区块链上。
由于EOS合约运行在WASM虚拟机环境中,因此在EOS合约中没有办法使用你习 惯的文件系统 —— 例如fstream —— 来实现状态的持久化。唯一的 办法是使用EOS提供的区块链数据库 —— 多索引表(multi-index table)。
为了利用多索引表持久化,我们需要对increase()方法的实现做 简单地调整:首先需要从数据表中读取当前值,然后递增,最后用新的值更新数据表。
和以太坊不同,在EOS中要发行代币并不需要编写自己的合约,只需要使 用系统的eosio.token合约就可以了
代币合约提供了三个方法:create、issue和transfer分别用于代币 的创建、发行与转账。这几个方法的实质就是操作两张多索引表:stat和accounts。
例如,当提交create动作创建一个代币时,就是在stat表中注册 代币信息;当提交issue动作发行一个代币时,就是更新stat表中该代币 的供给量,同时在accounts表中登记发行人持有的代币总量;而提交 transfer动作执行转账时,则是更新转出和转入账户的accounts表数据。
当一个账户希望在EOS上发行自己的代币时,首先需要由代币合约的托管账户 在系统中登记拟发行代币的信息,例如代币符号、最大发行量和授权发行人账户 等信息。这一申请流程是在EOS系统外完成的。
一旦合约托管账户,例如eosio.token完成了代币创建操作,登记过的授权 发行人账户,例如happy.com就可以在最大发行量范围之内,向特定的账户 ,例如tommy发行一定数量的代币。
任何账户都可以向其他账户转让其持有的代币,也可以查询账户的代币余额。
在接下来的课程中,我们将参照上图实现HAPY代币的整个流转过程。 因此首先使用方便脚本创建相应的账号:
~ new-account.sh happy.com
~ new-account jerry
由于是我们自己的测试链,首先需要动手部署eosio.token系统合约:
~$ cleos set contract eosio.token ~/eos/build/contracts/eosio.token
现在,账户eosio.token已经部署了系统合约eosio.token,希望你能 分清这两个eosio.token的不同指代
发行自己代币的第一步,是由代币合约托管账户向合约提交create动作来注册 代币信息,例如发行人账户、最大发行量和代币符号。
例如,下面的命令将注册一条新的代币信息,发行账户为happy.com,最大发行量 为100万,代币符号为HAPY:
~$ cleos push action eosio.token create '["happy.com","1000000.00 HAPY"]' -p eosio.token
需要再一次强调的是,只有系统代币合约的部署账户,也就是eosio.token 才有权限执行create动作来注册一个新的代币信息。
当代币信息登记成功后,我们可以查询stat表
cleos get table eosio.token HAPY stat
注意stat表是以代币符号作为数据域来分隔不同代币的记录。
一旦合约账户注册了代币信息,发行人就可以进行代币发行了。
例如,下面的命令向账户tommy发行100枚HAPY币:
~$ cleos push action eosio.token issue '["tommy","100.00 HAPY"]' -p happy.com
注意该交易是由发行人账户happy.com授权的,只有在创建代币时登记 的发行人账户,才可以执行发行动作。
现在查看stat表的内容,你会发现supply已经是100.00 HAPY了:
~$ cleos get table eosio.token HAPY stat
{
"rows": [{
"supply": "100.00 HAPY",
"max_supply": "1000000.00 HAPY",
"issuer": "happy.com"
}]
}
发行代币动作更新的另一个表是accounts,利用这个表可以查看指定账户的余额
cleos get table eosio.token tommy accounts
也可以使用cleos的get currency balance子命令查看指定代币合约上指定账户 的余额,例如:
~$ cleos get currency balance eosio.token tommy
100.00 HAPY
容易理解,这个命令使用的就是accounts表中的数据。
一个账户可以将其持有的代币转给其他账户。
例如,tommy利用下面的命令向jerry转了2个HAPY币:
cleos push action eosio.token transfer '["tommy","jerry","2.00 HAPY"]' -p tommy
同样需要指出,提交转账交易必须获得转出账户(tommy)的授权。
转账的另一种方法是使用cleos封装过的transfer子命令,例如, 下面的命令同样实现了tommy向jerry转2个HAPY代币:
~ cleos get currency balance eosio.token tommy
98.00 HAPY
~$ cleos get currency balance eosio.token jerry
2.00 HAPY
一切都如预期。
在前面的课程中,我们使用客户端工具cleos与EOS智能合约交互, 这很方便,但是如果期望在我们自己的代码增加EOS智能合约的访问 功能,cleos就不够了,我们需要挖的稍微再深入一层。
前面提到过,cleos其实只是一个和用户交互的壳,绝大多数的任务 其实是在nodeos和keosd上完成的,cleos是通过访问这两个 服务器所提供的HTTP RPC API来实现具体的功能。
我们也需要这么做:跳过cleos,在代码里直接访问EOS节点旳RPC接口
例如,cleos的get info子命令可以获取区块链的统计信息:
~ curl http://127.0.0.1:8888/v1/chain/get_info -s | jq
jq是一个命令行json解析器,我们使用它来更友好的显示nodeos的响应结果。
EOS的RPC API分为几个不同的系列,例如chain/系列的API用于操作区块链, 而wallet/系列的API则用户钱包操作
nodeos默认是不加载任何API插件的,因此应当根据需要在其配置文件中加载 相应的插件,例如,在nodeos配置文件中开启所有API插件:
plugin = chain-api-plugin
plugin = history-api-plugin
plugin = net-api-plugin
plugin = producer-api-plugin
plugin = wallet-api-plugin
一旦nodeos启用了wallet-api-plugin,它也可以管理钱包了 —— 这意味着你可以 使用单一的nodeos来完成nodeos+keosd的工作,不需要开启额外的keosd服务。
但是由于cleos默认总是连接8888端口的nodeos和8900端口的keosd,因此通常 我们还是会使用单独的在8900端口监听的keosd服务进行钱包管理,以避免在命令行 显式指定钱包服务器的地址。
集成钱包服务的nodeos与独立的keosd,除了默认监听端口的区别,另一个差异就是 它们使用不同的目录来保存钱包文件,因此在keosd中创建的钱包,默认情况下在 nodeos的钱包服务中是找不到的。
keosd默认总是加载wallet-api-plugin,因此无须额外的配置来启用API。
使用EOS节点旳RPC API,基本上可以在你的程序中完成cleos能做的所有的任务。 在前面里我们用cleos命令行实现了代币的转账功能。
整个代币转账流程涉及到nodeos和keosd的多个RPC API,除了对交易进行签名是利用keosd完成的,其他的调用都是提交给nodeos的。
假设tommy要转给jerry一些HAPY代币,我们首先要做的是利用chain/abi_json_to_bin 调用将这个动作进行序列化,以便签名:
~$ curl http://127.0.0.1:8888/v1/chain/abi_json_to_bin -X POST -d '{
"code":"eosio.token",
"action":"transfer",
"args":{
"from": "tommy",
"to": "jerry",
"quantity":"2.00 HAPY",
"memo":"take care"
}}'
得到一个二进制的字符串binargs,在sign_transaction和push_transaction中作为 data 请求参数:
"binargs":"00000000002f25cd00000000...4150590000000974616b652063617265"
接下来我们需要利用chain/get_info和chain/get_block这两个调用查询链ID和头块信息
~ curl http://127.0.0.1:8888/v1/chain/get-block -X POST -s -d '{
"block_num_or_id": 3318
}' | jq
在这个响应中包含了我们感兴趣的两个信息 —— 时间戳timestamp和参考块前缀ref_block_prefix:
"timestamp": "2018-07-18T00:38:40.000"
...
"block_num": 3318
"ref_block_prefix": 1666290079
收集到上面信息之后,我们就可以对转账交易进行签名了:
~ curl http://127.0.0.1:8888/v1/chain/push_transaction -X POST -s -d '{
"compression": "none",
"transaction": {...} ,
"signatures": ["SIG_K1_KiZkwynkcgbN...aTD26todc4fRTn357328z1xTgVh1yHZk3o"]
}' | jq
响应是我们的交易收据:
"transaction_id": "6b4331de05f413b499f4050557e56a503800974daa5e1682c8294dc5c164f96c",
....
现在,你可以检查tommy和jerry的代币余额了。
你可以在~/repo/chapter6/rpc-transfer.sh中查看上述转账流程的bash脚本参考实现代码
深入理解EOS的内部机制,RPC API提供了很好的切入点。但是, 从前面的转账流程实现来看,对于应用开发而言,直接使用它实在是效率太低了。
在大多数情况下,我们应该使用更高效一点的封装开发库,例如eosjs —— EOS官方的针对JavaScript的RPC API封装库, 可以用于Nodejs环境和浏览器环境。eosjs封装了chain/和history/系列的API,同时可以根据abi信息 自动为智能合约生成封装对象,对于应用开发人员来讲,eosjs要比直接使用RPC高效多了。
首先引入eosjs包,然后创建一个实例:
const Eos = require('eosjs')
const nodeos = Eos({
httpEndpoint: 'http://localhost:8888',
keyProvider: ['5KJ9cYKZJWsF2Q7u6HrARN5NiXXTUJmoSRVGP13jT2isfQT26ru']
})
在创建Eos实例时,需要指定一个配置对象,其中的httpEndpoint声明nodeos 的监听地址,keyProvider提供一组用来签名交易的私钥 —— 我们将实现从tommy 到jerry的转账,因此这里只需要提供tommy的私钥。
一旦创建了Eos实例,就可以使用其contract()方法构建一个对应于合约 的js封装对象,该对象具有与合约动作同名的方法:
nodeos.contract('eosio.token')
.then( contract => contract.transfer('tommy','jerry','2.00 HAPY',{authorization:['tommy']}))
.then( rsp => console.log(rsp.transaction_id))
.catch( err => console.log(err))
容易注意到在调用封装合约对象的transfer()方法时,最后一个参数对象使用 authorization声明了授权本次交易的账户,该账户的私钥必须出现在我们创建 Eos实例时提供的keyProvider列表中。
eosjs很容易使用,但是在前一节的代码中,有没有让你担心的问题?
在代码中包含私钥安全吗?
的确不安全,尤其当你希望在浏览器中使用eosjs时,更加不安全。
前一节的代码我们应该视为使用eosjs操作区块链的概念验证(Proof of Concept) 代码,而不应在生产环境中使用。事实上,eosjs预留了相应扩展接口: 使用自定义的签名提供器
当我们在实例化Eos对象时,如果是使用keyProvider提供的私钥,那么eosjs 将创建一个默认的签名提供器,该提供器将根据具体交易的需求,使用这些私钥 进行本地离线签名 —— 也就是说eosjs默认不需要使用钱包服务器keosd,所以它 需要你提供发起交易的账户的私钥。
但是我们可以在创建Eos实例对象时,指定一个自定义的签名提供器来 避免泄漏私钥,例如:
const PRIVATE_KEY = '...'
const nodeos = Eos({
httpEndpoint: 'http://localhost:8888',
signProvider: function({transaction,buf,sign}){
//should return a signature
return sign(buf,PRIVATE_KEY)
}
})
签名提供器是一个函数,eosjs会传入一个参数对象,其中 transaction是要签名的交易,buf是序列化后的交易,sign是 默认的签名函数。签名提供器应当根据这些信息来返回交易的签名, 例如,上面的代码中使用默认的签名函数,用指定的私钥进行签名。
如果在浏览器环境中使用eosjs,一种解决方案是使用eos的scatter钱包,这 是一个浏览器插件,类似于以太坊的metamask钱包。scatter钱包会在浏览器 的本地存储中加密保存你的私钥,并在你访问任何网址时向浏览器注入一个scatter对象。 使用该注入对象创建的eos实例,将由scatter接管交易签名过程
const network = {
protocol:'http', // Defaults to https
blockchain:'eos',
host:'127.0.0.1', // ( or null if endorsed chainId )
port:8888, // ( or null if defaulting to 80 )
chainId:1 || 'abcd',
}
const eosOptions = {};
const eos = scatter.eos( network, Eos, eosOptions, 'https' );
scatter的签名过程和eosjs的默认签名过程一样,都是使用私钥离线签名, 只是scatter加密保存了私钥。
另一种解决方案是和cleos一样,使用keosd来进行签名
根据前面的描述,我们只需要在签名提供器中调用keosd的wallet/sign_transaction 接口并返回得到签名即可,由于不需要buf和sign,我们只提取参数中的transaction:
const keosdSigner = function({transaction}){}
const nodeos = Eos({
httpEndpoint: 'http://127.0.0.1:8888',
signProvider: keosdSigner
})
由于eosjs没有封装wallet/*系列的接口,我们需要费点事,先做这个工作:
const apiGen = require('eosjs-api/lib/apigen')
const apiDefs = {
"wallet": {
"list_wallets": {
"params": null,
"results": "string[]"
},
"sign_transaction":{
"params": "array",
"results": "signed_transaction"
}
}
}
const WalletApi = function(config) {
return apiGen('v1', apiDefs, config)
}
上面的代码基于eosjs-api的代码,对wallet/*中的部分接口进行封装。 abiGen根据所提供的api定义生成对应的方法调用,其中方法名会转化为 camelCase。例如,我们可以这样调用wallet/list_wallets接口:
const keosd = WalletApi({
httpEndpoint: 'http://127.0.0.1:8900'
})
keosd.listWallets({}).then(rsp => console.log(rsp))
NEAT.
接下来keosdSigner的实现,只需要将传入的transaction对象推给keosd, 然后提取返回结果中的签名:
const PUBLIC_KEY = '...'
const CHAIN_ID = '...'
const keosdSigner = function({transaction}){
const payload = [
transaction,
[PUBLIC_KEY],
CHAIN_ID
]
return keosd.signTransaction(payload)
.then( rsp => rsp.signatures[0])
}
在调用wallet/sign_transaction接口时,除了要签名的交易,另外两个 信息也至关重要:
交易中授权账户的公钥,使用PUBLIC_KEY给出。例如,tommy给jerry转代币, 那么我们就需要tommy的公钥
区块链ID,可以从chain/get_info调用返回结果中提取,对于测试链来讲,这个 值是cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f
现在,我们可以进行转账了:
nodeos.contract('eosio.token')
.then( contract => contract.transfer('tommy','jerry','2.00 HAPY',{authorization:['tommy']}) )
.then( rsp => console.log(rsp) )
.catch( err => console.log(err) )
开发一个基于EOS的去中心化应用 —— 便签DApp, 来实现日常任务事项的管理,提高工作效率
在这个项目的开发中,我们将综合利用前面学到的知识来开发 一个用于管理待办事宜的智能合约,以及提供给最终用户的 基于React开发的前端网页操作界面。
整个应用本质上是去中心化的,在开发过程中我们使用一个 web服务器来提供网页,但完全可以不使用web服务器而将网页部署为本地文件
按照以下步骤来完成这个项目:
需求分析:分析便签DApp的核心功能需求
用户界面设计:采用组件化思路设计便签DApp的用户界面
数据结构设计:设计便签DApp所需要的链上存储结构
智能合约代码实现:编写便签智能合约
前端代码实现:编写前端具体实现代码
运行调试:检验最终的成果
让我们从用户的角度思考一下,便签DApp的使用方法:
添加待办事项、关闭待办事项、删除待办事项、查看待办事项清单
将用户界面分割为几个不同的React组件:
TodoBox组件处于最外层,将显示标题和统计数据,并作为TodoList组件 和TodoEditor组件的容器。
TodoList组件是TodoBox的子组件,它同时也是一个简单的列表项容器, 其成员为TodoItem组件。
TodoEditor组件是TodoBox的另一个子组件,它负责采集用户的输入。
React是状态驱动的组件化界面库,因此我们还需要设计视图状态:
我们可以使用一个数组tasks来保存所有的待办事宜,每一个待办事宜 都对应一个TodoItem组件;同时我们使用一个布尔变量loading来表示 是否正在操作区块链,当loading被置位时,在TodoBox底部的状态栏 我们将显示额外的文字来提醒用户目前正在操作区块链。
对于一个待办事宜,只需要两个字段就可以表示了:描述文本和是否完成 的标志,分别使用一个字符串和一个布尔变量即可。
下面是一个示例状态树,内容恰好对应上面的示例界面:
state = {
tasks: [
{desc:'搭建EOS开发环境',done:false},
{desc:'发行HAPY代币',done:false},
{desc:'成功ICO',done:false}
],
loading: false
}
容易理解,当我们点击TodoEditor中的保存按钮,或者点击TodoItem中的 删除按钮,都会触发对状态树的修改,进而重新驱动视图的更新。
在大多数情况下,React组件都不应该持有自己的状态,而是尽可能使用 外部传入的属性来调整自己的行为。这可以让组件更轻量,嵌套更方便, 也更便于调试与跟踪。
因此我们将实现一个单独的状态树类来管理便签应用的状态:
除了两个状态节点loading和tasks,一个TodoStore类还将实现 修改状态的方法,例如:
createTask():创建待办事宜
deleteTask():删除待办事宜
toggleTask():切换待办事宜完成状态
setLoading():设置加载状态
考虑到可能需要从网络加载初识状态,因此我们为TodoStore增加一个 额外的方法initState()来初始化状态,而不是在构造函数中完成。
为了应用状态树,我们需要将其与一个顶层React组件关联起来,以便利用其 setState()方法来驱动整个视图的重绘,私有成员_host用来保存这个 关联的React组件对象。
class TodoMemStore{
constructor(host){
this._host = host
this.loading = false,
this.tasks = []
this.initState = () => {}
this.toggleTask = id => {
let idx = this._find(id)
if(idx <0) return
this.tasks[idx].done = ! this.tasks[idx].done
this._host.setState({tasks:this.tasks})
}
this.removeTask = id => {
let idx = this._find(id)
if(idx < 0) return
this.tasks.splice(idx,1)
this._host.setState({tasks:this.tasks})
}
this.createTask = desc => {
let id = Date.now()
this.tasks.push({id,desc,done:false})
this._host.setState({tasks:this.tasks})
}
this._find = id => {
for(let i=0;i<this.tasks.length;i++){
if(this.tasks[i].id === id) return i
}
return -1
}
}
}
视图的状态需要传播到相关的组件,同时相关的组件也需要更新视图的状态, 我们将确定如何管理视图状态的传递与更新。
第一种方案是采用标准的逐级传递方案:
在这种方案中,状态从组件树的根节点(TodoBox)作为属性流入,然后 逐级传递到后代组件,而后代组件对状态的修改,也需要通过事件逐级上传 至根组件,由根组件最终完成。这种方案非常规范有序,但可以想像,需要不少的胶水代码来完成逐级传递 的任务。
另一种方案是采用全局上下文对象,每个组件都可以直接访问到状态树。
由于每个组件都可以访问到状态树,因此会带来更简洁的代码。在本项目的 实现中,我们将采用这种方案来传递和更新视图状态。
出于简单化考虑,在这个项目中我们将使用React内置的Context接口来 向各组件注入状态树。当然,你也可以使用redux。
Context(上下文)是软件开发中经常遇到的一个词。基本上,任何时候你想 避免繁琐的多层嵌套函数的参数传递,都可以借助于一个保有全局信息的 上下文对象。
React的Context API采用发布订阅模型,由发布者组件在顶层提供上下文对象, 其他组件使用订阅者组件接收上下文对象
首先创建一对组件:发布者/订阅者,并设置初始上下文对象为null:
const {Provider,Consumer} = React.createContext(null)
然后我们定义一个顶层组件来封装发布者,并将其上下文设置为TodoStore对象:
//TodoProvider.jsx
export default class TodoProvider extend React.Component{
constructor(props){
super(props)
this.state = new TodoMemStore(this)
}
componentDidMount(){
this.state.initState()
}
render(){
return <Provider value={this.state}>{this.props.children}</Provider>
}
}
现在可以在使用订阅者组件来将上下文注入其他组件。例如,对于TodoBox组件:
//TodoBox.jsx
class TodoBox extends React.Component {...}
export default props => (
<Consumer>
{ store => <TodoBox {...props} store={store}/>}
</Consumer>
)
现在整合到一起,渲染到DOM树:
ReactDOM.render(
<TodoProvider><TodoBox/></TodoProvider>,
document.getElementById('app'));
import React from 'react'
import TodoStore from '../services/TodoEosStore'
//import TodoStore from '../services/TodoMemStore'
export const TodoContext = React.createContext({})
export class TodoProvider extends React.Component{
constructor(props){
super(props)
this.state = new TodoStore(this)
}
componentDidMount(){
this.state.initState()
}
render(){
return (
<TodoContext.Provider value={this.state}>
{this.props.children}
</TodoContext.Provider>
)
}
}
设计便签合约的状态和动作:
便签合约的核心是状态表todos,它存储所有的待办事宜,合约的三个 方法create、remove和toggle则用于操作这个状态表。
首先我们需要一张数据表来保存每个账户的便签记录,记录的内容包括 记录序号、待办事宜文本及是否完成的标志:
//@abi table todos
struct todo{
uint64_t id;
std::string desc;
bool done;
auto primary_key() const { return id; }
EOSIO_SERIALIZE(todo,(id)(desc)(done))
}
typedef multi_index<N(todos),todo> todo_table;
注意我们在注解中声明了表名为todos,因此在声明多索引表类型时, 也要使用这个名字,即N(todos)。
然后我们需要提供三个动作供用户来操作便签数据表:创建、删除和切换状态。
create:创建待办事宜
create动作用于将一个新的待办事宜添加到数据表中:
//@abi action
void create(account_name author,uint64_t id,std::string desc){
require_auth(author);
todo_table todos(_self,author);
todos.emplace(author,[&](auto& record){
record.id = id;
record.desc = desc;
record.done = false;
});
}
新添加的待办事宜总是未完成的,因此create方法只需要两个参数:序号和描述文本。 由于我们需要分别保存每个账户的待办事宜,因此在声明数据表变量todos时,需要将其scope参数设置为提交动作的账户。
remove:删除待办事宜
remove动作用于从数据表中删除指定序号的待办事宜:
//@abi action
void remove(account_name author,uint64_t id){
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(),"not found");
todos.erase(iter);
}
remove动作处理逻辑很简单:找到指定记录,然后删除。在上面 的代码中我们使用eosio_assert()函数来确保指定序号的记录存在, 否则将抛出异常并停止继续执行。
toggle:切换待办事宜的状态
toggle动作用于切换数据表中指定序号待办事宜的完成标志:
//@abi action
void toggle(account_name author,uint64_t id){
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(),"not found");
todos.modify(iter,author,[&](auto& record){
record.done = ! record.done;
});
}
合约
include <eosiolib/eosio.hpp>
class todo_contract : public eosio::contract {
public:
todo_contract(account_name self):eosio::contract(self){}
// @abi action
void create(account_name author, const uint64_t id, const std::string& desc) {
require_auth(author);
todo_table todos(_self,author);
todos.emplace(author, [&](auto& record) {
record.id = id;
record.desc = desc;
record.done = false;
});
eosio::print("todo#", id, " created");
}
// @abi action
void remove(account_name author, const uint64_t id) {
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
todos.erase(iter);
eosio::print("todo#", id, " deleted");
}
// @abi action
void toggle(account_name author, const uint64_t id) {
require_auth(author);
todo_table todos(_self,author);
auto iter = todos.find(id);
eosio_assert(iter != todos.end(), "Todo does not exist");
todos.modify(iter, author, [&](auto& record) {
record.done = ! record.done;
});
eosio::print("todo#", id, " toggle todo state");
}
private:
// @abi table todos i64
struct todo {
uint64_t id;
std::string desc;
有了TodoMemStore的实现,有了完成的合约,现在我们可以实现基于 EOS的TodoStore了。
出于简单化考虑,我们将固定使用一个账户来操作区块链,同时使用离线 签名方式,因此使用一个变量保存这个信息:
const wallet = {
account: 'sodfans',
privateKey: '5JHZdDQ7cfHQ8PnoNqvW9kWbdsCZDTUiBBs3YLxowJEbjCKfvET'
}
在构造函数中,我们使用一个额外的参数options来允许调用者自定义节点url 等信息,默认情况下,使用的合约为todo.user,表为todos:
constructor(host,options){
const defaults= {
code: 'todo.user',
table: 'todos',
wallet: wallet,
nodeosUrl: NODEOS_URL ? NODEOS_URL : 'http://127.0.0.1:8888',
keosdUrl: KEOSD_URL ? KEOSD_URL : 'http://127.0.0.1:8900',
}
this._options = Object.assign({},defaults,options)
this._host = host
this._nodeos = Eos({
httpEndpoint: this._options.nodeosUrl,
keyProvider: [this._options.wallet.privateKey],
})
}
根据合并的参数,在构造函数中同时创建_nodeos成员用于后续的EOS操作。
切换待办事宜操作将执行合约的toggle动作:
this.toggleTask = id => {
const opts= {authorization:this._options.wallet.account}
return this._nodeos.contract(this._options.code)
.then( contract => contract.toggle(this._options.wallet.account,id,opts) )
}
删除待办事宜操作将执行合约的remove动作:
this.removeTask = id => {
const opts= {authorization:this._options.wallet.account}
return this._nodeos.contract(this._options.code)
.then( contract => contract.remove(this._options.wallet.account,id,opts) )
}
创建待办事宜操作将执行合约的create动作,简单地使用时间戳作为记录id:
this.createTask = desc => {
const opts= {authorization:this._options.wallet.account}
const id = Date.now()
return this._nodeos.contract(this._options.code)
.then( contract => contract.create(this._options.wallet.account,id,desc,opts) )
}
网友评论