Toc
  1. 环境准备
  2. 使用客户端访问
  3. 开发智能合约
  4. 使用 web3.js 开发客户端应用
  5. 总结
Toc
0 results found
FT-JOHN
阿里云BaaS下企业以太坊(Quorum)开发实践

antblockchain

环境准备

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

使用客户端访问

  1. 下载geth交互控制台
  2. 使用节点的用户名和密码连接到节点RPC服务端口,启动 geth 交互控制台。
    geth attach http://${username}:${userpassword}@${noderpcaddress}
  3. 连接成功后可以看到以太坊节点版本信息:
    连接成功
  4. 输入以下命令,测试网络:
    eth.blockNumber
    eth.accounts
    命令执行结果如下,表示连接正常:
    控制台执行结果

开发智能合约

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;
    }
    }
  2. 定义所需的结构体:
    // 资产动态属性
    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;
    }
  3. 定义回调事件(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);
  4. 实现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];
    }
  5. 实现资产(产品)创建、转移、追踪管理等功能:
        /// @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工具进行编译:
  6. 下载与安装最新的solc:
    sudo npm install -g solc
  7. 编译智能合约:
    solcjs --abi --bin ./traceability.sol -o build
    编译成功后,在build子目录下会生成对应的abi与bin文件

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

  1. 项目初始化,在项目目录下执行以下命令,项目目录会生成package.json文件:
    npm init
  2. 安装web3.js包
    我一开始安装的是最新的web3包(1.2.4),但发现BaaS下的Quorum连接连接不上(用户密码没有错误),会把以下错误:
    npm install web3 --save
    1.2.4版本连接失败

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

npm install web3@0.20.7 --save

0.20.7版本连接成功

  1. 连接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
  2. 部署智能合约,部署成功后会输出合约地址,需要记录下来,以备调用合约时使用:
    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);
    }
    }
    });
    部署合约
  3. 调用合约:
    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

本文作者:FT-JOHN
版权声明:本文首发于FT-JOHN的博客,转载请注明出处!