diff --git a/target_chains/stylus/Cargo.lock b/target_chains/stylus/Cargo.lock index 5551eab31c..adde73d5a9 100644 --- a/target_chains/stylus/Cargo.lock +++ b/target_chains/stylus/Cargo.lock @@ -3286,6 +3286,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mock_instant" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" + [[package]] name = "motsu" version = "0.1.0" @@ -3982,6 +3988,7 @@ dependencies = [ "ethers", "eyre", "hex", + "mock_instant", "motsu 0.9.1", "pythnet-sdk", "serde", diff --git a/target_chains/stylus/contracts/pyth-receiver/Cargo.toml b/target_chains/stylus/contracts/pyth-receiver/Cargo.toml index f3d8a12ba8..8b4921655d 100644 --- a/target_chains/stylus/contracts/pyth-receiver/Cargo.toml +++ b/target_chains/stylus/contracts/pyth-receiver/Cargo.toml @@ -26,6 +26,7 @@ stylus-sdk = { version = "0.9.0", features = ["stylus-test"] } dotenv = "0.15.0" motsu = "0.9.0" wormhole-contract = { path = "../wormhole" } +mock_instant = "0.6.0" [features] default = ["mini-alloc"] diff --git a/target_chains/stylus/contracts/pyth-receiver/src/error.rs b/target_chains/stylus/contracts/pyth-receiver/src/error.rs index 5f0bb2ecf6..caeb2e4922 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/error.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/error.rs @@ -18,6 +18,8 @@ pub enum PythReceiverError { InvalidEmitterAddress, TooManyUpdates, PriceFeedNotFoundWithinRange, + NoFreshUpdate, + PriceFeedNotFound, } impl core::fmt::Debug for PythReceiverError { @@ -45,6 +47,8 @@ impl From for Vec { PythReceiverError::InvalidEmitterAddress => 14, PythReceiverError::TooManyUpdates => 15, PythReceiverError::PriceFeedNotFoundWithinRange => 16, + PythReceiverError::NoFreshUpdate => 17, + PythReceiverError::PriceFeedNotFound => 18, }] } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs index cc17c457c2..dbe7c799f9 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/integration_tests.rs @@ -1,23 +1,19 @@ #[cfg(test)] mod test { use crate::error::PythReceiverError; - use crate::test_data; + use crate::test_data::*; use crate::PythReceiver; - use alloy_primitives::{address, Address, I32, I64, U256, U64}; + use alloy_primitives::{Address, U256}; + use mock_instant::global::MockClock; use motsu::prelude::*; use pythnet_sdk::wire::v1::{AccumulatorUpdateData, Proof}; + use std::time::Duration; use wormhole_contract::WormholeContract; const TEST_PRICE_ID: [u8; 32] = [ 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, 0xe5, 0xdb, 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, 0x4a, 0x41, 0x5b, 0x43, ]; - const TEST_PUBLISH_TIME: u64 = 1751563000; - const TEST_PRICE: i64 = 10967241867779; - const TEST_CONF: u64 = 4971244966; - const TEST_EXPO: i32 = -8; - const TEST_EMA_PRICE: i64 = 10942391100000; - const TEST_EMA_CONF: u64 = 4398561400; const PYTHNET_CHAIN_ID: u16 = 26; const PYTHNET_EMITTER_ADDRESS: [u8; 32] = [ @@ -31,44 +27,29 @@ mod test { const GOVERNANCE_CONTRACT: U256 = U256::from_limbs([4, 0, 0, 0]); const SINGLE_UPDATE_FEE_IN_WEI: U256 = U256::from_limbs([100, 0, 0, 0]); + const TRANSACTION_FEE_IN_WEI: U256 = U256::from_limbs([32, 0, 0, 0]); #[cfg(test)] - fn current_guardians() -> Vec
{ - vec![ - address!("0x5893B5A76c3f739645648885bDCcC06cd70a3Cd3"), // Rockaway - address!("0xfF6CB952589BDE862c25Ef4392132fb9D4A42157"), // Staked - address!("0x114De8460193bdf3A2fCf81f86a09765F4762fD1"), // Figment - address!("0x107A0086b32d7A0977926A205131d8731D39cbEB"), // ChainodeTech - address!("0x8C82B2fd82FaeD2711d59AF0F2499D16e726f6b2"), // Inotel - address!("0x11b39756C042441BE6D8650b69b54EbE715E2343"), // HashKey Cloud - address!("0x54Ce5B4D348fb74B958e8966e2ec3dBd4958a7cd"), // ChainLayer - address!("0x15e7cAF07C4e3DC8e7C469f92C8Cd88FB8005a20"), // xLabs - address!("0x74a3bf913953D695260D88BC1aA25A4eeE363ef0"), // Forbole - address!("0x000aC0076727b35FBea2dAc28fEE5cCB0fEA768e"), // Staking Fund - address!("0xAF45Ced136b9D9e24903464AE889F5C8a723FC14"), // Moonlet Wallet - address!("0xf93124b7c738843CBB89E864c862c38cddCccF95"), // P2P Validator - address!("0xD2CC37A4dc036a8D232b48f62cDD4731412f4890"), // 01node - address!("0xDA798F6896A3331F64b48c12D1D57Fd9cbe70811"), // MCF - address!("0x71AA1BE1D36CaFE3867910F99C09e347899C19C3"), // Everstake - address!("0x8192b6E7387CCd768277c17DAb1b7a5027c0b3Cf"), // Chorus One - address!("0x178e21ad2E77AE06711549CFBB1f9c7a9d8096e8"), // Syncnode - address!("0x5E1487F35515d02A92753504a8D75471b9f49EdB"), // Triton - address!("0x6FbEBc898F403E4773E95feB15E80C9A99c8348d"), // Staking Facilities - ] - } - - #[cfg(test)] - fn mock_get_update_fee(update_data: Vec) -> Result { - let update_data_array: &[u8] = &update_data; - let accumulator_update = AccumulatorUpdateData::try_from_slice(&update_data_array) - .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; - match accumulator_update.proof { - Proof::WormholeMerkle { vaa: _, updates } => { - let num_updates = - u8::try_from(updates.len()).map_err(|_| PythReceiverError::TooManyUpdates)?; - Ok(U256::from(num_updates).saturating_mul(SINGLE_UPDATE_FEE_IN_WEI)) + fn mock_get_update_fee(update_data: Vec>) -> Result { + let mut total_num_updates: u64 = 0; + for data in &update_data { + let update_data_array: &[u8] = &data; + let accumulator_update = AccumulatorUpdateData::try_from_slice(&update_data_array) + .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; + match accumulator_update.proof { + Proof::WormholeMerkle { vaa: _, updates } => { + let num_updates = u64::try_from(updates.len()) + .map_err(|_| PythReceiverError::TooManyUpdates)?; + total_num_updates += num_updates; + } } } + Ok(get_total_fee(total_num_updates)) + } + + fn get_total_fee(total_num_updates: u64) -> U256 { + U256::from(total_num_updates).saturating_mul(SINGLE_UPDATE_FEE_IN_WEI) + + TRANSACTION_FEE_IN_WEI } #[cfg(test)] @@ -123,29 +104,21 @@ mod test { ) { pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); - - let update_data = test_data::good_update1(); + let update_data = ban_usd_update(); let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + alice.fund(update_fee); + let result = pyth_contract .sender_and_value(alice, update_fee) .update_price_feeds(update_data); assert!(result.is_ok()); - let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); + let price_result = pyth_contract + .sender(alice) + .get_price_unsafe(ban_usd_feed_id()); assert!(price_result.is_ok()); - assert_eq!( - price_result.unwrap(), - ( - U64::from(TEST_PUBLISH_TIME), - I32::from_le_bytes(TEST_EXPO.to_le_bytes()), - I64::from_le_bytes(TEST_PRICE.to_le_bytes()), - U64::from(TEST_CONF), - I64::from_le_bytes(TEST_EMA_PRICE.to_le_bytes()), - U64::from(TEST_EMA_CONF) - ) - ); + assert_eq!(price_result.unwrap(), ban_usd_results_get_price()); } #[motsu::test] @@ -158,7 +131,7 @@ mod test { alice.fund(U256::from(200)); - let update_data = test_data::good_update1(); + let update_data = ban_usd_update(); let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); let small_update_fee = update_fee / U256::from(2); @@ -170,43 +143,36 @@ mod test { } #[motsu::test] - fn test_get_price_after_multiple_updates_returns_recent_price( + fn test_get_price_after_multiple_different_updates_returns_recent_price( pyth_contract: Contract, wormhole_contract: Contract, alice: Address, ) { pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); - - let update_data1 = test_data::good_update1(); + let update_data1 = ban_usd_update(); let update_fee1 = mock_get_update_fee(update_data1.clone()).unwrap(); + + let update_data2 = btc_usd_update(); + let update_fee2 = mock_get_update_fee(update_data2.clone()).unwrap(); + + alice.fund(update_fee1 + update_fee2); + let result1 = pyth_contract .sender_and_value(alice, update_fee1) .update_price_feeds(update_data1); assert!(result1.is_ok()); - let update_data2 = test_data::good_update2(); - let update_fee2 = mock_get_update_fee(update_data2.clone()).unwrap(); - let result2 = pyth_contract .sender_and_value(alice, update_fee2) .update_price_feeds(update_data2); assert!(result2.is_ok()); - let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); + let price_result = pyth_contract + .sender(alice) + .get_price_unsafe(ban_usd_feed_id()); assert!(price_result.is_ok()); - assert_eq!( - price_result.unwrap(), - ( - U64::from(1751573860u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10985663592646i64.to_le_bytes()), - U64::from(4569386330u64), - I64::from_le_bytes(10977795800000i64.to_le_bytes()), - U64::from(3919318300u64) - ) - ); + assert_eq!(price_result.unwrap(), ban_usd_results_get_price()); } #[motsu::test] @@ -217,7 +183,9 @@ mod test { ) { pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - let price_result = pyth_contract.sender(alice).get_price_unsafe(TEST_PRICE_ID); + let price_result = pyth_contract + .sender(alice) + .get_price_unsafe(ban_usd_feed_id()); assert!(price_result.is_err()); assert_eq!( price_result.unwrap_err(), @@ -231,6 +199,7 @@ mod test { wormhole_contract: Contract, alice: Address, ) { + MockClock::set_time(Duration::from_secs(1761573860)); pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); let random_id: [u8; 32] = [ @@ -255,13 +224,14 @@ mod test { wormhole_contract: Contract, alice: Address, ) { + MockClock::set_time(Duration::from_secs(1761573860)); pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); - - let update_data = test_data::good_update2(); + let update_data = btc_usd_update(); let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + alice.fund(update_fee); + let result = pyth_contract .sender_and_value(alice, update_fee) .update_price_feeds(update_data); @@ -269,19 +239,9 @@ mod test { let price_result = pyth_contract .sender(alice) - .get_price_no_older_than(TEST_PRICE_ID, u64::MAX); + .get_price_no_older_than(btc_usd_feed_id(), u64::MAX); assert!(price_result.is_ok()); - assert_eq!( - price_result.unwrap(), - ( - U64::from(1751573860u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10985663592646i64.to_le_bytes()), - U64::from(4569386330u64), - I64::from_le_bytes(10977795800000i64.to_le_bytes()), - U64::from(3919318300u64) - ) - ); + assert_eq!(price_result.unwrap(), btc_usd_results_get_price()); } #[motsu::test] @@ -290,13 +250,14 @@ mod test { wormhole_contract: Contract, alice: Address, ) { + MockClock::set_time(Duration::from_secs(1761573860)); pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); - - let update_data = test_data::good_update2(); + let update_data = btc_usd_update(); let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + alice.fund(update_fee); + let result = pyth_contract .sender_and_value(alice, update_fee) .update_price_feeds(update_data); @@ -304,7 +265,8 @@ mod test { let price_result = pyth_contract .sender(alice) - .get_price_no_older_than(TEST_PRICE_ID, 1); + .get_price_no_older_than(btc_usd_feed_id(), 1); + println!("Price result: {:?}", price_result); assert!(price_result.is_err()); assert_eq!( price_result.unwrap_err(), @@ -320,63 +282,145 @@ mod test { ) { pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); - - let update_data = test_data::multiple_updates(); + let update_data = multiple_updates_diff_vaa(); let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + alice.fund(update_fee); + let result = pyth_contract .sender_and_value(alice, update_fee) .update_price_feeds(update_data); assert!(result.is_ok()); - let first_id: [u8; 32] = [ - 0xe6, 0x2d, 0xf6, 0xc8, 0xb4, 0xa8, 0x5f, 0xe1, 0xa6, 0x7d, 0xb4, 0x4d, 0xc1, 0x2d, - 0xe5, 0xdb, 0x33, 0x0f, 0x7a, 0xc6, 0x6b, 0x72, 0xdc, 0x65, 0x8a, 0xfe, 0xdf, 0x0f, - 0x4a, 0x41, 0x5b, 0x43, - ]; - let second_id: [u8; 32] = [ - 0xff, 0x61, 0x49, 0x1a, 0x93, 0x11, 0x12, 0xdd, 0xf1, 0xbd, 0x81, 0x47, 0xcd, 0x1b, - 0x64, 0x13, 0x75, 0xf7, 0x9f, 0x58, 0x25, 0x12, 0x6d, 0x66, 0x54, 0x80, 0x87, 0x46, - 0x34, 0xfd, 0x0a, 0xce, - ]; - - let first_price_result = pyth_contract.sender(alice).get_price_unsafe(first_id); + let first_price_result = pyth_contract + .sender(alice) + .get_price_unsafe(ban_usd_feed_id()); assert!(first_price_result.is_ok()); assert_eq!( first_price_result.unwrap(), - ( - U64::from(1751573123u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(10990356724259i64.to_le_bytes()), - U64::from(3891724259u64), - I64::from_le_bytes(10974970400000i64.to_le_bytes()), - U64::from(3918344000u64) - ) + multiple_updates_diff_vaa_results_get_price()[0] ); - let second_price_result = pyth_contract.sender(alice).get_price_unsafe(second_id); + let second_price_result = pyth_contract + .sender(alice) + .get_price_unsafe(btc_usd_feed_id()); assert!(second_price_result.is_ok()); assert_eq!( second_price_result.unwrap(), - ( - U64::from(1751573123u64), - I32::from_le_bytes((-8i32).to_le_bytes()), - I64::from_le_bytes(258906787480i64.to_le_bytes()), - U64::from(158498649u64), - I64::from_le_bytes(258597182000i64.to_le_bytes()), - U64::from(131285914u64) - ) + multiple_updates_diff_vaa_results_get_price()[1] ); } #[motsu::test] - fn test_multiple_updates_same_id_updates_latest( + fn test_price_feed_exists( pyth_contract: Contract, wormhole_contract: Contract, alice: Address, ) { pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); - alice.fund(U256::from(200)); + + assert!(!pyth_contract + .sender(alice) + .price_feed_exists(ban_usd_feed_id())); + + let update_data = ban_usd_update(); + let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + + alice.fund(update_fee); + + let result = pyth_contract + .sender_and_value(alice, update_fee) + .update_price_feeds(update_data); + assert!(result.is_ok()); + + assert!(pyth_contract + .sender(alice) + .price_feed_exists(ban_usd_feed_id())); + } + + #[motsu::test] + fn test_query_price_feed_doesnt_exist( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { + pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); + + let price_result = pyth_contract + .sender(alice) + .query_price_feed(ban_usd_feed_id()); + + assert!(price_result.is_err()); + assert_eq!( + price_result.unwrap_err(), + PythReceiverError::PriceFeedNotFound + ); + } + + #[motsu::test] + fn test_query_price_feed_after_one_feed_update( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { + pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); + + let update_data = ban_usd_update(); + let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + + alice.fund(update_fee); + + let result = pyth_contract + .sender_and_value(alice, update_fee) + .update_price_feeds(update_data); + + assert!(result.is_ok()); + + let price_result = pyth_contract + .sender(alice) + .query_price_feed(ban_usd_feed_id()); + + assert!(price_result.is_ok()); + assert_eq!(price_result.unwrap(), ban_usd_results_full()); + } + + #[motsu::test] + fn test_query_price_feed_after_multiple_updates( + pyth_contract: Contract, + wormhole_contract: Contract, + alice: Address, + ) { + pyth_wormhole_init(&pyth_contract, &wormhole_contract, &alice); + + let update_data = multiple_updates_diff_vaa(); + let update_fee = mock_get_update_fee(update_data.clone()).unwrap(); + + alice.fund(update_fee); + + let result = pyth_contract + .sender_and_value(alice, update_fee) + .update_price_feeds(update_data); + + assert!(result.is_ok()); + + let price_result1 = pyth_contract + .sender(alice) + .query_price_feed(ban_usd_feed_id()); + + assert!(price_result1.is_ok()); + assert_eq!( + price_result1.unwrap(), + multiple_updates_diff_vaa_results_full()[0] + ); + + let price_result2 = pyth_contract + .sender(alice) + .query_price_feed(btc_usd_feed_id()); + + assert!(price_result2.is_ok()); + assert_eq!( + price_result2.unwrap(), + multiple_updates_diff_vaa_results_full()[1] + ); } } diff --git a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs index 7e2dbb048b..707309bff6 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/lib.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/lib.rs @@ -12,7 +12,10 @@ mod structs; #[cfg(test)] mod test_data; -use alloc::{collections::BTreeMap, vec::Vec}; +#[cfg(test)] +use mock_instant::global::MockClock; + +use alloc::vec::Vec; use stylus_sdk::{ alloy_primitives::{Address, FixedBytes, I32, I64, U16, U256, U32, U64}, call::Call, @@ -36,7 +39,7 @@ use pythnet_sdk::{ }, }, }; -use structs::{DataSource, DataSourceStorage, PriceInfoReturn, PriceInfoStorage}; +use structs::{DataSource, DataSourceStorage, PriceFeedReturn, PriceFeedStorage, PriceReturn}; use wormhole_vaas::{Readable, Vaa, Writeable}; sol_interface! { @@ -60,7 +63,7 @@ pub struct PythReceiver { pub governance_data_source_emitter_address: StorageFixedBytes<32>, pub last_executed_governance_sequence: StorageUint<64, 1>, pub governance_data_source_index: StorageUint<32, 1>, - pub latest_price_info: StorageMap, PriceInfoStorage>, + pub latest_price_info: StorageMap, PriceFeedStorage>, pub transaction_fee_in_wei: StorageU256, } @@ -108,16 +111,23 @@ impl PythReceiver { } } - pub fn get_price_unsafe(&self, id: [u8; 32]) -> Result { + pub fn price_feed_exists(&self, id: [u8; 32]) -> bool { + let id_fb = FixedBytes::<32>::from(id); + let price_info = self.latest_price_info.get(id_fb); + return price_info.publish_time.get() != U64::ZERO; + } + + pub fn query_price_feed(&self, id: [u8; 32]) -> Result { let id_fb = FixedBytes::<32>::from(id); let price_info = self.latest_price_info.get(id_fb); if price_info.publish_time.get() == U64::ZERO { - return Err(PythReceiverError::PriceUnavailable); + return Err(PythReceiverError::PriceFeedNotFound); } Ok(( + id_fb, price_info.publish_time.get(), price_info.expo.get(), price_info.price.get(), @@ -127,19 +137,36 @@ impl PythReceiver { )) } + pub fn get_price_unsafe(&self, id: [u8; 32]) -> Result { + let id_fb = FixedBytes::<32>::from(id); + + let price_info = self.latest_price_info.get(id_fb); + + if price_info.publish_time.get() == U64::ZERO { + return Err(PythReceiverError::PriceUnavailable); + } + + Ok(( + price_info.price.get(), + price_info.conf.get(), + price_info.expo.get(), + price_info.publish_time.get(), + )) + } + pub fn get_price_no_older_than( &self, id: [u8; 32], age: u64, - ) -> Result { + ) -> Result { let price_info = self.get_price_unsafe(id)?; - if !self.is_no_older_than(price_info.0, age) { + if !self.is_no_older_than(price_info.3, age) { return Err(PythReceiverError::NewPriceUnavailable); } Ok(price_info) } - pub fn get_ema_price_unsafe(&self, id: [u8; 32]) -> Result { + pub fn get_ema_price_unsafe(&self, id: [u8; 32]) -> Result { let id_fb = FixedBytes::<32>::from(id); let price_info = self.latest_price_info.get(id_fb); @@ -148,12 +175,10 @@ impl PythReceiver { } Ok(( - price_info.publish_time.get(), - price_info.expo.get(), - price_info.ema_price.get(), - price_info.ema_conf.get(), price_info.ema_price.get(), price_info.ema_conf.get(), + price_info.expo.get(), + price_info.publish_time.get(), )) } @@ -161,27 +186,59 @@ impl PythReceiver { &self, id: [u8; 32], age: u64, - ) -> Result { + ) -> Result { let price_info = self.get_ema_price_unsafe(id)?; - if !self.is_no_older_than(price_info.0, age) { + if !self.is_no_older_than(price_info.3, age) { return Err(PythReceiverError::NewPriceUnavailable); } Ok(price_info) } #[payable] - pub fn update_price_feeds(&mut self, update_data: Vec) -> Result<(), PythReceiverError> { - self.update_price_feeds_internal(update_data, Vec::new(), 0, 0, false)?; + pub fn update_price_feeds( + &mut self, + update_data: Vec>, + ) -> Result<(), PythReceiverError> { + for data in &update_data { + self.update_price_feeds_internal(data.clone(), Vec::new(), 0, 0, false)?; + } + + let total_fee = self.get_update_fee(update_data)?; + + let value = self.vm().msg_value(); + + if value < total_fee { + return Err(PythReceiverError::InsufficientFee); + } Ok(()) } pub fn update_price_feeds_if_necessary( &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _publish_times: Vec, - ) { - // dummy implementation + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + publish_times: Vec, + ) -> Result<(), PythReceiverError> { + if (price_ids.len() != publish_times.len()) + || (price_ids.is_empty() && publish_times.is_empty()) + { + return Err(PythReceiverError::InvalidUpdateData); + } + + for i in 0..price_ids.len() { + if self.latest_price_info_publish_time(price_ids[i]) < publish_times[i] { + self.update_price_feeds(update_data.clone())?; + return Ok(()); + } + } + + return Err(PythReceiverError::NoFreshUpdate); + } + + fn latest_price_info_publish_time(&self, price_id: [u8; 32]) -> u64 { + let price_id_fb: FixedBytes<32> = FixedBytes::from(price_id); + let recent_price_info = self.latest_price_info.get(price_id_fb); + recent_price_info.publish_time.get().to::() } fn update_price_feeds_internal( @@ -191,44 +248,56 @@ impl PythReceiver { min_publish_time: u64, max_publish_time: u64, _unique: bool, - ) -> Result, PythReceiverError> { - let price_pairs = self.parse_price_feed_updates_internal( + ) -> Result, PythReceiverError> { + let price_feeds = self.parse_price_feed_updates_internal( update_data, min_publish_time, max_publish_time, false, // check_uniqueness )?; - for (price_id, price_return) in price_pairs.clone() { - let price_id_fb: FixedBytes<32> = FixedBytes::from(price_id); + for price_return in &price_feeds { + let price_id_fb: FixedBytes<32> = FixedBytes::from(price_return.0); let mut recent_price_info = self.latest_price_info.setter(price_id_fb); - if recent_price_info.publish_time.get() < price_return.0 + if recent_price_info.publish_time.get() < price_return.1 || recent_price_info.price.get() == I64::ZERO { - recent_price_info.publish_time.set(price_return.0); - recent_price_info.expo.set(price_return.1); - recent_price_info.price.set(price_return.2); - recent_price_info.conf.set(price_return.3); - recent_price_info.ema_price.set(price_return.4); - recent_price_info.ema_conf.set(price_return.5); + recent_price_info + .price_id + .set(FixedBytes::from(price_return.0)); + recent_price_info.publish_time.set(price_return.1); + recent_price_info.expo.set(price_return.2); + recent_price_info.price.set(price_return.3); + recent_price_info.conf.set(price_return.4); + recent_price_info.ema_price.set(price_return.5); + recent_price_info.ema_conf.set(price_return.6); } } - Ok(price_pairs) + Ok(price_feeds) } - fn get_update_fee(&self, update_data: Vec) -> Result { - let update_data_array: &[u8] = &update_data; - let accumulator_update = AccumulatorUpdateData::try_from_slice(&update_data_array) - .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; - match accumulator_update.proof { - Proof::WormholeMerkle { vaa: _, updates } => { - let num_updates = - u8::try_from(updates.len()).map_err(|_| PythReceiverError::TooManyUpdates)?; - Ok(U256::from(num_updates).saturating_mul(self.single_update_fee_in_wei.get())) + fn get_update_fee(&self, update_data: Vec>) -> Result { + let mut total_num_updates: u64 = 0; + for data in &update_data { + let update_data_array: &[u8] = &data; + let accumulator_update = AccumulatorUpdateData::try_from_slice(&update_data_array) + .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; + match accumulator_update.proof { + Proof::WormholeMerkle { vaa: _, updates } => { + let num_updates = u64::try_from(updates.len()) + .map_err(|_| PythReceiverError::TooManyUpdates)?; + total_num_updates += num_updates; + } } } + Ok(self.get_total_fee(total_num_updates)) + } + + fn get_total_fee(&self, total_num_updates: u64) -> U256 { + U256::from(total_num_updates).saturating_mul(self.single_update_fee_in_wei.get()) + + self.transaction_fee_in_wei.get() } pub fn get_twap_update_fee(&self, _update_data: Vec>) -> U256 { @@ -241,9 +310,9 @@ impl PythReceiver { price_ids: Vec<[u8; 32]>, min_publish_time: u64, max_publish_time: u64, - ) -> Result, PythReceiverError> { + ) -> Result, PythReceiverError> { let price_feeds = self.parse_price_feed_updates_with_config( - update_data, + vec![update_data], price_ids, min_publish_time, max_publish_time, @@ -256,43 +325,46 @@ impl PythReceiver { pub fn parse_price_feed_updates_with_config( &mut self, - update_data: Vec, + update_data: Vec>, price_ids: Vec<[u8; 32]>, min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, check_update_data_is_minimal: bool, store_updates_if_fresh: bool, - ) -> Result, PythReceiverError> { - let price_pairs; - if store_updates_if_fresh { - price_pairs = self.update_price_feeds_internal( - update_data, - price_ids.clone(), - min_allowed_publish_time, - max_allowed_publish_time, - check_uniqueness, - )?; - } else { - price_pairs = self.parse_price_feed_updates_internal( - update_data, - min_allowed_publish_time, - max_allowed_publish_time, - check_uniqueness, - )?; + ) -> Result, PythReceiverError> { + let mut all_parsed_price_feeds = Vec::new(); + for data in &update_data { + if store_updates_if_fresh { + all_parsed_price_feeds.extend(self.update_price_feeds_internal( + data.clone(), + price_ids.clone(), + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + )?); + } else { + all_parsed_price_feeds.extend(self.parse_price_feed_updates_internal( + data.clone(), + min_allowed_publish_time, + max_allowed_publish_time, + check_uniqueness, + )?); + } } - if check_update_data_is_minimal && price_ids.len() != price_pairs.len() { + if check_update_data_is_minimal && all_parsed_price_feeds.len() != price_ids.len() { return Err(PythReceiverError::InvalidUpdateData); } - let price_map: BTreeMap<[u8; 32], PriceInfoReturn> = price_pairs.into_iter().collect(); - - let mut result: Vec = Vec::with_capacity(price_ids.len()); + let mut result: Vec = Vec::with_capacity(price_ids.len()); for price_id in price_ids { - if let Some(price_info) = price_map.get(&price_id) { - result.push(*price_info); + if let Some(price_info) = all_parsed_price_feeds + .iter() + .find(|feed| feed.0 == price_id) + { + result.push(price_info.clone()); } else { return Err(PythReceiverError::PriceFeedNotFoundWithinRange); } @@ -307,7 +379,7 @@ impl PythReceiver { min_allowed_publish_time: u64, max_allowed_publish_time: u64, check_uniqueness: bool, - ) -> Result, PythReceiverError> { + ) -> Result, PythReceiverError> { let update_data_array: &[u8] = &update_data; // Check the first 4 bytes of the update_data_array for the magic header if update_data_array.len() < 4 { @@ -324,7 +396,7 @@ impl PythReceiver { let accumulator_update = AccumulatorUpdateData::try_from_slice(&update_data_array) .map_err(|_| PythReceiverError::InvalidAccumulatorMessage)?; - let mut price_feeds: BTreeMap<[u8; 32], PriceInfoReturn> = BTreeMap::new(); + let mut price_feeds = Vec::new(); match accumulator_update.proof { Proof::WormholeMerkle { vaa, updates } => { @@ -355,14 +427,6 @@ impl PythReceiver { let root_digest: MerkleRoot = parse_wormhole_proof(vaa_obj)?; - let total_fee = self.get_update_fee(update_data)?; - - let value = self.vm().msg_value(); - - if value < total_fee { - return Err(PythReceiverError::InsufficientFee); - } - for update in updates { let message_vec = Vec::from(update.message); let proof: MerklePath = update.proof; @@ -377,23 +441,31 @@ impl PythReceiver { match msg { Message::PriceFeedMessage(price_feed_message) => { let publish_time = price_feed_message.publish_time; - - if (min_allowed_publish_time > 0 && publish_time < min_allowed_publish_time as i64) || - (max_allowed_publish_time > 0 && publish_time > max_allowed_publish_time as i64) { + + if (min_allowed_publish_time > 0 + && publish_time < min_allowed_publish_time as i64) + || (max_allowed_publish_time > 0 + && publish_time > max_allowed_publish_time as i64) + { return Err(PythReceiverError::PriceFeedNotFoundWithinRange); } - + + let price_id_fb = FixedBytes::<32>::from(price_feed_message.feed_id); + if check_uniqueness { - let price_id_fb = FixedBytes::<32>::from(price_feed_message.feed_id); let prev_price_info = self.latest_price_info.get(price_id_fb); - let prev_publish_time = prev_price_info.publish_time.get().to::(); - - if prev_publish_time > 0 && min_allowed_publish_time <= prev_publish_time { + let prev_publish_time = + prev_price_info.publish_time.get().to::(); + + if prev_publish_time > 0 + && min_allowed_publish_time <= prev_publish_time + { return Err(PythReceiverError::PriceFeedNotFoundWithinRange); } } - + let price_info_return = ( + price_id_fb, U64::from(publish_time), I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()), I64::from_be_bytes(price_feed_message.price.to_be_bytes()), @@ -402,7 +474,7 @@ impl PythReceiver { U64::from(price_feed_message.ema_conf), ); - price_feeds.insert(price_feed_message.feed_id, price_info_return); + price_feeds.push(price_info_return); } _ => { return Err(PythReceiverError::InvalidAccumulatorMessageType); @@ -412,25 +484,34 @@ impl PythReceiver { } }; - Ok(price_feeds.into_iter().collect()) + Ok(price_feeds) } pub fn parse_twap_price_feed_updates( &mut self, _update_data: Vec>, _price_ids: Vec<[u8; 32]>, - ) -> Vec { + ) -> Vec { Vec::new() } pub fn parse_price_feed_updates_unique( &mut self, - _update_data: Vec>, - _price_ids: Vec<[u8; 32]>, - _min_publish_time: u64, - _max_publish_time: u64, - ) -> Vec { - Vec::new() + update_data: Vec>, + price_ids: Vec<[u8; 32]>, + min_publish_time: u64, + max_publish_time: u64, + ) -> Result, PythReceiverError> { + let price_feeds = self.parse_price_feed_updates_with_config( + update_data, + price_ids, + min_publish_time, + max_publish_time, + true, + false, + false, + ); + price_feeds } fn is_no_older_than(&self, publish_time: U64, max_age: u64) -> bool { @@ -444,7 +525,7 @@ impl PythReceiver { fn get_current_timestamp(&self) -> u64 { #[cfg(test)] { - 1761573860u64 + MockClock::time().as_secs() } #[cfg(not(test))] { diff --git a/target_chains/stylus/contracts/pyth-receiver/src/structs.rs b/target_chains/stylus/contracts/pyth-receiver/src/structs.rs index 722eb7312a..a6bfdaa448 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/structs.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/structs.rs @@ -1,36 +1,15 @@ -use alloc::{boxed::Box, format, vec::Vec}; -use pythnet_sdk::wire::to_vec; -use serde::Serialize; +use alloc::vec::Vec; use stylus_sdk::alloy_primitives::{keccak256, FixedBytes, B256, I32, I64, U16, U256, U64}; use stylus_sdk::{ prelude::*, storage::{StorageFixedBytes, StorageI32, StorageI64, StorageKey, StorageU16, StorageU64}, }; -#[derive(Serialize)] -struct SerializableDataSource { - chain_id: u16, - #[serde(with = "pythnet_sdk::wire::array")] - emitter_address: [u8; 32], -} - -fn serialize_data_source_to_bytes( - chain_id: u16, - emitter_address: &[u8; 32], -) -> Result<[u8; 34], Box> { - let data_source = SerializableDataSource { - chain_id, - emitter_address: *emitter_address, - }; - - let bytes = to_vec::<_, byteorder::BE>(&data_source)?; - if bytes.len() != 34 { - return Err(format!("Expected 34 bytes, got {}", bytes.len()).into()); - } - +fn serialize_data_source_to_bytes(chain_id: u16, emitter_address: &[u8; 32]) -> [u8; 34] { let mut result = [0u8; 34]; - result.copy_from_slice(&bytes); - Ok(result) + result[0..2].copy_from_slice(&chain_id.to_be_bytes()); + result[2..].copy_from_slice(emitter_address); + result } #[derive(Debug)] @@ -46,49 +25,19 @@ pub struct DataSource { pub emitter_address: FixedBytes<32>, } -// impl StorageKey for DataSourceStorage { -// fn to_slot(&self, root: B256) -> U256 { -// let chain_id: u16 = self.chain_id.get().to::(); -// let emitter_address = self.emitter_address.get(); - -// let bytes = serialize_data_source_to_bytes(chain_id, emitter_address.as_slice().try_into().unwrap()) -// .expect("Failed to serialize DataSource"); - -// keccak256(bytes).to_slot(root) -// } -// } - impl StorageKey for DataSource { fn to_slot(&self, root: B256) -> U256 { let chain_id: u16 = self.chain_id.to::(); let emitter_address: [u8; 32] = self.emitter_address.as_slice().try_into().unwrap(); - let bytes = serialize_data_source_to_bytes(chain_id, &emitter_address) - .expect("Failed to serialize DataSource"); + let bytes = serialize_data_source_to_bytes(chain_id, &emitter_address); keccak256(bytes).to_slot(root) } } - -// pub trait GetDataSource { -// fn data_source(&self) -> DataSourceStorage; -// } - -// impl GetDataSource for VerifiedVM { -// fn data_source(&self) -> DataSourceStorage { -// let mut ds = DataSourceStorage { -// chain_id: StorageU16::new(storage_key!("chain_id")), -// emitter_address: StorageFixedBytes::<32>::new(storage_key!("emitter_address")), -// }; -// ds.chain_id.set(self.emitter_chain_id.into()); -// ds.emitter_address.set(self.emitter_address); -// ds -// } -// } - -// PriceInfo struct storing price information #[storage] -pub struct PriceInfoStorage { +pub struct PriceFeedStorage { + pub price_id: StorageFixedBytes<32>, pub publish_time: StorageU64, pub expo: StorageI32, pub price: StorageI64, @@ -98,9 +47,9 @@ pub struct PriceInfoStorage { } // Addressing nit -- running into some versioning issues that preclude me -// from returning the PriceInfo struct directly. Need to figure that out. +// from returning the PriceFeed struct directly. Need to figure that out. -// pub struct PriceInfo { +// pub struct PriceFeed { // pub publish_time: U64, // pub expo: I32, // pub price: I64, @@ -109,21 +58,10 @@ pub struct PriceInfoStorage { // pub ema_conf: U64, // } -// impl From<&PriceFeedMessage> for PriceInfo { -// fn from(price_feed_message: &PriceFeedMessage) -> Self { -// Self { -// publish_time: U64::from(price_feed_message.publish_time), -// expo: I32::from_be_bytes(price_feed_message.exponent.to_be_bytes()), -// price: I64::from_be_bytes(price_feed_message.price.to_be_bytes()), -// conf: U64::from(price_feed_message.conf), -// ema_price: I64::from_be_bytes(price_feed_message.ema_price.to_be_bytes()), -// ema_conf: U64::from(price_feed_message.ema_conf), -// } -// } -// } +pub type PriceFeedReturn = (FixedBytes<32>, U64, I32, I64, U64, I64, U64); -// PriceInfo struct storing price information -pub type PriceInfoReturn = (U64, I32, I64, U64, I64, U64); +// (price, conf, expo, publish_time) +pub type PriceReturn = (I64, U64, I32, U64); #[cfg(test)] mod tests { @@ -144,8 +82,7 @@ mod tests { expected_bytes[0..2].copy_from_slice(&chain_id.to_be_bytes()); expected_bytes[2..].copy_from_slice(&emitter_address); - let actual_bytes = serialize_data_source_to_bytes(chain_id, &emitter_address) - .expect("Serialization should succeed"); + let actual_bytes = serialize_data_source_to_bytes(chain_id, &emitter_address); assert_eq!( actual_bytes, expected_bytes, diff --git a/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs b/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs index 07aac6deb1..cd5316f642 100644 --- a/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs +++ b/target_chains/stylus/contracts/pyth-receiver/src/test_data.rs @@ -1,19 +1,127 @@ +#[cfg(test)] +use alloy_primitives::{address, Address, FixedBytes, I32, I64, U64}; use hex::FromHex; +use std::vec; -pub fn good_update1() -> Vec { - let hex_str = "504e41550100000003b801000000040d0216f3809b6396fdb0708bc94515ddb96a3bc8fb1993916e74f522ab4c34a268836a0bb38786303b55cc40ecb50d18c92bf9fd61688a143f3d24a73a3f468e4ab0000365be4f3a330fe96bab246922d9fa4816a865dec40d7c522e37f7e3605673b8f52eb30a4dad7aee3e1ac4b21337b8831c6147ef6e359e843b307872e83b5c7fe80004b6f12766eb395d04bdb146b1b69a4b9a8831a9c9ea20ee841d55efe72c629aef17038b925584493af7882981260b09587d73b6463415f4e1f8fb0a2a2c959c8301065c18a776cf558e43494bf31dd26ac0e3aed07a01ca0063abcfca3736b892368e4bf8b957f2d2e83ff9eb3fa8f667ef97a5f48c286103f70fda4a72e077719a0200085896c930febfb864a8c292bde679092e386e53389cb2d26bd12ceed64d22c0cd27676dc5c46d3e3c5949abbce89d4cc5ec378cfbf9ab1b0c24cfeb3371df4864000a1c67e67adea9f8a71eee76765f1fe4c7d9539d21b62d0336513692897cc6acbe1c827599a1f8c73b7478e8d788443b4cfd4e373344ab1ac9f771c9c494124863000b7e08e1cc1291340e6ec7d1b04c6a9c63f74077a074ee68edf4f95cf24c9743620f8abc8213884c8e4848fb76ec319d365e0cdc746a6e534a4cb828e51719be79010c61a2fedeedf34e4c80624d80cb93e24bafe9f6d23339173004d84e61c2395bfb5f552e87b901beb192ba6f4fc905eae30317c52b8614071e08a265592f24e8de000de38d08612edc5ebc863ea5a96325cb991750a94ff0e50f86bc322cce81db7cd17cb4ed3703f7477eb111a9d881e1345f5b79d618814f31b46034196563ee6a18000e5b11510d585a2decaba0be9e71386865b6069061478f7254d7852d55116569eb426c385267e40264d16db08e0f1a9e2c44b7d1c926d3c3ba662b212572386483010fa1ad0278402104b146b6f4d1cf85ac4df961d24eea0c7948b700f9973596cad130abfff25feef37125ba38507f34308d967455651e7014e40264a0c6510d3af001101f2e4e70868d6c327c92229537f1ffa33e488da3140ccc086f8210b437b162190742bd01ad1cb495a93f20045b4bb47e1562fdd82a8a1548d87d788a492b17190111847f5095df3edefc58be0956aee19876e850516e132506fd67504afee3c8cf240f07fb607b3c8282a2e56dcad23959e6759bdf8ad345ba8150448da56ff34fcc016866baf800000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000880cea9014155575600000000000d8e5c8d0000271085e6ab1bb044f57c4cd6c1d32aa0a82a5032198301005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000009f9828e420300000001284f2da6fffffff8000000006866baf8000000006866baf8000009f3b955a26000000001062cb8780c1283a49180b4986f0dec3c746d3daeb597887747b8f66a09876e1253a1ebb8a6bc4a5793daaa343db6057b82ec29715d7e1db0ffd6db523f7b80b03e3866ef5f5c80728801b92f5acecc28d8517e5615335d89c553f94b4370f3a20be0bc23e0fd401c4e5bd8bd32948a26233fc48f116428a490f087030ccfc442753e3074e2b9bbc1c61a009d86aaa200645c627a6b7f2f6597e34c60b14a58ef2583bcbb1d0e21b71a264fad2648ecc545031c7ed598772ffe875bf94a488389a49e6025e1b2a1f07ec598d0d9aa8ef7dd2733c8502c49d1d1323f1ae664e82e8a5e14978d52ab448ba9b1afc78f06c8cd17415a17"; - let bytes = Vec::from_hex(hex_str).expect("Invalid hex string"); - bytes +#[cfg(test)] +pub fn ban_usd_feed_id() -> [u8; 32] { + let hex_string = "a6320c8329924601f4d092dd3f562376f657fa0b5d0cba9e4385a24aaf135384"; + let bytes_vec = hex::decode(hex_string).expect("Invalid hex string"); + + let byte_array: [u8; 32] = bytes_vec + .try_into() + .expect("Hex string must decode to exactly 32 bytes"); + byte_array } -pub fn good_update2() -> Vec { - let hex_str = "504e41550100000003b801000000040d0239010392dab908eb9903d480abf8118d887f2a0c5eaeb9062e6eabab86c1b382127604c63358a7266cc5bade7726e159403c2bf317c7b3a4d8b489bfad4e334301031823d70352d3260226cbdddab0cf7d1584c0e1d23d4358ed12f9620e18a0db2154fbb096ac4cb8d5728e2cecf2b1398d7b9b51954f3fb8b4f59990ce017b0260000495e2691d8e6a0537d8ab3f41b5eb655acde7fbeaea0fdbe1f582383680f54c8a3a697c2c0f8b4110422f1b6beb0bfb601c929148b54dbf85fb19c333ccbb833c00066993a56c5980bf17d2790b933861fffb1fd09618921a90db4ab82cc8b148301f1a55d804d14cb39f648fdb0ef8c9ef1e24edc38d30f2aea7151025240a614bca0008a64a366c59bd6c4ce9d24a0e3beef2a33d28546826b1b969af184a257d648aab5672ad8a9eaf14473da40327e12e5c18168892bcebd693c8bed3df8ee50b85db010a36daa7c639c412969283f83749af93aef2464b27b83914b6026b721a59c8a04446a655686725247bd9154c71ca66505719df5867f775863a788d8bffb1bd637c000b237772560d72da81a782e89b138caf8bf1221b929ead77ca7d178b7b7af1c9141d9e77e22c98fe41b819f023695e6feed6f5215a5cdb6436bf52dc3c4c93e309010c89f2f3c64a8c77ccea47448e7871bbd70b59ed5761e5677458dbe6f82796efa2399e9ad9bf846d88d4688f1d19f9e2adeb2299017baf015c36a811d05c539b86000d6ba11d2f9a0edfd3a4bc23024d18dd010a83803faa79d40aec10a4deee40e8dd3c4c5401118b67bd6d879683cae3ea83d4f9afa744c655775615a7ce34237a02000e09a554d70c0f8e57bb79ce41552e38b836ad7b6bd1967e60c880f831341ad412699e4a9f5346713a6db2c7032bb7d1b3cc8e42f49ba17000f9d0916a13f2debf000f1ce88af88b96aaeb0104d4c966303eb9609df1b851a0d6149d05bba82f3fd70820a26d7f9d6fe18a7653fd3e3eda94fd9184726dadd2e8d58d09a8473e919f0800104583407293c41bef15c05ac20fc45fd5f9d00639c5b1f738d1ba42cd290fe5291e05219cefa8568806bfc1de76bcf5f799c90c9c6dd54bd69f9d459e994acb7a00110638c8067b42005ae678a7619e9eaad5fb66f0630547ab252179668e60b738c479ba6ff7e1f3dcffddab15e1bfebf93e0e4cb051535bdda3ecef6620aea32132016866e56400000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa710000000008813690014155575600000000000d8ec4750000271098d4f856e398eb41afbd0f2b24ad80e58b1f57b601005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000009fdcc9378c600000001105b4d5afffffff8000000006866e564000000006866e564000009fbf79e7bc000000000e99c0d1c0c02b95abadee324fbb6534576de1507c74c8ddef2b928c314cb3d4978a5ada03db907df05ba0fc051e659facec6479c324c276e5098fde9dcae0b462cd32d9e2e5b617b51ced85d38a8456022f3ab370d3c45a07acb686cfb39976b2f4bb1007a91e599951ed929f714a04dab0e6bd885a0c91a076f3b83ee8f765b70a3edda569876102f2c62cae15024e529a2e5e17c50411aa736c7511278a92f4d9cdda3239057c3a942a1365a58771734a982e41e1d7aa8bae87748f1becd045fcb5e1cb1993e978168147d6be8a2cba24a3cc8a2f78e7313f18c87ec2bb238510ebeb47aab50a449fd2ce3dc6b8c0d08d361c102"; +#[cfg(test)] +pub fn ban_usd_update() -> Vec> { + let hex_str = "504e41550100000003b801000000040d01c9e767c1685410aa1ad3a221af6d257b01d98301b27b72e78f3d7a8d580a90127451c5c699cbb1ef0b5bfd57645456c2898a997ccd4de6eb79d277ce56a12d0b01027599b2f2450d4d59904dc8f738dd73825db0b5235707b9f44119a84e8632c460652e5d7b3ba142120f2510374a478bd7e5cd71b954cae9ef6ea15d7a08a1c3e90103f3a1bd74938fa7c6b2c917303a002474a5f716501c19789fece2a3f80bd05c457c8087a8b2d14e84762059daa5608e38e4e3e8ed572787a20100b8c1d69777f30104c5640a58148caeab59ed9dcc025f7b7dcdeecbfc51108cc29d3659e8b0a1c1aa4079f43e0e846ed353d45b5f436431096cd3094c2fa15e4920e2d35e33632e00010693790cbfaca431837658775a3b274c05676b272b916245f939e978789874ce0f2daa426325986f38c2ee3b9053008362b60b9851d2e9db69d718faddb96db68700098eafe76c684d04f99292d536de3e95eb446c3fac2f70aaac11d5dbda0b5a38f516b56e9f3472528b675337746653c59ed2eae9079ae7f59c004a8cbb40139a7a010ae14fce0cc71f738ec21e66edcd867db036cd5e11a9476c710d2457e025c035c84518c8750b17197d308b9faa2561ec6532c2266eb36723a9d11871b04e3b1138000ba68cde478a18ebbc8e9c2b4bbb4ff16803d5402efbdc9fc34cad9d9ed6f1609f6c81596fac2eed2b98ce6f5b5d7efba530c8b9c15c70f2f10898b38ea2f9978c000d2ecb926686379023572b64d78aef7f61e9aa3e4ceae1d2b2917c1fae6d112b3d7ad1597e6768fffa2dff61a62012562eb68a7cf5597e9bfe755c280df36aef2c000e293c5cb9c805665057bedcfeae74139f47cb5cddc4d5190bbddc4d12cd53caa972281394ac02759b860382da06e8d9b003285090a6783de21786dfcb3b669c58000f3b90618d7a63cdd7da9e730dbd0bf5b22acdc35c08a17c9a6728b1115e63ff837c3267452dc29d8f77fa0cd39428066ea8ae1fd086293e2f17b9421b59f7922f0010629f08a3a59d8187eefef92a54b9bf55fb676f4e9fea826ffb4aa3331155c2162315bd092dd01776f0e45c5d857f9de70a0cbfa9b33f96d8c752bed5c37cf05600113038bf5593427383bfd0966064dc43f7a84f8c083c1bc1b03aa24fc857008f057778ca2393ac1146bbb51588f4903f0822cb94ac0dce7cdcba3a207969d529d000687028bf00000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000897d17f014155575600000000000da5600300002710bb2be176a294375c4430a694c2813c5823a56bef01005500a6320c8329924601f4d092dd3f562376f657fa0b5d0cba9e4385a24aaf1353840000000000625bef0000000000003c63fffffff800000000687028bf00000000687028bf00000000006223710000000000003c6b0c299b70feaac0e02b6430892ee820e1a0706a4099acf41781c8fa57ba6ca1f0b62d98994ccecb7d465eeae1c5a236df5ea50768f1d8e9300500a8824e608c5a02572ee89aa0f0490bd64d60482516a17a2cf6ef3140ac5e35e3ee1844aeb2fb2ab7740ed0905f80725663f8a7018025ea163ece851177137f0e1012b32a540bbaedd2be2b7ecbb6d7baa37298d5ea1e7d8b6c3e3f3c40ec0cdc546dcac1fc8fd0f16828f8d3d948e4ab67391bbca60a63de48273df382ca02f05bd3aa8a0f7513f2a722bd447d8c07a02e73f14d9bb625e82aea434b9378ffba62dd0ef4d04875b1a31cc077b7a9c58ddd0109e4b67e45"; let bytes = Vec::from_hex(hex_str).expect("Invalid hex string"); - bytes + vec![bytes] +} + +#[cfg(test)] +pub fn ban_usd_results_full() -> (FixedBytes<32>, U64, I32, I64, U64, I64, U64) { + ( + FixedBytes::<32>::from(ban_usd_feed_id()), + U64::from(1752180927u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(6446063i64.to_le_bytes()), + U64::from(15459u64), + I64::from_le_bytes(6431601i64.to_le_bytes()), + U64::from(15467u64), + ) +} + +#[cfg(test)] +pub fn ban_usd_results_get_price() -> (I64, U64, I32, U64) { + ( + I64::from_le_bytes(6446063i64.to_le_bytes()), + U64::from(15459u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + U64::from(1752180927u64), + ) } -pub fn multiple_updates() -> Vec { - let hex_str = "504e41550100000003b801000000040d02e57b0f291daa4d2f02f5c4a18793b278b238338f472d17897f8f0866549f77571cfe71fa55bae7f340b9124511559b73a0cf01c72adc8a8d9963cebecc5a503801039507a96b155046ab039f7c9cad17a4927e2ff34763bca9b65d572ddc7a5f019832ffbeeb5295447bfdb989efa0314865bb4571770ad8e75ae7a083288d6de232010412e7333ab5cf0f20274b0907da43b52016d5a095bb846962d13a222e4af1e7e63f7a8db49de04feb70f07a0e274dc58acc7a4c386a099369412c6813ba39916100063da672f75cf1d397829a39461e311ca366366828be8d12b19a00c552e7c8c5e7746b36d97dccc54e5b3aeae188b372ec885dc1fbd9c2285ce458764c86f0c1bb0008863aa237e9fe339683992121249a2e520b6483a3b3b60c703a1eb09ef33266312e729ff6d398e1a60be8474a95803cd1641ef6c1de2c74f3cd7e1f2510c919f9000a3bd5ec58424b21c48552c3be0f9cccd6e6c641eee2b4e550fb88cc93cfdf10c7409344ec3e81df711a293baba565a85e620d20028d9738e53939fa52f19ce622010b000f803511f89f02610fbece34fe327afb55196cc3e522bb28d71d6e4d5523ac77ca1afbbd8a28b4fe05c7f2aa1c3f428c89fe21096ba67bc505cbfa6ead9808010c315b34c9cac03647df4e12a050f8b739763498aa23999244036e09010e2a79a46d0cbabc22c535542896bc22df05dc5480db06a370dffeb0814424870fd50c21000d4a562686000b65df4e0ca00d2e00d10db9e913b481337ee1c80bb47b25553afb693d7be0c17f6fb106909a1eed52a6c27739471b719d4c450b99b066a02bd2c9010e309508bc7128030ca4b19fc34c0ee0e62eebb549c759c2e8ccfdf062793e41e935754ae1d5356ba98446fa2eaa837ae4b413d1ccdf1af6d9060a2885f18c19e1010f3e2ff50704a6ad1b491cb93a1e4678c0f58b91540ba3ce3b4424c96abbe922562c924debb3336ab2fe835237f16912d768e6e5b739f2ab44b57a1e2607c9bb89001070d0dfac758a38342b107870b4d5761df9e785c6be589317c4b1dad3c08998f11214c29201d172b278aa6f4d57171f0f05fb7a2718e6da6df4449e8897c0c2ac0011d9e885989fa2363ec311bf4e9ebd8738d4b3ecaf9a31c09ce06f9876c3ab772034c1df9ca09c847ee81de80a1f0f8592019fa60e55b02b657b8a7c99bee04701016866e28300000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa710000000008812f80014155575600000000000d8ebd6500002710f015dfd43b23aad91dcd4a7a8a113ed2d39233f202005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43000009fee44efa2300000000e7f6ffe3fffffff8000000006866e283000000006866e283000009fb4f364d0000000000e98d2f400c2704dab60f1b310d567acb60d77a3ce8003a6f564e8e1f567f00f004381d755e160a07372977a99288dcc9c9477cf9c1bb095403b514082aa774f7b243003e30548cbd97e8191d5ef2732796e06f84f05543a171f1e66052aa515c41a2d994a0d13e2e4016e6a28823201a52d408a5024797ec4b7629406062dd9ccc30a5d1eb4ac8b4a28a3d464bf4335ceda7646e03c29cc24b6c7c5e5924e6e69400a2c90561c9a2e4555084af13fbc7eaa0a884b75d4d1197933ac174b62c4b9b1cde3dd496f5e54cd2e01cdca0ba5c5a80a2bbd0d9d5dfb7aab5b638ef883e4e55f78a1536fe79c5c3e16cda9b53e364e3bbe95f005500ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace0000003c480c0e980000000009727f59fffffff8000000006866e283000000006866e2830000003c3597da300000000007d3439a0ccf5f10d7559e184107e994663aa0fc8f81718c0b281162b77eb09c774da30e2db5674df62494b3da820f6c986a0f32d1a195b6bc4676891d4e28cdb4e2f09dd47db3243547b37bdbb9799c82a42d6f1f18f8e17d7bed68408ef26e184f21e9b640e2c9f0416d91987acbe6fa8e72c2c99fa548f83c0eb5dd3c269ef52101521ef0b3d26f50b07dae68311bd138338881b20b78f8d21d2bfc27e9ac849b4c659d61c9a2e4555084af13fbc7eaa0a884b75d4d1197933ac174b62c4b9b1cde3dd496f5e54cd2e01cdca0ba5c5a80a2bbd0d9d5dfb7aab5b638ef883e4e55f78a1536fe79c5c3e16cda9b53e364e3bbe95f"; +#[cfg(test)] +pub fn btc_usd_feed_id() -> [u8; 32] { + let hex_string = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; + let bytes_vec = hex::decode(hex_string).expect("Invalid hex string"); + + let byte_array: [u8; 32] = bytes_vec + .try_into() + .expect("Hex string must decode to exactly 32 bytes"); + byte_array +} + +#[cfg(test)] +pub fn btc_usd_update() -> Vec> { + let hex_str = "504e41550100000003b801000000040d0239cafe615bc0f6037d0b2b596490a0473b07ed176082e6645c773678bafd705a3d3e86a03b17c44a9d0e2429051756514c2a247f9d08767b0bf093dd8313814c0103f5be04233a4e4f169428ee7c7e153978c593db55b9fc93ef6329333671eea8c567c143e88300ef470aeb7c3f5fc3d63582fe4c9f28fc451915f0c0a5ac1920f60104883322c38070deaaa013d45ffc4aa111a070938aaed1edda6853fa3676e7fbda6ffcd6711ac4fbb9e9c0320d193b9aace0b1fc42f0060c8ab54024c8cca55e5101066624f81e56e2981619eca215e07e2e055088a2c163dd457bf7d60828b0bc93a27c359999832a0d0534642defc8197b55ffcf1d52b9fcf5a8cee3e73c2910107a01081a8e5f8715f6128642bb34204c08415f972fed334add518e672be983f4e3830b7cdfb653976a37421a86d3ea1d0b5ee2d2baa8c54309539e4e7e64d57f13816d010a0df0714ee3e9d8f741c15b50f2d2b5a80bdfbdcd62963d37efde7690ee2f35ac4a134f425ec84610decb606254c81813d3be6372e64bf7d8692c1ceb7d3dbf31000bdbdfb38a07d2efd68bc96caeb986ab39473d5080575a0f7878bfbd4456da0df25dfad1e45c30bc1eb2d81922a5355db6fbc109b9bf4520f18b2da9cbd06c1b9d010ca5a2450d2339debfb36065567196c340721e260ea524003b2e73ef3f9ed1f9d2446e8b059c91ff124e83156e826c6cb496125ef6f54a578d03de9ce7a488a4f1010d2f389063583770fef355929ec57015b9b7f4d5879f63bc1dd1e8a1605b12cfbf534fb31d56be4e54ae1eba9442045b791b0cc9b56d3e3b0b4e8952c98841bed3000e343679701f31206503ee1014842cffdd7fb6b05173d8373e9fa99bd3a1f075a77339f1a588766d716cf650b7f04c6ec38b4f146932ab19345a5b976beb7fb932010ff8ecf6e31a7588184df483d4db07875c06c009f2837e8402fd5e72fc703317f5284be8d9354b4fab80397828dcee6e8d42e384bc0900e6c09bf03d062449d79b0010804e0a59b1ceaa6b27686390cf012f9b28c3f0631d625a02c2aa61924ae9e3f750d408ba01469ceba5c69750562a8764bf3f395fb2d3651ef7e785880dbf9f6d0011d624fbfc51b33be04143af0df857bff085ec613ce45c0afac19cb7b25d1386d4748a757c1d88c084a5917c0fa29e62777f52627100f9371e17b9cf3d7761e74c006870299c00000000001ae101faedac5851e32b9b23b5f9411a8c2bac4aae3ed4dd7b811dd1a72ea4aa71000000000897d392014155575600000000000da562160000271079aec689f7474fccda868e2c953a8034004c18f001005500e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b4300000a555e80406d00000000d970a713fffffff8000000006870299c000000006870299b00000a51d33b2f20000000010aa6a7740c9ed309739de34ae90fbadd4e31dfb63bdf4e6ce18be268a0b475c9fa239b3b3dc6413e6be59d3e081d18340d5f54f7c775bb8a3624c9b6365c377be3c7d8d95148bbeef8ed7337083c85b99f3d24fceef013b441f91efaf0d75d3054c798046ed109da12b59b60043a81139983cf1c68705c58279e7b3f122ff0562c5e8b925d8ebd8791842efc74260de6917967faea7c994b1bd8791cce3129f877447db2fe0e5436384e4e2dd7facc50127c072a25f69aad5d3c89ed0f38cb6b792c0d12c123f76b7059e9217b959969ba558430b06a4aeb769fd7a1a8cf3eb74680f0bc1b84882fb8e311d3db94ea051d2e209f6b"; let bytes = Vec::from_hex(hex_str).expect("Invalid hex string"); - bytes + vec![bytes] +} + +#[cfg(test)] +pub fn btc_usd_results_full() -> (FixedBytes<32>, U64, I32, I64, U64, I64, U64) { + ( + FixedBytes::<32>::from(btc_usd_feed_id()), + U64::from(1752181148u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + I64::from_le_bytes(11361773961325i64.to_le_bytes()), + U64::from(3648038675u64), + I64::from_le_bytes(11346552500000i64.to_le_bytes()), + U64::from(4473661300u64), + ) +} + +#[cfg(test)] +pub fn btc_usd_results_get_price() -> (I64, U64, I32, U64) { + ( + I64::from_le_bytes(11361773961325i64.to_le_bytes()), + U64::from(3648038675u64), + I32::from_le_bytes((-8i32).to_le_bytes()), + U64::from(1752181148u64), + ) +} + +#[cfg(test)] +pub fn multiple_updates_diff_vaa() -> Vec> { + vec![ban_usd_update()[0].clone(), btc_usd_update()[0].clone()] +} + +#[cfg(test)] +pub fn multiple_updates_diff_vaa_results_full( +) -> [(FixedBytes<32>, U64, I32, I64, U64, I64, U64); 2] { + [ban_usd_results_full(), btc_usd_results_full()] +} + +#[cfg(test)] +pub fn multiple_updates_diff_vaa_results_get_price() -> [(I64, U64, I32, U64); 2] { + [ban_usd_results_get_price(), btc_usd_results_get_price()] +} + +#[cfg(test)] +pub fn current_guardians() -> Vec
{ + vec![ + address!("0x5893B5A76c3f739645648885bDCcC06cd70a3Cd3"), // Rockaway + address!("0xfF6CB952589BDE862c25Ef4392132fb9D4A42157"), // Staked + address!("0x114De8460193bdf3A2fCf81f86a09765F4762fD1"), // Figment + address!("0x107A0086b32d7A0977926A205131d8731D39cbEB"), // ChainodeTech + address!("0x8C82B2fd82FaeD2711d59AF0F2499D16e726f6b2"), // Inotel + address!("0x11b39756C042441BE6D8650b69b54EbE715E2343"), // HashKey Cloud + address!("0x54Ce5B4D348fb74B958e8966e2ec3dBd4958a7cd"), // ChainLayer + address!("0x15e7cAF07C4e3DC8e7C469f92C8Cd88FB8005a20"), // xLabs + address!("0x74a3bf913953D695260D88BC1aA25A4eeE363ef0"), // Forbole + address!("0x000aC0076727b35FBea2dAc28fEE5cCB0fEA768e"), // Staking Fund + address!("0xAF45Ced136b9D9e24903464AE889F5C8a723FC14"), // Moonlet Wallet + address!("0xf93124b7c738843CBB89E864c862c38cddCccF95"), // P2P Validator + address!("0xD2CC37A4dc036a8D232b48f62cDD4731412f4890"), // 01node + address!("0xDA798F6896A3331F64b48c12D1D57Fd9cbe70811"), // MCF + address!("0x71AA1BE1D36CaFE3867910F99C09e347899C19C3"), // Everstake + address!("0x8192b6E7387CCd768277c17DAb1b7a5027c0b3Cf"), // Chorus One + address!("0x178e21ad2E77AE06711549CFBB1f9c7a9d8096e8"), // Syncnode + address!("0x5E1487F35515d02A92753504a8D75471b9f49EdB"), // Triton + address!("0x6FbEBc898F403E4773E95feB15E80C9A99c8348d"), // Staking Facilities + ] }