Fetching Price Data with Supra Oracles¶
Introduction¶
Oracles play a crucial role in blockchain ecosystems by facilitating the interaction between smart contracts and external data sources.
Supra Oracles is one Oracle service provider that enables you to retrieve price data from external services and feed it to smart contracts to validate the accuracy of such data and publish it on-chain. Supra achieves this flow using a pull model that fetches price data as needed.
In this guide, you'll learn about Supra's pull model and how to use their price feeds to fetch price data in smart contracts on Moonbeam.
An Overview of Supra's V1 Pull Model¶
Supra uses a pull model as a customized approach that publishes price data upon request. It combines Web2 and Web3 methods to achieve low latency when sending data from Supra to destination chains. The process involves the following steps:
- Web2 methods are used to retrieve price data from Supra
- Smart contracts are utilized for cryptographically verifying and writing the latest price data on-chain, where it lives on immutable ledgers, using Supra's Pull contract
- Once the data has been written on-chain, the most recently published price feed data will be available in Supra's Storage contract
You can fetch price data from Supra for any available data pairs.
The addresses for Supra's contracts on Moonbeam are as follows:
Contract | Address |
---|---|
Pull Oracle | 0x2FA6DbFe4291136Cf272E1A3294362b6651e8517 |
Storage | 0xD02cc7a670047b6b012556A88e275c685d25e0c9 |
Contract | Address |
---|---|
Pull Oracle | 0xaa2f56843Cec7840F0C106F0202313d8d8CB13d6 |
Storage | 0x4591d1B110ad451d8220d82252F829E8b2a91B17 |
Note
Moonriver is not supported at this time.
Checking Prerequisites¶
To follow along with this guide, you will need:
- An account with funds. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
Use Web2 Code to Retrieve Price Data¶
To build out the Web2 component required to fetch price data from Supra, you can use their Pull Service Client library, designed to interact with a gRPC server to fetch price data. gRPC is a modern remote procedure call (RPC) framework created by Google. You can check out the gRPC documentation for more information if you need to familiarize yourself.
The library offers JavaScript or Rust-based clients for EVM, Sui, and Aptos-based chains. For Moonbeam, you can use the JavaScript or Rust-based EVM client. We'll use the JavaScript client.
We'll copy the JavaScript client code and add it to our project, but you can also clone the repository with all the clients.
Create a Project¶
Follow these steps to create your project:
-
Create an empty project directory
mkdir moonbeam-supra
-
Create a basic
package.json
file for your projectcd moonbeam-supra && npm init-y
-
Install dependencies needed to work with Supra's gRPC server
npm install @grpc/grpc-js @grpc/proto-loader
Create the Pull Service Client¶
To create the pull service client, you'll need to create two files: one that defines the schema for the gRPC service, client.proto
, and another that relies on the schema and is used to fetch the proof for a given data pair, pullServiceClient.js
.
You can create both files using the following command:
touch client.proto pullServiceClient.js
Then, you can copy the following code snippets and add them to their respective files:
Pull service client files
syntax = "proto3";
package pull_service;
message PullResponse {
oneof resp {
PullResponseEvm evm = 1;
PullResponseSui sui = 2;
PullResponseAptos aptos = 3;
}
}
service PullService {
rpc GetProof(PullRequest) returns (PullResponse);
}
message PullRequest {
repeated uint32 pair_indexes = 1;
string chain_type = 2;
}
message PullResponseEvm {
repeated uint32 pair_indexes = 1;
bytes proof_bytes = 2;
}
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
class PullServiceClient {
constructor(address) {
var PROTO_PATH = __dirname + './client.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const pullProto =
grpc.loadPackageDefinition(packageDefinition).pull_service;
this.client = new pullProto.PullService(
address,
grpc.credentials.createSsl()
);
}
getProof(request, callback) {
this.client.getProof(request, callback);
}
}
module.exports = PullServiceClient;
Use the Pull Service Client to Fetch Price Data¶
In this section, you'll create an instance of the PullServiceClient
to retrieve the proof for the ETH_USDT pair. You can modify this example for any of the available data pairs.
To get started, create a file to add our logic:
touch main.js
In the main.js
file, take the following steps to add the logic for retrieving proof data:
-
Import the
PullServiceClient
from thepullServiceClient.js
filemain.jsconst PullServiceClient = require('./pullServiceClient');
-
Create a variable to store the index of the data pair for which you want to retrieve the price data. This example requests the ETH_USDT data pair, but you can use the index of any available data pair
main.jsconst PullServiceClient = require('./pullServiceClient'); const pairIndex = 1;
-
Create a
getProofs
function, where you'll add all the logicmain.jsconst getProofs = () => { // Add logic };
-
In the
getProofs
function, you can define the address for the gRPC server and use it to create an instance of thePullServiceClient
. Supra has one address for MainNets,'mainnet-dora.supraoracles.com'
and one for TestNets,'testnet-dora.supraoracles.com'
main.jsconst getProofs = () => { const address = 'mainnet-dora.supraoracles.com'; const client = new PullServiceClient(address); };
main.jsconst getProofs = () => { const address = 'testnet-dora.supraoracles.com'; const client = new PullServiceClient(address); };
-
To request data from the client, first, you need to define the data you want to request
main.jsconst getProofs = () => { // ... const request = { pair_indexes: [pairIndex], // ETH_USDT chain_type: 'evm', }; };
-
Now you can request the proof for the data pair(s) by calling the
getProof
method of the Pull Service Clientmain.jsconst getProofs = () => { // ... return new Promise((resolve, reject) => { client.getProof(request, (err, response) => { if (err) { console.error('Error:', err.details); return; } resolve(response); }); }); };
-
Create a
main
function that calls thegetProofs
function and saves the proofs to be consumed in later stepsmain.jsconst main = async () => { const proofs = await getProofs(); }; main();
main.js
const PullServiceClient = require('./pullServiceClient');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'mainnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
const main = async () => {
const proofs = await getProofs();
};
main();
const PullServiceClient = require('./pullServiceClient');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'testnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
const main = async () => {
const proofs = await getProofs();
};
main();
So far, you have the logic required to retrieve proofs for data pairs. The proofs are bytes of data that are not human-readable, but you can follow the steps in the next section to deserialize the data into human-readable formats. This step is optional, so you can skip ahead to verify the proofs and write the price data on-chain.
Deserialize the Proofs¶
If you want to deserialize the data to read the latest price data you've retrieved, you can use the interfaces for the proof data and the signed coherent cluster data.
Coherent cluster data is a set of values where all the values in that set agree. This is a component of Supra's DORA (Distributed Oracle Agreement) protocol, which, in its simplest form, is a protocol that aggregates a set of data into a single representative value. If you want to dive deeper, check out the DORA litepaper.
You'll need to create a file for each interface, which you can store in a resources
directory:
mkdir resources && touch resources/oracleProof.json resources/signedCoherentCluster.json
Then, you can copy the following code snippets and add them to their respective files:
Interface files
[
{
"type": "tuple",
"name": "OracleProof",
"components": [
{
"type": "tuple[]",
"name": "votes",
"components": [
{
"type": "tuple",
"name": "smrBlock",
"components": [
{ "type": "uint64", "name": "round" },
{ "type": "uint128", "name": "timestamp" },
{ "type": "bytes32", "name": "author" },
{ "type": "bytes32", "name": "qcHash" },
{ "type": "bytes32[]", "name": "batchHashes" }
]
},
{ "type": "bytes8", "name": "roundLE" }
]
},
{ "type": "uint256[2][]", "name": "sigs" },
{
"type": "tuple[]",
"name": "smrBatches",
"components": [
{ "type": "bytes10", "name": "protocol" },
{ "type": "bytes32[]", "name": "txnHashes" },
{ "type": "uint256", "name": "batchIdx" }
]
},
{
"type": "tuple[]",
"name": "smrTxns",
"components": [
{ "type": "bytes32[]", "name": "clusterHashes" },
{ "type": "bytes32", "name": "sender" },
{ "type": "bytes10", "name": "protocol" },
{ "type": "bytes1", "name": "tx_sub_type" },
{ "type": "uint256", "name": "txnIdx" }
]
},
{ "type": "bytes[]", "name": "clustersRaw" },
{ "type": "uint256[]", "name": "batchToVote" },
{ "type": "uint256[]", "name": "txnToBatch" },
{ "type": "uint256[]", "name": "clusterToTxn" },
{ "type": "uint256[]", "name": "clusterToHash" },
{ "type": "bool[]", "name": "pairMask" },
{ "type": "uint256", "name": "pairCnt" }
]
}
]
[
{
"type": "tuple",
"name": "scc",
"components": [
{
"type": "tuple",
"name": "cc",
"components": [
{ "type": "bytes32", "name": "dataHash" },
{ "type": "uint256[]", "name": "pair" },
{ "type": "uint256[]", "name": "prices" },
{ "type": "uint256[]", "name": "timestamp" },
{ "type": "uint256[]", "name": "decimals" }
]
},
{ "type": "bytes", "name": "qc" },
{ "type": "uint256", "name": "round" },
{
"type": "tuple",
"name": "origin",
"components": [
{ "type": "bytes32", "name": "_publicKeyIdentity" },
{ "type": "uint256", "name": "_pubMemberIndex" },
{ "type": "uint256", "name": "_committeeIndex" }
]
}
]
}
]
To work with these interfaces, you must install the Ethereum library of your choice. For this example, we'll use Web3.js.
npm i web3
Next, you can take the following steps to create a function that deserializes the proof data:
-
In the
main.js
file, import the interfaces and Web3main.jsconst oracleProofABI = require('./resources/oracleProof.json'); const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json'); const { Web3 } = require('web3');
-
Create a Web3 instance, which will be used to interact with the interfaces. You can add this snippet directly after the imports
main.jsconst web3 = new Web3('https://rpc.api.moonbeam.network');
main.jsconst web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
-
Create a
deserializeProofBytes
function to add all the logic for deserializing the proofs. The function should accept the proof formatted in hex as a parametermain.jsconst deserializeProofBytes = (proofHex) => { // Add logic here };
-
First you can decode the parameters of the proof data using the Oracle Proof interface and extract the raw bytes of the signed pair cluster data and which pair IDs have been requested
main.jsconst deserializeProofBytes = (proofHex) => { const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex); // Fatching the raw bytes of the signed pair cluster data const clusters = proof_data[0].clustersRaw; // Fetching which pair IDs have been requested const pairMask = proof_data[0].pairMask; };
-
Next, you can iterate over the signed pair cluster data and decode the parameters using the Signed Coherent Cluster interface, and then save the data for each pair to variables that you can log to the console
main.jsconst deserializeProofBytes = (proofHex) => { // ... // Helps in iterating the vector of pair masking let pair = 0; // Lists of all the pair IDs, prices, decimals, and timestamps requested const pairId = []; const pairPrice = []; const pairDecimal = []; const pairTimestamp = []; for (let i = 0; i < clusters.length; ++i) { // Deserialize the raw bytes of the signed pair cluster data const scc = web3.eth.abi.decodeParameters( signedCoherentClusterABI, clusters[i] ); for (let j = 0; j < scc[0].cc.pair.length; ++j) { pair += 1; // Verify whether the pair is requested or not if (!pairMask[pair - 1]) { continue; } // Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector pairId.push(scc[0].cc.pair[j].toString(10)); pairPrice.push(scc[0].cc.prices[j].toString(10)); pairDecimal.push(scc[0].cc.decimals[j].toString(10)); pairTimestamp.push(scc[0].cc.timestamp[j].toString(10)); } } console.log('----- Deserialized Data ------'); console.log('Pair index : ', pairId); console.log('Pair Price : ', pairPrice); console.log('Pair Decimal : ', pairDecimal); console.log('Pair Timestamp : ', pairTimestamp); console.log('------------------------------'); };
-
In the
main
function that you created in the previous section, you can convert theproofs
to hex and call thedeserializeProofBytes
functionmain.jsconst main = async () => { const proofs = await getProofs(); // Convert oracle proof bytes to hex const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes); deserializeProofBytes(hex); }; main();
main.js
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'mainnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
};
main();
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'testnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
};
main();
When you request the proof data, you can view that data in a human-readable format. You can try it out by running:
node main.js
The terminal output should look something like the following:
----- Deserialized Data ------
Pair index : [ '1' ]
Pair Price : [ '3424260000000000000000' ]
Pair Decimal : [ '18' ]
Pair Timestamp : [ '1709317443269' ]
------------------------------
Use Web3 to Verify and Publish the Proofs¶
Now that we've retrieved the price data, we need to be able to consume it to verify and publish the data on-chain. To do this, we'll need a smart contract that uses Supra's Pull contract to verify the proof data.
Create the Consumer Contract¶
You can take the following steps to create our smart contract:
-
Create a new file for the smart contract, which we'll name
OracleClient
touch OracleClient.sol
-
In the file, create an interface for Supra's Pull contract. The interface outlines the data structure for the price data and has a function that we'll call to verify proofs. Then, in our
OracleClient
contract, we'll instantiate theISupraOraclePull
interface with the address of Supra's Pull contract on Moonbeam or Moonbase AlphaOracleClient.sol// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.20; interface ISupraOraclePull { // Verified price data struct PriceData { // List of pairs uint256[] pairs; // List of prices // prices[i] is the price of pairs[i] uint256[] prices; // List of decimals // decimals[i] is the decimals of pairs[i] uint256[] decimals; } function verifyOracleProof( bytes calldata _bytesProof ) external returns (PriceData memory); }
-
In the same file, create the
OracleClient
contract. As mentioned in the previous step, the constructor of this contract instantiates theISupraOraclePull
interface with the address of Supra's Pull contract. The contract also includes a function that calls theverifyOracleProof
function of the Pull contract and saves the price data on-chainOracleClient.sol// ... // Contract which can consume oracle pull data contract OracleClient { // The oracle contract ISupraOraclePull internal oracle; // Event emitted when a pair price is received event PairPrice(uint256 pair, uint256 price, uint256 decimals); constructor(address oracle_) { oracle = ISupraOraclePull(oracle_); } function GetPairPrice( bytes calldata _bytesProof, uint256 pair ) external returns (uint256) { // Verify the proof ISupraOraclePull.PriceData memory prices = oracle.verifyOracleProof( _bytesProof ); // Set the price and decimals for the requested data pair uint256 price = 0; uint256 decimals = 0; for (uint256 i = 0; i < prices.pairs.length; i++) { if (prices.pairs[i] == pair) { price = prices.prices[i]; decimals = prices.decimals[i]; break; } } require(price != 0, "Pair not found"); return price; } }
OracleClient.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
interface ISupraOraclePull {
// Verified price data
struct PriceData {
// List of pairs
uint256[] pairs;
// List of prices
// prices[i] is the price of pairs[i]
uint256[] prices;
// List of decimals
// decimals[i] is the decimals of pairs[i]
uint256[] decimals;
}
function verifyOracleProof(
bytes calldata _bytesProof
) external returns (PriceData memory);
}
// Contract which can consume oracle pull data
contract OracleClient {
// The oracle contract
ISupraOraclePull internal oracle;
// Event emitted when a pair price is received
event PairPrice(uint256 pair, uint256 price, uint256 decimals);
constructor(address oracle_) {
oracle = ISupraOraclePull(oracle_);
}
function GetPairPrice(
bytes calldata _bytesProof,
uint256 pair
) external returns (uint256) {
// Verify the proof
ISupraOraclePull.PriceData memory prices = oracle.verifyOracleProof(
_bytesProof
);
// Set the price and decimals for the requested data pair
uint256 price = 0;
uint256 decimals = 0;
for (uint256 i = 0; i < prices.pairs.length; i++) {
if (prices.pairs[i] == pair) {
price = prices.prices[i];
decimals = prices.decimals[i];
break;
}
}
require(price != 0, "Pair not found");
return price;
}
}
Note
This contract only saves the price data for one pair. So, if you want to save the price data for multiple pairs, you must modify the contract.
Deploy the Contract¶
With the contract created, you must next deploy the contract. Since we've already installed Web3.js, let's use it to deploy the contract. If you're unfamiliar with the process, you can reference the Web3.js docs on deploying a smart contract.
To deploy the contract, take the following steps:
-
Create a file that will contain the logic for compiling and deploying the smart contract
touch deploy.js
-
Install the Solidity compiler. We're installing version 0.8.20, as that is the version required by the
OracleClient
contractnpm i solc@0.8.20
-
Add the following imports
deploy.jsconst solc = require('solc'); const { Web3 } = require('web3');
-
Create the Web3 instance
const web3 = new Web3('https://rpc.api.moonbeam.network');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
-
Create a function that compiles the
OracleClient
contract, saves the ABI in theresources
directory for later use, and returns the ABI and bytecode for the deploymentdeploy.jsconst compile = () => { // Get path and load contract const source = fs.readFileSync('OracleClient.sol', 'utf8'); // Create input object const input = { language: 'Solidity', sources: { 'OracleClient.sol': { content: source, }, }, settings: { outputSelection: { '*': { '*': ['*'], }, }, }, }; // Compile the contract const tempFile = JSON.parse(solc.compile(JSON.stringify(input))); const contractFile = tempFile.contracts['OracleClient.sol']['OracleClient']; // Save ABI to a file fs.writeFileSync( './resources/oracleClient.json', JSON.stringify(contractFile.abi, null, 4), 'utf8' ); return { abi: contractFile.abi, bytecode: contractFile.evm.bytecode.object }; };
-
Create the function to deploy the compiled contract. You'll need to pass the address of Supra's Pull contract to the constructor. You'll also need to provide your address and your private key
Remember
Never store your private key in a JavaScript file; this is for demo purposes only.
deploy.jsconst deploy = async () => { // Compile the contract const { abi, bytecode } = compile(); // Create contract instance const contract = new web3.eth.Contract(abi); // Create the deployment transaction and pass in the Pull Oracle contract address const deployTx = contract.deploy({ data: bytecode, arguments: ['0x2FA6DbFe4291136Cf272E1A3294362b6651e8517'], }); // Sign transaction with PK const createTransaction = await web3.eth.accounts.signTransaction( { data: deployTx.encodeABI(), gas: await deployTx.estimateGas(), gasPrice: await web3.eth.getGasPrice(), nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'), }, 'INSERT_PRIVATE_KEY' ); // Send transaction and wait for receipt const createReceipt = await web3.eth.sendSignedTransaction( createTransaction.rawTransaction ); console.log(`Contract deployed at address: ${createReceipt.contractAddress}`); }; deploy();
deploy.jsconst deploy = async () => { // Compile the contract const { abi, bytecode } = compile(); // Create contract instance const contract = new web3.eth.Contract(abi); // Create the deployment transaction and pass in the Pull Oracle contract address const deployTx = contract.deploy({ data: bytecode, arguments: ['0xaa2f56843Cec7840F0C106F0202313d8d8CB13d6'], }); // Sign transaction with PK const createTransaction = await web3.eth.accounts.signTransaction( { data: deployTx.encodeABI(), gas: await deployTx.estimateGas(), gasPrice: await web3.eth.getGasPrice(), nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'), }, 'INSERT_PRIVATE_KEY' ); // Send transaction and wait for receipt const createReceipt = await web3.eth.sendSignedTransaction( createTransaction.rawTransaction ); console.log(`Contract deployed at address: ${createReceipt.contractAddress}`); }; deploy();
deploy.js
const solc = require('solc');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbeam.network');
const compile = () => {
// Get path and load contract
const source = fs.readFileSync('OracleClient.sol', 'utf8');
// Create input object
const input = {
language: 'Solidity',
sources: {
'OracleClient.sol': {
content: source,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};
// Compile the contract
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['OracleClient.sol']['OracleClient'];
// Save ABI to a file
fs.writeFileSync(
'./resources/oracleClient.json',
JSON.stringify(contractFile.abi, null, 4),
'utf8'
);
return { abi: contractFile.abi, bytecode: contractFile.evm.bytecode.object };
};
const deploy = async () => {
// Compile the contract
const { abi, bytecode } = compile();
// Create contract instance
const contract = new web3.eth.Contract(abi);
// Create the deployment transaction and pass in the Pull Oracle contract address
const deployTx = contract.deploy({
data: bytecode,
arguments: ['0x2FA6DbFe4291136Cf272E1A3294362b6651e8517'],
});
// Sign transaction with PK
const createTransaction = await web3.eth.accounts.signTransaction(
{
data: deployTx.encodeABI(),
gas: await deployTx.estimateGas(),
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'),
},
'INSERT_PRIVATE_KEY'
);
// Send transaction and wait for receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(`Contract deployed at address: ${createReceipt.contractAddress}`);
};
deploy();
const solc = require('solc');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
const compile = () => {
// Get path and load contract
const source = fs.readFileSync('OracleClient.sol', 'utf8');
// Create input object
const input = {
language: 'Solidity',
sources: {
'OracleClient.sol': {
content: source,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};
// Compile the contract
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['OracleClient.sol']['OracleClient'];
// Save ABI to a file
fs.writeFileSync(
'./resources/oracleClient.json',
JSON.stringify(contractFile.abi, null, 4),
'utf8'
);
return { abi: contractFile.abi, bytecode: contractFile.evm.bytecode.object };
};
const deploy = async () => {
// Compile the contract
const { abi, bytecode } = compile();
// Create contract instance
const contract = new web3.eth.Contract(abi);
// Create the deployment transaction and pass in the Pull Oracle contract address
const deployTx = contract.deploy({
data: bytecode,
arguments: ['0xaa2f56843Cec7840F0C106F0202313d8d8CB13d6'],
});
// Sign transaction with PK
const createTransaction = await web3.eth.accounts.signTransaction(
{
data: deployTx.encodeABI(),
gas: await deployTx.estimateGas(),
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'),
},
'INSERT_PRIVATE_KEY'
);
// Send transaction and wait for receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(`Contract deployed at address: ${createReceipt.contractAddress}`);
};
deploy();
To deploy the contract, run:
node deploy.js
The contract's address will be printed to the terminal; save it as you'll need it in the following steps.
Contract deployed at address: 0xaf1207d950a2231937372cedc2e8ddfa22c40665
Call the Contract¶
To verify the proof data and publish the latest price on-chain, the last step you'll need to do is to create a function that calls the GetPairPrice
function of the OracleClient
contract.
Back in the main.js
file, take the following steps:
-
Import the ABI for the
OracleClient
contractmain.jsconst oracleClientABI = require('./resources/oracleClient.json');
-
Create a function that accepts the hex-formatted proof data and will be responsible for calling the
OracleClient
contractmain.jsconst callContract = async (proofHex) => { // Add logic here };
-
In the
callContract
function, create an instance of the deployedOracleClient
contract using the ABI and the contract address, which you should have saved from deploying the contract in the previous set of stepsmain.jsconst callContract = async (proofHex) => { const contractAddress = 'INSERT_CONTRACT_ADDRESS'; const contract = new web3.eth.Contract(oracleClientABI, contractAddress); };
-
Create the transaction object that will call the
GetPairPrice
function of theOracleClient
contract. You'll need to provide your address in the transaction objectmain.jsconst callContract = async (proofHex) => { const contractAddress = 'INSERT_CONTRACT_ADDRESS'; const contract = new web3.eth.Contract(oracleClientABI, contractAddress); // Create the transaction object using the hex-formatted proof and the index of the // data pair you requested price data for const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI(); const gasEstimate = await contract.methods .GetPairPrice(proofHex, pairIndex) .estimateGas(); const transactionObject = { from: 'INSERT_ADDRESS', to: contractAddress, data: txData, gas: gasEstimate, gasPrice: await web3.eth.getGasPrice(), }; };
-
Add logic for signing and sending the transaction. You'll need to provide your private key
Remember
Never store your private key in a JavaScript file; this is for demo purposes only.
main.jsconst callContract = async (proofHex) => { const contractAddress = 'INSERT_CONTRACT_ADDRESS'; const contract = new web3.eth.Contract(oracleClientABI, contractAddress); // Create the transaction object using the hex-formatted proof and the index of the // data pair you requested price data for const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI(); const gasEstimate = await contract.methods .GetPairPrice(proofHex, pairIndex) .estimateGas(); const transactionObject = { from: 'INSERT_ADDRESS', to: contractAddress, data: txData, gas: gasEstimate, gasPrice: await web3.eth.getGasPrice(), }; // Sign the transaction with the private key const signedTransaction = await web3.eth.accounts.signTransaction( transactionObject, 'INSERT_PRIVATE_KEY' ); // Send the signed transaction return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction); };
-
The last step is to call the
callContract
function from themain
functionmain.jsconst main = async () => { const proofs = await getProofs(); // Convert oracle proof bytes to hex const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes); deserializeProofBytes(hex); // Verify and write the latest price data on-chain const receipt = await callContract(hex); console.log('Transaction receipt:', receipt); }; main();
main.js
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const oracleClientABI = require('./resources/oracleClient.json');
const web3 = new Web3('https://rpc.api.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'mainnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
// Function to call the Oracle client to verify and publish the latest price data
const callContract = async (proofHex) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(oracleClientABI, contractAddress);
// Create the transaction object using the hex-formatted proof and the index of the
// data pair you requested price data for
const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI();
const gasEstimate = await contract.methods
.GetPairPrice(proofHex, pairIndex)
.estimateGas();
const transactionObject = {
from: 'INSERT_ADDRESS',
to: contractAddress,
data: txData,
gas: gasEstimate,
gasPrice: await web3.eth.getGasPrice(),
};
// Sign the transaction with the private key
const signedTransaction = await web3.eth.accounts.signTransaction(
transactionObject,
'INSERT_PRIVATE_KEY'
);
// Send the signed transaction
return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
// Verify and write the latest price data on-chain
const receipt = await callContract(hex);
console.log('Transaction receipt:', receipt);
};
main();
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const oracleClientABI = require('./resources/oracleClient.json');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'testnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
// Function to call the Oracle client to verify and publish the latest price data
const callContract = async (proofHex) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(oracleClientABI, contractAddress);
// Create the transaction object using the hex-formatted proof and the index of the
// data pair you requested price data for
const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI();
const gasEstimate = await contract.methods
.GetPairPrice(proofHex, pairIndex)
.estimateGas();
const transactionObject = {
from: 'INSERT_ADDRESS',
to: contractAddress,
data: txData,
gas: gasEstimate,
gasPrice: await web3.eth.getGasPrice(),
};
// Sign the transaction with the private key
const signedTransaction = await web3.eth.accounts.signTransaction(
transactionObject,
'INSERT_PRIVATE_KEY'
);
// Send the signed transaction
return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
// Verify and write the latest price data on-chain
const receipt = await callContract(hex);
console.log('Transaction receipt:', receipt);
};
main();
And that's all the logic you'll need to request, verify, and write the latest price data for a data pair on-chain using Supra!
To verify and write the price data on-chain, go ahead and run:
node main.js
The deserialized output and the transaction receipt will be printed to the console.
----- Deserialized Data ------
Pair index : [ '1' ]
Pair Price : [ '3424260000000000000000' ]
Pair Decimal : [ '18' ]
Pair Timestamp : [ '1709317443269' ]
------------------------------ Transaction receipt: {
transactionHash: '0x7d6f14a049e41f8e873dedfed4aff53bc0d52ff06a17fb0901e35464511708b1',
transactionIndex: 1n,
blockHash: '0xb3551d522371b192f37e68634e6b0ce616adecf0d8b18e139980d2cf564f9313',
from: '0x097d9eea23de2d3081169e0225173d0c55768338',
to: '0xaf1207d950a2231937372cedc2e8ddfa22c40665',
blockNumber: 6178886n,
cumulativeGasUsed: 467872n,
gasUsed: 415356n,
logs: [],
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
status: 1n,
effectiveGasPrice: 125000000n,
type: 0n
}
Retrieve On-Chain Price Data¶
If you want to access the on-chain price data, you can create another contract that interacts with Supra's Storage contract.
Create the Retrieval Contract¶
To create the contract, you can take the following steps:
-
Create a new file for the smart contract, which we'll name
FeedClient
touch FeedClient.sol
-
In the file, create an interface for Supra's Storage contract. The interface has two functions: one for retrieving the price data for a single data pair and another that retrieves price data for multiple data pairs. Then, in our
FeedClient
contract, we'll instantiate theISupraSValueFeed
interface with the address of Supra's Storage contract on Moonbeam or Moonbase AlphaFeedClient.solpragma solidity 0.8.20; interface ISupraSValueFeed { function getSvalue(uint64 _pairIndex) external view returns (bytes32, bool); function getSvalues( uint64[] memory _pairIndexes ) external view returns (bytes32[] memory, bool[] memory); }
-
In the same file, create the
FeedClient
contract. As mentioned in the previous step, the constructor of this contract instantiates theISupraSValueFeed
interface with the address of Supra's Storage contract. The contract also includes functions that call thegetSValue
andgetSValues
functions of the Storage contract and return the response in a decoded formatFeedClient.sol// ... contract FeedClient { // The storage contract ISupraSValueFeed internal sValueFeed; constructor(address storage_) { sValueFeed = ISupraSValueFeed(storage_); } function unpack(bytes32 data) internal pure returns (uint256[4] memory) { uint256[4] memory info; info[0] = bytesToUint256(abi.encodePacked(data >> 192)); // round info[1] = bytesToUint256(abi.encodePacked((data << 64) >> 248)); // decimal info[2] = bytesToUint256(abi.encodePacked((data << 72) >> 192)); // timestamp info[3] = bytesToUint256(abi.encodePacked((data << 136) >> 160)); // price return info; } function bytesToUint256( bytes memory _bs ) internal pure returns (uint256 value) { require(_bs.length == 32, "bytes length is not 32."); assembly { value := mload(add(_bs, 0x20)) } } function getPrice( uint64 _priceIndex ) external view returns (uint256[4] memory) { (bytes32 val, ) = sValueFeed.getSvalue(_priceIndex); uint256[4] memory decoded = unpack(val); return decoded; } function getPriceForMultiplePair( uint64[] memory _pairIndexes ) external view returns (uint256[4][] memory) { (bytes32[] memory val, ) = sValueFeed.getSvalues(_pairIndexes); uint256[4][] memory decodedArray = new uint256[4][](val.length); for (uint256 i = 0; i < val.length; i++) { uint256[4] memory decoded = unpack(val[i]); decodedArray[i] = decoded; } return decodedArray; } }
FeedClient.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
interface ISupraSValueFeed {
function getSvalue(uint64 _pairIndex) external view returns (bytes32, bool);
function getSvalues(
uint64[] memory _pairIndexes
) external view returns (bytes32[] memory, bool[] memory);
}
contract FeedClient {
// The storage contract
ISupraSValueFeed internal sValueFeed;
constructor(address storage_) {
sValueFeed = ISupraSValueFeed(storage_);
}
function unpack(bytes32 data) internal pure returns (uint256[4] memory) {
uint256[4] memory info;
info[0] = bytesToUint256(abi.encodePacked(data >> 192)); // round
info[1] = bytesToUint256(abi.encodePacked((data << 64) >> 248)); // decimal
info[2] = bytesToUint256(abi.encodePacked((data << 72) >> 192)); // timestamp
info[3] = bytesToUint256(abi.encodePacked((data << 136) >> 160)); // price
return info;
}
function bytesToUint256(
bytes memory _bs
) internal pure returns (uint256 value) {
require(_bs.length == 32, "bytes length is not 32.");
assembly {
value := mload(add(_bs, 0x20))
}
}
function getPrice(
uint64 _priceIndex
) external view returns (uint256[4] memory) {
(bytes32 val, ) = sValueFeed.getSvalue(_priceIndex);
uint256[4] memory decoded = unpack(val);
return decoded;
}
function getPriceForMultiplePair(
uint64[] memory _pairIndexes
) external view returns (uint256[4][] memory) {
(bytes32[] memory val, ) = sValueFeed.getSvalues(_pairIndexes);
uint256[4][] memory decodedArray = new uint256[4][](val.length);
for (uint256 i = 0; i < val.length; i++) {
uint256[4] memory decoded = unpack(val[i]);
decodedArray[i] = decoded;
}
return decodedArray;
}
}
Deploy the Contract¶
The steps for compiling and deploying the contract are similar to those in the previous section. You can either duplicate the deploy.js
file and make the necessary edits or directly edit the existing deploy.js
file with the following two changes:
- Update the contract name from
OracleClient
toFeedClient
- Update the contract address in the deployment transaction to be the Storage contract address instead of the Pull contract address
You should end up with the following code:
deploy.js
const fs = require('fs');
const solc = require('solc');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbeam.network');
const compile = () => {
// Get path and load contract
const source = fs.readFileSync('FeedClient.sol', 'utf8');
// Create input object
const input = {
language: 'Solidity',
sources: {
'FeedClient.sol': {
content: source,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};
// Compile the contract
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['FeedClient.sol']['FeedClient'];
// Save ABI to a file
fs.writeFileSync(
'./resources/feedClient.json',
JSON.stringify(contractFile.abi, null, 4),
'utf8'
);
return { abi: contractFile.abi, bytecode: contractFile.evm.bytecode.object };
};
const deploy = async () => {
// Compile the contract
const { abi, bytecode } = compile();
// Create contract instance
const contract = new web3.eth.Contract(abi);
// Create the deployment transaction and pass in the Storage contract address
const deployTx = contract.deploy({
data: bytecode,
arguments: ['0xD02cc7a670047b6b012556A88e275c685d25e0c9'],
});
// Sign transaction with PK
const createTransaction = await web3.eth.accounts.signTransaction(
{
data: deployTx.encodeABI(),
gas: await deployTx.estimateGas(),
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'),
},
'INSERT_PRIVATE_KEY'
);
// Send transaction and wait for receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(`Contract deployed at address: ${createReceipt.contractAddress}`);
};
deploy();
const fs = require('fs');
const solc = require('solc');
const { Web3 } = require('web3');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
const compile = () => {
// Get path and load contract
const source = fs.readFileSync('FeedClient.sol', 'utf8');
// Create input object
const input = {
language: 'Solidity',
sources: {
'FeedClient.sol': {
content: source,
},
},
settings: {
outputSelection: {
'*': {
'*': ['*'],
},
},
},
};
// Compile the contract
const tempFile = JSON.parse(solc.compile(JSON.stringify(input)));
const contractFile = tempFile.contracts['FeedClient.sol']['FeedClient'];
// Save ABI to a file
fs.writeFileSync(
'./resources/feedClient.json',
JSON.stringify(contractFile.abi, null, 4),
'utf8'
);
return { abi: contractFile.abi, bytecode: contractFile.evm.bytecode.object };
};
const deploy = async () => {
// Compile the contract
const { abi, bytecode } = compile();
// Create contract instance
const contract = new web3.eth.Contract(abi);
// Create the deployment transaction and pass in the Storage contract address
const deployTx = contract.deploy({
data: bytecode,
arguments: ['0x4591d1B110ad451d8220d82252F829E8b2a91B17'],
});
// Sign transaction with PK
const createTransaction = await web3.eth.accounts.signTransaction(
{
data: deployTx.encodeABI(),
gas: await deployTx.estimateGas(),
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount('INSERT_ADDRESS'),
},
'INSERT_PRIVATE_KEY'
);
// Send transaction and wait for receipt
const createReceipt = await web3.eth.sendSignedTransaction(
createTransaction.rawTransaction
);
console.log(`Contract deployed at address: ${createReceipt.contractAddress}`);
};
deploy();
To deploy the contract, run:
node deploy.js
The contract address will be printed to your terminal; save it as you'll need it in the next section.
Call the Contract¶
For simplicity, you can add a function to the main.js
file that retrieves the data from the FeedClient
contract:
-
Import the ABI of the
FeedClient
contractconst feedClientABI = require('./resources/feedClient.json');
-
Create a function named
getPriceData
that accepts the index of a data pair as a parameter and will be responsible for calling theFeedClient
contractmain.jsconst getPriceData = async (index) => { // Add logic here };
-
In the
getPriceData
function, create an instance of the deployedFeedClient
contract using the ABI and the contract address, which you should have saved from deploying the contract in the previous set of stepsmain.jsconst getPriceData = async (index) => { const contractAddress = 'INSERT_CONTRACT_ADDRESS'; const contract = new web3.eth.Contract(feedClientABI, contractAddress); };
-
Call the
getPrice
function of theFeedClient
contract and log the results to the consolemain.jsconst getPriceData = async (index) => { const contractAddress = 'INSERT_CONTRACT_ADDRESS'; const contract = new web3.eth.Contract(feedClientABI, contractAddress); // Get the price data and log it to the console const priceData = await contract.methods.getPrice(index).call(); console.log('----- On-Chain Price Data ------'); console.log('Round : ', priceData[0]); console.log('Decimals : ', priceData[1]); console.log('Timestamp : ', priceData[2]); console.log('Price : ', priceData[3]); };
-
In the
main
function, add the logic to call thegetPriceData
functionmain.jsconst main = async () => { const proofs = await getProofs(); // Convert oracle proof bytes to hex const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes); deserializeProofBytes(hex); // Verify and write the latest price data on-chain const receipt = await callContract(hex); console.log('Transaction receipt:', receipt); // Get the latest price data await getPriceData(pairIndex); }; main();
main.js
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const oracleClientABI = require('./resources/oracleClient.json');
const feedClientABI = require('./resources/feedClient.json');
const web3 = new Web3('https://rpc.api.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'mainnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
// Function to call the Oracle client to verify and publish the latest price data
const callContract = async (proofHex) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(oracleClientABI, contractAddress);
// Create the transaction object using the hex-formatted proof and the index of the
// data pair you requested price data for
const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI();
const gasEstimate = await contract.methods
.GetPairPrice(proofHex, pairIndex)
.estimateGas();
const transactionObject = {
from: 'INSERT_ADDRESS',
to: contractAddress,
data: txData,
gas: gasEstimate,
gasPrice: await web3.eth.getGasPrice(),
};
// Sign the transaction with the private key
const signedTransaction = await web3.eth.accounts.signTransaction(
transactionObject,
'INSERT_PRIVATE_KEY'
);
// Send the signed transaction
return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
};
// Function to get the most recently published on-chain price data
const getPriceData = async (index) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(feedClientABI, contractAddress);
// Get the price data and log it to the console
const priceData = await contract.methods.getPrice(index).call();
console.log('----- On-Chain Price Data ------');
console.log('Round : ', priceData[0]);
console.log('Decimals : ', priceData[1]);
console.log('Timestamp : ', priceData[2]);
console.log('Price : ', priceData[3]);
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
// Verify and write the latest price data on-chain
const receipt = await callContract(hex);
console.log('Transaction receipt:', receipt);
// Get the latest price data
await getPriceData(pairIndex);
};
main();
const PullServiceClient = require('./pullServiceClient');
const oracleProofABI = require('./resources/oracleProof.json');
const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
const { Web3 } = require('web3');
const oracleClientABI = require('./resources/oracleClient.json');
const feedClientABI = require('./resources/feedClient.json');
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
const pairIndex = 1;
// Function that fetches proof data from the gRPC server using the specified parameters
const getProofs = () => {
const address = 'testnet-dora.supraoracles.com';
const client = new PullServiceClient(address);
const request = {
pair_indexes: [pairIndex], // ETH_USDT
chain_type: 'evm',
};
return new Promise((resolve, reject) => {
client.getProof(request, (err, response) => {
if (err) {
console.error('Error:', err.details);
return;
}
resolve(response);
});
});
};
// Function to convert the proof data to human-readable price data
const deserializeProofBytes = (proofHex) => {
const proof_data = web3.eth.abi.decodeParameters(oracleProofABI, proofHex);
// Fatching the raw bytes of the signed pair cluster data
const clusters = proof_data[0].clustersRaw;
// Fetching which pair IDs have been requested
const pairMask = proof_data[0].pairMask;
// Helps in iterating the vector of pair masking
let pair = 0;
// Lists of all the pair IDs, prices, decimals, and timestamps requested
const pairId = [];
const pairPrice = [];
const pairDecimal = [];
const pairTimestamp = [];
for (let i = 0; i < clusters.length; ++i) {
// Deserialize the raw bytes of the signed pair cluster data
const scc = web3.eth.abi.decodeParameters(
signedCoherentClusterABI,
clusters[i]
);
for (let j = 0; j < scc[0].cc.pair.length; ++j) {
pair += 1;
// Verify whether the pair is requested or not
if (!pairMask[pair - 1]) {
continue;
}
// Pushing the pair IDs, prices, decimals, and timestamps requested in the output vector
pairId.push(scc[0].cc.pair[j].toString(10));
pairPrice.push(scc[0].cc.prices[j].toString(10));
pairDecimal.push(scc[0].cc.decimals[j].toString(10));
pairTimestamp.push(scc[0].cc.timestamp[j].toString(10));
}
}
console.log('----- Deserialized Data ------');
console.log('Pair index : ', pairId);
console.log('Pair Price : ', pairPrice);
console.log('Pair Decimal : ', pairDecimal);
console.log('Pair Timestamp : ', pairTimestamp);
console.log('------------------------------');
};
// Function to call the Oracle client to verify and publish the latest price data
const callContract = async (proofHex) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(oracleClientABI, contractAddress);
// Create the transaction object using the hex-formatted proof and the index of the
// data pair you requested price data for
const txData = contract.methods.GetPairPrice(proofHex, pairIndex).encodeABI();
const gasEstimate = await contract.methods
.GetPairPrice(proofHex, pairIndex)
.estimateGas();
const transactionObject = {
from: 'INSERT_ADDRESS',
to: contractAddress,
data: txData,
gas: gasEstimate,
gasPrice: await web3.eth.getGasPrice(),
};
// Sign the transaction with the private key
const signedTransaction = await web3.eth.accounts.signTransaction(
transactionObject,
'INSERT_PRIVATE_KEY'
);
// Send the signed transaction
return await web3.eth.sendSignedTransaction(signedTransaction.rawTransaction);
};
// Function to get the most recently published on-chain price data
const getPriceData = async (index) => {
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
const contract = new web3.eth.Contract(feedClientABI, contractAddress);
// Get the price data and log it to the console
const priceData = await contract.methods.getPrice(index).call();
console.log('----- On-Chain Price Data ------');
console.log('Round : ', priceData[0]);
console.log('Decimals : ', priceData[1]);
console.log('Timestamp : ', priceData[2]);
console.log('Price : ', priceData[3]);
};
const main = async () => {
const proofs = await getProofs();
// Convert oracle proof bytes to hex
const hex = web3.utils.bytesToHex(proofs.evm.proof_bytes);
deserializeProofBytes(hex);
// Verify and write the latest price data on-chain
const receipt = await callContract(hex);
console.log('Transaction receipt:', receipt);
// Get the latest price data
await getPriceData(pairIndex);
};
main();
Run the following command to print the price data to the terminal:
Note
Feel free to comment out the calls to the deserializeProofBytes
and callContract
functions if you only want to retrieve the price data.
node main.js
The terminal output should now include the price data.
----- On-Chain Price Data ------
Round : 1709333298000n
Decimals : 18n
Timestamp : 1709333298216n
Price : 3430850000000000000000n
And that's it! You've successfully fetched the proof data from Supra, verified and published it on-chain, and retrieved it! For more information on Supra Oracles, please check out their documentation.
| Created: March 4, 2024