保姆级DApp开发指南—— 开发DApp原来如此简单

提到DApp开发,许多开发者们都是一头雾水。如何开发DApp?开发工具是什么?开发难度又有多高?——Ultrain的这篇《Ultrain DApp开发指南》或许能让你找到一些眉目。

提到DApp开发,许多开发者们都是一头雾水。如何开发DApp?开发工具是什么?开发难度又有多高?——Ultrain的这篇《Ultrain DApp开发指南》从工具、api、合约编写以及如何将合约与前端工程相结合等方面全方位指导,手把手教开发者如何基于Ultrain开发DApp,可供入门级开发者参考。


📒目录

– 关于文档

技术简介

– 环境搭建

– 工具使用

– 与链交互

– 合约编写

– 实战演示


一、关于文档

本指南的目的是教会你如何基于Ultrain开发DApp。我们将会涉及到你需要知道的所有内容,从工具、api到合约编写,以及怎么将合约与前端工程相结合。

本指南中涉及到的所有项目,均已开源且被存储和记录在Ben的公共dapp-tutorial库(https://github.com/benyasin/dapp-tutorial)。大家可以随意地Fork、Clone、和改善这些指南。

适用人群

本指南针对未使用过Ultrain平台进行DApp开发的入门级用户。

学习前提

学习本教程需要开发者了解NodeJS、TypeScript,熟悉VUE、React等前端相关知识。

你将学会

Ultrain核心技术架构简介

Longclaw本地开发环境搭建

Robin framework命令行操作

使用U3.js与链进行常用交互操作

使用TypeScript进行合约编写

基于前端框架编写完整DApp项目


二、技术简介

ULTRAIN通过整合闲散的计算资源,提供“信任计算 ”服务,该服务类似于云计算服务,是一种基于信任计算技术的独立计算模式,为信任计算的商业模式提供计算支撑。

高TPS的价值计算

高TPS的计算支撑

1)完全的去中心化设计

2)支持百万节点,异构系统,低能耗系统接入

3)TPS理论设计2万笔/秒

高效的智能合约执行

1)无代码行数限制

2)无执行时间限制

3)并发的智能合约执行

人人可编程的智能合约

基于TypeScript的开发框架

1)内置安全编码规范和语法自动化检查

2)支持基于NodeJS的单元测试

3)基于命令行的合约脚手架和一键部署

4)开发者友好的合约模板

5)安全高性能的指令集

可接受的使用成本,是竞争对手的1/20

1)EOS 350万/年

2)以太坊 250万/年

完备的隐私保护方案

可编程零知识证明

1)可编程:按用户的业务逻辑自定义,可灵活配置的零知识证明模块;可无需Setup过程(任意资产/任意逻辑)

2)高效:比传统的零知识证明运算速度提升25% – 50%

3)客户端低运算量:客户端运算量降低为传统方法的25%以下,可以集成到手机芯片中,极大地拓展其应用范围

R-PoS共识机制

R-PoS是一种有多项优化创新的混合共识算法。Ultrain通过吸收改良VRF随机算法,做出大规模节点的委员会成员选举,并借鉴了PoS的Stake机制来增强整个共识算法的安全性和稳定性,同时结合了BFT具备快速最终确定特性的共识算法,针对BFT进行了大量工程上的优化,最终研发出R-PoS共识机制

保姆级DApp开发指南—— 开发DApp原来如此简单

 

主侧链随机调度技术

企业客户通过使用信任计算服务,可以大幅降低其商业环境中的信任成本,重构其原有的商业模式,实现收入的高速增长。从全球范围来看,Ultrain是唯一一个可以为企业客户提供一站式信任计算商业服务的项目,也是唯一一个同时解决了高TPS、高成本和数据安全问题的项目。

保姆级DApp开发指南—— 开发DApp原来如此简单

保姆级DApp开发指南—— 开发DApp原来如此简单

Ultrain通过领先的技术研发与生态拓展能力打造可持续的、良性健康的商业模式,形成闭环。


三、环境搭建

Ultrain平台环境可以分为线下开发环境、线上测试网环境与线上主网环境。

其中,线上主网环境需要购买资源套餐后方可使用,线上测试网环境可经水龙头程序自行充值后使用,线下开发环境是指在本地自行构建的网络共识环境。以下篇幅重点介绍线下开发环境的搭建流程。

我们推荐首选的操作系统为MacOS,你将获得最佳的开发体验,其次是Linux与Windows。如果你在Windows下开发遇到一些兼容性问题,请参考Windows下Dapp开发攻略

  • Longclaw

Ultrain线下开发环境借助于Docker来构建,所以需要你在本机上提前安装并启动Docker。关于Docker安装及使用可以参考阮一峰的Docker入门教程

我们使用集成工具Longclaw来构建本地共识网络环境。首先在开发者网站上下载相应系统的Longclaw,安装并启动它。

注意:Longclaw第一次初始化环境可能要花费几分钟,需要你耐心等待,当出现以下界面,则说明Longclaw已经成功帮你构建了共识网络环境,也就是本地开发环境。

保姆级DApp开发指南—— 开发DApp原来如此简单

  • 测试账号

