---
# System prepended metadata

title: 從以太坊白皮書理解 web 3 概念 - Day19
tags: [' Web 3', it 鐵人賽 30 天, ' ethereum']

---

---
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 的訊息了

![](https://i.imgur.com/AZarnfA.png)
