簡介:前幾篇文章我們一直在討論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區塊鏈平台上有一套鑒權體系,一個合約要被成功調用需要經過多層鑒權:
- 校驗模塊開關,開:繼續鑒權,關:直接通過
- 校驗合約開關,開:繼續鑒權,關:直接通過
- 檢驗函數開關,開:繼續鑒權,關:直接通過
- 校驗用戶是否存在,存在則訪問通過,不存在則鑒權失敗
注意:如果是合約發布者owner(超級管理員)則不需要鑒權可直接通過。
- HelloWorldModuleMgr該合約的主要功能就是做數據的初始化操作,當合約被發布時觸發構造函數的調用。
- 添加一個新的模塊到角色過濾器(默認過濾器)
- 添加綁定合約與模塊的關系
- 添加菜單(新的DAPP如果需要菜單-如:用戶管理)
- 添加權限,合約中的每個函數操作都是一個Action,如果需要訪問就需要進行配置;
- 添加角色,初始化某些角色到模塊中,並綁定對應的權限到角色上;
編譯部署、測試
編譯部署
業務合約,模塊合約編寫完成后
- 首先,處理業務合約
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
智能合約開發教程視頻:http://edu.51cto.com/course/13403.html