Longclaw为开发者默认创建了八个测试账号,这些账号拥有无限的资源使用权。当然如果你通过程序接口自行创建了账号,则默认是没有可用资源的,需要调用购买资源套餐的接口购买资源。相关文档参考与链交互一章中U3.js的对应接口用法说明。

注意:如果在Linux与Window下,Longclaw因不兼容问题,导致不能正常启动,建议直接连接线上测试网环境进行开发,相关配置可参考教程测试公链开发配置指南

(https://developer.ultrain.io/tutorial/testnet_guide)


四、工具使用

当有了可用的开发环境后,接下来就可以使用Robin framework创建一个DApp了。Robin framework是一个NodeJS编写的全局命令行程序接口。

它提供以下服务:

  • 一键式合约初始化、编译与部署;

  • 自动化合约测试与开发;

  • 友好的代码审查与错误提示;

  • 大量的合约模板与示例参考;

  • 脚本式与可配置化部署流程;

  • 交互式合约日志控制台输出;

要求

NodeJS 8.0+

Linux、MacOSX、Windows

安装

sudo npm install -g robin-framework

创建工程

执行robinorrobin -h来查看所有的robin子命令

要启动项目,首先需要创建一个新的空目录,然后进入目录:

mkdir testing cd testing

然后初始化一个项目。使用-cor--contract来指定名称。此时,你有多个模板可以选择,默认的是纯合约项目,其余的是带界面的DApp框架。

robin init

保姆级DApp开发指南—— 开发DApp原来如此简单

合约项目目录结构:

|--build // built WebAssembly targets
|--contract // contract source files
|--migrations // assign built files location and account who will deploy the contract
|--templates // some contract templates that will guide you
|--test // test files
|--config.js // configuration
...

语法检查

robin-lint的帮助下,借助定制的tslint项目,您将找到错误和警告,然后快速修复它们。 只需进入项目的根目录并执行:

robin lint

编译合约

依赖于ultrascript,合约源文件将会被编译为WebAsssembly目标文件: *.abi, *.wast, *.wasm. 只需进入项目的根目录并执行:

robin build

部署合约

更新配置文件config.jsmigrate.js, 确保你已正确连接上一个Ultrain节点。如果你正在使用longclaw初始化的本地环境,那么使用默认配置即可。也可以是你定制的节点。 只需进入项目的根目录并执行:

robin deploy

测试合约

参考测试目录下*.spec.js文件, 编写测试用例来覆盖你的合约中的所有用例场景。Robin提供给你一些测试工具类,比如mocha,chai,u3.jsandu3-utils, 尤其是用在处理异步测试,只需进入项目的根目录并执行:

robin test

集成UI

如果你想将一个合约项目升级为带界面的DAPP项目, 使用UI子命令。你有多个框架可以选择,它们分别是vue-boilerplatereact-boilerplatereact-native-boilerplate。只需进入项目的根目录并执行:

robin ui


五、与链交互

U3.js是用JavaScript封装的负责与链交互的通用库。而Robin framework引用了U3.js,借助它的接口实现了合约的deploy上链。

应用环境

浏览器(ES6)或 NodeJS

如果你想集成u3.js到react native环境中,有一个可行的方法,借助rn-nodeify实现,参考示例U3RNDemo

(https://github.com/benyasin/U3RNDemo)

使用方法

一、如果是在浏览器中使用u3,请参考以下用法:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
<script src="../dist/u3.js"></script>
<script>
let u3 = U3.createU3(
httpEndpoint: 'http://127.0.0.1:8888',
httpEndpoint_history: 'http://127.0.0.1:3000',
broadcast: true,
debug: false,
sign: true,
logger: {
log: console.log,
error: console.error,
debug: console.log
},
chainId:'0eaaff4003d4e08a541332c62827c0ac5d96766c712316afe7ade6f99b8d70fe',
symbol: 'UGAS'
});
u3.getChainInfo((err, info) =>
if (err) {throw err;}
console.log(info);
});
</script>
</head>
<body>
</body>
</html>

二、 如果是在NodeJS环境中使用u3,请参照以下用法:

  • 安装u3

npm install u3.jsyarn add u3.js

  • 初始化

const { createU3 } = require('u3.js/src');
let config = {
httpEndpoint: 'http://127.0.0.1:8888',
httpEndpoint_history: 'http://127.0.0.1:3000',
chainId: '0eaaff4003d4e08a541332c62827c0ac5d96766c712316afe7ade6f99b8d70fe',
keyProvider: ['PrivateKeys...'],
broadcast: true,
sign: true
}
let u3 = createU3(config);

u3.getChainInfo((err, info) =>{
if (err) {throw err;}
console.log(info);
});

配置

