天天看点

阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结

环境准备

  1. 在阿里云BaaS中部署Quorum网络节点,具体可参考另一篇博文
  2. 在节点管理器中找到客户端要连接节点信息
    • 连接地址:在节点列表中可以查看节点rpc访问链接地址(提供http与ws两种格式)
    • 安全信息:在节点设置中可以查看或修改节点访问的用户名密码

使用客户端访问

  1. 下载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环境下访问。
  2. 使用节点的用户名和密码连接到节点RPC服务端口,启动 geth 交互控制台。
geth attach http://${username}:${userpassword}@${noderpcaddress}
           
  1. 连接成功后可以看到以太坊节点版本信息:
    阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结
  2. 输入以下命令,测试网络:
eth.blockNumber
eth.accounts
           

命令执行结果如下,表示连接正常:

阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结

开发智能合约

Quorum一直跟随着以太坊公网版本的发展,支持使用最新版本的solidity语法开发智能合约。在Quorum中智能合约执行不再消耗gas,但在区块链网络中还是会配置一个gaslimit参数,以防止智能合约可能出现性能低下甚至死循环等错误。

智能合约开发方式完全与以太坊一致,可以使用remix在线开发,也可以使用VSCode等开发工具。各种常见的智能合约框架如truffle,在quorum也可以使用。

在我的Demo中,我将开发一个包含资产转移与产品溯源功能的合约,以下展示主要部分代码:

  1. 定义合约,在合约初始化时发行指定数量的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;
    }
}
           
  1. 定义所需的结构体:
// 资产动态属性
    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;
    }
           
  1. 定义回调事件(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);
           
  1. 实现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];
    }
           
  1. 实现资产(产品)创建、转移、追踪管理等功能:
/// @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工具进行编译:

  1. 下载与安装最新的solc:
sudo npm install -g solc
           
  1. 编译智能合约:
solcjs --abi --bin ./traceability.sol -o build 
           

编译成功后,在build子目录下会生成对应的abi与bin文件

使用 web3.js 开发客户端应用

  1. 项目初始化,在项目目录下执行以下命令,项目目录会生成package.json文件:
npm init
           
  1. 安装web3.js包

    我一开始安装的是最新的web3包(1.2.4),但发现BaaS下的Quorum连接连接不上(用户密码没有错误),会把以下错误:

npm install web3 --save
           
阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结

然后将web3替换成0.20.7版本,发现可以正常工作:

npm install [email protected] --save
           
阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结
  1. 连接Quorum节点:
let Web3 = require("web3");

let provider = new Web3.providers.HttpProvider(
'http://xxx.xxx.xxx.xxx:xxxx',  //rpc地址
50000,  //超时时间(毫秒)
'user',  //用户名
'[email protected]' //密码
);
let web3 = new Web3(provider);
console.log("web3 is connected:", web3.isConnected());
           

连接成功后,会输出以下日志:

web3 is connected: true
           
  1. 部署智能合约,部署成功后会输出合约地址,需要记录下来,以备调用合约时使用:
let Web3 = require("web3");
var fs = require('fs');

provider = new Web3.providers.HttpProvider('http://xxx.xxx.xxx.xxx:xxxx', 5000, 'user', '[email protected]');
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);
     }
 }
});
           
阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结

5. 调用合约:

let Web3 = require("web3");
let fs = require("fs");

let provider = new Web3.providers.HttpProvider('http://xxx.xxx.xxx.xxx:xxxx', 50000, 'user', '[email protected]');
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);
//     }
// });
           

合约与事件输出:

阿里云BaaS下企业以太坊(Quorum)开发实践环境准备使用客户端访问开发智能合约使用 web3.js 开发客户端应用总结

总结

Quorum是在以太坊的基础上发展起来的,以太坊的开发工具链基本都可以用上,如果熟悉以太坊智能合约与web3.js开发的话,开发quorum应用也就会非常轻松。

只是不知为何web3.js 1.0以上版本连接不上,看阿里云文档说明是可以支持的,此问题后面将继续跟进。

Quorum本地原生部署确实比较麻烦,原代码编译不通过,文档说明也不够细致。好在阿里云BaaS平台提供了对Quorum的支持,通过BaaS可以在短时间内搭建起自己的Quorum网络。

Demo项目源码可参考:https://github.com/ft-john/quorum_demo

继续阅读