本博客整理于 cryptozombies.io/zh/lesson
查看原教程请参考如上链接,本博客做个人记录!
CATALOGUE
从零开始写预言机(二)
1、概览
在前面我们已经完成了EthPriceOracle.sol、CallerContract的合约的编写,接下来我们就要在链下调用合约对ETH价格的请求
前面我们已经配置好了相关的环境,除此之外,我们还要在oracleDemo
目录下再新建Client.js
,EthpriceOracle.js
,utils/common.js
Client定义用户相关的函数,EthpriceOracle定义与预言机相关的函数,common定义读取账户等初始化的操作
文件目录大致如下
.
├── caller
│ ├── build
│ ├── caller_private_key
│ ├── contracts
│ ├── migrations
│ ├── test
│ └── truffle-config.js
├── Client.js
├── EthPriceOracle.js
├── oracle
│ ├── build
│ ├── contracts
│ ├── migrations
│ ├── oracle_private_key.js
│ ├── test
│ └── truffle-config.js
├── package.json
├── package-lock.json
└── utils
└── common.js
2、 EthPriceOracle.js
const axios = require('axios')
const BN = require('bn.js')
const common = require('./utils/common.js')
const SLEEP_INTERVAL = process.env.SLEEP_INTERVAL || 2000
const PRIVATE_KEY_FILE_NAME = process.env.PRIVATE_KEY_FILE || './oracle/oracle_private_key'
const CHUNK_SIZE = process.env.CHUNK_SIZE || 3
const MAX_RETRIES = process.env.MAX_RETRIES || 5
var OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
// Start here
async function getOracleContract(web3js) {
var networkId = await web3js.eth.net.getId()
console.log(networkId)
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
async function retrieveLatestEthPrice () {
const resp = await axios({
url: "https://api.nomics.com/v1/currencies/ticker?key=dc5568f283d8744cbb3eef663762d14095265cca&ids=ETH&interval=1h&per-page=100&page=1",
params: {
id: 'ETH'
},
method: 'get'
})
return resp.data[0].price
}
async function filterEvents(oracleContract, web3js) {
oracleContract.events.GetLatestEthPriceEvent(async (err, event) => {
if (err) {
console.error('Error on event', err)
return
}
await addRequestToQueue(event)
})
oracleContract.events.SetLatestEthPriceEvent(async (err, event) => {
if (err) console.error('Error on event', err)
})
}
async function addRequestToQueue(event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({
callerAddress,id})
console.log("接收到一个请求, id = ",event.returnValues.id)
}
async function processQueue(oracleContract,ownerAddress) {
let processedRequests = 0
while(pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
console.log("开始处理 id = " + req.id)
await processRequest(oracleContract,ownerAddress,req.id,req.callerAddress)
processedRequests++
}
}
async function processRequest(oracleContract,ownerAddress,id,callerAddress) {
let retries = 0
while(retries < MAX_RETRIES) {
try {
const ethPrice = await retrieveLatestEthPrice()
console.log("已成功获取数据 ethPrice = " + ethPrice)
await setLatestEthPrice(oracleContract,callerAddress,ownerAddress,ethPrice,id)
return
} catch (error) {
//console.log(error)
if (retries === MAX_RETRIES - 1) {
await setLatestEthPrice(oracleContract,callerAddress,ownerAddress,'0',id)
return
}
retries++
}
}
}
async function setLatestEthPrice(oracleContract,callerAddress,ownerAddress,ethPrice,id) {
// Start here
ethPrice = ethPrice.replace('.','')
const multiplier = new BN(10**10,10)
const ethPriceInt = (new BN(parseInt(ethPrice), 10)).mul(multiplier)
const idInt = new BN(parseInt(id))
try {
await oracleContract.methods.setLatestEthPrice(ethPriceInt.toString(), callerAddress, idInt.toString()).send({
from: ownerAddress })
} catch (error) {
console.log('Error encountered while calling setLatestEthPrice.')
console.log(error);
// Do some error handling
}
}
async function init() {
const {
accounts, web3js, client } = await common.loadAccount(PRIVATE_KEY_FILE_NAME)
const ownerAddress = accounts[0]
console.log("Oracle_Address = " + ownerAddress)
const oracleContract = await getOracleContract(web3js)
filterEvents(oracleContract,web3js)
console.log("start to listen!")
return {
oracleContract,ownerAddress,client}
}
(async () => {
const {
oracleContract, ownerAddress, client } = await init()
process.on( 'SIGINT', () => {
console.log('Calling client.disconnect()')
// 1. Execute client.disconnect
// client.disconnect()
process.exit( )
})
setInterval(async () => {
// 2. Run processQueue
await processQueue(oracleContract,ownerAddress)
}, SLEEP_INTERVAL)
})()
2.1 web3.js如何监听事件?
oracleContract.events.GetLatestEthPriceEvent(async (err, event) => {
if (err) {
console.error('Error on event', err)
return
}
await addRequestToQueue(event)
})
上述代码监听GetLatestEthPriceEvent事件,并在监听到事件之后调用addRequestToQueue函数
2.2 如何处理访问API时网络不稳定导致的访问失败?
// 处理请求
async function processRequest(oracleContract,ownerAddress,id,callerAddress) {
let retries = 0
while(retries < MAX_RETRIES) {
try {
const ethPrice = await retrieveLatestEthPrice()
console.log("已成功获取数据 ethPrice = " + ethPrice)
await setLatestEthPrice(oracleContract,callerAddress,ownerAddress,ethPrice,id)
return
} catch (error) {
//console.log(error)
// 如果已经达到最大查询次数,那么就返回0
if (retries === MAX_RETRIES - 1) {
await setLatestEthPrice(oracleContract,callerAddress,ownerAddress,'0',id)
return
}
retries++
}
}
}
上述代码记录了一个retries值,没访问失败一次就加一,直到达到最大的失败次数,返回“0”为eth价格
2.3 如何循环处理pendingRequest队列?
setInterval(async () => {
// 2. Run processQueue
await processQueue(oracleContract,ownerAddress)
}, SLEEP_INTERVAL)
利用setInterval函数
3、Client.js
const common = require('./utils/common.js')
const SLEEP_INTERVAL = process.env.SLEEP_INTERVAL || 20000
const PRIVATE_KEY_FILE_NAME = process.env.PRIVATE_KEY_FILE || './caller/caller_private_key'
const CallerJSON = require('./caller/build/contracts/CallerContract.json')
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
async function getCallerContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(CallerJSON.abi, CallerJSON.networks[networkId].address)
}
async function retrieveLatestEthPrice () {
const resp = await axios({
url: 'https://api.binance.com/api/v3/ticker/price',
params: {
symbol: 'ETHUSDT'
},
method: 'get'
})
return resp.data.price
}
async function filterEvents (callerContract) {
callerContract.events.PriceUpdatedEvent({
filter: {
} }, async (err, event) => {
if (err) console.error('Error on event', err)
console.log('* New PriceUpdated event. ethPrice: ' + event.returnValues.ethPrice)
})
callerContract.events.ReceivedNewRequestIdEvent({
filter: {
} }, async (err, event) => {
if (err) console.error('Error on event', err)
console.log("请求已成功创建, id = " + event.returnValues.id)
})
}
async function init () {
const {
accounts, web3js, client } = await common.loadAccount(PRIVATE_KEY_FILE_NAME)
const ownerAddress = accounts[0]
console.log("Caller_Address = " + ownerAddress)
const callerContract = await getCallerContract(web3js)
filterEvents(callerContract)
return {
callerContract, ownerAddress, client, web3js }
}
(async () => {
const {
callerContract, ownerAddress, client, web3js } = await init()
process.on( 'SIGINT', () => {
console.log('Calling client.disconnect()')
// client.disconnect();
process.exit();
})
const networkId = await web3js.eth.net.getId()
const oracleAddress = OracleJSON.networks[networkId].address
await callerContract.methods.setOracleInstanceAddress(oracleAddress).send({
from: ownerAddress })
setInterval( async () => {
// Start here
await callerContract.methods.updateEthPrice().send({
from:ownerAddress})
//console.log("Have send a request!");
}, SLEEP_INTERVAL);
})()
这里的基本逻辑和EthPriceOracle.js中的基本相同
不过在setInterval 函数中会调用callerContract.methods.updateEthPrice().send
函数发起请求
4、utils/commo.js
const fs = require('fs')
const Web3 = require('web3')
const ganache = require('ganache-cli')
const {
type } = require('os')
//const { Client, NonceTxMiddleware, SignedTxMiddleware, LocalAddress, CryptoUtils, LoomProvider } = require('loom-js')
async function loadAccount (privateKeyFileName) {
const provider = new Web3.providers.WebsocketProvider("ws://<YOUR_IP>:8546")
const web3js = new Web3(provider);
//console.log(web3js)
const objAccounts = await web3js.eth.getAccounts()
var accounts = []
for (var i in objAccounts) {
accounts.push(objAccounts[i]);
}
const client = 0;
return {
accounts,
web3js: web3js,
client
}
}
module.exports = {
loadAccount,
};
common.js主要是初始化owneraddress与web3对象,在2.3小结中在init中调用。
因为原来的教程里面的loom-js死活安装不上,所以就放弃了,换用了在geth的私有链进行部署,为什么不用ganache呢?因为http的provider已经被web3给弃用了,我没有找到ganache如何开启ws的方法,所以采用了geth,如果知道怎么用ganache进行测试,能告诉我一下嘛哈哈
这里与cryptozombies中有一些不同
- 没有用到私钥文件,传入common.js的文件路径实际没用
- client实际上没用
- 这里部署caller,oracle合约的用户其实是同一个,有需要可以才truffle中的配置文件中更改
5、部署
5.1 启动geth
接下来我们的代码文件都已经做好了,现在让我们启动我们的私有链吧!
geth --datadir ./ --networkid <YOUR_NETWORK_ID> --http --http.port 7545 --http.addr "0.0.0.0" --ws --ws.addr "0.0.0.0" --allow-insecure-unlock console
5.2 部署合约
编译合约
truffle compile
修改truffle-config.json中的networks
更改你的geth的ip、端口就可以啦
geth: {
host: "",
port: 7545,
network_id: "*",
},
大致可能是这样子的
部署合约
truffle migrate --network <YOUR_NETWORK_NAME>
6、运行!
分别运行
node Client.js
node EthPriceOracle.js
7、结果!