//tips
//smart contract
先に確認したコントラクトはoracleのコントラクトとコミュニケーションするもので、calling関数はoracleからのみ呼ばせるようにしたい。
oracleAddressはすでにsetOracleInstanceAddressでセットしているのでmodifierできちんとoracleのアドレスが呼ばれているかを確認すれば良いことになる。
modifier onlyOracle() {
require(msg.sender == oracleAddress,"You are not authorized to call this function.");
_;
}
ここからやっとoracleコントラクトの中身に入っていく。
先に通常コントラクトのupdateEthPrice() にてoracleInstance.getLatestEthPrice();として呼んだが、
getLatestEthPriceとsetLatestEthPriceを設定することがoracleコントラクトの主な役割になりそう。
getLatestEthPriceはrequest idをコントラクトに返していた。セキュリティ面からrequest idは他の人に推測できない乱数である必要がある。
Solidityでこの乱数を生成する際にはkeccak256 関数を使うのが良いよう。これでrandom hashに変換する。その上で必要な乱数を hashからuintにする。
以前に使用した% modulusで桁数を制限してやれば良い。余りは下位の桁となるのでそちらを取得する。
3456÷1000=3・・・456
3桁のuintを取得したい場合には1000を使用し、456を取得する。
uint id = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % modulus;
getLatestEthPrice() の中身は下記のようになった。乱数idを返し、pendingRequestsにてidステータスをtrueに設定。そして呼ばれたidをreturnする。つまり、単にoracleコントラクトで擬似乱数生成をして、通常のコントラクトで取得しているだけ。
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;
}
setLatestEthPriceの方はoracleとの連携になってくる。jsを通してBinance public API からethの価格を取得。ownerのみが呼べるようにしておく。
function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner{
require( pendingRequests[_id], "This request is not in my pending list.");
delete pendingRequests[_id];
}
あとはCallerContractInstanceのインスタンスを作成してcallbackを呼び出せるようにすることで、oracleコントラクトとスマートコントラクトの連携が確認できる。
function setLatestEthPrice(uint256 _ethPrice, address _callerAddress, uint256 _id) public onlyOwner {
require(pendingRequests[_id], "This request is not in my pending list.");
delete pendingRequests[_id];
CallerContractInterface callerContractInstance;
CallerContractInstance = CallerContractInterface(_callerAddress);
callerContractInstance.callback(_ethPrice, _id);
emit SetLatestEthPriceEvent(_ethPrice, _callerAddress);
}
ここまではコントラクト間のみの対話なので、ETH priceをthe Binance APIから取得するjsコンポーネントoracleへの連携を見ていく。
Oracle本体はEthPriceOracle.jsのようにかけて、スパコンのような特別なハードウェアではなくプログラムの一つであることが理解できる。
Oracleコントラクトと連携するためにconst OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json’)のような形でbuild artifactsをインポートする。これはスマートコントラクトのデプロイ時に生成されるバイトコードとABIなど。oracleコントラクトと連携する方法と考えておけばわかりやすい。
The build artifacts are comprised of the bytecode versions of the smart contracts, ABIs, and some internal data Truffle is using to correctly deploy the code.
An ABI describes how functions can be called and how data is stored in a machine-readable format.
例えば、mapping(uint256=>bool) pendingRequests;のABIは下記のように表現される。
{
"constant": false,
"id": 143,
"name": "pendingRequests",
"nodeType": "VariableDeclaration",
"scope": 240,
"src": "229:38:2",
"stateVariable": true,
"storageLocation": "default",
"typeDescriptions": {
"typeIdentifier": "t_mapping$_t_uint256_$_t_bool_$",
"typeString": "mapping(uint256 => bool)"
},
この構造をjs側で取得することになる。
それがconst OracleJSON = require('./oracle/build/contracts/EthPriceOracle.json’)。
ここからsetLatestEthPriceを呼び出すためにはまずweb3に接続する必要がある。これはフロントエンドへの接続と同じ流れ。
const myContract = new web3js.eth.Contract(myContractJSON.abi, myContractJSON.networks[networkId].address)
ここではloomチェーンへの接続の流れで確認しているのでExtdev testnetとなる。networkIdはそちらのもの。
下記のように記述すればチェーンごとの差異を無くして自動的に処理してくれる。
const networkId = await web3js.eth.net.getId()
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);
}
だんだんoracleの仕組みに近づけてきた。
さらにここにイベントを感知する仕組みも追加。
例えば、oracleコントラクトでemit GetLatestEthPriceEvent(msg.sender, id);でイベントを発行していたら、フロントエンドでは、下記のような形で受け取れる。フィルターとなる{ filter: { myParam: 1 }}は加えても加えなくても問題ない。
ontract.events.EventName({ filter: { myParam: 1 }}, async (err, event) => {
if (err) {
console.error('Error on event', err)
return
}
// Do something
})
さらに進めると、GetLatestEthPriceEventの発生が確認されるとaddRequestToQueueを実行させる仕組みが構築できる。通知受け取りからイベント実行へのコンボ。
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
}
})
}
つまり、コントラクトからevent TransferTokens(address from, address to, uint256 amount)の通知が来たらそれを下記のようにevent.returnValues.で受け取ってしまえる。
async function parseEvent (event) {
const from = event.returnValues.from
const to = event.returnValues.to
const amount = event.returnValues.amount
}
Pushも加えると以下のようにできる。
async function addRequestToQueue (event){
const callerAddress = event.returnValues.callerAddress
const id = event.returnValues.id
pendingRequests.push({ callerAddress, id })
}
ただ、oracleコントラクトがひっきりなしにGetLatestEthPriceEventを発行してしまうと、oracl本体での処理が面倒なものになる。なのでSLEEP_INTERVALを導入して間隔を緩める。
実行中のrequestの管理機能。まだ、Binanceの方には繋がないよう。イベント着火からくるrequest処理が結構大変なのかも。shift()は一番先頭の配列を取得するもの。配列からは除外される。
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++
}
ただ、もしエラーが発生した場合リトライの永遠のループ現象が生じる。このループ地獄から抜け出すわざも追加する必要がある。
これは< MAX_RETRIESで制限すれば良い。try/catchを組み込んでテストをしてエラーがあったらcatchの中身を実行させるようにする。
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++
}
また、EVMは小数点のの扱えないので、10の階上を行い小数点を無くして対応する。
The Ethereum Virtual Machine doesn't support floating-point numbers, meaning that divisions truncate the decimals.
Binance API は小数8位まで使うよう。10**10をかけて対処することになるが、これは8+10=18でweiになるため。ethでは扱えないのでweiに換算して一旦取得するというやり方。(eth=wei*10**10)
The Binance API returns eight decimals numbers and we'll also multiply this by 10**10.
その上でjsでは16桁までしか扱えないのでBN.js というライブラリを使用することで18桁まで対応できるようにする。
(一般的にJavaScriptの数値型Numberは倍精度浮動小数点で表現されるため,最大の整数はNumber.MAX_SAFE_INTEGER=9007199254740991)
強引に.を取り、BNにならす。
aNumber = aNumber.replace('.', '')
const aNumber = new BN(aNumber, 10)
これを使うと以下のようにできるとのこと。いまいちBNの使い方がわからぬ。
ethPrice = ethPrice.replace('.', '')
const multiplier = new BN(10**10, 10)
const ethPriceInt = (new BN(parseInt(ethPrice), 10)).mul(multiplier)
ただ、ついにバイナンスとの接続も見えてきた。
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
}