1pub use pythnet_sdk::messages::{
2 FeedId,
3 PriceFeedMessage,
4};
5use {
6 crate::{
7 check,
8 error::GetPriceError,
9 },
10 anchor_lang::prelude::{
11 borsh::BorshSchema,
12 *,
13 },
14 solana_program::pubkey::Pubkey,
15};
16
17
18#[derive(AnchorSerialize, AnchorDeserialize, Copy, Clone, PartialEq, BorshSchema, Debug)]
30pub enum VerificationLevel {
31 Partial { num_signatures: u8 },
32 Full,
33}
34
35impl VerificationLevel {
36 pub fn gte(&self, other: VerificationLevel) -> bool {
39 match self {
40 VerificationLevel::Full => true,
41 VerificationLevel::Partial { num_signatures } => match other {
42 VerificationLevel::Full => false,
43 VerificationLevel::Partial {
44 num_signatures: other_num_signatures,
45 } => *num_signatures >= other_num_signatures,
46 },
47 }
48 }
49}
50
51#[account]
58#[derive(BorshSchema)]
59pub struct PriceUpdateV2 {
60 pub write_authority: Pubkey,
61 pub verification_level: VerificationLevel,
62 pub price_message: PriceFeedMessage,
63 pub posted_slot: u64,
64}
65
66impl PriceUpdateV2 {
67 pub const LEN: usize = 8 + 32 + 2 + 32 + 8 + 8 + 4 + 8 + 8 + 8 + 8 + 8;
68}
69
70#[derive(PartialEq, Debug, Clone, Copy)]
73pub struct Price {
74 pub price: i64,
75 pub conf: u64,
76 pub exponent: i32,
77 pub publish_time: i64,
78}
79
80impl PriceUpdateV2 {
81 pub fn get_price_unchecked(
90 &self,
91 feed_id: &FeedId,
92 ) -> std::result::Result<Price, GetPriceError> {
93 check!(
94 self.price_message.feed_id == *feed_id,
95 GetPriceError::MismatchedFeedId
96 );
97 Ok(Price {
98 price: self.price_message.price,
99 conf: self.price_message.conf,
100 exponent: self.price_message.exponent,
101 publish_time: self.price_message.publish_time,
102 })
103 }
104
105 pub fn get_price_no_older_than_with_custom_verification_level(
132 &self,
133 clock: &Clock,
134 maximum_age: u64,
135 feed_id: &FeedId,
136 verification_level: VerificationLevel,
137 ) -> std::result::Result<Price, GetPriceError> {
138 check!(
139 self.verification_level.gte(verification_level),
140 GetPriceError::InsufficientVerificationLevel
141 );
142 let price = self.get_price_unchecked(feed_id)?;
143 check!(
144 price
145 .publish_time
146 .saturating_add(maximum_age.try_into().unwrap())
147 >= clock.unix_timestamp,
148 GetPriceError::PriceTooOld
149 );
150 Ok(price)
151 }
152
153 pub fn get_price_no_older_than(
176 &self,
177 clock: &Clock,
178 maximum_age: u64,
179 feed_id: &FeedId,
180 ) -> std::result::Result<Price, GetPriceError> {
181 self.get_price_no_older_than_with_custom_verification_level(
182 clock,
183 maximum_age,
184 feed_id,
185 VerificationLevel::Full,
186 )
187 }
188}
189
190pub fn get_feed_id_from_hex(input: &str) -> std::result::Result<FeedId, GetPriceError> {
202 let mut feed_id: FeedId = [0; 32];
203 match input.len() {
204 66 => feed_id.copy_from_slice(
205 &hex::decode(&input[2..]).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
206 ),
207 64 => feed_id.copy_from_slice(
208 &hex::decode(input).map_err(|_| GetPriceError::FeedIdNonHexCharacter)?,
209 ),
210 _ => return Err(GetPriceError::FeedIdMustBe32Bytes),
211 }
212 Ok(feed_id)
213}
214
215#[cfg(test)]
216pub mod tests {
217 use {
218 crate::{
219 error::GetPriceError,
220 price_update::{
221 Price,
222 PriceUpdateV2,
223 VerificationLevel,
224 },
225 },
226 anchor_lang::Discriminator,
227 pythnet_sdk::messages::PriceFeedMessage,
228 solana_program::{
229 borsh0_10,
230 clock::Clock,
231 pubkey::Pubkey,
232 },
233 };
234
235 #[test]
236 fn check_size() {
237 assert!(
238 PriceUpdateV2::discriminator().len() + borsh0_10::get_packed_len::<PriceUpdateV2>()
239 == PriceUpdateV2::LEN
240 );
241 }
242
243 #[test]
244 fn gte() {
245 assert!(VerificationLevel::Full.gte(VerificationLevel::Full));
246 assert!(VerificationLevel::Full.gte(VerificationLevel::Partial {
247 num_signatures: 255,
248 }));
249 assert!(VerificationLevel::Partial { num_signatures: 8 }
250 .gte(VerificationLevel::Partial { num_signatures: 8 }));
251 assert!(!VerificationLevel::Partial { num_signatures: 8 }.gte(VerificationLevel::Full));
252 assert!(!VerificationLevel::Partial { num_signatures: 8 }
253 .gte(VerificationLevel::Partial { num_signatures: 9 }));
254 }
255
256 #[test]
257 fn get_feed_id_from_hex() {
258 let feed_id = "0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d";
259 let expected_feed_id = [
260 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, 42, 13, 47,
261 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109,
262 ];
263 assert_eq!(super::get_feed_id_from_hex(feed_id), Ok(expected_feed_id));
264 assert_eq!(
265 super::get_feed_id_from_hex(&feed_id[2..]),
266 Ok(expected_feed_id)
267 );
268
269 assert_eq!(
270 super::get_feed_id_from_hex(&feed_id[..64]),
271 Err(GetPriceError::FeedIdNonHexCharacter)
272 );
273
274 assert_eq!(
275 super::get_feed_id_from_hex(
276 "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b5"
277 ),
278 Err(GetPriceError::FeedIdMustBe32Bytes)
279 );
280 }
281
282 #[test]
283 fn get_price() {
284 let expected_price = Price {
285 price: 1,
286 conf: 2,
287 exponent: 3,
288 publish_time: 900,
289 };
290
291 let feed_id = [0; 32];
292 let mismatched_feed_id = [1; 32];
293 let mock_clock = Clock {
294 unix_timestamp: 1000,
295 ..Default::default()
296 };
297
298 let price_update_unverified = PriceUpdateV2 {
299 write_authority: Pubkey::new_unique(),
300 verification_level: VerificationLevel::Partial { num_signatures: 0 },
301 price_message: PriceFeedMessage {
302 feed_id,
303 ema_conf: 0,
304 ema_price: 0,
305 price: 1,
306 conf: 2,
307 exponent: 3,
308 prev_publish_time: 899,
309 publish_time: 900,
310 },
311 posted_slot: 0,
312 };
313
314 let price_update_partially_verified = PriceUpdateV2 {
315 write_authority: Pubkey::new_unique(),
316 verification_level: VerificationLevel::Partial { num_signatures: 5 },
317 price_message: PriceFeedMessage {
318 feed_id,
319 ema_conf: 0,
320 ema_price: 0,
321 price: 1,
322 conf: 2,
323 exponent: 3,
324 prev_publish_time: 899,
325 publish_time: 900,
326 },
327 posted_slot: 0,
328 };
329
330 let price_update_fully_verified = PriceUpdateV2 {
331 write_authority: Pubkey::new_unique(),
332 verification_level: VerificationLevel::Full,
333 price_message: PriceFeedMessage {
334 feed_id,
335 ema_conf: 0,
336 ema_price: 0,
337 price: 1,
338 conf: 2,
339 exponent: 3,
340 prev_publish_time: 899,
341 publish_time: 900,
342 },
343 posted_slot: 0,
344 };
345
346
347 assert_eq!(
348 price_update_unverified.get_price_unchecked(&feed_id),
349 Ok(expected_price)
350 );
351 assert_eq!(
352 price_update_partially_verified.get_price_unchecked(&feed_id),
353 Ok(expected_price)
354 );
355 assert_eq!(
356 price_update_fully_verified.get_price_unchecked(&feed_id),
357 Ok(expected_price)
358 );
359
360 assert_eq!(
361 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
362 &mock_clock,
363 100,
364 &feed_id,
365 VerificationLevel::Partial { num_signatures: 5 }
366 ),
367 Err(GetPriceError::InsufficientVerificationLevel)
368 );
369 assert_eq!(
370 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
371 &mock_clock,
372 100,
373 &feed_id,
374 VerificationLevel::Partial { num_signatures: 5 }
375 ),
376 Ok(expected_price)
377 );
378 assert_eq!(
379 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
380 &mock_clock,
381 100,
382 &feed_id,
383 VerificationLevel::Partial { num_signatures: 5 }
384 ),
385 Ok(expected_price)
386 );
387
388 assert_eq!(
389 price_update_unverified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
390 Err(GetPriceError::InsufficientVerificationLevel)
391 );
392 assert_eq!(
393 price_update_partially_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
394 Err(GetPriceError::InsufficientVerificationLevel)
395 );
396 assert_eq!(
397 price_update_fully_verified.get_price_no_older_than(&mock_clock, 100, &feed_id,),
398 Ok(expected_price)
399 );
400
401 assert_eq!(
403 price_update_unverified.get_price_no_older_than_with_custom_verification_level(
404 &mock_clock,
405 10,
406 &feed_id,
407 VerificationLevel::Partial { num_signatures: 5 }
408 ),
409 Err(GetPriceError::InsufficientVerificationLevel)
410 );
411 assert_eq!(
412 price_update_partially_verified.get_price_no_older_than_with_custom_verification_level(
413 &mock_clock,
414 10,
415 &feed_id,
416 VerificationLevel::Partial { num_signatures: 5 }
417 ),
418 Err(GetPriceError::PriceTooOld)
419 );
420 assert_eq!(
421 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
422 &mock_clock,
423 10,
424 &feed_id,
425 VerificationLevel::Partial { num_signatures: 5 }
426 ),
427 Err(GetPriceError::PriceTooOld)
428 );
429
430 assert_eq!(
431 price_update_unverified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
432 Err(GetPriceError::InsufficientVerificationLevel)
433 );
434 assert_eq!(
435 price_update_partially_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
436 Err(GetPriceError::InsufficientVerificationLevel)
437 );
438 assert_eq!(
439 price_update_fully_verified.get_price_no_older_than(&mock_clock, 10, &feed_id,),
440 Err(GetPriceError::PriceTooOld)
441 );
442
443 assert_eq!(
445 price_update_fully_verified.get_price_unchecked(&mismatched_feed_id),
446 Err(GetPriceError::MismatchedFeedId)
447 );
448 assert_eq!(
449 price_update_fully_verified.get_price_no_older_than_with_custom_verification_level(
450 &mock_clock,
451 100,
452 &mismatched_feed_id,
453 VerificationLevel::Partial { num_signatures: 5 }
454 ),
455 Err(GetPriceError::MismatchedFeedId)
456 );
457 assert_eq!(
458 price_update_fully_verified.get_price_no_older_than(
459 &mock_clock,
460 100,
461 &mismatched_feed_id,
462 ),
463 Err(GetPriceError::MismatchedFeedId)
464 );
465 }
466}