本篇教程将指导开发者基于一个真实的商业场景,采用Ultrain的Robin Framework,编写一个DApp,在这个过程中您可以深入了解Ultrain在DApp开发方面的技术特点,快速掌握如何编写一个DApp,您会发现基于Robin Framework编写DApp是一件非常轻松的事情。本教程的所有源代码都可以在github中找到:https://github.com/ultrain-os/UltrainDappDemo

本教程属于中级教程,通过本教程,你可以学习DApp开发的基本概念,Ultrain Database和触发器的用法;

前提假设:

  1. 您已经学习过“快速编写XXXXX”教程,并通过该教程的指引,已经安装了Ultrain Robin开发环境,本地测试环境LongClaw;

系统要求:

  1. Mac操作系统(最佳体验)或Linux、Windows系统;
  2. IDE:WebStorm
  3. Docker

业务场景介绍:

NewEnergy公司生产的新能源设备,通过使用新的清洁能源,可以极大的降低碳排放,NewEnergy公司希望将其降低的碳排放量转换为相应的碳币,一方面使用NewEnergy公司设备的公司,通过该碳币可以获取额外收益;另一方面,碳排放较多的公司,可以通过购买碳币,为减排提供资金支持,提升企业在碳排放方面的公益形象;所以,该场景中存在如下的角色:

  • NewEnergy公司:碳币的运营方,创造并发行碳币,NewEnergy使用的Ultrain账户为ben;
  • B公司:使用NewEnergy公司生产的新能源设备B,在使用设备的过程中,其消耗的热量值每30秒会纪录在区块链上,并由该值转换为碳币发放到B公司的Ultrain账户,tom;
  • C公司:航空公司,化石能源使用大户,通过购买并消耗碳币,提升企业公益形象;C公司使用的Ultrain账户为tony
  • D公司:汽车制造公司,化石能源使用大户,通过购买并消耗碳币,提升企业公益形象;D公司使用的Ultrain账户为jerry
  • Ultrain账户jack,为兑换公益积分时,CarbonCoin燃烧掉打入的地址,打入该地址的CarbonCoin将不能再被使用;

程序架构介绍:

0_1541589204993_a8207102-77c7-45e5-b11b-62697930e625-image.png

如上图所示,系统由4个程序构成:

  1. Energy智能合约:运行在Ultrain链上的智能合约,基于TypeScript开发;
  2. MiningDApp:采集节能量数据的终端DApp,基于U3框架开发;
  3. UDApp:兑换公益积分DApp,基于U3框架开发
  4. EnergyServer:监听并执行兑换的服务器程序,基于NodeJS框架开发;

Step1: Energy智能合约开发

1. 建立项目目录,初始化项目环境

mkdir CarbonProject
cd CarbonProject
robin init

2. 使用Javascript开发IDE WebStorm,打开项目CarbonProject

0_1541589483260_80821be6-aa38-4c59-bf72-07db56d79518-image.png
简单介绍一下项目中各目录的功能:

contract : 项目的智能合约目录,该目录下放置项目的主智能合约,每个项目中只有一个智能合约被编译,就是该目录下的智能合约;同时,Ultrain现在每个账户只支持部署一个智能合约;
migrations:智能合约部署参数的配置文件
node_modules:项目依赖的JS类库
template:用于示例的智能合约代码
test: 测试代码目录,测试代码都是javascript代码;
config.js:U3框架访问Ultrain链时,需要配置的默认参数;这些参数也可以在U3框架创建时动态赋值;

3. 编写Energy智能合约

a. 删除contract目录下的MyContract.ts文件,将template/token/token.ts文件拷贝到contract目录下,修改文件名为 energy.ts;修改类名为 energy; 需要注意的是:发行代币的合约需要遵循UIP的规范,本例中需要实现UIP06的接口,相关规范文档请参考:https://developer.ultrain.io/tutorial/UIP_Introduce.
b.编写调用energy.ts,发行CarbonCoin的代码。

  • 在test目录下,建立一个新的javascript文件,命名为issueCarbonCoin.js
    In the test directory, create a new javascript file named issueCarbonCoin.js
  • 建立代码框架
