How to Integrate Your Smart Contracts With UDF

In 5 easy steps

This guide will help developers who want to integrate their smart contracts with UDF. It uses Hardhat for contract compilation, although similar methods apply with other frameworks.

Step #1: Initialize new project and install dependencies

In the first step, we are establising a basic solidity development setup.

$ yarn init
// yarn initialization flow
$ yarn install --dev hardhat
$ yarn hardhat init
// Hardhat initialization flow

Step #2: Write a contract that accepts UDF updates for verification

To create a new contract that utilizes the PullOracle functionality for verifying updates, follow these steps:

  1. Create a file named PullOracleConsumer.sol.

  2. Copy the IPullOracle interface into this file, or create a separate .sol file for the interface and import it into your main contract using an import statement.

interface IPullOracle {
    struct LatestUpdate {
        /// @notice The price for asset from latest update
        uint256 latestPrice;
        /// @notice The timestamp of latest update
        uint256 latestTimestamp;
    }
/// @dev Represents a digital signature
    struct Signature {
        bytes32 r; // The first 32 bytes of the signature
        bytes32 s; // The second 32 bytes of the signature
        uint8 v; // The recovery byte
    }
/// @notice mapping of dataKey to the latest update
function latestUpdate(bytes32 dataKey) external pure returns (LatestUpdate memory);
// @notice Verifies that the update was emitted on EOB. 
 // @dev It does so by checking following properties:
 // * Calculated merkle root of Update + merkle proofs, and ensure that it is
//   equal to the provided merkle root
// * Validate the signatures of EOB agents on the Merkle root to ensure
    //   merkle root integrity. The consensus check passes only if the number of valid
    //   unique signatures meets or exceeds the protocol's consensus rate threshold.
    function getLastPrice(
        bytes32 merkleRoot,
        bytes32[] calldata merkleProof,
        Signature[] calldata signatures,
        bytes32 dataKey,
        uint256 price,
        uint256 timestamp
    ) external returns (uint256);
}

In this example, the PullOracleConsumer contract takes in data required for update verification, checks the update, and then emits an event to signal a price change.

contract PullOracleConsumer {
    IPullOracle public pullOracle;
    mapping (bytes32 => uint256) public lastPrice;
    event PriceUpdated(uint256 oldPrice, uint256 newPrice, address updater);
    constructor(address _pullOracle) {
        pullOracle = IPullOracle(_pullOracle);
    }
    function getLastPrice( 
        bytes32 merkleRoot,
        bytes32[] calldata merkleProof,
        IPullOracle.Signature[] calldata signatures,
        bytes32 dataKey,
        uint256 price,
        uint256 timestamp
    ) external {
        uint256 oldPrice = lastPrice[dataKey];
        lastPrice[dataKey] = pullOracle.getLastPrice(merkleRoot, merkleProof, signatures, dataKey, price, timestamp);
        emit PriceUpdated(oldPrice, lastPrice[dataKey], msg.sender);
    }
}

Step #3: Deploy the contract

Deploy the contract on one of supported testnets with the address of the existing PullOracle contract.

Step #4: Use the Finalized Data API to retrieve the latest EOB updates for required assets

For this example, let’s imagine you need NGL/USD price in your contract. First we need to fetch the latest update from the Finalized Data API.

$ curl 
https://pricefeed.entangle.fi/spotters/prices-feed1?assets=NGL/USD

{
  "calldata": {
    "merkleRoot": "0xa4d6d7594888f1fe8675d033737baf6106819f7b0e59f3d794f5e1adcec70022",
    "signatures": [
      {
        "R": "0x6ddc9b92d85143cca3676e99ec80542aeda2be4dddc86cdfe51eaa7d5282463b",
        "S": "0x4a2a2f401289b63516722fd1ee0e82f8f602a7341239986a2e8ed469ec3efde8",
        "V": 28
      },
      {
        "R": "0x23b500c294ec06b1882e4a7ddae72a9293c15ed5478d847ec32a993a2e98f7a9",
        "S": "0x6b75f9bb6277737ce73ef963d304c0633e075c449314bbe009000aeefe2535cd",
        "V": 28
      },
      {
        "R": "0x16a9849fe9fad62d9fda9065029ddfc8af50436282709dddf2ea1c4695cad6a6",
        "S": "0x46553e9ac51b03f18af4e71c383fa44af6fbfb9daa986aebe10769f7b7e55f0e",
        "V": 28
      }
    ],
    "feeds": [
      {
        "key": "NGL/USD",
        "value": {
          "data": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcHqjZobCo0=",
          "timestamp": 1722866387
        },
        "merkleProofs": [
          "wg/sd9GqECY5zFKdsHRThUdPda0Parv24Npcj15EqJ4=",
          "CK5qUUFgAaZBYE/Aq3x+Y61HHAQnW7q6A2K1obw55ZE="
        ]
      }
    ]
  },
  "error": ""
}

