智慧合約從入門到精通:完整範例

區塊鏈技術發表於2018-07-06

簡介:前幾篇文章我們一直在討論Solidity語言的相關語法,從本文開始,我們將介紹智慧合約開發。今天我們將介紹一個完整範例。

此章節將介紹一個完整案例來幫助開發者快速瞭解合約的開發規範及流程。

注意:

在進行案例編寫前,請先前往JUICE開放服務平臺,完成使用者註冊,JUICE區塊鏈賬戶建立;並下載、安裝、配置好JUICE客戶端。https://open.juzix.net/

場景描述

在案例實踐前請確保已擁有可用的JUICE區塊鏈平臺環境!!!

現假設一個場景,編寫一個顧客管理合約。主要實現以下功能:

  • 提供增加顧客資訊功能,手機號作為唯一KEY;

  • 提供根據手機號刪除顧客資訊的功能;

  • 提供輸出所有顧客資訊的功能;

介面定義

說明:此介面定義了顧客管理合約的基本操作,介面的定義可以開放給三方進行呼叫而不暴露原始碼;

檔案目錄:${workspace}/contracts/interfaces 用於存放抽象合約目錄

pragma solidity ^0.4.2;

contract IConsumerManager {

    function add(string _mobile, string _name, string _account, string _remark) public returns(uint);

    function deleteByMobile(string _mobile) public returns(uint);

    function listAll() constant public returns (string _json);

}
複製程式碼
  • add(string _mobile, string _name, string _account, string _remark) 新增一個顧客資訊

  • deleteByMobile(string_mobile) 根據手機號刪除顧客資訊

  • listAll() 輸出所有顧客資訊,此方法不影響變數狀態,因此使用constant修飾;

資料結構定義

說明:當介面中的輸入輸出資料項比較多,或者儲存在鏈上的資料項比較多時,開發者可以定義一個結構化資料,來簡化資料項的宣告。並且在這個結構化資料,還可以封裝對資料的序列化操作,主要包括通過將json格式轉為結構化資料 或 反序列化為json格式。

可以把結構化資料,看成物件導向程式設計中的物件。

檔案目錄:${workspace}/contracts/librarys 用於存放資料結構的定義

pragma solidity ^0.4.2;

import "../utillib/LibInt.sol";
import "../utillib/LibString.sol";
import "../utillib/LibStack.sol";
import "../utillib/LibJson.sol";

library LibConsumer {

    using LibInt for *;
    using LibString for *;
    using LibJson for *;
    using LibConsumer for *;


    struct Consumer {
        string mobile;
        string name;
        string account;
        string remark;
    }

    /**
    *@desc fromJson for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function fromJson(Consumer storage _self, string _json) internal returns(bool succ) {
        _self.reset();

        if (!_json.isJson())
            return false;

        _self.mobile = _json.jsonRead("mobile");
        _self.name = _json.jsonRead("name");
        _self.account = _json.jsonRead("account");
        _self.remark = _json.jsonRead("remark");

        return true;
    }

    /**
    *@desc toJson for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function toJson(Consumer storage _self) internal constant returns (string _json) {
        LibStack.push("{");
        LibStack.appendKeyValue("mobile", _self.mobile);
        LibStack.appendKeyValue("name", _self.name);
        LibStack.appendKeyValue("account", _self.account);
        LibStack.appendKeyValue("remark", _self.remark);
        LibStack.append("}");
        _json = LibStack.pop();
    }

    /**
    *@desc fromJsonArray for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function fromJsonArray(Consumer[] storage _self, string _json) internal returns(bool succ) {
        _self.length = 0;

        if (!_json.isJson())
            return false;

        while (true) {
            string memory key = "[".concat(_self.length.toString(), "]");
            if (!_json.jsonKeyExists(key))
                break;

            _self.length++;
            _self[_self.length-1].fromJson(_json.jsonRead(key));
        }

        return true;
    }

    /**
    *@desc toJsonArray for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function toJsonArray(Consumer[] storage _self) internal constant returns(string _json) {
        _json = _json.concat("[");
        for (uint i=0; i<_self.length; ++i) {
            if (i == 0)
                _json = _json.concat(_self[i].toJson());
            else
                _json = _json.concat(",", _self[i].toJson());
        }
        _json = _json.concat("]");
    }

    /**
    *@desc update for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function update(Consumer storage _self, string _json) internal returns(bool succ) {
        if (!_json.isJson())
            return false;

        if (_json.jsonKeyExists("mobile"))
            _self.mobile = _json.jsonRead("mobile");
        if (_json.jsonKeyExists("name"))
            _self.name = _json.jsonRead("name");
        if (_json.jsonKeyExists("account"))
            _self.account = _json.jsonRead("account");
        if (_json.jsonKeyExists("remark"))
            _self.remark = _json.jsonRead("remark");

        return true;
    }

    /**
    *@desc reset for Consumer
    *      Generated by juzhen SolidityStructTool automatically.
    *      Not to edit this code manually.
    */
    function reset(Consumer storage _self) internal {
        delete _self.mobile;
        delete _self.name;
        delete _self.account;
        delete _self.remark;
    }


}
複製程式碼
  • toJson(Consumer storage _self) 將struct結構序列化為JSON格式:{"mobile":"xxx",...}.

  • fromJson(Consumer storage _self, string _json) 將一個JSON串反序列為struct結構.

  • fromJsonArray(Consumer[] storage _self, string _json),將一個陣列形式的JSON串轉為資料struct結構

  • toJsonArray(Consumer[] storage _self) 陣列結構反序列化,eg.[{"mobile":"xxx",...},...]

  • reset(Consumer _self) 重置struct中為預設值.

