1pub use pythnet_sdk::messages::{FeedId, PriceFeedMessage};
2use {
3 crate::{check, error::GetPriceError},
4 anchor_lang::prelude::{borsh::BorshSchema, *},
5};
6
7#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
19pub enum VerificationLevel {
20 Partial {
21 #[allow(unused)]
22 num_signatures: u8,
23 },
24 Full,
25}
26
27impl VerificationLevel {
28 pub fn gte(&self, other: VerificationLevel) -> bool {
31 match self {
32 VerificationLevel::Full => true,
33 VerificationLevel::Partial { num_signatures } => match other {
34 VerificationLevel::Full => false,
35 VerificationLevel::Partial {
36 num_signatures: other_num_signatures,
37 } => *num_signatures >= other_num_signatures,
38 },
39 }
40 }
41}
42
43#[account]
50#[derive(BorshSchema)]
51pub struct PriceUpdateV2 {
52 pub write_authority: Pubkey,
53 pub verification_level: VerificationLevel,
54 pub price_message: PriceFeedMessage,
55 pub posted_slot: u64,
56}
57
58impl PriceUpdateV2 {
59 pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8;
60}
61#[account]
70#[derive(BorshSchema)]
71pub struct TwapUpdate {
72 pub write_authority: Pubkey,
73 pub twap: TwapPrice,
74}
75
76impl TwapUpdate {
77 pub const LEN: usize = (
78 8 + 32 + (32 + 8 + 8 + 8 + 8 + 4 + 4)
81 );
83
84 pub fn get_twap_unchecked(
95 &self,
96 feed_id: &FeedId,
97 ) -> std::result::Result<TwapPrice, GetPriceError> {
98 check!(
99 self.twap.feed_id == *feed_id,
100 GetPriceError::MismatchedFeedId
101 );
102 Ok(self.twap)
103 }
104 pub fn get_twap_no_older_than(
132 &self,
133 clock: &Clock,
134 maximum_age: u64,
135 window_seconds: u64,
136 feed_id: &FeedId,
137 ) -> std::result::Result<TwapPrice, GetPriceError> {
138 let twap_price = self.get_twap_unchecked(feed_id)?;
140 check!(
141 twap_price
142 .end_time
143 .saturating_add(maximum_age.try_into().unwrap())
144 >= clock.unix_timestamp,
145 GetPriceError::PriceTooOld
146 );
147
148 let actual_window = twap_price.end_time.saturating_sub(twap_price.start_time);
150 check!(
151 actual_window == i64::try_from(window_seconds).unwrap(),
152 GetPriceError::InvalidWindowSize
153 );
154
155 Ok(twap_price)
156 }
157}
158#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
161pub struct TwapPrice {
162 pub feed_id: [u8; 32],
164 pub start_time: i64,
165 pub end_time: i64,
166 pub price: i64,
167 pub conf: u64,
168 pub exponent: i32,
169 pub down_slots_ratio: u32,
172}
173
174#[derive(PartialEq, Debug, Clone, Copy)]
177pub struct Price {
178 pub price: i64,
179 pub conf: u64,
180 pub exponent: i32,
181 pub publish_time: i64,
182}
183
184impl PriceUpdateV2 {
185 pub fn get_price_unchecked(
194 &self,
195 feed_id: &FeedId,
196 ) -> std::result::Result<Price, GetPriceError> {
197 check!(
198 self.price_message.feed_id == *feed_id,
199 GetPriceError::MismatchedFeedId
200 );
201 Ok(Price {
202 price: self.price_message.price,
203 conf: self.price_message.conf,
204 exponent: self.price_message.exponent,
205 publish_time: self.price_message.publish_time,
206 })
207 }
208
209 pub fn get_price_no_older_than_with_custom_verification_level(
236 &self,
237 clock: &Clock,
238 maximum_age: u64,
239 feed_id: &FeedId,
240 verification_level: VerificationLevel,
241 ) -> std::result::Result<Price, GetPriceError> {
242 check!(
243 self.verification_level.gte(verification_level),
244 GetPriceError::InsufficientVerificationLevel
245 );
246 let price = self.get_price_unchecked(feed_id)?;
247 check!(
248 price
249 .publish_time
250 .saturating_add(maximum_age.try_into().unwrap())
251 >= clock.unix_timestamp,
252 GetPriceError::PriceTooOld
253 );
254 Ok(price)
255 }
256
257 pub fn get_price_no_older_than(
280 &self,
281 clock: &Clock,
282 maximum_age: u64,
283 feed_id: &FeedId,
284 ) -> std::result::Result<Price, GetPriceError> {
285 self.get_price_no_older_than_with_custom_verification_level(
286 clock,
287 maximum_age,
288 feed_id,
289 VerificationLevel::Full,
290 )
291 }
292}
293
294pub fn get_feed_id_from_hex(input: &str) -> std::result::Result<FeedId, GetPriceError> {
306 let mut feed_id: FeedId = [0; 32];
307 match input.len() {
308 66 => feed_id.copy_from_slice(
309 &hex::decode(&input[2..]).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
310 ),
311 64 => feed_id.copy_from_slice(
312 &hex::decode(input).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
313 ),
314 _ => return Err(GetPriceError::FeedIdMustBe32Bytes),
315 }
316 Ok(feed_id)
317}
318
319#[cfg(test)]
320pub mod tests {
321 use {
322 crate::{
323 error::GetPriceError,
324 price_update::{Price, PriceUpdateV2, TwapPrice, TwapUpdate, VerificationLevel},
325 },
326 anchor_lang::{prelude::*, solana_program::borsh0_10},
327 pythnet_sdk::messages::PriceFeedMessage,
328 };
329
330 #[test]
331 fn check_size() {
332 #[allow(deprecated)]
334 let len = PriceUpdateV2::DISCRIMINATOR.len() + borsh0_10::get_packed_len::<PriceUpdateV2>();
335 assert_eq!(len, PriceUpdateV2::LEN);
336 }
337
338 #[test]
339 fn gte() {
340 assert!(VerificationLevel::Full.gte(VerificationLevel::Full));
341 assert!(VerificationLevel::Full.gte(VerificationLevel::Partial {
342 num_signatures: 255,
343 }));
344 assert!(VerificationLevel::Partial { num_signatures: 8 }
345 .gte(VerificationLevel::Partial { num_signatures: 8 }));
346 assert!(!VerificationLevel::Partial { num_signatures: 8 }.gte(VerificationLevel::Full));
347 assert!(!VerificationLevel::Partial { num_signatures: 8 }
348 .gte(VerificationLevel::Partial { num_signatures: 9 }));
349 }
350
351 #[test]
352 fn get_feed_id_from_hex() {
353 let feed_id = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
354 let expected_feed_id = [
355 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42, 13, 47,
356 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
357 ];
358 assert_eq!(super::get_feed_id_from_hex(feed_id), Ok(expected_feed_id));
359 assert_eq!(
360 super::get_feed_id_from_hex(&feed_id[2..]),
361 Ok(expected_feed_id)
362 );
363
364 assert_eq!(
365 super::get_feed_id_from_hex(&feed_id[..64]),
366 Err(GetPriceError::FeedIdNonHexCharacter)
367 );
368
369 assert_eq!(
370 super::get_feed_id_from_hex(
371 "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b5"
372 ),
373 Err(GetPriceError::FeedIdMustBe32Bytes)
374 );
375 }
376
377 #[test]
378 fn get_price() {
379 let expected_price = Price {
380 price: 1,
381 conf: 2,
382 exponent: 3,
383 publish_time: 900,
384 };
385
386 let feed_id = [0; 32];
387 let mismatched_feed_id = [1; 32];
388 let mock_clock = Clock {
389 unix_timestamp: 1000,
390 ..Default::default()
391 };
392
393 let price_update_unverified = PriceUpdateV2 {
394 write_authority: Pubkey::new_unique(),
395 verification_level: VerificationLevel::Partial { num_signatures: 0 },
396 price_message: PriceFeedMessage {
397 feed_id,
398 ema_conf: 0,
399 ema_price: 0,
400 price: 1,
401 conf: 2,
402 exponent: 3,
403 prev_publish_time: 899,
404 publish_time: 900,
405 },
406 posted_slot: 0,
407 };
408
409 let price_update_partially_verified = PriceUpdateV2 {
410 write_authority: Pubkey::new_unique(),
411 verification_level: VerificationLevel::Partial { num_signatures: 5 },
412 price_message: PriceFeedMessage {
413 feed_id,
414 ema_conf: 0,
415 ema_price: 0,
416 price: 1,
417 conf: 2,
418 exponent: 3,
419 prev_publish_time: 899,
420 publish_time: 900,
421 },
422 posted_slot: 0,
423 };
424
425 let price_update_fully_verified = PriceUpdateV2 {
426 write_authority: Pubkey::new_unique(),
427 verification_level: VerificationLevel::Full,
428 price_message: PriceFeedMessage {
429 feed_id,
430 ema_conf: 0,
431 ema_price: 0,
432 price: 1,
433 conf: 2,
434 exponent: 3,
435 prev_publish_time: 899,
436 publish_time: 900,
437 },
438 posted_slot: 0,
439 };
440
441 assert_eq!(
442 price_update_unverified.get_price_unchecked(&feed_id),
443 Ok(expected_price)
444 );
445 assert_eq!(
446 price_update_partially_verified.get_price_unchecked(&feed_id),
447 Ok(expected_price)
448 );
449 assert_eq!(
450 price_update_fully_verified.get_price_unchecked(&feed_id),
451 Ok(expected_price)
452 );
453
454 assert_eq!(
455 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
456 &mock_clock,
457 100,
458 &feed_id,
459 VerificationLevel::Partial { num_signatures: 5 }
460 ),
461 Err(GetPriceError::InsufficientVerificationLevel)
462 );
463 assert_eq!(
464 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
465 &mock_clock,
466 100,
467 &feed_id,
468 VerificationLevel::Partial { num_signatures: 5 }
469 ),
470 Ok(expected_price)
471 );
472 assert_eq!(
473 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
474 &mock_clock,
475 100,
476 &feed_id,
477 VerificationLevel::Partial { num_signatures: 5 }
478 ),
479 Ok(expected_price)
480 );
481
482 assert_eq!(
483 price_update_unverified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
484 Err(GetPriceError::InsufficientVerificationLevel)
485 );
486 assert_eq!(
487 price_update_partially_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
488 Err(GetPriceError::InsufficientVerificationLevel)
489 );
490 assert_eq!(
491 price_update_fully_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
492 Ok(expected_price)
493 );
494
495 assert_eq!(
497 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
498 &mock_clock,
499 10,
500 &feed_id,
501 VerificationLevel::Partial { num_signatures: 5 }
502 ),
503 Err(GetPriceError::InsufficientVerificationLevel)
504 );
505 assert_eq!(
506 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
507 &mock_clock,
508 10,
509 &feed_id,
510 VerificationLevel::Partial { num_signatures: 5 }
511 ),
512 Err(GetPriceError::PriceTooOld)
513 );
514 assert_eq!(
515 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
516 &mock_clock,
517 10,
518 &feed_id,
519 VerificationLevel::Partial { num_signatures: 5 }
520 ),
521 Err(GetPriceError::PriceTooOld)
522 );
523
524 assert_eq!(
525 price_update_unverified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
526 Err(GetPriceError::InsufficientVerificationLevel)
527 );
528 assert_eq!(
529 price_update_partially_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
530 Err(GetPriceError::InsufficientVerificationLevel)
531 );
532 assert_eq!(
533 price_update_fully_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
534 Err(GetPriceError::PriceTooOld)
535 );
536
537 assert_eq!(
539 price_update_fully_verified.get_price_unchecked(&mismatched_feed_id),
540 Err(GetPriceError::MismatchedFeedId)
541 );
542 assert_eq!(
543 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
544 &mock_clock,
545 100,
546 &mismatched_feed_id,
547 VerificationLevel::Partial { num_signatures: 5 }
548 ),
549 Err(GetPriceError::MismatchedFeedId)
550 );
551 assert_eq!(
552 price_update_fully_verified.get_price_no_older_than(
553 &mock_clock,
554 100,
555 &mismatched_feed_id,
556 ),
557 Err(GetPriceError::MismatchedFeedId)
558 );
559 }
560 #[test]
561 fn test_get_twap_no_older_than() {
562 let expected_twap = TwapPrice {
563 feed_id: [0; 32],
564 start_time: 800,
565 end_time: 900, price: 1,
567 conf: 2,
568 exponent: -3,
569 down_slots_ratio: 0,
570 };
571
572 let feed_id = [0; 32];
573 let mismatched_feed_id = [1; 32];
574 let mock_clock = Clock {
575 unix_timestamp: 1000,
576 ..Default::default()
577 };
578
579 let update = TwapUpdate {
580 write_authority: Pubkey::new_unique(),
581 twap: expected_twap,
582 };
583
584 assert_eq!(update.get_twap_unchecked(&feed_id), Ok(expected_twap));
586
587 assert_eq!(
589 update.get_twap_no_older_than(&mock_clock, 100, 100, &feed_id),
590 Ok(expected_twap)
591 );
592
593 assert_eq!(
595 update.get_twap_no_older_than(&mock_clock, 100, 101, &feed_id),
596 Err(GetPriceError::InvalidWindowSize)
597 );
598
599 assert_eq!(
601 update.get_twap_no_older_than(&mock_clock, 100, 99, &feed_id),
602 Err(GetPriceError::InvalidWindowSize)
603 );
604
605 assert_eq!(
607 update.get_twap_no_older_than(&mock_clock, 10, 100, &feed_id),
608 Err(GetPriceError::PriceTooOld)
609 );
610
611 assert_eq!(
613 update.get_twap_unchecked(&mismatched_feed_id),
614 Err(GetPriceError::MismatchedFeedId)
615 );
616 assert_eq!(
617 update.get_twap_no_older_than(&mock_clock, 100, 100, &mismatched_feed_id),
618 Err(GetPriceError::MismatchedFeedId)
619 );
620 }
621}