const U3Utils = require('u3-utils/dist/es5');
const { createU3, format } = require('u3.js/src');
const config = require('../config');

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

const should = chai.should();
const expect = chai.expect;
const assert = chai.assert;

describe('Contract\'s test cases', function() {

});
  • 在describe中编写发行CarbonCoin核心逻辑代码
it('create and issue a token', async () => {
    let SYMBOL = 'CARB';
    const u3 = createU3(config);
    let account = 'ben';
    const tx = await u3.transaction(account, token => {
        token.create(account, '10000000.0000 ' + SYMBOL);
        token.issue(account, '10000000.0000 ' + SYMBOL, 'issue');
    });
});

简单解析上述代码,我们设定了发行的Token的代码为CARB,发行的账户为ben,发行了1000万枚,并将所有Token存储到发行账户ben中。

  • 由于智能合约代码是异步执行的,我们需要在await u3.transaction 代码执行后,采用下述代码,等待发行Token逻辑被Ultrain链确认,才能真正确认代码执行成功
//wait util it was packed in a block
let tx_trace = await u3.getTxByTxId(tx.transaction_id);
while (!tx_trace.irreversible) {
    await U3Utils.wait(1000);
    tx_trace = await u3.getTxByTxId(tx.transaction_id);
    if (tx_trace.irreversible) {
        console.log(tx);
        break;
    }
}

以上,发行Token代码编写完成。

4. 编译、部署并运行Energy智能合约

a. 确认LongClaw已经启动;参见之前教程XXXXXX;
b. 修改migrate.js

u3.deploy('build/MyContract', 'ben')中,MyContract修改为energy
at u3.deploy('build/MyContract', 'ben') , MyContract changed to energy;
通过查看config.js可以发现,我们通过“ben”账户的私钥,将该智能合约部署到httpEndpoint指定的地址下的chainId指定的链上;

c. 进入IDE的terminal界面,输入命令

robin build
robin deploy

出现如下显示,并没有错误提示,表示部署成功
0_1541589822287_fd9f17a6-6eee-4a1c-b433-490392946a47-image.png
d. 运行issueCarbonCoin.js
0_1541590045095_c0530a28-48b0-4ebb-8a77-6f3e2bcdb56e-image.png
出现以上信息,说明CarbonCoin发行成功;

5.为 energy智能合约添加更多功能

5.1 查询账户

在getBalance方法前添加@action
@action方法让getBalance方法可以被外部调用访问;
该方法可以查询指定账户拥有的指定Token的数量

5.2 记录设备发送的热量值

新能源设备会将其产生的热量值发送到链上,智能合约将该数据保存到链上的数据库中,同时,智能合约向注册的服务器地址发送热量值产生的事件通知;

在本节,我们将学习如何操作链上数据;如何发送事件通知;
a. energy.ts中,引入新的申明
引入代码

import { Block } from 'ultrain-ts-lib/src/block';
import { NAME,RNAME } from 'ultrain-ts-lib/src/account';
import "allocator/arena";
import { Log } from "ultrain-ts-lib/src/log";

修改之前引入代码

import { TransferParams, dispatchInline} from 'ultrain-ts-lib/src/action'; 为
import { TransferParams, dispatchInline,Action } from 'ultrain-ts-lib/src/action';

b. 建立HeatRecord类,用于记录热量值

class HeatRecord implements Serializable {
    miner: string;
    timestamp: u64;
    heatValue: u64;
    primaryKey(): u64 { return NAME(this.miner) + this.timestamp; }

    prints(): void {
        Log.s("name = ").s(this.miner).s(", timestamp = ").i(this.timestamp).s(", heatValue = ").i(this.heatValue).flush();
    }
}