全局配置
  • httpEndpoint string – 链实时API的http或https地址。如果是在浏览器环境中使用u3,请注意配置相同的域名。
  • httpEndpoint_history string – 链历史API的http或https地址。如果是在浏览器环境中使用u3,请注意配置相同的域名。
  • chainId 链唯一的ID。链ID可以通过[httpEndpoint]/v1/chain/get_chain_info获得。
  • keyProvider [array|string|function] – 提供私钥用来签名交易,提供用于签名事务的私钥。 如果提供了多个私钥,不能确定使用哪个私钥,可以使用调用get_required_keysAPI 获取要使用签名的密钥。如果是函数,那么每一个交易都将会使用该函数。如果这里不提供keyProvider,那么它可能会Options配置项提供在每一个action或每一个transaction中。
  • expireInSeconds number – 事务到期前的秒数,时间基于nodultrain的时间。
  • broadcast [boolean=true] – 默认是true。使用true将交易发布到区块链,使用false将获取签名的事务。
  • verbose [boolean=false] – 默认是false。详细日志记录。
  • debug [boolean=false] – 默认是false。低级调试日志记录。
  • sign [boolean=true] – 默认是true。使用私钥签名交易,保留未签名的交易避免了提供私钥的需要。
  • logger
    • 默认日志配置。

logger: {
log: config.verbose ? console.log : null, // 如果值为null,则禁用日志
error: config.verbose ? console.error : null,
}

Options配置项

Options可以在方法参数之后添加,Authorization应用于单独的actions。比如:

options = {
authorization: 'alice@active',
broadcast: true,
sign: true
}

u3.transfer('alice', 'bob', '1.0000 UGAS', '', options)

  • authorization [array|auth] – 指明账号和权限,典型地应用于多重签名的配置中。Authorization必须是一个字符串格式,形如account@permission。
  • broadcast [boolean=true] – 默认是true。使用true将交易发布到区块链,使用false将获取签名的事务。
  • sign [boolean=true] – 默认是true。使用私钥签名交易。保留未签名的交易避免了提供私钥的需要。
  • keyProvider [array|string|function] – 就像global配置项中的keyProvider一样,这里的配置可以以覆盖全局配置的形式为每一个action或每一个transaction提供单独的私钥。

await u3.anyAction('args', {keyProvider})
await u3.transaction(tr ={
tr.anyAction()}, {keyProvider}
)

创建账号

创建账号需要花费creator账号的一些代币,为新账号抵押部分RAM和带宽。

const u3 = createU3(config);
const name = 'abcdefg12345';//普通账号需要满足规则:必须为12345abcdefghijklmnopqrstuvwxyz中的12位
let params = {
creator: 'ben',
name: name,
owner: pubkey,
active: pubkey,
updateable: 1,//可选,账号是否可以更新(更新合约)
};
await u3.createUser(params);

转账

转账方法使用非常频繁,UGAS的转账需要调用系统合约utrio.token。

const u3 = createU3(config);
const c = await u3.contract('utrio.token')

// 使用位置参数
await c.transfer(‘ben’, ‘bob’, ‘1.2000 UGAS’, ”)
// 使用名称参数
await c.transfer({from: ‘bob’, to: ‘ben’, quantity: ‘1.3000 UGAS’, memo: ”})

签名

使用{ sign: false, broadcast: false }创建一个u3实例并且做一些action, 然后将未签名的交易发送到钱包中。

const u3_offline = createU3({ sign: false, broadcast: false });
const c = u3_offline.contract('utrio.token');
let unsigned_transaction = await c.transfer('ultrainio', 'ben', '1 UGAS', 'uu');

在钱包中你可以提供私钥或助记词来签名,并将签名后的交易发送到链上。

const u3_online = createU3();
let signature = await u3_online.sign(unsigned_transaction, privateKeyOrMnemonic, chainId);
if (signature) {
let signedTransaction = Object.assign({}, unsigned_transaction.transaction, { signatures: [signature] });
let processedTransaction = await u3_online.pushTx(signedTransaction);
}

资源

调用合约只会消耗合约Owner的资源,所以如果你想部署一个合约,请先购买一些资源。

  • resourcelease(payer,receiver,slot,days)

const u3 = createU3(config);
const c = await u3.contract('ultrainio')

await c.resourcelease(‘ben’, ‘bob’, 1, 10);// 1 slot for 10 days

通过以下方法查询资源详情。

const resource = await u3.queryResource('abcdefg12345');
console.log(resource)

合约

部署合约

部署合约需要提供包含目标文件为.abi,.wast,*.wasm 的三个文件的文件夹.

  • deploy(contracts_files_path, deploy_account) 第一个参数为合约目标文件的绝对路径,第二个合约部署者账号。

const u3 = createU3(config);
await u3.deploy(path.resolve(__dirname, '../contracts/token/token'), 'bob');

调用合约

const u3 = createU3(config);
const c = await u3.contract('ben');
await c.transfer('bob', 'ben', '1.0000 UGAS','');

//或者像这样调用
await u3.contract(‘ben’).then(sm =>
sm.transfer(‘bob’, ‘ben’, ‘1.0000 UGAS’,”)
)

// 一笔交易也可以包含多个合约中的多个action
await u3.transaction([‘ben’, ‘bob’], ({sm1, sm2}) =>{
sm1.myaction(..)
sm2.myaction(..)
})

发行代币

const u3 = createU3(config);
const account = 'bob';
await u3.transaction(account, token =>{
token.create(account, '10000000.0000 DDD');
token.issue(account, '10000000.0000 DDD', 'issue');
});

const balance = await u3.getCurrencyBalance(account, account, ‘DDD’)
console.log(‘currency balance’, balance)

事件

