Getting Started

Introduction

APRO Data Pull is a Oracle Product based on the pull-model. The Pull model, where price data is fetched from APRO's decentralized network only when required, providing cost-effective, on-demand access to updates. This model reduces the need for continuous on-chain interactions, ensuring accuracy and minimizing costs.

You can use APRO Data Pull to connect your smart contracts to real-time asset pricing data. These data feeds aggregate information from many independent APRO node operators, allowing contracts to fetch data on-demand when needed.

How to Use Real-Time Data

Anyone can submit a report verification to the on-chain APRO contract, the report includes the price, timestamp, signatures. The price data contained in the report will be stored in the contract for future use when the report is verified successfully.

This guide explains how to use real-time APRO data in EVM contracts.

  1. Acquire report data (include price, timestamp, signatures) from our Live-Api service

Consensused price data is published via Live-Api Service. Please see the REST API & WebSocket Integration Guide for detailed instructions on fetching and decoding reports.

  1. Using the report data in the contract

Report data can be used in the following scenarios.

Scenario 1: Get the latest price of a price feed from the Live-Api server, verify and update the price in the on-chain contract, and then use the latest price in subsequent business logic. The price update logic and business logic can be processed in the same transaction.

Suitable for on-chain applications that always use the latest price data. Please refer to the function verifyAndReadLatestPrice.

Scenario 2: Get a specific timestamp price of a price feed from the Live-Api server, verify the price in the on-chain contract, and use this price in subsequent business logic.

Suitable for on-chain applications that need a specific timestamp price data. Please refer to the function verifyAndReadThePrice.

WARN: The validity period of report data is 24 hours, so some not-so-new report data can also be verified successfully. Please ensure that you do not mistake this price for the latest price.

Scenario 3: Get the latest price of a feed from the Live-Api server, verify the price in the on-chain contract.

This usage is similar to using the traditional push-model based oracle product. The price update logic and business logic are separated. Please refer to the function verifyReportWithWrapNativeToken or verifyReportWithNativeToken.

Scenario 4: Read only the latest price of a feed that has been verified in the on-chain contract.

The price read in this way may be not timely if no other users actively submit report verifications. Please refer to the function readPrice.

Here are some codes for your reference.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

interface IERC20 {
  /**
   * @dev Emitted when `value` tokens are moved from one account (`from`) to
   * another (`to`).
   *
   * Note that `value` may be zero.
   */
  event Transfer(address indexed from, address indexed to, uint256 value);

  /**
   * @dev Emitted when the allowance of a `spender` for an `owner` is set by
   * a call to {approve}. `value` is the new allowance.
   */
  event Approval(address indexed owner, address indexed spender, uint256 value);

  /**
   * @dev Returns the amount of tokens in existence.
   */
  function totalSupply() external view returns (uint256);

  /**
   * @dev Returns the amount of tokens owned by `account`.
   */
  function balanceOf(address account) external view returns (uint256);

  /**
   * @dev Moves `amount` tokens from the caller's account to `to`.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  function transfer(address to, uint256 amount) external returns (bool);

  /**
   * @dev Returns the remaining number of tokens that `spender` will be
   * allowed to spend on behalf of `owner` through {transferFrom}. This is
   * zero by default.
   *
   * This value changes when {approve} or {transferFrom} are called.
   */
  function allowance(address owner, address spender) external view returns (uint256);

  /**
   * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * IMPORTANT: Beware that changing an allowance with this method brings the risk
   * that someone may use both the old and the new allowance by unfortunate
   * transaction ordering. One possible solution to mitigate this race
   * condition is to first reduce the spender's allowance to 0 and set the
   * desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   *
   * Emits an {Approval} event.
   */
  function approve(address spender, uint256 amount) external returns (bool);

  /**
   * @dev Moves `amount` tokens from `from` to `to` using the
   * allowance mechanism. `amount` is then deducted from the caller's
   * allowance.
   *
   * Returns a boolean value indicating whether the operation succeeded.
   *
   * Emits a {Transfer} event.
   */
  function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

library Common {
  // @notice The asset struct to hold the address of an asset and amount
  struct Asset {
    address assetAddress;
    uint256 amount;
  }
}

interface IWERC20 {
  function deposit() external payable;

