[转]bitsharesjs 库详解一:ChainStore

本贴最后更新于 2525 天前,其中的信息可能已经事过景迁

bitshares 开发入门:开源代码总览 介绍了比特股开源代码的总体情况,其中,bitsharesjs 位于 UI 层之下, bitsharesjs-ws 之上。本文尝试开一个系列之头:这个系列全部解析 bitsharesjs 代码。

bitsharesjs 库有三个主要模块,ECC, Chain 和 Serializer。ECC 是关于椭圆曲线密码学的一些贴近钱包操作的库,Chain 是关于链上数据获取和交易发起的,Serializer 是 Chain 的工具支持,一般无需直接使用。 本文阐述 Chain 中的一个类: ChainStore。ChainStore 的功能是链上数据的获取和缓存。本文提到的代码,如无特别说明,均以 bitsharesjs 的根目录为相对目录的起点。

环境准备

  1. 安装 Nodejs 到本地,建议安装当前的 LTS 版本,本文写作时,为 6.10.3 (如果已经安装请跳过这一步)
  2. 克隆代码到本地 ( 命令行下执行:git clone https://github.com/bitshares/bitsharesjs.git )
  3. 进入 bitsharesjs 目录, npm install

注意:本系列文章依赖 bitsharesjs 的 git 版本 bdda47c2250b9b9ecf92d682849c7b5b1efae90f ,请确保一致,否则可能会造成理解偏差,尤其涉及代码行号。

从测试代码说起

测试代码文件: test/chain/ChainStore.js

测试方法,命令行键入

npm run test:chain

注意这个测试会测试 test/chain 目录下的所有测试文件, ChainStore 只是一个。如果没有本地重钱包,你会发现 ChainStore 会测试失败。下文教你如何修改代码来做测试。

背景说明:测试使用的是 mocha BDD 测试框架 ,并且(整个项目)使用了 babel 转码。

第 3 行

|

3

|

import { FetchChain, ChainStore } from "../../lib";

|

导入了 ChainStore。

第 9-15 行

|

9
10
11
12
13
14
15

|

before(function() {
/* use if no local node is available */
return Apis.instance("", true).init_promise.then(function (result) {
coreAsset = result[0].network.core_asset;
ChainStore.init();
});
});

|

所有测试用例运行之前需要做初始化:先连接上全节点,测试代码使用的是本地节点,第 10 行注释说得明白,如果没有本地节点,那么就使用公网节点,例如 openleger 的。国内测试,建议改成帝国的: 。 另外第 13 行有个 bug ,需要在前面加上 return,否则默认 return undefined,整个函数就会 resolve 掉,可能导致 ChainStore 没有初始化完成就执行测试用例,会出错的。修改后的代码应该是这个样子:

|

9
10
11
12
13
14
15

|

before(function() {
/* use if no local node is available */
return Apis.instance(", true).init_promise.then(function (result) {
coreAsset = result[0].network.core_asset;
return ChainStore.init();
});
});

|

这样就可以测试了。但是,读者会发现,测试用例不见得全部 pass。这里面有另一个 BUG,下文详解。

init 函数

当底层 Api(bitsharesjs-ws 提供的 Apis)初始化 OK 时,必须调用 ChainStore 的 init 函数初始化,正如第 13 行所做的那样。

首先, ChainStore 这个变量,容易混淆,这个是从 lib/chain/src/ChainStore.js 这个文件导入的,而这个文件定义了一个 ChainStore 类,但本身导出的确实 ChainStore 类的一个全局 Singleton

|

1352

|

let chain_store = new ChainStore();

|

1352 行生成了 ChainStore 类的一个实例。

|

1407

|

export default chain_store;

|

1407 行导出这个实例。

因此测试代码导入的 ChainStore,是 ChainStore.js 文件中定义的 ChainStore 类的一个全局实例。这句话很绕口,多读几遍。

回到 init 函数,该函数返回一个 promise,resolve 的时候初始化成功。其他函数必须在 init 函数返回 resolve 之后调用。正因为这个特点,才有了上文所述第 13 行的少 return 的 BUG。

4 个测试用例的所调用的两个函数

4 个测试用例实际上主要调用了 ChainStore(Singleton)的两个函数:

  • getAsset
  • subscribe

其中 getAsset 是 getObject 的封装,表示获取资产。而 getObject 是一般的“获取对象”函数,而“对象”是 bitshares 区块链的核心数据。对象的 id 是 3 个整数, a.b.c。其中:

  • a 表示空间,两个取值:1 表示协议对象,这些对象会在 websocket 和 p2p 网络上传输;2 表示实现对象,用于节点本地存储,可认为是共识数据的衍生数据。
  • b 表示类别,协议对象和实现对象都有十多类不同数据。
  • c 表示实例,不同类型数据的实例编号。

