前边两篇教程能够称之为热身,从这里开始,进入正题。 这一次,咱们要正式建立新的交易类型或者智能合约了。前端
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的源码,有了上面的基础后,不难理解。