c. 在export class CarbonToken extends Contract { 前添加数据库操作申明

const tblname = "heatvalue";
const scope = "carbon.heat";

@database(HeatRecord, tblname)

d. 在class CarbonToken中添加数据库操作代码

db: DBManager<HeatRecord>;
public onInit(): void {
    this.db = new DBManager<HeatRecord>(NAME(tblname), this.receiver, NAME(scope));
}
public onStop(): void {
}
constructor(code: u64) {
    super(code);
    this._receiver = code;
    this.onInit();
}

e. 在class CarbonToken中添加记录热量值的核心代码

@action
public recordHeat(quantity:u64):void{

    let r = new HeatRecord();
    r.miner = RNAME(Action.sender);
    r.timestamp = Block.timestamp;
    r.heatValue = quantity;

    let existing = this.db.exists(Action.sender+r.timestamp);
    ultrain_assert(!existing, "this record has existed in db yet.");
    r.prints();
    this.db.emplace(this.receiver,r);

    let value = r.heatValue+","+r.miner;
    emit("onHeatInvoked", EventObject.setString("heat",value));
}

热量值记录主要需要记录三个数据,发送数据的设备账号,发送的热量值以及发送时间;针对设备账号,DApp客户端调用智能合约的方法时,需要用调用者的私钥进行签名,所以智能合约可以知道是谁调用的本方法,同时能保证系统的安全;我们使用U3框架提供的Action.sender方法得到调用者账号,由于该账号是一个转移后的u64类型字段,我们用RNAME将其转义为string类型,即账号名;

针对时间戳,由于区块链技术的特点,我们取上个区块的时间戳作为记录;

我们将 账号名+时间戳 作为记录的主键;

f. 触发onHeatInvoked事件,我们通过该事件告知运营方的服务器程序,有设备上传新的热量值记录了;

引入类:

import { EventObject, emit } from 'ultrain-ts-lib/src/events';

在recordHeat方法中添加触发器代码

emit("onHeatInvoked", EventObject.setString("heat",value));
5.3 热量值兑换CarbonCoin

在energy.ts中添加如下代码:

@action
public exchangeCarbonCoin(from: account_name, to: account_name, quantity: Asset,memo:string): void {
    let carbonToken:Asset = quantity.divide(10);
    this.transfer(from,to,carbonToken,memo);
}

其中quantity.divide(10);是一个简单的热量值兑换CarbonCoin的计算公式,可以发现,该公式是记录在区块链中,对所有人都是公开和透明的,并且不能篡改,保证兑换的公平性;

然后我们调用transfer方法,由ben将兑换的CarbonCoin发给相应的账户;

5.4 燃烧CarbonCoin兑换公益积分

添加公益积分对象;

class ScoreRecord implements Serializable {
    name: string;
    score:u64;
    primaryKey(): u64 {return NAME(this.name)};
    prints():void{
        Log.s("name = ").s(this.name).s(",score = ").i(this.score);
    }
}

在energy.ts前配置公益积分对象的数据库参数

const tblname_s = "score";
@database(ScoreRecord, tblname_s)

在energy.ts内添加数据库操作代码

db_s: DBManager<ScoreRecord>;

public onInit(): void {
    this.db_s = new DBManager<ScoreRecord>(NAME(tblname_s), this.receiver, NAME(scope));
}

在energy.ts内添加核心业务逻辑代码

@action
public exchangeScore(from: account_name,to : account_name, quantity: Asset,memo:string): void {

    let s = new ScoreRecord();

    let existing = this.db_s.exists(from);
    if (existing){
        this.db_s.get(from,s);
        s.score = s.score + quantity.amount;
        this.db_s.modify(this.receiver,s);
        Log.s("thi is a edit obj");
    }else{
        s.score = quantity.amount;
        s.name = RNAME(from);
        this.db_s.emplace(this.receiver, s);
        Log.s("thi is a new obj");
    }

    this.transfer(from,to,quantity,memo);

}

对积分分为两步操作,首先我们定义CarbonCoin兑换公益积分的比例是1:1,我们将兑换到的积分累加起来,用兑换人name作为主键保存到链上数据库;然后将兑换后应该消耗掉的CarbonCoin转移到一个账户“jack”,用于销毁;

6. 更新energy.ts智能合约

至此,我们完成了智能合约的编写工作,再次执行命令完成对智能合约的编译和部署
robin build
robin deploy

Step2:DApp编写

本教程有两个DApp需要编写,一个是MiningDApp,一个是UDApp,我们采用基于Javascript语言的U3框架编写DApp,为简化,我们将这两个DApp包含在一个Project中;

1. 建立DApp项目

在WebStorm中,新建项目->Node.js Express App->Template选择Pug(Jade);项目名称修改为CarbonDApp;点击‘Create’创建项目
0_1541590414902_6f675a81-1b07-4084-bc40-686d7f5bd9c0-image.png

2. 导入项目依赖类库

将CarbonProject中node_modules中所有目录拷贝到CarbonDApp的node_modules目录下,重复目录跳过不用拷贝;

3. 修改DApp服务器端口

由于默认的3000端口已经被占用,需要修改DApp的服务器端口;
打开bin/www 文件,找到var port = normalizePort(process.env.PORT || '3000');将3000修改为3001

4. 建立账户信息浏览页面

在public目录下建立index.html页面,添加如下代码到页面:

<html>

<head>
  <meta charset="UTF-8">
  <title>test</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
  <script src="./javascripts/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:'2616bfbc21e11d60d10cb798f00893c2befba10e2338b7277bb3865d2e658f58',
          symbol: 'UGAS'
      });


      async function getBalanceInfo() {
          let SYMBOL = 'CARB';
          let account = 'ben';

          const ben_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: account
          });

          document.getElementById("carbon_balance").innerHTML = '账户余额:'+ben_balance;

          const bob_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: 'bob'
          });

          document.getElementById("bob_balance").innerHTML = '账户余额:'+ bob_balance;

          const tom_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: 'tom'
          });

          document.getElementById("tom_balance").innerHTML = '账户余额:'+tom_balance;

          const tony_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: 'tony'
          });

          document.getElementById("tony_balance").innerHTML = '账户余额:'+tom_balance;

          const jerry_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: 'jerry'
          });

          document.getElementById("jerry_balance").innerHTML = '账户余额:'+tom_balance;

          const jack_balance = await u3.getCurrencyBalance({
              code:'ben',
              symbol: SYMBOL,
              account: 'jack'
          });

          document.getElementById("jack_balance").innerHTML = '账户余额:'+tom_balance;
      }

      getBalanceInfo();
      //transfer();
  </script>
