Hyperledger Fabric V1.4(二)使用JavaScript语言进行链码开发
Ⅰ 使用 Visual Studio Code 扩展来开发智能合同
原本已经搭建好Fabric V2.0的环境,并希望使用Java语言进行链码、SDK以及web应用的开发。但后来发现V2.0处于Beta(β)测试阶段,链码及SDK部分没有相应版本的示例作为参考,自己曾尝试编写java版本链码向first-network网络中进行部署,也尝试了samples目录下的其他示例中的java链码,全都报错说事务失败,尝试解决未果。因此,便考虑更换链码语言,由于后续要做web应用,离不了JS,干脆直接换为JS进行全栈开发。无意间在IBM官网看到其针对VS Code开发的扩展,叫做IBM Blockchain Platform,通过该插件可以进行快速、便捷的链码开发(多种语言)。其功能主要包括:
① 创建链码项目
② 在本地启动一个最简化的Fabric小网络(一个ca,一个order,一个peer,一个couchDB),以供进行合约的部署与调试
③ 通过可视化的操作,进行链码的打包、安装与实例化
④ 点击要测试的函数,提供相应的参数,便可提交事务,对链码的逻辑进行测试
⑤ 其最强大的功能在于其调试功能,链码修改后,不需要重新打包、安装与实例化,点击调试便可再次进行测试
关于其具体使用教程,请参考https://cloud.ibm.com/docs/services/blockchain/howto?topic=blockchain-develop-vscode,在此不在赘述。以下为插件安装与使用过程中的一些注意事项,请关注:
① Fabric兼容性问题,该插件仅兼容Fabric1.4+版本,不兼容最新的2.0版本,因此本人已回退到Fabric1.4.6(2.0版本最大的变化是链码的生命周期)
② VS Code兼容性问题,目前插件最新版本为1.0.23,由于grpc的原因,导致ibm-blockchain-platform-1.0.23与VS Code1.40+版本不兼容,请下载1.38或1.39
③ 启动调试功能后,一定要手动点击FABRIC GATAWAYS窗口的刷新按钮以刷新合约内容,否则运行结果将不会发生变化。
④ 直接从VS Code的扩展功能模块搜索IBM Blockchain Platform安装的版本过低,安装后点击升级也不成功。可从https://marketplace.visualstudio.com/搜索IBM Blockchain Platform,下载ibm-blockchain-platform-1.0.23.vsix,然后在VS Code中通过vsix进行安装。
Ⅱ 以下为一个基础的合约示例,仅用于参考代码结构:
注:本合约要往状态数据库存储的资产对象为‘电子数据’,一下代码仅展示了最基本的状态的添加与查询,更多API请参考剩余内容。
const { Contract } = require('fabric-contract-api'); //引入依赖
class ElecDataContract extends Contract { //ElecDataContract 为自定义的合约类
//自定义事务一,查询由elecDataId标识的电子数据是否已存在
async elecDataExists(ctx, elecDataId) {
const buffer = await ctx.stub.getState(elecDataId); //调用ctx.stub.getState()方法
return (!!buffer && buffer.length > 0);
}
//自定义事务二,创建‘电子数据’对象,并将其存储到状态数据库
async createElecData(ctx, name, format, size, hash, ownerId) {
var elecDataId = utils.randomString(); //自己编写的工具函数,使用一个32位的随机字符串作为电子数据编号,该编号也作为状态数据库中存储的对象的键值
//检查电子数据编号是否已存在,避免使用相同的编号作为Key而覆盖原有值
const exists = await this.elecDataExists(ctx, elecDataId);
if (exists) {
throw new Error(`要添加的电子数据已存在!`);
}
const timestamp = utils.getCurrentTimestamp(); //自己编写的工具函数,获得当前时间戳
const elecData = { //构建电子数据对象
elecDataId,
name,
format,
size,
hash,
timestamp,
ownerId
};
await ctx.stub.putState(elecDataId, Buffer.from(JSON.stringify(elecData))); //调用putState()API,将以数据编号为标识的电子数据对象写入状态数据库
}
//自定义事务三,通过数据编号,检索对应的电子数据
async readElecDataByElecDataId(ctx, elecDataId) { //由电子数据编号elecDataId查询电子数据,用于应用的校验电子数据功能
//检查电子数据是否存在
const exists = await this.elecDataExists(ctx, elecDataId);
if (!exists) {
throw new Error(`The elec data ${elecDataId} does not exist`);
}
const buffer = await ctx.stub.getState(elecDataId); //从状态数据库查询
const elecData = JSON.parse(buffer.toString());
return elecData; //返回查询结果,此处无论返回结果是Json还是Buffer,SDK拿到的数据均为一个Buffer对象
}
}
Ⅲ ChaincodeStub类封装了链码和peer节点之间交互的APIs,使用方式为ctx.stub.method(),要查看更详细的说明,请访问https://hyperledger.github.io/fabric-chaincode-node/release-1.4/api/,现总结常用API如下:
① <async> getState(key) //参数类型string,返回类型buffer
功能:检索状态数据库中键的当前值
② <async> putState(key, value) //参数类型string,buffer或string
功能:向状态数据库写入新的键值对,如果变量已经存在,则该值将被覆盖
③ <async>deleteState(key) //参数类型string
功能:根据key从状态数据库中删除键值对
④ <async> getStateByRange(startKey, endKey) //参数类型string,string,返回类型StateQueryIterator迭代器(迭代访问后需要调用其close()函数)
功能:在账本中的一组键上返回一个范围迭代器。迭代器可用于迭代startKey(包含)和endKey(排除)之间的所有键。startKey和endKey可以是空字符串,这意味着开始或结束的查询范围是不受限制的
⑤ <async> getHistoryForKey(key) //参数类型string,返回类型HistoryQueryIterator迭代器
功能:返回键值随时间变化的历史记录。对于每个历史键更新,将返回历史值和关联的事务id和时间戳。
⑥ createCompositeKey(objectType, attributes) //参数类型string,string数组,返回类型string
功能:通过组合objectType字符串和给定的“属性”来创建组合键,objectType和属性应该是有效的utf8字符串,得到的组合键可以用作putState()中的键
解析:Hyperledger Fabric使用一个简单的键/值模型来保存链码状态。在某些用例场景中,有必要跟踪多个属性。此外,可能需要使各种属性可搜索。可以使用组合键来满足这些需求。类似于在关系数据库表中使用组合键,这里可以将可搜索属性视为组成组合键的键列。属性的值成为键的一部分,因此可以通过getStateByRange()和getStateByPartialCompositeKey()等函数来搜索它们。
示例(创建资产的事务):
marbleName = ‘marble1’
let marble = { //构造一个marble对象
name: marbleName,
color: ‘blue’,
owner: ‘wang’,
size: 50
};
//首先将marble对象添加到由marbleName标识的状态数据库
await stub.putState(marbleName, Buffer.from(JSON.stringify(marble)));
//然后生成color和owner的组合键,以支持基于这些属性的范围查询,如返回所有蓝色marble
let indexName = 'color~name';
let colorNameIndexKey = await stub.createCompositeKey(indexName, [marble.color, marble.name]);
//将组合键保存到状态数据库,组合键作为key,而value在此置为null(nil)(value为空时,删除效率高)
await stub.putState(colorNameIndexKey, Buffer.from('\u0000'));
注:删除状态数据库中某个键值对时,除了调用deleteState(marbleName)函数删除marble,同样需要删除marble对应的组合键
let indexName = 'color~name';
let colorNameIndexKey = await stub.createCompositeKey(indexName, [marble.color, marble.name]);
await stub.deleteState(colorNameIndexKey);
⑦ <async> getStateByPartialCompositeKey(objectType, attributes) //参数类型string,string数组,返回类型StateQueryIterator迭代器(迭代访问后需要调用其close()函数)
功能:根据给定的部分组合键查询分类账中的状态。
示例:查询color为blue的所有marble
let coloredMarbleResultsIterator = await stub.getStateByPartialCompositeKey('color~name', [“blue”]);
⑧ splitCompositeKey(compositeKey) //参数类型string,返回类型是一个对象,该对象包括两个属性,objectType与attributes,objectType为string,attributes为字符数组
功能:将组合键拆分为组成复合键的属性。在范围查询或部分组合键查询中得到的组合键可以被分割成它们原来的组合部分,该函数本质上是恢复属性的值。
示例:
//查询所有color为‘blue’的marble,查到的结果为符合键作为键的键值对(value为空值),而不是以marbleName为标识的那些键值对,因此需要从符合键中拆分出marbleName属性,并通过getState(marbleName)获得符合最初查询条件的marble对象,并进行后续其他处理。
let coloredMarbleResultsIterator = await stub.getStateByPartialCompositeKey('color~name', [“blue”]);
//迭代
while (true) {
let responseRange = await coloredMarbleResultsIterator.next(); //迭代器元素的value属性为键值对,responseRange.value.key为组合键
let objectType;
let attributes;
({ //拆解组合键
objectType,
attributes
} = await stub.splitCompositeKey(responseRange.value.key));
let returnedColor = attributes[0];
let returnedMarbleName = attributes[1]; //然后可通过getState(marbleName)获得符合最初查询条件的marble对象,并进行后续其他处理。
}
⑨ <async> getQueryResult(query) //参数类型string,返回类型StateQueryIterator迭代器
功能:执行“富”查询。query为底层状态数据库(Couchdb)的本地查询字符串(json格式)
示例:查询指定owner的所有marble
let queryString = {};
queryString.selector = {};
queryString.selector.owner = owner;
let resultsIterator = await stub.getQueryResult(JSON.stringify(queryString));
⑩ 其他
getChannelID() //返回chaincode要处理的事务的通道ID
getCreator () //返回chaincode调用提交者的标识对象
getTxID() //返回当前chaincode调用请求的事务ID。事务ID惟一地标识通道范围内的事务。
getTxTimestamp() //返回创建事务时的时间戳(客户端发起请求的时间)。
注:未提到的函数包括对私有数据的操作(所有的get及put均有面向私有数据的函数)以及分页操作(所有的get查询均有对应的分页查询)
四 由于存在多个查询(get)函数返回结果为迭代器,因此需要一个自定义函数对迭代器中的查询结果进行遍历:
//自定义函数对迭代器中的查询结果进行遍历,根据是否查询键值历史,调用该方法的方式为getAllResults(iterator, true)或getAllResults(iterator, false)
//可以迭代由getStateByRange(startKey, endKey)、getQueryResult(query)、getHistoryForKey(key)返回的迭代器
async getAllResults(iterator, isHistory) {
let allResults = []; //空数组
while (true) { //开始迭代
let res = await iterator.next(); //获取元素
if (res.value && res.value.value.toString()) {
let jsonRes = {}; //将每个元素都构造为json
console.log(res.value.value.toString('utf8'));
if (isHistory && isHistory === true) { //若进行的是键值历史查询
jsonRes.TxId = res.value.tx_id; //获得历史值变更的事务ID
jsonRes.Timestamp = res.value.timestamp; //获得历史值变更的事务时间戳
jsonRes.IsDelete = res.value.is_delete.toString(); //是否为删除操作
try {
jsonRes.Record = JSON.parse(res.value.value.toString('utf8')); //获得历史键值在状态数据库的value
} catch (err) {
console.log(err);
jsonRes.Record = res.value.value.toString('utf8');
}
} else { //若是其他查询结果
jsonRes.Key = res.value.key; //获得键值
try {
jsonRes.Value = JSON.parse(res.value.value.toString('utf8')); //获得value
} catch (err) {
console.log(err);
jsonRes.Value = res.value.value.toString('utf8');
}
}
allResults.push(jsonRes); //放入数组
}
if (res.done) { //若遍历结束
console.log('end of data');
await iterator.close(); //关闭迭代器
console.info(allResults);
return allResults;
}
}//end while
}
功能:迭代器函数,可以迭代由getStateByRange(startKey, endKey)、getQueryResult(query)、getHistoryForKey(key)返回的迭代器获得其中元素的值。由于键值历史查询的内容与常规状态查询的内容不同,因此根据是否查询键值历史,调用该方法的方式为getAllResults(iterator, true)或getAllResults(iterator, false)。
注:getStateByPartialCompositeKey(objectType, attributes)获得的迭代器使用splitCompositeKey(compositeKey)另作处理,因为查询到的键值对的key为组合键,值为空,我们需要恢复组合键的属性做其他后续处理。该函数主要用于范围查询与处理。
posted on 2020-03-15 04:54 always-kaixuan 阅读(840) 评论(0) 编辑 收藏 举报