This response provides all necessary details to verify the NGL/USD update, including:

  • merkleRoot: The resulting merkle root from a merkle tree containing the latest updates for our asset collection (prices-feed1).

  • signatures: Transmitter signatures on the merkleRoot bytes, confirming its emission on the EOB.

  • feeds: An array containing updates and verification data for requested assets.

    • key: Asset data key.

    • value: The price value and its timestamp.

    • merkleProofs: An array of merkle proofs to verify the presence of the NGL/USD asset in the merkle tree.

This data enables executing the getLastPrice transaction on the PullOracleConsumer contract. To begin, fetch the latest update from the Finalized Data API for the NGL/USD price.

Step #5: Send a transaction with the update data

Now that we understand the Finalized Data API, we can create a script that retrieves an update from the API, parses the response, encodes it to EVM calldata, and sends a getLastPrice transaction for the NGL/USD asset.

Below is a TypeScript script that accomplishes this task.

import axios from 'axios';
import { ethers } from 'hardhat'
import { BytesLike } from "ethers";
interface FeedValue {
  data: string;
  timestamp: number;
}
interface Feed {
  key: string;
  value: FeedValue;
  merkleProofs: string[];
}
export interface Signature {
  R: string;
  S: string;
  V: number;
}
interface Calldata {
  merkleRoot: string;
  signatures: Signature[];
  feeds: Feed[];
}
export interface ApiResponse {
  calldata: Calldata;
  error: string;
}
export function decodeBase64ToBytes32(base64: string): string {
  const buffer = Buffer.from(base64, 'base64');
  return ethers.hexlify(buffer);
}
function decodeBase64(base64: string): string {
  const buffer = Buffer.from(base64, 'base64');
  const value = buffer.readBigUInt64BE(buffer.length - 8);
  return (value).toString();
}
async function fetchData(url: string): Promise<ApiResponse> {
  try {
    const response = await axios.get<ApiResponse>(url);
    const data = response.data;
    data.calldata.feeds = data.calldata.feeds.map(feed => ({
      ...feed,
      value: {
        ...feed.value,
        data: decodeBase64(feed.value.data)
      }
    }));
    return data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.error('Axios error:', error.message);
      if (error.response) {
        console.error('Response status:', error.response.status);
        console.error('Response data:', error.response.data);
      }
    } else {
      console.error('Unknown error:', error);
    }
    throw error;
  }
}
export async function getPriceForAsset(asset: string): Promise<ApiResponse | null> {
  const baseUrl = 'https://pricefeed.entangle.fi/spotters/prices-feed1';
  const url = `${baseUrl}?assets=${asset}`;
  try {
    return await fetchData(url);
  } catch (error) {
    console.error('An error occurred while executing the request');
    return null;
  }
}
interface SignatureStruct {
    r: BytesLike;
    s: BytesLike;
    v: number;
  }
  type ContractMethodArgs = [
    merkleRoot: BytesLike,
    merkleProof: BytesLike[],
    signatures: SignatureStruct[],
    dataKey: BytesLike,
    price: string,
    timestamp: string
  ];
  function prepareDataForContract(data: ApiResponse): ContractMethodArgs {
    const feed = data.calldata.feeds[0]; // Assume that we only have one feed
    const signatures: SignatureStruct[] = data.calldata.signatures.map(sig => ({
      r: sig.R,
      s: sig.S,
      v: sig.V
    }));
    const decodedMerkleProofs = feed.merkleProofs.map(decodeBase64ToBytes32);
    return [
      data.calldata.merkleRoot,
      decodedMerkleProofs,
      signatures,
      ethers.encodeBytes32String(feed.key),
      feed.value.data,
      feed.value.timestamp.toString()
    ];
  }
async function main() {
  const pullOracleConsumerAddress = "0x..."
  let consumer = await ethers.getContractAt(
    "PullOracleConsumer",
    pullOracleConsumerAddress
  );
  let dataForCall: ApiResponse | null = await getPriceForAsset('NGL/USD');
  if (!dataForCall) {
    console.log('api response returned null, aborting');
    return;
  }
  let contractData = prepareDataForContract(dataForCall);
  let tx = await consumer.getLastPrice(
    contractData[0],
    contractData[1],
    contractData[2],
    contractData[3],
    contractData[4],
    contractData[5]
  );
  await tx.wait();
  console.log(tx);
  let priceFromContract = await consumer.lastPrice(contractData[3]);
  console.log(`latest price from contract: ${priceFromContract}`);
}
main().catch(error => {
  console.error(error);
  // throw error;
  process.exitCode = 1;
});

Last updated