</head>

<body>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>CarbonCoin发行方</h1>
        <p>账户:ben</p>
        <p>描述:进行CarbonCoin发行的发行方,持有所有未分配的CarbonCoin</p>
        <p id="carbon_balance"></p>

    </div>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>节能设备1</h1>
        <p>账户:bob</p>
        <p>描述:通过节能产生CarbonCoin的节能设备1</p>
        <p id="bob_balance"></p>
    </div>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>节能设备2</h1>
        <p>账户:tom</p>
        <p>描述:通过节能产生CarbonCoin的节能设备2</p>
        <p id="tom_balance"></p>
    </div>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>航空公司C</h1>
        <p>账户:tony</p>
        <p>描述:大型航空公司,化石能源使用大户</p>
        <p id="tony_balance"></p>

    </div>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>汽车制造商D</h1>
        <p>账户:jerry</p>
        <p>描述:大型汽车制造商,化石能源使用大户</p>
        <p id="jerry_balance"></p>

    </div>

    <div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
        <h1>CarbonCoin销毁账户</h1>
        <p>账户:jack</p>
        <p>描述:用于CarbonCoin销毁的账户,持有所有销毁的CarbonCoin</p>
        <p id="jack_balance"></p>

    </div>

</body>

</html>

5.启动DApp服务,测试页面

在npm界面,双击start按钮

0_1541590527389_eb30fbdd-9a06-4871-9545-330f571a0774-image.png

服务器启动成功后,访问http://127.0.0.1:3001/index.html,出现如下页面:
可以看到,发行方ben账户中持有发行的所有CARB Token 1000万枚;其他账户还没有持有CARB Token;

0_1541590587092_bca3f250-044c-4da7-8f0f-fff1dddc7f29-image.png

5. 建立mining.html页面

mining.html页面,节能设备1和节能设备2每30秒将前30秒产生的热量值发送给energy智能合约,兑换CarbonCoin;
在public目录下建立mining.html页面,输入如下代码:

