This guide explains how to securely integrate with Chaos oracle price feeds on the Solana blockchain. It follows a similar structure to the EVM Integration Guide to provide a consistent developer experience for both pull and push oracles.
How to Use the Pull Oracle
To integrate with the pull oracle on Solana, you will need to fetch signed price data from the Chaos API and then verify and consume it in your on-chain program.
Step 1: Obtain API Keys and Signer Address
Before you can fetch data or verify signatures, you must know the oracle’s trusted signer address. Contact the Chaos team to obtain your API keys and the public key of the signer for the feeds you intend to use. This signer address is required for making API requests and for on-chain signature verification.
Step 2: Fetch Signed Price Data
Using your API key, make a GET request to the /prices/{feedId}/latest
endpoint to retrieve the latest SVM-signed price data. You can find more details in the API reference.
Other options for requesting SVM compatible price data are the following:
- Batch prices: Use
/prices/batch
to fetch multiple feeds in a single request
- Historical prices: Use
/prices/queryhistory
to retrieve historical price data
- Latest or historical: Use
/prices
to get either latest or historical prices based on parameters
Example API Response:
{
"feedId": "BTCUSD",
"price": 11936907500000,
"ts": 1753085844,
"expo": -8,
"signature": "61410a3b6be6fd59351ff053455e97c9840dff6c7c8d4668b46b8f90ca705f7157aed0b53f534e2706cfff9f8d44badaf9c2a94f7443a35117cfaf4c0dd484ee",
"recoveryId": 1
}
Step 3: Verify and Consume the Price On-Chain
The data from the API response must be passed to your Solana program to be verified. The program should hash the price data and verify the signature against the trusted signer address you obtained in Step 1 and stored on-chain.
Here is an example of how to implement the verification logic and consume the price in your program.
The following code is for illustrative purposes only, has not been audited, and should not be used in production without thorough testing.
use anchor_lang::{
prelude::*,
solana_program::{keccak, secp256k1_recover},
};
/// Represents a signed price message from the Chaos Pull Oracle
/// Each message contains:
/// - A price value with its exponent (e.g., 19476500000 * 10^-8 = $194.765)
/// - A timestamp in seconds since Unix epoch
/// - A secp256k1 signature (64 bytes) and recovery ID (1 byte)
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct SignedPriceMessage {
/// Recovery ID (0-3) used in secp256k1 signature recovery
pub recovery_id: u8,
/// 64-byte secp256k1 signature of the encoded price message
pub signature: [u8; 64],
/// Integer price value (must be combined with expo for actual price)
pub price: u64,
/// Price exponent (e.g., -8 means price is divided by 10^8)
pub expo: i8,
/// Unix timestamp in seconds when the price was signed
pub timestamp: i64,
}
impl SignedPriceMessage {
/// Encodes the price message into the standard 49-byte format for verification
/// Format:
/// - Bytes 0-31: Feed pair ID (e.g., "BTCUSD" padded with zeros)
/// - Bytes 32-39: Price as little-endian u64
/// - Bytes 40: Exponent as little-endian i8
/// - Bytes 41-48: Timestamp as little-endian i64
pub fn encode_price_message(&self, feed: &PriceFeed) -> [u8; 49] {
let mut m = [0u8; 49];
// First 32 bytes: Feed pair identifier
m[..32].clone_from_slice(&feed.pair);
// Next 8 bytes: Price in little-endian
m[32..40].clone_from_slice(&self.price.to_le_bytes());
// Next 1 byte: Exponent in little-endian
m[40..41].clone_from_slice(&self.expo.to_le_bytes());
// Last 8 bytes: Timestamp in little-endian
m[41..49].clone_from_slice(&self.timestamp.to_le_bytes());
m
}
/// Creates the Keccak256 hash of the encoded price message
/// This hash is what gets signed by the oracle
pub fn create_verification_hash(&self, feed: &PriceFeed) -> [u8; 32] {
keccak::hash(&self.encode_price_message(feed)).to_bytes()
}
/// Verifies the signature using Solana's secp256k1 recovery
/// Steps:
/// 1. Creates message hash from encoded price data
/// 2. Recovers the public key from the signature
/// 3. Verifies the recovered key matches the authorized signer
/// 4. Checks both X-coordinate and parity (even/odd) of the key
pub fn verify_signature(&self, feed: &PriceFeed) -> Result<()> {
let signer: &[u8; 33] = &feed.signer;
// Check if the expected public key is even or odd
let is_even = signer[0].rem(2) == 0;
// Recover the public key from signature
let pubkey = secp256k1_recover::secp256k1_recover(
&self.create_verification_hash(feed),
self.recovery_id,
&self.signature,
).map_err(|_| error!(ErrorCode::InvalidSigner))?;
// Verify the X-coordinate matches
require!(
pubkey.0[..32].eq(&signer[1..]),
ErrorCode::InvalidSigner
);
// Verify the parity (even/odd) matches
require_eq!(
(pubkey.0[63].rem(2) == 0),
is_even,
ErrorCode::InvalidSigner
);
Ok(())
}
}
To use the verification logic, you need to:
- Store the Signer Address: The trusted signer’s public key must be stored on-chain in an account (like the
PriceFeed
account shown above) so your program can access it for verification.
- Integrate the Verification Logic: Incorporate the
SignedPriceMessage
struct and its implementation into your Solana program. You will also need a PriceFeed
account structure to hold the trusted signer key for each price feed.
- Create a Consume Instruction: Create an instruction in your program that accepts a
SignedPriceMessage
and the PriceFeed
account. Your off-chain client will fetch the signed price from the API and then call this instruction to verify and consume the price on-chain.
This code is provided for illustrative purposes only and has not undergone any formal security audit.
#[program]
pub mod price_consumer {
use super::*;
pub fn consume_price(ctx: Context<ConsumePrice>, msg: SignedPriceMessage) -> Result<()> {
// 1. Verify the signature against the trusted signer in the 'feed' account.
msg.verify_signature(&ctx.accounts.feed)?;
// 2. Check the timestamp to prevent replay attacks.
let clock = Clock::get()?;
require!(clock.unix_timestamp - msg.timestamp < 60, ErrorCode::StalePrice);
// 3. Use the verified price.
let feed_id = std::str::from_utf8(&ctx.accounts.feed.pair).unwrap().trim_end_matches('\0');
msg!("Verified price for feed {}: {}", feed_id, msg.price);
// Your logic to consume the price goes here.
Ok(())
}
}
#[derive(Accounts)]
pub struct ConsumePrice<'info> {
pub feed: Account<'info, PriceFeed>,
// Add any other accounts your instruction needs.
}
Support
For assistance with SVM integration, contact our support team at [email protected].