  function withdraw(uint256) external;
}

interface IVerifierFeeManager {}

contract AproStructs {
    struct Price {
        uint192 value;
        int8 decimal;
        uint32 observeAt;
        uint32 expireAt;
    }
}

// Custom interfaces for IVerifierProxy and IFeeManager
interface IVerifierProxy {
    /**
     * @notice Verifies that the data encoded has been signed.
     * correctly by routing to the correct verifier, and bills the user if applicable.
     * @param payload The encoded data to be verified, including the signed
     * report.
     * @param parameterPayload Fee metadata for billing. For the current implementation this is just the abi-encoded fee token ERC-20 address.
     * @return verifierResponse The encoded report from the verifier.
     */
    function verify(
        bytes calldata payload,
        bytes calldata parameterPayload
    ) external payable returns (bytes memory verifierResponse);

    function s_feeManager() external view returns (IVerifierFeeManager);
}

interface IPriceReader {

    /// @notice Returns the period (in seconds) that a price feed is considered valid since its observe time
    function getValidTimePeriod() external view returns (uint256 validTimePeriod);

    function updateReport(bytes calldata report) external;

    function getPrice(
        bytes32 id
    ) external view returns (AproStructs.Price memory price);

    function getPriceUnsafe(
        bytes32 id
    ) external view returns (AproStructs.Price memory price);

    function getPriceNoOlderThan(
        bytes32 id,
        uint age
    ) external view returns (AproStructs.Price memory price);

}

interface IFeeManager {
    function getFeeAndReward(
        address subscriber,
        bytes memory unverifiedReport,
        address quoteAddress
    ) external returns (Common.Asset memory, Common.Asset memory, uint256);

    function i_linkAddress() external view returns (address);

    function i_nativeAddress() external view returns (address);

