Files
khadhroony-solana-project/crates/ksp-app-solprices-desk/src/market_price_runtime.rs
2026-08-27 11:27:39 +02:00

600 lines
30 KiB
Rust

// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs
// version: 3
//! Provider-neutral market-price presentation runtime owned by SOL Prices Desk.
use ts_rs::TS; // rust-rules: trait-import
/// 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<String>,
/// 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<String>,
/// KSP receipt timestamp in exact Unix milliseconds, when an observation exists.
pub(crate) received_at_unix_millis: std::option::Option<String>,
/// 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<String>,
/// 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<String>,
}
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<String> {
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<MarketPriceProviderRowDto>,
}
/// 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<MarketPricePresentationState>,
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<Self> {
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<std::vec::Vec<crate::MarketPriceProviderRowDto>> {
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<crate::MarketPriceProviderRowDto> {
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<String>) -> ksp_core_lib::Result<crate::MarketPriceRefreshResultDto> {
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<crate::MarketPriceRefreshResultDto> {
let registry = self.startup.resolved().service().registry();
let provider_ids = registry.entries().iter().map(|entry| return entry.descriptor().id().clone()).collect::<std::vec::Vec<_>>();
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<crate::MarketPriceProviderRowDto> {
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<crate::MarketPriceRefreshResultDto> {
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<ksp_offchain_transport_lib::MarketPriceObservation>,
}
struct MarketPricePresentationState {
entries: std::collections::BTreeMap<String, MarketPricePresentationEntry>,
}
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<u32> {
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<MarketPricePresentationState>,
) -> ksp_core_lib::Result<std::sync::MutexGuard<'_, MarketPricePresentationState>> {
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<String>) -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderId>> {
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;