0.7.24-pre.0

This commit is contained in:
2026-05-02 11:27:10 +02:00
parent 60db521a88
commit aaff2dbd94
38 changed files with 3074 additions and 207 deletions

View File

@@ -0,0 +1,524 @@
// 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(),
}
}