Integrate Pyth Lazer as a Consumer on Solana
This guide is intended to serve users who wants to consume prices from the Pyth Lazer on Solana.
Integrating with Pyth Lazer in smart contracts as a consumer is a three-step process:
- Use Pyth Lazer SDK into Solana smart contracts to parse the price updates.
- Subscribe to Pyth Lazer websocket to receive price updates on backend or frontend.
- Include the price updates into the smart contract transactions.
Use Pyth Lazer SDK in smart contracts
Pyth Lazer provides a Solana SDK (opens in a new tab), which allows consumers to parse and verify the price updates on Solana-compatible chains.
To start, add the following to your Cargo.toml
file (please check the current latest version at crates.io (opens in a new tab)):
[dependencies]
pyth-lazer-solana-contract = { version = "x.y.z", features = ["no-entrypoint"] }
Now you can create an instruction or multiple instructions that will receive Pyth Lazer messages. The instruction data sent to your program should include a byte array containing the Pyth Lazer message. The instruction data can also contain any other parameters your contracts may need.
In order to successfully validate the Pyth Lazer message, the instruction needs to receive the following accounts:
- Fee payer account
- Pyth Lazer program (
pytd2yyk641x7ak7mkaasSJVXh6YYZnC7wTmtgAyxPt
(opens in a new tab)) - Pyth Lazer storage account (
3rdJbqfnagQ4yx9HXJViD4zc4xpiSqmFsKpPuSCQVyQL
) - Pyth Lazer treasury account (
Gx4MBPb1vqZLJajZmsKLg8fGw9ErhoKsR8LeKcCKFyak
) - The standard Solana system program account
- The standard Solana instructions sysvar account
You may also add any other accounts your contract needs.
The code snippets below are part of the full consumer contract example available on Github (opens in a new tab).
The following code can be used to set up a new instruction within a Solana contract:
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromPrimitive)]
pub enum Instruction {
//...
/// Update price.
/// Data: `UpdateArgs` followed by a signed Pyth Lazer update.
/// Accounts:
/// 1. payer account
/// 2. example data account [writable]
/// 3. pyth program account [readonly]
/// 4. pyth storage account [readonly]
/// 5. pyth treasury account [writable]
/// 6. system program [readonly]
/// 7. instructions sysvar sysvar account [readonly]
Update = 1,
}
/// Inputs to the `Update` instruction. `UpdateArgs` must be followed by a signed Pyth Lazer message.
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
#[repr(C, packed)]
pub struct UpdateArgs {
/// Example argument
pub hello: u64,
}
/// Program entrypoint's implementation.
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// In our example contract, the first byte is the ID of the instruction.
let instruction = *instruction_data
.first()
.ok_or(ProgramError::InvalidInstructionData)?;
let instruction =
Instruction::from_u8(instruction).ok_or(ProgramError::InvalidInstructionData)?;
let instruction_args = &instruction_data[1..];
match instruction {
Instruction::Initialize => {
process_initialize_instruction(program_id, accounts, instruction_args)
}
Instruction::Update => process_update_instruction(program_id, accounts, instruction_args),
}
}
pub fn process_update_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_args: &[u8],
) -> ProgramResult {
// Verify accounts passed to the instruction.
if accounts.len() != 7 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let payer_account = &accounts[0];
let data_account = &accounts[1];
let _pyth_program_account = &accounts[2];
let pyth_storage_account = &accounts[3];
let pyth_treasury_account = &accounts[4];
let system_program_account = &accounts[5];
let instructions_sysvar_account = &accounts[6];
// See below for next steps...
}
Invoke the Pyth Lazer on-chain program with appropriate arguments to validate the Pyth Lazer signature of the message.
// We expect the instruction to the built-in ed25519 program to be
// the first instruction within the transaction.
let ed25519_instruction_index = 0;
// We expect our signature to be the first (and only) signature to be checked
// by the built-in ed25519 program within the transaction.
let signature_index = 0;
// Verify Lazer signature.
invoke(
&ProgramInstruction::new_with_bytes(
pyth_lazer_solana_contract::ID,
&VerifyMessage {
message_data: pyth_message.to_vec(),
ed25519_instruction_index,
signature_index,
}
.data(),
vec![
AccountMeta::new(*payer_account.key, true),
AccountMeta::new_readonly(*pyth_storage_account.key, false),
AccountMeta::new(*pyth_treasury_account.key, false),
AccountMeta::new_readonly(*system_program_account.key, false),
AccountMeta::new_readonly(*instructions_sysvar_account.key, false),
],
),
&[
payer_account.clone(),
pyth_storage_account.clone(),
pyth_treasury_account.clone(),
system_program_account.clone(),
instructions_sysvar_account.clone(),
],
)?;
Note: When using native ed25519 signatures on Solana, we must use the built-in ed25519 program provided by the Solana runtime. This program can't be invoked from another contract. Instead, it must be called in an explicit instruction within the submitted transaction. This means that the sender of the transaction must include that instruction in the transaction. Our SDK leverages Solana runtime capabilities to ensure the ed25519 program has been correctly called in the transaction.
Now parse the Pyth Lazer message.
// Deserialize and use the payload.
let data = PayloadData::deserialize_slice_le(verified.payload)
.map_err(|_| ProgramError::InvalidInstructionData)?;
if data.feeds.is_empty() || data.feeds[0].properties.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
Now you can update the state according to the contract's logic:
// Read the data PDA of our example contract.
let mut state_data = data_account.data.borrow_mut();
let state =
try_from_bytes_mut::<State>(*state_data).map_err(|_| ProgramError::InvalidAccountData)?;
if state.price_feed != data.feeds[0].feed_id.0 {
return Err(ProgramError::InvalidInstructionData);
}
if data.channel_id != Channel::RealTime.id() {
return Err(ProgramError::InvalidInstructionData);
}
if data.timestamp_us.0 <= state.latest_timestamp {
return Err(ProgramError::AccountAlreadyInitialized);
}
let PayloadPropertyValue::Price(Some(price)) = data.feeds[0].properties[0] else {
return Err(ProgramError::InvalidInstructionData);
};
state.latest_price = price.into_inner().into();
state.latest_timestamp = data.timestamp_us.0;
Pyth Lazer also provides pyth_lazer_protocol (opens in a new tab) Rust crate, which allows consumers to parse the price updates off-chain.
Subscribe to Pyth Lazer to receive Price Updates
Pyth Lazer provides a websocket endpoint to receive price updates. Moreover, Pyth Lazer also provides a typescript SDK (opens in a new tab) to subscribe to the websocket endpoint.
Consult How to fetch price updates from Pyth Lazer for a complete step-by-step guide.
Include the price updates into smart contract transactions
Now that you have the price updates, and your smart contract is able to parse the price updates, you can include the price updates into the smart contract transactions by passing the price updates received from the previous step as an argument to the update_price
method of your smart contract.
In order to execute signature verification, you need to include an instruction for the built-in Solana ed25519 program in your transaction.
In Rust, you can leverage helpers provided in the pyth_lazer_solana_contract
crate:
// Instruction #0 will be ed25519 instruction;
// Instruction #1 will be our contract instruction.
let instruction_index = 1;
// Total offset of Pyth Lazer update within the instruction data;
// 1 byte is the instruction type.
let message_offset = (size_of::<UpdateArgs>() + 1).try_into().unwrap();
let ed25519_args = pyth_lazer_solana_contract::Ed25519SignatureOffsets::new(
&message,
instruction_index,
message_offset,
);
let mut tx = Transaction::new_with_payer(
&[
Instruction::new_with_bytes(
solana_program::ed25519_program::ID,
&pyth_lazer_solana_contract::ed25519_program_args(&[ed25519_args]),
vec![],
),
Instruction::new_with_bytes(
pyth_lazer_solana_example::ID,
&update_data,
vec![
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new(data_pda_key, false),
AccountMeta::new(pyth_lazer_solana_contract::ID, false),
AccountMeta::new_readonly(pyth_lazer_solana_contract::STORAGE_ID, false),
AccountMeta::new(treasury, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(sysvar::instructions::ID, false),
],
),
],
Some(&payer.pubkey()),
);
Additional Resources
You may find these additional resources helpful for integrating Pyth Lazer into your Solana smart contracts.
Price Feed IDs
Pyth Lazer supports a wide range of price feeds. Consult the Price Feed IDs page for a complete list of supported price feeds.
Examples
pyth-lazer-example-solana (opens in a new tab) is a simple example contract that parses and consumes price updates from Pyth Lazer.
pyth-lazer-example-js (opens in a new tab) is a simple example for subscribing to the Pyth Lazer websocket.