Skip to main content

How to Use Pyth for Price Feeds on Solana

Created on
Updated on
Jul 3, 2023

9 min read

Overview

Pyth is an Oracle that brings real-time data fees on-chain in a simple and easy-to-use way. Pyth works directly with first-party data publishers to bring financial asset pricing data on-chain. Pyth aggregates data for cryptocurrencies, equities, forex currencies, and commodities. By working with many data providers, Pyth can provide high data integrity and security. In this guide, we will learn how to fetch price data from Pyth in your Solana programs.

What You Will Do

In this guide, you will learn how to use the Pyth SDK to integrate real-time pricing data into your program. You will:

  1. Learn the Basics of the Pyth protocol
  2. Build a simple Solana program that requests and logs a price feed from Pyth
  3. Test the program on Solana's devnet

What You Will Need

Dependencies Used in this Guide

In this guide, we will be using Solana Playground. Our tests were performed with the following dependencies (May 30, 2023):

DependencyVersion
anchor-lang0.27.0
solana-program1.14.17
pyth-sdk-solana0.7.0

How does Pyth Work?

Pyth is a data oracle that brings price feeds for various asset classes to the Solana blockchain. "Pyth price updates are created on Pythnet and streamed off-chain via the Wormhole Network, a cross-chain messaging protocol. These updates are signed so the Pyth on-chain program can verify their authenticity." *(Source: Pyth Docs) Pythnet is a private cluster powered by the Solana blockchain. Because Pyth uses Wormhole, it can bring data to multiple blockchains.

For Solana, asset and pricing data is stored and broadcast on-chain via Solana Accounts. Three main types of accounts exist in Pyth:

  1. Product (Metadata) Accounts: These accounts store information about the asset (e.g., symbol, asset type, description).
  2. Price Accounts: These accounts store the price data for the asset, a confidence interval for the price, and the time of the last update.
  3. Mapping Accounts: These accounts map the product account to the price account.

Today's exercise will be primarily focused on the Price Accounts. For more information on other account types, check out the Pyth Docs.

Initiate a New Anchor Project

New to Anchor?

Anchor is a popular development framework for building Programs on Solana. To get started, check out our Intro to Anchor Guide.

We will be using Solana Playground to accelerate our development. Solana Playground is a web-based IDE that allows you to write, deploy, and test Solana programs. If you prefer to use your own local Anchor project, just make sure to add pyth-sdk-solana = "0.7.1" to your Cargo.toml file.

Head over to beta.solpg.io and click "➕" create a new project:

  • Select "Anchor (Rust)"
  • Name it "Pyth Demo" and click "Create"

Create a new Anchor Project

Create Your Program

Let's create a new program that will fetch a price feed from Pyth and log it to the Solana Program logs. Open src>lib.rs, which should be prepopulated with a simple program. Go ahead and delete the default content.

Let's start by importing the dependencies we will need for this program. We will need the following dependencies:

use anchor_lang::prelude::*;
use pyth_sdk_solana::{load_price_feed_from_account_info};
use std::str::FromStr;

// This is your program's public key, and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");

In addition to the Anchor dependencies, we will need to import the Pyth SDK's load_price_feed_from_account_info and the FromStr trait from the Rust standard library (which will allow us to convert a string address into a public key).

Solana Playground will use and default declare_id! field to define your program ID. This will be updated automatically when you build your program.

We are going to define two constants as well:

  1. a price feed address (this will be the address of the price feed we want to fetch). A list of all available feeds can be found here (make sure to select "Solana Devnet" from the drop-down). For this demo, we will use the BTC/USD price feed on Solana's devnet, HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J.
  2. a "staleness threshold" that will be used to determine if the price feed is stale (i.e., if the price feed has not been updated in the last 60 seconds, we will consider it stale)

Add the following declarations below your imports:

const BTC_USDC_FEED: &str = "HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J";
const STALENESS_THRESHOLD: u64 = 60; // staleness threshold in seconds

Feel free to experiment with different price feeds and staleness thresholds, but for our example, we will be looking for BTC/USD price feeds that have been created at least within the last 60 seconds.

Create a Price Feed Struct

Let's define a struct for our price feed instruction. The struct will define which accounts we must pass to our program to fetch the price feed. Below your constants, add:

#[derive(Accounts)]
pub struct FetchBitcoinPrice<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(address = Pubkey::from_str(BTC_USDC_FEED).unwrap() @ FeedError::InvalidPriceFeed)]
pub price_feed: AccountInfo<'info>,
}

#[error_code]
pub enum FeedError {
#[msg("Invalid Price Feed")]
InvalidPriceFeed,
}

We are defining a struct called FetchBitcoinPrice that will have two accounts:

  1. signer: This will be the account that signs the transaction and pays the transaction fees.
  2. price_feed: This will be the price feed account that we want to fetch. We use the Anchor address constraint (learn more about Anchor constraints in our guide, here). We use the Pubkey::from_str function to convert our BTC_USDC_FEED string into a public key. We also define an error code, InvalidPriceFeed, that will be thrown if the price feed account is invalid.

Create a Price Feed Instruction

Let's define our program and create a function to fetch the price feed. Below your struct, add:

#[program]
mod hello_pyth {
use super::*;
pub fn fetch_btc_price(ctx: Context<FetchBitcoinPrice>) -> Result<()> {
// 1-Fetch latest price

// 2-Format display values rounded to the nearest dollar

// 3-Log result


Ok(())
}
}

We are defining a program called hello_pyth and a function called fetch_btc_price that will take in a Context of type FetchBitcoinPrice and return a Result (either an Ok or an Err).

Fetch the Latest Price

