diff --git a/integration-tests/tests/test_integration.py b/integration-tests/tests/test_integration.py index 5cb4577..26bd8e7 100644 --- a/integration-tests/tests/test_integration.py +++ b/integration-tests/tests/test_integration.py @@ -78,7 +78,7 @@ "nasdaq_symbol": "AAPL", "symbol": "Equity.US.AAPL/USD", "base": "AAPL", - "market_hours": "America/New_York,C,C,C,C,C,C,C" # Should never be published due to all-closed market hours + "weekly_schedule": "America/New_York,C,C,C,C,C,C,C" # Should never be published due to all-closed market hours }, "metadata": {"jump_id": "186", "jump_symbol": "AAPL", "price_exp": -5, "min_publishers": 1}, } diff --git a/src/agent/market_hours.rs b/src/agent/market_hours.rs index 6e4d144..4205065 100644 --- a/src/agent/market_hours.rs +++ b/src/agent/market_hours.rs @@ -11,7 +11,7 @@ use { DateTime, Datelike, Duration, - TimeZone, + Utc, Weekday, }, chrono_tz::Tz, @@ -29,7 +29,7 @@ lazy_static! { /// Weekly market hours schedule #[derive(Clone, Default, Debug, Eq, PartialEq)] -pub struct MarketHours { +pub struct WeeklySchedule { pub timezone: Tz, pub mon: MHKind, pub tue: MHKind, @@ -40,7 +40,7 @@ pub struct MarketHours { pub sun: MHKind, } -impl MarketHours { +impl WeeklySchedule { pub fn all_closed() -> Self { Self { timezone: Default::default(), @@ -54,7 +54,7 @@ impl MarketHours { } } - pub fn can_publish_at(&self, when: &DateTime) -> bool { + pub fn can_publish_at(&self, when: &DateTime) -> bool { // Convert to time local to the market let when_market_local = when.with_timezone(&self.timezone); @@ -76,7 +76,7 @@ impl MarketHours { } } -impl FromStr for MarketHours { +impl FromStr for WeeklySchedule { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let mut split_by_commas = s.split(","); @@ -235,9 +235,9 @@ mod tests { // Mon-Fri 9-5, inconsistent leading space on Tuesday, leading 0 on Friday (expected to be fine) let s = "Europe/Warsaw,9:00-17:00, 9:00-17:00,9:00-17:00,9:00-17:00,09:00-17:00,C,C"; - let parsed: MarketHours = s.parse()?; + let parsed: WeeklySchedule = s.parse()?; - let expected = MarketHours { + let expected = WeeklySchedule { timezone: Tz::Europe__Warsaw, mon: MHKind::TimeRange( NaiveTime::from_hms_opt(9, 0, 0).unwrap(), @@ -273,7 +273,7 @@ mod tests { // Valid but missing a timezone let s = "O,C,O,C,O,C,O"; - let parsing_result: Result = s.parse(); + let parsing_result: Result = s.parse(); dbg!(&parsing_result); assert!(parsing_result.is_err()); @@ -284,7 +284,7 @@ mod tests { // One day short let s = "Asia/Hong_Kong,C,O,C,O,C,O"; - let parsing_result: Result = s.parse(); + let parsing_result: Result = s.parse(); dbg!(&parsing_result); assert!(parsing_result.is_err()); @@ -294,7 +294,7 @@ mod tests { fn test_parsing_gibberish_timezone_is_error() { // Pretty sure that one's extinct let s = "Pangea/New_Dino_City,O,O,O,O,O,O,O"; - let parsing_result: Result = s.parse(); + let parsing_result: Result = s.parse(); dbg!(&parsing_result); assert!(parsing_result.is_err()); @@ -303,7 +303,7 @@ mod tests { #[test] fn test_parsing_gibberish_day_schedule_is_error() { let s = "Europe/Amsterdam,mondays are alright I guess,O,O,O,O,O,O"; - let parsing_result: Result = s.parse(); + let parsing_result: Result = s.parse(); dbg!(&parsing_result); assert!(parsing_result.is_err()); @@ -313,7 +313,7 @@ mod tests { fn test_parsing_too_many_days_is_error() { // One day too many let s = "Europe/Lisbon,O,O,O,O,O,O,O,O,C"; - let parsing_result: Result = s.parse(); + let parsing_result: Result = s.parse(); dbg!(&parsing_result); assert!(parsing_result.is_err()); @@ -322,7 +322,7 @@ mod tests { #[test] fn test_market_hours_happy_path() -> Result<()> { // Prepare a schedule of narrow ranges - let mh: MarketHours = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00".parse()?; + let mh: WeeklySchedule = "America/New_York,00:00-1:00,1:00-2:00,2:00-3:00,3:00-4:00,4:00-5:00,5:00-6:00,6:00-7:00".parse()?; // Prepare UTC datetimes that fall before, within and after market hours let format = "%Y-%m-%d %H:%M"; @@ -379,7 +379,7 @@ mod tests { #[test] fn test_market_hours_midnight_00_24() -> Result<()> { // Prepare a schedule of midnight-neighboring ranges - let mh: MarketHours = "Europe/Amsterdam,23:00-24:00,00:00-01:00,O,C,C,C,C".parse()?; + let mh: WeeklySchedule = "Europe/Amsterdam,23:00-24:00,00:00-01:00,O,C,C,C,C".parse()?; let format = "%Y-%m-%d %H:%M"; let ok_datetimes = vec![ @@ -387,24 +387,28 @@ mod tests { .unwrap() .and_time(MAX_TIME_INSTANT.clone()) .and_local_timezone(Tz::Europe__Amsterdam) - .unwrap(), + .unwrap() + .with_timezone(&Utc), NaiveDateTime::parse_from_str("2023-11-21 00:00", format)? .and_local_timezone(Tz::Europe__Amsterdam) - .unwrap(), + .unwrap() + .with_timezone(&Utc), ]; let bad_datetimes = vec![ // Start of Monday Nov 20th, must not be confused for MAX_TIME_INSTANT on that day NaiveDateTime::parse_from_str("2023-11-20 00:00", format)? .and_local_timezone(Tz::Europe__Amsterdam) - .unwrap(), + .unwrap() + .with_timezone(&Utc), // End of Tuesday Nov 21st, borders Wednesday, must not be // confused for Wednesday 00:00 which is open. NaiveDate::from_ymd_opt(2023, 11, 21) .unwrap() .and_time(MAX_TIME_INSTANT.clone()) .and_local_timezone(Tz::Europe__Amsterdam) - .unwrap(), + .unwrap() + .with_timezone(&Utc), ]; dbg!(&mh); diff --git a/src/agent/metrics.rs b/src/agent/metrics.rs index e6bfe76..63c4098 100644 --- a/src/agent/metrics.rs +++ b/src/agent/metrics.rs @@ -40,7 +40,9 @@ use { }, warp::{ hyper::StatusCode, - reply::{self,}, + reply::{ + self, + }, Filter, Rejection, Reply, diff --git a/src/agent/pythd/adapter.rs b/src/agent/pythd/adapter.rs index 9e929a9..db81b27 100644 --- a/src/agent/pythd/adapter.rs +++ b/src/agent/pythd/adapter.rs @@ -955,7 +955,7 @@ mod tests { ) .unwrap(), solana::oracle::ProductEntry { - account_data: pyth_sdk_solana::state::ProductAccount { + account_data: pyth_sdk_solana::state::ProductAccount { magic: 0xa1b2c3d4, ver: 6, atype: 4, @@ -992,8 +992,8 @@ mod tests { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }, - market_hours: Default::default(), - price_accounts: vec![ + weekly_schedule: Default::default(), + price_accounts: vec![ solana_sdk::pubkey::Pubkey::from_str( "GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU", ) @@ -1015,7 +1015,7 @@ mod tests { ) .unwrap(), solana::oracle::ProductEntry { - account_data: pyth_sdk_solana::state::ProductAccount { + account_data: pyth_sdk_solana::state::ProductAccount { magic: 0xa1b2c3d4, ver: 5, atype: 3, @@ -1052,8 +1052,8 @@ mod tests { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }, - market_hours: Default::default(), - price_accounts: vec![ + weekly_schedule: Default::default(), + price_accounts: vec![ solana_sdk::pubkey::Pubkey::from_str( "GG3FTE7xhc9Diy7dn9P6BWzoCrAEE4D3p5NBYrDAm5DD", ) diff --git a/src/agent/solana/exporter.rs b/src/agent/solana/exporter.rs index 96a8d6c..f50cee2 100644 --- a/src/agent/solana/exporter.rs +++ b/src/agent/solana/exporter.rs @@ -10,7 +10,7 @@ use { network::Network, }, crate::agent::{ - market_hours::MarketHours, + market_hours::WeeklySchedule, remote_keypair_loader::{ KeypairRequest, RemoteKeypairLoader, @@ -22,10 +22,7 @@ use { Result, }, bincode::Options, - chrono::{ - Local, - Utc, - }, + chrono::Utc, futures_util::future::{ self, join_all, @@ -63,7 +60,6 @@ use { collections::{ BTreeMap, HashMap, - HashSet, }, time::Duration, }, @@ -175,7 +171,7 @@ pub fn spawn_exporter( network: Network, rpc_url: &str, rpc_timeout: Duration, - publisher_permissions_rx: mpsc::Receiver>>, + publisher_permissions_rx: mpsc::Receiver>>, key_store: KeyStore, local_store_tx: Sender, global_store_tx: Sender, @@ -263,10 +259,10 @@ pub struct Exporter { inflight_transactions_tx: Sender, /// publisher => { permissioned_price => market hours } as read by the oracle module - publisher_permissions_rx: mpsc::Receiver>>, + publisher_permissions_rx: mpsc::Receiver>>, /// Currently known permissioned prices of this publisher along with their market hours - our_prices: HashMap, + our_prices: HashMap, /// Interval to update the dynamic price (if enabled) dynamic_compute_unit_price_update_interval: Interval, @@ -290,7 +286,7 @@ impl Exporter { global_store_tx: Sender, network_state_rx: watch::Receiver, inflight_transactions_tx: Sender, - publisher_permissions_rx: mpsc::Receiver>>, + publisher_permissions_rx: mpsc::Receiver>>, keypair_request_tx: mpsc::Sender, logger: Logger, ) -> Self { @@ -474,20 +470,22 @@ impl Exporter { "publish_pubkey" => publish_keypair.pubkey().to_string(), ); - let now = Local::now(); + // Get a fresh system time + let now = Utc::now(); // Filter out price accounts we're not permissioned to update Ok(fresh_updates .into_iter() .filter(|(id, _data)| { let key_from_id = Pubkey::from((*id).clone().to_bytes()); - if let Some(market_hours) = self.our_prices.get(&key_from_id) { - let ret = market_hours.can_publish_at(&now); + if let Some(weekly_schedule) = self.our_prices.get(&key_from_id) { + let ret = weekly_schedule.can_publish_at(&now); if !ret { debug!(self.logger, "Exporter: Attempted to publish price outside market hours"; "price_account" => key_from_id.to_string(), - "market_hours" => format!("{:?}", market_hours), + "weekly_schedule" => format!("{:?}", weekly_schedule), + "utc_time" => now.format("%c").to_string(), ); } diff --git a/src/agent/solana/oracle.rs b/src/agent/solana/oracle.rs index 72e7842..c4df8b4 100644 --- a/src/agent/solana/oracle.rs +++ b/src/agent/solana/oracle.rs @@ -1,4 +1,4 @@ -use crate::agent::market_hours::MarketHours; +use crate::agent::market_hours::WeeklySchedule; // This module is responsible for loading the current state of the // on-chain Oracle program accounts from Solana. use { @@ -49,7 +49,7 @@ pub struct Data { pub product_accounts: HashMap, pub price_accounts: HashMap, /// publisher => {their permissioned price accounts => market hours} - pub publisher_permissions: HashMap>, + pub publisher_permissions: HashMap>, } impl Data { @@ -57,7 +57,7 @@ impl Data { mapping_accounts: HashMap, product_accounts: HashMap, price_accounts: HashMap, - publisher_permissions: HashMap>, + publisher_permissions: HashMap>, ) -> Self { Data { mapping_accounts, @@ -71,9 +71,9 @@ impl Data { pub type MappingAccount = pyth_sdk_solana::state::MappingAccount; #[derive(Debug, Clone)] pub struct ProductEntry { - pub account_data: pyth_sdk_solana::state::ProductAccount, - pub market_hours: MarketHours, - pub price_accounts: Vec, + pub account_data: pyth_sdk_solana::state::ProductAccount, + pub weekly_schedule: WeeklySchedule, + pub price_accounts: Vec, } pub type PriceEntry = pyth_sdk_solana::state::PriceAccount; @@ -136,7 +136,7 @@ pub fn spawn_oracle( wss_url: &str, rpc_timeout: Duration, global_store_update_tx: mpsc::Sender, - publisher_permissions_tx: mpsc::Sender>>, + publisher_permissions_tx: mpsc::Sender>>, key_store: KeyStore, logger: Logger, ) -> Vec> { @@ -351,7 +351,7 @@ struct Poller { data_tx: mpsc::Sender, /// Updates about permissioned price accounts from oracle to exporter - publisher_permissions_tx: mpsc::Sender>>, + publisher_permissions_tx: mpsc::Sender>>, /// The RPC client to use to poll data from the RPC node rpc_client: RpcClient, @@ -371,7 +371,7 @@ struct Poller { impl Poller { pub fn new( data_tx: mpsc::Sender, - publisher_permissions_tx: mpsc::Sender>>, + publisher_permissions_tx: mpsc::Sender>>, rpc_url: &str, rpc_timeout: Duration, commitment: CommitmentLevel, @@ -439,9 +439,10 @@ impl Poller { .entry(component.publisher) .or_insert(HashMap::new()); - let market_hours = if let Some(prod_entry) = product_accounts.get(&price_entry.prod) + let weekly_schedule = if let Some(prod_entry) = + product_accounts.get(&price_entry.prod) { - prod_entry.market_hours.clone() + prod_entry.weekly_schedule.clone() } else { warn!(&self.logger, "Oracle: INTERNAL: could not find product from price `prod` field, market hours falling back to 24/7."; "price" => price_key.to_string(), @@ -450,7 +451,7 @@ impl Poller { Default::default() }; - component_pub_entry.insert(*price_key, market_hours); + component_pub_entry.insert(*price_key, weekly_schedule); } } @@ -538,15 +539,15 @@ impl Poller { let product = load_product_account(prod_acc.data.as_slice()) .context(format!("Could not parse product account {}", product_key))?; - let market_hours: MarketHours = if let Some((_mh_key, mh_val)) = - product.iter().find(|(k, _v)| *k == "market_hours") + let weekly_schedule: WeeklySchedule = if let Some((_wsched_key, wsched_val)) = + product.iter().find(|(k, _v)| *k == "weekly_schedule") { - mh_val.parse().unwrap_or_else(|err| { + wsched_val.parse().unwrap_or_else(|err| { warn!( self.logger, - "Oracle: Product has market_hours defined but it could not be parsed. Falling back to 24/7 publishing."; + "Oracle: Product has weekly_schedule defined but it could not be parsed. Falling back to 24/7 publishing."; "product_key" => product_key.to_string(), - "market_hours" => mh_val, + "weekly_schedule" => wsched_val, ); debug!(self.logger, "parsing error context"; "context" => format!("{:?}", err)); Default::default() @@ -560,7 +561,7 @@ impl Poller { *product_key, ProductEntry { account_data: *product, - market_hours, + weekly_schedule, price_accounts: vec![], }, );