How to Get Prices On-Chain – Solana

This guide is designed for external developers who need to retrieve and use prices provided through the Push Model of the Entangle Universal Data Feeds (UDF) protocol on the Solana blockchain. The price feeds are regularly pushed to and stored within the Solana Price Oracle program.

Below is a sample Solana consumer program written in Rust that demonstrates how to retrieve prices for a specific asset from the Price Oracle program. This example can serve as a template for external developers to build their own price-consumer programs.

Key Function: consume_price

The consume_price method in the example program retrieves the price for a given asset by constructing an instruction and invoking the on-chain Entangle Price Oracle.

  • The Price Oracle address is provided as part of the accounts set.

  • The target asset's address is defined as a Program Derived Address (PDA), derived using predefined attributes and the asset name according to Price Oracle rules.

  • The returned data consists of two parts: price and timestamp.

    • The price is a 32-byte value, but only the rightmost 16 bytes are used.

    • The timestamp is a Unix timestamp.

Program Setup

To begin, you’ll need to create a consumer program using the Solana Rust SDK. The program will interact with the Entangle Price Oracle and retrieve asset prices.

use anchor_lang::prelude::*;
declare_id!("3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv");

#[program]
pub mod price_consumer {
    use super::*;
    use anchor_lang::solana_program::{
        instruction::Instruction,
        program::{get_return_data, invoke},
    };

    pub fn consume_price(ctx: Context<ConsumePrice>, asset: String) -> Result<()> {
        // Convert the asset to a byte vector
        let payload = asset
            .try_to_vec()
            .expect("Asset is expected to be converted to the vec of bytes");

        // Prepare the data to be passed in the instruction
        let data = [&sighash("global", "last_price")[..], &payload[..]].concat();

        // Create the instruction to invoke the price oracle
        let ix = Instruction {
            program_id: ctx.accounts.price_oracle.key(),
            accounts: ctx.accounts.latest_update.to_account_metas(Some(false)),
            data,
        };

        // Invoke the price oracle program
        invoke(&ix, &[ctx.accounts.latest_update.to_account_info()])?;

        // Get the returned data from the price oracle
        let (_, data) = get_return_data().expect("Data expected to be retrieved from the price oracle");

        // Deserialize the price and timestamp from the returned data
        let (price, timestamp) = <([u8; 32], u64)>::try_from_slice(&data)
            .expect("Expected price and timestamp to be deserialized with Borsh");

        // Extract the price
        let (_, price_right) = price.split_at(16);
        let price = u128::try_from_slice(price_right).unwrap().to_be();

        // Log the result
        msg!("Price of: {} is: {} at: {}", asset, price, timestamp);

        Ok(())
    }
}

// Function to generate a sighash
fn sighash(namespace: &str, name: &str) -> [u8; 8] {
    let preimage = format!("{}:{}", namespace, name);
    let mut sighash = [0u8; 8];
    sighash.copy_from_slice(
        &anchor_lang::solana_program::hash::hash(preimage.as_bytes()).to_bytes()[..8],
    );
    sighash
}

// Constants for seeds and protocol ID
pub const ROOT: &[u8] = b"UDF0";

#[cfg(not(feature = "mainnet"))]
const UDF_PROTOCOL_ID: &[u8] = b"universal-datafeeds3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";

#[cfg(feature = "mainnet")]
const UDF_PROTOCOL_ID: &[u8] = b"universal-datafeeds\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";

// Struct to define the accounts used in the consume_price function
#[derive(Accounts)]
#[instruction(asset: String)]
pub struct ConsumePrice<'info> {
    #[account(signer, mut)]
    payer: Signer<'info>,

    /// CHECK: This is an external price oracle, and no specific owner is expected. 
    /// The account is marked as executable to ensure it's a program.
    #[account(executable)]
    price_oracle: UncheckedAccount<'info>,

    /// CHECK: This account is derived using specific seeds, including the asset. 
    /// Ensure the seeds match the provided asset to trust the account.
    #[account(
        seeds = [ROOT, b"LAST_UPDATE", UDF_PROTOCOL_ID, asset.as_bytes()],
        bump, 
        seeds::program = price_oracle
    )]
    latest_update: UncheckedAccount<'info>,
}

