---
tags: it 鐵人賽 30 天, Web 3, ethereum
---
# 從以太坊白皮書理解 web 3 概念 - Day19
## Learn Solidity - Day 11 - Build an Oracle - Part 2
在今天將會透過 [Lession 15 - Build an Oracle - Part 2](https://cryptozombies.io/en/lesson/15) 學習關於前端與 Oracle Contract 互動的部份
## 設定
這邊會先實作 Javascript 讀取 Binance API 的 ETH 價格並與 Oracle Contract 互動的元件。
建立步驟如下
* 建立 EthPriceOracle.js 檔案
* 引入一些必要的 library
* 初始化變數
* 設定連接到 Extdev Testnet 的設定
備註:
1. 這裡會把 ABI 讀取到一個 OracleJSON 變數內
2. 會把之前 pendRequests 初始化為一個空陣列,用來儲存要處理的 ETH price Request
### 初始化 Oracle Contract
透過以下語法可以讀取 Oracle Contract ABI 資訊
```javascript=
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json');
```
這個檔案讀出的是 Oracle Contract 的 ABI 資訊,比如 setLatestEthPrice function 的 signature。
而要把 Oracle Contract 實體化則需要透過 web3.eth.Contract 語法如下:
```javascript=
const myContract = new web3js.eth.Contract(myContractJSON.abi, myContractJSON.networks[networkId].address);
```
要注意得是,這邊的 networkId 是用來區別 Contract 所在發佈的鏈
舉例來說: 假設是發佈到 Extdev ,則 networkId 會是 9545242630824
然而每次要去手動更改 networkId 實在太不方便
比較建議的方式是使用語法 web3.eth.net.getId() 動態讀取 networkId 如下:
```javascript=
const networkId = await web3js.eth.net.getId()
```
### 實作步驟
1. 建立一個 async function getOracleContract
需要一個參數: web3js
2. 首先需要透過 web3js.eth.net.getId 讀取當下的 networkId 到一個 const 變數 networkId
3. 接著透過 web3js.eth.Contract 語法實體化讀入的 Contract 並且傳這個實體。
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
// Start here
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
```
## 監聽 event
接下來要撰寫前端監聽 event 的邏輯
首先來看前端如何監聽 event
Oracle Contract 會發起一個 event 。在 Oracle Contract 被呼叫之前,前端 app 必須要先做監聽 event 的動作
以下是 Oracle Contract 發起 GetLatestEthPriceEvent 的邏輯
```solidity=
function getLatestEthPrice() public returns (uint256) {
randNonce++;
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
pendingRequests[id] = true;
emit GetLatestEthPriceEvent(msg.sender, id);
return id;
}
```
而當 Oracle Contract 發起 GetLatestEthPriceEvent 時,
前端的 app 接收到該 event 之後應該把該 event 放入 pendingRequests 陣列
如下:
```javascript=
myContract.events.EventName(async (err, event) => {
if (err) {
console.error('Error on event', err)
return
}
// Do something
})
```
上面範例只已針對 EventName 做監聽。要做更複雜的邏輯,可以使用 filter 如下
```javascript=
myContract.events.EventName({ filter: { myParam: 1 }}, async (err, event) => {
if (err) {
console.error('Error on event', err)
return
}
// Do something
})
```
### 實作監聽 event
1. 宣告一個 async function filterEvent
需要兩個參數: oracleContract, web3js
2. 使用上面範例的 code 來監聽事件。並且做以下更動
* 更新 Smart Contract 的名稱
* 更新 EventName 為 GetLatestEthPriceEvent
* 更新註解地方為 await addRequestToQueue(event)
3. 最後,加入監聽 SetLatestEthPriceEvent 邏輯
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
// Start here
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)
return
}
// Do something
})
}
```
## 實作 addRequestToQueue
目前前端 app 監聽 GetLatestEthPriceEvent 事件後,會執行 addRequestToQueue
addRequestToQueue 的執行邏輯如下
* 首先,會拿出 caller 的 address 與 Request ID。這邊可以透過 event 物件中拿取
假設 event 定義如下:
```solidity=
event TransferTokens(address from, address to, uint256 amount)
```
實作語法會類似以下
```javascript=
async function parseEvent (event) {
const from = event.returnValues.from
const to = event.returnValues.to
const amount = event.returnValues.amount
}
```
* 接下來,會把 callerAddress 與 id 放到 pendingRequests 陣列
### 實作 addRequestToQueue
1. 宣告 async function addRequestToQueue(event)
2. 前兩行需要從 event 物件取出 callerAddress 與 id 並且存在兩個 const 變數 callerAddress 與 id
3. 把 {callerAddress, id} push 到 pendingRequests 陣列
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
// Start here
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
}
}
```
## 實作處理 Queue 邏輯
處理邏輯如下
* 把 pendingRequest 陣列第一個值 , shift 出來
* 把剛剛 shift 出來的值傳入 processRequest 當作參數呼叫
* 最後把 processedRequest+1
### 實作
1. 把以下邏輯加入 while loop
```javascript=
const req = pendingRequests.shift()
```
2. 執行 processRequest , 這個 function 需要以下參數:
* oracleContract 與 ownerAddress 來自於 function proccessQueue 參數
* id 與 callerAddress 來自於物件 req
3. 把 processedRequest++
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
// Start here
const req = pendingRequests.shift()
await processRequest(oracleContract, ownerAddress, req.id, req.callerAddress)
processedRequests++
}
}
```
## 實作 Retry loop
由於有時候 fetch ETH 價格的 api 也許會 fail
所以必須要在 fetch fail 時執行 retry 來防止需要整個流程重新執行
然而如果一直 retry 會造成無限回圈
因此需要設定一個 retry 上限值
### 實作步驟
1. 宣告 async function processRequest(oracleContract, owenrAddress, id, callerAddress)
2. 第一行宣告 let retries = 0
3. 實作一個 while loop 其執行條件為 retries < MAX_RETRIES
4. 在 while loop 內實作一個 try catch 邏輯
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
await processRequest(oracleContract, ownerAddress, req.id, req.callerAddress)
processedRequests++
}
}
// Start here
async function processRequest (oracleContract, ownerAddress, id, callerAddress) {
let retries = 0
while (retries < MAX_RETRIES) {
try {
} catch (error) {
}
}
}
```
## 實作 try catch 邏輯
1. 在 try block 加入以下邏輯
```javascript=
const ethPrice = await retrieveLatestEthPrice()
```
2. 在 try block 第二行加入以下邏輯
```javascript=
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
```
3. 在 try block 第三行加入 return
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
await processRequest(oracleContract, ownerAddress, req.id, req.callerAddress)
processedRequests++
}
}
async function processRequest (oracleContract, ownerAddress, id, callerAddress) {
let retries = 0
while (retries < MAX_RETRIES) {
try {
// Start here
const ethPrice = await retrieveLatestEthPrice()
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
return
} catch (error) {
}
}
}
```
## 實作 errpr 處理
1. 寫一個判斷式 if ( retries === MAX_RETRIES - 1)
2. 如果當上面判斷式成立,則呼叫 setLatestEthPrice,並且把 ethPrice 代入 '0'
3. 當 retries 到達最大重試上現,則直接 return
4. 在 if 之外使用 retries++ 來累計 retry 次數
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.networks[networkId].address)
}
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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
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()
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
return
} catch (error) {
// Start here
if (retries === MAX_RETRIES - 1) {
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, '0', id)
return
}
retries++
}
}
}
```
## 在 EVM 與 Javascript 中處理 Number
在 EVM 中並不支援浮點數運算,所以只能透過先把浮點數數字用一個 10^n 做相成,最後運算完才除回來
因為 Binance API 回傳值是一個具有在小數點前面有8位數的浮點數
所以可以把回傳值乘與 10^10 次方。因為 1 Ether = 10^18 wei
如果是小數點前面有8位數的浮點數,代表小數後面有 10 位數
在 Javascript Number 型別是雙精準度 64 位元的二進位 IEEE 754 數值,只支援到 16位數個精準度
所以只能使用 BN.js 這個函式庫來處理大精準度的數值
假設 Biance API 回傳 169.8700000
以下示範如何使用 BN.js 做轉換
因為 Javascript 是動態型別
所以可以透過以下方式把小數點先拿掉
```javascript=
aNumber = aNumber.replace('.', '')
```
接著使用以下方式把 aNumber 轉換成 BN 物件
```javascript=
const bNumber = new BN(aNumber, 10)
```
其中第二個參數代入 10 代表是以 10 進位為主
### 實作 setLatestEthPrice
1. 使用 replace 來移除原本 ethPrice 的小數點
2. 建立一個 const multipler = 10**10
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.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 (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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
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()
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
return
} catch (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.')
// Do some error handling
}
}
```
## 在 javacript 回傳多個數值
繼續往下實作 client 與 Oracle 互動的部份之前
需要知道幾個要點:
### Javascript 起動 Oracle Contract 流程
以下是 Javascript 起動 Oracle Contract 流程
1. 透過呼叫 common.loadAccount 連接到 Extdev 測試鏈
2. 初始化 Oracle Contract
3. 開始監聽事件
### javacript 回傳多個數值語法
在 Javascript 要回傳多個值,必須要以 JSON 物件或是陣列行時回傳如下
```javascript=
function myAwesomeFunction () {
const one = '1'
const two = '2'
return { one, two }
}
```
接收方則需用以下語法接收
```javascript=
const { one, two } = myAwesomeFunction()
```
### 實作 init function
1. 宣告 init function
2. 第一行執行 common.loadAccount
需要帶入一個參數: PRIVATE_KEY_FILE_NAME
這個 function 回傳一個物件帶有3個屬性 ownerAddress, web3js, client
3. 使用 getOracleContract 初始化一個 Oracle Contract 物件
帶入一個參數 web3js
把結果存在一個變數 const oracleContract
4. 使用 filterEvent 帶入 oracleContract 與 web3js 當作參數
5. 最後回傳 return { oracleContract, ownerAddress, client }
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.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 (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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
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()
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
return
} catch (error) {
if (retries === MAX_RETRIES - 1) {
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, '0', id)
return
}
retries++
}
}
}
async function setLatestEthPrice (oracleContract, callerAddress, ownerAddress, ethPrice, id) {
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.')
// Do some error handling
}
}
async function init () {
// Start here
const { ownerAddress, web3js, client } = common.loadAccount(PRIVATE_KEY_FILE_NAME)
const oracleContract = await getOracleContract(web3js)
filterEvents(oracleContract, web3js)
return { oracleContract, ownerAddress, client }
}
```
## 實作 sleep 邏輯
在處理 queue 那部份需要加入 sleep SLEEP_INTERVAL 邏輯
在 javascript 可以使用 setInterval 如下來實現
```javascript=
setInterval(async () => {
doSomething()
}, SLEEP_INTERVAL)
```
而當整個 app 停止時,需要能夠做到 graceful shutdown 來關閉所有使用過的資源
在 nodejs 可以使用 SIGINT 這個訊號的監聽 app 關閉如下
```javascript=
process.on( 'SIGINT', () => {
// Gracefully shut down the oracle
})
```
### 實作關閉 Oracle 邏輯
1. 透過 client.disconnect 來關閉連線資源,在收到訊號 SIGINT 的時候
2. 在 setInterval 加入 await processQueue(oracleContract, ownerAddress)
```javascript=
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
const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json')
var pendingRequests = []
async function getOracleContract (web3js) {
const networkId = await web3js.eth.net.getId()
return new web3js.eth.Contract(OracleJSON.abi, OracleJSON.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 (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)
// Do something
})
}
async function addRequestToQueue (event) {
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
async function processQueue (oracleContract, ownerAddress) {
let processedRequests = 0
while (pendingRequests.length > 0 && processedRequests < CHUNK_SIZE) {
const req = pendingRequests.shift()
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()
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
return
} catch (error) {
if (retries === MAX_RETRIES - 1) {
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, '0', id)
return
}
retries++
}
}
}
async function setLatestEthPrice (oracleContract, callerAddress, ownerAddress, ethPrice, id) {
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.')
// Do some error handling
}
}
async function init () {
const { ownerAddress, web3js, client } = common.loadAccount(PRIVATE_KEY_FILE_NAME)
const oracleContract = await getOracleContract(web3js)
filterEvents(oracleContract, web3js)
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)
})()
```
## 實作 client 更新 Etc Price 的部份
1. 在 setInterval 內部執行 callerContract.methods.updateEthPrice().send({ from: ownerAddress })
```javascript=
const common = require('./utils/common.js')
const SLEEP_INTERVAL = process.env.SLEEP_INTERVAL || 2000
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)
})
}
async function init () {
const { ownerAddress, web3js, client } = common.loadAccount(PRIVATE_KEY_FILE_NAME)
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 })
}, SLEEP_INTERVAL);
})()
```
## 發佈 Contract
### 產生 Private Keys
直接建立 scripts/gen-key.js 如下
```javascript=
const { CryptoUtils } = require('loom-js')
const fs = require('fs')
if (process.argv.length <= 2) {
console.log("Usage: " + __filename + " <filename>.")
process.exit(1);
}
const privateKey = CryptoUtils.generatePrivateKey()
const privateKeyString = CryptoUtils.Uint8ArrayToB64(privateKey)
let path = process.argv[2]
fs.writeFileSync(path, privateKeyString)
```
接著就可以透過以下指令來產生 private key 給 oracle contract
```shell=
node scripts/gen-key.js oracle/oracle_private_key
```
然後透過以下指令來產生 private key 給 caller contract
```shell=
node scripts/gen-key.js caller/caller_private_key.
```
### 設定 Truffle
為了要 deploy 兩個 Contract ,但兩個 Contract 具有不同的 private key
要做到使用 Truffle 可以分別使用不同 private key deploy 不同 Contract 最簡單作法就是建立兩個不同的設定檔案
* 建立 oracle/truffle-config.js 來發佈 Oracle Contract 如下
```javascript=
const LoomTruffleProvider = require('loom-truffle-provider')
const path = require('path')
const fs = require('fs')
module.exports = {
networks: {
extdev: {
provider: function () {
const privateKey = fs.readFileSync(path.join(__dirname, 'oracle_private_key'), 'utf-8')
const chainId = 'extdev-plasma-us1'
const writeUrl = 'wss://extdev-plasma-us1.dappchains.com/websocket'
const readUrl = 'wss://extdev-plasma-us1.dappchains.com/queryws'
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
},
network_id: '9545242630824'
}
},
compilers: {
solc: {
version: '0.5.0'
}
}
}
```
* 建立 caller/truffle-config.js 來發佈 Oracle Contract 如下
```javascript=
const LoomTruffleProvider = require('loom-truffle-provider')
const path = require('path')
const fs = require('fs')
module.exports = {
networks: {
extdev: {
provider: function () {
const privateKey = fs.readFileSync(path.join(__dirname, 'caller_private_key'), 'utf-8')
const chainId = 'extdev-plasma-us1'
const writeUrl = 'wss://extdev-plasma-us1.dappchains.com/websocket'
const readUrl = 'wss://extdev-plasma-us1.dappchains.com/queryws'
return new LoomTruffleProvider(chainId, writeUrl, readUrl, privateKey)
},
network_id: '9545242630824'
}
},
compilers: {
solc: {
version: '0.5.0'
}
}
}
```
兩個設定檔分別引入不同 private key
### 建立 migration 檔案
在 ./oracle/migrations/2_eth_price_oracle.js 使用以下內容
```javascript=
const EthPriceOracle = artifacts.require('EthPriceOracle')
module.exports = function (deployer) {
deployer.deploy(EthPriceOracle)
}
```
在 ./caller/migrations/02_caller_oracle.js 使用以下內容
```javascript=
const CallerContract = artifacts.require('CallerContract')
module.exports = function (deployer) {
deployer.deploy(CallerContract)
}
```
### 更新 package.json 檔案
更新 scripts 欄位如下
```javascript=
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"deploy:oracle": "cd oracle && npx truffle migrate --network extdev --reset -all && cd ..",
"deploy:caller": "cd caller && npx truffle migrate --network extdev --reset -all && cd ..",
"deploy:all": "npm run deploy:oracle && npm run deploy:caller"
},
```
然後就可以透過 npm run deploy:all 一次發佈兩個 Contract 了
## 最後執行
首先使用以下讓 EthPriceOracle.js 跑起來
```shell=
node EthPriceOracle.js
```
接者 Client.js 需要透過以下指令做執行
```shell=
node Client.js
```
然後就可以看到畫面上有 ethPrice 的訊息了
