use anyhow::Result;
use borsh::BorshSerialize;
use solana_sdk::{account::Account, pubkey, pubkey::Pubkey};
use crate::context::TestContext;
pub const PYTH_PROGRAM_ID: Pubkey = pubkey!("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH");
const PYTH_PRICE_ACCOUNT_SIZE: usize = 3312;
const PYTH_MAGIC: u32 = 0xa1b2c3d4;
const PYTH_VERSION: u32 = 2;
const PYTH_ACCOUNT_TYPE_PRICE: u32 = 3;
const PYTH_PRICE_TYPE: u32 = 1;
const PYTH_STATUS_TRADING: u32 = 1;
const OFF_MAGIC: usize = 0;
const OFF_VERSION: usize = 4;
const OFF_ACCOUNT_TYPE: usize = 8;
const OFF_SIZE: usize = 12;
const OFF_PRICE_TYPE: usize = 16;
const OFF_EXPONENT: usize = 20;
const OFF_NUM_COMPONENTS: usize = 24;
const OFF_LAST_SLOT: usize = 32;
const OFF_VALID_SLOT: usize = 40;
const OFF_AGG_PRICE: usize = 208;
const OFF_AGG_CONF: usize = 216;
const OFF_AGG_STATUS: usize = 224;
const OFF_AGG_PUB_SLOT: usize = 232;
const OFF_AGG_PUBLISH_TIME: usize = 240;
pub fn build_spl_mint(decimals: u8) -> Account {
use solana_sdk::program_pack::Pack;
use spl_token::state::Mint;
let mint = Mint {
mint_authority: solana_sdk::program_option::COption::None,
supply: 1_000_000_000_000_000,
decimals,
is_initialized: true,
freeze_authority: solana_sdk::program_option::COption::None,
};
let mut data = vec![0u8; Mint::LEN];
mint.pack_into_slice(&mut data);
Account { lamports: 10_000_000, data, owner: spl_token::ID, executable: false, rent_epoch: 0 }
}
pub fn build_spl_token_account(mint: Pubkey, owner: Pubkey, amount: u64) -> Account {
use solana_sdk::program_pack::Pack;
use spl_token::state::Account as TokenAccount;
let token = TokenAccount {
mint,
owner,
amount,
delegate: solana_sdk::program_option::COption::None,
state: spl_token::state::AccountState::Initialized,
is_native: solana_sdk::program_option::COption::None,
delegated_amount: 0,
close_authority: solana_sdk::program_option::COption::None,
};
let mut data = vec![0u8; TokenAccount::LEN];
token.pack_into_slice(&mut data);
Account { lamports: 10_000_000, data, owner: spl_token::ID, executable: false, rent_epoch: 0 }
}
pub fn pack_into_program_account<T: BorshSerialize>(
data: &T,
program_id: &Pubkey,
) -> Result<Account, std::io::Error> {
let bytes = borsh::to_vec(data)?;
Ok(Account {
lamports: 10_000_000,
data: bytes,
owner: *program_id,
executable: false,
rent_epoch: 0,
})
}
pub fn build_pyth_price_account(price: i64, expo: i32, conf: u64) -> Account {
let mut data = vec![0u8; PYTH_PRICE_ACCOUNT_SIZE];
let now = chrono::Utc::now().timestamp();
write_le_u32(&mut data, OFF_MAGIC, PYTH_MAGIC);
write_le_u32(&mut data, OFF_VERSION, PYTH_VERSION);
write_le_u32(&mut data, OFF_ACCOUNT_TYPE, PYTH_ACCOUNT_TYPE_PRICE);
write_le_u32(&mut data, OFF_SIZE, PYTH_PRICE_ACCOUNT_SIZE as u32);
write_le_u32(&mut data, OFF_PRICE_TYPE, PYTH_PRICE_TYPE);
write_le_i32(&mut data, OFF_EXPONENT, expo);
write_le_u32(&mut data, OFF_NUM_COMPONENTS, 1);
write_le_u64(&mut data, OFF_LAST_SLOT, 1);
write_le_u64(&mut data, OFF_VALID_SLOT, 1);
write_le_i64(&mut data, OFF_AGG_PRICE, price);
write_le_u64(&mut data, OFF_AGG_CONF, conf);
write_le_u32(&mut data, OFF_AGG_STATUS, PYTH_STATUS_TRADING);
write_le_u64(&mut data, OFF_AGG_PUB_SLOT, 1);
write_le_i64(&mut data, OFF_AGG_PUBLISH_TIME, now);
Account { lamports: 10_000_000, data, owner: PYTH_PROGRAM_ID, executable: false, rent_epoch: 0 }
}
pub fn update_oracle_price(ctx: &TestContext, oracle: Pubkey, new_price: i64) -> Result<()> {
let mut svm = ctx.lock_svm();
let mut account = svm
.get_account(&oracle)
.ok_or_else(|| anyhow::anyhow!("oracle account {oracle} not found in SVM"))?;
if account.data.len() < OFF_AGG_PRICE + 8 {
anyhow::bail!(
"account data too short ({} bytes) to be a Pyth V2 price account",
account.data.len()
);
}
let magic = u32::from_le_bytes(account.data[OFF_MAGIC..OFF_MAGIC + 4].try_into().unwrap());
if magic != PYTH_MAGIC {
anyhow::bail!("account does not have Pyth V2 magic (got {:#x})", magic);
}
write_le_i64(&mut account.data, OFF_AGG_PRICE, new_price);
svm.set_account(oracle, account)?;
Ok(())
}
fn write_le_u32(buf: &mut [u8], offset: usize, value: u32) {
buf[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn write_le_i32(buf: &mut [u8], offset: usize, value: i32) {
buf[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
fn write_le_u64(buf: &mut [u8], offset: usize, value: u64) {
buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
fn write_le_i64(buf: &mut [u8], offset: usize, value: i64) {
buf[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{accounts::set_account, context::new_test_context};
fn read_le_u32(buf: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(buf[offset..offset + 4].try_into().unwrap())
}
fn read_le_i32(buf: &[u8], offset: usize) -> i32 {
i32::from_le_bytes(buf[offset..offset + 4].try_into().unwrap())
}
fn read_le_u64(buf: &[u8], offset: usize) -> u64 {
u64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap())
}
fn read_le_i64(buf: &[u8], offset: usize) -> i64 {
i64::from_le_bytes(buf[offset..offset + 8].try_into().unwrap())
}
#[test]
fn test_build_pyth_price_account_fields() {
let price: i64 = 12_345_000_000;
let expo: i32 = -8;
let conf: u64 = 500_000;
let account = build_pyth_price_account(price, expo, conf);
assert_eq!(account.data.len(), PYTH_PRICE_ACCOUNT_SIZE);
assert_eq!(account.owner, PYTH_PROGRAM_ID);
let data = &account.data;
assert_eq!(read_le_u32(data, OFF_MAGIC), PYTH_MAGIC, "magic mismatch");
assert_eq!(read_le_u32(data, OFF_VERSION), PYTH_VERSION, "version mismatch");
assert_eq!(
read_le_u32(data, OFF_ACCOUNT_TYPE),
PYTH_ACCOUNT_TYPE_PRICE,
"account type mismatch"
);
assert_eq!(read_le_i32(data, OFF_EXPONENT), expo, "exponent mismatch");
assert_eq!(read_le_i64(data, OFF_AGG_PRICE), price, "aggregate price mismatch");
assert_eq!(read_le_u64(data, OFF_AGG_CONF), conf, "aggregate conf mismatch");
assert_eq!(
read_le_u32(data, OFF_AGG_STATUS),
PYTH_STATUS_TRADING,
"aggregate status should be Trading"
);
}
#[test]
fn test_update_oracle_price() {
let ctx = new_test_context().unwrap();
let oracle = Pubkey::new_unique();
let initial_price: i64 = 10_000_000_000; let account = build_pyth_price_account(initial_price, -8, 100_000);
set_account(&ctx, oracle, account).unwrap();
{
let svm = ctx.lock_svm();
let stored = svm.get_account(&oracle).unwrap();
assert_eq!(read_le_i64(&stored.data, OFF_AGG_PRICE), initial_price);
}
let new_price: i64 = 15_000_000_000; update_oracle_price(&ctx, oracle, new_price).unwrap();
{
let svm = ctx.lock_svm();
let stored = svm.get_account(&oracle).unwrap();
assert_eq!(read_le_i64(&stored.data, OFF_AGG_PRICE), new_price);
}
{
let svm = ctx.lock_svm();
let stored = svm.get_account(&oracle).unwrap();
assert_eq!(read_le_u32(&stored.data, OFF_MAGIC), PYTH_MAGIC);
assert_eq!(read_le_i32(&stored.data, OFF_EXPONENT), -8);
assert_eq!(read_le_u64(&stored.data, OFF_AGG_CONF), 100_000);
}
}
}