使用Node.js后台监听合约事件及提供服务

  • Tiny熊
  • 更新于 2023-05-24 16:06
  • 阅读 12622

在上个文章众筹案例中,每个参与者可以看到自己的参与的状态,创作者却没有办法查看所有参与者,这篇文章我们实现在合约中加入参与事件,后台通过监听参与事件记录所有的参与者。

在上个文章众筹案例中,每个参与者可以看到自己的参与的状态,创作者却没有办法查看所有参与者,这篇文章我们实现在合约中加入参与事件,后台通过监听参与事件记录所有的参与者。

需求背景

其实有二个办法实现查看所有参与者:

  1. 加入一个状态变量:address[] joinAccouts ,这是一个Solidity数组类型,用来记录所有参与者的地址,每当有新的参与者进来时,给数组加入参与者地址。
  2. 通过触发事件把参与者地址记录到日志中,然后通过启动一个服务程序监听事件,当事件触发时,把参与者地址记录到数据库中,并提供一个后端服务,它数据库中的参与者列表返回给前端。

两种方法各有优缺点,方法1的gas消耗会远高于方法2,优点是更加简单,方法2则相反,并且方法2可以实时获取到状态,这在一些场合非常有用。来看看方法二如何实现。

Node.js 及 Express 简介

这里使用Node.js来作为后端服务,Node.js 就是运行在服务端的 JavaScript。Node.js 是一个基于Google的V8 引擎运行时建立的一个事件驱动I/O服务端JavaScript环境,速度非常快,性能非常好。

Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助创建各种 Web 应用。

下面通过编写一个简单的Hello World来看看如何使用Express 如何使用,在众筹项目目录下新建一个文件夹server,进入此目录并将其作为后端项目工作目录,命令如下:

> mkdir server
> cd server
> npm init

然后通过npm init命令进行初始化,npm init 会创建一个 package.json 文件进行包管理,同时还会要求我们输入几个参数,例如此应用的名称和版本,命令效果如下:

> npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (server) server
version: (1.0.0)
description: 众筹监听后台demon
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /User/xxx/crowdfunding/server/package.json:

{
  "name": "server",
  "version": "1.0.0",
  "description": "众筹监听后台demon",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Is this OK? (yes) yes

大部分直接按“回车”键接受默认设置即可,这个时候,就可以看到在server目录下产生了一个pacckage.json 文件,继续安装Express, 命令如下:

npm install express --save

环境完成了,新建一个文件 index.js ,编写一段服务端的程序HelloWorld, 代码如下:

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

app.listen(3000, () => console.log('Start Server, listening on port 3000!'))

上面代码中,引入了 express 模块,它在后台常驻运行,并监听着3000 端口,当客户端发起请求后,响应 "Hello World!" 字符串,通过以下命令启动服务:

> node index.js
Start Server, listening on port 3000! 

启动后,在浏览器访问地址: http://localhost:3000 就可以看到Hello World!, 如下图

Express 启动

监听合约事件

Express 会启动一个常驻在后台的服务,对刚刚编写的index.js 进行下修改,加入web3 就能够实现监听合约事件。

合约加入事件

先在Crowdfunding合约中加入一个事件:

event Join(address indexed, uint price);

然后在回退函数中加入触发事件:

emit Join(msg.sender, msg.value); 

监听事件

修改合约后,使用truffle migrate --reset 命令重新编译部署,之后在 index.js中加入监听Join事件代码,代码如下:

// express 启动代码
...

// 引入web库
var Web3 = require('web3');
// 使用WebSocket协议 连接节点
let web3 = new Web3(new Web3.providers.WebsocketProvider('ws://localhost:7545'));

// 获取合约实例
var Crowdfunding = require('../build/contracts/Crowdfunding.json');
const crowdFund = new web3.eth.Contract(
  Crowdfunding.abi,
  Crowdfunding.networks[2337].address
);

//  监听Join 加速事件
crowdFund.events.Join(function(error, event) {
  if (error) {
    console.log(error);
  }

  // 打印出交易hash 及区块号
  console.log("交易hash:" + event.transactionHash);
  console.log("区块高度:" + event.blockNumber);

  //   获得监听到的数据:
  console.log("参与地址:" + event.returnValues.user);
  console.log("参与金额:" + event.returnValues.price);
});
});

除以上注释外,对以上代码关键点进行下介绍:

  • 在初始化web3时,使用了 WebsocketProvider,通过 WebSocket 通信协议与节点通信,如果是使用 geth 节点需要使用选项 --ws 开启服务,开发使用的Ganache默认开启了WebSocket服务。

    补充知识: HTTP 协议有一个缺陷是只能单向通行,通信只能由客户端发起, 只能通过"轮询"去获取状态的更新,而WebSocket 支持双向通行,服务端可以主动向客户端推送信息,客户端也可以主动向服务器发送信息(注意 Express 服务在与节点程序通信时,节点是作为服务端角色)。

  • 获取合约实例时,使用的合约的ABI 及合约地址参数,均是通过Truffle 编译部署生成的文件 Crowdfunding.json 获取。

  • 通过Web3合约实例后,可以通过 myContract.events.EventName()函数传入一个回调函数监听事件的变化。 更多的监听事件可以查看Web3.js文档

重新启动后台服务:

> node index.js
Start Server, listening on port 3000! 

另开一个命令行控制台窗口,启动前端:

> npm run serve(或  yarn serve )

浏览器输入:http://localhost:8080/ 进入前端页面,点击“参与众筹”,切换到后端的命令行控制台窗口, 可以看到打印出了四条日志:

> node index.js
Start Server, listening on port 3000!
交易hash: 0x6c1c5172eae236e9c1f535e...5d45d45254a2435b1cd3891e83635
区块高度: 58
参与地址: 0x132f44857fe61526...6b4109A198ea...
参与金额: 20000000000000000

