use hopper_runtime::error::ProgramError;
pub const PYTH_MAGIC: u32 = 0xa1b2c3d4;
pub const PYTH_VERSION: u32 = 2;
pub const PYTH_PRICE_TYPE: u32 = 3;
pub const STATUS_TRADING: u32 = 1;
pub const PYTH_HEADER_LEN: usize = 240;
const OFF_MAGIC: usize = 0;
const OFF_VERSION: usize = 4;
const OFF_ATYPE: usize = 8;
const OFF_EXPO: usize = 20;
const OFF_EMA_PRICE: usize = 48;
const OFF_EMA_CONF: usize = 72;
const OFF_TIMESTAMP: usize = 96;
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;
#[inline(always)]
fn read_u32(data: &[u8], off: usize) -> u32 {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
#[inline(always)]
fn read_i32(data: &[u8], off: usize) -> i32 {
i32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
#[inline(always)]
fn read_u64(data: &[u8], off: usize) -> u64 {
u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
])
}
#[inline(always)]
fn read_i64(data: &[u8], off: usize) -> i64 {
i64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
])
}
pub struct PythPrice {
pub price: i64,
pub conf: u64,
pub expo: i32,
pub publish_time: i64,
}
pub struct PythEma {
pub price: i64,
pub conf: u64,
pub expo: i32,
}
#[inline(always)]
pub fn read_pyth_price(data: &[u8]) -> Result<PythPrice, ProgramError> {
if data.len() < PYTH_HEADER_LEN {
return Err(ProgramError::AccountDataTooSmall);
}
check_pyth_header(data)?;
let status = read_u32(data, OFF_AGG_STATUS);
if status != STATUS_TRADING {
return Err(ProgramError::InvalidAccountData);
}
Ok(PythPrice {
price: read_i64(data, OFF_AGG_PRICE),
conf: read_u64(data, OFF_AGG_CONF),
expo: read_i32(data, OFF_EXPO),
publish_time: read_i64(data, OFF_TIMESTAMP),
})
}
#[inline(always)]
pub fn read_pyth_ema(data: &[u8]) -> Result<PythEma, ProgramError> {
if data.len() < PYTH_HEADER_LEN {
return Err(ProgramError::AccountDataTooSmall);
}
check_pyth_header(data)?;
Ok(PythEma {
price: read_i64(data, OFF_EMA_PRICE),
conf: read_u64(data, OFF_EMA_CONF),
expo: read_i32(data, OFF_EXPO),
})
}
#[inline(always)]
pub fn pyth_agg_pub_slot(data: &[u8]) -> Result<u64, ProgramError> {
if data.len() < PYTH_HEADER_LEN {
return Err(ProgramError::AccountDataTooSmall);
}
check_pyth_header(data)?;
Ok(read_u64(data, OFF_AGG_PUB_SLOT))
}
#[inline(always)]
pub fn check_pyth_price_fresh(
publish_time: i64,
current_time: i64,
max_age_seconds: i64,
) -> Result<(), ProgramError> {
if current_time.wrapping_sub(publish_time) > max_age_seconds {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
#[inline(always)]
pub fn check_pyth_confidence(price: i64, conf: u64, max_conf_pct: u64) -> Result<(), ProgramError> {
let abs_price = (price as i128).unsigned_abs();
if abs_price == 0 {
return Err(ProgramError::InvalidAccountData);
}
let ratio = (conf as u128)
.checked_mul(100)
.ok_or(ProgramError::ArithmeticOverflow)?
/ abs_price;
if ratio > max_conf_pct as u128 {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
#[inline(always)]
fn check_pyth_header(data: &[u8]) -> Result<(), ProgramError> {
let magic = read_u32(data, OFF_MAGIC);
let ver = read_u32(data, OFF_VERSION);
let atype = read_u32(data, OFF_ATYPE);
if magic != PYTH_MAGIC || ver != PYTH_VERSION || atype != PYTH_PRICE_TYPE {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use alloc::vec;
use alloc::vec::Vec;
fn write_u32(data: &mut [u8], off: usize, val: u32) {
data[off..off + 4].copy_from_slice(&val.to_le_bytes());
}
fn write_i32(data: &mut [u8], off: usize, val: i32) {
data[off..off + 4].copy_from_slice(&val.to_le_bytes());
}
fn write_u64(data: &mut [u8], off: usize, val: u64) {
data[off..off + 8].copy_from_slice(&val.to_le_bytes());
}
fn write_i64(data: &mut [u8], off: usize, val: i64) {
data[off..off + 8].copy_from_slice(&val.to_le_bytes());
}
fn sample_pyth_price_account(
price: i64,
conf: u64,
expo: i32,
ts: i64,
status: u32,
) -> Vec<u8> {
let mut data = vec![0u8; PYTH_HEADER_LEN];
write_u32(&mut data, OFF_MAGIC, PYTH_MAGIC);
write_u32(&mut data, OFF_VERSION, PYTH_VERSION);
write_u32(&mut data, OFF_ATYPE, PYTH_PRICE_TYPE);
write_i32(&mut data, OFF_EXPO, expo);
write_i64(&mut data, OFF_EMA_PRICE, price / 2); write_u64(&mut data, OFF_EMA_CONF, conf / 2);
write_i64(&mut data, OFF_TIMESTAMP, ts);
write_i64(&mut data, OFF_AGG_PRICE, price);
write_u64(&mut data, OFF_AGG_CONF, conf);
write_u32(&mut data, OFF_AGG_STATUS, status);
write_u64(&mut data, OFF_AGG_PUB_SLOT, 42);
data
}
#[test]
fn read_price_valid() {
let data = sample_pyth_price_account(12345678, 1000, -8, 1_700_000_000, STATUS_TRADING);
let p = read_pyth_price(&data).unwrap();
assert_eq!(p.price, 12345678);
assert_eq!(p.conf, 1000);
assert_eq!(p.expo, -8);
assert_eq!(p.publish_time, 1_700_000_000);
}
#[test]
fn rejects_non_trading() {
let data = sample_pyth_price_account(100, 10, -8, 100, 0);
assert!(read_pyth_price(&data).is_err());
}
#[test]
fn rejects_wrong_magic() {
let mut data = sample_pyth_price_account(100, 10, -8, 100, STATUS_TRADING);
write_u32(&mut data, OFF_MAGIC, 0xdeadbeef);
assert!(read_pyth_price(&data).is_err());
}
#[test]
fn rejects_wrong_version() {
let mut data = sample_pyth_price_account(100, 10, -8, 100, STATUS_TRADING);
write_u32(&mut data, OFF_VERSION, 99);
assert!(read_pyth_price(&data).is_err());
}
#[test]
fn rejects_too_short() {
let data = vec![0u8; 100];
assert!(read_pyth_price(&data).is_err());
}
#[test]
fn read_ema_valid() {
let data = sample_pyth_price_account(12345678, 1000, -8, 100, STATUS_TRADING);
let ema = read_pyth_ema(&data).unwrap();
assert_eq!(ema.price, 12345678 / 2);
assert_eq!(ema.conf, 500);
assert_eq!(ema.expo, -8);
}
#[test]
fn ema_reads_non_trading() {
let data = sample_pyth_price_account(100, 10, -8, 100, 0);
assert!(read_pyth_ema(&data).is_ok());
}
#[test]
fn pub_slot_reads() {
let data = sample_pyth_price_account(100, 10, -8, 100, STATUS_TRADING);
assert_eq!(pyth_agg_pub_slot(&data).unwrap(), 42);
}
#[test]
fn freshness_check() {
assert!(check_pyth_price_fresh(100, 110, 30).is_ok());
assert!(check_pyth_price_fresh(100, 131, 30).is_err());
assert!(check_pyth_price_fresh(100, 130, 30).is_ok());
}
#[test]
fn confidence_check() {
assert!(check_pyth_confidence(100, 5, 5).is_ok());
assert!(check_pyth_confidence(100, 5, 4).is_err());
assert!(check_pyth_confidence(0, 5, 5).is_err());
assert!(check_pyth_confidence(-100, 5, 5).is_ok());
}
}