業務合約編寫

說明:顧客管理合約的主要業務邏輯,即合約介面的實現類.ConsumerManager.sol,該合約繼承了基礎合約OwnerNamed以及抽象合約IConsumerManager。

  • OwnerNamed 主要提供一些基礎操作,主要包含模組註冊、合約註冊、資料寫入DB等操作,所有業務合約需按規定繼承該合約。

檔案目錄:${workspace}/contracts 用於存放業務合約主體邏輯

pragma solidity ^0.4.2;

import "./library/LibConsumer.sol";
import "./sysbase/OwnerNamed.sol";
import "./interfaces/IConsumerManager.sol";
import "./interfaces/IUserManager.sol";
import "./utillib/LibLog.sol";

contract ConsumerManager is OwnerNamed, IConsumerManager {

    using LibConsumer
    for * ;
    using LibString
    for * ;
    using LibInt
    for * ;
    using LibLog
    for * ;

    event Notify(uint _errno, string _info);

    LibConsumer.Consumer[] consumerList;
    mapping(string => uint) keyMap;


    //定義錯誤資訊
    enum ErrorNo {
        NO_ERROR,
        BAD_PARAMETER,
        MOBILE_EMPTY,
        USER_NOT_EXISTS,
        MOBILE_ALREADY_EXISTS,
        ACCOUNT_ALREDY_EXISTS,
        NO_PERMISSION
    }

    // 建構函式,在合約釋出時會被觸發呼叫
    function ConsumerManager() {
        LibLog.log("deploy ConsumerModule....");

        //把合約註冊到JUICE鏈上, 引數必須和ConsumerModule.sol中的保持一致
        register("ConsumerModule", "0.0.1.0", "ConsumerManager", "0.0.1.0");

        //或者註冊到特殊的模組"juzix.io.debugModule",這樣使用者就不需要編寫模組合約了
        //register("juzix.io.debugModule", "0.0.1.0", "ConsumerManager", "0.0.1.0");
    }


    function add(string _mobile, string _name, string _account, string _remark) public returns(uint) {
        LibLog.log("into add..", "ConsumerManager");
        LibLog.log("ConsumerManager into add..");

        if (_mobile.equals("")) {
            LibLog.log("Invalid mobile.", "ConsumerManager");
            errno = 15200 + uint(ErrorNo.MOBILE_EMPTY);
            Notify(errno, "顧客手機號為空,插入失敗.");
            return errno;
        }

        if (keyMap[_mobile] == 0) {
            if (consumerList.length > 0) {
                if (_mobile.equals(consumerList[0].mobile)) {
                    LibLog.log("mobile aready exists", "ConsumerManager");
                    errno = 15200 + uint(ErrorNo.MOBILE_ALREADY_EXISTS);
                    Notify(errno, "顧客手機號已存在,插入失敗.");
                    return errno;
                }
            }
        } else {
            LibLog.log("mobile aready exists", "ConsumerManager");
            errno = 15200 + uint(ErrorNo.MOBILE_ALREADY_EXISTS);
            Notify(errno, "顧客手機號已存在,插入失敗.");
            return errno;
        }

        uint idx = consumerList.length;
        consumerList.push(LibConsumer.Consumer(_mobile, _name, _account, _remark));

        keyMap[_mobile] = idx;

        errno = uint(ErrorNo.NO_ERROR);

        LibLog.log("add a consumer success", "ConsumerManager");
        Notify(errno, "add a consumer success");
        return errno;
    }

    function deleteByMobile(string _mobile) public returns(uint) {
        LibLog.log("into delete..", "ConsumerManager");

        //合約擁有者,才能刪除顧客資訊
        if (tx.origin != owner) {
            LibLog.log("msg.sender is not owner", "ConsumerManager");
            LibLog.log("operator no permission");
            errno = 15200 + uint(ErrorNo.NO_PERMISSION);
            Notify(errno, "無操作許可權,非管理員");
            return;
        }

        //顧客列表不為空
        if (consumerList.length > 0) {
            if (keyMap[_mobile] == 0) {
                //_mobile不存在,或者是陣列第一個元素
                if (!_mobile.equals(consumerList[0].mobile)) {
                    LibLog.log("consumer not exists: ", _mobile);
                    errno = 15200 + uint(ErrorNo.USER_NOT_EXISTS);
                    Notify(errno, "顧客手機號不存在,刪除失敗.");
                    return;
                }
            }
        } else {
            LibLog.log("consumer list is empty: ", _mobile);
            errno = 15200 + uint(ErrorNo.USER_NOT_EXISTS);
            Notify(errno, "顧客列表為空,刪除失敗.");
            return;
        }

        //陣列總長度
        uint len = consumerList.length;

        //此使用者在陣列中的序號
        uint idx = keyMap[_mobile];

        if (idx >= len) return;
        for (uint i = idx; i < len - 1; i++) {
            //從待刪除的陣列element開始,把後一個element移動到前一個位置
            consumerList[i] = consumerList[i + 1];
            //同時修改keyMap中,對應key的在陣列中的序號
            keyMap[consumerList[i].mobile] = i;
        }
        //刪除陣列最後一個元素(和倒數第二個重複了)
        delete consumerList[len - 1];
        //刪除mapping中元素,實際上是設定value為0
        delete keyMap[_mobile];

        //陣列總長度-1
        consumerList.length--;


        LibLog.log("delete user success.", "ConsumerManager");
        errno = uint(ErrorNo.NO_ERROR);

        Notify(errno, "刪除顧客成功.");
    }

    function listAll() constant public returns(string _json) {
        uint len = 0;
        uint counter = 0;
        len = LibStack.push("");
        for (uint i = 0; i < consumerList.length; i++) {
            if (counter > 0) {
                len = LibStack.append(",");
            }
            len = LibStack.append(consumerList[i].toJson());
            counter++;
        }
        len = itemsStackPush(LibStack.popex(len), counter);
        _json = LibStack.popex(len);
    }

    function itemsStackPush(string _items, uint _total) constant private returns(uint len) {
        len = 0;
        len = LibStack.push("{");
        len = LibStack.appendKeyValue("result", uint(0));
        len = LibStack.appendKeyValue("total", _total);
        len = LibStack.append(",\"data\":[");
        len = LibStack.append(_items);
        len = LibStack.append("]");
        len = LibStack.append("}");
        return len;
    }
}
複製程式碼

