525 lines
18 KiB
Rust
525 lines
18 KiB
Rust
// 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<std::string::String>,
|
|
/// Optional token name.
|
|
pub name: std::option::Option<std::string::String>,
|
|
}
|
|
|
|
/// 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<i64>,
|
|
/// Optional DEX code.
|
|
pub dex_code: std::option::Option<std::string::String>,
|
|
}
|
|
|
|
/// 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<std::string::String>,
|
|
/// Optional DEX code.
|
|
pub dex_code: std::option::Option<std::string::String>,
|
|
/// Optional local trade count.
|
|
#[ts(type = "number | null")]
|
|
pub trade_count: std::option::Option<i64>,
|
|
/// Optional local last price.
|
|
#[ts(type = "number | null")]
|
|
pub last_price_quote_per_base: std::option::Option<f64>,
|
|
}
|
|
|
|
/// 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<KbDemoPipeline2TokenItem>,
|
|
/// Known pool list.
|
|
pub pools: std::vec::Vec<KbDemoPipeline2PoolItem>,
|
|
/// Known pair list.
|
|
pub pairs: std::vec::Vec<KbDemoPipeline2PairItem>,
|
|
}
|
|
|
|
/// 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<std::string::String>,
|
|
/// 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<std::string::String>,
|
|
/// 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<KbDemoPipeline2CatalogPayload, std::string::String> {
|
|
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<KbDemoPipeline2BackfillPayload, std::string::String> {
|
|
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<KbDemoPipeline2BackfillPayload, std::string::String> {
|
|
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<KbDemoPipeline2PairCandlesPayload, std::string::String> {
|
|
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<kb_lib::KbDatabase>,
|
|
) -> Result<KbDemoPipeline2CatalogPayload, std::string::String> {
|
|
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::<i64, std::string::String>::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::<KbDemoPipeline2TokenItem>::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::<i64, kb_lib::KbPairDto>::new();
|
|
for pair in &pairs {
|
|
pair_by_pool_id.insert(pair.pool_id, pair.clone());
|
|
}
|
|
let mut pair_items = std::vec::Vec::<KbDemoPipeline2PairItem>::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::<KbDemoPipeline2PoolItem>::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>,
|
|
) -> 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(),
|
|
}
|
|
}
|