TypeScript Snippet: Invoking the Consumer Program from the Backend

The following TypeScript snippet demonstrates how to invoke the given consumer program from the backend. This example follows a common approach for constructing transactions.

Key Details

  • Entangle Price Oracle Deployment: The address 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7 is where the Entangle Price Oracle is deployed, and it is accessible on both the Solana Devnet and Mainnet.

  • Asset to Retrieve: The "NGL/USD" asset is intended to be retrieved on-chain using the Entangle Price Oracle.

  • latestUpdatePda Calculation: The latestUpdatePda is calculated according to predefined rules, which include using certain seeds and the asset name.

import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { PriceConsumer } from "../target/types/price_consumer";

// Initialize the PriceConsumer program
const consumer_program = anchor.workspace.PriceConsumer as Program<PriceConsumer>;

// Constants for UDF protocol
const UDF_ROOT = utf8.encode("UDF0");
const UDF_PROTOCOL_ID = Buffer.from(
    utf8.encode(
        "universal-data-feeds\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    )
);

// Encode asset name as a UTF-8 byte array
let utf8Encode = new TextEncoder();
const dataKey = new Uint8Array(32);
dataKey.set(utf8Encode.encode("NGL/USD"));

// Define the UDF program ID
const udf_program_id = new web3.PublicKey("7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7");

// Find the PDA (Program Derived Address) for the latest update
let latestUpdatePda = web3.PublicKey.findProgramAddressSync(
    [UDF_ROOT, utf8.encode("LAST_UPDATE"), UDF_PROTOCOL_ID, dataKey],
    udf_program_id
)[0];

// Send a transaction to consume the price from the oracle
const getLastPriceTx = await consumer_program.methods
    .consumePrice(dataKey)
    .accounts({
        signer: owner.publicKey,
        priceOracle: udf_program.programId,
        latestUpdate: latestUpdatePda,
    })
    .signers([owner])
    .rpc();

// Log the transaction signature
console.log("Consume tx signature", getLastPriceTx);
// Some code

The code above requires the following dependencies:

Example of Logs Generated During the Call

The following logs are generated when the transaction is executed. The call invokes two Solana programs in turn:

  1. Consumer Program:

    • Program ID: 3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv

  2. Entangle Price Oracle:

    • Program ID: 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7

Price Data for NGL/USD

  • The resulting price for the NGL/USD pair is interpreted as a u128 big endian value: 129898283383055207

  • This price should be multiplied by 10^18 to get the final value.

Interpretation of the Result:

  • Final Price: 129898283383055207 multiplied by 10^18 is interpreted as 0.12989.

  • Timestamp: The price was issued on August 12, 2024, at 22:45:24.

[
"Program 3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv invoke [1]",
"Program log: Instruction: ConsumePrice",
"Program 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7 invoke [2]",
"Program log: Instruction: LastPrice",
"Program 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7 consumed 5014 of 192696
compute units",
"Program return: 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc19zO3642eEkLpmAAAAAA==",
"Program 7HramSnctpbXqZ4SEzqvqteZdMdj3tEB2c9NT7egPQi7 success",
"Program log: Price of:
NGL/USD\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u
0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 is:
129898283383055207 at: 1723502724",
"Program 3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv consumed 14134 of 200000
compute units",
"Program return: 3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc19zO3642eEkLpmAAAAAA==",
"Program 3r5ixGQu8DRmJWgFEjwnDUQ6yasfYFXDsUbqkA6gkRtv success"
]

With this guide, you can now integrate Solana's Entangle Universal Data Feeds price data into your decentralized applications. This system efficiently retrieves price and timestamp information, ensuring that your program can access accurate on-chain data with minimal latency.

Last updated