Skip to content

Commit b451050

Browse files
feat(solana-receiver-sdk): add get_twap_no_older_than to consume TwapUpdates (#2174)
* feat: impl post_twap_update and necessary types * refactor: move TwapPrice to receiver sdk * feat: add functions to consume twapupdate accounts * fix: merge conflict * feat: bump crate version
1 parent a08be17 commit b451050

File tree

3 files changed

+144
-4
lines changed

3 files changed

+144
-4
lines changed

target_chains/solana/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/pyth_solana_receiver_sdk/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pyth-solana-receiver-sdk"
3-
version = "0.3.2"
3+
version = "0.4.0"
44
description = "SDK for the Pyth Solana Receiver program"
55
authors = ["Pyth Data Association"]
66
repository = "https://github.com/pyth-network/pyth-crosschain"
@@ -15,5 +15,7 @@ name = "pyth_solana_receiver_sdk"
1515
[dependencies]
1616
anchor-lang = ">=0.28.0"
1717
hex = ">=0.4.3"
18-
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", version = "2.1.0", features = ["solana-program"]}
18+
pythnet-sdk = { path = "../../../pythnet/pythnet_sdk", version = "2.1.0", features = [
19+
"solana-program",
20+
] }
1921
solana-program = ">=1.16.0, <2.0.0"

target_chains/solana/pyth_solana_receiver_sdk/src/price_update.rs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,70 @@ impl TwapUpdate {
8383
+ 8
8484
// posted_slot
8585
);
86+
87+
/// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId`.
88+
///
89+
/// # Warning
90+
/// This function does not check :
91+
/// - How recent the price is
92+
/// - Whether the price update has been verified
93+
///
94+
/// It is therefore unsafe to use this function without any extra checks,
95+
/// as it allows for the possibility of using unverified or outdated price updates.
96+
pub fn get_twap_unchecked(
97+
&self,
98+
feed_id: &FeedId,
99+
) -> std::result::Result<TwapPrice, GetPriceError> {
100+
check!(
101+
self.twap.feed_id == *feed_id,
102+
GetPriceError::MismatchedFeedId
103+
);
104+
Ok(self.twap)
105+
}
106+
107+
/// Get a `TwapPrice` from a `TwapUpdate` account for a given `FeedId` no older than `maximum_age` with `Full` verification.
108+
///
109+
/// # Example
110+
/// ```
111+
/// use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, TwapUpdate};
112+
/// use anchor_lang::prelude::*;
113+
///
114+
/// const MAXIMUM_AGE : u64 = 30;
115+
/// const FEED_ID: &str = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d"; // SOL/USD
116+
///
117+
/// #[derive(Accounts)]
118+
/// pub struct ReadTwapAccount<'info> {
119+
/// pub twap_update: Account<'info, TwapUpdate>,
120+
/// }
121+
///
122+
/// pub fn read_twap_account(ctx : Context<ReadTwapAccount>) -> Result<()> {
123+
/// let twap_update = &ctx.accounts.twap_update;
124+
/// let twap = twap_update.get_twap_no_older_than(&Clock::get()?, MAXIMUM_AGE, &get_feed_id_from_hex(FEED_ID)?)?;
125+
/// Ok(())
126+
/// }
127+
/// ```
128+
pub fn get_twap_no_older_than(
129+
&self,
130+
clock: &Clock,
131+
maximum_age: u64,
132+
feed_id: &FeedId,
133+
) -> std::result::Result<TwapPrice, GetPriceError> {
134+
// Ensure the update is fully verified
135+
check!(
136+
self.verification_level.eq(&VerificationLevel::Full),
137+
GetPriceError::InsufficientVerificationLevel
138+
);
139+
// Ensure the update isn't outdated
140+
let twap_price = self.get_twap_unchecked(feed_id)?;
141+
check!(
142+
twap_price
143+
.end_time
144+
.saturating_add(maximum_age.try_into().unwrap())
145+
>= clock.unix_timestamp,
146+
GetPriceError::PriceTooOld
147+
);
148+
Ok(twap_price)
149+
}
86150
}
87151
/// The time weighted average price & conf for a feed over the window [start_time, end_time].
88152
/// This type is used to persist the calculated TWAP in TwapUpdate accounts on Solana.
@@ -249,7 +313,7 @@ pub mod tests {
249313
use {
250314
crate::{
251315
error::GetPriceError,
252-
price_update::{Price, PriceUpdateV2, VerificationLevel},
316+
price_update::{Price, PriceUpdateV2, TwapPrice, TwapUpdate, VerificationLevel},
253317
},
254318
anchor_lang::Discriminator,
255319
pythnet_sdk::messages::PriceFeedMessage,
@@ -486,4 +550,78 @@ pub mod tests {
486550
Err(GetPriceError::MismatchedFeedId)
487551
);
488552
}
553+
554+
#[test]
555+
fn test_get_twap_no_older_than() {
556+
let expected_twap = TwapPrice {
557+
feed_id: [0; 32],
558+
start_time: 800,
559+
end_time: 900,
560+
price: 1,
561+
conf: 2,
562+
exponent: -3,
563+
down_slots_ratio: 0,
564+
};
565+
566+
let feed_id = [0; 32];
567+
let mismatched_feed_id = [1; 32];
568+
let mock_clock = Clock {
569+
unix_timestamp: 1000,
570+
..Default::default()
571+
};
572+
573+
let twap_update_unverified = TwapUpdate {
574+
write_authority: Pubkey::new_unique(),
575+
verification_level: VerificationLevel::Partial { num_signatures: 0 },
576+
twap: expected_twap,
577+
posted_slot: 0,
578+
};
579+
580+
let twap_update_fully_verified = TwapUpdate {
581+
write_authority: Pubkey::new_unique(),
582+
verification_level: VerificationLevel::Full,
583+
twap: expected_twap,
584+
posted_slot: 0,
585+
};
586+
587+
// Test unchecked access
588+
assert_eq!(
589+
twap_update_unverified.get_twap_unchecked(&feed_id),
590+
Ok(expected_twap)
591+
);
592+
assert_eq!(
593+
twap_update_fully_verified.get_twap_unchecked(&feed_id),
594+
Ok(expected_twap)
595+
);
596+
597+
// Test with age and verification checks
598+
assert_eq!(
599+
twap_update_unverified.get_twap_no_older_than(&mock_clock, 100, &feed_id),
600+
Err(GetPriceError::InsufficientVerificationLevel)
601+
);
602+
assert_eq!(
603+
twap_update_fully_verified.get_twap_no_older_than(&mock_clock, 100, &feed_id),
604+
Ok(expected_twap)
605+
);
606+
607+
// Test with reduced maximum age
608+
assert_eq!(
609+
twap_update_fully_verified.get_twap_no_older_than(&mock_clock, 10, &feed_id),
610+
Err(GetPriceError::PriceTooOld)
611+
);
612+
613+
// Test with mismatched feed id
614+
assert_eq!(
615+
twap_update_fully_verified.get_twap_unchecked(&mismatched_feed_id),
616+
Err(GetPriceError::MismatchedFeedId)
617+
);
618+
assert_eq!(
619+
twap_update_fully_verified.get_twap_no_older_than(
620+
&mock_clock,
621+
100,
622+
&mismatched_feed_id
623+
),
624+
Err(GetPriceError::MismatchedFeedId)
625+
);
626+
}
489627
}

0 commit comments

Comments
 (0)