1pub use pythnet_sdk::messages::{FeedId, PriceFeedMessage};
2use {
3 crate::{check, error::GetPriceError},
4 anchor_lang::prelude::{borsh::BorshSchema, *},
5 solana_program::pubkey::Pubkey,
6};
7
8#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
20pub enum VerificationLevel {
21 Partial {
22 #[allow(unused)]
23 num_signatures: u8,
24 },
25 Full,
26}
27
28impl VerificationLevel {
29 pub fn gte(&self, other: VerificationLevel) -> bool {
32 match self {
33 VerificationLevel::Full => true,
34 VerificationLevel::Partial { num_signatures } => match other {
35 VerificationLevel::Full => false,
36 VerificationLevel::Partial {
37 num_signatures: other_num_signatures,
38 } => *num_signatures >= other_num_signatures,
39 },
40 }
41 }
42}
43
44#[account]
51#[derive(BorshSchema)]
52pub struct PriceUpdateV2 {
53 pub write_authority: Pubkey,
54 pub verification_level: VerificationLevel,
55 pub price_message: PriceFeedMessage,
56 pub posted_slot: u64,
57}
58
59impl PriceUpdateV2 {
60 pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8;
61}
62#[account]
71#[derive(BorshSchema)]
72pub struct TwapUpdate {
73 pub write_authority: Pubkey,
74 pub twap: TwapPrice,
75}
76
77impl TwapUpdate {
78 pub const LEN: usize = (
79 8 + 32 + (32 + 8 + 8 + 8 + 8 + 4 + 4)
82 );
84
85 pub fn get_twap_unchecked(
96 &self,
97 feed_id: &FeedId,
98 ) -> std::result::Result<TwapPrice, GetPriceError> {
99 check!(
100 self.twap.feed_id == *feed_id,
101 GetPriceError::MismatchedFeedId
102 );
103 Ok(self.twap)
104 }
105 pub fn get_twap_no_older_than(
133 &self,
134 clock: &Clock,
135 maximum_age: u64,
136 window_seconds: u64,
137 feed_id: &FeedId,
138 ) -> std::result::Result<TwapPrice, GetPriceError> {
139 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
149 let actual_window = twap_price.end_time.saturating_sub(twap_price.start_time);
151 check!(
152 actual_window == i64::try_from(window_seconds).unwrap(),
153 GetPriceError::InvalidWindowSize
154 );
155
156 Ok(twap_price)
157 }
158}
159#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
162pub struct TwapPrice {
163 pub feed_id: FeedId,
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::Discriminator,
327 pythnet_sdk::messages::PriceFeedMessage,
328 solana_program::{borsh0_10, clock::Clock, pubkey::Pubkey},
329 };
330
331 #[test]
332 fn check_size() {
333 assert!(
334 PriceUpdateV2::discriminator().len() + borsh0_10::get_packed_len::<PriceUpdateV2>()
335 == PriceUpdateV2::LEN
336 );
337 }
338
339 #[test]
340 fn gte() {
341 assert!(VerificationLevel::Full.gte(VerificationLevel::Full));
342 assert!(VerificationLevel::Full.gte(VerificationLevel::Partial {
343 num_signatures: 255,
344 }));
345 assert!(VerificationLevel::Partial { num_signatures: 8 }
346 .gte(VerificationLevel::Partial { num_signatures: 8 }));
347 assert!(!VerificationLevel::Partial { num_signatures: 8 }.gte(VerificationLevel::Full));
348 assert!(!VerificationLevel::Partial { num_signatures: 8 }
349 .gte(VerificationLevel::Partial { num_signatures: 9 }));
350 }
351
352 #[test]
353 fn get_feed_id_from_hex() {
354 let feed_id = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
355 let expected_feed_id = [
356 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42, 13, 47,
357 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
358 ];
359 assert_eq!(super::get_feed_id_from_hex(feed_id), Ok(expected_feed_id));
360 assert_eq!(
361 super::get_feed_id_from_hex(&feed_id[2..]),
362 Ok(expected_feed_id)
363 );
364
365 assert_eq!(
366 super::get_feed_id_from_hex(&feed_id[..64]),
367 Err(GetPriceError::FeedIdNonHexCharacter)
368 );
369
370 assert_eq!(
371 super::get_feed_id_from_hex(
372 "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b5"
373 ),
374 Err(GetPriceError::FeedIdMustBe32Bytes)
375 );
376 }
377
378 #[test]
379 fn get_price() {
380 let expected_price = Price {
381 price: 1,
382 conf: 2,
383 exponent: 3,
384 publish_time: 900,
385 };
386
387 let feed_id = [0; 32];
388 let mismatched_feed_id = [1; 32];
389 let mock_clock = Clock {
390 unix_timestamp: 1000,
391 ..Default::default()
392 };
393
394 let price_update_unverified = PriceUpdateV2 {
395 write_authority: Pubkey::new_unique(),
396 verification_level: VerificationLevel::Partial { num_signatures: 0 },
397 price_message: PriceFeedMessage {
398 feed_id,
399 ema_conf: 0,
400 ema_price: 0,
401 price: 1,
402 conf: 2,
403 exponent: 3,
404 prev_publish_time: 899,
405 publish_time: 900,
406 },
407 posted_slot: 0,
408 };
409
410 let price_update_partially_verified = PriceUpdateV2 {
411 write_authority: Pubkey::new_unique(),
412 verification_level: VerificationLevel::Partial { num_signatures: 5 },
413 price_message: PriceFeedMessage {
414 feed_id,
415 ema_conf: 0,
416 ema_price: 0,
417 price: 1,
418 conf: 2,
419 exponent: 3,
420 prev_publish_time: 899,
421 publish_time: 900,
422 },
423 posted_slot: 0,
424 };
425
426 let price_update_fully_verified = PriceUpdateV2 {
427 write_authority: Pubkey::new_unique(),
428 verification_level: VerificationLevel::Full,
429 price_message: PriceFeedMessage {
430 feed_id,
431 ema_conf: 0,
432 ema_price: 0,
433 price: 1,
434 conf: 2,
435 exponent: 3,
436 prev_publish_time: 899,
437 publish_time: 900,
438 },
439 posted_slot: 0,
440 };
441
442 assert_eq!(
443 price_update_unverified.get_price_unchecked(&feed_id),
444 Ok(expected_price)
445 );
446 assert_eq!(
447 price_update_partially_verified.get_price_unchecked(&feed_id),
448 Ok(expected_price)
449 );
450 assert_eq!(
451 price_update_fully_verified.get_price_unchecked(&feed_id),
452 Ok(expected_price)
453 );
454
455 assert_eq!(
456 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
457 &mock_clock,
458 100,
459 &feed_id,
460 VerificationLevel::Partial { num_signatures: 5 }
461 ),
462 Err(GetPriceError::InsufficientVerificationLevel)
463 );
464 assert_eq!(
465 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
466 &mock_clock,
467 100,
468 &feed_id,
469 VerificationLevel::Partial { num_signatures: 5 }
470 ),
471 Ok(expected_price)
472 );
473 assert_eq!(
474 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
475 &mock_clock,
476 100,
477 &feed_id,
478 VerificationLevel::Partial { num_signatures: 5 }
479 ),
480 Ok(expected_price)
481 );
482
483 assert_eq!(
484 price_update_unverified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
485 Err(GetPriceError::InsufficientVerificationLevel)
486 );
487 assert_eq!(
488 price_update_partially_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
489 Err(GetPriceError::InsufficientVerificationLevel)
490 );
491 assert_eq!(
492 price_update_fully_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
493 Ok(expected_price)
494 );
495
496 assert_eq!(
498 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
499 &mock_clock,
500 10,
501 &feed_id,
502 VerificationLevel::Partial { num_signatures: 5 }
503 ),
504 Err(GetPriceError::InsufficientVerificationLevel)
505 );
506 assert_eq!(
507 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
508 &mock_clock,
509 10,
510 &feed_id,
511 VerificationLevel::Partial { num_signatures: 5 }
512 ),
513 Err(GetPriceError::PriceTooOld)
514 );
515 assert_eq!(
516 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
517 &mock_clock,
518 10,
519 &feed_id,
520 VerificationLevel::Partial { num_signatures: 5 }
521 ),
522 Err(GetPriceError::PriceTooOld)
523 );
524
525 assert_eq!(
526 price_update_unverified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
527 Err(GetPriceError::InsufficientVerificationLevel)
528 );
529 assert_eq!(
530 price_update_partially_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
531 Err(GetPriceError::InsufficientVerificationLevel)
532 );
533 assert_eq!(
534 price_update_fully_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
535 Err(GetPriceError::PriceTooOld)
536 );
537
538 assert_eq!(
540 price_update_fully_verified.get_price_unchecked(&mismatched_feed_id),
541 Err(GetPriceError::MismatchedFeedId)
542 );
543 assert_eq!(
544 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
545 &mock_clock,
546 100,
547 &mismatched_feed_id,
548 VerificationLevel::Partial { num_signatures: 5 }
549 ),
550 Err(GetPriceError::MismatchedFeedId)
551 );
552 assert_eq!(
553 price_update_fully_verified.get_price_no_older_than(
554 &mock_clock,
555 100,
556 &mismatched_feed_id,
557 ),
558 Err(GetPriceError::MismatchedFeedId)
559 );
560 }
561 #[test]
562 fn test_get_twap_no_older_than() {
563 let expected_twap = TwapPrice {
564 feed_id: [0; 32],
565 start_time: 800,
566 end_time: 900, price: 1,
568 conf: 2,
569 exponent: -3,
570 down_slots_ratio: 0,
571 };
572
573 let feed_id = [0; 32];
574 let mismatched_feed_id = [1; 32];
575 let mock_clock = Clock {
576 unix_timestamp: 1000,
577 ..Default::default()
578 };
579
580 let update = TwapUpdate {
581 write_authority: Pubkey::new_unique(),
582 twap: expected_twap,
583 };
584
585 assert_eq!(update.get_twap_unchecked(&feed_id), Ok(expected_twap));
587
588 assert_eq!(
590 update.get_twap_no_older_than(&mock_clock, 100, 100, &feed_id),
591 Ok(expected_twap)
592 );
593
594 assert_eq!(
596 update.get_twap_no_older_than(&mock_clock, 100, 101, &feed_id),
597 Err(GetPriceError::InvalidWindowSize)
598 );
599
600 assert_eq!(
602 update.get_twap_no_older_than(&mock_clock, 100, 99, &feed_id),
603 Err(GetPriceError::InvalidWindowSize)
604 );
605
606 assert_eq!(
608 update.get_twap_no_older_than(&mock_clock, 10, 100, &feed_id),
609 Err(GetPriceError::PriceTooOld)
610 );
611
612 assert_eq!(
614 update.get_twap_unchecked(&mismatched_feed_id),
615 Err(GetPriceError::MismatchedFeedId)
616 );
617 assert_eq!(
618 update.get_twap_no_older_than(&mock_clock, 100, 100, &mismatched_feed_id),
619 Err(GetPriceError::MismatchedFeedId)
620 );
621 }
622}