@@ -83,6 +83,70 @@ impl TwapUpdate {
83
83
+ 8
84
84
// posted_slot
85
85
) ;
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
+ }
86
150
}
87
151
/// The time weighted average price & conf for a feed over the window [start_time, end_time].
88
152
/// This type is used to persist the calculated TWAP in TwapUpdate accounts on Solana.
@@ -249,7 +313,7 @@ pub mod tests {
249
313
use {
250
314
crate :: {
251
315
error:: GetPriceError ,
252
- price_update:: { Price , PriceUpdateV2 , VerificationLevel } ,
316
+ price_update:: { Price , PriceUpdateV2 , TwapPrice , TwapUpdate , VerificationLevel } ,
253
317
} ,
254
318
anchor_lang:: Discriminator ,
255
319
pythnet_sdk:: messages:: PriceFeedMessage ,
@@ -486,4 +550,78 @@ pub mod tests {
486
550
Err ( GetPriceError :: MismatchedFeedId )
487
551
) ;
488
552
}
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
+ }
489
627
}
0 commit comments