之前在《数字藏品发行平台的架构》里讲过,有一种架构希望以区块链的数据为核心。这样就需要将合约保存在区块链上的数据同步到数据库里,方便后续中间件接口的开发。

本次我们以同步ConfirmSale事件日志为例,介绍数据同步程序开发的相关知识点。

一、先上全部代码

```javascript

const { ethers } = require("hardhat");
var mysql = require("mysql");
let dotenv = require('dotenv')
dotenv.config({ path: "./.env" })

var connection = mysql.createConnection({
  host: process.env.MYSQL_HOST,
  port: process.env.MYSQL_PORT,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  charset: process.env.MYSQL_CHARSET
});

function DbQuery(sqlstr, values) {
  return new Promise((resolve, reject) => {
    connection.query(sqlstr, [values], (error, results) => {
      if (error) {
        reject(error);
      } else {
        resolve(results);
      }
    });
  });
}

async function main() {
  let addr = "salePlainAddress"; // 此处使用交易合约地址
  let blockNum = await ethers.provider.getBlockNumber();
  let topic = ethers.utils.id("ConfirmSale(address,uint256,address,address,address,uint256)");
  let iface = new ethers.utils.Interface([
    "event ConfirmSale(address indexed _contract, uint _tokenId, address indexed solder, address indexed buyer, address token, uint _price)"
  ]);

  let values = [];
  let filter = {
    address: addr,
    fromBlock: 11040000,
    toBlock: blockNum,
    topics: [topic]
  }
  let logs = await ethers.provider.getLogs(filter);
  if (logs.length > 0) {
    for (i in logs) {
      let blockinfo = await ethers.provider.getBlock(logs[i].blockNumber);
      let date = new Date(blockinfo.timestamp * 1000);
      let dt = date.getFullYear() + "-" + (date.getMonth() < 9 ? '0' + (date.getMonth()+1) : (date.getMonth()+1)) + "-" + (date.getDate() < 10 ? '0' + date.getDate() : date.getDate());
      let h = date.getHours();
      let saleInfo = iface.decodeEventLog("ConfirmSale", logs[i].data, logs[i].topics);
      values.push([saleInfo._contract, saleInfo._tokenId, saleInfo.solder, saleInfo.buyer, saleInfo.token, saleInfo._price, logs[i].blockNumber, logs[i].transactionHash, blockinfo.timestamp, dt, h]);
    }
    let sqlstr = "insert into meta_order (`contract`,`tokenID`,`seller`,`buyer`,`token`,`price`,`blocknum`,`txhash`,`timestamp`,`dt`,`h`) values ? on duplicate key update contract=values(contract), tokenID=values(tokenID), seller=values(seller), buyer=values(buyer), token=values(token), price=values(price), blocknum=values(blocknum), timestamp=values(timestamp), dt=values(dt), h=values(h)";
    let s = await DbQuery(sqlstr, values);
    console.log(s);
  } else {
    console.log("have no eventlogs");
  }
}

main()
  .then(() => {
    connection.end();
    process.exit(0);
  })
  .catch(error => {
    console.error(error);
    process.exit(1);
  })

```

 二、相关知识点介绍

  1. event logs

在Solidity代码中,使用event关键字来定义一个事件,如:event EventName(address bidder, uint amount);

这个用法和定义函数式一样的,并且事件在合约中同样可以被继承。触发一个事件使用emit(说明,之前的版本里并不需要使用emit),如:emit EventName(msg.sender, msg.value);

触发事件可以在任何函数中调用,如:

```javascript

function testEvent() public {
    emit EventName(msg.sender, msg.value);
}

```

在我们的实例程序中,数字藏品交易合约定义了5种event log,分别是:商品上架、更新商品信息、取消上架、订单确认和拍卖出价。

```javascript

interface ISales{
    //events
    event CreateSale(address indexed _contract, uint _tokenId, address seller, address token, uint _price, uint _due);
    event UpdateSale(address indexed _contract, uint _tokenId, address seller, address token, uint _price, uint _due);
    event CancelSale(address indexed _contract, uint _tokenId);
    event ConfirmSale(address indexed _contract, uint _tokenId, address indexed solder, address indexed buyer, address token, uint _price);
    event NewBid(address indexed _contract, uint _tokenId, address indexed bidder, address token, uint _price);
    
    //other code
}

```

我们需要将这5种事件从区块链同步下来,存入activity表中,作为基础数据,方便我们进行数据处理。

同时我们将event ConfirmSale单独拿出来,因为该事件数据作可以存入order为订单表,表示交易完成。

  1. 使用环境变量