模組合約

說明:模組合約是JUICE區塊鏈中,為了管理使用者的業務合約,以及為了管理DAPP和業務的關係而引入的。開發者在實現業務合約後,必須編寫一個或多個模組合約,並在模組 合約中說明本模組中用到的業務合約。從DAPP的角度來理解,就是一個DAPP必須對應一個模組,一個DAPP能呼叫的業務合約,必須在DAPP對應的模組合約中說明。

模組合約繼承了基礎模組合約BaseModule

  • BaseModule 主要提供一些基礎操作,主要包含:模組新增、合約新增、角色新增等操作.

檔案目錄:${workspace}/contracts 用於存放業務模組合約主體邏輯

/**
 * @file      ConsumerModule.sol
 * @author    JUZIX.IO
 * @time      2017-12-11
 * @desc      給使用者展示如何編寫一個自己的模組。
 *            ConsumerModule本身也是一個合約,它需要部署到鏈上;同時,它又負責管理使用者的合約。只有新增到模組中的使用者合約,使用者才能在dapp中呼叫這些合約
 */
pragma solidity ^ 0.4 .2;

//juice的管理庫,必須引入
import "./sysbase/OwnerNamed.sol";
import "./sysbase/BaseModule.sol";

//juice提供的模組庫,必須引入
import "./library/LibModule.sol";