    function i_rewardManager() external view returns (address);
}

contract DemoContract {

    error NothingToWithdraw(); // Thrown when a withdrawal attempt is made but the contract holds no tokens of the specified type.
    error NotOwner(address caller); // Thrown when a caller tries to execute a function that is restricted to the contract's owner.

    struct Report {
        bytes32 feedId;
        uint32 validFromTimestamp; 
        uint32 observationsTimestamp;
        uint192 nativeFee;
        uint192 linkFee;
        uint32 expiresAt;
        uint192 price;
        uint192 bid; 
        uint192 ask;
    }

    IVerifierProxy public s_verifierProxy;

    address private s_owner;

    constructor(address _verifierProxy) {
        s_owner = msg.sender;
        s_verifierProxy = IVerifierProxy(_verifierProxy);
    }

    /// @notice Checks if the caller is the owner of the contract.
    modifier onlyOwner() {
        if (msg.sender != s_owner) revert NotOwner(msg.sender);
        _;
    }
    
    receive () external payable virtual {}

    /**
     * This method is an example of how to interact with the APRO contract.
     * Fetch the latest report from the live-api server and pass it to the APRO contract to verify the price.
     * If the price observation time in the report is later than the price observation time stored in the contract, the price in the contract will be updated.
     * 
     * @param payload The encoded report data contains the latest timestamp's price.
    */     

    function verifyAndReadLatestPrice(bytes calldata payload) public {

      (bytes32 feedId, , ) = verifyReportWithWrapNativeToken(payload);

      // Read the current price from a price feed if it is less than 60 seconds old.
      // Note: You can decrease the time value according to your business logic. 
      AproStructs.Price memory price = IPriceReader(address(s_verifierProxy)).getPriceNoOlderThan(feedId, 60);

      //do some business logic with the latest price

    }

    /**
     * This method is an example of how to interact with the APRO contract.
     * Fetch the report at a specific timestamp from the live-api server and pass it to the APRO contract to verify the price.
     * Note: This price contained in the report is not necessarily the latest price.
     * @param payload The encoded report data contains the feedId's specific timestamp's price.
    */     

    function verifyAndReadThePrice(bytes calldata payload) public {

      (bytes32 feedId, uint32 timestamp, uint192 verifiedPrice) = verifyReportWithWrapNativeToken(payload);

      //do some business logic with the verifiedPrice

    }

     /**
     * This method is an example of how to interact with the APRO contract.
     * Read the feedId's price through three methods.
     
     * @param feedId each price feed (e.g., BTC/USD) is identified by a price feed ID.
     * The complete list of feed IDs is available at https://docs.apro.com/en/data-pull/price-feed-id
    */     

    function readPrice(bytes32 feedId) public view returns(AproStructs.Price memory price){

      // Read the current price from a price feed.
      // Note: this transactions may failed with PriceFeedExpire or PriceFeedNotFound error
      AproStructs.Price memory price1 = IPriceReader(address(s_verifierProxy)).getPriceUnsafe(feedId);

      // Read the current price from a price feed if it is less than `validTimePeriod` (default: 120) seconds old.
      // Note: this transactions may failed with PriceFeedExpire or PriceFeedNotFound or StalePrice error
      //AproStructs.Price memory price2 = IPriceReader(address(s_verifierProxy)).getPrice(feedId);

      // Read the current price from a price feed if it is less than 60 seconds old.
      // Note: this transactions may failed with PriceFeedExpire or PriceFeedNotFound or StalePrice error
      //AproStructs.Price memory price3 = IPriceReader(address(s_verifierProxy)).getPriceNoOlderThan(feedId, 60);

      return price1;
    }


    function verifyReportWithWrapNativeToken(bytes calldata payload) public returns(bytes32, uint32, uint192){
        // Report verification fees
        IFeeManager feeManager = IFeeManager(address(s_verifierProxy.s_feeManager()));

        //decode the report from the payload
        (/* bytes32[3] reportContextData */ , bytes memory reportData,,,) = abi
        .decode(payload, (bytes32[3], bytes, bytes32[], bytes32[], bytes32));

        address feeTokenAddress = feeManager.i_nativeAddress();

        (Common.Asset memory fee, ,) = feeManager.getFeeAndReward(
            address(this),
            reportData,
            feeTokenAddress
        );

        // Approve feeManager to spend this contract's balance in fees
        IERC20(feeTokenAddress).approve(address(feeManager), fee.amount);

        // Verify the report
        bytes memory verifiedReportData = s_verifierProxy.verify(
            payload,
            abi.encode(feeTokenAddress)
        );

        // Decode verified report data into a Report struct
        Report memory verifiedReport = abi.decode(verifiedReportData, (Report));

        return (verifiedReport.feedId, verifiedReport.validFromTimestamp, verifiedReport.price);
    }

    
    function verifyReportWithNativeToken(bytes calldata payload) payable public returns(bytes32, uint32, uint192)  {
        // Report verification fees
        IFeeManager feeManager = IFeeManager(address(s_verifierProxy.s_feeManager()));
        address feeTokenAddress = feeManager.i_nativeAddress();

        //decode the report from the payload
        (/* bytes32[3] reportContextData */ , bytes memory reportData,,,) = abi
        .decode(payload, (bytes32[3], bytes, bytes32[], bytes32[], bytes32));

        (Common.Asset memory fee, ,) = feeManager.getFeeAndReward(
            address(this),
            reportData,
            feeTokenAddress
        );

        // Verify the report
        bytes memory verifiedReportData = s_verifierProxy.verify{value: msg.value}(
            payload,
            abi.encode(feeTokenAddress)
        );

        uint256 change;
        unchecked {
            //msg.value is always >= to fee.amount
            change = msg.value - fee.amount;
        }

        if (change != 0) {
            payable(msg.sender).transfer(change);
        }

        // Decode verified report data into a Report struct
        Report memory verifiedReport = abi.decode(verifiedReportData, (Report));
        return (verifiedReport.feedId, verifiedReport.validFromTimestamp, verifiedReport.price);
    }

    /**
     * @notice Withdraws all tokens of a specific ERC20 token type to a beneficiary address.
     * @dev Utilizes SafeERC20's safeTransfer for secure token transfer. Reverts if the contract's balance of the specified token is zero.
     * @param _beneficiary Address to which the tokens will be sent. Must not be the zero address.
     * @param _token Address of the ERC20 token to be withdrawn. Must be a valid ERC20 token contract.
     */
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        IERC20(_token).transfer(_beneficiary, amount);
    }

}

You can test the contract code by following these steps:

  1. Compile and deploy the DemoContract with the VerifierProxy contract address.

  2. Deposit some wrapped native token to the DemoContract.

  3. Use the full report obtained in the API& WebSocket Guide guide as the payload params and trigger the appropriate functions.