bitsharesjs庫詳解一:ChainStore
bitshares開發入門:開源代碼總覽介紹了比特股開源代碼的總體情況,其中,bitsharesjs位於UI層之下,bitsharesjs-ws之上。本文嘗試開一個系列之頭:這個系列全部解析 bitsharesjs 代碼。
bitsharesjs 庫有三個主要模塊,ECC, Chain和Serializer。ECC是關於橢圓曲線密碼學的一些貼近錢包操作的庫,Chain是關於鏈上數據獲取和交易發起的,Serializer是Chain的工具支持,一般無需直接使用。 本文闡述Chain中的一個類: ChainStore。ChainStore的功能是鏈上數據的獲取和緩存。本文提到的代碼,如無特別說明,均以bitsharesjs的根目錄為相對目錄的起點。
環境準備
安裝Nodejs到本地,建議安裝當前的LTS版本,本文寫作時,為 6.10.3 (如果已經安裝請跳過這一步)
克隆代碼到本地 ( 命令行下執行:git clonehttps://github.com/bitshares/bitsharesjs.git)
進入 bitsharesjs目錄, npm install
從測試代碼說起
測試代碼文件: test/chain/ChainStore.js
測試方法,命令行鍵入
npm run test:chain
注意這個測試會測試 test/chain目錄下的所有測試文件, ChainStore只是一個。如果沒有本地重錢包,你會發現ChainStore會測試失敗。下文教你如何修改代碼來做測試。
背景說明:測試使用的是mocha BDD測試框架,並且(整個項目)使用了 babel轉碼。
第3行
import{FetchChain,ChainStore}from"../../lib";
導入了ChainStore。
第9-15行
before(function(){/* use wss://bitshares.openledger.info/ws if no
local node is available */returnApis.instance("ws://127.0.0.1:8090",true)
.init_promise.then(function(result){coreAsset=result[].network.core_asset;ChainStore.init();});});
所有測試用例運行之前需要做初始化:先連接上全節點,測試代碼使用的是本地節點,第10行注釋說得明白,如果沒有本地節點,那麼就使用公網節點,例如openleger的。國內測試,建議改成帝國的: wss://bit.btsabc.org/ws 。 另外第13行有個 bug ,需要在前面加上 return,否則默認 return undefined,整個函數就會resolve掉,可能導致ChainStore沒有初始化完成就執行測試用例,會出錯的。修改後的代碼應該是這個樣子:
before(function(){/* use wss://bitshares.openledger.info/ws if no
local node is available */returnApis.instance("wss://bit.btsabc.org/ws",true)
.init_promise.then(function(result){coreAsset=result[].network.core_asset;returnChainStore.init();});});
這樣就可以測試了。但是,讀者會發現,測試用例不見得全部pass。這裡面有另一個BUG,下文詳解。
init函數
當底層Api(bitsharesjs-ws提供的Apis)初始化OK時,必須調用ChainStore的init函數初始化,正如第13行所做的那樣。
首先, ChainStore這個變數,容易混淆,這個是從 lib/chain/src/ChainStore.js這個文件導入的,而這個文件定義了一個ChainStore類,但本身導出的確實ChainStore類的一個全局Singleton
letchain_store=newChainStore();
1352行生成了ChainStore類的一個實例。
exportdefaultchain_store;
1407行導出這個實例。
因此測試代碼導入的ChainStore,是ChainStore.js文件中定義的ChainStore類的一個全局實例。這句話很繞口,多讀幾遍。
回到init函數,該函數返回一個promise,resolve的時候初始化成功。其他函數必須在init函數返回resolve之後調用。正因為這個特點,才有了上文所述第13行的少return的BUG。
4個測試用例的所調用的兩個函數
4個測試用例實際上主要調用了ChainStore(Singleton)的兩個函數:
getAsset
subscribe
a表示空間,兩個取值:1表示協議對象,這些對象會在websocket和p2p網路上傳輸;2表示實現對象,用於節點本地存儲,可認為是共識數據的衍生數據。
b表示類別,協議對象和實現對象都有十多類不同數據。
c表示實例,不同類型數據的實例編號。
例如
2.1.0 表示動態全局相關數據,抓取的實例請看原文
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,運行例子代碼會對上文提到的無效監聽設計有直觀的認識。
例子代碼的修改
運行修改的例子代碼會不停的輸出巨蟹的賬號相關信息。關於操作歷史,最重要的是什麼操作?op的數據結構是二元組,第一個數表示操作類型,第二個對象表示具體的數據。而操作類型可以在 lib/chain/src/ChainTypes.js 裡面找到,代碼我就不貼了。
總結
關於ChainStore的代碼解讀就這些了,這個過程我總結下來:
ChainStore的介面設計不算特別合理。怎麼樣才更好呢?是一個值得思考的問題。
業務邏輯和代碼需要結合起來,比如a.b.c對象的意義,操作類型的意義。
ChainStore測試和例子的質量不高,大體可判斷,bitsharesjs總體的代碼質量有待改進,如果對質量要求高,可以考慮直接使用錢包和節點的 JSON RPC API。


TAG:跟我一起學區塊鏈開發 |