0.7.24-pre.0
This commit is contained in:
524
kb_app/src/demo_pipeline2.rs
Normal file
524
kb_app/src/demo_pipeline2.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user