--- 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)