dotenv是一个零依赖的模块,它将环境变量从.env文件加载到process.env中。这使得我们可以隐藏数据库密码细节到.env文件,同时通过git进行代码版本管理。

```javascript

let dotenv = require('dotenv')
dotenv.config({ path: "./.env" })

```
  1. 使用mysql数据库
```javascript

var mysql = require("mysql");

var connection = mysql.createConnection({
  host: process.env.MYSQL_HOST,
  port: process.env.MYSQL_PORT,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  database: process.env.MYSQL_DATABASE,
  charset: process.env.MYSQL_CHARSET
});

function DbQuery(sqlstr, values) {
  return new Promise((resolve, reject) => {
    connection.query(sqlstr, [values], (error, results) => {
      if (error) {
        reject(error);
      } else {
        resolve(results);
      }
    });
  });
}

```

此处我们将query包装在DbQuery方法的Promise中,目的是为了在程序中同步运行并等待。防止程序停止后,数据来不及存入数据库丢失。

  1. 代码整体架构


```javascript

async function main() {
  // some code here
  
}

main()
  .then(() => {
    connection.end();
    process.exit(0);
  })
  .catch(error => {
    console.error(error);
    process.exit(1);
  })

```

使用异步方式方便使用wait语法

  1. 使用hardhat同步数据
```javascript

// 导入hardhat库
const { ethers } = require("hardhat");

async function main() {
  let addr = "salePlainAddress"; // salePlainAddress
  let blockNum = await ethers.provider.getBlockNumber(); // 获取最新区块号
  let topic = ethers.utils.id("ConfirmSale(address,uint256,address,address,address,uint256)"); // 此处与合约event定义相对应,但只使用数据类型,不需要变量名
  
  // iface会用户日志分析
  let iface = new ethers.utils.Interface([
    "event ConfirmSale(address indexed _contract, uint _tokenId, address indexed solder, address indexed buyer, address token, uint _price)"
  ]);

  let values = [];
  let filter = {
    address: addr,
    fromBlock: 11040000,
    toBlock: blockNum,
    topics: [topic]
  }
  let logs = await ethers.provider.getLogs(filter); //从链上获取数据
  if (logs.length > 0) {
    for (i in logs) {
      let blockinfo = await ethers.provider.getBlock(logs[i].blockNumber);
      let date = new Date(blockinfo.timestamp * 1000);
      let dt = date.getFullYear() + "-" + (date.getMonth() < 9 ? '0' + (date.getMonth()+1) : (date.getMonth()+1)) + "-" + (date.getDate() < 10 ? '0' + date.getDate() : date.getDate());
      let h = date.getHours();
      let saleInfo = iface.decodeEventLog("ConfirmSale", logs[i].data, logs[i].topics);
      values.push([saleInfo._contract, saleInfo._tokenId, saleInfo.solder, saleInfo.buyer, saleInfo.token, saleInfo._price, logs[i].blockNumber, logs[i].transactionHash, blockinfo.timestamp, dt, h]);
    }
    
    将数据存入数据库
    let sqlstr = "insert into meta_order (`contract`,`tokenID`,`seller`,`buyer`,`token`,`price`,`blocknum`,`txhash`,`timestamp`,`dt`,`h`) values ? on duplicate key update contract=values(contract), tokenID=values(tokenID), seller=values(seller), buyer=values(buyer), token=values(token), price=values(price), blocknum=values(blocknum), timestamp=values(timestamp), dt=values(dt), h=values(h)";
    let s = await DbQuery(sqlstr, values);
    console.log(s);
  } else {
    console.log("have no eventlogs");
  }
}

```

此处mysql语法使用“on duplicate key update”,为防止重复录入数据库。

6.附加order表数据结构

```sql

CREATE TABLE `meta_order` (
  `txType` smallint NOT NULL DEFAULT '1' COMMENT '交易类型:1.一口价;2.拍卖;',
  `contract` char(42) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'nft合约地址',
  `tokenID` int NOT NULL COMMENT 'tokenID',
  `seller` char(42) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '卖家',
  `buyer` char(42) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '买家',
  `token` char(42) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代币合约地址',
  `price` decimal(65,0) NOT NULL COMMENT '价格',
  `blocknum` int NOT NULL COMMENT '交易所在区块号',
  `txhash` char(66) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '交易哈希',
  `timestamp` int NOT NULL COMMENT '交易区块对应的时间戳',
  `dt` char(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '""' COMMENT '交易日期',
  `h` int NOT NULL COMMENT '交易小时数',
  PRIMARY KEY (`txhash`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC

```