<html>

<head>
    <meta charset="UTF-8">
    <title>test</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
    <script src="./javascripts/u3.js"></script>
    <script>

        let u3_bob = 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:'2616bfbc21e11d60d10cb798f00893c2befba10e2338b7277bb3865d2e658f58',
            keyProvider: '5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt',//bob's private_key
            symbol: 'CARB'
        });

        let u3_tom = 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:'2616bfbc21e11d60d10cb798f00893c2befba10e2338b7277bb3865d2e658f58',
            keyProvider: '5KXYYEWSFRWfNVrMPaVcxiRTjD9PzHjBSzxhA9MeQKHPMxWP8kU',//tom's private_key
            symbol: 'CARB'
        });

        function wait(ms = 0) {
            return new Promise(res => setTimeout(res, ms));
        }

        function randomFrom(lowerValue,upperValue)
        {
            return Math.floor(Math.random() * (upperValue - lowerValue + 1) + lowerValue);
        }

        async function mining() {

            let owner_account = 'ben';

            const tr_bob = await u3_bob.contract(owner_account);
            const tr_tom = await u3_tom.contract(owner_account);

            // let amount = U3.format.UDecimalPad(2000,4);
            let heat_bob = randomFrom(15000,20000);
            let heat_tom = randomFrom(15000,20000);
            await tr_bob.recordHeat(heat_bob,{ authorization: [`bob@active`] });
            await tr_tom.recordHeat(heat_tom,{ authorization: [`tom@active`] });

            var d=new Date();
            var t=d.toLocaleTimeString();

            document.getElementById("bob_log").innerHTML = 'time :'+t+', heat value :'+ heat_bob;

            document.getElementById("tom_log").innerHTML = 'time :'+t+', heat value :'+ heat_tom;
        }

        mining();
        var int=self.setInterval("mining()",30*1000);
    </script>
</head>

<body>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>节能设备1</h1>
    <p>账户:bob</p>
    <p>描述:通过节能产生CarbonCoin的节能设备1</p>
    <p id="bob_log"></p>
</div>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>节能设备2</h1>
    <p>账户:tom</p>
    <p>描述:通过节能产生CarbonCoin的节能设备2</p>
    <p id="tom_log"></p>
</div>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <button onclick="int=window.clearInterval(int)">stop</button>
</div>

</body>

</html>

访问http://127.0.0.1:3001/mining.html,出现如下页面:
0_1541590651229_36d0df6d-2c55-4377-bced-0469502b2e0b-image.png

这里需要注意两点,

  • 给energy智能合约发送数据时,需要持有该账户的私钥才可以签名并发送,这里为了简化编程将私钥直接写到了网页上,实际程序中需要对私钥进行保护和隐藏;
  • 为简化编写,bob和tom的DApp程序直接写到了一个页面中,实际应该是两个DApp程序实例,分别运行中两个设备上;

进展到这里,我们的节能设备已经可以发送数据给智能合约了,而energy智能合约接受到数据后,会将数据传递给运营商服务器,由运营商服务器再次调用energy智能合约,兑换carboncoin,为此,我们需要建立EnergyServer;

Step3: EnergyServer

1. 建立事件监听并处理事件

本节,我们来处理上面的热量值数据上传事件

1.1 建立监听事件服务器代码

在CarbonDApp项目中bin目录下,新建一个文件msgBroker.js,输入如下代码:

const { createU3 , format} = require('u3.js/src');

const http = require('http');
const port = 3002;

let 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:'2616bfbc21e11d60d10cb798f00893c2befba10e2338b7277bb3865d2e658f58',
    keyProvider:'5JbedY3jGfNK7HcLXcqGqSYrmX2n8wQWqZAuq6K7Gcf4Dj62UfL',//ben's private key
    symbol: 'UGAS'
});