为了方便管理,接下来把监听到的数据写入数据库里。

建数据库及表

我们这里选用的是MySQL,读者也可以根据自己的喜好选用其他的如PostgreSQL数据库, 这里假定数据库已经安装配置好(如何安装大家可自行搜索),连接上MySQL服务器, 使用以下命令创建数据库和表来存储众筹数据:

CREATE DATABASE crowdfund;
use crowdfund;

CREATE TABLE joins(
  id INT UNIQUE AUTO_INCREMENT,
  address VARCHAR(42) UNIQUE,
  price FLOAT NOT NULL,
  tx VARCHAR(66) NOT NULL,
  block_no INT NOT NULL,
  created_at datetime,
  PRIMARY KEY (id)
);

以上MySQL代码可以复制到MySQL客户端中执行,它创建名为 crowdfund 的数据库,并在数据库中创建joins表,joins表从来存储用户参与众筹信息,包含列有:主键id,地址address,价格price, 交易hash,区块号blockNo,以及插入数据时间。

监听数据入库

先安装 node-mysql 驱动,它提供在Node.js程序里连接MySQL服务器的接口,安装命令如下:

> cd server
> npm install mysql --save

在inde.js 中引入mysql,并加入一个插入数据库函数,代码如下:

var mysql  = require('mysql');

// 定义数据插入数据库函数
function insertJoins(address, price, tx, blockNo) {

// 连接数据库 (请使用你自己的mysql连接信息)
  var connection = mysql.createConnection({
    host     : 'localhost',
    user     : 'username',
    password : 'pwd',
    port     : '3306',
    database : 'crowdfund'
  });

  connection.connect();

  // 构建插入语句
  const query = `INSERT into joins (
        address,
        price,
        tx,
        block_no,
        created_at
    ) Values (?,?,?,?,NOW())`;
  const params = [address, price, tx, blockNo];  

// 执行插入操作
  connection.query(query, params, function (error, results) {
    if (error) throw error;
  });

  connection.end();
}

并在上面监听打印日志的后面加入调用insertJoins()函数插入数据库, 调用代码如下:

  insertJoins(event.returnValues.user,
  // 把以wei为单位的价格转为ether单位
    web3.utils.fromWei(event.returnValues.price),
    event.transactionHash,
    event.blockNumber )

使用node重新启动index.js:

node index.js,

在前端切换不同的账号点击“参与众筹”后,可以在数据库查询到相应的众筹记录,如下图所示:

查询数据众筹数据

为前端提供众筹记录

后端提供接口

通过读取数据库的数据,并在Express加入一个路由,接受前端请求后返回读取到的数据,在index.js 先加入一个getJoins()函数来读取数据库数据:

// 通过一个回调函数把结果返回出去
function getJoins(callback) {
// 获取数据库链接
  var connection = getConn();
  connection.connect();

// 查询 SQL
  const query = `SELECT address, price from joins`;
  const params = [];
    // 查询数据库
  connection.query(query, params, (err, rows)=>{
      if(err){
          return callback(err);
      }
      console.log(`result=>`, rows);        
      callback(rows);
  });

  connection.end();
}

getJoins() 和 insertJoins() 都需要获取数据库连接,为了代码复用,抽象出了getConn() 函数:

// 请使用你自己的mysql连接信息
function getConn() {
  return mysql.createConnection({
    host     : 'localhost',
    user     : 'username',
    password : 'pwd',
    port     : '3306',
    database : 'crowdfund'
  });
}

加入一个新的路由/joins,代码如下:

app.get('/joins', (req, res) => {
  getJoins( rows=> {
    //  设置允许跨域访问
    res.set({'Access-Control-Allow-Origin': '*'})
    .send(rows)
  });
})

使用node index.js,重新启动index.js, 浏览器访问:http://localhost:3000/joins, 将返回参与众筹用户的JSON数组:

[
  {
    "address": "0x...b7579156EcD9A...",
    "price": 0.02
  },
  {
    "address": "0x...D4FCDF3332BcB31770...",
    "price": 0.02
  },
  {
    "address": "0x...45730F3756a5E8C8f0D3B...",
    "price": 0.02
  }
]

前端通过接口获取数据

现在前端组件就可以通过Ajax请求访问/joins获取众筹列表,前端可以安装Axios来进行HTTP请求,依旧通过npm 来安装:

> npm install axios (在项目根目录下执行)

修改前端 CrowdFund.vue , 加入获取众筹列表 getJoins()函数:

import axios from 'axios'
...
    // 获取众筹列表
    getJoins() {
      axios.get('http://localhost:3000/joins')
        .then(response => {
          this.joinList = response.data
        })
        .catch(function (error) { // Ajax请求失败处理
          console.log(error);
        });
    }

以上代码把axios获取到的众筹列表赋值给joinList变量。

前端渲染数据

在HTML模板中加入joinList的渲染:

  <div class="box" v-if="isAuthor">
    <div >
      <p v-bind:key="item" v-for="item in joinList" >
        <label> 地址:{{ item.address }} </label>
        金额:<b> {{ item.price }} </b>
      </p>
    </div>
   <button :disabled="closed" @click="withdrawFund"> 提取资金</button>
</div>

浏览器输入 http://localhost:8080,MetaMask切换到创作者账号,可以看到输出的众筹列表,效果如下图:

展示众筹列表

通过Vue 开发前端,后端使用Node.js监控合约事件,这个案例就介绍完了,书中对于Vue及Node.js的使用也仅仅是抛砖引玉,毕竟不是本书的重点,相关知识点,需要读者自己扩展。

书中展示的代码略有删减,完整代码请订阅我的小专栏

点赞 2
收藏 8
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

2 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。