iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0
Web 3

從以太坊白皮書理解 web 3 概念系列 第 20

從以太坊白皮書理解 web 3 概念 - Day19

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day19

Learn Solidity - Day 11 - Build an Oracle - Part 2

在今天將會透過 Lession 15 - Build an Oracle - Part 2 學習關於前端與 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 資訊

const OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json');

這個檔案讀出的是 Oracle Contract 的 ABI 資訊,比如 setLatestEthPrice function 的 signature。

而要把 Oracle Contract 實體化則需要透過 web3.eth.Contract 語法如下:

const myContract = new web3js.eth.Contract(myContractJSON.abi, myContractJSON.networks[networkId].address);

要注意得是,這邊的 networkId 是用來區別 Contract 所在發佈的鏈

舉例來說: 假設是發佈到 Extdev ,則 networkId 會是 9545242630824

然而每次要去手動更改 networkId 實在太不方便

比較建議的方式是使用語法 web3.eth.net.getId() 動態讀取 networkId 如下:

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 並且傳這個實體。
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 的邏輯

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 陣列

如下:

myContract.events.EventName(async (err, event) => {
  if (err) {
    console.error('Error on event', err)
    return
  }
  // Do something
})

上面範例只已針對 EventName 做監聽。要做更複雜的邏輯,可以使用 filter 如下

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 邏輯
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 定義如下:
event TransferTokens(address from, address to, uint256 amount)

實作語法會類似以下

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 陣列
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
const req = pendingRequests.shift()
  1. 執行 processRequest , 這個 function 需要以下參數:
  • oracleContract 與 ownerAddress 來自於 function proccessQueue 參數
  • id 與 callerAddress 來自於物件 req
  1. 把 processedRequest++
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 邏輯
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 加入以下邏輯
const ethPrice = await retrieveLatestEthPrice()
  1. 在 try block 第二行加入以下邏輯
await setLatestEthPrice(oracleContract, callerAddress, ownerAddress, ethPrice, id)
  1. 在 try block 第三行加入 return
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 次數
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 是動態型別

所以可以透過以下方式把小數點先拿掉

aNumber = aNumber.replace('.', '')

接著使用以下方式把 aNumber 轉換成 BN 物件

const bNumber = new BN(aNumber, 10)

其中第二個參數代入 10 代表是以 10 進位為主

實作 setLatestEthPrice

  1. 使用 replace 來移除原本 ethPrice 的小數點
  2. 建立一個 const multipler = 10**10
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 物件或是陣列行時回傳如下

function myAwesomeFunction () {
  const one = '1'
  const two = '2'
  return { one, two }
}

接收方則需用以下語法接收

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 }
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 如下來實現

setInterval(async () => {
 doSomething()
}, SLEEP_INTERVAL)

而當整個 app 停止時,需要能夠做到 graceful shutdown 來關閉所有使用過的資源

在 nodejs 可以使用 SIGINT 這個訊號的監聽 app 關閉如下

process.on( 'SIGINT', () => {
 // Gracefully shut down the oracle
 })

實作關閉 Oracle 邏輯

  1. 透過 client.disconnect 來關閉連線資源,在收到訊號 SIGINT 的時候
  2. 在 setInterval 加入 await processQueue(oracleContract, ownerAddress)
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 })
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 如下

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

node scripts/gen-key.js oracle/oracle_private_key

然後透過以下指令來產生 private key 給 caller contract

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 如下
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 如下
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 使用以下內容

const EthPriceOracle = artifacts.require('EthPriceOracle')

module.exports = function (deployer) {
  deployer.deploy(EthPriceOracle)
}

在 ./caller/migrations/02_caller_oracle.js 使用以下內容

const CallerContract = artifacts.require('CallerContract')

module.exports = function (deployer) {
  deployer.deploy(CallerContract)
}

更新 package.json 檔案

更新 scripts 欄位如下

"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 跑起來

node EthPriceOracle.js

接者 Client.js 需要透過以下指令做執行

node Client.js

然後就可以看到畫面上有 ethPrice 的訊息了


上一篇
從以太坊白皮書理解 web 3 概念 - Day18
下一篇
從以太坊白皮書理解 web 3 概念 - Day20
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言