环境准备
- 在阿里云BaaS中部署Quorum网络节点,具体可参考另一篇博文
- 在节点管理器中找到客户端要连接节点信息
- 连接地址:在节点列表中可以查看节点rpc访问链接地址(提供http与ws两种格式)
- 安全信息:在节点设置中可以查看或修改节点访问的用户名密码
使用客户端访问
- 下载geth交互控制台
- MAC OS: geth_v2.2.0_darwin_amd64.tar.gz
- Ubuntu: geth_v2.2.0_ubuntu_amd64.tar.gz
- 目前只支持MacOS与Ubuntu, Windows平台下可以通过WSL(Windows Subsystem for Linux)进入Ubuntu Bash环境下访问。
- 使用节点的用户名和密码连接到节点RPC服务端口,启动 geth 交互控制台。
geth attach http://${username}:${userpassword}@${noderpcaddress}
- 连接成功后可以看到以太坊节点版本信息:
- 输入以下命令,测试网络:
eth.blockNumber
eth.accounts
命令执行结果如下,表示连接正常:
开发智能合约
Quorum一直跟随着以太坊公网版本的发展,支持使用最新版本的solidity语法开发智能合约。在Quorum中智能合约执行不再消耗gas,但在区块链网络中还是会配置一个gaslimit参数,以防止智能合约可能出现性能低下甚至死循环等错误。
智能合约开发方式完全与以太坊一致,可以使用remix在线开发,也可以使用VSCode等开发工具。各种常见的智能合约框架如truffle,在quorum也可以使用。
在我的Demo中,我将开发一个包含资产转移与产品溯源功能的合约,以下展示主要部分代码:
- 定义合约,在合约初始化时发行指定数量的token给到管理员账户:
pragma solidity >=0.4.0 <0.7.0;
//支持在方法中返回数组与结构体等类型
pragma experimental ABIEncoderV2;
contract traceability {
// 初始发币金额
uint constant initTokens = 1 * 1e6 * 1e18;
// 合约管理员-初始发币接收人
address public admin;
constructor() public {
admin = msg.sender;
balances[admin] = initTokens;
}
}
- 定义所需的结构体:
// 资产动态属性
struct MetadataMapping {
string[] keys;
mapping(string => string) metadata;
}
struct Asset {
// 资产id
bytes32 id;
// 资产名称
string name;
// 当前拥有人
address ownership;
// 资产其他动态属性
MetadataMapping metadata;
}
struct Batch {
bytes32 id;
// 发货方
address sender;
// 物流方
address transporter;
// 收货方
address receiver;
// 物流费用,可选
uint shipReward;
//token转移数量,可选
uint tokenReward;
// 状态,1-Created, 2-sent, 3-logisticReceived, 4-logisticSent, 5-received
uint status;
// 发货时间
uint256 sendTime;
// 物流收货时间
uint256 logisticReceiveTime;
// 物流发货时间
uint256 logisticSendTime;
// 收货时间
uint256 receiveTime;
// 包含的资产明细id
bytes32[] assetList;
}
// 资产历史动态追踪
struct Track {
bytes32 id;
// 资产id
bytes32 assetId;
// 当前拥有人
address ownership;
// 当前动态,1-CreateAsset, 2-CreateBatch, 3-Send, 4-LogisticReceive, 5-LogisticSend, 6-Receive
uint action;
// 备注
bytes remark;
// 动态发生时间
uint256 timestamp;
}
- 定义回调事件(Event):
event TokenSent(address from, address to, uint amount);
event AssetCreated(address ownership, bytes32 assetId);
event BatchCreated(address sender, bytes32 batchId);
event BatchSent(address sender, bytes32 batchId);
event BatchReceived(address receiver, bytes32 batchId);
- 实现Token管理方法:
/// @notice token转账
/// @param receiver 收款人
/// @param amount 转账金额
function sendToken(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "Insufficient balance.");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit TokenSent(msg.sender, receiver, amount);
}
/// @notice 获取当前账户token余额
/// @return 当前余额
function getBalance() public view returns (uint) {
return balances[msg.sender];
}
- 实现资产(产品)创建、转移、追踪管理等功能:
/// @notice 创建资产
/// @param name 资产名称
/// @param keys 动态属性名
/// @param values 动态属性值
/// @return 新资产id
function createAsset(string memory name, string[] memory keys, string[] memory values) public returns (bytes32) {
require(keys.length == values.length, "keys and values not matched");
bytes32 id = getUniqueId(1, assetSize, msg.sender);
assets[id].id = id;
assets[id].name = name;
assets[id].ownership = msg.sender;
assets[id].metadata.keys = keys;
assetSize ++;
for(uint i = 0; i < keys.length; i ++) {
assets[id].metadata.metadata[keys[i]] = values[i];
}
assetIds.push(id);
saveTrack(id, msg.sender, 1, empty);
emit AssetCreated(msg.sender, id);
return id;
}
/// @notice 创建batch
/// @param transporter 物流方地址
/// @param receiver 收货方地址
/// @param shipReward 物流费用,可为0
/// @param tokenReward Token转移金额,可为0
/// @param assetList 资产明细id
/// @return batch id
function createBatch(address transporter, address receiver, uint shipReward, uint tokenReward, bytes32[] memory assetList) public returns (bytes32) {
require((shipReward + tokenReward) <= balances[msg.sender], "Insufficient balance.");
bytes32 id = getUniqueId(2, batchSize, msg.sender);
batches[id] = Batch(id, msg.sender, transporter, receiver, shipReward, tokenReward, 1, now, 0, 0, 0, assetList);
batchSize ++;
balances[msg.sender] -= (shipReward + tokenReward);
for(uint i = 0; i < assetList.length; i ++) {
require(assets[assetList[i]].ownership == msg.sender, "Insufficient assets privileges");
saveTrack(assetList[i], msg.sender, 2, abi.encodePacked(id));
}
emit BatchCreated(msg.sender, id);
return id;
}
// 后续省略 ...
智能合约开发完成,可以在本地使用solc-js工具进行编译:
- 下载与安装最新的solc:
sudo npm install -g solc
- 编译智能合约:
solcjs --abi --bin ./traceability.sol -o build
编译成功后,在build子目录下会生成对应的abi与bin文件
使用 web3.js 开发客户端应用
- 项目初始化,在项目目录下执行以下命令,项目目录会生成package.json文件:
npm init
- 安装web3.js包
我一开始安装的是最新的web3包(1.2.4),但发现BaaS下的Quorum连接连接不上(用户密码没有错误),会把以下错误:
npm install web3 --save
然后将web3替换成0.20.7版本,发现可以正常工作:
npm install web3@0.20.7 --save
- 连接Quorum节点:
let Web3 = require("web3");
let provider = new Web3.providers.HttpProvider(
'http://xxx.xxx.xxx.xxx:xxxx', //rpc地址
50000, //超时时间(毫秒)
'user', //用户名
'Abcd@1234' //密码
);
let web3 = new Web3(provider);
console.log("web3 is connected:", web3.isConnected());
连接成功后,会输出以下日志:
web3 is connected: true
- 部署智能合约,部署成功后会输出合约地址,需要记录下来,以备调用合约时使用:
let Web3 = require("web3");
var fs = require('fs');
provider = new Web3.providers.HttpProvider('http://xxx.xxx.xxx.xxx:xxxx', 5000, 'user', 'Abcd@1234');
var web3 = new Web3(provider);
console.log(web3.isConnected());
// set first account to default account
var account = web3.eth.accounts[0];
web3.eth.defaultAccount = account;
// unlock default account
web3.personal.unlockAccount(account, "", 300);
// read abi for contract
var abi = JSON.parse(fs.readFileSync("../contract/build/traceability_sol_traceability.abi"));
// read bytecode for contract
var bytecode = "0x" + fs.readFileSync('../contract/build/traceability_sol_traceability.bin');
var address = ""
var simpleContract = web3.eth.contract(abi);
var simple = simpleContract.new({
from: account,
data: bytecode,
gas: 0x47b760
}, function(e, contract) {
if (e) {
console.log("err creating contract", e);
} else {
if (!contract.address) {
console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined...");
} else {
// get contract address from here!
console.log("Contract mined! Address: " + contract.address);
address = contract.address
console.log(contract);
}
}
});
5. 调用合约:
let Web3 = require("web3");
let fs = require("fs");
let provider = new Web3.providers.HttpProvider('http://xxx.xxx.xxx.xxx:xxxx', 50000, 'user', 'Abcd@1234');
let web3 = new Web3(provider);
console.log("web3 is connected:", web3.isConnected());
let account = web3.eth.accounts[0];
web3.eth.defaultAccount = account;
web3.personal.unlockAccount(account, "", 3000);
// abi for contract
var abi = JSON.parse(fs.readFileSync("../contract/build/traceability_sol_traceability.abi"));
var contractAddress = "0x1dbaccedfe36189819d2f6029b8036f9a0ea398b";
var contract = web3.eth.contract(abi).at(contractAddress);
console.log("getBalance:", contract.getBalance().toString());
var assetName = "asset1";
var assetKeys = ["color", "weight"];
var assetValues = ["red", "0.1kg"];
contract.TokenSent({a:5}, function(error, result) {
console.log("TokenSent event");
if(!error) {
console.log(result);
} else {
console.log("error:", error);
}
});
contract.AssetCreated({}, function(error, result) {
console.log("AssetCreated event");
if(!error) {
console.log(result);
} else {
console.log("error:", error);
}
});
console.log(contract.sendToken(0xf07b2cb4d766ffa81bea15b99cd459c69b9f766a, 1*1e18));
console.log("getBalance:", contract.getBalance().toString());
// console.log("getAssetList:", contract.getAssetList());
// console.log("createAsset:", contract.createAsset(assetName, assetKeys, assetValues, {gas:30000000}));
// console.log("getCurrentAssetId:", contract.getCurrentAssetId().toString());
// console.log("getAssetInfo:", contract.getAssetInfo(0xb3a2d41842b3b53a8bf82c3aae28f6ad7a752c793715244182b7839f37f07d20));
// contract.createAsset(assetName, assetKeys, assetValues, {}, function(error, result){
// console.log("call CreateAsset");
// if(!error) {
// console.log("result:", result);
// } else {
// console.log("error:", error);
// }
// });
合约与事件输出:
总结
Quorum是在以太坊的基础上发展起来的,以太坊的开发工具链基本都可以用上,如果熟悉以太坊智能合约与web3.js开发的话,开发quorum应用也就会非常轻松。
只是不知为何web3.js 1.0以上版本连接不上,看阿里云文档说明是可以支持的,此问题后面将继续跟进。
Quorum本地原生部署确实比较麻烦,原代码编译不通过,文档说明也不够细致。好在阿里云BaaS平台提供了对Quorum的支持,通过BaaS可以在短时间内搭建起自己的Quorum网络。
Demo项目源码可参考:https://github.com/ft-john/quorum_demo