前邊兩篇教程能夠稱之爲熱身,從這裏開始,進入正題。 這一次,咱們要正式建立新的交易類型或者智能合約了。前端
1 建立合約sql
首先要進入dapp所在目錄數據庫
cd dapps/<dapp id>/
而後執行asch-cli的contract子命令json
asch-cli contract -a
接下來會提示輸入合約的名字,這裏輸入的是"Project"api
? Contract file name (without .js) Project New contract created: ./contracts/Project.js Updating contracts list Done
這個命令會幫咱們作三件事數組
新增了合約模板文件modules/contracts/Project.js
在modules/helper/transaction-types.js註冊了交易類型
在modules.full.json中註冊了新的模塊
2 定義實體字段app
在實現一個智能合約以前,須要定義好合約執行後生成的交易數據實體,即最終存儲到區塊鏈上的是哪些數據,也就是至關於建立關係數據的表格 一個合約類型對應一張表格 表格的schema在blockchain.json中進行配置less
project類型比較簡單,只包含name和description字段 另外transactionId字段是每一個實體表格都須要的,是做爲基礎交易transactions的外鍵。異步
{ "table": "asset_project", "alias": "t_p", "type": "table", "tableFields": [ { "name": "name", "type": "String", "length": 16, "not_null": true }, { "name": "description", "type": "Text", "not_null": true }, { "name": "transactionId", "type": "String", "length": 21, "not_null": true } ], "foreignKeys": [ { "field": "transactionId", "table": "transactions", "table_field": "id", "on_delete": "cascade" } ] }
而後須要在join字段種加入新的配置,仍是爲了聯合查詢以及序列化和反序列化時使用區塊鏈
{ "type": "left outer", "table": "asset_project", "alias": "t_p", "on": { "t.id": "t_p.transactionId" } }
未來,asch會把這些配置經過自動化的方式生成,開發者只須要輸入實體字段的名稱和類型便可。
3 實現合約接口
一個合約包含以下接口,有的必需要實現,有個則使用默認生成的代碼便可
create # 建立一個交易的數據對象,主要是賦值操做 calculateFee # 設置交易費,即生成一次交易須要消耗的XAS數量 verify # 驗證交易數據,好比字段是否合法,依賴條件是否知足等 getBytes # 返回交易的二進制數據,類型爲Buffer apply # 合約的執行邏輯,在區塊打包時調用,主要是分配和轉移交易涉及到的各個帳戶的資產,以及帳戶其餘字段的設置等 undo # apply的相反操做,在區塊回滾時會調用 applyUnconfirmed # 合約的預執行邏輯,與apply相似,可是這個會實時的調用,就是說區塊打包前就會調用,所以涉及到的帳戶操做都是臨時、未確認的 undoUnconfirmed # applyUnconfirmed的相反操做,回滾時使用 ready # 交易是否準備完畢,是否知足打包的條件,這是個高級功能,大部分狀況都不須要,之後會單獨講解 save # 交易數據的序列化操做,就是將json字段映射到數據庫表格字段 dbRead # 交易的反序列化操做,將數據庫表格字段映射到json字段 normalize # 交易數據的格式化,把不相關的對象字段刪除,相關的對象統一類型,通常狀況不須要
上面的接口大部分狀況下使用默認的就能夠了 開發者須要注意的主要是apply和applyUnconfirmed兩個接口,這是業務邏輯的主體部分。
4 實現Project合約
實現create
trs.recipientId = null; // 建立項目只須要發起者,不須要接收者,因此設爲null trs.amount = 0; // 也不須要金額,只須要手續費 trs.asset.project = { name: data.name, description: data.description } // project對象的兩個數據字段 return trs;
設置交易費
這個項目不但願與XAS對接,那麼就把交易費設置爲0就好了
Project.prototype.calculateFee = function (trs) { return 0; }
數據檢驗
這個沒啥可解釋的
Project.prototype.verify = function (trs, sender, cb, scope) { if (trs.recipientId) { return cb("Recipient should not exist"); } if (trs.amount != 0) { return cb("Amount should be zero"); } if (!trs.asset.project.name) { return cb("Project must have a name"); } if (trs.asset.project.name.length > 16) { return cb("Project name must be 16 characters or less"); } if (!trs.asset.project.description) { return cb("Invalid project description"); } if (trs.asset.project.description.length > 1024) { return cb("Project description must be 1024 characters or less"); } cb(null, trs); }
獲取二進制數據
二進制數據主要是爲了生成簽名數據,因此只須要把交易的實體數據組合起來打包成Buffer就能夠了。 組合的方式能夠隨便,好比,能夠經過bytebuffer,也能夠經過簡單的字符串鏈接。
Project.prototype.getBytes = function (trs) { try { var buf = new Buffer(trs.asset.project.name + trs.asset.project.description, "utf8"); } catch (e) { throw Error(e.toString()); } return buf; }
合約執行邏輯
先看未確認合約的執行
Project.prototype.applyUnconfirmed = function (trs, sender, cb, scope) { if (sender.u_balance["POINTS"] < BURN_POINTS) { return setImmediate(cb, "Account does not have enough POINTS: " + trs.id); } if (private.uProjects[trs.asset.project.name]){ return setImmediate(cb, "Project already exists"); } modules.blockchain.accounts.mergeAccountAndGet({ address: sender.address, u_balance: { "POINTS": -BURN_POINTS } }, function (err, accounts) { if (!err) { private.uProjects[trs.asset.project.name] = trs; } cb(err, accounts); }, scope); }
在這一步,檢查用戶的餘額是否足夠,不然拒絕執行, 接着判斷是否已經存在相同的項目名稱, 最後會看到一個dapp開發中最重要的api,即modules.blockchain.accounts.mergeAccountAndGet。
這個api的功能是對帳戶進行操做,這個操做包括對數字的加減法、數組的增刪、字符串的設置等。 這裏對帳戶餘額執行了減法操做,即把u_balance中的POINTS資產,減去BURN_POINTS。 這裏取名BURN_POINTS主要是爲了表達這個合約的執行須要燃燒必定數量的資產,由於沒有指定被消耗掉的資產的去向,那麼這些被消耗的資產就只有消失了,也就是被燃燒了。 這裏只是爲了簡單起見,若是業務邏輯不但願燃燒,能夠把這些資產做爲手續費,轉給應用的開發者或者節點運營者,或者轉移到一個基金帳戶中,用做未來的開發經費,徹底由你本身決定。
接下來再看看確認合約的執行代碼
Project.prototype.apply = function (trs, sender, cb, scope) { modules.blockchain.accounts.mergeAccountAndGet({ address: sender.address, balance: {"POINTS": -BURN_POINTS} }, cb, scope); }
很是簡單,只有一個操做,僅僅是對帳戶資產進行一個減法操做。 大部分狀況下, applyUnconfirmed是比apply要複雜的,特別是涉及到資產的減法操做時,由於前者要比後者執行的更早,後者就不必作多餘的條件檢查了。 咱們要注意到,apply修改的是balance字段,applyUnconfirmed修改的是u_balance字段,
因此若是u_balance知足條件(即有足夠的剩餘資產),那麼balance必定也會知足條件,因此就不必進行進一步檢查了。
接下來的save, dbRead就不必解釋了,開發者能夠本身發現其中的規律,直接套用便可。
5 實現http接口
在上一個步驟,已經定義了一個project合約的全部邏輯了。 在這一步,咱們須要增長兩個接口,都是爲客戶端或前端服務的,一個是用於建立交易,一個是用於查詢交易歷史。
幾乎全部的交易建立都是相似的,通常能夠分解成一下幾步
使用客戶端傳過來的secret生成密鑰對keypair
使用公鑰查詢或新建帳戶數據,經過api modules.blockchain.accounts.getAccount
而後使用客戶端傳過來的交易實體數據和帳戶數據以及密鑰對,建立一個交易對象,經過api modules.logic.transaction.create
最後是調用api modules.blockchain.transactions.processUnconfirmedTransaction來處理這個交易
有一點須要注意的是library.sequence.add接口的使用,這個接口能夠保證多個交易按前後順序嚴格執行,若是你的合約邏輯中涉及到異步操做,應該要使用這個api。
再來看一下list這個查詢接口,熟悉sql的同窗一眼就看出,這只不過是個聯表查詢操做。
爲何要聯表查詢呢?
由於transactions和asset_xxx表示的是一個交易的不一樣部分,前者是數據的基礎數據,全部交易都通用,好比交易的發起者,交易數據的簽名,金額等等, 後者則屬於交易數據的擴展部分,是用戶自定義的數據,與具體的業務邏輯相關。
6 實現投票合約
這個就不逐行解釋了,開發者能夠本身研究asch-mini-dao的源碼,有了上面的基礎後,不難理解。