美文网首页
2020-10-10

2020-10-10

作者: 逝者如斯夫不舍昼夜L | 来源:发表于2020-10-10 19:44 被阅读0次

    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.

    如果你需要保留之前的数据,可以清除节点中的可逆块,然后重放 交易来重新生成:
    ~rm -rf ~/.local/share/eosio/nodeos/data/blocks/reversible ~ nodeos --replay-blockchain

    当然,你也可以完全删除整个数据目录,如果不再需要其中的账户和交易:
    ~rm -rf ~/.local/share/eosio/nodeos/data ~ 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 lock_all 此时使用wallet list子命令查看钱包状态,代表解锁标志的* 符号已经不见了 PW5KUFLj4hMpo6boyRja1xpGChNXTsz7a7rkJsSQiWG3BPxrJaVwe 解锁默认钱包 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 5Jmw8TexQMCqderdzNBjAVYn4Tj7z9c8aBTEYou3DBrCumwnpB8 ~ 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:
    ~mkdir -p ~/repo/counter ~ 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/counternew-account.sh sodfans 然后,使用cleos的set contract子命令将counter合约部署到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/counterbuild-contract.sh counter.cpp ~/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 eosio.token ~ new-account.sh happy.com
    ~new-account.sh tommy ~ 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 transfer tommy jerry '2.00 HAPY' 转账完成后,使用get currency balance命令查看双方余额: ~ 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子命令可以获取区块链的统计信息:
    ~cleos get info 这实际上是向nodeos发送一个chain/get_info的请求,我们可以使用curl 来完成同样的任务。例如,下面的命令向nodeos服务器发送上述请求: ~ 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-info | jq 在这个响应中包含了我们感兴趣的chain_id和head_block_num: ... "chain_id": "cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f" "head_block_num": 3318 "last_irreversible_block_num": 3317 ... 然后利用chain/get_block调用获取头块信息: ~ 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://localhost:8900/v1/wallet/sign_transaction -X POST -s -d '[ { "ref_block_num": 3318, "ref_block_prefix": 1666290079 , "expiration": "2018-07-18T01:38:40.000", "actions": [ { "account": "eosio.token", "name": "transfer", "authorization": [{ "actor": "tommy", "permission": "active" }], "data": "00000000002f25cd0000000000...0002484150590000000974616b652063617265" } ], "signatures": [] }, ["EOS7nLhBxUftxxtnu8nmn8djRFs43KDGD9TPHFNWTo7Py7XJYK26S"], "cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f" ]' |jq chain/sign_transaction的载荷是一个数组,第一项是交易对象,第二项是tommy 的公钥 —— 注意在你的系统中这个公钥不会一样的,需要替换。第三项是我们在之前获得 的chain_id —— 这是非常重要的信息,如果遗漏,你得到的签名将在提交交易时被nodeos拒绝。 另一个需要提醒的是,这个请求是发送到keosd的,因此访问端口为8900。 我们将得到如下的签名交易,注意其中的signatures项: ... "signatures": ["SIG_K1_KiZkwynkcgbNrohvMJD89GBv3WudPXpxiTWxUDc9RQ4VEYxQoDAzg8f...3o"] ... 最后我们使用chain/push_transaction来发送签名交易,该调用的载荷包括三部分: 压缩格式、前面得到的签名交易、前面得到的签名: ~ 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) )
    }

    相关文章

      网友评论

          本文标题:2020-10-10

          本文链接:https://www.haomeiwen.com/subject/nirdpktx.html