Skip to content

feat(solana-receiver-sdk): add get_twap_no_older_than to consume TwapUpdates #2174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion target_chains/solana/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pyth-solana-receiver-sdk"
version = "0.3.2"
version = "0.4.0"
description = "SDK for the Pyth Solana Receiver program"
authors = ["Pyth Data Association"]
repository = "https://github.com/pyth-network/pyth-crosschain"
Expand All @@ -15,5 +15,7 @@ name = "pyth_solana_receiver_sdk"
[dependencies]
anchor-lang = ">=0.28.0"
hex = ">=0.4.3"
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", version = "2.1.0", features = ["solana-program"]}
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", version = "2.1.0", features = [
"solana-program",
] }
solana-program = ">=1.16.0, <2.0.0"
140 changes: 139 additions & 1 deletion target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,70 @@ impl TwapUpdate {
+ 8
// posted_slot
);

/// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId`.
///
/// # Warning
/// This function does not check :
/// - How recent the price is
/// - Whether the price update has been verified
///
/// It is therefore unsafe to use this function without any extra checks,
/// as it allows for the possibility of using unverified or outdated price updates.
pub fn get_twap_unchecked(
&self,
feed_id: &FeedId,
) -> std::result::Result<TwapPrice, GetPriceError> {
check!(
self.twap.feed_id == *feed_id,
GetPriceError::MismatchedFeedId
);
Ok(self.twap)
}

/// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age` with `Full` verification.
///
/// # Example
/// ```
/// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, TwapUpdate};
/// use anchor_lang::prelude::*;
///
/// const MAXIMUM_AGE : u64 = 30;
/// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
///
/// #[derive(Accounts)]
/// pub struct ReadTwapAccount<'info> {
/// pub twap_update: Account<'info, TwapUpdate>,
/// }
///
/// pub fn read_twap_account(ctx : Context<ReadTwapAccount>) -> Result<()> {
/// let twap_update = &ctx.accounts.twap_update;
/// let twap = twap_update.get_twap_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?;
/// Ok(())
/// }
/// ```
pub fn get_twap_no_older_than(
&self,
clock: &Clock,
maximum_age: u64,
feed_id: &FeedId,
) -> std::result::Result<TwapPrice, GetPriceError> {
// Ensure the update is fully verified
check!(
self.verification_level.eq(&VerificationLevel::Full),
GetPriceError::InsufficientVerificationLevel
);
// Ensure the update isn't outdated
let twap_price = self.get_twap_unchecked(feed_id)?;
check!(
twap_price
.end_time
.saturating_add(maximum_age.try_into().unwrap())
>= clock.unix_timestamp,
GetPriceError::PriceTooOld
);
Ok(twap_price)
}
}
/// The time weighted average price & conf for a feed over the window [start_time, end_time].
/// This type is used to persist the calculated TWAP in TwapUpdate accounts on Solana.
Expand Down Expand Up @@ -249,7 +313,7 @@ pub mod tests {
use {
crate::{
error::GetPriceError,
price_update::{Price, PriceUpdateV2, VerificationLevel},
price_update::{Price, PriceUpdateV2, TwapPrice, TwapUpdate, VerificationLevel},
},
anchor_lang::Discriminator,
pythnet_sdk::messages::PriceFeedMessage,
Expand Down Expand Up @@ -486,4 +550,78 @@ pub mod tests {
Err(GetPriceError::MismatchedFeedId)
);
}

#[test]
fn test_get_twap_no_older_than() {
let expected_twap = TwapPrice {
feed_id: [0; 32],
start_time: 800,
end_time: 900,
price: 1,
conf: 2,
exponent: -3,
down_slots_ratio: 0,
};

let feed_id = [0; 32];
let mismatched_feed_id = [1; 32];
let mock_clock = Clock {
unix_timestamp: 1000,
..Default::default()
};

let twap_update_unverified = TwapUpdate {
write_authority: Pubkey::new_unique(),
verification_level: VerificationLevel::Partial { num_signatures: 0 },
twap: expected_twap,
posted_slot: 0,
};

let twap_update_fully_verified = TwapUpdate {
write_authority: Pubkey::new_unique(),
verification_level: VerificationLevel::Full,
twap: expected_twap,
posted_slot: 0,
};

// Test unchecked access
assert_eq!(
twap_update_unverified.get_twap_unchecked(&feed_id),
Ok(expected_twap)
);
assert_eq!(
twap_update_fully_verified.get_twap_unchecked(&feed_id),
Ok(expected_twap)
);

// Test with age and verification checks
assert_eq!(
twap_update_unverified.get_twap_no_older_than(&mock_clock, 100, &feed_id),
Err(GetPriceError::InsufficientVerificationLevel)
);
assert_eq!(
twap_update_fully_verified.get_twap_no_older_than(&mock_clock, 100, &feed_id),
Ok(expected_twap)
);

// Test with reduced maximum age
assert_eq!(
twap_update_fully_verified.get_twap_no_older_than(&mock_clock, 10, &feed_id),
Err(GetPriceError::PriceTooOld)
);

// Test with mismatched feed id
assert_eq!(
twap_update_fully_verified.get_twap_unchecked(&mismatched_feed_id),
Err(GetPriceError::MismatchedFeedId)
);
assert_eq!(
twap_update_fully_verified.get_twap_no_older_than(
&mock_clock,
100,
&mismatched_feed_id
),
Err(GetPriceError::MismatchedFeedId)
);
}
}
Loading