// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs // version: 4 //! Provider-neutral market-price presentation runtime owned by SOL Prices Desk. use ts_rs::TS; // rust-rules: trait-import const MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS: usize = 64; /// Safe provider-neutral row projected to the SOL Prices Desk frontend. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceProviderRowDto.ts")] pub(crate) struct MarketPriceProviderRowDto { /// Generic authentication capability code for the configured provider mode. pub(crate) auth_mode: String, /// Generic current availability code. pub(crate) availability: String, /// Safe provider display name supplied by Off-chain Transport. pub(crate) display_name: String, /// Whether one refresh is currently in flight for this provider. pub(crate) loading: bool, /// Stable V1 pair label. pub(crate) pair: String, /// Exact canonical SOL/USD decimal string, when an observation exists. pub(crate) price: std::option::Option, /// Opaque provider identifier accepted by refresh commands. pub(crate) provider_id: String, /// Provider-supplied observation timestamp in exact Unix milliseconds, when genuinely supplied. pub(crate) provider_timestamp_unix_millis: std::option::Option, /// KSP receipt timestamp in exact Unix milliseconds, when an observation exists. pub(crate) received_at_unix_millis: std::option::Option, /// Whether this row currently retains at least one successful observation. pub(crate) refreshed: bool, /// Known next retry timestamp in exact Unix milliseconds for cooldown/temporary states. pub(crate) retry_at_unix_millis: std::option::Option, /// Generic price-semantics code retained from the provider descriptor. pub(crate) semantics: String, } /// Bounded provider-neutral request used by the selected-provider refresh command. #[derive(Debug, serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshManyRequestDto.ts")] pub(crate) struct MarketPriceRefreshManyRequestDto { /// Opaque provider identifiers requested by the frontend in deterministic row order. pub(crate) provider_ids: std::vec::Vec, } impl crate::MarketPriceRefreshManyRequestDto { /// Consumes the IPC DTO and returns the opaque provider identifiers for authoritative backend validation. pub(crate) fn into_provider_ids(self) -> std::vec::Vec { return self.provider_ids; } } /// Safe result of one selected/global refresh operation. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_solprices_desk/market_price_runtime/MarketPriceRefreshResultDto.ts")] pub(crate) struct MarketPriceRefreshResultDto { /// Number of provider refreshes that produced a new normalized observation. pub(crate) refreshed_count: u32, /// Number of providers included in the validated operation. pub(crate) requested_count: u32, /// Updated provider-neutral rows in the exact dispatch/outcome order. pub(crate) rows: std::vec::Vec, } /// Backend application runtime that owns the Config-selected Off-chain service and current in-memory presentation state. pub(crate) struct MarketPriceRuntime { presentation: std::sync::Mutex, startup: crate::OffchainTransportStartup, } impl crate::MarketPriceRuntime { /// Builds the application presentation runtime from the already Config-resolved Off-chain startup. pub(crate) fn new(startup: crate::OffchainTransportStartup) -> ksp_core_lib::Result { let registry = startup.resolved().service().registry(); let mut entries = std::collections::BTreeMap::new(); for entry in registry.entries() { let previous = entries .insert(entry.descriptor().id().as_str().to_owned(), MarketPricePresentationEntry { loading: false, observation: std::option::Option::None }); if previous.is_some() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime contains duplicate provider state") .with_context("provider_id", entry.descriptor().id().as_str()), ); } } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_count = entries.len(), "initialized SOL Prices Desk market-price presentation runtime" ); return std::result::Result::Ok(Self { presentation: std::sync::Mutex::new(MarketPricePresentationState { entries }), startup }); } /// Returns the Config/bootstrap metadata retained by the price runtime. #[must_use] pub(crate) const fn startup(&self) -> &crate::OffchainTransportStartup { return &self.startup; } /// Returns total, ready and unavailable provider counts from the detached generic registry snapshot. pub(crate) fn provider_counts(&self) -> ksp_core_lib::Result<(u32, u32, u32)> { let registry = self.startup.resolved().service().registry(); let total = count_to_u32(registry.len(), "provider_count"); let total = match total { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut ready_count = 0usize; for entry in registry.entries() { if matches!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready) { ready_count += 1; } } let ready = count_to_u32(ready_count, "ready_provider_count"); let ready = match ready { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unavailable = total.saturating_sub(ready); return std::result::Result::Ok((total, ready, unavailable)); } /// Projects the current registry and in-memory observation state as deterministic frontend rows without network dispatch. pub(crate) fn list_rows(&self) -> ksp_core_lib::Result> { let registry = self.startup.resolved().service().registry(); let presentation = lock_presentation(&self.presentation); let presentation = match presentation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut rows = std::vec::Vec::with_capacity(registry.len()); for entry in registry.entries() { let provider_id = entry.descriptor().id().as_str(); let presentation_entry = presentation.entries.get(provider_id); let presentation_entry = match presentation_entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime is missing one registry row") .with_context("provider_id", provider_id), ); }, }; rows.push(project_registry_entry(entry, presentation_entry)); } return std::result::Result::Ok(rows); } /// Refreshes one opaque provider through Off-chain Transport and updates only its in-memory presentation row. pub(crate) async fn refresh_one(&self, provider_id: String) -> ksp_core_lib::Result { let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id); let provider_id = match provider_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let provider_ids = [provider_id.clone()]; let loading = self.begin_refresh(provider_ids.as_slice()); if let std::result::Result::Err(error) = loading { return std::result::Result::Err(error); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_id = provider_id.as_str(), "started SOL Prices Desk single-provider market-price refresh" ); let outcome = self.startup.resolved().service().refresh(&provider_id).await; let outcome = match outcome { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; let row = self.apply_outcome(&outcome); let row = match row { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_id = provider_id.as_str(), refreshed = outcome.refreshed(), availability = row.availability.as_str(), "completed SOL Prices Desk single-provider market-price refresh" ); return std::result::Result::Ok(row); } /// Refreshes a frontend-selected provider set in deterministic request order through the generic Off-chain service. pub(crate) async fn refresh_many(&self, provider_ids: std::vec::Vec) -> ksp_core_lib::Result { let provider_ids = parse_provider_ids(provider_ids); let provider_ids = match provider_ids { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let loading = self.begin_refresh(provider_ids.as_slice()); if let std::result::Result::Err(error) = loading { return std::result::Result::Err(error); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_count = provider_ids.len(), "started SOL Prices Desk selected-provider market-price refresh" ); let outcomes = self.startup.resolved().service().refresh_many(provider_ids.as_slice()).await; let outcomes = match outcomes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; let result = self.apply_outcomes(outcomes.as_slice()); let result = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_count = result.requested_count, refreshed_count = result.refreshed_count, "completed SOL Prices Desk selected-provider market-price refresh" ); return std::result::Result::Ok(result); } /// Refreshes every configured provider through the generic Off-chain service in its stable provider order. pub(crate) async fn refresh_all(&self) -> ksp_core_lib::Result { let registry = self.startup.resolved().service().registry(); let provider_ids = registry.entries().iter().map(|entry| return entry.descriptor().id().clone()).collect::>(); let loading = self.begin_refresh(provider_ids.as_slice()); if let std::result::Result::Err(error) = loading { return std::result::Result::Err(error); } ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_count = provider_ids.len(), "started SOL Prices Desk global market-price refresh" ); let outcomes = self.startup.resolved().service().refresh_all().await; let outcomes = match outcomes { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; let result = self.apply_outcomes(outcomes.as_slice()); let result = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let cleared = self.clear_loading(provider_ids.as_slice()); if let std::result::Result::Err(clear_error) = cleared { return std::result::Result::Err(clear_error); } return std::result::Result::Err(error); }, }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_OFFCHAIN_TRANSPORT, provider_count = result.requested_count, refreshed_count = result.refreshed_count, "completed SOL Prices Desk global market-price refresh" ); return std::result::Result::Ok(result); } fn apply_outcome(&self, outcome: &ksp_offchain_transport_lib::MarketPriceRefreshOutcome) -> ksp_core_lib::Result { let registry = self.startup.resolved().service().registry(); let registry_entry = registry.entry(outcome.provider_id()); let registry_entry = match registry_entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk refresh outcome references an unknown registry provider") .with_context("provider_id", outcome.provider_id().as_str()), ); }, }; let presentation = lock_presentation(&self.presentation); let mut presentation = match presentation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let applied = apply_outcome_to_state(&mut presentation, outcome); if let std::result::Result::Err(error) = applied { return std::result::Result::Err(error); } let presentation_entry = presentation.entries.get(outcome.provider_id().as_str()); return match presentation_entry { std::option::Option::Some(value) => std::result::Result::Ok(project_registry_entry(registry_entry, value)), std::option::Option::None => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime is missing refreshed provider state") .with_context("provider_id", outcome.provider_id().as_str()), ), }; } fn apply_outcomes(&self, outcomes: &[ksp_offchain_transport_lib::MarketPriceRefreshOutcome]) -> ksp_core_lib::Result { let registry = self.startup.resolved().service().registry(); let presentation = lock_presentation(&self.presentation); let mut presentation = match presentation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut rows = std::vec::Vec::with_capacity(outcomes.len()); let mut refreshed_count = 0usize; for outcome in outcomes { let registry_entry = registry.entry(outcome.provider_id()); let registry_entry = match registry_entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk refresh outcome references an unknown registry provider", ) .with_context("provider_id", outcome.provider_id().as_str()), ); }, }; let applied = apply_outcome_to_state(&mut presentation, outcome); if let std::result::Result::Err(error) = applied { return std::result::Result::Err(error); } if outcome.refreshed() { refreshed_count += 1; } let presentation_entry = presentation.entries.get(outcome.provider_id().as_str()); let presentation_entry = match presentation_entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime is missing refreshed provider state", ) .with_context("provider_id", outcome.provider_id().as_str()), ); }, }; rows.push(project_registry_entry(registry_entry, presentation_entry)); } let requested_count = count_to_u32(outcomes.len(), "refresh_requested_count"); let requested_count = match requested_count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let refreshed_count = count_to_u32(refreshed_count, "refresh_success_count"); let refreshed_count = match refreshed_count { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::MarketPriceRefreshResultDto { refreshed_count, requested_count, rows }); } fn begin_refresh(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> { let presentation = lock_presentation(&self.presentation); let mut presentation = match presentation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return begin_refresh_in_state(&mut presentation, provider_ids); } fn clear_loading(&self, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId]) -> ksp_core_lib::Result<()> { let presentation = lock_presentation(&self.presentation); let mut presentation = match presentation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; for provider_id in provider_ids { let cleared = set_loading_in_state(&mut presentation, provider_id.as_str(), false); if let std::result::Result::Err(error) = cleared { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } } struct MarketPricePresentationEntry { loading: bool, observation: std::option::Option, } struct MarketPricePresentationState { entries: std::collections::BTreeMap, } fn apply_outcome_to_state( presentation: &mut MarketPricePresentationState, outcome: &ksp_offchain_transport_lib::MarketPriceRefreshOutcome, ) -> ksp_core_lib::Result<()> { let entry = presentation.entries.get_mut(outcome.provider_id().as_str()); let entry = match entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price presentation state is missing refresh provider") .with_context("provider_id", outcome.provider_id().as_str()), ); }, }; entry.loading = false; if let std::option::Option::Some(observation) = outcome.observation() { entry.observation = std::option::Option::Some(observation.clone()); } return std::result::Result::Ok(()); } fn auth_mode_code(auth_mode: ksp_offchain_transport_lib::MarketPriceProviderAuthMode) -> &'static str { return match auth_mode { ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None => "none", ksp_offchain_transport_lib::MarketPriceProviderAuthMode::OptionalApiKey => "optional_api_key", ksp_offchain_transport_lib::MarketPriceProviderAuthMode::RequiredApiKey => "required_api_key", _ => "unknown", }; } fn availability_code(availability: ksp_offchain_transport_lib::MarketPriceProviderAvailability) -> &'static str { return match availability { ksp_offchain_transport_lib::MarketPriceProviderAvailability::AuthenticationUnavailable => "authentication_unavailable", ksp_offchain_transport_lib::MarketPriceProviderAvailability::CoolingDown { .. } => "cooling_down", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled => "disabled", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Misconfigured => "misconfigured", ksp_offchain_transport_lib::MarketPriceProviderAvailability::QuotaUnavailable => "quota_unavailable", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready => "ready", ksp_offchain_transport_lib::MarketPriceProviderAvailability::TemporarilyUnavailable { .. } => "temporarily_unavailable", _ => "unknown", }; } fn begin_refresh_in_state( presentation: &mut MarketPricePresentationState, provider_ids: &[ksp_offchain_transport_lib::MarketPriceProviderId], ) -> ksp_core_lib::Result<()> { let mut seen = std::collections::BTreeSet::new(); for provider_id in provider_ids { if !seen.insert(provider_id.as_str().to_owned()) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID, "SOL Prices Desk market-price refresh selection contains a duplicate provider", ) .with_context("provider_id", provider_id.as_str()), ); } let entry = presentation.entries.get(provider_id.as_str()); let entry = match entry { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID, "SOL Prices Desk market-price refresh selection contains an unknown provider", ) .with_context("provider_id", provider_id.as_str()), ); }, }; if entry.loading { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_MARKET_PRICE_REFRESH_CONFLICT, "SOL Prices Desk market-price provider already has a refresh in flight", ) .with_context("provider_id", provider_id.as_str()), ); } } for provider_id in provider_ids { let updated = set_loading_in_state(presentation, provider_id.as_str(), true); if let std::result::Result::Err(error) = updated { return std::result::Result::Err(error); } } return std::result::Result::Ok(()); } fn count_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result { let converted = u32::try_from(value); return match converted { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price runtime count exceeds DTO bounds") .with_context("field", field) .with_source(error), ), }; } fn lock_presentation( presentation: &std::sync::Mutex, ) -> ksp_core_lib::Result> { return match presentation.lock() { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "SOL Prices Desk market-price presentation state lock is poisoned", )), }; } fn parse_provider_ids(provider_ids: std::vec::Vec) -> ksp_core_lib::Result> { if provider_ids.is_empty() || provider_ids.len() > MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_MARKET_PRICE_SELECTION_INVALID, "SOL Prices Desk market-price refresh selection has an invalid provider count", ) .with_context("provider_count", provider_ids.len().to_string()) .with_context("max_provider_count", MARKET_PRICE_REFRESH_MAX_PROVIDER_IDS.to_string()), ); } let mut parsed = std::vec::Vec::with_capacity(provider_ids.len()); for provider_id in provider_ids { let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id); let provider_id = match provider_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; parsed.push(provider_id); } return std::result::Result::Ok(parsed); } fn project_registry_entry( entry: &ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry, presentation: &MarketPricePresentationEntry, ) -> crate::MarketPriceProviderRowDto { let descriptor = entry.descriptor(); let availability = entry.state().availability(); let observation = presentation.observation.as_ref(); return crate::MarketPriceProviderRowDto { auth_mode: auth_mode_code(descriptor.auth_mode()).to_owned(), availability: availability_code(availability).to_owned(), display_name: descriptor.display_name().to_owned(), loading: presentation.loading, pair: ksp_offchain_transport_lib::MarketPricePair::SolUsd.code().to_owned(), price: observation.map(|value| return value.price().to_canonical_string()), provider_id: descriptor.id().as_str().to_owned(), provider_timestamp_unix_millis: observation.and_then(|value| return value.provider_timestamp()).map(|value| return value.unix_millis().to_string()), received_at_unix_millis: observation.map(|value| return value.received_at().unix_millis().to_string()), refreshed: observation.is_some(), retry_at_unix_millis: availability.retry_at().map(|value| return value.unix_millis().to_string()), semantics: semantics_code(descriptor.semantics()).to_owned(), }; } fn semantics_code(semantics: ksp_offchain_transport_lib::MarketPriceSemantics) -> &'static str { return match semantics { ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket => "aggregated_market", ksp_offchain_transport_lib::MarketPriceSemantics::DexPairUsd => "dex_pair_usd", ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade => "exchange_last_trade", ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic => "solana_heuristic", ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot => "solana_spot", _ => "unknown", }; } fn set_loading_in_state(presentation: &mut MarketPricePresentationState, provider_id: &str, loading: bool) -> ksp_core_lib::Result<()> { let entry = presentation.entries.get_mut(provider_id); return match entry { std::option::Option::Some(value) => { value.loading = loading; std::result::Result::Ok(()) }, std::option::Option::None => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "SOL Prices Desk market-price presentation state is missing provider") .with_context("provider_id", provider_id), ), }; } #[cfg(test)] #[path = "../unit_tests/market_price_runtime.rs"] mod tests;