let server = http.createServer((request, response) => {

    const { headers, method, url } = request;
    console.log(method);
    console.log(url);
    let body = [];
    request.on('error', (err) => {
        console.error(err);
    }).on('data', (chunk) => {
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();

        console.log("received msg:", body);

        if (body != null && body.trim()!="") {

            var obj = JSON.parse(body);

            let heatValue = JSON.parse(obj[1]).heat.split(",")[0];
            let account = JSON.parse(obj[1]).heat.split(",")[1];

            exchangeCarbonCoin(account,heatValue);
        }


        response.on('error', (err) => {
            console.error(err);
        });

        response.statusCode = 200;
        response.setHeader('Content-Type', 'application/json');
        response.write("ok");
        response.end();

    });

});

server.keepAliveTimeout = 0;
server.timeout = 0;

server.listen(port, function () {
    console.log((new Date()) + " Server is listening on port " + port);
});

function wait(ms = 0) {
    return new Promise(res => setTimeout(res, ms));
}

async function exchangeCarbonCoin(account,heat) {
    let SYMBOL = 'CARB';
    let code = 'ben';

    const tr = await u3.contract(code);

    let heatValue = format.DecimalPad(heat,4);

    const result = await tr.exchangeCarbonCoin('ben', account, heatValue+' ' + SYMBOL, 'test', { authorization: [`ben@active`] });

    let tx = await u3.getTxByTxId(result.transaction_id);
    while (!tx.irreversible) {
        await wait(1000);
        tx = await u3.getTxByTxId(result.transaction_id);
        if (tx.irreversible) {
            console.log(tx);
            break;
        }
    }
}

该代码在3002端口建立了一个服务,在节能设备上传其热量值后,智能合约会给该服务发送哪台节能设备上传了热量值数据,该服务调用energy智能合约的exchangeCarbonCoin方法进行CarbonCoin兑换。

1.2 建立监听事件服务器启动脚本

打开CarbonDApp/package.json文件,在scripts属性中添加如下代码:"event": "node ./bin/msgBroker"

1.3 启动监听服务器

刷新npm窗口,出现‘event’选项
双击event,完成服务器启动
0_1541590746148_9916a209-c517-447f-aca9-faec2536f94b-image.png

0_1541590782407_f774f5ce-6fb8-4a80-8b0a-8b4d110445c5-image.png

1.4 在链上注册事件监听机制

通过ifconfig命令,查询本机内网地址,比如:10.20.8.32
在CarbonProject/test目录下,建立registerEvent.js文件;在文件中输入如下代码:

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

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

const should = chai.should();
const expect = chai.expect;
const assert = chai.assert;

describe('Test cases', function() {

    it('event register', async () => {

        let account = 'ben';
        const u3 = createU3(config);
        await u3.registerEvent(account, 'http:// 10.20.8.32:3002');

        U3Utils.wait(1000);

    });


    it('event unregister', async () => {

        let account = 'ben';
        const u3 = createU3(config);
        await u3.unregisterEvent(account, ' http:// 10.20.8.32:3002');

        U3Utils.wait(1000);
    });
});

注意修改注册地址为本机的内网地址,并且不能使用127.0.0.1。
运行event register测试用例,完成事件注册;

0_1541590854785_4f9481ac-04bb-4b47-a6c7-38b5b68c21cf-image.png

1.5 测试热量值兑换CarbonCoin功能

访问http://127.0.0.1:3001/mining.html页面,该页面每30秒,bob和tom两个账户都会随机发送15000到20000之间的一个随机值作为热量值给智能合约兑换CarbonCoin;
访问并刷新http://127.0.0.1:3001/index.html页面,可以发现bob和tom的CarbonCoin账户每30秒都在增加;

Step4: 完善DApp功能

1. 添加转账功能

节能设备获取到CarbonCoin后,可以将CarbonCoin出售给企业,比如航空公司等获利,简单体现就是设备账户可以转账给企业公司账户;

打开/CarbonDApp/index.html页面,添加代码

1.1 建立转账时使用的u3对象
U3 object used when establishing the transfer

let u3_bob = U3.createU3({
    keyProvider:'5JoQtsKQuH8hC9MyvfJAqo6qmKLm8ePYNucs7tPu2YxG12trzBt',//bob's private key
})

