|
| 1 | +use log::warn; |
1 | 2 | #[cfg(test)]
|
2 | 3 | use mock_instant::{SystemTime, UNIX_EPOCH};
|
3 | 4 | use pythnet_sdk::messages::TwapMessage;
|
@@ -60,6 +61,7 @@ pub type UnixTimestamp = i64;
|
60 | 61 | #[derive(Clone, PartialEq, Eq, Debug)]
|
61 | 62 | pub enum RequestTime {
|
62 | 63 | Latest,
|
| 64 | + LatestTimeEarliestSlot, |
63 | 65 | FirstAfter(UnixTimestamp),
|
64 | 66 | AtSlot(Slot),
|
65 | 67 | }
|
@@ -242,7 +244,7 @@ where
|
242 | 244 | async fn get_twaps_with_update_data(
|
243 | 245 | &self,
|
244 | 246 | price_ids: &[PriceIdentifier],
|
245 |
| - start_time: RequestTime, |
| 247 | + window_seconds: u64, |
246 | 248 | end_time: RequestTime,
|
247 | 249 | ) -> Result<TwapsWithUpdateData>;
|
248 | 250 | }
|
@@ -410,16 +412,11 @@ where
|
410 | 412 | async fn get_twaps_with_update_data(
|
411 | 413 | &self,
|
412 | 414 | price_ids: &[PriceIdentifier],
|
413 |
| - start_time: RequestTime, |
| 415 | + window_seconds: u64, |
414 | 416 | end_time: RequestTime,
|
415 | 417 | ) -> Result<TwapsWithUpdateData> {
|
416 |
| - match get_verified_twaps_with_update_data( |
417 |
| - self, |
418 |
| - price_ids, |
419 |
| - start_time.clone(), |
420 |
| - end_time.clone(), |
421 |
| - ) |
422 |
| - .await |
| 418 | + match get_verified_twaps_with_update_data(self, price_ids, window_seconds, end_time.clone()) |
| 419 | + .await |
423 | 420 | {
|
424 | 421 | Ok(twaps_with_update_data) => Ok(twaps_with_update_data),
|
425 | 422 | Err(e) => {
|
@@ -637,33 +634,68 @@ where
|
637 | 634 | async fn get_verified_twaps_with_update_data<S>(
|
638 | 635 | state: &S,
|
639 | 636 | price_ids: &[PriceIdentifier],
|
640 |
| - start_time: RequestTime, |
| 637 | + window_seconds: u64, |
641 | 638 | end_time: RequestTime,
|
642 | 639 | ) -> Result<TwapsWithUpdateData>
|
643 | 640 | where
|
644 | 641 | S: Cache,
|
645 | 642 | {
|
646 |
| - // Get all start messages for all price IDs |
647 |
| - let start_messages = state |
| 643 | + // Get all end messages for all price IDs |
| 644 | + let end_messages = state |
648 | 645 | .fetch_message_states(
|
649 | 646 | price_ids.iter().map(|id| id.to_bytes()).collect(),
|
650 |
| - start_time.clone(), |
| 647 | + end_time.clone(), |
651 | 648 | MessageStateFilter::Only(MessageType::TwapMessage),
|
652 | 649 | )
|
653 | 650 | .await?;
|
654 | 651 |
|
655 |
| - // Get all end messages for all price IDs |
656 |
| - let end_messages = state |
| 652 | + // Calculate start_time based on the publish time of the end messages |
| 653 | + // to guarantee that the start and end messages are window_seconds apart |
| 654 | + let start_timestamp = if end_messages.is_empty() { |
| 655 | + // If there are no end messages, we can't calculate a TWAP |
| 656 | + tracing::warn!( |
| 657 | + price_ids = ?price_ids, |
| 658 | + time = ?end_time, |
| 659 | + "Could not find TWAP messages" |
| 660 | + ); |
| 661 | + return Err(anyhow!( |
| 662 | + "Update data not found for the specified timestamps" |
| 663 | + )); |
| 664 | + } else { |
| 665 | + // Use the publish time from the first end message |
| 666 | + end_messages[0].message.publish_time() - window_seconds as i64 |
| 667 | + }; |
| 668 | + let start_time = RequestTime::FirstAfter(start_timestamp); |
| 669 | + |
| 670 | + // Get all start messages for all price IDs |
| 671 | + let start_messages = state |
657 | 672 | .fetch_message_states(
|
658 | 673 | price_ids.iter().map(|id| id.to_bytes()).collect(),
|
659 |
| - end_time.clone(), |
| 674 | + start_time.clone(), |
660 | 675 | MessageStateFilter::Only(MessageType::TwapMessage),
|
661 | 676 | )
|
662 | 677 | .await?;
|
663 | 678 |
|
| 679 | + if start_messages.is_empty() { |
| 680 | + tracing::warn!( |
| 681 | + price_ids = ?price_ids, |
| 682 | + time = ?start_time, |
| 683 | + "Could not find TWAP messages" |
| 684 | + ); |
| 685 | + return Err(anyhow!( |
| 686 | + "Update data not found for the specified timestamps" |
| 687 | + )); |
| 688 | + } |
| 689 | + |
664 | 690 | // Verify we have matching start and end messages.
|
665 | 691 | // The cache should throw an error earlier, but checking just in case.
|
666 | 692 | if start_messages.len() != end_messages.len() {
|
| 693 | + tracing::warn!( |
| 694 | + price_ids = ?price_ids, |
| 695 | + start_message_length = ?price_ids, |
| 696 | + end_message_length = ?start_time, |
| 697 | + "Start and end messages length mismatch" |
| 698 | + ); |
667 | 699 | return Err(anyhow!(
|
668 | 700 | "Update data not found for the specified timestamps"
|
669 | 701 | ));
|
@@ -695,6 +727,11 @@ where
|
695 | 727 | });
|
696 | 728 | }
|
697 | 729 | Err(e) => {
|
| 730 | + tracing::error!( |
| 731 | + feed_id = ?start_twap.feed_id, |
| 732 | + error = %e, |
| 733 | + "Failed to calculate TWAP for price feed" |
| 734 | + ); |
698 | 735 | return Err(anyhow!(
|
699 | 736 | "Failed to calculate TWAP for price feed {:?}: {}",
|
700 | 737 | start_twap.feed_id,
|
@@ -1295,7 +1332,7 @@ mod test {
|
1295 | 1332 | PriceIdentifier::new(feed_id_1),
|
1296 | 1333 | PriceIdentifier::new(feed_id_2),
|
1297 | 1334 | ],
|
1298 |
| - RequestTime::FirstAfter(100), // Start time |
| 1335 | + 100, // window seconds |
1299 | 1336 | RequestTime::FirstAfter(200), // End time
|
1300 | 1337 | )
|
1301 | 1338 | .await
|
@@ -1329,6 +1366,97 @@ mod test {
|
1329 | 1366 | // update_data should have 2 elements, one for the start block and one for the end block.
|
1330 | 1367 | assert_eq!(result.update_data.len(), 2);
|
1331 | 1368 | }
|
| 1369 | + |
| 1370 | + #[tokio::test] |
| 1371 | + /// Tests that the TWAP calculation correctly selects TWAP messages that are the first ones |
| 1372 | + /// for their timestamp (non-optional prices). This is important because if a message such that |
| 1373 | + /// `publish_time == prev_publish_time`is chosen, the TWAP calculation will fail due to the optionality check. |
| 1374 | + async fn test_get_verified_twaps_with_update_data_uses_non_optional_prices() { |
| 1375 | + let (state, _update_rx) = setup_state(10).await; |
| 1376 | + let feed_id = [1u8; 32]; |
| 1377 | + |
| 1378 | + // Store start TWAP message |
| 1379 | + store_multiple_concurrent_valid_updates( |
| 1380 | + state.clone(), |
| 1381 | + generate_update( |
| 1382 | + vec![create_basic_twap_message( |
| 1383 | + feed_id, 100, // cumulative_price |
| 1384 | + 0, // num_down_slots |
| 1385 | + 100, // publish_time |
| 1386 | + 99, // prev_publish_time |
| 1387 | + 1000, // publish_slot |
| 1388 | + )], |
| 1389 | + 1000, |
| 1390 | + 20, |
| 1391 | + ), |
| 1392 | + ) |
| 1393 | + .await; |
| 1394 | + |
| 1395 | + // Store end TWAP messages |
| 1396 | + |
| 1397 | + // This first message has the latest publish_time and earliest slot, |
| 1398 | + // so it should be chosen as the end_message to calculate TWAP with. |
| 1399 | + store_multiple_concurrent_valid_updates( |
| 1400 | + state.clone(), |
| 1401 | + generate_update( |
| 1402 | + vec![create_basic_twap_message( |
| 1403 | + feed_id, 300, // cumulative_price |
| 1404 | + 50, // num_down_slots |
| 1405 | + 200, // publish_time |
| 1406 | + 180, // prev_publish_time |
| 1407 | + 1100, // publish_slot |
| 1408 | + )], |
| 1409 | + 1100, |
| 1410 | + 21, |
| 1411 | + ), |
| 1412 | + ) |
| 1413 | + .await; |
| 1414 | + |
| 1415 | + // This second message has the same publish_time as the previous one and a later slot. |
| 1416 | + // It will fail the optionality check since publish_time == prev_publish_time. |
| 1417 | + // Thus, it should not be chosen to calculate TWAP with. |
| 1418 | + store_multiple_concurrent_valid_updates( |
| 1419 | + state.clone(), |
| 1420 | + generate_update( |
| 1421 | + vec![create_basic_twap_message( |
| 1422 | + feed_id, 900, // cumulative_price |
| 1423 | + 50, // num_down_slots |
| 1424 | + 200, // publish_time |
| 1425 | + 200, // prev_publish_time |
| 1426 | + 1101, // publish_slot |
| 1427 | + )], |
| 1428 | + 1101, |
| 1429 | + 22, |
| 1430 | + ), |
| 1431 | + ) |
| 1432 | + .await; |
| 1433 | + |
| 1434 | + // Get TWAPs over timestamp window 100 -> 200 |
| 1435 | + let result = get_verified_twaps_with_update_data( |
| 1436 | + &*state, |
| 1437 | + &[PriceIdentifier::new(feed_id)], |
| 1438 | + 100, // window seconds |
| 1439 | + RequestTime::LatestTimeEarliestSlot, // End time |
| 1440 | + ) |
| 1441 | + .await |
| 1442 | + .unwrap(); |
| 1443 | + |
| 1444 | + // Verify that the first end message was chosen to calculate the TWAP |
| 1445 | + // and that the calculation is accurate |
| 1446 | + assert_eq!(result.twaps.len(), 1); |
| 1447 | + let twap_1 = result |
| 1448 | + .twaps |
| 1449 | + .iter() |
| 1450 | + .find(|t| t.id == PriceIdentifier::new(feed_id)) |
| 1451 | + .unwrap(); |
| 1452 | + assert_eq!(twap_1.twap.price, 2); // (300-100)/(1100-1000) = 2 |
| 1453 | + assert_eq!(twap_1.down_slots_ratio, Decimal::from_f64(0.5).unwrap()); // (50-0)/(1100-1000) = 0.5 |
| 1454 | + assert_eq!(twap_1.start_timestamp, 100); |
| 1455 | + assert_eq!(twap_1.end_timestamp, 200); |
| 1456 | + |
| 1457 | + // update_data should have 2 elements, one for the start block and one for the end block. |
| 1458 | + assert_eq!(result.update_data.len(), 2); |
| 1459 | + } |
1332 | 1460 | #[tokio::test]
|
1333 | 1461 |
|
1334 | 1462 | async fn test_get_verified_twaps_with_missing_messages_throws_error() {
|
@@ -1385,7 +1513,7 @@ mod test {
|
1385 | 1513 | PriceIdentifier::new(feed_id_1),
|
1386 | 1514 | PriceIdentifier::new(feed_id_2),
|
1387 | 1515 | ],
|
1388 |
| - RequestTime::FirstAfter(100), |
| 1516 | + 100, |
1389 | 1517 | RequestTime::FirstAfter(200),
|
1390 | 1518 | )
|
1391 | 1519 | .await;
|
|
0 commit comments