Skip to content

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.

The information presented herein is for informational purposes only and has been provided by third parties. Moonbeam does not endorse any project listed and described on the Moonbeam docs website (https://docs.moonbeam.network/).

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:

  1. Web2 methods are used to retrieve price data from Supra
  2. 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
  3. 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:

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:

  1. Create an empty project directory

    mkdir moonbeam-supra
    
  2. Create a basic package.json file for your project

    cd moonbeam-supra && npm init-y
    
  3. 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:

  1. Import the PullServiceClient from the pullServiceClient.js file

    main.js
    const PullServiceClient = require('./pullServiceClient');
    
  2. 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.js
    const PullServiceClient = require('./pullServiceClient');
    
    const pairIndex = 1;
    
  3. Create a getProofs function, where you'll add all the logic

    main.js
    const getProofs = () => {
      // Add logic
    };
    
  4. In the getProofs function, you can define the address for the gRPC server and use it to create an instance of the PullServiceClient. Supra has one address for MainNets, 'mainnet-dora.supraoracles.com' and one for TestNets, 'testnet-dora.supraoracles.com'

    main.js
    const getProofs = () => {
      const address = 'mainnet-dora.supraoracles.com';
      const client = new PullServiceClient(address);
    };
    
    main.js
    const getProofs = () => {
      const address = 'testnet-dora.supraoracles.com';
      const client = new PullServiceClient(address);
    };
    
  5. To request data from the client, first, you need to define the data you want to request

    main.js
    const getProofs = () => {
      // ...
      const request = {
        pair_indexes: [pairIndex], // ETH_USDT
        chain_type: 'evm',
      };
    };
    
  6. Now you can request the proof for the data pair(s) by calling the getProof method of the Pull Service Client

    main.js
    const getProofs = () => {
      // ...
      return new Promise((resolve, reject) => {
        client.getProof(request, (err, response) => {
          if (err) {
            console.error('Error:', err.details);
            return;
          }
          resolve(response);
        });
      });
    };
    
  7. Create a main function that calls the getProofs function and saves the proofs to be consumed in later steps

    main.js
    const 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:

  1. In the main.js file, import the interfaces and Web3

    main.js
    const oracleProofABI = require('./resources/oracleProof.json');
    const signedCoherentClusterABI = require('./resources/signedCoherentCluster.json');
    const { Web3 } = require('web3');
    
  2. Create a Web3 instance, which will be used to interact with the interfaces. You can add this snippet directly after the imports

    main.js
    const web3 = new Web3('https://rpc.api.moonbeam.network');
    
    main.js
    const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
    
  3. Create a deserializeProofBytes function to add all the logic for deserializing the proofs. The function should accept the proof formatted in hex as a parameter

    main.js
    const deserializeProofBytes = (proofHex) => {
      // Add logic here
    };
    
  4. 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.js
    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;
    };
    
  5. 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.js
    const 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('------------------------------');
    };
    
  6. In the main function that you created in the previous section, you can convert the proofs to hex and call the deserializeProofBytes function

    main.js
    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();
    
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:

node main.js
----- 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:

  1. Create a new file for the smart contract, which we'll name OracleClient

    touch OracleClient.sol
    
  2. 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 the ISupraOraclePull interface with the address of Supra's Pull contract on Moonbeam or Moonbase Alpha

    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);
    }
    
  3. In the same file, create the OracleClient contract. As mentioned in the previous step, the constructor of this contract instantiates the ISupraOraclePull interface with the address of Supra's Pull contract. The contract also includes a function that calls the verifyOracleProof function of the Pull contract and saves the price data on-chain

    OracleClient.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:

  1. Create a file that will contain the logic for compiling and deploying the smart contract

    touch deploy.js
    
  2. Install the Solidity compiler. We're installing version 0.8.20, as that is the version required by the OracleClient contract

    npm i solc@0.8.20
    
  3. Add the following imports

    deploy.js
    const solc = require('solc');
    const { Web3 } = require('web3');
    
  4. Create the Web3 instance

    const web3 = new Web3('https://rpc.api.moonbeam.network');
    
    const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network');
    
  5. Create a function that compiles the OracleClient contract, saves the ABI in the resources directory for later use, and returns the ABI and bytecode for the deployment

    deploy.js
    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 };
    };
    
  6. 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.js
    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();
    
    deploy.js
    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();
    
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.

node deploy.js
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:

  1. Import the ABI for the OracleClient contract

    main.js
    const oracleClientABI = require('./resources/oracleClient.json');
    
  2. Create a function that accepts the hex-formatted proof data and will be responsible for calling the OracleClient contract

    main.js
    const callContract = async (proofHex) => {
      // Add logic here
    };
    
  3. In the callContract function, create an instance of the deployed OracleClient contract using the ABI and the contract address, which you should have saved from deploying the contract in the previous set of steps

    main.js
    const callContract = async (proofHex) => {
      const contractAddress = 'INSERT_CONTRACT_ADDRESS';
      const contract = new web3.eth.Contract(oracleClientABI, contractAddress);
    };
    
  4. Create the transaction object that will call the GetPairPrice function of the OracleClient contract. You'll need to provide your address in the transaction object

    main.js
    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(),
      };
    };
    
  5. 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.js
    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);
    };
    
  6. The last step is to call the callContract function from the main function

    main.js
    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();
    
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.

node main.js
----- 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:

  1. Create a new file for the smart contract, which we'll name FeedClient

    touch FeedClient.sol
    
  2. 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 the ISupraSValueFeed interface with the address of Supra's Storage contract on Moonbeam or Moonbase Alpha

    FeedClient.sol
    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);
    }
    
  3. In the same file, create the FeedClient contract. As mentioned in the previous step, the constructor of this contract instantiates the ISupraSValueFeed interface with the address of Supra's Storage contract. The contract also includes functions that call the getSValue and getSValues functions of the Storage contract and return the response in a decoded format

    FeedClient.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 to FeedClient
  • 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:

  1. Import the ABI of the FeedClient contract

    const feedClientABI = require('./resources/feedClient.json'); 
    
  2. Create a function named getPriceData that accepts the index of a data pair as a parameter and will be responsible for calling the FeedClient contract

    main.js
    const getPriceData = async (index) => {
      // Add logic here
    };
    
  3. In the getPriceData function, create an instance of the deployed FeedClient contract using the ABI and the contract address, which you should have saved from deploying the contract in the previous set of steps

    main.js
    const getPriceData = async (index) => {
      const contractAddress = 'INSERT_CONTRACT_ADDRESS';
      const contract = new web3.eth.Contract(feedClientABI, contractAddress);
    };
    
  4. Call the getPrice function of the FeedClient contract and log the results to the console

    main.js
    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]);
    };
    
  5. In the main function, add the logic to call the getPriceData function

    main.js
    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();
    
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.

node main.js
----- 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.

This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.
The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website (https://docs.moonbeam.network/). Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: October 3, 2024
| Created: March 4, 2024