let u3_tom = U3.createU3({
    keyProvider:'5KXYYEWSFRWfNVrMPaVcxiRTjD9PzHjBSzxhA9MeQKHPMxWP8kU',//tom's private key
})
1.2 添加转账的js代码,用于调用相应的智能合约方法
async function sendCoin(from,to){

    let SYMBOL = 'CARB';
    let code = 'ben';

    let coins = U3.format.DecimalPad(this.randomFrom(100,500),4);

    if (from == 'bob'){
        const tr = await u3_bob.contract(code);
        const result = await tr.transfer(from, to, coins + ' ' + SYMBOL, 'sendCoin', { authorization: [`bob@active`] });

        let tx = await u3_bob.getTxByTxId(result.transaction_id);
        while (!tx.irreversible) {
            await wait(1000);
            tx = await u3_bob.getTxByTxId(result.transaction_id);
            if (tx.irreversible) {
                alert("bob send coin success:"+ coins);
                break;
            }
        }
    } else if (from == 'tom'){
        const tr = await u3_tom.contract(code);
        const result = await tr.transfer(from, to, coins + ' ' + SYMBOL, 'sendCoin', { authorization: [`tom@active`] });

        let tx = await u3_tom.getTxByTxId(result.transaction_id);
        while (!tx.irreversible) {
            await wait(1000);
            tx = await u3_tom.getTxByTxId(result.transaction_id);
            if (tx.irreversible) {
                alert("tom send coin success:"+ coins);
                break;
            }
        }
    }
}
1.3 在页面添加对sendcoin()方法的调用,更新如下代码:
<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>节能设备1</h1>
    <p>账户:bob</p>
    <p>描述:通过节能产生CarbonCoin的节能设备1</p>
    <p id="bob_balance"></p>
    <p><a href="#" OnClick="sendCoin('bob','tony')">给航空公司C转账</a></p>
</div>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>节能设备2</h1>
    <p>账户:tom</p>
    <p>描述:通过节能产生CarbonCoin的节能设备2</p>
    <p id="tom_balance"></p>
    <p><a href="#" OnClick="sendCoin('tom','jerry')">给汽车制造商D转账</a></p>
</div>
1.4 访问页面并测试功能

访问:http://127.0.0.1:3001/index.html,点击“给航空公司C转账”按钮,等待一段时间后(最长约10秒,为Ultrain交易确认时间),出现如下提示,表示转账成功。
0_1541590953725_ff8eddb5-76a5-472d-b7a7-e4a7ecedb0fb-image.png

2.添加企业公司将CarbonCoin兑换为公益积分功能

企业公司通过将CarbonCoin兑换为公益积分,对降低碳排放作出自己的贡献,提升企业声誉:

打开/CarbonDApp/index.html页面,添加代码

2.1 建立积分兑换时使用的u3对象
let u3_tony = U3.createU3({
    keyProvider:'5KbHvFfDXovPvo2ACNd23yAE9kJF7Mxaws7srp6VapjMr7TrHZB',//tony's private key
})

let u3_jerry = U3.createU3({
    keyProvider:'5JFz7EbcsCNHrDLuf9VpHtnLdepL4CcAEXu7AtSUYfcoiszursr',//jerry's private key
})
2.2 添加兑换的js代码,用于调用相应的智能合约方法
async function exchangeScore(account){
    let SYMBOL = 'CARB';
    let code = 'ben';
    let to = 'jack';

    let coins = U3.format.DecimalPad(this.randomFrom(10,50),4);

    if (account == 'tony'){
        const tr = await u3_tony.contract(code);
        const result = await tr.exchangeScore(account, to, coins + ' ' + SYMBOL, 'sendCoin', { authorization: [`tony@active`] });

        let tx = await u3_tony.getTxByTxId(result.transaction_id);
        while (!tx.irreversible) {
            await wait(1000);
            tx = await u3_tony.getTxByTxId(result.transaction_id);
            if (tx.irreversible) {
                alert("tony buy score success:"+ coins);
                break;
            }
        }
    } else if (account == 'jerry'){
        const tr = await u3_jerry.contract(code);
        const result = await tr.exchangeScore(account, to, coins + ' ' + SYMBOL, 'sendCoin', { authorization: [`jerry@active`] });

        let tx = await u3_jerry.getTxByTxId(result.transaction_id);
        while (!tx.irreversible) {
            await wait(1000);
            tx = await u3_jerry.getTxByTxId(result.transaction_id);
            if (tx.irreversible) {
                alert("jerry buy score success:"+ coins);
                break;
            }
        }
    }
}
2.3 在页面添加对exchangeScore()方法的调用,更新如下代码:
<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>航空公司C</h1>
    <p>账户:tony</p>
    <p>描述:大型航空公司,化石能源使用大户</p>
    <p id="tony_balance"></p>
    <p><a href="#" OnClick="exchangeScore('tony');return false;">购买公益积分</a></p>

