Skip to content

Latest commit

 

History

History
320 lines (283 loc) · 13.3 KB

09.md

File metadata and controls

320 lines (283 loc) · 13.3 KB
title actions requireLogin material
Working with Numbers in Ethereum and JavaScript
checkAnswer
hints
true
editor
language startingCode answer
JavaScript
EthPriceOracle.js utils/common.js oracle/EthPriceOracle.sol caller/CallerContract.sol caller/EthPriceOracleInterface.sol oracle/CallerContractInterface.sol
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 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 } }
const fs = require('fs') const Web3 = require('web3') const { Client, NonceTxMiddleware, SignedTxMiddleware, LocalAddress, CryptoUtils, LoomProvider } = require('loom-js') function loadAccount (privateKeyFileName) { const extdevChainId = 'extdev-plasma-us1' const privateKeyStr = fs.readFileSync(privateKeyFileName, 'utf-8') const privateKey = CryptoUtils.B64ToUint8Array(privateKeyStr) const publicKey = CryptoUtils.publicKeyFromPrivateKey(privateKey) const client = new Client( extdevChainId, 'wss://extdev-plasma-us1.dappchains.com/websocket', 'wss://extdev-plasma-us1.dappchains.com/queryws' ) client.txMiddleware = [ new NonceTxMiddleware(publicKey, client), new SignedTxMiddleware(privateKey) ] client.on('error', msg => { console.error('Connection error', msg) }) return { ownerAddress: LocalAddress.fromPublicKey(publicKey).toString(), web3js: new Web3(new LoomProvider(client, privateKey)), client } } module.exports = { loadAccount, };
pragma solidity 0.5.0; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "./CallerContractInterface.sol"; contract EthPriceOracle is Ownable { uint private randNonce = 0; uint private modulus = 1000; mapping(uint256=>bool) pendingRequests; event GetLatestEthPriceEvent(address callerAddress, uint id); event SetLatestEthPriceEvent(uint256 ethPrice, address callerAddress); 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; } 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); } }
pragma solidity 0.5.0; import "./EthPriceOracleInterface.sol"; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract CallerContract is Ownable { uint256 private ethPrice; EthPriceOracleInterface private oracleInstance; address private oracleAddress; mapping(uint256=>bool) myRequests; event newOracleAddressEvent(address oracleAddress); event ReceivedNewRequestIdEvent(uint256 id); event PriceUpdatedEvent(uint256 ethPrice, uint256 id); function setOracleInstanceAddress (address _oracleInstanceAddress) public onlyOwner { oracleAddress = _oracleInstanceAddress; oracleInstance = EthPriceOracleInterface(oracleAddress); emit newOracleAddressEvent(oracleAddress); } function updateEthPrice() public { uint256 id = oracleInstance.getLatestEthPrice(); myRequests[id] = true; emit ReceivedNewRequestIdEvent(id); } function callback(uint256 _ethPrice, uint256 _id) public onlyOracle { require(myRequests[_id], "This request is not in my pending list."); ethPrice = _ethPrice; delete myRequests[_id]; emit PriceUpdatedEvent(_ethPrice, _id); } modifier onlyOracle() { require(msg.sender == oracleAddress, "You are not authorized to call this function."); _; } }
pragma solidity 0.5.0; contract EthPriceOracleInterface { function getLatestEthPrice() public returns (uint256); }
pragma solidity 0.5.0; contract CallerContractInterface { function callback(uint256 _ethPrice, uint256 id) public; }
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 } }

Remember we've mentioned that data needs a bit of massaging before it's sent to the oracle contract. Let's look into why.

The Ethereum Virtual Machine doesn't support floating-point numbers, meaning that divisions truncate the decimals. The workaround is to simply multiply the numbers in your front-end by 10**n. The Binance API returns eight decimals numbers and we'll also multiply this by 10**10. Why did we choose 10**10? There's a reason: one ether is 10**18 wei. This way, we'll be sure that no money will be lost.

But there's more to it. The Number type in JavaScript is "double-precision 64-bit binary format IEEE 754 value" which supports only 16 decimals...

Luckily, there's a small library called BN.js that'll help you overcome these issues.

☞ For the above reasons, it's recommended that you always use BN.js when dealing with numbers.

Now, the Binance API returns something like 169.87000000.

Let's see how you can convert this to BN.

First, you'll have to get rid of the decimal separator (the dot). Since JavaScript is a dynamically typed language (that's a fancy way of saying that the interpreter analyzes the values of the variables at runtime and, based on the values, it assigns them a type), the easiest way to do this is...

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

Continuing with this example, converting aNumber to BN would look something like this:

const aNumber = new BN(aNumber, 10)

Note: The second argument represents the base. Make sure it's always specified.

We've gone ahead and filled in almost all the code that goes to the setLatestEthPrice function. Here's what's left for you to do.

Put It to the test

  1. The function takes a parameter called ethPrice which is the actual value returned by the Binance API. Let's remove the . by running the replace function on it.
  2. Create a const called multiplier. Initialize it with 10**10 typecasted as a BN. This should be almost similar to the aNumber example above.