v0.2.12-pre.005

This commit is contained in:
2026-08-27 10:43:01 +02:00
parent 5ac2197b6e
commit 9d351dd1c3
14 changed files with 486 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-solprices-desk/frontend/main.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
@@ -47,7 +47,7 @@
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 class="h3 mb-1">Prices</h1>
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Aucun refresh réseau n'est encore déclenché dans cette tranche.</p>
<p class="text-body-secondary mb-0">Inventaire provider-neutral du registry Off-chain. Chaque ligne peut être rafraîchie explicitement sans polling ni fallback automatique.</p>
</div>
</div>
<div id="pricesLoading" class="app-placeholder">
@@ -78,6 +78,7 @@
<th scope="col">Provider timestamp (ms)</th>
<th scope="col">Received at (ms)</th>
<th scope="col">Retry at (ms)</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody id="marketPriceRows"></tbody>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/frontend/ts/main.ts
// version: 4
// version: 5
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -20,6 +20,8 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics",
};
let currentMarketPriceRows: MarketPriceProviderRowDto[] = [];
function isViewId(value: string | undefined): value is ViewId {
return value === "prices" || value === "diagnostics";
}
@@ -60,6 +62,64 @@ function appendMarketPriceCell(row: HTMLTableRowElement, value: string): void {
row.append(cell);
}
function clearPricesDiagnostic(): void {
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
if (diagnostic) {
diagnostic.hidden = true;
diagnostic.textContent = "";
}
}
function showPricesDiagnostic(message: string): void {
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
if (diagnostic) {
diagnostic.hidden = false;
diagnostic.textContent = message;
}
}
async function refreshMarketPrice(providerId: string): Promise<void> {
frontendDebug("main", "SOL Prices Desk market-price refresh control clicked", { providerId });
clearPricesDiagnostic();
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
return provider.providerId === providerId ? { ...provider, loading: true } : provider;
});
renderMarketPriceRows(currentMarketPriceRows);
try {
const refreshed = await invokeKsp<MarketPriceProviderRowDto>("main", "refresh_market_price", { providerId });
currentMarketPriceRows = currentMarketPriceRows.map(provider => {
return provider.providerId === refreshed.providerId ? refreshed : provider;
});
renderMarketPriceRows(currentMarketPriceRows);
frontendTrace("main", "SOL Prices Desk market-price refresh row applied", { providerId });
} catch {
frontendWarn("main", "SOL Prices Desk market-price refresh failed", { providerId });
showPricesDiagnostic("Le provider sélectionné n'a pas pu être rafraîchi. L'état précédent est conservé lorsqu'il reste disponible.");
try {
await loadMarketPrices();
} catch {
frontendWarn("main", "SOL Prices Desk market-price refresh recovery reload failed", { providerId });
}
}
}
function appendRefreshCell(row: HTMLTableRowElement, provider: MarketPriceProviderRowDto): void {
const cell = document.createElement("td");
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-sm btn-outline-primary text-nowrap";
button.disabled = provider.loading;
button.dataset.providerId = provider.providerId;
button.innerHTML = provider.loading
? '<i class="fa-solid fa-spinner fa-spin me-1" aria-hidden="true"></i>Refreshing…'
: '<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>Refresh';
button.addEventListener("click", () => {
void refreshMarketPrice(provider.providerId);
});
cell.append(button);
row.append(cell);
}
function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
const loading = document.querySelector<HTMLElement>("#pricesLoading");
const empty = document.querySelector<HTMLElement>("#pricesEmpty");
@@ -77,6 +137,7 @@ function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
tableContainer.hidden = rows.length === 0;
for (const provider of rows) {
const row = document.createElement("tr");
row.dataset.providerId = provider.providerId;
appendMarketPriceCell(row, provider.displayName);
appendMarketPriceCell(row, provider.pair);
appendMarketPriceCell(row, provider.semantics);
@@ -86,6 +147,7 @@ function renderMarketPriceRows(rows: MarketPriceProviderRowDto[]): void {
appendMarketPriceCell(row, provider.providerTimestampUnixMillis ?? "—");
appendMarketPriceCell(row, provider.receivedAtUnixMillis ?? "—");
appendMarketPriceCell(row, provider.retryAtUnixMillis ?? "—");
appendRefreshCell(row, provider);
tableBody.append(row);
}
frontendTrace("main", "SOL Prices Desk market-price registry rows rendered", { rowCount: rows.length });
@@ -127,6 +189,7 @@ function renderRuntimeStatus(status: MarketPriceRuntimeStatusDto): void {
async function loadMarketPrices(): Promise<void> {
frontendTrace("main", "SOL Prices Desk provider registry load started");
const rows = await invokeKsp<MarketPriceProviderRowDto[]>("main", "list_market_prices");
currentMarketPriceRows = rows;
renderMarketPriceRows(rows);
frontendTrace("main", "SOL Prices Desk provider registry load completed", { rowCount: rows.length });
}
@@ -155,14 +218,10 @@ async function initializeMain(): Promise<void> {
await loadMarketPrices();
} catch {
const loading = document.querySelector<HTMLElement>("#pricesLoading");
const diagnostic = document.querySelector<HTMLElement>("#pricesDiagnostic");
if (loading) {
loading.hidden = true;
}
if (diagnostic) {
diagnostic.hidden = false;
diagnostic.textContent = "L'inventaire provider-neutral n'a pas pu être chargé.";
}
showPricesDiagnostic("L'inventaire provider-neutral n'a pas pu être chargé.");
frontendWarn("main", "SOL Prices Desk provider registry load failed");
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "ksp-app-solprices-desk",
"private": true,
"version": "0.2.12-pre.4.fix.2",
"version": "0.2.12-pre.5",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/src/app_state.rs
// version: 3
// version: 4
//! Shared backend state owned by the SOL Prices Desk Tauri application.
@@ -68,6 +68,11 @@ impl crate::AppState {
return self.market_price_runtime.list_rows();
}
/// Refreshes one provider by opaque identifier and returns its updated safe row projection.
pub(crate) async fn refresh_market_price(&self, provider_id: String) -> ksp_core_lib::Result<crate::MarketPriceProviderRowDto> {
return self.market_price_runtime.refresh_one(provider_id).await;
}
/// Builds the safe runtime status exposed by the SOL Prices Desk shell.
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::MarketPriceRuntimeStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
@@ -111,7 +116,7 @@ impl crate::AppState {
fallback_logging_active: runtime.fallback_active,
provider_count,
ready_provider_count,
shell_phase: "pre.004-market-price-runtime".to_owned(),
shell_phase: "pre.005-single-refresh".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
unavailable_provider_count,
});

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/src/market_price_runtime.rs
// version: 1
// version: 2
//! Provider-neutral market-price presentation runtime owned by SOL Prices Desk.
@@ -22,7 +22,7 @@ pub(crate) struct MarketPriceProviderRowDto {
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 future refresh commands.
/// 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>,
@@ -98,15 +98,10 @@ impl crate::MarketPriceRuntime {
/// 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 = self.presentation.lock();
let presentation = lock_presentation(&self.presentation);
let presentation = match presentation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return 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",
));
},
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() {
@@ -125,6 +120,90 @@ impl crate::MarketPriceRuntime {
}
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 loading = self.set_loading(&provider_id, true);
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.set_loading(&provider_id, false);
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) => 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);
}
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 set_loading(&self, provider_id: &ksp_offchain_transport_lib::MarketPriceProviderId, loading: bool) -> 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 set_loading_in_state(&mut presentation, provider_id.as_str(), loading);
}
}
struct MarketPricePresentationEntry {
@@ -136,6 +215,27 @@ 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",
@@ -170,6 +270,18 @@ fn count_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32>
};
}
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 project_registry_entry(
entry: &ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry,
presentation: &MarketPricePresentationEntry,
@@ -204,6 +316,20 @@ fn semantics_code(semantics: ksp_offchain_transport_lib::MarketPriceSemantics) -
};
}
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;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/src/tauri.rs
// version: 3
// version: 4
//! Tauri runtime assembly for the KSP SOL prices desktop application.
@@ -72,7 +72,13 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, list_market_prices, splash_frontend_ready,]);
return builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
get_runtime_status,
list_market_prices,
refresh_market_price,
splash_frontend_ready,
]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
@@ -122,6 +128,18 @@ fn list_market_prices(
};
}
#[tauri::command]
async fn refresh_market_price(
provider_id: String,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::MarketPriceProviderRowDto, crate::CommandErrorDto> {
let result = state.refresh_market_price(provider_id).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP SOL Prices Desk",
"version": "0.2.12-pre.4.fix.2",
"version": "0.2.12-pre.5",
"identifier": "com.sasedev.ksp-app-solprices-desk",
"build": {
"beforeDevCommand": {

View File

@@ -192,7 +192,7 @@ fn pre_002_fix_001_reuses_the_common_ksp_desktop_template() {
}
#[test]
fn pre_004_market_price_runtime_projects_registry_rows_without_refresh_controls() {
fn pre_005_market_price_runtime_adds_single_refresh_without_many_or_all_controls() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains(r#"ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }"#));
@@ -201,21 +201,26 @@ fn pre_004_market_price_runtime_projects_registry_rows_without_refresh_controls(
assert!(lib_rs.contains("mod market_price_runtime;"));
assert!(lib_rs.contains("MarketPriceProviderRowDto"));
assert!(lib_rs.contains("MarketPriceRuntime"));
let runtime_rs = read_text(root.join("src/market_price_runtime.rs").as_path());
assert!(runtime_rs.contains("pub(crate) async fn refresh_one"));
assert!(runtime_rs.contains("service().refresh(&provider_id).await"));
assert!(runtime_rs.contains("self.set_loading(&provider_id, true)"));
assert!(runtime_rs.contains("apply_outcome_to_state"));
let tauri_rs = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri_rs.contains("list_market_prices"));
for forbidden in ["refresh_market_price", "refresh_market_prices", "refresh_all_market_prices"] {
assert!(tauri_rs.contains("refresh_market_price"));
for forbidden in ["refresh_market_prices", "refresh_all_market_prices"] {
assert!(!tauri_rs.contains(forbidden));
}
let main_html = read_text(root.join("frontend/main.html").as_path());
assert!(main_html.contains(r#"id="pricesLoading""#));
assert!(main_html.contains(r#"id="pricesEmpty""#));
assert!(main_html.contains(r#"id="marketPriceTable""#));
assert!(main_html.contains(r#"id="marketPriceRows""#));
assert!(!main_html.contains("Refresh"));
assert!(main_html.contains(">Action</th>"));
assert!(main_html.contains("rafraîchie explicitement"));
let main_typescript = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(main_typescript.contains(r#""list_market_prices""#));
assert!(main_typescript.contains("MarketPriceProviderRowDto"));
assert!(main_typescript.contains(r#"provider.price ?? "—""#));
assert!(main_typescript.contains(r#""refresh_market_price""#));
assert!(main_typescript.contains("SOL Prices Desk market-price refresh control clicked"));
assert!(main_typescript.contains("provider.providerId"));
assert!(main_typescript.contains("provider.loading"));
assert!(main_typescript.contains("Refreshing…"));
assert!(!main_typescript.contains("parseFloat("));
assert!(!main_typescript.contains("Number(provider.price"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/tests/desktop_security.rs
// version: 3
// version: 4
//! Security, ownership and frontend instrumentation canaries for SOL Prices Desk.
@@ -69,7 +69,7 @@ fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() {
}
#[test]
fn pre_004_tauri_commands_remain_centralized_and_refresh_is_absent() {
fn pre_005_tauri_commands_remain_centralized_and_only_single_refresh_is_present() {
let root = app_root();
let mut rust_files = std::vec::Vec::new();
collect_files(root.join("src").as_path(), "rs", &mut rust_files);
@@ -82,13 +82,19 @@ fn pre_004_tauri_commands_remain_centralized_and_refresh_is_absent() {
} else {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
assert!(!source.contains("refresh_market_price"), "{} advances refresh commands before their planned tranche", path.display());
if path.file_name().and_then(std::ffi::OsStr::to_str) != std::option::Option::Some("tauri.rs") {
assert!(!source.contains("refresh_market_price"), "{} declares refresh command plumbing outside tauri.rs", path.display());
}
assert!(!source.contains("refresh_market_prices"), "{} advances multi-refresh before pre.006", path.display());
assert!(!source.contains("refresh_all_market_prices"), "{} advances global refresh before pre.006", path.display());
}
assert_eq!(command_count, 4);
assert_eq!(command_count, 5);
let tauri = read_text(root.join("src/tauri.rs").as_path());
assert!(tauri.contains("refresh_market_price"));
}
#[test]
fn pre_004_frontend_registry_loading_is_logged_without_business_values() {
fn pre_005_frontend_refresh_interactions_are_logged_without_business_values() {
let root = app_root();
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
@@ -101,6 +107,9 @@ fn pre_004_frontend_registry_loading_is_logged_without_business_values() {
assert!(invoke.contains("frontendTrace"));
assert!(main.contains("SOL Prices Desk provider registry load started"));
assert!(main.contains("SOL Prices Desk market-price registry rows rendered"));
assert!(main.contains("SOL Prices Desk market-price refresh control clicked"));
assert!(main.contains("SOL Prices Desk market-price refresh row applied"));
assert!(main.contains("SOL Prices Desk market-price refresh failed"));
for forbidden in ["JSON.stringify(status)", "JSON.stringify(payload)", "apiKey", "authorization"] {
assert!(!main.contains(forbidden), "main frontend logging must not serialize business/secret values: {forbidden}");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-solprices-desk/unit_tests/market_price_runtime.rs
// version: 1
// version: 2
fn descriptor() -> std::option::Option<ksp_offchain_transport_lib::MarketPriceProviderDescriptor> {
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new("test-provider");
@@ -138,3 +138,95 @@ fn observation_projection_keeps_exact_decimal_and_distinct_timestamps() {
assert!(row.loading);
assert!(row.refreshed);
}
#[tokio::test]
async fn disabled_refresh_clears_loading_without_inventing_observation() -> ksp_core_lib::Result<()> {
let settings = match ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service =
match ksp_offchain_transport_lib::MarketPriceService::new(std::vec![ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(settings),]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider_id = match ksp_offchain_transport_lib::MarketPriceProviderId::new("coinpaprika") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut entries = std::collections::BTreeMap::new();
entries.insert(provider_id.as_str().to_owned(), super::MarketPricePresentationEntry { loading: true, observation: std::option::Option::None });
let mut presentation = super::MarketPricePresentationState { entries };
let outcome = match service.refresh(&provider_id).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!outcome.refreshed());
let applied = super::apply_outcome_to_state(&mut presentation, &outcome);
if let std::result::Result::Err(error) = applied {
return std::result::Result::Err(error);
}
let entry = presentation.entries.get(provider_id.as_str());
assert!(entry.is_some());
if let std::option::Option::Some(entry) = entry {
assert!(!entry.loading);
assert!(entry.observation.is_none());
}
return std::result::Result::Ok(());
}
#[tokio::test]
async fn non_refresh_outcome_preserves_last_successful_observation() -> ksp_core_lib::Result<()> {
let settings = match ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let service =
match ksp_offchain_transport_lib::MarketPriceService::new(std::vec![ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(settings),]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider_id = match ksp_offchain_transport_lib::MarketPriceProviderId::new("coinpaprika") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let price = match ksp_offchain_transport_lib::MarketPriceDecimal::parse("101.2500") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provenance = match ksp_offchain_transport_lib::MarketPriceProvenance::new("unit-test") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let observation = match ksp_offchain_transport_lib::MarketPriceObservation::new(
provider_id.clone(),
price,
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(10),
ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(30),
std::option::Option::Some(ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(20)),
provenance,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut entries = std::collections::BTreeMap::new();
entries.insert(provider_id.as_str().to_owned(), super::MarketPricePresentationEntry { loading: true, observation: std::option::Option::Some(observation) });
let mut presentation = super::MarketPricePresentationState { entries };
let outcome = match service.refresh(&provider_id).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!outcome.refreshed());
let applied = super::apply_outcome_to_state(&mut presentation, &outcome);
if let std::result::Result::Err(error) = applied {
return std::result::Result::Err(error);
}
let entry = presentation.entries.get(provider_id.as_str());
assert!(entry.is_some());
if let std::option::Option::Some(entry) = entry {
assert!(!entry.loading);
assert_eq!(entry.observation.as_ref().map(|value| return value.price().to_canonical_string()).as_deref(), std::option::Option::Some("101.25"));
}
return std::result::Result::Ok(());
}