</div>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>汽车制造商D</h1>
    <p>账户:jerry</p>
    <p>描述:大型汽车制造商,化石能源使用大户</p>
    <p id="jerry_balance"></p>
    <p><a href="#" OnClick="exchangeScore('jerry');return false;">购买公益积分</a></p>

</div>
2.4 访问页面并测试功能

访问:http://127.0.0.1:3001/index.html 点击“购买公益积分”按钮,等待一段时间后(最长约10秒,为Ultrain交易确认时间),出现如下提示,表示积分兑换成功,同时可以看到页面最下方的CarbonCoin销毁账户中,销毁的CarbonCoin数量增多了。
0_1541591037743_c6e6a2c4-a117-4172-854c-a67a3ecea04d-image.png

3.查询公益积分兑换历史

通过查询公益积分的兑换历史,我们可以建立一个企业公益积分的排行榜

3.1 建立页面

在CarbonDApp/public/目录中,新建leaderboard.html,输入如下代码:

<html>

<head>
    <meta charset="UTF-8">
    <title>leaderboard</title>
    <link rel="stylesheet" href="/stylesheets/style.css">
    <script src="./javascripts/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:'2616bfbc21e11d60d10cb798f00893c2befba10e2338b7277bb3865d2e658f58',
            symbol: 'UGAS'
        });

        function wait(ms = 0) {
            return new Promise(res => setTimeout(res, ms));
        }

        async function getLeaderboard() {

            let owner_account = 'ben';

            const tr = await u3.contract(owner_account);

            const result = await u3.getAllTxs(1,10000000,{"actions.name":"exchangeScore"},{_id:-1});
            //console.log(result);
            let content = '';
            for ( let i = 0 ;i< result.results.length;i++){
                content = content + '<tr><td>'+ result.results[i].actions[0].data.from+":"+result.results[i].actions[0].data.quantity
                    +'</td></tr>';
            }

            document.getElementById("leaderboard").innerHTML = content;
        }

        getLeaderboard();
    </script>
</head>

<body>

<div style="padding:8px;border:1px solid #96c2f1;background:#eff7ff">
    <h1>leaderboard</h1>

    <table id="leaderboard">
    </table>
</div>
</body>
</html>

我们通过

u3.getAllTxs(1,10000000,{"actions.name":"exchangeScore"},{_id:-1});

方法,查询到所有积分兑换的交易,将其具体数据打印到页面。

3.2 运行页面

访问http://127.0.0.1:3001/leaderboard.html,可以看到如下页面:
0_1541591082327_8036739d-a11f-479f-8a7f-f71a0d177816-image.png
这里展示了所有积分兑换的历史记录。

总结

本教程系统的介绍了如何编写一个端到端的网页版DApp应用,虽然在编写的过程中做了相应的化简,但总体来说描绘了DApp应用开发的全貌,开发者可以做这个框架的基础上,丰富并完善自己的DApp应用。
关于在iOS和Android上开发DApp,可以将u3.js整合到相应的混合Native开发框架中实现,其原理与网页版DApp一致。u3.deploy('build/MyContract', 'ben')中,MyContract修改为energy
at u3.deploy('build/MyContract', 'ben') , MyContract changed to energy;
通过查看config.js可以发现,我们通过“ben”账户的私钥,将该智能合约部署到httpEndpoint指定的地址下的chainId指定的链上;