To fetch the latest price, we pass our price feed address, &ctx.accounts.price_feed into the Pyth load_price_feed_from_account_info() function:

        // 1-Fetch latest price
let price_account_info = &ctx.accounts.price_feed;
let price_feed = load_price_feed_from_account_info( &price_account_info ).unwrap();
let current_timestamp = Clock::get()?.unix_timestamp;
let current_price = price_feed.get_price_no_older_than(current_timestamp, STALENESS_THRESHOLD).unwrap();

We also use the Clock struct to get the current timestamp and define a current_price variable that fetches a price as long as it is no older than the staleness threshold we specified earlier.

Format Display Values

Pyth pricing data is stored as a 64-bit signed integer with a 32-bit signed exponent.

Pyth's Price struct
Here's the definition of Pyth's Price struct (source):
pub struct Price {
/// Price.
#[serde(with = "utils::as_string")] // To ensure accuracy on conversion to json.
#[schemars(with = "String")]
pub price: i64,
/// Confidence interval.
#[serde(with = "utils::as_string")]
#[schemars(with = "String")]
pub conf: u64,
/// Exponent.
pub expo: i32,
/// Publish time.
pub publish_time: UnixTimestamp,
}

To log the price in a human-readable format (e.g., 27400 instead of 27400000000000 ), we will need to convert the price and confidence interval (conf) into a u64 and then divide it by the number of decimals (which we must also convert to a u64). Since Pyth stores this as a negative, we must convert it to a positive number before dividing. Add the following to your fetch_btc_price function:

        // 2-Format display values rounded to nearest dollar
let display_price = u64::try_from(current_price.price).unwrap() / 10u64.pow(u32::try_from(-current_price.expo).unwrap());
let display_confidence = u64::try_from(current_price.conf).unwrap() / 10u64.pow(u32::try_from(-current_price.expo).unwrap());

Finally, we will log the result to Solana's program logs using msg!. Your final instruction should look like this:

#[program]
mod hello_pyth {
use super::*;
pub fn fetch_btc_price(ctx: Context<FetchBitcoinPrice>) -> Result<()> {
// 1-Fetch latest price
let price_account_info = &ctx.accounts.price_feed;
let price_feed = load_price_feed_from_account_info( &price_account_info ).unwrap();
let current_timestamp = Clock::get()?.unix_timestamp;
let current_price = price_feed.get_price_no_older_than(current_timestamp, STALENESS_THRESHOLD).unwrap();

// 2-Format display values rounded to nearest dollar
let display_price = u64::try_from(current_price.price).unwrap() / 10u64.pow(u32::try_from(-current_price.expo).unwrap());
let display_confidence = u64::try_from(current_price.conf).unwrap() / 10u64.pow(u32::try_from(-current_price.expo).unwrap());


// 3-Log result
msg!("BTC/USD price: ({} +- {})", display_price, display_confidence);
Ok(())
}
}

Nice job! Let's test it out.

Build the Program

Click 🔧 Build in the left-hand menu to compile your program. You should see a success message in the console:

Building...
Build successful. Completed in 3.17s.

If you have followed the instructions, you should not get any errors, but if you do, try and follow the instructions to debug. If you need help, feel free to reach out on Discord. We're happy to help!

Deploy the Program

Since this project is just for demonstration purposes, we can use a "throw-away" wallet. Solana Playground makes it easy to create one. You should see a red dot "Not connected" in the bottom left corner of the browser window. Click it:

Not Connected

Solana Playground will generate a wallet for you (or you can import your own). Feel free to save it for later use, and click continue when you're ready. A new wallet will be initiated and connected to Solana devnet. You will need about 5 Devnet SOL to deploy the program (Check out our Guide on Airdropping Devnet SOL).

You should click the "⚙️" icon in the bottom left to verify you are on "devnet". You can also select "custom" from the drop-down list if you would like to connect to your QuickNode Devnet Endpoint.

💡 Creating a QuickNode Endpoint

Connect to a Solana Cluster with Your QuickNode Endpoint

To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us. See why over 50% of projects on Solana choose QuickNode and sign up for a free account here. We're going to use a Solana Devnet endpoint.

Copy the HTTP Provider link:

When you are ready, click the Tools Icon 🛠 on the left side of the page, and then click "Deploy." This will take a few minutes, but you should see a success message when it is complete.

Test the Program

Now that your program is on-chain let's test it out. Click the "🧪" icon on the left side of the page. Solana Playground has built an easy-to-use interface for testing programs. Expand the fetchBtcPrice instruction by clicking the toggle arrow:

Test Program

Select My address as the signer, and paste the same Pyth price feed you used in your program (we used HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J).

Click "Test" to send the transaction to the devnet cluster. You should see a success message in the console:

Testing 'fetchBtcPrice'...
✅ Test 'fetchBtcPrice' passed.

You'll also see a notification pop-up with a Solana Explorer link. Open it. If the notification disappeared before you clicked it, you can either test your instruction again or find the transaction in the Solana Explorer by searching for your program ID on devnet (e.g., explorer.solana.com/address/YOUR_PROGRAM_ID?cluster=devnet). Your program ID is available in the 🛠️ Tools menu under "Program ID" and in your declare_id statement in lib.rs.

When you open the transaction in the Solana Explorer, you should see a "Program Instructions Logs" section that includes the price of BTC in USD:

Solana Explorer

Nice job!

You can access our complete code on Solana Playground.

Wrap Up

You just built a program that fetches the price of BTC from Pyth and logs it to Solana's program logs. You can use this same pattern to create programs that fetch any data from Pyth and use it as a starting point for building more complex programs.

Got a question or idea you want to share, drop us a line on Discord or Twitter!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Resources

Share this guide