//juice提供的合約庫,必須引入
import "./library/LibContract.sol";

//juice提供的string庫
import "./utillib/LibString.sol";

//juice提供的log庫
import "./utillib/LibLog.sol";

contract ConsumerModule is BaseModule {

    using LibModule
    for * ;
    using LibContract
    for * ;
    using LibString
    for * ;
    using LibInt
    for * ;
    using LibLog
    for * ;

    LibModule.Module tmpModule;
    LibContract.Contract tmpContract;

    //定義Demo模組中的錯誤資訊
    enum MODULE_ERROR {
        NO_ERROR
    }

    //定義Demo模組中用的事件,可以用於返回錯誤資訊,也可以返回其他資訊
    event Notify(uint _code, string _info);

    // module : predefined data
    function ConsumerModule() {

        //定義模組合約名稱
        string memory moduleName = "ConsumerModule";

        //定義模組合約名稱
        string memory moduleDesc = "顧客模組";

        //定義模組合約版本號
        string memory moduleVersion = "0.0.1.0";

        //指定模組合約ID
        //moduleId = moduleName.concat("_", moduleVersion);
        string memory moduleId = moduleName.concat("_", moduleVersion);

        //把合約註冊到JUICE鏈上
        LibLog.log("register DemoModule");
        register(moduleName, moduleVersion);

        //模組名稱,只是JUICE區塊鏈內部管理模組使用,和moduleText有區別
        tmpModule.moduleName = moduleName;
        tmpModule.moduleVersion = moduleVersion;
        tmpModule.moduleEnable = 0;
        tmpModule.moduleDescription = moduleDesc;
        //顯示JUICE開放平臺,我的應用列表中的DAPP名字
        tmpModule.moduleText = moduleDesc;

        uint nowTime = now * 1000;
        tmpModule.moduleCreateTime = nowTime;
        tmpModule.moduleUpdateTime = nowTime;

        tmpModule.moduleCreator = msg.sender;

        //這裡設定使用者DAPP的連線地址(目前DAPP需要有使用者自己釋出、部署到公網上)
        tmpModule.moduleUrl = "http://host.domain.com/youDapp/";



        tmpModule.icon = "";
        tmpModule.publishTime = nowTime;

        //把模組合約本身新增到系統的模組管理合約中。這一步是必須的,只有這樣,使用者的dapp才能呼叫新增到此模組合約的相關合約。
        //並在使用者的“我的應用”中展示出來
        LibLog.log("add ConsumerModule to SysModule");
        uint ret = addModule(tmpModule.toJson());

        if (ret != 0) {
            LibLog.log("add ConsumerModule to SysModule failed");
            return;
        }

        //新增使用者合約到模組合約中
        LibLog.log("add ConsumerManager to ConsumerModule");
        ret = initContract(moduleName, moduleVersion, "ConsumerManager", "顧客管理合約", "0.0.1.0");
        if (ret != 0) {
            LibLog.log("add ConsumerManager to ConsumerModule failed");
            return;
        }


        //返回訊息,以便控制檯能看到是否部署成功
        Notify(1, "deploy ConsumerModule success");
    }

    /**
     * 初始化使用者自定義合約。
     * 如果使用者有多個合約檔案,則需要多次呼叫此方法。
     * @param moduleName        約合所屬模組名
     * @param moduleVersion     約合所屬模組版本
     * @param contractName      約合名
     * @param contractDesc      約合描述
     * @param contractVersion   約合版本
     * @return return 0 if success;
     */
    function initContract(string moduleName, string moduleVersion, string contractName, string contractDesc, string contractVersion) private returns(uint) {
        tmpContract.moduleName = moduleName;
        tmpContract.moduleVersion = moduleVersion;

        //合約名稱
        tmpContract.cctName = contractName;
        //合約描述
        tmpContract.description = contractDesc;
        //合約版本
        tmpContract.cctVersion = contractVersion;

        //保持false
        tmpContract.deleted = false;
        //保持0
        tmpContract.enable = 0;

        uint nowTime = now * 1000;
        //合約建立時間
        tmpContract.createTime = nowTime;
        //合約修改時間
        tmpContract.updateTime = nowTime;

        //合約建立人
        tmpContract.creator = msg.sender;
        //預約塊高
        tmpContract.blockNum = block.number;

        uint ret = addContract(tmpContract.toJson());
        return ret;
    }

}
複製程式碼
  • 模組合約作用:當進行一個新的DAPP開發時會伴隨著一些合約的業務服務的編寫,即,合約為DAPP應用提供業務邏輯的服務,我們將這一類(或一組)合約統一歸屬到一個模組中(eg:HelloWorldModuleMgr)。在JUICE區塊鏈平臺上有一套鑑權體系,一個合約要被成功呼叫需要經過多層鑑權:

o校驗模組開關,開:繼續鑑權,關:直接通過

o校驗合約開關,開:繼續鑑權,關:直接通過

o檢驗函式開關,開:繼續鑑權,關:直接通過

o校驗使用者是否存在,存在則訪問通過,不存在則鑑權失敗

注意:如果是合約釋出者owner(超級管理員)則不需要鑑權可直接通過。

  • HelloWorldModuleMgr該合約的主要功能就是做資料的初始化操作,當合約被髮布時觸發建構函式的呼叫。

o新增一個新的模組到角色過濾器(預設過濾器)

o新增繫結合約與模組的關係

o新增選單(新的DAPP如果需要選單-如:使用者管理)

o新增許可權,合約中的每個函式操作都是一個Action,如果需要訪問就需要進行配置;

o新增角色,初始化某些角色到模組中,並繫結對應的許可權到角色上;

編譯部署、測試

編譯部署

業務合約,模組合約編寫完成後

  • 首先,處理業務合約

    1.編譯業務合約,編譯成功後,在控制檯分別複製出ABI,BIN,並分別儲存到contracts/ConsumerManager.abi,contracts/ConsumerManager.bin文字檔案中。這兩個檔案,可以用web3j生成呼叫業務合約的JAVA代理類,這個在編寫DAPP時有用,因此在編譯階段就先儲存這兩個檔案。(注:JUICE客戶端的後續版本中,將在編譯業務合約時,直接生成JAVA代理類,開發者不用再手工儲存bin/abi,再手工生成JAVA代理類)

    2.部署業務合約

  • 然後,處理模組合約

    1.編譯模組合約。編譯成功後的的bin/abi,不需要儲存。

    2.部署模組合約

測試

在JUICE客戶端中,選擇需要測試的業務合約,以及相應的業務方法,然後填寫輸入引數,即可執行。使用者可觀察控制檯的日誌輸出,來判斷業務方法是否執行成功。

參考內容:https://open.juzix.net/doc

智慧合約開發教程視訊:區塊鏈系列視訊課程之智慧合約簡介

相關文章