Merge pull request #29 from nhaga/pages/oracle_guide
Adding Milkomeda Open Oracle dev guide
Adding Milkomeda Open Oracle dev guide
---
sidebar_label: "Milkomeda Open Oracle"
---
# Milkomeda Open Oracle Developer guide
:::info
Milkomeda C1 Sidechain is fully operational on Mainnet, which means that it is currently deployed and connected to production version of the Cardano blockchain.
:::
## Introduction
Milkomeda Open Oracle is a decentralized oracle service with a minimal governance system. It permits multiple owners to post data, and the majority of the current owners can add or remove owners. One of its fundamental services is the Price Feed, which offers precise and real-time price data for the USD/ADA pricefeeds.
Sources are checked every ~10 seconds, outliers are removed if the prices they provide differ from the mean by more than 2 standard deviations and the mean of the remaining prices is calculated as the final value. If the new mean price differs from the last price posted on the blockchain by more than 1%, the new mean price is posted and the price is updated on the blockchain.
Developers have two options to fetch prices from the oracle contract. They can integrate the oracle interface into their smart contracts, allowing them to access on-chain data updated at regular intervals. Alternatively, developers can call the oracle contract directly with Javascript/Typescript to use the price feed on a DApp frontend.
In this guide, we will provide an example of each alternative, requiring the Milkomeda Open Oracle's ABI, which is accessible [here](https://raw.githubusercontent.com/dcSpark/milkomeda-guides/open_oracle/javascript/Oracle.json), and the contract's deployment address, found in the following table.
| Milkomeda C1 | Open Oracle Deployment address |
|---|---|
| Mainnet | 0xc531410f61FA22e19048D406EDE3361b3de5c386 |
| Testnet | 0x47a7d67e89E5714456b9af39703C1dc62203002A |
It is crucial to note that to leverage the capabilities of the Milkomeda Open Oracle, the caller of the read function must agree to the terms of service, which are accessible [here](https://ext-oracle-disclaimer.milkomeda.com/disclaimer.pdf). This is a one-time requirement (per address), and we will provide guidance on how to accomplish that in this guide.
For the purposes of this tutorial, we will be utilizing the Milkomeda C1 Testnet. However, the implementation process is identical on the C1 Mainnet, and the only adjustment necessary would be the deployment address.
:::tip
You can choose to follow allong completing the coding steps or download the full code for this tutorial by running:
```
npx degit dcspark/milkomeda-guides#open_oracle oracle_guide
```
:::
## Calling Price Feed using Javascript
To call the price feed using Javascript, we will use the `ethers` (^6) package, under the assumption that the Terms of Service have not yet been accepted, indicating that the contract is being called for the first time from our address.
Let’s create a new javascript project with a script that can be called from the command line. This can easily be adapted to be used for a frontend in a browser.
Navigate to a new folder where you want to create your project.In a new folder, create an empty project and add the ethers and dotenv packages.
```bash
yarn init -y
yarn add ethers dotenv
```
Navigate to the root directory of your project. Create a .env file with the private key for the account you intend to use:
```
PRIVATE_KEY=<PRIVATE_KEY>
```
NOTE: Remember to keep your private key safe from prying eyes. It is recommended to use a new separate account just for testing purposes.
Now let’s create a file named “callPriceFeed.js” and add the imports and initial setup.
```javascript
const ethers = require("ethers");
require("dotenv").config();
const abi = require('./Oracle.json').abi
const rpcURL = "https://rpc-devnet-cardano-evm.c1.milkomeda.com"
const provider = new ethers.JsonRpcProvider(rpcURL)
const signer = new ethers.Wallet( `0x${process.env.PRIVATE_KEY}`, provider )
const oracle = new ethers.Contract("0x47a7d67e89E5714456b9af39703C1dc62203002A", abi, signer)
```
After importing the `ethers` and `dotenv` package, we’re importing the `Oracle.json` that contains the ABI and setting up a provider, signer and an instance of the oracle contract. Notice we are using the rpc URL and deployment address for Milkomeda C1 Testnet.
To get the price feed for USD from the oracle we need to call the readData() function, like so:
```javascript
const getPriceFeed = async () => {
const price = await oracle.readData()
console.log("Price:", ethers.formatEther(price))
}
getPriceFeed()
```
However, if you are running the readData function for the first time, the transaction will fail because, although it is a read function, it requires the caller of the function to have accepted the Term of Service. That acceptance is stored in the smart contract as a mapping and can be queried by calling the acceptedTermsOfService function and providing an address.
```javascript
oracle.acceptedTermsOfService(signer.address)
.then(resp => console.log("Accepted Terms of Service:", resp))
```
Making this call should output “Accepted Terms of Service: false” which means that calling the readData() function will fail.
In the `getPriceFeed` function of our script, we can add a test to check if the terms of service have been accepted and call the accept function if needed. Let’s add that to the `getPriceFeed` function.
We will also add a function call to retrieve the description of the price feed:
```javascript
oracle.description().then(resp => console.log("Description:", resp))
```
The final script will look like this:
```javascript
const ethers = require("ethers");
require("dotenv").config();
const abi = require('./Oracle.json').abi
const rpcURL = "https://rpc-devnet-cardano-evm.c1.milkomeda.com"
const provider = new ethers.JsonRpcProvider(rpcURL)
const signer = new ethers.Wallet( `0x${process.env.PRIVATE_KEY}`, provider )
const oracle = new ethers.Contract("0x47a7d67e89E5714456b9af39703C1dc62203002A", abi, signer)
// oracle.acceptedTermsOfService(signer.address)
// .then(resp => console.log("Accepted Terms of Service:", resp))
oracle.description()
.then(resp => console.log("Description:", resp))
const getPriceFeed = async () => {
const accepted = await oracle.acceptedTermsOfService(signer.address)
if (!accepted) {
console.log("Accepting ToS...")
const tx = await oracle.acceptTermsOfService();
await tx.wait();
}
const price = await oracle.readData()
console.log("Price:", ethers.formatEther(price))
}
getPriceFeed()
```
Running node CallPriceFeed for the first time will output the following lines (the price will probably be different):
**
Description: ADA/USD<br></br>
Accepting ToS...<br></br>
Price: 2.8398199554148265
**
Subsequent calls to the function, from the same address, will only show the description and the price since the call to `acceptedTermsOfService(address)` will return true.
:::info
The result from the price feed, approx 2.84 (formatted from 18 decimals), is the amount of USD in ADA. Readers are probably used to seeing the price of ADA in USD, which would be the inverse of the result.
USD/ADA = 2.8398199554148265
ADA/USD = 1 / (USD/ADA) = 1 / 2.8398199554148265 = 0.352135
:::
## Integrate Milkomeda Open Oracle into your Smart Contracts using Hardhat
To use the oracle in our smart contract, we need to interact with the oracle contract on the relevant chain while ensuring that our smart contract accepts the terms of service.
For testing purposes, we can use Hardhat, a development environment that enables us to compile, deploy, and test smart contracts in a local environment.
To set up Hardhat, create a new folder, initialize a JavaScript project, and install Hardhat by executing the following commands in your terminal:
```bash
yarn init -y
yarn add -D hardhat
```
Once Hardhat is installed, we need to initialize a new Hardhat project:
```bash
npx hardhat init
```
This will create a new Hardhat project with a basic directory structure and some example files.
Next, we need to create a new smart contract that will use the Milkomeda Open Oracle. Let's call it PriceFeedContract.sol.
Create a new file called PriceFeedContract.sol in the contracts directory of your project. Then, add the following code:
```javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IOracle {
function readData() external view returns (uint256);
function acceptTermsOfService() external;
}
Bumps [bech32](https://github.com/rust-bitcoin/rust-bech32) from 0.8.1 to 0.9.1. - [Changelog](https://github.com/rust-bitcoin/rust-bech32/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-bitcoin/rust-bech32/compare/v0.8.1...v0.9.1) --- updated-dependencies: - dependency-name: bech32 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]>
Bumps [log](https://github.com/rust-lang/log) from 0.4.17 to 0.4.18. - [Release notes](https://github.com/rust-lang/log/releases) - [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/log/compare/0.4.17...0.4.18) --- updated-dependencies: - dependency-name: log dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.28.1 to 1.28.2. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.28.1...tokio-1.28.2) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
PLT-5901 Implemented checks for valid network addresses.
fix: tuple clause must preserve previous clause properties state
fix: rearrange clauses and fill in gaps now handles nested patterns in a uniform way fix: discards in records was being sorted incorrectly leading to type issues chore: remove some filter maps in cases where None is impossible anyway chore: some refactoring on a couple functions to clean up
fix: tuple clause must preserve previous clause properties state