Ultrain提供了一个事件注册监听机制用来解决异步场景下业务需求.客户端首先订阅一个事件,提供一个用来接收消息的地址,当合约中的某个方法触发时,该地址会收到来自链的推送消息。

订阅/取消订阅
  • registerEvent(deployer, listen_url)
  • unregisterEvent(deployer, listen_url)

deployer: 合约的部署者账号

listen_url: 接收消息的地址

注意: 如果你是在本地docker环境中使用改机制,请确认接收地址是一个可以从docker访问到的本地宿主地址.

const u3 = createU3(config);
const subscribe = await u3.registerEvent('ben', 'http://192.168.1.5:3002');

//or
const unsubscribe = await u3.unregisterEvent(‘ben’, ‘http://192.168.1.5:3002’);

监听

const { createU3, listener } = require('u3.js/src');
listener(function(data) {
// do callback logic
console.log(data);
});

U3Utils.test.wait(2000);

//must call listener function before emit event
const contract = await u3.contract(account);
contract.hi(‘ben’, 30, ‘It is a test’, { authorization: [`ben@active`] });


六、合约编写

Ultrain使用类JavaScript的语言来编写智能合约,这个类JavaScript的语言以TypeScript为原型,通过扩展的数据类型标志符,来达到强类型语言的编程语法。

系统内置的方法

function NAME(str: string): u64 : 方法**NAME()**用来将一个string转成一个account_name类型.str的字符长度不超过12个字符, 内容只能包括以下字符(不能以.结尾):.012345abcdefghijklmnopqrstuvwxyz

function RNAME(account: account_name): string : 方法**RNAME()用来将一个account_name类型转为string类型, 它是NAME()**方法的反向方法。

function ACTION(str: string): Action : 方法**ACTION()**将一个string类型转为Action类型,str的长度不超过21个字符, 内容只能包括以下字符(不能包含.):._0-9a-zA-Z. Action类封装了action相关的信息。

Action.sender : 当前transaction的发起者, account_name类型。

Action.receiver : 当前transaction的接收者, 即合约帐户, account_name类型。

Block.number : head block的块高。

Block.id : head block的id,sha256的hash值。

Block.timestamp : head block的时间戳,从EPOCH开始的秒数。

编写第一个合约Hello world

import { NAME, RNAME } from "ultrain-ts-lib/src/account";
import { Log } from "ultrain-ts-lib/src/log";
import { Contract } from "ultrain-ts-lib/lib/contract";

class HelloWorld extends Contract {

@action
hi(name: account_name, age: u32, msg: string): void {
Log.s(“hi: name = “).s(RNAME(name)).s(” age = “).i(age, 10).s(” msg = “).s(msg).flush();
}
}

我们以上代码做如下说明:

  1. import: 用来引入其它文件中定义的类和方法,详细用法可参考 typescript (https://www.tutorialspoint.com/typescript/index.htm)的说明。
  2. extends Contract: 合约都需要派生自Contract,而且一个项目中只能有一个Contract。
  3. @action: 申明一个合约方法。只有@action标志的方法,才能被调用。
  4. Log: 打印Log。需要在config.ini文件中配置 contracts-console = true 才能打印到终端。

在Action中Return信息

为了便于在调用方与节点中传递部分执行状态信息,引入Return模块,Return模块返回的数据会附加在http的response中,调用方可以通过分析response得到Return的信息。
需要强调的是,Return的信息仅仅是在一个节点(host_url )上预执行的结果,并非区块链网络共识的结果。也就是说, Return返回的结果, 并不是最终交易执行的结果。
Return的信息只供参考,它可能与区块链网络共识结果不一致。

要Return信息,可以在action调用中,通过Return,ReturnArray方法来完成。Return信息有以下需要注意的点:

NOTICE

  1. Return的message是有长度限制的,默认的message长度为128个character。(int型数据会转成对应的string)。如果是在侧链中使用,可以在config.ini文件中配置 contract-return-string-length 来扩展长度限制。
  2. 只支持Return基本数据类型int和string, 以及int[]和string[]。
  3. 可以调用Return或ReturnArray多次,信息将被concat。
  4. 超出长度限制的信息,会直接丢弃,不会抛出异常。
Return信息的示例

class HelloContract extends Contract{
@action
on_hi(name: u64, age: u32, msg: string): void {
Return<string>("call hi() succeed.");
ReturnArray<u8>([1,2,3]);
}
}

执行正常的情况下,Return的结果是call hi() succeed.123

资产查询和转移

在合约中,可以查询一个帐号在ultrainio.token合约中的资产,即ultrain平台资产。查询资产使用Asset.balanceOf(who: account_name): Asset方法。 转移ultrain平台资产,可以使用Asset.transfer(from: account_name, to: account_name, val: Asset, memo: string): void方法。

使用详情请参考示例balance

(https://github.com/ultrain-os/ultrain-ts-lib/blob/master/example/balance/balance.ts)。

import "allocator/arena";
import { Contract } from "ultrain-ts-lib/src/contract";
import { Asset } from "ultrain-ts-lib/src/asset";
import { ultrain_assert } from "ultrain-ts-lib/src/utils";
class BalanceContract extends Contract {

@action
transfer(from: account_name, to: account_name, bet: Asset): void {

let balance = Asset.balanceOf(from);
ultrain_assert(balance.gte(bet), “your balance is not enough.”);

balance.prints(“banalce from: “);

Asset.transfer(from, to, bet, “this is a transfer test”);
}
}

NOTICE 使用Asset.transfer命令转移资产时,需要保证from的权限已经授权给了utrio.code,在使用命令行的情况下,可以通过以下命令来授权:

clutrain set account permission $from active '{"threshold": 1, "keys":[{"key":"$PubKey_of_from", "wieght": 1}], "accounts": [{"permission": {"actor": "$from", "permission": "utrio.code"}, "weight": 1]}' owner -p $from

$from是需要授权的帐号。

持久化存储

Ultrain的智能合约提供了DBManager来存储合约数据到数据库中。不同于以太坊会自动保存数据,Ultrain需要明确的调用API来保存、读取数据。

Serializable接口

Serializable是一个Interface, 定义以下三个方法:

import {DataStream} from "ultrain-ts-lib/src/datastream";
export interface Serializable {
deserialize(ds: DataStream): void;
serialize(ds : DataStream) : void;
primaryKey(): u64;
}

deserialize(ds: DataStream): void;

  • 方法用来做反序列化工作,从DataStream的字节流中读取数据进行初始化工作。
  • serialize(ds: DataStream): void; 方法用来做序列化工作,将class的数据写入到字节流中。
  • primaryKey(): u64; 标志一个primary key。如果这个class将作为一条独立的记录写入数据库,那primaryKey()返回的数据将成为数据库中的primary key.

NOTICE

  1. 一个实现了ISerialzable接口的class,编译器将自动实现以上三个方法,并将class中的成员变量都序列化/反序列化。如果需要单独override某一个/全部方法,则可以手动实现对应的方法。
  2. 如果要排除某个成员变量,以避免序列化和反序列化,可以使用 @ignore 注解。
  3. 如果要指定某个成员变量为primaryKey,可以使用 @primaryid 注解。需要注意的是,被注解为@primaryid的变量必须是u64类型,如果没有变量被注解为@primaryid,则primaryKey()方法默认使用0 作为返回值。
  4. 如果使用了@注解,同时又override了serialize()、deserialize()、primaryKey()方法中的某一个(或全部),编译器将优先使用override的方法。

对于Serializable接口的使用,举例如下:

class Person implements Serializable{
name: string;
age: u32;
sex: string;
salary: u32;
@ignore
address: string; // 被忽略,不序列化和反序列化

constructor() {
this.name = “xx”;
//…
}
// 重写primaryKey()方法,返回Person的id
primaryKey(): u64 {
return NAME(this.name);
}
}

可序列化存储的数据

存储到数据库中的数据,必须是能够序列化和反序列化。可以序列化存储的数据有以下几类:

  1. 内置基本数据类型: u8/i8, u16/i16, u32/i32, u64/i64, boolean, string。 有一些类型其实也是基本数据类型的别名,如account_name。
  2. 基本数据类型的一维数组: u8[], i8[], …, string[]
  3. 实现了Serializable接口的类, 如上的Person。
  4. 实现了Serializable接口的类的一维数组,如Person[]。
声明合约中DB的table信息

如果合约中需要使用到DB进行数据存取,则需要在具体的Contract类中注解说明table的信息。 如下简单的一份伪代码:

class Person implements Serializable {
name: string;
sex: string;
}

class Car implements Serializable {
model: string;
power: u32;
color: string;
}

@database(Person, “persons”)
@database(Car, “cars”)
// @database() if any more
clas MyContract extends Contract {
//…
// your logic here
}

上述代码将会生成两张表格: “persons”和”cars”。 需要注意的是,@database注解中的Person和Car两个类,必须实现Serializable接口。

数据库读写

Contract中数据存取要通过DBManager来管理。

DBManager的定义:

export class DBManager<T extends Serializable> {
constructor(tblname: u64, owner: u64, scope: u64) {}
public cursor(): Cursor<T> {}
public emplace(payer: u64, obj: T): void {}
public modify(payer: u64, newobj: T): void {}
public exists(primary: u64): boolean {}
public get(primary: u64, out: T): boolean { }
public erase(obj: T): void {}
}

constructor()方法接收三个参数,

  • tblname: u64表示表名; owner:u64表示这个表在哪个合约中,一般的,owner和该合约的receiver是一样的。 scope: u64表示表中的一个上下文。
  • cursor()方法读取数据表中的所有记录。
  • emplace()方法向表中加入一条记录。 payer表示这个帐号将为数据存储付费, obj是一个Serializable的对象,将数据存入DB。
  • modify()方法更新表中的数据。 payer表示这条记录的创建者、付费方; newobj是更新后的数据,newobj的primaryKey对应的对象会被更新。
  • exists()方法判断一个primaryKey是否存在。
  • get()方法从DB中读取primary对应的记录,并反序列化到out中。
  • erase()方法用来删除一条记录,obj的primaryKey对应的记录如果存在,将被删掉。

NOTICE table没有方法可以显式删除,只有当table中的记录都删掉时,table会自动被删除。

使用Cursor遍历所有记录

我们提供了cursor来遍历所有的记录,但是必须明白,这个操作非常非常低效,因为在当调用cursor()方法时,会将所有的表中的数据都加载到内存里面。如果表中的数据很多的话,那这个交易将会被cursor方法阻塞,从而导致交易超时失败。 如下示例演示了怎样使用cursor:

let cursor = this.db.cursor();
Log.s("cursor.count =").i(cursor.count).flush();

while(cursor.hasNext()) {
let p: Person = cursor.get();
p.prints();
cursor.next();
}

table里面scope和primary key的关系

table中的数据,可以按scope来分类,也可以通过primary key来分类。尽管它们都可以达到分类数据的效果,但是在table中,scope和primary key是两个不同的维度,它们之间的关系,大概可用下面的结构来表示:

|--table
|----scope1
|--------primaryKey_1
|--------primaryKey_2
|--------........
|----scope2
|--------primaryKey_x
|--------primaryKey_y
|--------.......

在不同的scope下面,primary key可以取相同的值。

使用示例

DB的读写操作,请参考示例Person

(https://github.com/ultrain-os/ultrain-ts-lib/blob/master/example/person/person.ts)。

import "allocator/arena";
import { Contract } from "ultrain-ts-lib/src/contract";
import { Log } from "ultrain-ts-lib/src/log";
import { ultrain_assert } from "ultrain-ts-lib/src/utils";
import { DBManager } from "ultrain-ts-lib/src/dbmanager";
import { NAME } from "ultrain-ts-lib/src/account";

class Person implements Serializable {
// name: string;
name: string
age: u32;
salary: u32;

primaryKey(): u64 { return NAME(this.name); }

prints(): void {
Log.s(“name = “).s(this.name).s(“, age = “).i(this.age).s(“, salary = “).i(this.salary).flush();
}
}

const tblname = “humans”;
const scope = “dept.sales”;

@database(Person, “humans”)
// @database(SomeMoreRecordStruct, “other_table”)
class PersonContract extends Contract {

db: DBManager<Person>;

public onInit(): void {
this.db = new DBManager<Person>(NAME(tblname), this.receiver, NAME(scope));
}

public onStop(): void {

}

constructor(code: u64) {
super(code);
this._receiver = code;

this.onInit();
}

@action
add(name: string, age: u32, salary: u32): void {
let p = new Person();
p.name = name;
p.age = age;
p.salary = salary;

let existing = this.db.exists(NAME(name));
ultrain_assert(!existing, “this person has existed in db yet.”);
p.prints();
this.db.emplace(this.receiver, p);
}

@action
modify(name: string, salary: u32): void {
let p = new Person();
let existing = this.db.get(NAME(name), p);
ultrain_assert(existing, “the person does not exist.”);

p.salary = salary;

this.db.modify(this.receiver, p);
}

@action
remove(name: string): void {
Log.s(“start to remove: “).s(name).flush();
this.db.erase(NAME(name));
}
}


七、实战演示

本指南以一个简单的网络投票系统vote为例,详细讲述完整DApp的开发流程。

  • 初始化本地开发环境

首先我们根据环境搭建一章的内容,在本机上安装并成功启动Longclaw。

  • 初始化项目结构

当掌握了robin工具的使用方法之后,我们在任意目录下新建一个空目录vote,然后进到vote下,执行

robin init -c Vote

此时出现模板选择界面,由于网络投票需要面向的是普通用户,所以提供一个友好的交互界面是非常有必要的,因此这里选择一个开发者熟悉的前端框架,本文示例代码使用的是vue-boilerplate。

  • 修改上链配置信息

Robin生成的默认与链交互的U3.js配置信息在根目录下的config.js文件中。如果你是基于Longclaw构建的本地开发环境,那么你不需要做任何修改。如果你要基于线上测试网环境做开发,那么你需要修改为如下配置:

const config = {
httpEndpoint: 'http://benyasin.s1.natapp.cc',
httpEndpoint_history: 'http://history.natapp1.cc',
broadcast: true,
debug: false,
verbose: false,
sign: true,
logger: {
log: console.log,
error: console.error,
debug: console.log
},
chainId: '262ba309c51d91e8c13a7b4bb1b8d25906135317b09805f61fcdf4e044cd71e8',
keyProvider: '改为你申请的测试网账号的私钥',
binaryen: require('binaryen')
};
module.exports = config;

而对应的账号与可用资源需要你在测试网络申请,相关操作可以参照测试网开发配置指南

(https://developer.ultrain.io/tutorial/testnet_guide)。

  • 编写智能合约

vote的核心逻辑是要实现所有人的公开网络投票。我们采用面向对象的设计思想进行设计,vote涉及的对象有选民、选票,候选人三个。只有管理员才有添加候选人的权限,候选人和选票信息只能添加不能修改。任何人都可以公开投票,每个选民只有投一票的权限,且必须投给一个有效的候选人。

根据上述分析,我们来定义三个Class,同时指定三组表空间与scope空间。

import { Contract } from "ultrain-ts-lib/src/contract";
import { RNAME, NAME } from "ultrain-ts-lib/src/account";
import { Action } from "ultrain-ts-lib/src/action";
import { account_name } from "../../../ultrain-ts-lib/internal/alias";

class Votes implements Serializable {
@primaryid
name: account_name = 0;
count: u32 = 0;
}

class Voters implements Serializable {
@primaryid
name: account_name = 0;
}

class Candidate implements Serializable {
@primaryid
name: account_name = 0;
}

const votestable = “votes”;
const votesscope = “s.votes”;

const canditable = “candidate”;
const candiscope = “s.candidate”;

const voterstable = “voters”;
const votersscope = “s.voters”;

@database(Votes, votestable)
@database(Voters, voterstable)
@database(Candidate, canditable)
class VoteContract extends Contract {

接下来,根据上述合约编写的教程,我们知道需要定义DBManager来存取数据库对象。

candidateDB: DBManager<Candidate>;
votesDB: DBManager<Votes>;
votersDB: DBManager<Voters>;

constructor(code: u64) {
super(code);
this.candidateDB = new DBManager<Candidate>(NAME(canditable), this.receiver, NAME(candiscope));
this.votesDB = new DBManager<Votes>(NAME(votestable), this.receiver, NAME(votesscope));
this.votersDB = new DBManager<Voters>(NAME(voterstable), this.receiver, NAME(votersscope));
}

然后我们来定义添加候选人的方法,这个方法中首先要检查调用者的权限,必须是合约的Owner。候选人以name为唯一性验证,不允许重复添加。最后不要忘了,添加@action注解,才能将这个方法暴露给外部调用。

@action
addCandidate(candidate: account_name): void {
ultrain_assert(Action.sender == this.receiver, "only contract owner can add candidates.");

let c = new Candidate();
c.name = candidate;
let existing = this.candidateDB.exists(candidate);
if (!existing) {
this.candidateDB.emplace(this.receiver, c);
} else {
ultrain_assert(false, “you also add this account as candidate.”);
}
}

最后,我们来定义核心投票的方法。首先要检查权限,如果调用者已经投过票,则拒绝再次投票,如果传进来候选人不在管理员添加的候选人列表中,则认为是无效的候选人,也要拒绝其投票。由于每个Votes有一个记录候选人的选票的count,所以当某候选人的选票为第一票时,直接设count为1,插入新记录,当选票不是第一票时,则要取出原count,加1后,再修改该条记录。

@action
vote(candidate: account_name): void {
ultrain_assert(this.votersDB.exists(Action.sender) == false, "you have voted.");
ultrain_assert(this.candidateDB.exists(candidate) == true, "you should vote a valid candidate.");

let votes = new Votes();
votes.name = candidate;
let existing = this.votesDB.get(candidate, votes);
if (existing) {
votes.count += 1;
this.votesDB.modify(this.receiver, votes);
} else {
votes.count = 1;
this.votesDB.emplace(this.receiver, votes);
}

let voters = new Voters();
voters.name = Action.sender;
this.votersDB.emplace(this.receiver, voters);
}

好了,以上就是合约的全部源代码。

  • 语法合法性检查

我们先通过robin lint命令检查一下所写代码的语法合法性,如果有错误,就根据错误提示进行修复。

  • 编译并部署合约

如果语法没有错误,那么接下来要做的就是编译合约并部署上链。

通过robin build命令将源代码编译成目标文件,再通过robin deploy命令将目标文件部署到链上。以下是成功部署后的界面:

保姆级DApp开发指南—— 开发DApp原来如此简单

  • 测试业务逻辑正确性

接下来,我们要编写测试用例测试合约业务逻辑的正确性。

测试用例定义在Vote.spec.js文件中。由于合约的owner是ben,所以第一个用例就是用ben来添加几个候选人。

const U3Utils = require("u3-utils/src");
const { createU3, format } = require("u3.js/src");
const config = require("../config");

const chai = require(“chai”);
require(“chai”)
.use(require(“chai-as-promised”))
.should();

describe(“Tests”, function() {

let creator = “ben”;

it(“candidates”, async () => {
const u3 = createU3(config);
await u3.transaction(creator, c => {
c.addCandidate(“trump”, { authorization: [`ben@active`] });
c.addCandidate(“hillary”, { authorization: [`ben@active`] });
c.addCandidate(“obama”, { authorization: [`ben@active`] });
});

U3Utils.test.wait(3000);

const canditable = “candidate”;
const candiscope = “s.candidate”;
let candidates = await u3.getTableRecords({
“json”: true,
“code”: creator,
“scope”: candiscope,
“table”: canditable
});
candidates.rows.length.should.equal(3);
});

如果你使用的WebStorm,则可以定位到candidates这个用例中,直接右键选择Run或Debug ‘candidates’,如果是其它编辑器,也可以在命令行中,执行

mocha test/Vote.spec.js -g candidates

当然前提是要全局安装过mocha,否则你需要先执行

npm install -g mocha

有了候选人后,我们就可以模拟bob来投给hillary一票了。

it("bob-voting-hillary", async () => {
config.keyProvider = "5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt";
const u3 = createU3(config);

const votingtable = “votes”;
const votingscope = “s.votes”;
await u3.getTableRecords({
“json”: true,
“code”: creator,
“scope”: votingscope,
“table”: votingtable
});

let contract = await u3.contract(creator);
await contract.vote(“hillary”, { authorization: [`bob@active`] });

U3Utils.test.wait(3000);

await u3.getTableRecords({
“json”: true,
“code”: creator,
“scope”: votingscope,
“table”: votingtable
});
});

更多测试用例,不在这里一一赘述,你可以直接查看源代码。

  • 前端UI集成

以上是编写智能合约的流程,但作为一个完整的dapp,少不了一个交互友好的用户界面。因此接下来的内容将教你如何将智能合约与前端框架进行集成。

在robin init时,我们已经为vote项目选择过了vue-boilerplate模板,如果一开始选用的纯合约模板,在这一步也可以使用robin ui命令再次将合约项目升级为带界面的dapp项目。

总的来说,我们的界面上需要一个表格来实时的展示每个候选人及其选票数,另外需要一个表单来提交投票信息。接下来,定义一个Voting.vue组件。

在mounted阶段,我们需要将候选人列表与选票列表从数据库中查出来,并双向绑定到DOM上。

注意,我们通过在vue的dat中定义某个状态值,来控制只有选择了某个候选人之后才出现可点击的投票按钮,同时,限制在异步方法的等待过程中,投票按钮是禁用的以防止重复点击。

<script>
const { createU3 } = require("u3.js/src");
const config = require("../../config");
export default {
name: "Voting",
data() {
return {
candidate: "",
voteFormShow: false,
votes: [],
candidates: [],
voterName: "",
privateKey: "",
showLoading: false
};
},
async mounted() {
let account = "ben";
const u3 = createU3(config);
const canditable = "candidate";
const candiscope = "s.candidate";
let candidates = await u3.getTableRecords({
"json": true,
"code": account,
"scope": candiscope,
"table": canditable
});
this.candidates = candidates.rows;

const votestable = “votes”;
const votesscope = “s.votes”;
let votes = await u3.getTableRecords({
“json”: true,
“code”: account,
“scope”: votesscope,
“table”: votestable
});
this.votes = votes.rows.sort(this.compare);
},
methods: {

模板部分的代码如下:

<template>
<div class="main">
<h1>Voting Result</h1>
<table border="1">
<thead>
<tr>
<th>NO.</th>
<th>Candidate</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<tr v-bind:key="v.name" v-for="(v,index) in votes">
<td>{{index+1}}</td>
<td>{{v.name}}</td>
<td>{{v.count}}</td>
</tr>
</tbody>
</table>
<h4>All candidates</h4>
<div class="form-inline">
<select v-model="candidate">
<option value="">Choose a candidate</option>
<option v-bind:key="c.name" v-for="c in candidates">{{c.name}}</option>
</select>
</div>

<div class=”go-to-vote” @click=”goToVote()”>Start voting</div>
<div class=”vote-form” v-show=”voteFormShow”>
<div class=”form-inline”>
<label>Voter</label><input v-model=”voterName”/>
</div>
<div class=”form-inline”>
<label>PrivateKey</label><input v-model=”privateKey”/>
</div>
<button :disabled=”showLoading” v-show=”candidate” class=”vote-btn” @click=”vote()”>{{
showLoading?”waiting…”:”Send votes” }}
</button>
</div>
</div>
</template>

投票逻辑是合约方法的调用,所有的合约方法调用都是异步接口,而且投票方法是需要签名的。从产生交易Hash后到链上确认是需要10秒以上的,具体多久取决于你的网络拥堵情况。默认的交易过期时间是一分钟,所以如果一分钟后还得不到确认,则可以认为交易失败了。因此我们做一个轮询来处理交易确认后的返回逻辑。

async vote() {
if (this.candidate) {
let creator = "ben";
config.keyProvider = this.privateKey;
const u3 = createU3(config);
let contract = await u3.contract(creator);
let tx = await contract.vote(this.candidate, { authorization: this.voterName + "@active" });
this.showLoading = true;

//等待最长一分钟,来确认交易的最终打包结果
let tx_trace = await u3.getTxByTxId(tx.transaction_id);
let time = 0;
let timer = setInterval(async () => {
time++;
if (time >= 60) {
clearInterval(timer);
return;
}
tx_trace = await u3.getTxByTxId(tx.transaction_id);
if (tx_trace.irreversible) {
this.showLoading = false;
alert(“Voted success”);
clearInterval(timer);
document.location.reload();
} else {
// eslint-disable-next-line
console.log(“waiting ” + time + “s”);
}
}, 1000);
}
}

下图展示的是一次投票后等待的过程:

保姆级DApp开发指南—— 开发DApp原来如此简单

通过以上指南,我们阐述了开发一个DApp的完整过程。当然也略过了一些较复杂或用得较少的功能,比如事件机制等。

如果任何疑问,欢迎给我们提意见,也可以在本项目的代码库

(https://github.com/benyasin/dapp-tutorial)提issue。


本文来自Ultrain,经授权后发布,本文观点不代表DAppChaser立场,转载请联系原作者。

Lucas进行回复 取消回复

邮箱地址不会被公开。 必填项已用*标注

评论列表(1条)

  • Lucas 2019年3月5日 20:12

    作为非技术,只能膜拜这种内容了><

联系我们

邮件:contact@dappchaser.com

QR code