// file: kb_app/src/demo_pipeline2.rs //! Tauri commands for the focused pipeline demo window. //! //! This demo is intentionally narrower than `Demo Pipeline`: //! - read the local catalog of tokens / pools / pairs, //! - trigger targeted backfills from the chain, //! - load candles for one selected pair and timeframe. use tauri::Manager; use ts_rs::TS; /// One token item for the local catalog. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2TokenItem.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2TokenItem { /// Token mint. pub mint: std::string::String, /// Optional token symbol. pub symbol: std::option::Option, /// Optional token name. pub name: std::option::Option, } /// One pool item for the local catalog. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2PoolItem.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2PoolItem { /// Pool address. pub pool_address: std::string::String, /// Optional internal pair id when known. #[ts(type = "number | null")] pub pair_id: std::option::Option, /// Optional DEX code. pub dex_code: std::option::Option, } /// One pair item for the local catalog. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2PairItem.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2PairItem { /// Internal pair id. #[ts(type = "number")] pub pair_id: i64, /// Related pool address. pub pool_address: std::string::String, /// Optional pair symbol. pub symbol: std::option::Option, /// Optional DEX code. pub dex_code: std::option::Option, /// Optional local trade count. #[ts(type = "number | null")] pub trade_count: std::option::Option, /// Optional local last price. #[ts(type = "number | null")] pub last_price_quote_per_base: std::option::Option, } /// Full local catalog payload. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2CatalogPayload.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2CatalogPayload { /// Open database URL. pub database_url: std::string::String, /// Observed token list. pub tokens: std::vec::Vec, /// Known pool list. pub pools: std::vec::Vec, /// Known pair list. pub pairs: std::vec::Vec, } /// Request payload for token backfill. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillTokenRequest.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2BackfillTokenRequest { /// Token mint to backfill. pub token_mint: std::string::String, /// Optional HTTP role. pub http_role: std::option::Option, /// Limit for signatures fetched from the mint. pub mint_signature_limit: u32, /// Limit for signatures fetched from each discovered pool. pub pool_signature_limit: u32, } /// Request payload for pool backfill. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillPoolRequest.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2BackfillPoolRequest { /// Pool address to backfill. pub pool_address: std::string::String, /// Optional HTTP role. pub http_role: std::option::Option, /// Limit for signatures fetched from the pool. pub pool_signature_limit: u32, } /// Shared backfill response payload. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2BackfillPayload.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2BackfillPayload { /// Object key used by the backfill. pub object_key: std::string::String, /// Mode: `tokenMint` or `poolAddress`. pub mode: std::string::String, /// HTTP role used. pub http_role: std::string::String, /// Pretty JSON summary. pub summary_json: std::string::String, /// Refreshed local catalog after backfill. pub catalog: KbDemoPipeline2CatalogPayload, } /// Request payload for pair candles. #[derive(Clone, Debug, serde::Deserialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2PairCandlesRequest.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2PairCandlesRequest { /// Pair id to load. #[ts(type = "number")] pub pair_id: i64, /// Timeframe in seconds. #[ts(type = "number")] pub timeframe_seconds: i64, /// Whether materialized candles should be preferred when available. pub prefer_materialized: bool, } /// Candle payload returned to the UI. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, export_to = "../frontend/ts/bindings/KbDemoPipeline2PairCandlesPayload.ts" )] #[serde(rename_all = "camelCase")] pub(crate) struct KbDemoPipeline2PairCandlesPayload { /// Pair id. #[ts(type = "number")] pub pair_id: i64, /// Timeframe in seconds. #[ts(type = "number")] pub timeframe_seconds: i64, /// Pretty JSON array of candles. pub candles_json: std::string::String, } /// Opens the `Demo Pipeline 2` window. #[tauri::command] pub(crate) fn open_demo_pipeline2_window( app_handle: tauri::AppHandle, ) -> Result<(), std::string::String> { let existing_window_option = app_handle.get_webview_window("demo_pipeline2"); let demo_window = match existing_window_option { Some(demo_window) => demo_window, None => { let builder = tauri::WebviewWindowBuilder::new( &app_handle, "demo_pipeline2", tauri::WebviewUrl::App("demo_pipeline2.html".into()), ) .title("Demo Pipeline 2") .inner_size(1480.0, 920.0) .min_inner_size(1100.0, 720.0) .center() .visible(true) .transparent(false) .decorations(true); let build_result = builder.build(); match build_result { Ok(window) => window, Err(error) => { return Err(format!("cannot create demo_pipeline2 window: {error:?}")); } } } }; let show_result = demo_window.show(); if let Err(error) = show_result { return Err(format!("cannot show demo_pipeline2 window: {error:?}")); } let focus_result = demo_window.set_focus(); if let Err(error) = focus_result { return Err(format!("cannot focus demo_pipeline2 window: {error:?}")); } Ok(()) } /// Returns the local catalog of observed tokens, pools and pairs. #[tauri::command] pub(crate) async fn demo_pipeline2_get_catalog( state: tauri::State<'_, crate::KbAppState>, ) -> Result { kb_demo_pipeline2_build_catalog(state.database.clone()).await } /// Runs a targeted token backfill then returns the refreshed catalog. #[tauri::command] pub(crate) async fn demo_pipeline2_backfill_token_mint( state: tauri::State<'_, crate::KbAppState>, request: KbDemoPipeline2BackfillTokenRequest, ) -> Result { let token_mint = request.token_mint.trim().to_string(); if token_mint.is_empty() { return Err("token mint must not be empty".to_string()); } if request.mint_signature_limit == 0 { return Err("mintSignatureLimit must be > 0".to_string()); } if request.pool_signature_limit == 0 { return Err("poolSignatureLimit must be > 0".to_string()); } let http_role = kb_demo_pipeline2_normalize_http_role(request.http_role); let database = state.database.clone(); let http_pool = std::sync::Arc::new(state.http_pool.clone()); let service = kb_lib::KbTokenBackfillService::new(http_pool, database.clone(), http_role.clone()); let result = service .backfill_token_by_mint( token_mint.as_str(), request.mint_signature_limit as usize, request.pool_signature_limit as usize, ) .await; let backfill = match result { Ok(backfill) => backfill, Err(error) => { return Err(format!( "cannot backfill token mint '{}' with role '{}': {}", token_mint, http_role, error )); } }; let summary_json_result = serde_json::to_string_pretty(&backfill); let summary_json = match summary_json_result { Ok(summary_json) => summary_json, Err(error) => { return Err(format!( "cannot serialize token backfill result for '{}': {}", token_mint, error )); } }; let catalog = kb_demo_pipeline2_build_catalog(database).await?; Ok(KbDemoPipeline2BackfillPayload { object_key: token_mint, mode: "tokenMint".to_string(), http_role, summary_json, catalog, }) } /// Runs a targeted pool backfill then returns the refreshed catalog. #[tauri::command] pub(crate) async fn demo_pipeline2_backfill_pool_address( state: tauri::State<'_, crate::KbAppState>, request: KbDemoPipeline2BackfillPoolRequest, ) -> Result { let pool_address = request.pool_address.trim().to_string(); if pool_address.is_empty() { return Err("pool address must not be empty".to_string()); } if request.pool_signature_limit == 0 { return Err("poolSignatureLimit must be > 0".to_string()); } let http_role = kb_demo_pipeline2_normalize_http_role(request.http_role); let database = state.database.clone(); let http_pool = std::sync::Arc::new(state.http_pool.clone()); let service = kb_lib::KbTokenBackfillService::new(http_pool, database.clone(), http_role.clone()); let result = service .backfill_pool_by_address(pool_address.as_str(), request.pool_signature_limit as usize) .await; let backfill = match result { Ok(backfill) => backfill, Err(error) => { return Err(format!( "cannot backfill pool address '{}' with role '{}': {}", pool_address, http_role, error )); } }; let summary_json_result = serde_json::to_string_pretty(&backfill); let summary_json = match summary_json_result { Ok(summary_json) => summary_json, Err(error) => { return Err(format!( "cannot serialize pool backfill result for '{}': {}", pool_address, error )); } }; let catalog = kb_demo_pipeline2_build_catalog(database).await?; Ok(KbDemoPipeline2BackfillPayload { object_key: pool_address, mode: "poolAddress".to_string(), http_role, summary_json, catalog, }) } /// Loads candles for one pair and one timeframe. #[tauri::command] pub(crate) async fn demo_pipeline2_get_pair_candles( state: tauri::State<'_, crate::KbAppState>, request: KbDemoPipeline2PairCandlesRequest, ) -> Result { if request.pair_id <= 0 { return Err("pairId must be > 0".to_string()); } if request.timeframe_seconds <= 0 { return Err("timeframeSeconds must be > 0".to_string()); } let query_service = kb_lib::KbPairCandleQueryService::new(state.database.clone()); let candles_result = query_service .list_pair_candles( request.pair_id, request.timeframe_seconds, None, None, request.prefer_materialized, ) .await; let candles = match candles_result { Ok(candles) => candles, Err(error) => { return Err(format!( "cannot load candles for pair '{}' timeframe '{}': {}", request.pair_id, request.timeframe_seconds, error )); } }; let candles_json_result = serde_json::to_string_pretty(&candles); let candles_json = match candles_json_result { Ok(candles_json) => candles_json, Err(error) => { return Err(format!( "cannot serialize candles for pair '{}' timeframe '{}': {}", request.pair_id, request.timeframe_seconds, error )); } }; Ok(KbDemoPipeline2PairCandlesPayload { pair_id: request.pair_id, timeframe_seconds: request.timeframe_seconds, candles_json, }) } async fn kb_demo_pipeline2_build_catalog( database: std::sync::Arc, ) -> Result { let dexes_result = kb_lib::list_dexes(database.as_ref()).await; let dexes = match dexes_result { Ok(dexes) => dexes, Err(error) => { return Err(format!("cannot list DEXes: {}", error)); } }; let mut dex_code_by_id = std::collections::BTreeMap::::new(); for dex in dexes { if let Some(dex_id) = dex.id { dex_code_by_id.insert(dex_id, dex.code); } } let tokens_result = kb_lib::list_tokens(database.as_ref()).await; let db_tokens = match tokens_result { Ok(db_tokens) => db_tokens, Err(error) => { return Err(format!("cannot list tokens: {}", error)); } }; let mut tokens = std::vec::Vec::::new(); for token in db_tokens { tokens.push(KbDemoPipeline2TokenItem { mint: token.mint, symbol: token.symbol, name: token.name, }); } let pools_result = kb_lib::list_pools(database.as_ref()).await; let pools = match pools_result { Ok(pools) => pools, Err(error) => { return Err(format!("cannot list pools: {}", error)); } }; let pairs_result = kb_lib::list_pairs(database.as_ref()).await; let pairs = match pairs_result { Ok(pairs) => pairs, Err(error) => { return Err(format!("cannot list pairs: {}", error)); } }; let mut pair_by_pool_id = std::collections::BTreeMap::::new(); for pair in &pairs { pair_by_pool_id.insert(pair.pool_id, pair.clone()); } let mut pair_items = std::vec::Vec::::new(); for pair in pairs { let pair_id = match pair.id { Some(pair_id) => pair_id, None => continue, }; let pool_result = kb_lib::get_pool_by_address(database.as_ref(), "").await; let _ = pool_result; let pool_address = { let all_pools_result = kb_lib::list_pools(database.as_ref()).await; let all_pools = match all_pools_result { Ok(all_pools) => all_pools, Err(error) => { return Err(format!("cannot reload pools for pair catalog: {}", error)); } }; let mut found_address = std::string::String::new(); for pool in all_pools { let pool_id = match pool.id { Some(pool_id) => pool_id, None => continue, }; if pool_id == pair.pool_id { found_address = pool.address; break; } } found_address }; let pair_metric_result = kb_lib::get_pair_metric_by_pair_id(database.as_ref(), pair_id).await; let pair_metric_option = match pair_metric_result { Ok(pair_metric_option) => pair_metric_option, Err(error) => { return Err(format!( "cannot fetch pair metric for pair '{}': {}", pair_id, error )); } }; let trade_count = pair_metric_option.as_ref().map(|metric| metric.trade_count); let last_price_quote_per_base = pair_metric_option.and_then(|metric| metric.last_price_quote_per_base); pair_items.push(KbDemoPipeline2PairItem { pair_id, pool_address, symbol: pair.symbol, dex_code: dex_code_by_id.get(&pair.dex_id).cloned(), trade_count, last_price_quote_per_base, }); } let mut pool_items = std::vec::Vec::::new(); for pool in pools { let pool_id = match pool.id { Some(pool_id) => pool_id, None => continue, }; let pair_id = pair_by_pool_id.get(&pool_id).and_then(|pair| pair.id); pool_items.push(KbDemoPipeline2PoolItem { pool_address: pool.address, pair_id, dex_code: dex_code_by_id.get(&pool.dex_id).cloned(), }); } tokens.sort_by(|left, right| left.mint.cmp(&right.mint)); pool_items.sort_by(|left, right| left.pool_address.cmp(&right.pool_address)); pair_items.sort_by(|left, right| left.pair_id.cmp(&right.pair_id)); Ok(KbDemoPipeline2CatalogPayload { database_url: database.database_url().to_string(), tokens, pools: pool_items, pairs: pair_items, }) } fn kb_demo_pipeline2_normalize_http_role( role: std::option::Option, ) -> std::string::String { match role { Some(role) => { let trimmed = role.trim().to_string(); if trimmed.is_empty() { "history_backfill".to_string() } else { trimmed } } None => "history_backfill".to_string(), } }