例如

  • 2.1.0 表示动态全局相关数据,例如一个抓取的实例:

    { participation: 100,
    recently_missed_count: 0,
    accounts_registered_this_interval: 22,
    next_maintenance_time: '2017-05-24T04:00:00',
    dynamic_flags: 0,
    witness_budget: 76200000,
    head_block_id: '0100685ba0b1d1902e8ccea5e0eac2172f679873',
    time: '2017-05-24T03:47:27',
    recent_slots_filled: '340282366920938463463374607431768211455',
    current_witness: '1.6.72',
    current_aslot: 16909777,
    head_block_number: 16803931,
    id: '2.1.0',
    last_irreversible_block_num: 16803912,
    last_budget_time: '2017-05-24T03:00:00' }

  • 1.3.x 表示各种类型的资产

  • 1.3.0 核心资产 BTS
  • 1.3.113 锚定资产 bitCNY
  • 1.2.x 表示各个账号
  • 1.2.0 理事会多重签名账号
  • 1.2.121 理事会成员巨蟹的账号 bitcrab
  • 1.2.12376 理事会成员 abit 的账号 abit
  • 1.7.x 表示用户提交的限价单
  • 1.8.x 表示 call order(我还真没搞清楚是什么意思,请留言)
  • 1.11.x 表示用户相关的活动历史,提交限价单,取消限价单,转账给别人,收到转账等等

常用对象列表 可参看大部分的对象类型。

好,回到 getObject 函数,这个函数总是立即返回的,返回值有三种情况:

  • 返回 Map 类型 的对象,表示缓存中有了这个对象
  • 返回 null,表示没有这个 Object(id 无效)
  • 返回 undefined,表示正在查询 API 节点,需要以后重新调用

getAsset 是 getObject 的封装,因此返回值同样遵守这个约定。由于 getObject 立即返回,而调用的时候如果返回 undefined,怎么等呢?用 subscribe 函数。 subscribe 函数是通用的事件监听函数, 当 websocket 连接之后,任何从 API 节点的事件,都会触发所有的监听者(subscriber)。

这个设计本身是否足够好?我认为不够好,因为 subscribe 会导致大量的无效监听,而 getObject 和 subscribe 的联合使用,从理论上说不一定能达到预期的效果: 因为监听者无法区分事件本身,而 JS 的异步特性会导致不确定性。从测试代码来说,4 个测试用例并行执行,和 webSocket 的事件触发次序的不确定性,会导致 subscribe 里面的 getAsset 函数不一定得到想要的结果。如果改写其中的一个测试用例,设成 it.only (忽略其他的测试用例),目前我的测试结果是总可以通过的,但从理论上,我仍然不相信这种单个测试用例的测试方法:万一监听到一个不相关的事件呢?从这个意义上来说,测试代码还不好写得正确,现有测试代码怎么改成逻辑自洽的还很难。

另外,就 ChainStore 来说,测试代码的覆盖也完全不够,下面看看例子代码。

例子代码

例子代码在这里: examples/chainstore.js

运行

npm run example:chainStore

可以发现一直打印 ChainStore 的全局动态对象 2.1.0 的当前值。

例子代码比测试代码简单,用到的函数是 getObject,运行例子代码会对上文提到的无效监听设计有直观的认识。

例子代码的修改

例子代码太简单了,只获得一个动态全局对象,不利于理解很多其他的概念。我通过阅读 ChainSore.js 的源代码,改了下,可以获得巨蟹的账号情况,注意其中的资产和操作历史:

import {Apis} from "bitsharesjs-ws";
import {ChainStore} from "../lib";

Apis.instance(", true).init_promise.then((res) => {
console.log("connected to:", res[0].network);
ChainStore.init().then(() => {
ChainStore.subscribe(getBitcrabAccount);
});
});

function getBitcrabAccount() {
var bitcrab = ChainStore.getAccount('bitcrab');

if( bitcrab) {
    var bitcrabObj = bitcrab.toJS();
    console.log('my account', bitcrabObj);

    var balances = bitcrabObj.balances;

    if( balances ){
        for (var assetId in balances ){
            var asset = ChainStore.getAsset(assetId);

            if( asset ){
                var assetObj = asset.toJS();
                console.log('asset:', assetId, assetObj);

                if( assetObj.dynamic_asset_data_id) {
                    var dynamicAsset = ChainStore.getObject(assetObj.dynamic_asset_data_id);

                    if( dynamicAsset ){
                        var dynamicAssetObj = dynamicAsset.toJS();
                        console.log('asset dynamic:', assetId, dynamicAssetObj);
                    }
                }
            }

            // var balance =;

            var balance = ChainStore.getObject( balances[assetId]);
            console.log('asset balance:', assetId, balance.toJS());

        }
    }

    var history = bitcrabObj.history;

    if( history ){
        history.forEach( function(h){
            console.log('history', h);
            console.log('opration', h.op);
        });

    }
}

}

运行修改的例子代码会不停的输出巨蟹的账号相关信息。关于操作历史,最重要的是什么操作?op 的数据结构是二元组,第一个数表示操作类型,第二个对象表示具体的数据。而操作类型可以在 lib/chain/src/ChainTypes.js 里面找到,代码我就不贴了。

总结

关于 ChainStore 的代码解读就这些了,这个过程我总结下来:

  • ChainStore 的接口设计不算特别合理。怎么样才更好呢?是一个值得思考的问题。
  • 业务逻辑和代码需要结合起来,比如 a.b.c 对象的意义,操作类型的意义。
  • ChainStore 测试和例子的质量不高,大体可判断,bitsharesjs 总体的代码质量有待改进,如果对质量要求高,可以考虑直接使用钱包和节点的 JSON RPC API。
  • 比特币

    比特币(BitCoin)的概念最初由中本聪在 2009 年提出,根据中本聪的思路设计发布的开源软件以及建构其上的 P2P 网络。比特币是一种 P2P 形式的数字货币。点对点的传输意味着一个去中心化的支付系统。

    27 引用 • 169 回帖 • 79 关注
  • 区块连
    3 引用
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    729 引用 • 1327 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...