600 lines
30 KiB
Rust
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;
|