0.1.0
This commit is contained in:
217
migration/khadhroony-bot2-reference/kb_app_demo/src/app_state.rs
Normal file
217
migration/khadhroony-bot2-reference/kb_app_demo/src/app_state.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
// file: kb_app_demo/src/app_state.rs
|
||||
// version: 11
|
||||
|
||||
//! Shared Tauri application state and startup state initialization.
|
||||
|
||||
/// Shared state managed by Tauri for the demo application.
|
||||
pub(crate) struct AppState {
|
||||
config_path: std::string::String,
|
||||
app_config: kb_config::AppConfig,
|
||||
active_profile: kb_config::ProfileConfig,
|
||||
logging_guard: std::sync::Mutex<kb_logging::LoggingGuard>,
|
||||
http_pool: kb_rpc::HttpEndpointPool,
|
||||
ws_pool: kb_rpc::WsEndpointPool,
|
||||
demo_ws_session: tokio::sync::Mutex<std::option::Option<std::sync::Arc<kb_rpc::WsSession>>>,
|
||||
demo_backfill_running: std::sync::atomic::AtomicBool,
|
||||
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
demo_core_extraction_running: std::sync::atomic::AtomicBool,
|
||||
demo_core_extraction_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
demo_decode_replay_running: std::sync::atomic::AtomicBool,
|
||||
demo_decode_replay_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
demo_decode_replay_campaign_id: std::sync::Mutex<std::option::Option<std::string::String>>,
|
||||
demo_execution_solana_core_running: std::sync::atomic::AtomicBool,
|
||||
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::AppState {
|
||||
/// Initializes configuration, logging and shared runtime state.
|
||||
pub(crate) fn initialize() -> kb_core::Result<crate::AppState> {
|
||||
let config_path = resolve_config_path();
|
||||
let app_config = match kb_config::read_config_json_file(&config_path) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let active_profile = match kb_config::active_profile(&app_config) {
|
||||
std::result::Result::Ok(profile) => profile.clone(),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let logging_config = convert_logging_config(&active_profile.logging);
|
||||
let logging_guard = match kb_logging::init_logging(&logging_config) {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let http_pool = match kb_rpc::HttpEndpointPool::from_profile(&active_profile) {
|
||||
std::result::Result::Ok(pool) => pool,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let ws_pool = match kb_rpc::WsEndpointPool::from_profile(&active_profile) {
|
||||
std::result::Result::Ok(pool) => pool,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::AppState {
|
||||
config_path: config_path.display().to_string(),
|
||||
app_config,
|
||||
active_profile,
|
||||
logging_guard: std::sync::Mutex::new(logging_guard),
|
||||
http_pool,
|
||||
ws_pool,
|
||||
demo_ws_session: tokio::sync::Mutex::new(std::option::Option::None),
|
||||
demo_backfill_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_backfill_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_core_extraction_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_core_extraction_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_decode_replay_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_decode_replay_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_decode_replay_campaign_id: std::sync::Mutex::new(std::option::Option::None),
|
||||
demo_execution_solana_core_running: std::sync::atomic::AtomicBool::new(false),
|
||||
demo_execution_solana_core_cancel_requested: std::sync::atomic::AtomicBool::new(false),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the path used to load the configuration file.
|
||||
pub(crate) fn config_path(&self) -> &str {
|
||||
return self.config_path.as_str();
|
||||
}
|
||||
|
||||
/// Returns the complete parsed application configuration.
|
||||
pub(crate) fn app_config(&self) -> &kb_config::AppConfig {
|
||||
return &self.app_config;
|
||||
}
|
||||
|
||||
/// Returns the active profile selected from the configuration.
|
||||
pub(crate) fn active_profile(&self) -> &kb_config::ProfileConfig {
|
||||
return &self.active_profile;
|
||||
}
|
||||
|
||||
/// Returns the configured HTTP endpoint pool.
|
||||
pub(crate) fn http_pool(&self) -> &kb_rpc::HttpEndpointPool {
|
||||
return &self.http_pool;
|
||||
}
|
||||
|
||||
/// Returns the configured WebSocket endpoint pool.
|
||||
pub(crate) fn ws_pool(&self) -> &kb_rpc::WsEndpointPool {
|
||||
return &self.ws_pool;
|
||||
}
|
||||
|
||||
/// Returns the demo WebSocket runtime session slot.
|
||||
pub(crate) fn demo_ws_session(
|
||||
&self,
|
||||
) -> &tokio::sync::Mutex<std::option::Option<std::sync::Arc<kb_rpc::WsSession>>> {
|
||||
return &self.demo_ws_session;
|
||||
}
|
||||
|
||||
/// Returns the single-campaign execution flag used by the backfill demo.
|
||||
pub(crate) fn demo_backfill_running(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_backfill_running;
|
||||
}
|
||||
|
||||
/// Returns the cooperative cancellation flag used by the backfill demo.
|
||||
pub(crate) fn demo_backfill_cancel_requested(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_backfill_cancel_requested;
|
||||
}
|
||||
|
||||
/// Returns the single-campaign execution flag used by the core extraction demo.
|
||||
pub(crate) fn demo_core_extraction_running(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_core_extraction_running;
|
||||
}
|
||||
|
||||
/// Returns the cooperative cancellation flag used by the core extraction demo.
|
||||
pub(crate) fn demo_core_extraction_cancel_requested(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_core_extraction_cancel_requested;
|
||||
}
|
||||
|
||||
/// Returns the single-campaign execution flag used by the decode replay demo.
|
||||
pub(crate) fn demo_decode_replay_running(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_decode_replay_running;
|
||||
}
|
||||
|
||||
/// Returns the cooperative cancellation flag used by the decode replay demo.
|
||||
pub(crate) fn demo_decode_replay_cancel_requested(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_decode_replay_cancel_requested;
|
||||
}
|
||||
|
||||
/// Returns the active contextual decode campaign identifier slot.
|
||||
pub(crate) fn demo_decode_replay_campaign_id(
|
||||
&self,
|
||||
) -> &std::sync::Mutex<std::option::Option<std::string::String>> {
|
||||
return &self.demo_decode_replay_campaign_id;
|
||||
}
|
||||
|
||||
/// Returns the single-execution flag used by the Solana Core execution demo.
|
||||
pub(crate) fn demo_execution_solana_core_running(&self) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_execution_solana_core_running;
|
||||
}
|
||||
|
||||
/// Returns the cooperative cancellation flag used by the execution demo.
|
||||
pub(crate) fn demo_execution_solana_core_cancel_requested(
|
||||
&self,
|
||||
) -> &std::sync::atomic::AtomicBool {
|
||||
return &self.demo_execution_solana_core_cancel_requested;
|
||||
}
|
||||
|
||||
/// Returns the number of logging routes held by the logging guard.
|
||||
pub(crate) fn logging_route_count(&self) -> usize {
|
||||
let lock_result = self.logging_guard.lock();
|
||||
let guard = match lock_result {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(_) => return 0,
|
||||
};
|
||||
return guard.route_count();
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_logging_config(config: &kb_config::LoggingConfig) -> kb_logging::LoggingConfig {
|
||||
let mut targets = std::vec::Vec::<kb_logging::LogTargetConfig>::new();
|
||||
for target in &config.targets {
|
||||
targets.push(kb_logging::LogTargetConfig {
|
||||
name: target.name.clone(),
|
||||
enabled: target.enabled,
|
||||
sink: target.sink.clone(),
|
||||
level: target.level.clone(),
|
||||
path: target.path.clone(),
|
||||
rotation: target.rotation.clone(),
|
||||
format: target.format.clone(),
|
||||
ansi: target.ansi,
|
||||
targets: target.targets.clone(),
|
||||
});
|
||||
}
|
||||
let mut target_filters = std::vec::Vec::<kb_logging::LogTargetFilterConfig>::new();
|
||||
for filter in &config.target_filters {
|
||||
target_filters.push(kb_logging::LogTargetFilterConfig {
|
||||
target: filter.target.clone(),
|
||||
level: filter.level.clone(),
|
||||
});
|
||||
}
|
||||
return kb_logging::LoggingConfig {
|
||||
default_level: config.default_level.clone(),
|
||||
targets,
|
||||
target_filters,
|
||||
};
|
||||
}
|
||||
|
||||
fn resolve_config_path() -> std::path::PathBuf {
|
||||
let configured = std::env::var("KB_CONFIG_PATH").ok();
|
||||
return resolve_config_path_from_value(configured.as_deref());
|
||||
}
|
||||
|
||||
fn resolve_config_path_from_value(value: std::option::Option<&str>) -> std::path::PathBuf {
|
||||
if let std::option::Option::Some(configured) = value {
|
||||
let trimmed = configured.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return std::path::PathBuf::from(trimmed);
|
||||
}
|
||||
}
|
||||
return crate::workspace_root_dir().join("config/example.config.json");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn resolve_config_path_uses_workspace_example_by_default() {
|
||||
let path = super::resolve_config_path_from_value(std::option::Option::None);
|
||||
assert_eq!(
|
||||
path.file_name().and_then(std::ffi::OsStr::to_str),
|
||||
std::option::Option::Some("example.config.json")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// file: kb_app_demo/src/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the `kb_app_demo` crate.
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_app_demo";
|
||||
@@ -0,0 +1,457 @@
|
||||
// file: kb_app_demo/src/demo_backfill.rs
|
||||
// version: 9
|
||||
|
||||
//! Tauri commands and UI payloads for bounded HTTP transaction backfills.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One endpoint role capable of discovering and hydrating transaction history.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_backfill/DemoBackfillRoleOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoBackfillRoleOption {
|
||||
/// Stable endpoint role code.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Providers exposing this role.
|
||||
pub(crate) providers: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Initial options shown by the backfill demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_backfill/DemoBackfillOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoBackfillOptionsPayload {
|
||||
/// Selectable endpoint roles.
|
||||
pub(crate) roles: std::vec::Vec<DemoBackfillRoleOption>,
|
||||
/// Preferred role when configured.
|
||||
pub(crate) default_role: std::option::Option<std::string::String>,
|
||||
/// Default transaction commitment.
|
||||
pub(crate) default_commitment: std::string::String,
|
||||
/// Default history page size.
|
||||
pub(crate) default_page_size: u16,
|
||||
/// Default maximum number of history pages.
|
||||
pub(crate) default_max_pages: u32,
|
||||
/// Default hydration concurrency requested by the operator.
|
||||
pub(crate) default_max_concurrent_requests: u32,
|
||||
/// Default retries after the initial attempt.
|
||||
pub(crate) default_max_retries: u32,
|
||||
/// Whether one campaign is currently running.
|
||||
pub(crate) running: bool,
|
||||
}
|
||||
|
||||
/// UI request for one bounded backfill campaign.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_backfill/DemoBackfillRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoBackfillRequest {
|
||||
/// Endpoint role used for history and transaction requests.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Commitment used by standard Solana HTTP requests.
|
||||
pub(crate) commitment: std::string::String,
|
||||
/// Source mode: explicit_signatures, program, token or pool.
|
||||
pub(crate) mode: std::string::String,
|
||||
/// Newline-separated signatures for explicit mode.
|
||||
pub(crate) signatures_text: std::option::Option<std::string::String>,
|
||||
/// Program, token mint or pool address for address history modes.
|
||||
pub(crate) address: std::option::Option<std::string::String>,
|
||||
/// Optional anchor transaction signature. It is required only for newer-history scans.
|
||||
pub(crate) anchor_signature: std::option::Option<std::string::String>,
|
||||
/// Direction relative to the anchor, or from the latest entry when `before` has no anchor.
|
||||
pub(crate) direction: std::option::Option<std::string::String>,
|
||||
/// Maximum number of address-history signatures to hydrate.
|
||||
pub(crate) limit: u32,
|
||||
/// Maximum RPC page size.
|
||||
pub(crate) page_size: u16,
|
||||
/// Maximum number of history pages inspected.
|
||||
pub(crate) max_pages: u32,
|
||||
/// Operator concurrency cap.
|
||||
pub(crate) max_concurrent_requests: u32,
|
||||
/// Retries after the initial HTTP attempt.
|
||||
pub(crate) max_retries: u32,
|
||||
}
|
||||
|
||||
/// One progress event emitted to the backfill window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_backfill/DemoBackfillProgressPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoBackfillProgressPayload {
|
||||
/// RFC 3339 timestamp.
|
||||
pub(crate) timestamp: std::string::String,
|
||||
/// Stable severity code.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Human-readable message.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Completed candidate count when known.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) completed: std::option::Option<u64>,
|
||||
/// Total candidate count when known.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) total: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
/// Final UI-safe summary for one backfill campaign.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_backfill/DemoBackfillSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoBackfillSummaryPayload {
|
||||
/// Unique capture session identifier.
|
||||
pub(crate) capture_session_id: std::string::String,
|
||||
/// Stable filter code.
|
||||
pub(crate) filter_code: std::string::String,
|
||||
/// Endpoint role.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Provider used for hydration.
|
||||
pub(crate) provider: std::string::String,
|
||||
/// Endpoint code used for hydration.
|
||||
pub(crate) endpoint_code: std::string::String,
|
||||
/// Number of history pages fetched.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) pages_fetched: u64,
|
||||
/// Number of unique candidates selected.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) candidates_selected: u64,
|
||||
/// Number of candidates admitted to the bounded execution queue.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) candidates_started: u64,
|
||||
/// Number of candidates that reached a terminal outcome.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) candidates_completed: u64,
|
||||
/// Number of admitted candidates interrupted before a terminal outcome.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) candidates_cancelled: u64,
|
||||
/// Number of selected candidates never admitted after cancellation.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) candidates_not_started: u64,
|
||||
/// Number of transactions received and normalized.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) transactions_received: u64,
|
||||
/// Number of canonical rows inserted.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) canonical_inserted: u64,
|
||||
/// Number of canonical inserts skipped by idempotence.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) canonical_skipped: u64,
|
||||
/// Number of signatures skipped before hydration because they already existed.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) existing_skipped: u64,
|
||||
/// Number of missing transactions after retries.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) missing: u64,
|
||||
/// Number of failed candidates.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) failed: u64,
|
||||
/// Number of observation rows inserted.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) observations_inserted: u64,
|
||||
/// Number of source attempts.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) attempts: u64,
|
||||
/// Whether the operator cancelled the campaign.
|
||||
pub(crate) cancelled: bool,
|
||||
/// Optional cursor for continuing an older-history scan.
|
||||
pub(crate) resume_before_signature: std::option::Option<std::string::String>,
|
||||
/// Campaign start time.
|
||||
pub(crate) started_at: std::string::String,
|
||||
/// Campaign completion time.
|
||||
pub(crate) finished_at: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) struct DemoBackfillObserver<'a> {
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl kb_pipeline::BackfillObserver for crate::DemoBackfillObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::BackfillProgressEvent) {
|
||||
let payload = crate::DemoBackfillProgressPayload {
|
||||
timestamp: event.timestamp.clone(),
|
||||
level: event.level.code().to_string(),
|
||||
message: event.message.clone(),
|
||||
completed: event.completed,
|
||||
total: event.total,
|
||||
};
|
||||
let emit_result =
|
||||
self.app_handle.emit_to("demo_backfill", "demo-backfill-progress", payload);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
"cannot emit backfill progress: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoBackfillRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl std::ops::Drop for crate::DemoBackfillRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_role_options(
|
||||
snapshots: std::vec::Vec<kb_rpc::HttpPoolClientSnapshot>,
|
||||
) -> std::vec::Vec<DemoBackfillRoleOption> {
|
||||
let mut role_methods = std::collections::BTreeMap::<
|
||||
std::string::String,
|
||||
(
|
||||
std::collections::BTreeSet<std::string::String>,
|
||||
std::collections::BTreeSet<std::string::String>,
|
||||
),
|
||||
>::new();
|
||||
for snapshot in snapshots {
|
||||
for role in snapshot.roles {
|
||||
if !role.enabled {
|
||||
continue;
|
||||
}
|
||||
let entry = role_methods.entry(role.role).or_default();
|
||||
for request_kind in role.request_kinds {
|
||||
entry.0.insert(request_kind);
|
||||
}
|
||||
entry.1.insert(snapshot.provider.clone());
|
||||
}
|
||||
}
|
||||
let mut output = std::vec::Vec::new();
|
||||
for (role, (request_kinds, providers)) in role_methods {
|
||||
let supports_all = request_kinds.contains("*");
|
||||
if !supports_all
|
||||
&& (!request_kinds.contains("get_signatures_for_address")
|
||||
|| !request_kinds.contains("get_transaction"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
output.push(DemoBackfillRoleOption {
|
||||
role,
|
||||
providers: providers.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn build_demo_backfill_pipeline_request(
|
||||
request: DemoBackfillRequest,
|
||||
) -> std::result::Result<kb_pipeline::BackfillRequest, std::string::String> {
|
||||
let source_result = match request.mode.trim() {
|
||||
"explicit_signatures" => explicit_source(request.signatures_text.as_deref()),
|
||||
"program" => address_source(
|
||||
kb_pipeline::BackfillAddressKind::Program,
|
||||
request.address.as_deref(),
|
||||
request.anchor_signature.as_deref(),
|
||||
request.direction.as_deref(),
|
||||
request.limit,
|
||||
),
|
||||
"token" => address_source(
|
||||
kb_pipeline::BackfillAddressKind::Token,
|
||||
request.address.as_deref(),
|
||||
request.anchor_signature.as_deref(),
|
||||
request.direction.as_deref(),
|
||||
request.limit,
|
||||
),
|
||||
"pool" => address_source(
|
||||
kb_pipeline::BackfillAddressKind::Pool,
|
||||
request.address.as_deref(),
|
||||
request.anchor_signature.as_deref(),
|
||||
request.direction.as_deref(),
|
||||
request.limit,
|
||||
),
|
||||
_ => std::result::Result::Err("unsupported backfill mode".to_string()),
|
||||
};
|
||||
let source = match source_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pipeline_request = kb_pipeline::BackfillRequest {
|
||||
role: request.role,
|
||||
commitment: request.commitment,
|
||||
source,
|
||||
page_size: request.page_size,
|
||||
max_pages: request.max_pages,
|
||||
max_concurrent_requests: request.max_concurrent_requests,
|
||||
max_retries: request.max_retries,
|
||||
};
|
||||
let validation_result = pipeline_request.validate();
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
return std::result::Result::Ok(pipeline_request);
|
||||
}
|
||||
|
||||
fn explicit_source(
|
||||
signatures_text: std::option::Option<&str>,
|
||||
) -> std::result::Result<kb_pipeline::BackfillSource, std::string::String> {
|
||||
let text = match signatures_text {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let signatures = text
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|value| return !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if signatures.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
"the signatures textarea must contain at least one signature".to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(kb_pipeline::BackfillSource::ExplicitSignatures(signatures));
|
||||
}
|
||||
|
||||
fn address_source(
|
||||
kind: kb_pipeline::BackfillAddressKind,
|
||||
address: std::option::Option<&str>,
|
||||
anchor_signature: std::option::Option<&str>,
|
||||
direction: std::option::Option<&str>,
|
||||
limit: u32,
|
||||
) -> std::result::Result<kb_pipeline::BackfillSource, std::string::String> {
|
||||
let address_text = match address {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let address_value = address_text.trim();
|
||||
if address_value.is_empty() {
|
||||
return std::result::Result::Err("address is required".to_string());
|
||||
}
|
||||
let direction_text = match direction {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let direction_value = match direction_text.trim() {
|
||||
"before" => kb_pipeline::BackfillDirection::Before,
|
||||
"after" => kb_pipeline::BackfillDirection::After,
|
||||
_ => {
|
||||
return std::result::Result::Err("direction must be before or after".to_string());
|
||||
},
|
||||
};
|
||||
let anchor_value = anchor_signature
|
||||
.map(str::trim)
|
||||
.filter(|value| return !value.is_empty())
|
||||
.map(str::to_string);
|
||||
if direction_value == kb_pipeline::BackfillDirection::After && anchor_value.is_none() {
|
||||
return std::result::Result::Err(
|
||||
"anchor signature is required for newer-history backfill".to_string(),
|
||||
);
|
||||
}
|
||||
let limit_value = match usize::try_from(limit) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("signature limit conversion failed: {error}"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(kb_pipeline::BackfillSource::AddressHistory {
|
||||
kind,
|
||||
address: address_value.to_string(),
|
||||
anchor_signature: anchor_value,
|
||||
direction: direction_value,
|
||||
limit: limit_value,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn demo_backfill_summary_payload(
|
||||
summary: kb_pipeline::BackfillSummary,
|
||||
) -> DemoBackfillSummaryPayload {
|
||||
return crate::DemoBackfillSummaryPayload {
|
||||
capture_session_id: summary.capture_session_id,
|
||||
filter_code: summary.filter_code,
|
||||
role: summary.role,
|
||||
provider: summary.provider,
|
||||
endpoint_code: summary.endpoint_code,
|
||||
pages_fetched: summary.pages_fetched,
|
||||
candidates_selected: summary.candidates_selected,
|
||||
candidates_started: summary.candidates_started,
|
||||
candidates_completed: summary.candidates_completed,
|
||||
candidates_cancelled: summary.candidates_cancelled,
|
||||
candidates_not_started: summary.candidates_not_started,
|
||||
transactions_received: summary.transactions_received,
|
||||
canonical_inserted: summary.canonical_inserted,
|
||||
canonical_skipped: summary.canonical_skipped,
|
||||
existing_skipped: summary.existing_skipped,
|
||||
missing: summary.missing,
|
||||
failed: summary.failed,
|
||||
observations_inserted: summary.observations_inserted,
|
||||
attempts: summary.attempts,
|
||||
cancelled: summary.cancelled,
|
||||
resume_before_signature: summary.resume_before_signature,
|
||||
started_at: summary.started_at,
|
||||
finished_at: summary.finished_at,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn explicit_source_splits_lines_and_ignores_empty_rows() {
|
||||
let result = super::explicit_source(std::option::Option::Some(" first \n\n second\n"));
|
||||
let source = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("source failed: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
source,
|
||||
kb_pipeline::BackfillSource::ExplicitSignatures(std::vec![
|
||||
"first".to_string(),
|
||||
"second".to_string(),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn older_history_accepts_missing_anchor_and_uses_latest_filter() {
|
||||
let source_result = super::address_source(
|
||||
kb_pipeline::BackfillAddressKind::Program,
|
||||
std::option::Option::Some("11111111111111111111111111111111"),
|
||||
std::option::Option::Some(" "),
|
||||
std::option::Option::Some("before"),
|
||||
100,
|
||||
);
|
||||
let source = match source_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("source failed: {error}"),
|
||||
};
|
||||
let request = kb_pipeline::BackfillRequest {
|
||||
role: "history_backfill".to_string(),
|
||||
commitment: "confirmed".to_string(),
|
||||
source,
|
||||
page_size: 100,
|
||||
max_pages: 1,
|
||||
max_concurrent_requests: 1,
|
||||
max_retries: 0,
|
||||
};
|
||||
assert!(request.validate().is_ok());
|
||||
assert_eq!(request.filter_code(), "program_latest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn newer_history_rejects_missing_anchor() {
|
||||
let source_result = super::address_source(
|
||||
kb_pipeline::BackfillAddressKind::Program,
|
||||
std::option::Option::Some("11111111111111111111111111111111"),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some("after"),
|
||||
100,
|
||||
);
|
||||
assert!(source_result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// file: kb_app_demo/src/demo_config.rs
|
||||
// version: 8
|
||||
|
||||
//! Configuration demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Serializable payload shown by the configuration demo page.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_config/DemoConfigPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoConfigPayload {
|
||||
/// Path used to load the configuration file.
|
||||
pub(crate) config_path: std::string::String,
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Active profile environment.
|
||||
pub(crate) environment: std::string::String,
|
||||
/// Entire parsed configuration.
|
||||
pub(crate) app_config: kb_config::AppConfig,
|
||||
/// Active profile configuration.
|
||||
pub(crate) active_profile: kb_config::ProfileConfig,
|
||||
/// Embedded JSON Schema text used during loading.
|
||||
pub(crate) schema_json: std::string::String,
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// file: kb_app_demo/src/demo_core_extraction.rs
|
||||
// version: 9
|
||||
|
||||
//! Tauri commands and UI payloads for canonical transaction to core extraction.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Initial options shown by the core extraction demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoCoreExtractionOptionsPayload {
|
||||
/// Current extractor implementation version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Default maximum selected canonical transactions.
|
||||
pub(crate) default_limit: u32,
|
||||
/// Default maximum concurrent extractions.
|
||||
pub(crate) default_max_concurrent_extractions: u32,
|
||||
/// Whether one extraction campaign is currently running.
|
||||
pub(crate) running: bool,
|
||||
}
|
||||
|
||||
/// UI request for one bounded core extraction campaign.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoCoreExtractionRequest {
|
||||
/// Source mode: signatures, pending or slot_range.
|
||||
pub(crate) mode: std::string::String,
|
||||
/// Newline-separated signatures for exact selection.
|
||||
pub(crate) signatures_text: std::option::Option<std::string::String>,
|
||||
/// Optional program id already present in core instructions.
|
||||
pub(crate) program_id: std::option::Option<std::string::String>,
|
||||
/// Optional inclusive minimum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) min_slot: std::option::Option<u64>,
|
||||
/// Optional inclusive maximum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) max_slot: std::option::Option<u64>,
|
||||
/// Maximum selected canonical transactions.
|
||||
pub(crate) limit: u32,
|
||||
/// Maximum concurrent extraction operations.
|
||||
pub(crate) max_concurrent_extractions: u32,
|
||||
/// Forces replacement of already current core rows.
|
||||
pub(crate) force_replay: bool,
|
||||
}
|
||||
|
||||
/// One progress event emitted to the core extraction window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionProgressPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoCoreExtractionProgressPayload {
|
||||
/// RFC 3339 timestamp.
|
||||
pub(crate) timestamp: std::string::String,
|
||||
/// Stable severity code.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Human-readable message.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Number of terminal candidates.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) completed: u64,
|
||||
/// Total selected candidates.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) total: u64,
|
||||
}
|
||||
|
||||
/// Final UI-safe summary for one extraction campaign.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_core_extraction/DemoCoreExtractionSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoCoreExtractionSummaryPayload {
|
||||
/// Current extractor implementation version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Number of selected canonical transactions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) selected: u64,
|
||||
/// Number admitted to the bounded execution queue.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) started: u64,
|
||||
/// Number reaching a terminal result.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) completed: u64,
|
||||
/// Number skipped by version/hash idempotence.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) skipped: u64,
|
||||
/// Number extracted and committed.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) extracted: u64,
|
||||
/// Number failed.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) failed: u64,
|
||||
/// Number admitted but cancelled before a terminal result.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) cancelled_candidates: u64,
|
||||
/// Number selected but not started after cancellation.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) not_started: u64,
|
||||
/// Whether the campaign was cancelled.
|
||||
pub(crate) cancelled: bool,
|
||||
/// Campaign start timestamp.
|
||||
pub(crate) started_at: std::string::String,
|
||||
/// Campaign finish timestamp.
|
||||
pub(crate) finished_at: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) struct DemoCoreExtractionObserver<'a> {
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl kb_pipeline::CoreExtractionObserver for crate::DemoCoreExtractionObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::CoreExtractionProgressEvent) {
|
||||
let payload = crate::DemoCoreExtractionProgressPayload {
|
||||
timestamp: event.timestamp.clone(),
|
||||
level: event.level.code().to_string(),
|
||||
message: event.message.clone(),
|
||||
completed: event.completed,
|
||||
total: event.total,
|
||||
};
|
||||
let emit_result = self.app_handle.emit_to(
|
||||
"demo_core_extraction",
|
||||
"demo-core-extraction-progress",
|
||||
payload,
|
||||
);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
"cannot emit core extraction progress: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoCoreExtractionRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl std::ops::Drop for crate::DemoCoreExtractionRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_demo_core_extraction_pipeline_request(
|
||||
request: crate::DemoCoreExtractionRequest,
|
||||
) -> std::result::Result<kb_pipeline::CoreExtractionRequest, std::string::String> {
|
||||
let source_result = match request.mode.trim() {
|
||||
"signatures" => {
|
||||
let signatures = split_signatures(request.signatures_text.as_deref());
|
||||
std::result::Result::Ok(kb_pipeline::CoreExtractionSource::Signatures(signatures))
|
||||
},
|
||||
"pending" => std::result::Result::Ok(kb_pipeline::CoreExtractionSource::Pending),
|
||||
"program_id" => {
|
||||
let program_id = match request.program_id {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
"program id is required for program extraction".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
std::result::Result::Ok(kb_pipeline::CoreExtractionSource::ProgramId { program_id })
|
||||
},
|
||||
"slot_range" => {
|
||||
let min_slot = match request.min_slot {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"minimum slot is required for slot range extraction".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let max_slot = match request.max_slot {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"maximum slot is required for slot range extraction".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
std::result::Result::Ok(kb_pipeline::CoreExtractionSource::SlotRange {
|
||||
min_slot,
|
||||
max_slot,
|
||||
})
|
||||
},
|
||||
_ => std::result::Result::Err("unsupported core extraction mode".to_string()),
|
||||
};
|
||||
let source = match source_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pipeline_request = kb_pipeline::CoreExtractionRequest {
|
||||
source,
|
||||
limit: request.limit,
|
||||
max_concurrent_extractions: request.max_concurrent_extractions,
|
||||
force_replay: request.force_replay,
|
||||
};
|
||||
let validation_result = pipeline_request.validate();
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
return std::result::Result::Ok(pipeline_request);
|
||||
}
|
||||
|
||||
fn split_signatures(text: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
|
||||
let source = match text {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let mut unique = std::collections::BTreeSet::<std::string::String>::new();
|
||||
let mut output = std::vec::Vec::new();
|
||||
for line in source.lines() {
|
||||
let signature = line.trim();
|
||||
if signature.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if unique.insert(signature.to_string()) {
|
||||
output.push(signature.to_string());
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn demo_core_extraction_summary_payload(
|
||||
summary: kb_pipeline::CoreExtractionSummary,
|
||||
) -> crate::DemoCoreExtractionSummaryPayload {
|
||||
return crate::DemoCoreExtractionSummaryPayload {
|
||||
processor_version: summary.processor_version,
|
||||
selected: summary.selected,
|
||||
started: summary.started,
|
||||
completed: summary.completed,
|
||||
skipped: summary.skipped,
|
||||
extracted: summary.extracted,
|
||||
failed: summary.failed,
|
||||
cancelled_candidates: summary.cancelled_candidates,
|
||||
not_started: summary.not_started,
|
||||
cancelled: summary.cancelled,
|
||||
started_at: summary.started_at,
|
||||
finished_at: summary.finished_at,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn signature_source_splits_deduplicates_and_ignores_empty_rows() {
|
||||
let values = super::split_signatures(std::option::Option::Some("alpha\n\n beta \nalpha\n"));
|
||||
assert_eq!(values, std::vec!["alpha".to_string(), "beta".to_string()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,953 @@
|
||||
// file: kb_app_demo/src/demo_decode_replay.rs
|
||||
// version: 24
|
||||
|
||||
//! Tauri commands and UI payloads for contextual instruction decode replay.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One decoder selectable by the decode replay demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayDecoderOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeReplayDecoderOption {
|
||||
/// Stable decoder name.
|
||||
pub(crate) name: std::string::String,
|
||||
/// Stable decoder version.
|
||||
pub(crate) version: std::string::String,
|
||||
/// Exact supported program identifiers.
|
||||
pub(crate) program_ids: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Initial options shown by the contextual decode replay demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeReplayOptionsPayload {
|
||||
/// Common orchestration version.
|
||||
pub(crate) pipeline_version: std::string::String,
|
||||
/// Available contextual decoders.
|
||||
pub(crate) decoders: std::vec::Vec<crate::DemoDecodeReplayDecoderOption>,
|
||||
/// Stable names of materializers currently registered by the demo.
|
||||
pub(crate) materializer_names: std::vec::Vec<std::string::String>,
|
||||
/// Default bounded selection limit.
|
||||
pub(crate) default_limit: u32,
|
||||
/// Default maximum concurrent contextual inputs.
|
||||
pub(crate) default_max_concurrent_inputs: u32,
|
||||
/// Whether one replay campaign is running.
|
||||
pub(crate) running: bool,
|
||||
}
|
||||
|
||||
/// UI request for one bounded contextual decode replay campaign.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeReplayRequest {
|
||||
/// Newline-separated exact transaction signatures.
|
||||
pub(crate) signatures_text: std::option::Option<std::string::String>,
|
||||
/// Optional exact program identifier.
|
||||
pub(crate) program_id: std::option::Option<std::string::String>,
|
||||
/// Instruction processing state code, actionable or incomplete_signatures.
|
||||
pub(crate) instruction_state: std::string::String,
|
||||
/// Newline-separated exact stable instruction paths.
|
||||
pub(crate) instruction_paths_text: std::option::Option<std::string::String>,
|
||||
/// Explicit selected decoder names.
|
||||
pub(crate) decoder_names: std::vec::Vec<std::string::String>,
|
||||
/// Maximum selected contextual inputs.
|
||||
pub(crate) limit: u32,
|
||||
/// Maximum concurrent contextual inputs.
|
||||
pub(crate) max_concurrent_inputs: u32,
|
||||
/// Whether every compatible decoder must run.
|
||||
pub(crate) all_compatible: bool,
|
||||
/// Replaces only processor-owned outputs for the same version and input.
|
||||
pub(crate) force_replay: bool,
|
||||
/// Explicitly authorizes a bounded force replay without exact signatures.
|
||||
pub(crate) force_replay_all_matching: bool,
|
||||
/// Runs compatible materializers after decoded event persistence.
|
||||
pub(crate) materialize_after_decode: bool,
|
||||
}
|
||||
|
||||
/// One progress event emitted to the decode replay window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplayProgressPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeReplayProgressPayload {
|
||||
/// Stable process-local campaign identifier.
|
||||
pub(crate) campaign_id: std::string::String,
|
||||
/// RFC 3339 timestamp.
|
||||
pub(crate) timestamp: std::string::String,
|
||||
/// Stable severity code.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Human-readable message.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Number of terminal contextual inputs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) completed: u64,
|
||||
/// Total selected contextual inputs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) total: u64,
|
||||
}
|
||||
|
||||
/// One processor counter row returned to the UI.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeProcessorSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeProcessorSummaryPayload {
|
||||
/// Stable processor name.
|
||||
pub(crate) processor_name: std::string::String,
|
||||
/// Stable processor version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Compatible dispatch count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) dispatched: u64,
|
||||
/// Version and hash skip count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) skipped: u64,
|
||||
/// Decoded input count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) decoded: u64,
|
||||
/// Ignored input count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) ignored: u64,
|
||||
/// Unsupported input count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) unsupported: u64,
|
||||
/// Failed input count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) failed: u64,
|
||||
/// Materialized output count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) materialized_outputs: u64,
|
||||
/// Materialization refusal count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) materialization_refused: u64,
|
||||
}
|
||||
|
||||
/// Final UI-safe summary for one contextual decode replay campaign.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeReplaySummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeReplaySummaryPayload {
|
||||
/// Stable process-local campaign identifier.
|
||||
pub(crate) campaign_id: std::string::String,
|
||||
/// Common pipeline version.
|
||||
pub(crate) pipeline_version: std::string::String,
|
||||
/// Number of selected contextual inputs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) selected: u64,
|
||||
/// Number admitted to execution.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) started: u64,
|
||||
/// Number reaching a terminal state.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) completed: u64,
|
||||
/// Number with no compatible enabled decoder.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) unmatched: u64,
|
||||
/// Number never started after cancellation.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) not_started: u64,
|
||||
/// Number of failed contextual inputs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) failed_inputs: u64,
|
||||
/// Whether cancellation was observed.
|
||||
pub(crate) cancelled: bool,
|
||||
/// Per-processor counters.
|
||||
pub(crate) processors: std::vec::Vec<crate::DemoDecodeProcessorSummaryPayload>,
|
||||
/// Campaign start timestamp.
|
||||
pub(crate) started_at: std::string::String,
|
||||
/// Campaign finish timestamp.
|
||||
pub(crate) finished_at: std::string::String,
|
||||
}
|
||||
|
||||
/// One aggregated coverage row returned to the UI.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeCoverageSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeCoverageSummaryPayload {
|
||||
/// Stable processor name.
|
||||
pub(crate) processor_name: std::string::String,
|
||||
/// Stable processor version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Exact program identifier.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Optional stable surface code.
|
||||
pub(crate) surface_code: std::option::Option<std::string::String>,
|
||||
/// Stable entry classifier.
|
||||
pub(crate) entry_code: std::string::String,
|
||||
/// Declared count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) declared_count: i64,
|
||||
/// Observed count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) observed_count: i64,
|
||||
/// Recognized count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) recognized_count: i64,
|
||||
/// Decoded observation count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) decoded_count: i64,
|
||||
/// Materialized output count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) materialized_count: i64,
|
||||
/// Error count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) error_count: i64,
|
||||
/// Unknown or unsupported observation count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) unknown_count: i64,
|
||||
/// Successful source transaction count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) successful_transaction_count: i64,
|
||||
/// Failed source transaction count.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) failed_transaction_count: i64,
|
||||
}
|
||||
|
||||
/// Read-only decode, ledger and coverage diagnostics.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoDecodeDiagnosticsPayload {
|
||||
/// Decode, materialization and ledger table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
/// Aggregated declared and observed coverage.
|
||||
pub(crate) coverage: std::vec::Vec<crate::DemoDecodeCoverageSummaryPayload>,
|
||||
}
|
||||
|
||||
/// Bounded read request for committed transaction annotations.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoTransactionAnnotationRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoTransactionAnnotationRequest {
|
||||
/// Optional partial transaction signature.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// UI-safe committed transaction annotation row.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_decode_replay/DemoTransactionAnnotationRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoTransactionAnnotationRow {
|
||||
/// Materializer processor version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Source decoder and version.
|
||||
pub(crate) decoder: std::string::String,
|
||||
/// Transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Decimal slot rendered as text to preserve JSON precision.
|
||||
pub(crate) slot: std::string::String,
|
||||
/// Stable outer or inner instruction path.
|
||||
pub(crate) instruction_path: std::string::String,
|
||||
/// Exact Memo generation.
|
||||
pub(crate) generation: std::string::String,
|
||||
/// Exact Memo Program ID.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Complete bounded UTF-8 Memo text.
|
||||
pub(crate) text: std::string::String,
|
||||
/// Exact payload byte length.
|
||||
pub(crate) payload_length_bytes: u32,
|
||||
/// Canonical payload SHA-256.
|
||||
pub(crate) payload_sha256: std::string::String,
|
||||
/// Runtime-verified ordered signer keys.
|
||||
pub(crate) verified_signers: std::vec::Vec<std::string::String>,
|
||||
/// Stable materializer idempotence key.
|
||||
pub(crate) idempotence_key: std::string::String,
|
||||
/// Database creation timestamp.
|
||||
pub(crate) created_at: std::string::String,
|
||||
/// Database replacement timestamp.
|
||||
pub(crate) updated_at: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) struct DemoDecodeReplayObserver<'a> {
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
pub(crate) campaign_id: std::string::String,
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl kb_pipeline::DecodeReplayObserver for crate::DemoDecodeReplayObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::DecodeReplayProgressEvent) {
|
||||
let payload = DemoDecodeReplayProgressPayload {
|
||||
campaign_id: self.campaign_id.clone(),
|
||||
timestamp: event.timestamp.clone(),
|
||||
level: event.level.code().to_string(),
|
||||
message: event.message.clone(),
|
||||
completed: event.completed,
|
||||
total: event.total,
|
||||
};
|
||||
let emit_result =
|
||||
self.app_handle
|
||||
.emit_to("demo_decode_replay", "demo-decode-replay-progress", payload);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(target: crate::TRACING_TARGET, action = "emit_progress", campaign_id = %self.campaign_id, error = %error, "cannot emit decode replay progress");
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoDecodeReplayRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
pub(crate) campaign_id: &'a std::sync::Mutex<std::option::Option<std::string::String>>,
|
||||
}
|
||||
|
||||
impl std::ops::Drop for crate::DemoDecodeReplayRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
let lock_result = self.campaign_id.lock();
|
||||
if let std::result::Result::Ok(mut active_campaign_id) = lock_result {
|
||||
*active_campaign_id = std::option::Option::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_active_campaign(
|
||||
campaign_slot: &std::sync::Mutex<std::option::Option<std::string::String>>,
|
||||
campaign_id: &str,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
let lock_result = campaign_slot.lock();
|
||||
let mut active_campaign_id = match lock_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(
|
||||
"cannot register active contextual decode campaign".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
*active_campaign_id = std::option::Option::Some(campaign_id.to_string());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
pub(crate) fn available_materializers()
|
||||
-> std::vec::Vec<std::sync::Arc<dyn kb_materializer_api::EventMaterializer>> {
|
||||
return std::vec![
|
||||
std::sync::Arc::new(kb_materializer_admin::AdminMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_compliance_audit::ComplianceAuditMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_fees::FeesMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_lifecycle::LifecycleMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_staking::StakingMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_token_accounts::TokenAccountsMaterializer),
|
||||
std::sync::Arc::new(kb_materializer_risk::RiskMaterializer),
|
||||
std::sync::Arc::new(
|
||||
kb_materializer_transaction_annotations::TransactionAnnotationMaterializer,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
pub(crate) fn available_decoders()
|
||||
-> std::vec::Vec<std::sync::Arc<dyn kb_decoder_api::InstructionDecoder>> {
|
||||
return std::vec![
|
||||
std::sync::Arc::new(kb_decoder_solana_core::SolanaCoreDecoder),
|
||||
std::sync::Arc::new(
|
||||
kb_decoder_spl_associated_token_account::SplAssociatedTokenAccountDecoder,
|
||||
),
|
||||
std::sync::Arc::new(kb_decoder_spl_elgamal_registry::SplElgamalRegistryDecoder),
|
||||
std::sync::Arc::new(kb_decoder_spl_memo::SplMemoDecoder),
|
||||
std::sync::Arc::new(kb_decoder_spl_token::SplTokenDecoder),
|
||||
std::sync::Arc::new(kb_decoder_spl_token_2022::SplToken2022Decoder),
|
||||
];
|
||||
}
|
||||
|
||||
pub(crate) fn build_demo_decode_replay_pipeline_request(
|
||||
request: crate::DemoDecodeReplayRequest,
|
||||
campaign_id: std::string::String,
|
||||
) -> std::result::Result<kb_pipeline::DecodeReplayRequest, std::string::String> {
|
||||
let states_result = processing_states(request.instruction_state.as_str());
|
||||
let states = match states_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let signatures = split_lines(request.signatures_text.as_deref());
|
||||
let instruction_paths = split_lines(request.instruction_paths_text.as_deref());
|
||||
let incomplete_signatures = request.instruction_state.trim() == "incomplete_signatures";
|
||||
let program_ids = match request.program_id {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => {
|
||||
std::vec![value.trim().to_string()]
|
||||
},
|
||||
_ => std::vec::Vec::new(),
|
||||
};
|
||||
let selection_result = kb_store_core::DecodeSelectionFilter::new(
|
||||
signatures,
|
||||
states,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
program_ids,
|
||||
instruction_paths,
|
||||
incomplete_signatures,
|
||||
request.limit,
|
||||
);
|
||||
let selection = match selection_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let dispatch_policy = if request.all_compatible {
|
||||
kb_pipeline::DecodeDispatchPolicy::AllCompatible
|
||||
} else {
|
||||
kb_pipeline::DecodeDispatchPolicy::HighestPriority
|
||||
};
|
||||
let pipeline_request = kb_pipeline::DecodeReplayRequest {
|
||||
campaign_id,
|
||||
selection,
|
||||
decoder_names: request.decoder_names,
|
||||
dispatch_policy,
|
||||
max_concurrent_inputs: request.max_concurrent_inputs,
|
||||
force_replay: request.force_replay,
|
||||
force_replay_all_matching: request.force_replay_all_matching,
|
||||
materialize_after_decode: request.materialize_after_decode,
|
||||
};
|
||||
let validation_result = pipeline_request.validate();
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "build_request",
|
||||
campaign_id = %pipeline_request.campaign_id,
|
||||
signature_count = pipeline_request.selection.signatures.len(),
|
||||
signature_sample = ?text_sample(pipeline_request.selection.signatures.as_slice(), 5),
|
||||
processing_states = ?pipeline_request.selection.processing_states,
|
||||
program_ids = ?pipeline_request.selection.program_ids,
|
||||
instruction_paths = ?pipeline_request.selection.instruction_paths,
|
||||
incomplete_signatures = pipeline_request.selection.incomplete_signatures,
|
||||
limit = pipeline_request.selection.limit,
|
||||
decoder_names = ?pipeline_request.decoder_names,
|
||||
dispatch_policy = ?pipeline_request.dispatch_policy,
|
||||
max_concurrent_inputs = pipeline_request.max_concurrent_inputs,
|
||||
force_replay = pipeline_request.force_replay,
|
||||
force_replay_all_matching = pipeline_request.force_replay_all_matching,
|
||||
materialize_after_decode = pipeline_request.materialize_after_decode,
|
||||
"built normalized contextual decode replay request"
|
||||
);
|
||||
return std::result::Result::Ok(pipeline_request);
|
||||
}
|
||||
|
||||
pub(crate) fn optional_line_count(value: &std::option::Option<std::string::String>) -> usize {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => {
|
||||
text.lines().filter(|line| return !line.trim().is_empty()).count()
|
||||
},
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn text_sample(values: &[std::string::String], limit: usize) -> std::vec::Vec<&str> {
|
||||
return values.iter().take(limit).map(std::string::String::as_str).collect();
|
||||
}
|
||||
|
||||
fn processing_states(
|
||||
value: &str,
|
||||
) -> std::result::Result<
|
||||
std::vec::Vec<kb_store_core::CoreInstructionProcessingState>,
|
||||
std::string::String,
|
||||
> {
|
||||
return match value.trim() {
|
||||
"incomplete_signatures" | "actionable" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Pending,
|
||||
kb_store_core::CoreInstructionProcessingState::Failed,
|
||||
kb_store_core::CoreInstructionProcessingState::ReplayRequested,
|
||||
]),
|
||||
"pending" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Pending
|
||||
]),
|
||||
"failed" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Failed
|
||||
]),
|
||||
"replay_requested" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::ReplayRequested
|
||||
]),
|
||||
"decoded" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Decoded
|
||||
]),
|
||||
"ignored" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Ignored
|
||||
]),
|
||||
"materialized" => std::result::Result::Ok(std::vec![
|
||||
kb_store_core::CoreInstructionProcessingState::Materialized
|
||||
]),
|
||||
_ => std::result::Result::Err("unsupported instruction processing state".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
fn split_lines(text: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
|
||||
let source = match text {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
let mut unique = std::collections::BTreeSet::<std::string::String>::new();
|
||||
let mut output = std::vec::Vec::new();
|
||||
for line in source.lines() {
|
||||
let value = line.trim();
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if unique.insert(value.to_string()) {
|
||||
output.push(value.to_string());
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn demo_decode_replay_summary_payload(
|
||||
summary: kb_pipeline::DecodeReplaySummary,
|
||||
) -> crate::DemoDecodeReplaySummaryPayload {
|
||||
return crate::DemoDecodeReplaySummaryPayload {
|
||||
campaign_id: summary.campaign_id,
|
||||
pipeline_version: summary.pipeline_version,
|
||||
selected: summary.selected,
|
||||
started: summary.started,
|
||||
completed: summary.completed,
|
||||
unmatched: summary.unmatched,
|
||||
not_started: summary.not_started,
|
||||
failed_inputs: summary.failed_inputs,
|
||||
cancelled: summary.cancelled,
|
||||
processors: summary
|
||||
.processors
|
||||
.into_iter()
|
||||
.map(|processor| {
|
||||
return crate::DemoDecodeProcessorSummaryPayload {
|
||||
processor_name: processor.processor_name,
|
||||
processor_version: processor.processor_version,
|
||||
dispatched: processor.dispatched,
|
||||
skipped: processor.skipped,
|
||||
decoded: processor.decoded,
|
||||
ignored: processor.ignored,
|
||||
unsupported: processor.unsupported,
|
||||
failed: processor.failed,
|
||||
materialized_outputs: processor.materialized_outputs,
|
||||
materialization_refused: processor.materialization_refused,
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
started_at: summary.started_at,
|
||||
finished_at: summary.finished_at,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn coverage_payload(
|
||||
value: kb_store_core::DecodeCoverageSummaryRow,
|
||||
) -> crate::DemoDecodeCoverageSummaryPayload {
|
||||
return crate::DemoDecodeCoverageSummaryPayload {
|
||||
processor_name: value.processor_name,
|
||||
processor_version: value.processor_version,
|
||||
program_id: value.program_id,
|
||||
surface_code: value.surface_code,
|
||||
entry_code: value.entry_code,
|
||||
declared_count: value.declared_count,
|
||||
observed_count: value.observed_count,
|
||||
recognized_count: value.recognized_count,
|
||||
decoded_count: value.decoded_count,
|
||||
materialized_count: value.materialized_count,
|
||||
error_count: value.error_count,
|
||||
unknown_count: value.unknown_count,
|
||||
successful_transaction_count: value.successful_transaction_count,
|
||||
failed_transaction_count: value.failed_transaction_count,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn annotation_payload(
|
||||
row: kb_store_core::MaterializedEventQueryRow,
|
||||
) -> std::result::Result<crate::DemoTransactionAnnotationRow, std::string::String> {
|
||||
if row.processor_name != "transaction_annotations"
|
||||
|| row.materialized_family != "transaction_annotation"
|
||||
{
|
||||
return std::result::Result::Err(
|
||||
"materialized row is not a transaction annotation".to_string(),
|
||||
);
|
||||
}
|
||||
let payload = &row.payload_json;
|
||||
let byte_length = match payload.get("payloadLengthBytes").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"transaction annotation payloadLengthBytes is absent".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let byte_length_result = u32::try_from(byte_length);
|
||||
let payload_length_bytes = match byte_length_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"transaction annotation payloadLengthBytes is not UI-safe: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
let signers = match payload.get("signers") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"transaction annotation signers are absent".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let verified_values = match signers.get("verified").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"transaction annotation verified signers are invalid".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut verified_signers = std::vec::Vec::with_capacity(verified_values.len());
|
||||
for value in verified_values {
|
||||
match value.as_str() {
|
||||
std::option::Option::Some(signer) if !signer.trim().is_empty() => {
|
||||
verified_signers.push(signer.to_string());
|
||||
},
|
||||
_ => {
|
||||
return std::result::Result::Err(
|
||||
"transaction annotation verified signer is invalid".to_string(),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
let instruction_path = match required_annotation_text(payload, "instructionPath") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let generation = match required_annotation_text(payload, "generation") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let program_id = match required_annotation_text(payload, "programId") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let text = match required_annotation_text(payload, "text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payload_sha256 = match required_annotation_text(payload, "payloadSha256") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let idempotence_key = match required_annotation_text(payload, "idempotenceKey") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::DemoTransactionAnnotationRow {
|
||||
processor_version: row.processor_version,
|
||||
decoder: format!("{}@{}", row.source_decoder_name, row.source_decoder_version),
|
||||
signature: row.signature,
|
||||
slot: row.slot.to_string(),
|
||||
instruction_path,
|
||||
generation,
|
||||
program_id,
|
||||
text,
|
||||
payload_length_bytes,
|
||||
payload_sha256,
|
||||
verified_signers,
|
||||
idempotence_key,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
fn required_annotation_text(
|
||||
payload: &serde_json::Value,
|
||||
field: &str,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
return match payload.get(field).and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
|
||||
std::option::Option::None => {
|
||||
std::result::Result::Err(format!("transaction annotation {field} is absent"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn ata_observation(entry: &str) -> kb_decoder_api::DecodedObservation {
|
||||
return kb_decoder_api::DecodedObservation {
|
||||
event_key: format!("ata:{entry}:0"),
|
||||
event: kb_model::DecodedProtocolEvent {
|
||||
signature: kb_model::Signature("signature".to_string()),
|
||||
slot: kb_model::Slot(1),
|
||||
instruction_path: kb_model::InstructionPath("0".to_string()),
|
||||
program_id: kb_model::ProgramId(
|
||||
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
||||
),
|
||||
protocol_code: kb_model::ProtocolCode("spl_associated_token_account".to_string()),
|
||||
surface_code: kb_model::SurfaceCode("spl_associated_token_account".to_string()),
|
||||
event_code: kb_model::EventCode(format!("spl_associated_token_account.{entry}")),
|
||||
event_name: kb_model::EventName(entry.to_string()),
|
||||
event_family: kb_model::EventFamily::Lifecycle,
|
||||
source_kind: kb_model::EventSourceKind::Instruction,
|
||||
confidence: kb_model::DecoderConfidence::ManualExact,
|
||||
},
|
||||
payload_json: serde_json::json!({}),
|
||||
transaction_failed: false,
|
||||
transaction_error: std::option::Option::None,
|
||||
observation_committed: true,
|
||||
proof: kb_decoder_api::DecoderProof {
|
||||
kind: kb_decoder_api::DecoderProofKind::Manual,
|
||||
confidence: kb_model::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec!["fixture".to_string()],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_memo_token_2022_elgamal_classic_token_and_ata_decoders_are_registered() {
|
||||
let decoders = crate::available_decoders();
|
||||
assert_eq!(decoders.len(), 6);
|
||||
let names = decoders
|
||||
.iter()
|
||||
.map(|decoder| return decoder.identity().name)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(
|
||||
names,
|
||||
std::vec![
|
||||
"solana_native_classifier".to_string(),
|
||||
"spl_associated_token_account".to_string(),
|
||||
"spl_elgamal_registry".to_string(),
|
||||
"spl_memo".to_string(),
|
||||
"spl_token".to_string(),
|
||||
"spl_token_2022".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_materializer_registry_is_complete_for_current_instructional_surfaces() {
|
||||
let materializers = crate::available_materializers();
|
||||
assert_eq!(materializers.len(), 8);
|
||||
let names = materializers
|
||||
.iter()
|
||||
.map(|materializer| return materializer.identity().name)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(
|
||||
names,
|
||||
std::vec![
|
||||
"solana_native_admin".to_string(),
|
||||
"solana_native_compliance_audit".to_string(),
|
||||
"fees".to_string(),
|
||||
"solana_native_lifecycle".to_string(),
|
||||
"solana_native_staking".to_string(),
|
||||
"spl_token_accounts".to_string(),
|
||||
"spl_token_risk".to_string(),
|
||||
"transaction_annotations".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ata_materializer_ownership_is_exact_in_the_runtime_registry() {
|
||||
for (entry, expected) in [
|
||||
("create", std::vec!["spl_token_accounts".to_string()]),
|
||||
(
|
||||
"recover_nested",
|
||||
std::vec!["spl_token_accounts".to_string(), "spl_token_risk".to_string(),],
|
||||
),
|
||||
] {
|
||||
let observation = ata_observation(entry);
|
||||
let owners = crate::available_materializers()
|
||||
.iter()
|
||||
.filter(|materializer| return materializer.accepts_observation(&observation))
|
||||
.map(|materializer| return materializer.identity().name)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(owners, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_selection_is_trimmed_and_deduplicated() {
|
||||
let values = super::split_lines(std::option::Option::Some(" a \n\na\nb\n"));
|
||||
assert_eq!(values, std::vec!["a".to_string(), "b".to_string()]);
|
||||
}
|
||||
|
||||
fn request(
|
||||
signatures_text: std::option::Option<&str>,
|
||||
force_replay: bool,
|
||||
force_replay_all_matching: bool,
|
||||
materialize_after_decode: bool,
|
||||
) -> crate::DemoDecodeReplayRequest {
|
||||
return crate::DemoDecodeReplayRequest {
|
||||
signatures_text: signatures_text.map(|value| return value.to_string()),
|
||||
program_id: std::option::Option::None,
|
||||
instruction_state: "actionable".to_string(),
|
||||
instruction_paths_text: std::option::Option::None,
|
||||
decoder_names: std::vec!["solana_native_classifier".to_string()],
|
||||
limit: 10,
|
||||
max_concurrent_inputs: 2,
|
||||
all_compatible: false,
|
||||
force_replay,
|
||||
force_replay_all_matching,
|
||||
materialize_after_decode,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_replay_requires_signatures_or_all_matching_authorization() {
|
||||
let result = crate::build_demo_decode_replay_pipeline_request(
|
||||
request(std::option::Option::None, true, false, false),
|
||||
"decode-test".to_string(),
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_replay_all_matching_is_forwarded() {
|
||||
let result = crate::build_demo_decode_replay_pipeline_request(
|
||||
request(std::option::Option::None, true, true, false),
|
||||
"decode-test".to_string(),
|
||||
);
|
||||
let request = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("request mapping failed: {error}"),
|
||||
};
|
||||
assert!(request.force_replay_all_matching);
|
||||
assert!(request.selection.signatures.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_guard_clears_active_campaign_state() {
|
||||
let running = std::sync::atomic::AtomicBool::new(true);
|
||||
let campaign_id =
|
||||
std::sync::Mutex::new(std::option::Option::Some("decode-test".to_string()));
|
||||
{
|
||||
let _guard = crate::DemoDecodeReplayRunGuard {
|
||||
running: &running,
|
||||
campaign_id: &campaign_id,
|
||||
};
|
||||
}
|
||||
assert!(!running.load(std::sync::atomic::Ordering::Acquire));
|
||||
let lock_result = campaign_id.lock();
|
||||
let active_campaign_id = match lock_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => panic!("campaign id mutex must not be poisoned"),
|
||||
};
|
||||
assert!(active_campaign_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_active_campaign_sets_slot_without_exposing_guard() {
|
||||
let campaign_id = std::sync::Mutex::new(std::option::Option::None);
|
||||
let result = crate::register_active_campaign(&campaign_id, "decode-test");
|
||||
assert!(result.is_ok());
|
||||
let lock_result = campaign_id.lock();
|
||||
let active_campaign_id = match lock_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => panic!("campaign id mutex must not be poisoned"),
|
||||
};
|
||||
assert_eq!(active_campaign_id.as_deref(), std::option::Option::Some("decode-test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_signature_state_uses_actionable_states_and_expansion() {
|
||||
let mut request = request(std::option::Option::None, false, false, true);
|
||||
request.instruction_state = "incomplete_signatures".to_string();
|
||||
let result = crate::build_demo_decode_replay_pipeline_request(
|
||||
request,
|
||||
"decode-incomplete".to_string(),
|
||||
);
|
||||
let pipeline_request = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("request mapping failed: {error}"),
|
||||
};
|
||||
assert!(pipeline_request.selection.incomplete_signatures);
|
||||
assert_eq!(pipeline_request.selection.processing_states.len(), 3);
|
||||
assert!(!pipeline_request.force_replay);
|
||||
assert!(pipeline_request.materialize_after_decode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actionable_state_contains_pending_failed_and_replay_requested() {
|
||||
let result = super::processing_states("actionable");
|
||||
let states = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("state mapping failed: {error}"),
|
||||
};
|
||||
assert_eq!(states.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_annotation_row_is_mapped_to_ui_safe_contract() {
|
||||
let row = kb_store_core::MaterializedEventQueryRow {
|
||||
processor_name: "transaction_annotations".to_string(),
|
||||
processor_version: "0.4.3".to_string(),
|
||||
input_key: "input".to_string(),
|
||||
output_key: "output".to_string(),
|
||||
source_event_key: "memo:0".to_string(),
|
||||
source_decoder_name: "spl_memo".to_string(),
|
||||
source_decoder_version: "0.4.3".to_string(),
|
||||
signature: "signature".to_string(),
|
||||
slot: u64::MAX,
|
||||
materialized_family: "transaction_annotation".to_string(),
|
||||
payload_json: serde_json::json!({
|
||||
"instructionPath": "1/0",
|
||||
"generation": "v4",
|
||||
"programId": kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
"text": "annotation",
|
||||
"payloadLengthBytes": 10,
|
||||
"payloadSha256": "11",
|
||||
"signers": {"verified": ["signer"]},
|
||||
"idempotenceKey": "stable"
|
||||
}),
|
||||
created_at: "created".to_string(),
|
||||
updated_at: "updated".to_string(),
|
||||
};
|
||||
let result = crate::annotation_payload(row);
|
||||
let mapped = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("annotation mapping failed: {error}"),
|
||||
};
|
||||
assert_eq!(mapped.slot, u64::MAX.to_string());
|
||||
assert_eq!(mapped.text, "annotation");
|
||||
assert_eq!(mapped.verified_signers, std::vec!["signer".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_annotation_payload_fails_closed() {
|
||||
let row = kb_store_core::MaterializedEventQueryRow {
|
||||
processor_name: "transaction_annotations".to_string(),
|
||||
processor_version: "0.4.3".to_string(),
|
||||
input_key: "input".to_string(),
|
||||
output_key: "output".to_string(),
|
||||
source_event_key: "memo:0".to_string(),
|
||||
source_decoder_name: "spl_memo".to_string(),
|
||||
source_decoder_version: "0.4.3".to_string(),
|
||||
signature: "signature".to_string(),
|
||||
slot: 1,
|
||||
materialized_family: "transaction_annotation".to_string(),
|
||||
payload_json: serde_json::json!({}),
|
||||
created_at: "created".to_string(),
|
||||
updated_at: "updated".to_string(),
|
||||
};
|
||||
assert!(crate::annotation_payload(row).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
// file: kb_app_demo/src/demo_execution_solana_core.rs
|
||||
// version: 5
|
||||
|
||||
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One Devnet execution profile exposed to the demo window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProfileOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreProfileOption {
|
||||
/// Stable profile name.
|
||||
pub(crate) name: std::string::String,
|
||||
/// Non-secret temporary wallet alias.
|
||||
pub(crate) wallet_alias: std::string::String,
|
||||
/// Maximum Devnet spend allowed by the profile.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_spend_lamports: u64,
|
||||
/// Maximum faucet request allowed by the profile.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_airdrop_lamports: u64,
|
||||
/// Whether signed Devnet submission is enabled.
|
||||
pub(crate) send_enabled: bool,
|
||||
/// Whether explicit operator confirmation is required.
|
||||
pub(crate) require_operator_confirmation: bool,
|
||||
}
|
||||
|
||||
/// Initial options shown by the Solana Core execution demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreOptionsPayload {
|
||||
/// Devnet profiles compatible with the execution laboratory.
|
||||
pub(crate) profiles: std::vec::Vec<crate::DemoExecutionSolanaCoreProfileOption>,
|
||||
/// Suggested profile name.
|
||||
pub(crate) default_profile_name: std::option::Option<std::string::String>,
|
||||
/// Default transfer amount suitable for a new zero-data account on Devnet.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) default_transfer_lamports: u64,
|
||||
/// Whether one execution is currently running.
|
||||
pub(crate) running: bool,
|
||||
}
|
||||
|
||||
/// Request sent by the execution demo.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Recipient public key.
|
||||
pub(crate) recipient: std::string::String,
|
||||
/// Lamports transferred by the System instruction.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) lamports: u64,
|
||||
/// Optional bounded faucet request when the source wallet is underfunded.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) airdrop_lamports: u64,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core/decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
/// Whether compatible materializers should run after decode replay.
|
||||
pub(crate) materialize_after_decode: bool,
|
||||
}
|
||||
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionMemoRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMemoRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Exact UTF-8 Memo payload.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Whether the wallet is supplied as a readonly Memo signer account.
|
||||
pub(crate) include_wallet_as_memo_signer: bool,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and first decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// Public key generated for a disposable Devnet recipient.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreGeneratedRecipientPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreGeneratedRecipientPayload {
|
||||
/// Base58 public key. The private key is not persisted or exposed.
|
||||
pub(crate) public_key: std::string::String,
|
||||
}
|
||||
|
||||
/// Progress event emitted during execution and post-validation.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreProgressPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreProgressPayload {
|
||||
/// RFC 3339 timestamp.
|
||||
pub(crate) timestamp: std::string::String,
|
||||
/// Stable severity code.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Stable stage code.
|
||||
pub(crate) stage: std::string::String,
|
||||
/// Human-readable message.
|
||||
pub(crate) message: std::string::String,
|
||||
/// Transaction signature when already available.
|
||||
pub(crate) signature: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// UI-safe result of one execution orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionSolanaCoreSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSolanaCoreSummaryPayload {
|
||||
/// Profile used by the execution.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Recipient public key.
|
||||
pub(crate) recipient: std::string::String,
|
||||
/// Whether the recipient account existed before execution.
|
||||
pub(crate) recipient_existed_before: bool,
|
||||
/// Recipient balance before execution when present.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) recipient_balance_before_lamports: std::option::Option<u64>,
|
||||
/// Rent-exempt minimum required for a new zero-data account.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) recipient_minimum_balance_lamports: u64,
|
||||
/// Source balance before optional funding.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) balance_before_lamports: u64,
|
||||
/// Source balance after optional funding.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) balance_after_funding_lamports: u64,
|
||||
/// Estimated transaction fee.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) fee_lamports: std::option::Option<u64>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Runtime simulation logs.
|
||||
pub(crate) simulation_logs: std::vec::Vec<std::string::String>,
|
||||
/// Faucet signature when funding was requested.
|
||||
pub(crate) airdrop_signature: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Canonical rows inserted after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) canonical_inserted: std::option::Option<u64>,
|
||||
/// Core transactions extracted after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) core_extracted: std::option::Option<u64>,
|
||||
/// Contextual decode inputs completed after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) decode_completed: std::option::Option<u64>,
|
||||
/// Contextual decode input failures after submission.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) decode_failed_inputs: std::option::Option<u64>,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of the simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and post-validation diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_solana_core/DemoExecutionMemoSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionMemoSummaryPayload {
|
||||
/// Profile used by the execution.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated transaction fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether Memo decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Whether the committed transaction annotation was validated.
|
||||
pub(crate) materialized: bool,
|
||||
/// Number of exact annotation rows returned by the bounded query.
|
||||
pub(crate) annotation_count: u32,
|
||||
/// Whether the second replay produced no failures or new materialized output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of the simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of annotations and post-validation diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) struct DemoExecutionSolanaCoreObserver<'a> {
|
||||
pub(crate) app_handle: tauri::AppHandle,
|
||||
pub(crate) cancel_requested: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn emit(
|
||||
&self,
|
||||
timestamp: std::string::String,
|
||||
level: &str,
|
||||
stage: &str,
|
||||
message: std::string::String,
|
||||
signature: std::option::Option<std::string::String>,
|
||||
) {
|
||||
let payload = crate::DemoExecutionSolanaCoreProgressPayload {
|
||||
timestamp,
|
||||
level: level.to_string(),
|
||||
stage: stage.to_string(),
|
||||
message,
|
||||
signature,
|
||||
};
|
||||
let emit_result = self.app_handle.emit_to(
|
||||
"demo_execution_solana_core",
|
||||
"demo-execution-solana-core-progress",
|
||||
payload,
|
||||
);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "emit_execution_progress",
|
||||
error = %error,
|
||||
"cannot emit Solana Core execution progress"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancelled(&self) -> bool {
|
||||
return self.cancel_requested.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::BackfillObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::BackfillProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"canonical_insert",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::CoreExtractionObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::CoreExtractionProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"core_extraction",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::DecodeReplayObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_progress(&self, event: &kb_pipeline::DecodeReplayProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
"decode_replay",
|
||||
event.message.clone(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_pipeline::SolanaExecutionObserver for crate::DemoExecutionSolanaCoreObserver<'_> {
|
||||
fn on_execution_progress(&self, event: &kb_pipeline::SolanaExecutionProgressEvent) {
|
||||
self.emit(
|
||||
event.timestamp.clone(),
|
||||
event.level.code(),
|
||||
event.stage.as_str(),
|
||||
event.message.clone(),
|
||||
event.signature.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
fn is_execution_cancelled(&self) -> bool {
|
||||
return self.cancelled();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DemoExecutionSolanaCoreRunGuard<'a> {
|
||||
pub(crate) running: &'a std::sync::atomic::AtomicBool,
|
||||
}
|
||||
impl std::ops::Drop for crate::DemoExecutionSolanaCoreRunGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn devnet_profile_options(
|
||||
config: &kb_config::AppConfig,
|
||||
) -> std::vec::Vec<crate::DemoExecutionSolanaCoreProfileOption> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
for profile in &config.profiles {
|
||||
if profile.wallet.cluster != "devnet"
|
||||
|| !profile.wallet.temporary_wallet_enabled
|
||||
|| !profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
continue;
|
||||
}
|
||||
output.push(crate::DemoExecutionSolanaCoreProfileOption {
|
||||
name: profile.name.clone(),
|
||||
wallet_alias: profile.wallet.temporary_wallet_alias.clone(),
|
||||
max_spend_lamports: profile.execution.devnet_max_spend_lamports,
|
||||
max_airdrop_lamports: profile.execution.devnet_airdrop_max_lamports,
|
||||
send_enabled: profile.wallet.devnet_send_enabled,
|
||||
require_operator_confirmation: profile.execution.require_operator_confirmation,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
pub(crate) fn select_devnet_profile(
|
||||
config: &kb_config::AppConfig,
|
||||
profile_name: &str,
|
||||
) -> std::result::Result<kb_config::ProfileConfig, std::string::String> {
|
||||
for profile in &config.profiles {
|
||||
if profile.name == profile_name
|
||||
&& profile.wallet.cluster == "devnet"
|
||||
&& profile.wallet.temporary_wallet_enabled
|
||||
&& profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
return std::result::Result::Ok(profile.clone());
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err(format!(
|
||||
"Devnet execution profile '{profile_name}' is unavailable"
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn demo_execution_solana_core_summary_payload(
|
||||
summary: kb_pipeline::DevnetSystemTransferSummary,
|
||||
) -> crate::DemoExecutionSolanaCoreSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let canonical_inserted = summary.backfill.as_ref().map(|value| return value.canonical_inserted);
|
||||
let core_extracted = summary.core_extraction.as_ref().map(|value| return value.extracted);
|
||||
let decode_completed = summary.decode_replay.as_ref().map(|value| return value.completed);
|
||||
let decode_failed_inputs =
|
||||
summary.decode_replay.as_ref().map(|value| return value.failed_inputs);
|
||||
let diagnostics_value = serde_json::json!({
|
||||
"airdropConfirmation": summary.airdrop_confirmation.as_ref(),
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
})
|
||||
});
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics_value);
|
||||
let cluster = crate::execution_cluster_code(summary.cluster);
|
||||
return crate::DemoExecutionSolanaCoreSummaryPayload {
|
||||
profile_name: summary.profile_name,
|
||||
cluster,
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
recipient: summary.recipient.0,
|
||||
recipient_existed_before: summary.recipient_existed_before,
|
||||
recipient_balance_before_lamports: summary.recipient_balance_before_lamports,
|
||||
recipient_minimum_balance_lamports: summary.recipient_minimum_balance_lamports,
|
||||
balance_before_lamports: summary.balance_before_lamports,
|
||||
balance_after_funding_lamports: summary.balance_after_funding_lamports,
|
||||
fee_lamports: summary.fee.fee_lamports,
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
simulation_logs: summary.simulation.logs,
|
||||
airdrop_signature: summary.airdrop_signature.map(|value| return value.0),
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_completed,
|
||||
decode_failed_inputs,
|
||||
plan_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn demo_execution_solana_core_memo_summary_payload(
|
||||
summary: kb_pipeline::DevnetMemoExecutionSummary,
|
||||
) -> crate::DemoExecutionMemoSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let canonical_inserted = summary
|
||||
.post_execution
|
||||
.as_ref()
|
||||
.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted =
|
||||
summary.post_execution.as_ref().is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = summary
|
||||
.post_execution
|
||||
.as_ref()
|
||||
.is_some_and(|value| return value.decode_replayed);
|
||||
let materialized =
|
||||
summary.post_execution.as_ref().is_some_and(|value| return value.materialized);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
})
|
||||
&& replay.processors.iter().map(|processor| return processor.skipped).sum::<u64>()
|
||||
>= 1;
|
||||
});
|
||||
let annotation_count = match u32::try_from(summary.annotations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics_value = serde_json::json!({
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"processors": value.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"processors": value.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}),
|
||||
"annotations": summary.annotations
|
||||
});
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics_value);
|
||||
return crate::DemoExecutionMemoSummaryPayload {
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialized,
|
||||
annotation_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn execution_cluster_code(
|
||||
cluster: kb_execution_api::ExecutionCluster,
|
||||
) -> std::string::String {
|
||||
return match cluster {
|
||||
kb_execution_api::ExecutionCluster::Localnet => "localnet".to_string(),
|
||||
kb_execution_api::ExecutionCluster::Devnet => "devnet".to_string(),
|
||||
kb_execution_api::ExecutionCluster::Testnet => "testnet".to_string(),
|
||||
kb_execution_api::ExecutionCluster::Mainnet => "mainnet".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn confirmation_status_code(
|
||||
status: kb_execution_api::ExecutionConfirmationStatus,
|
||||
) -> std::string::String {
|
||||
return match status {
|
||||
kb_execution_api::ExecutionConfirmationStatus::Processed => "processed".to_string(),
|
||||
kb_execution_api::ExecutionConfirmationStatus::Confirmed => "confirmed".to_string(),
|
||||
kb_execution_api::ExecutionConfirmationStatus::Finalized => "finalized".to_string(),
|
||||
kb_execution_api::ExecutionConfirmationStatus::Failed => "failed".to_string(),
|
||||
kb_execution_api::ExecutionConfirmationStatus::Expired => "expired".to_string(),
|
||||
kb_execution_api::ExecutionConfirmationStatus::TimedOut => "timed_out".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn pretty_json<T>(value: &T) -> std::string::String
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
return match serde_json::to_string_pretty(value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
serde_json::json!({"serializationError": error.to_string()}).to_string()
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn example_config_exposes_one_devnet_execution_profile() {
|
||||
let config =
|
||||
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||
};
|
||||
let profiles = crate::devnet_profile_options(&config);
|
||||
assert_eq!(profiles.len(), 1);
|
||||
assert_eq!(profiles[0].name, "local_devnet");
|
||||
assert!(profiles[0].send_enabled);
|
||||
assert!(profiles[0].max_spend_lamports >= 1_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_selection_rejects_mainnet_profiles() {
|
||||
let config =
|
||||
match kb_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||
};
|
||||
assert!(crate::select_devnet_profile(&config, "local_devnet").is_ok());
|
||||
assert!(crate::select_devnet_profile(&config, "mainnet").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// file: kb_app_demo/src/demo_execution_spl.rs
|
||||
// version: 3
|
||||
|
||||
//! Dedicated Tauri window for bounded SPL execution on Devnet.
|
||||
|
||||
/// Frontend payload for one independent Devnet SPL validation scenario.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ts_rs::TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_execution_spl/DevnetSplValidationScenarioPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DevnetSplValidationScenarioPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Stable family code.
|
||||
pub family: std::string::String,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Current implementation status.
|
||||
pub implementation_status: std::string::String,
|
||||
/// Whether the scenario requires proof material.
|
||||
pub proof_required: bool,
|
||||
/// Ordered required fixture variables.
|
||||
pub required_fixture_variables: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn memo_summary_keeps_lamports_as_json_strings() {
|
||||
let payload = crate::DemoExecutionMemoSummaryPayload {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
cluster: "devnet".to_string(),
|
||||
genesis_hash: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1".to_string(),
|
||||
wallet_public_key: kb_program_ids::SYSTEM_PROGRAM_ID.to_string(),
|
||||
balance_lamports: u64::MAX.to_string(),
|
||||
fee_lamports: std::option::Option::Some(u64::MAX.to_string()),
|
||||
simulation_success: true,
|
||||
simulation_error: std::option::Option::None,
|
||||
transaction_signature: std::option::Option::None,
|
||||
confirmation_status: std::option::Option::None,
|
||||
canonical_inserted: false,
|
||||
core_extracted: false,
|
||||
decode_replayed: false,
|
||||
materialized: false,
|
||||
annotation_count: 0,
|
||||
idempotence_validated: false,
|
||||
plan_json: "{}".to_string(),
|
||||
simulation_json: "{}".to_string(),
|
||||
diagnostics_json: "{}".to_string(),
|
||||
};
|
||||
let json = match serde_json::to_value(payload) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
assert_eq!(
|
||||
std::option::Option::Some(error.to_string()),
|
||||
std::option::Option::None,
|
||||
"Memo summary serialization failed"
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
assert!(json["balanceLamports"].is_string());
|
||||
assert!(json["feeLamports"].is_string());
|
||||
assert_eq!(json["balanceLamports"], u64::MAX.to_string());
|
||||
}
|
||||
}
|
||||
361
migration/khadhroony-bot2-reference/kb_app_demo/src/demo_http.rs
Normal file
361
migration/khadhroony-bot2-reference/kb_app_demo/src/demo_http.rs
Normal file
@@ -0,0 +1,361 @@
|
||||
// file: kb_app_demo/src/demo_http.rs
|
||||
// version: 8
|
||||
|
||||
//! HTTP JSON-RPC demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One selectable role shown by the HTTP demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_http/DemoHttpRoleOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoHttpRoleOption {
|
||||
/// Endpoint role code.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Request kinds accepted by this role.
|
||||
pub(crate) request_kinds: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// One selectable HTTP JSON-RPC method shown by the HTTP demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_http/DemoHttpMethodOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoHttpMethodOption {
|
||||
/// JSON-RPC method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Derived request kind used for endpoint routing.
|
||||
pub(crate) request_kind: std::string::String,
|
||||
/// Human-readable method label.
|
||||
pub(crate) label: std::string::String,
|
||||
/// Whether this method needs the first argument field.
|
||||
pub(crate) requires_first_arg: bool,
|
||||
/// Whether this method supports an optional configuration object.
|
||||
pub(crate) supports_config_json: bool,
|
||||
}
|
||||
|
||||
/// HTTP demo options derived from configuration and local method presets.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_http/DemoHttpOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoHttpOptionsPayload {
|
||||
/// Selectable roles.
|
||||
pub(crate) roles: std::vec::Vec<crate::DemoHttpRoleOption>,
|
||||
/// Selectable methods.
|
||||
pub(crate) methods: std::vec::Vec<crate::DemoHttpMethodOption>,
|
||||
}
|
||||
|
||||
/// Request payload for one HTTP JSON-RPC demo execution.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_http/DemoHttpRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoHttpRequest {
|
||||
/// Required endpoint role used by the HTTP pool.
|
||||
pub(crate) role: std::string::String,
|
||||
/// JSON-RPC method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Optional first argument string.
|
||||
pub(crate) first_arg: std::option::Option<std::string::String>,
|
||||
/// Optional JSON configuration string appended after the first argument.
|
||||
pub(crate) config_json: std::option::Option<std::string::String>,
|
||||
/// Optional raw JSON-RPC params array. When present, it overrides firstArg and configJson.
|
||||
pub(crate) params_json: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Response payload for one HTTP JSON-RPC demo execution.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_http/DemoHttpExecutionPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoHttpExecutionPayload {
|
||||
/// Selected endpoint name.
|
||||
pub(crate) endpoint_name: std::string::String,
|
||||
/// Selected provider name.
|
||||
pub(crate) provider: std::string::String,
|
||||
/// Selected endpoint URL.
|
||||
pub(crate) endpoint_url: std::string::String,
|
||||
/// Required role used by the selection.
|
||||
pub(crate) role: std::string::String,
|
||||
/// JSON-RPC method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Derived request kind.
|
||||
pub(crate) request_kind: std::string::String,
|
||||
/// Classified method family.
|
||||
pub(crate) method_class: std::string::String,
|
||||
/// Pretty JSON response text.
|
||||
pub(crate) response_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) async fn demo_http_execute_request_inner(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
request: crate::DemoHttpRequest,
|
||||
) -> std::result::Result<crate::DemoHttpExecutionPayload, std::string::String> {
|
||||
let role = request.role.trim().to_string();
|
||||
if role.is_empty() {
|
||||
return std::result::Result::Err("demo HTTP role must not be empty".to_string());
|
||||
}
|
||||
let method = request.method.trim().to_string();
|
||||
if method.is_empty() {
|
||||
return std::result::Result::Err("demo HTTP method must not be empty".to_string());
|
||||
}
|
||||
let params_json_value = match parse_optional_params_json(request.params_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let config_json_value = match parse_optional_json(request.config_json) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let params = match build_demo_http_params(
|
||||
&method,
|
||||
request.first_arg.as_deref(),
|
||||
config_json_value,
|
||||
params_json_value,
|
||||
) {
|
||||
std::result::Result::Ok(params) => params,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let selected_client = match state.http_pool().select_client_for_role_and_method(&role, &method)
|
||||
{
|
||||
std::result::Result::Ok(client) => client,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response_value =
|
||||
match selected_client.execute_json_rpc_result_raw(method.clone(), params).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response_json = match serde_json::to_string_pretty(&response_value) {
|
||||
std::result::Result::Ok(text) => text,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let method_class = kb_rpc::HttpClient::classify_method(&method);
|
||||
return std::result::Result::Ok(crate::DemoHttpExecutionPayload {
|
||||
endpoint_name: selected_client.endpoint_name().to_string(),
|
||||
provider: selected_client.provider().to_string(),
|
||||
endpoint_url: selected_client.endpoint_url().to_string(),
|
||||
role,
|
||||
request_kind: kb_rpc::request_kind_from_method(&method),
|
||||
method,
|
||||
method_class: method_class_to_string(method_class).to_string(),
|
||||
response_json,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn build_http_role_options(
|
||||
snapshots: std::vec::Vec<kb_rpc::HttpPoolClientSnapshot>,
|
||||
) -> std::vec::Vec<crate::DemoHttpRoleOption> {
|
||||
let mut roles = std::collections::BTreeMap::<
|
||||
std::string::String,
|
||||
std::collections::BTreeSet<std::string::String>,
|
||||
>::new();
|
||||
for snapshot in snapshots {
|
||||
for role in snapshot.roles {
|
||||
if !role.enabled {
|
||||
continue;
|
||||
}
|
||||
let role_entry = roles.entry(role.role).or_default();
|
||||
for request_kind in role.request_kinds {
|
||||
role_entry.insert(request_kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut options = std::vec::Vec::new();
|
||||
for (role, request_kinds) in roles {
|
||||
options.push(crate::DemoHttpRoleOption {
|
||||
role,
|
||||
request_kinds: request_kinds.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
pub(crate) fn build_http_method_options() -> std::vec::Vec<crate::DemoHttpMethodOption> {
|
||||
let methods = [
|
||||
("getAccountInfo", "Compte Solana", true, true),
|
||||
("getBalance", "Balance SOL", true, true),
|
||||
("getBlock", "Bloc par slot", true, true),
|
||||
("getBlockCommitment", "Commitment d’un bloc", true, false),
|
||||
("getBlockHeight", "Hauteur de bloc", false, true),
|
||||
("getBlockProduction", "Production de blocs", false, true),
|
||||
("getBlocks", "Liste de blocs", true, true),
|
||||
("getBlocksWithLimit", "Liste de blocs avec limite", true, true),
|
||||
("getClusterNodes", "Nœuds du cluster", false, false),
|
||||
("getEpochInfo", "Époque courante", false, true),
|
||||
("getEpochSchedule", "Planning des époques", false, false),
|
||||
("getFeeForMessage", "Frais pour message", true, true),
|
||||
("getFirstAvailableBlock", "Premier bloc disponible", false, false),
|
||||
("getGenesisHash", "Genesis hash", false, false),
|
||||
("getHealth", "Santé du nœud", false, false),
|
||||
("getHighestSnapshotSlot", "Dernier snapshot", false, false),
|
||||
("getIdentity", "Identité du nœud", false, false),
|
||||
("getInflationGovernor", "Gouverneur inflation", false, true),
|
||||
("getInflationRate", "Taux d’inflation", false, false),
|
||||
("getInflationReward", "Récompense inflation", true, true),
|
||||
("getLargestAccounts", "Plus gros comptes", false, true),
|
||||
("getLatestBlockhash", "Dernier blockhash", false, true),
|
||||
("getLeaderSchedule", "Planning leaders", false, true),
|
||||
("getMaxRetransmitSlot", "Slot retransmis maximum", false, false),
|
||||
("getMaxShredInsertSlot", "Shred insert slot maximum", false, false),
|
||||
("getMinimumBalanceForRentExemption", "Rent exemption", true, true),
|
||||
("getMultipleAccounts", "Comptes multiples", true, true),
|
||||
("getProgramAccounts", "Comptes de programme", true, true),
|
||||
("getRecentPerformanceSamples", "Samples performance", false, true),
|
||||
("getRecentPrioritizationFees", "Frais de priorité récents", false, true),
|
||||
("getSignaturesForAddress", "Signatures par adresse", true, true),
|
||||
("getSignatureStatuses", "Statuts signatures", true, true),
|
||||
("getSlot", "Slot courant", false, true),
|
||||
("getSlotLeader", "Leader du slot", false, true),
|
||||
("getSlotLeaders", "Leaders de slots", true, true),
|
||||
("getStakeActivation", "Activation stake", true, true),
|
||||
("getStakeMinimumDelegation", "Minimum delegation", false, true),
|
||||
("getSupply", "Supply SOL", false, true),
|
||||
("getTokenAccountBalance", "Balance token account", true, true),
|
||||
("getTokenAccountsByDelegate", "Token accounts par delegate", true, true),
|
||||
("getTokenAccountsByOwner", "Token accounts par owner", true, true),
|
||||
("getTokenLargestAccounts", "Plus gros comptes token", true, true),
|
||||
("getTokenSupply", "Supply token", true, true),
|
||||
("getTransaction", "Transaction", true, true),
|
||||
("getTransactionCount", "Nombre de transactions", false, true),
|
||||
("getVersion", "Version du nœud", false, false),
|
||||
("getVoteAccounts", "Vote accounts", false, true),
|
||||
("isBlockhashValid", "Validité blockhash", true, true),
|
||||
("requestAirdrop", "Airdrop devnet", true, true),
|
||||
("sendTransaction", "Envoi de transaction", true, true),
|
||||
("simulateTransaction", "Simulation transaction", true, true),
|
||||
];
|
||||
let mut options = std::vec::Vec::new();
|
||||
for (method, label, requires_first_arg, supports_config_json) in methods {
|
||||
options.push(crate::DemoHttpMethodOption {
|
||||
method: method.to_string(),
|
||||
request_kind: kb_rpc::request_kind_from_method(method),
|
||||
label: label.to_string(),
|
||||
requires_first_arg,
|
||||
supports_config_json,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
fn parse_optional_json(
|
||||
config_json: std::option::Option<std::string::String>,
|
||||
) -> std::result::Result<std::option::Option<serde_json::Value>, std::string::String> {
|
||||
let config_text = match config_json {
|
||||
std::option::Option::Some(value) => value.trim().to_string(),
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
if config_text.is_empty() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let parse_result = serde_json::from_str::<serde_json::Value>(&config_text);
|
||||
return match parse_result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(format!("invalid configJson: {error}"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_params_json(
|
||||
params_json: std::option::Option<std::string::String>,
|
||||
) -> std::result::Result<std::option::Option<std::vec::Vec<serde_json::Value>>, std::string::String>
|
||||
{
|
||||
let params_text = match params_json {
|
||||
std::option::Option::Some(value) => value.trim().to_string(),
|
||||
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
if params_text.is_empty() {
|
||||
return std::result::Result::Ok(std::option::Option::None);
|
||||
}
|
||||
let parse_result = serde_json::from_str::<serde_json::Value>(¶ms_text);
|
||||
let value = match parse_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid paramsJson: {error}"));
|
||||
},
|
||||
};
|
||||
let array = match value.as_array() {
|
||||
std::option::Option::Some(array) => array.clone(),
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err("paramsJson must be a JSON array".to_string());
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(std::option::Option::Some(array));
|
||||
}
|
||||
|
||||
fn build_demo_http_params(
|
||||
method: &str,
|
||||
first_arg: std::option::Option<&str>,
|
||||
config_json: std::option::Option<serde_json::Value>,
|
||||
params_json: std::option::Option<std::vec::Vec<serde_json::Value>>,
|
||||
) -> std::result::Result<std::vec::Vec<serde_json::Value>, std::string::String> {
|
||||
if let std::option::Option::Some(params) = params_json {
|
||||
return std::result::Result::Ok(params);
|
||||
}
|
||||
let needs_first_arg = method == "getBalance"
|
||||
|| method == "getAccountInfo"
|
||||
|| method == "getBlock"
|
||||
|| method == "getBlockCommitment"
|
||||
|| method == "getBlocks"
|
||||
|| method == "getBlocksWithLimit"
|
||||
|| method == "getFeeForMessage"
|
||||
|| method == "getInflationReward"
|
||||
|| method == "getMinimumBalanceForRentExemption"
|
||||
|| method == "getMultipleAccounts"
|
||||
|| method == "getProgramAccounts"
|
||||
|| method == "getSignaturesForAddress"
|
||||
|| method == "getSignatureStatuses"
|
||||
|| method == "getSlotLeaders"
|
||||
|| method == "getStakeActivation"
|
||||
|| method == "getTokenAccountBalance"
|
||||
|| method == "getTokenAccountsByDelegate"
|
||||
|| method == "getTokenAccountsByOwner"
|
||||
|| method == "getTokenLargestAccounts"
|
||||
|| method == "getTokenSupply"
|
||||
|| method == "getTransaction"
|
||||
|| method == "isBlockhashValid"
|
||||
|| method == "requestAirdrop"
|
||||
|| method == "sendTransaction"
|
||||
|| method == "simulateTransaction";
|
||||
if needs_first_arg {
|
||||
let first_arg_value = match first_arg {
|
||||
std::option::Option::Some(value) => value.trim(),
|
||||
std::option::Option::None => "",
|
||||
};
|
||||
if first_arg_value.is_empty() {
|
||||
return std::result::Result::Err(format!("method '{method}' requires firstArg"));
|
||||
}
|
||||
let mut params = std::vec::Vec::new();
|
||||
params.push(serde_json::Value::String(first_arg_value.to_string()));
|
||||
if let std::option::Option::Some(config_value) = config_json {
|
||||
params.push(config_value);
|
||||
}
|
||||
return std::result::Result::Ok(params);
|
||||
}
|
||||
let mut params = std::vec::Vec::new();
|
||||
if let std::option::Option::Some(config_value) = config_json {
|
||||
params.push(config_value);
|
||||
}
|
||||
return std::result::Result::Ok(params);
|
||||
}
|
||||
|
||||
fn method_class_to_string(method_class: kb_rpc::HttpMethodClass) -> &'static str {
|
||||
return match method_class {
|
||||
kb_rpc::HttpMethodClass::GeneralRpc => "GeneralRpc",
|
||||
kb_rpc::HttpMethodClass::SendTransaction => "SendTransaction",
|
||||
kb_rpc::HttpMethodClass::HeavyRead => "HeavyRead",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
// file: kb_app_demo/src/demo_spl_ata.rs
|
||||
// version: 3
|
||||
|
||||
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Request for one representative ATA creation on Devnet.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoExecutionSplAtaRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplAtaRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Wallet owner used by canonical derivation.
|
||||
pub(crate) wallet_owner: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// `classic` or `token_2022`.
|
||||
pub(crate) token_program: std::string::String,
|
||||
/// `create` or `create_idempotent`.
|
||||
pub(crate) mode: std::string::String,
|
||||
/// Whether to sign and submit after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// Request used to derive the representative wallet ATA before execution.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoSplAtaDerivationRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaDerivationRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// `classic` or `token_2022`.
|
||||
pub(crate) token_program: std::string::String,
|
||||
}
|
||||
|
||||
/// Readonly payer, wallet, Token Program and derived ATA values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoSplAtaDerivationPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaDerivationPayload {
|
||||
/// Profile wallet used as payer.
|
||||
pub(crate) payer: std::string::String,
|
||||
/// Wallet owner selected by the representative panel.
|
||||
pub(crate) wallet_owner: std::string::String,
|
||||
/// Exact Token Program ID.
|
||||
pub(crate) token_program_id: std::string::String,
|
||||
/// Canonically derived ATA.
|
||||
pub(crate) associated_token_account: std::string::String,
|
||||
}
|
||||
|
||||
/// UI-safe ATA execution result.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplAtaSummaryPayload {
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile name.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Profile wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Target Token Program ID.
|
||||
pub(crate) token_program_id: std::string::String,
|
||||
/// Derived ATA from the exact plan.
|
||||
pub(crate) associated_token_account: std::string::String,
|
||||
/// Wallet balance as lossless JSON text.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Fee estimate as lossless JSON text.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Aggregate stateful readiness.
|
||||
pub(crate) readiness_status: std::string::String,
|
||||
/// Exact simulation outcome.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Number of ATA-owned materialized facts.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay created no output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Exact plan JSON.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Stateful readiness JSON.
|
||||
pub(crate) readiness_json: std::string::String,
|
||||
/// Simulation JSON.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Confirmation and replay diagnostics JSON.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Bounded ATA lifecycle journal request.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoSplAtaJournalRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaJournalRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Optional partial signature.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional exact mint.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Optional exact ATA.
|
||||
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
||||
/// Optional exact materialized operation.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// One UI-safe ATA lifecycle journal row.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_ata/DemoSplAtaJournalRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplAtaJournalRow {
|
||||
/// Transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Slot as lossless JSON text.
|
||||
pub(crate) slot: std::string::String,
|
||||
/// Materialized family.
|
||||
pub(crate) family: std::string::String,
|
||||
/// Lifecycle operation.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Exact mint when present.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Exact ATA when present.
|
||||
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
||||
/// Full bounded payload JSON.
|
||||
pub(crate) payload_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_profile_wallet(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
) -> std::result::Result<kb_wallet::TemporaryWallet, std::string::String> {
|
||||
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
||||
let directory = if configured.is_absolute() {
|
||||
configured
|
||||
} else {
|
||||
crate::workspace_root_dir().join(configured)
|
||||
};
|
||||
let store = match kb_wallet::TemporaryWalletStore::new(directory) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let alias = match kb_wallet::WalletAlias::parse(profile.wallet.temporary_wallet_alias.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
return match store.load_or_create(alias).await {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn parse_token_program(
|
||||
value: &str,
|
||||
) -> std::result::Result<
|
||||
kb_executor_spl_associated_token_account::SplAssociatedTokenProgram,
|
||||
std::string::String,
|
||||
> {
|
||||
return match value.trim() {
|
||||
"classic" => std::result::Result::Ok(
|
||||
kb_executor_spl_associated_token_account::SplAssociatedTokenProgram::Classic,
|
||||
),
|
||||
"token_2022" => std::result::Result::Ok(
|
||||
kb_executor_spl_associated_token_account::SplAssociatedTokenProgram::Token2022,
|
||||
),
|
||||
_ => std::result::Result::Err("Token Program must be classic or token_2022".to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn derive_ata(
|
||||
wallet: &str,
|
||||
mint: &str,
|
||||
token_program: &str,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let wallet: solana_pubkey::Pubkey = match wallet.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid wallet: {error}"));
|
||||
},
|
||||
};
|
||||
let mint: solana_pubkey::Pubkey = match mint.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid mint: {error}"));
|
||||
},
|
||||
};
|
||||
let token_program: solana_pubkey::Pubkey = match token_program.parse() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("invalid Token Program: {error}"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
|
||||
&wallet,
|
||||
&mint,
|
||||
&token_program,
|
||||
)
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_ata_summary_payload(
|
||||
summary: kb_pipeline::DevnetSplAssociatedTokenAccountExecutionSummary,
|
||||
) -> crate::DemoExecutionSplAtaSummaryPayload {
|
||||
let (associated_token_account, token_program_id) = match summary.plan.instructions.first() {
|
||||
std::option::Option::Some(instruction) => {
|
||||
let ata = match instruction.accounts.get(1) {
|
||||
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
let token_program = match instruction.accounts.last() {
|
||||
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
(ata, token_program)
|
||||
},
|
||||
std::option::Option::None => (std::string::String::new(), std::string::String::new()),
|
||||
};
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation.as_ref(),
|
||||
"postStateValidation": summary.post_state_validation.as_ref(),
|
||||
"postExecution": summary.post_execution.as_ref(),
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"candidatesCompleted": value.candidates_completed,
|
||||
"candidatesCancelled": value.candidates_cancelled,
|
||||
"candidatesNotStarted": value.candidates_not_started,
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"campaignId": value.campaign_id,
|
||||
"selected": value.selected,
|
||||
"completed": value.completed,
|
||||
"unmatched": value.unmatched,
|
||||
"failedInputs": value.failed_inputs,
|
||||
"cancelled": value.cancelled,
|
||||
"materializedOutputs": value.processors.iter().map(|processor| {
|
||||
return processor.materialized_outputs;
|
||||
}).sum::<u64>(),
|
||||
"materializationRefused": value.processors.iter().map(|processor| {
|
||||
return processor.materialization_refused;
|
||||
}).sum::<u64>()
|
||||
});
|
||||
}),
|
||||
"materializations": summary.materializations.iter().map(|value| {
|
||||
return serde_json::json!({
|
||||
"processorName": value.processor_name,
|
||||
"outputKey": value.output_key,
|
||||
"family": value.materialized_family,
|
||||
"signature": value.signature,
|
||||
"slot": value.slot.to_string(),
|
||||
"payload": value.payload_json
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let readiness_json = crate::pretty_json(&summary.stateful_readiness);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let diagnostics_json = crate::pretty_json(&diagnostics);
|
||||
return crate::DemoExecutionSplAtaSummaryPayload {
|
||||
operation: summary.stateful_readiness.operation_code,
|
||||
profile_name: summary.profile_name,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
token_program_id,
|
||||
associated_token_account,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
readiness_status: match summary.stateful_readiness.status {
|
||||
kb_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Ready => {
|
||||
"ready".to_string()
|
||||
},
|
||||
kb_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Blocked => {
|
||||
"blocked".to_string()
|
||||
},
|
||||
},
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
readiness_json,
|
||||
simulation_json,
|
||||
diagnostics_json,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn journal_matches(
|
||||
payload: &serde_json::Value,
|
||||
request: &crate::DemoSplAtaJournalRequest,
|
||||
) -> bool {
|
||||
for (expected, key) in [
|
||||
(&request.operation, "operation"),
|
||||
(&request.mint, "mint"),
|
||||
(&request.associated_token_account, "associatedTokenAccount"),
|
||||
] {
|
||||
if expected.as_ref().is_some_and(|value| {
|
||||
return payload.get(key).and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(value.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub(crate) fn journal_row(
|
||||
row: kb_store_core::MaterializedEventQueryRow,
|
||||
) -> crate::DemoSplAtaJournalRow {
|
||||
let text = |key: &str| {
|
||||
return row
|
||||
.payload_json
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
};
|
||||
let operation = text("operation");
|
||||
let mint = text("mint");
|
||||
let associated_token_account = text("associatedTokenAccount");
|
||||
let payload_json = crate::pretty_json(&row.payload_json);
|
||||
return crate::DemoSplAtaJournalRow {
|
||||
signature: row.signature,
|
||||
slot: row.slot.to_string(),
|
||||
family: row.materialized_family,
|
||||
operation,
|
||||
mint,
|
||||
associated_token_account,
|
||||
payload_json,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn classic_and_token_2022_derivations_are_distinct_and_canonical() {
|
||||
let wallet = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mint = "So11111111111111111111111111111111111111112";
|
||||
let classic = crate::derive_ata(wallet, mint, kb_program_ids::SPL_TOKEN_PROGRAM_ID)
|
||||
.unwrap_or_else(|error| panic!("classic derivation failed: {error}"));
|
||||
let token_2022 = crate::derive_ata(wallet, mint, kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID)
|
||||
.unwrap_or_else(|error| panic!("Token-2022 derivation failed: {error}"));
|
||||
assert_eq!(classic, "aqxoAhCwpy3oB1BpNw9hL1HdLYLgPpbPjzxDrrQj3Fs");
|
||||
assert_eq!(token_2022, "2sZUUBGq1i6aE47ZoxCaCW89jmYm2EXLPPmNMgMDXHMS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lifecycle_journal_filters_are_exact() {
|
||||
let request = crate::DemoSplAtaJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::Some("mint111".to_string()),
|
||||
associated_token_account: std::option::Option::Some("ata111".to_string()),
|
||||
operation: std::option::Option::Some("create_idempotent".to_string()),
|
||||
limit: 100,
|
||||
};
|
||||
assert!(crate::journal_matches(
|
||||
&serde_json::json!({
|
||||
"operation": "create_idempotent",
|
||||
"mint": "mint111",
|
||||
"associatedTokenAccount": "ata111"
|
||||
}),
|
||||
&request,
|
||||
));
|
||||
assert!(!crate::journal_matches(
|
||||
&serde_json::json!({
|
||||
"operation": "create",
|
||||
"mint": "mint111",
|
||||
"associatedTokenAccount": "ata111"
|
||||
}),
|
||||
&request,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
// file: kb_app_demo/src/demo_spl_token.rs
|
||||
// version: 2
|
||||
|
||||
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Request for one checked classic SPL Token transfer on Devnet.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token/DemoExecutionSplTokenRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplTokenRequest {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Source token account.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Exact mint account carried by `TransferChecked`.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Destination token account.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Simple authority resolved by the selected profile wallet.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Exact raw amount represented as a decimal string.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals carried by the wire.
|
||||
pub(crate) decimals: u8,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one checked SPL Token transfer orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplTokenSummaryPayload {
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile used by the orchestration.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Exact raw token amount.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals.
|
||||
pub(crate) decimals: u8,
|
||||
/// Aggregate stateful preflight status.
|
||||
pub(crate) readiness_status: std::string::String,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether SPL Token decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Number of materialized rows for the submitted instruction.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay produced no failure or new output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of stateful readiness.
|
||||
pub(crate) readiness_json: std::string::String,
|
||||
/// Pretty JSON representation of the exact simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and replay diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Bounded exact filters for the classic SPL Token materialized journal.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token/DemoSplTokenJournalRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplTokenJournalRequest {
|
||||
/// Devnet profile whose PostgreSQL store is queried.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Optional partial signature handled by the bounded store query.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional exact mint account.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Optional exact account occurring in the ordered account list.
|
||||
pub(crate) account: std::option::Option<std::string::String>,
|
||||
/// Optional exact materialized family.
|
||||
pub(crate) family: std::option::Option<std::string::String>,
|
||||
/// Optional exact operation code without the `spl_token.` prefix.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Maximum number of rows returned after typed filtering.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// UI-safe materialized classic SPL Token journal row.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token/DemoSplTokenJournalRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplTokenJournalRow {
|
||||
/// Materializer processor name.
|
||||
pub(crate) processor_name: std::string::String,
|
||||
/// Materializer processor version.
|
||||
pub(crate) processor_version: std::string::String,
|
||||
/// Stable processor-owned output key.
|
||||
pub(crate) output_key: std::string::String,
|
||||
/// Transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Decimal slot rendered as text to preserve JSON precision.
|
||||
pub(crate) slot: std::string::String,
|
||||
/// Materialized family code.
|
||||
pub(crate) family: std::string::String,
|
||||
/// Exact operation when present.
|
||||
pub(crate) operation: std::option::Option<std::string::String>,
|
||||
/// Exact mint account when explicitly available.
|
||||
pub(crate) mint: std::option::Option<std::string::String>,
|
||||
/// Exact raw amount when carried by the materialized fact.
|
||||
pub(crate) amount_raw: std::option::Option<std::string::String>,
|
||||
/// Stable outer or inner instruction path when present.
|
||||
pub(crate) instruction_path: std::option::Option<std::string::String>,
|
||||
/// Ordered account keys preserved by the decoder.
|
||||
pub(crate) account_keys: std::vec::Vec<std::string::String>,
|
||||
/// Complete bounded typed payload rendered as JSON text.
|
||||
pub(crate) payload_json: std::string::String,
|
||||
/// Database creation timestamp.
|
||||
pub(crate) created_at: std::string::String,
|
||||
/// Database replacement timestamp.
|
||||
pub(crate) updated_at: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token_summary_payload(
|
||||
summary: kb_pipeline::DevnetSplTokenExecutionSummary,
|
||||
amount_raw: std::string::String,
|
||||
decimals: u8,
|
||||
) -> crate::DemoExecutionSplTokenSummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let readiness_json = crate::pretty_json(&summary.stateful_readiness);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let post_execution = summary.post_execution.as_ref();
|
||||
let canonical_inserted = post_execution.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted = post_execution.is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = post_execution.is_some_and(|value| return value.decode_replayed);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation,
|
||||
"postExecution": summary.post_execution,
|
||||
"backfill": summary.backfill.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"canonicalInserted": value.canonical_inserted,
|
||||
"canonicalSkipped": value.canonical_skipped,
|
||||
"existingSkipped": value.existing_skipped,
|
||||
"missing": value.missing,
|
||||
"failed": value.failed
|
||||
});
|
||||
}),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
||||
return serde_json::json!({
|
||||
"selected": value.selected,
|
||||
"extracted": value.extracted,
|
||||
"skipped": value.skipped,
|
||||
"failed": value.failed,
|
||||
"cancelled": value.cancelled
|
||||
});
|
||||
}),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
||||
return replay_diagnostics(value);
|
||||
}),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
||||
return replay_diagnostics(value);
|
||||
}),
|
||||
"materializations": summary.materializations
|
||||
});
|
||||
return crate::DemoExecutionSplTokenSummaryPayload {
|
||||
operation: summary.stateful_readiness.operation_code,
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
amount_raw,
|
||||
decimals,
|
||||
readiness_status: match summary.stateful_readiness.status {
|
||||
kb_pipeline::SplTokenStatefulReadinessStatus::Ready => "ready".to_string(),
|
||||
kb_pipeline::SplTokenStatefulReadinessStatus::Blocked => "blocked".to_string(),
|
||||
},
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
readiness_json,
|
||||
simulation_json,
|
||||
diagnostics_json: crate::pretty_json(&diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
fn replay_diagnostics(summary: &kb_pipeline::DecodeReplaySummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"campaignId": summary.campaign_id,
|
||||
"selected": summary.selected,
|
||||
"completed": summary.completed,
|
||||
"failedInputs": summary.failed_inputs,
|
||||
"cancelled": summary.cancelled,
|
||||
"processors": summary.processors.iter().map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"decoded": processor.decoded,
|
||||
"skipped": processor.skipped,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
}).collect::<std::vec::Vec<_>>()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn validate_journal_request(
|
||||
request: crate::DemoSplTokenJournalRequest,
|
||||
) -> std::result::Result<crate::DemoSplTokenJournalRequest, std::string::String> {
|
||||
if request.limit == 0 || request.limit > kb_store_core::MAX_MATERIALIZED_EVENT_QUERY_ROWS {
|
||||
return std::result::Result::Err(format!(
|
||||
"journal limit must be between 1 and {}",
|
||||
kb_store_core::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
||||
));
|
||||
}
|
||||
if request.profile_name.trim().is_empty() {
|
||||
return std::result::Result::Err("journal profile name must not be empty".to_string());
|
||||
}
|
||||
let signature_contains =
|
||||
match bounded_optional(request.signature_contains, "signature filter", 128) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mint = match bounded_optional(request.mint, "mint filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = match bounded_optional(request.account, "account filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let operation = match bounded_optional(request.operation, "operation filter", 64) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let family = match bounded_optional(request.family, "family filter", 32) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if family.as_ref().is_some_and(|value| {
|
||||
return !matches!(value.as_str(), "token_account" | "admin" | "risk");
|
||||
}) {
|
||||
return std::result::Result::Err(
|
||||
"journal family must be token_account, admin or risk".to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(crate::DemoSplTokenJournalRequest {
|
||||
profile_name: request.profile_name.trim().to_string(),
|
||||
signature_contains,
|
||||
mint,
|
||||
account,
|
||||
family,
|
||||
operation,
|
||||
limit: request.limit,
|
||||
});
|
||||
}
|
||||
|
||||
fn bounded_optional(
|
||||
value: std::option::Option<std::string::String>,
|
||||
label: &str,
|
||||
maximum_length: usize,
|
||||
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
|
||||
let trimmed = value.map(|text| return text.trim().to_string());
|
||||
let trimmed = trimmed.filter(|text| return !text.is_empty());
|
||||
if trimmed.as_ref().is_some_and(|text| return text.len() > maximum_length) {
|
||||
return std::result::Result::Err(format!("{label} must not exceed {maximum_length} bytes"));
|
||||
}
|
||||
return std::result::Result::Ok(trimmed);
|
||||
}
|
||||
|
||||
pub(crate) fn payload_matches(
|
||||
payload: &serde_json::Value,
|
||||
request: &crate::DemoSplTokenJournalRequest,
|
||||
) -> bool {
|
||||
if request.operation.as_ref().is_some_and(|expected| {
|
||||
return payload.get("operation").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(expected.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
if request.mint.as_ref().is_some_and(|expected| {
|
||||
return payload_mint(payload).as_deref() != std::option::Option::Some(expected.as_str());
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
if request.account.as_ref().is_some_and(|expected| {
|
||||
return !payload_account_keys(payload).iter().any(|value| return value == expected);
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token_journal_row(
|
||||
row: kb_store_core::MaterializedEventQueryRow,
|
||||
) -> DemoSplTokenJournalRow {
|
||||
let operation = row
|
||||
.payload_json
|
||||
.get("operation")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let amount_raw = row
|
||||
.payload_json
|
||||
.get("amountRaw")
|
||||
.or_else(|| {
|
||||
return row
|
||||
.payload_json
|
||||
.get("parameters")
|
||||
.and_then(|value| return value.get("amountRaw"));
|
||||
})
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let instruction_path = row
|
||||
.payload_json
|
||||
.get("instructionPath")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
let payload_json = crate::pretty_json(&row.payload_json);
|
||||
return DemoSplTokenJournalRow {
|
||||
processor_name: row.processor_name,
|
||||
processor_version: row.processor_version,
|
||||
output_key: row.output_key,
|
||||
signature: row.signature,
|
||||
slot: row.slot.to_string(),
|
||||
family: row.materialized_family,
|
||||
operation,
|
||||
mint: payload_mint(&row.payload_json),
|
||||
amount_raw,
|
||||
instruction_path,
|
||||
account_keys: payload_account_keys(&row.payload_json),
|
||||
payload_json,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
fn payload_mint(payload: &serde_json::Value) -> std::option::Option<std::string::String> {
|
||||
let direct = payload.get("mint").and_then(serde_json::Value::as_str);
|
||||
if let std::option::Option::Some(value) = direct {
|
||||
return std::option::Option::Some(value.to_string());
|
||||
}
|
||||
return payload.get("accounts").and_then(serde_json::Value::as_array).and_then(|rows| {
|
||||
return rows.iter().find_map(|row| {
|
||||
if row.get("role").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some("mint")
|
||||
{
|
||||
return std::option::Option::None;
|
||||
}
|
||||
return row
|
||||
.get("accountKey")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn payload_account_keys(payload: &serde_json::Value) -> std::vec::Vec<std::string::String> {
|
||||
let values = payload.get("accounts").and_then(serde_json::Value::as_array).map(|rows| {
|
||||
return rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
return row
|
||||
.get("accountKey")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::string::ToString::to_string);
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
return match values {
|
||||
std::option::Option::Some(values) => values,
|
||||
std::option::Option::None => std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn journal_filters_are_exact_bounded_and_preserve_raw_amounts() {
|
||||
let request = crate::DemoSplTokenJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::Some("mint111".to_string()),
|
||||
account: std::option::Option::Some("source111".to_string()),
|
||||
family: std::option::Option::Some("token_account".to_string()),
|
||||
operation: std::option::Option::Some("transfer_checked".to_string()),
|
||||
limit: 25,
|
||||
};
|
||||
let payload = serde_json::json!({
|
||||
"operation": "transfer_checked",
|
||||
"mint": "mint111",
|
||||
"amountRaw": "18446744073709551615",
|
||||
"accounts": [
|
||||
{"role": "source", "accountKey": "source111"},
|
||||
{"role": "mint", "accountKey": "mint111"}
|
||||
]
|
||||
});
|
||||
assert!(crate::payload_matches(&payload, &request));
|
||||
assert_eq!(
|
||||
payload.get("amountRaw").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("18446744073709551615")
|
||||
);
|
||||
let row = crate::demo_spl_token_journal_row(kb_store_core::MaterializedEventQueryRow {
|
||||
processor_name: "spl_token_accounts".to_string(),
|
||||
processor_version: "0.4.4".to_string(),
|
||||
input_key: "input".to_string(),
|
||||
output_key: "output".to_string(),
|
||||
source_event_key: "event".to_string(),
|
||||
source_decoder_name: "spl_token".to_string(),
|
||||
source_decoder_version: "0.4.4".to_string(),
|
||||
signature: "signature".to_string(),
|
||||
slot: u64::MAX,
|
||||
materialized_family: "token_account".to_string(),
|
||||
payload_json: payload,
|
||||
created_at: "created".to_string(),
|
||||
updated_at: "updated".to_string(),
|
||||
});
|
||||
assert_eq!(row.slot, u64::MAX.to_string());
|
||||
assert_eq!(row.amount_raw, std::option::Option::Some(u64::MAX.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn journal_rejects_unbounded_or_unknown_family_requests() {
|
||||
let request = crate::DemoSplTokenJournalRequest {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
signature_contains: std::option::Option::None,
|
||||
mint: std::option::Option::None,
|
||||
account: std::option::Option::None,
|
||||
family: std::option::Option::Some("fee".to_string()),
|
||||
operation: std::option::Option::None,
|
||||
limit: 501,
|
||||
};
|
||||
assert!(crate::validate_journal_request(request).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// file: kb_app_demo/src/demo_spl_token_2022.rs
|
||||
// version: 3
|
||||
|
||||
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Public values loaded from one persisted Token-2022 validation fixture.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token_2022/DemoSplToken2022FixturePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSplToken2022FixturePayload {
|
||||
/// Fixture file used by the application.
|
||||
pub(crate) fixture_path: std::string::String,
|
||||
/// Token-2022 program ID.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Source token account.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Destination token account.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Dedicated empty account reserved for CloseAccount validation.
|
||||
pub(crate) close_account: std::string::String,
|
||||
/// Delegate account.
|
||||
pub(crate) delegate: std::string::String,
|
||||
/// Profile-wallet authority.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Freeze authority when configured on the mint.
|
||||
pub(crate) freeze_authority: std::string::String,
|
||||
/// Mint decimals.
|
||||
pub(crate) decimals: u8,
|
||||
/// Default raw amount for the selected scenario.
|
||||
pub(crate) default_amount_raw: std::string::String,
|
||||
}
|
||||
|
||||
/// Request for one independent public Token-2022 Devnet scenario.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token_2022/DemoExecutionSplToken2022Request.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplToken2022Request {
|
||||
/// Selected Devnet profile.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Stable scenario identifier.
|
||||
pub(crate) scenario_id: std::string::String,
|
||||
/// Source or target token account depending on the selected operation.
|
||||
pub(crate) source: std::string::String,
|
||||
/// Exact mint account.
|
||||
pub(crate) mint: std::string::String,
|
||||
/// Destination token account or close-account lamport destination.
|
||||
pub(crate) destination: std::string::String,
|
||||
/// Delegate account used by `ApproveChecked`.
|
||||
pub(crate) delegate: std::string::String,
|
||||
/// Owner or mint authority resolved by the selected profile wallet.
|
||||
pub(crate) authority: std::string::String,
|
||||
/// Freeze authority used by freeze and thaw scenarios.
|
||||
pub(crate) freeze_authority: std::string::String,
|
||||
/// Exact raw amount represented as a decimal string.
|
||||
pub(crate) amount_raw: std::string::String,
|
||||
/// Expected mint decimals carried by checked instructions.
|
||||
pub(crate) decimals: u8,
|
||||
/// Whether the transaction should be signed and submitted after simulation.
|
||||
pub(crate) submit: bool,
|
||||
/// Explicit operator confirmation for signed submission.
|
||||
pub(crate) operator_confirmed: bool,
|
||||
/// Whether post-execution core and decode outputs should be force-replayed.
|
||||
pub(crate) force_post_validation_replay: bool,
|
||||
}
|
||||
|
||||
/// UI-safe result of one public Token-2022 Devnet orchestration.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_spl_token_2022/DemoExecutionSplToken2022SummaryPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoExecutionSplToken2022SummaryPayload {
|
||||
/// Stable scenario identifier.
|
||||
pub(crate) scenario_id: std::string::String,
|
||||
/// Stable operation code.
|
||||
pub(crate) operation: std::string::String,
|
||||
/// Profile used by the orchestration.
|
||||
pub(crate) profile_name: std::string::String,
|
||||
/// Classified cluster code.
|
||||
pub(crate) cluster: std::string::String,
|
||||
/// Genesis hash returned by the endpoint.
|
||||
pub(crate) genesis_hash: std::string::String,
|
||||
/// Source wallet public key.
|
||||
pub(crate) wallet_public_key: std::string::String,
|
||||
/// Exact wallet balance rendered without JSON integer precision loss.
|
||||
pub(crate) balance_lamports: std::string::String,
|
||||
/// Estimated fee rendered without JSON integer precision loss.
|
||||
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
||||
/// Whether exact simulation succeeded.
|
||||
pub(crate) simulation_success: bool,
|
||||
/// Runtime simulation error when present.
|
||||
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
||||
/// Submitted transaction signature.
|
||||
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
||||
/// Terminal confirmation status.
|
||||
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
||||
/// Whether canonical hydration was validated.
|
||||
pub(crate) canonical_inserted: bool,
|
||||
/// Whether core extraction was validated.
|
||||
pub(crate) core_extracted: bool,
|
||||
/// Whether Token-2022 decode replay was validated.
|
||||
pub(crate) decode_replayed: bool,
|
||||
/// Number of materialized rows for the submitted instruction.
|
||||
pub(crate) materialization_count: u32,
|
||||
/// Whether the second replay produced no failure or new output.
|
||||
pub(crate) idempotence_validated: bool,
|
||||
/// Pretty JSON representation of the exact execution plan.
|
||||
pub(crate) plan_json: std::string::String,
|
||||
/// Pretty JSON representation of stateful preflight.
|
||||
pub(crate) preflight_json: std::string::String,
|
||||
/// Pretty JSON representation of the exact simulation result.
|
||||
pub(crate) simulation_json: std::string::String,
|
||||
/// Pretty JSON representation of confirmation and replay diagnostics.
|
||||
pub(crate) diagnostics_json: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn operation_from_request(
|
||||
request: &crate::DemoExecutionSplToken2022Request,
|
||||
) -> std::result::Result<kb_executor_spl_token_2022::SplToken2022Operation, std::string::String> {
|
||||
let authority = kb_executor_spl_token_2022::SplTokenAuthority {
|
||||
authority: kb_model::Pubkey(request.authority.trim().to_string()),
|
||||
multisig_signers: std::vec::Vec::new(),
|
||||
};
|
||||
let freeze_authority = kb_executor_spl_token_2022::SplTokenAuthority {
|
||||
authority: kb_model::Pubkey(request.freeze_authority.trim().to_string()),
|
||||
multisig_signers: std::vec::Vec::new(),
|
||||
};
|
||||
let value = match request.scenario_id.trim() {
|
||||
"token_2022_mint_to_checked" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::MintToChecked {
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
destination: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_executor_spl_token_2022::SplTokenAmount(
|
||||
request.amount_raw.trim().to_string(),
|
||||
),
|
||||
decimals: request.decimals,
|
||||
}
|
||||
},
|
||||
"token_2022_transfer_checked" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::TransferChecked {
|
||||
source: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
destination: kb_model::Pubkey(request.destination.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_executor_spl_token_2022::SplTokenAmount(
|
||||
request.amount_raw.trim().to_string(),
|
||||
),
|
||||
decimals: request.decimals,
|
||||
}
|
||||
},
|
||||
"token_2022_approve_checked" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::ApproveChecked {
|
||||
source: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
delegate: kb_model::Pubkey(request.delegate.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_executor_spl_token_2022::SplTokenAmount(
|
||||
request.amount_raw.trim().to_string(),
|
||||
),
|
||||
decimals: request.decimals,
|
||||
}
|
||||
},
|
||||
"token_2022_revoke" => kb_executor_spl_token_2022::SplTokenSingleOperation::Revoke {
|
||||
source: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
authority,
|
||||
},
|
||||
"token_2022_burn_checked" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::BurnChecked {
|
||||
source: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
authority,
|
||||
amount: kb_executor_spl_token_2022::SplTokenAmount(
|
||||
request.amount_raw.trim().to_string(),
|
||||
),
|
||||
decimals: request.decimals,
|
||||
}
|
||||
},
|
||||
"token_2022_freeze_account" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::FreezeAccount {
|
||||
account: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
authority: freeze_authority,
|
||||
}
|
||||
},
|
||||
"token_2022_thaw_account" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::ThawAccount {
|
||||
account: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
mint: kb_model::Pubkey(request.mint.trim().to_string()),
|
||||
authority: freeze_authority,
|
||||
}
|
||||
},
|
||||
"token_2022_close_destination" => {
|
||||
kb_executor_spl_token_2022::SplTokenSingleOperation::CloseAccount {
|
||||
account: kb_model::Pubkey(request.source.trim().to_string()),
|
||||
destination: kb_model::Pubkey(request.destination.trim().to_string()),
|
||||
authority,
|
||||
}
|
||||
},
|
||||
other => {
|
||||
return std::result::Result::Err(format!(
|
||||
"unsupported public Token-2022 Devnet scenario {other}"
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(
|
||||
kb_executor_spl_token_2022::SplToken2022Operation::Instruction {
|
||||
value: std::boxed::Box::new(value),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn demo_spl_token_2022_summary_payload(
|
||||
scenario_id: std::string::String,
|
||||
operation: std::string::String,
|
||||
summary: kb_pipeline::DevnetSplToken2022ExecutionSummary,
|
||||
) -> crate::DemoExecutionSplToken2022SummaryPayload {
|
||||
let plan_json = crate::pretty_json(&summary.plan);
|
||||
let preflight_json = crate::pretty_json(&summary.stateful_preflight);
|
||||
let simulation_json = crate::pretty_json(&summary.simulation);
|
||||
let transaction_signature =
|
||||
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
||||
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
||||
return crate::confirmation_status_code(value.status);
|
||||
});
|
||||
let post_execution = summary.post_execution.as_ref();
|
||||
let canonical_inserted = post_execution.is_some_and(|value| return value.canonical_inserted);
|
||||
let core_extracted = post_execution.is_some_and(|value| return value.core_extracted);
|
||||
let decode_replayed = post_execution.is_some_and(|value| return value.decode_replayed);
|
||||
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
||||
return replay.failed_inputs == 0
|
||||
&& !replay.cancelled
|
||||
&& replay.processors.iter().all(|processor| {
|
||||
return processor.failed == 0
|
||||
&& processor.materialized_outputs == 0
|
||||
&& processor.materialization_refused == 0;
|
||||
});
|
||||
});
|
||||
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
};
|
||||
let diagnostics = serde_json::json!({
|
||||
"confirmation": summary.confirmation,
|
||||
"postExecution": summary.post_execution,
|
||||
"backfill": summary.backfill.as_ref().map(backfill_summary_json),
|
||||
"coreExtraction": summary.core_extraction.as_ref().map(core_extraction_summary_json),
|
||||
"decodeReplay": summary.decode_replay.as_ref().map(decode_replay_summary_json),
|
||||
"idempotenceReplay": summary.idempotence_replay.as_ref().map(decode_replay_summary_json),
|
||||
"materializations": summary.materializations
|
||||
});
|
||||
return crate::DemoExecutionSplToken2022SummaryPayload {
|
||||
scenario_id,
|
||||
operation,
|
||||
profile_name: summary.profile_name,
|
||||
cluster: crate::execution_cluster_code(summary.cluster),
|
||||
genesis_hash: summary.genesis_hash,
|
||||
wallet_public_key: summary.wallet.public_key,
|
||||
balance_lamports: summary.balance_lamports.to_string(),
|
||||
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
||||
simulation_success: summary.simulation.success,
|
||||
simulation_error: summary.simulation.error,
|
||||
transaction_signature,
|
||||
confirmation_status,
|
||||
canonical_inserted,
|
||||
core_extracted,
|
||||
decode_replayed,
|
||||
materialization_count,
|
||||
idempotence_validated,
|
||||
plan_json,
|
||||
preflight_json,
|
||||
simulation_json,
|
||||
diagnostics_json: crate::pretty_json(&diagnostics),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn parse_fixture(
|
||||
contents: &str,
|
||||
) -> std::collections::BTreeMap<std::string::String, std::string::String> {
|
||||
let mut values = std::collections::BTreeMap::new();
|
||||
for line in contents.lines() {
|
||||
let trimmed = line.trim();
|
||||
let assignment = match trimmed.strip_prefix("export ") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let (name, raw_value) = match assignment.split_once('=') {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let value = raw_value.trim().trim_matches('\'').trim_matches('"').to_string();
|
||||
values.insert(name.trim().to_string(), value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
fn backfill_summary_json(summary: &kb_pipeline::BackfillSummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"captureSessionId": summary.capture_session_id,
|
||||
"filterCode": summary.filter_code,
|
||||
"role": summary.role,
|
||||
"provider": summary.provider,
|
||||
"endpointCode": summary.endpoint_code,
|
||||
"pagesFetched": summary.pages_fetched,
|
||||
"candidatesSelected": summary.candidates_selected,
|
||||
"candidatesStarted": summary.candidates_started,
|
||||
"candidatesCompleted": summary.candidates_completed,
|
||||
"candidatesCancelled": summary.candidates_cancelled,
|
||||
"candidatesNotStarted": summary.candidates_not_started,
|
||||
"transactionsReceived": summary.transactions_received,
|
||||
"canonicalInserted": summary.canonical_inserted,
|
||||
"canonicalSkipped": summary.canonical_skipped,
|
||||
"existingSkipped": summary.existing_skipped,
|
||||
"missing": summary.missing,
|
||||
"failed": summary.failed,
|
||||
"observationsInserted": summary.observations_inserted,
|
||||
"attempts": summary.attempts,
|
||||
"cancelled": summary.cancelled,
|
||||
"resumeBeforeSignature": summary.resume_before_signature,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
fn core_extraction_summary_json(summary: &kb_pipeline::CoreExtractionSummary) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"processorVersion": summary.processor_version,
|
||||
"selected": summary.selected,
|
||||
"started": summary.started,
|
||||
"completed": summary.completed,
|
||||
"skipped": summary.skipped,
|
||||
"extracted": summary.extracted,
|
||||
"failed": summary.failed,
|
||||
"cancelledCandidates": summary.cancelled_candidates,
|
||||
"notStarted": summary.not_started,
|
||||
"cancelled": summary.cancelled,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_replay_summary_json(summary: &kb_pipeline::DecodeReplaySummary) -> serde_json::Value {
|
||||
let processors = summary
|
||||
.processors
|
||||
.iter()
|
||||
.map(|processor| {
|
||||
return serde_json::json!({
|
||||
"name": processor.processor_name,
|
||||
"version": processor.processor_version,
|
||||
"dispatched": processor.dispatched,
|
||||
"skipped": processor.skipped,
|
||||
"decoded": processor.decoded,
|
||||
"ignored": processor.ignored,
|
||||
"unsupported": processor.unsupported,
|
||||
"failed": processor.failed,
|
||||
"materializedOutputs": processor.materialized_outputs,
|
||||
"materializationRefused": processor.materialization_refused
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<serde_json::Value>>();
|
||||
return serde_json::json!({
|
||||
"campaignId": summary.campaign_id,
|
||||
"pipelineVersion": summary.pipeline_version,
|
||||
"selected": summary.selected,
|
||||
"started": summary.started,
|
||||
"completed": summary.completed,
|
||||
"unmatched": summary.unmatched,
|
||||
"notStarted": summary.not_started,
|
||||
"failedInputs": summary.failed_inputs,
|
||||
"cancelled": summary.cancelled,
|
||||
"processors": processors,
|
||||
"startedAt": summary.started_at,
|
||||
"finishedAt": summary.finished_at
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn request(scenario_id: &str) -> crate::DemoExecutionSplToken2022Request {
|
||||
return crate::DemoExecutionSplToken2022Request {
|
||||
profile_name: "local_devnet".to_string(),
|
||||
scenario_id: scenario_id.to_string(),
|
||||
source: "source".to_string(),
|
||||
mint: "mint".to_string(),
|
||||
destination: "destination".to_string(),
|
||||
delegate: "delegate".to_string(),
|
||||
authority: "authority".to_string(),
|
||||
freeze_authority: "freeze".to_string(),
|
||||
amount_raw: "1".to_string(),
|
||||
decimals: 9,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
force_post_validation_replay: true,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_public_scenario_builds_one_typed_operation() {
|
||||
for scenario in [
|
||||
"token_2022_mint_to_checked",
|
||||
"token_2022_transfer_checked",
|
||||
"token_2022_approve_checked",
|
||||
"token_2022_revoke",
|
||||
"token_2022_burn_checked",
|
||||
"token_2022_freeze_account",
|
||||
"token_2022_thaw_account",
|
||||
"token_2022_close_destination",
|
||||
] {
|
||||
assert!(crate::operation_from_request(&request(scenario)).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
// file: kb_app_demo/src/demo_sql_common.rs
|
||||
// version: 7
|
||||
|
||||
//! Shared SQL demo helpers and serializable payloads.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// UI-safe table diagnostics shown by SQL demo windows.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql/DemoSqlTableSnapshot.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlTableSnapshot {
|
||||
/// Table name.
|
||||
pub(crate) table_name: std::string::String,
|
||||
/// Logical Solana domain encoded in the table name.
|
||||
pub(crate) domain: std::string::String,
|
||||
/// Human-readable table role.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Whether the table currently exists.
|
||||
pub(crate) exists: bool,
|
||||
/// Row count when the table exists.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) row_count: std::option::Option<i64>,
|
||||
/// Minimum slot when available.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) min_slot: std::option::Option<i64>,
|
||||
/// Maximum slot when available.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) max_slot: std::option::Option<i64>,
|
||||
/// Latest insertion timestamp rendered by PostgreSQL when available.
|
||||
pub(crate) latest_created_at: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Opens or focuses a SQL demo window.
|
||||
pub(crate) fn open_sql_demo_window(
|
||||
app_handle: tauri::AppHandle,
|
||||
label: &str,
|
||||
html_path: &str,
|
||||
title: &str,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
tracing::info!(target: crate::TRACING_TARGET, window = label, "open SQL demo window");
|
||||
let existing_window = app_handle.get_webview_window(label);
|
||||
if let std::option::Option::Some(window) = existing_window {
|
||||
let show_result = window.show();
|
||||
if let std::result::Result::Err(error) = show_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot show {label} window: {error}")).to_string(),
|
||||
);
|
||||
}
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot focus {label} window: {error}")).to_string(),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let builder = tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
label,
|
||||
tauri::WebviewUrl::App(html_path.into()),
|
||||
)
|
||||
.title(title)
|
||||
.inner_size(1220.0, 780.0)
|
||||
.min_inner_size(920.0, 560.0)
|
||||
.resizable(true)
|
||||
.visible(true);
|
||||
let build_result = builder.build();
|
||||
return match build_result {
|
||||
std::result::Result::Ok(window) => {
|
||||
let focus_result = window.set_focus();
|
||||
if let std::result::Result::Err(error) = focus_result {
|
||||
return std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot focus created {label} window: {error}"))
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
std::result::Result::Ok(())
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
kb_core::Error::tauri(format!("cannot create {label} window: {error}")).to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Connects to PostgreSQL without reapplying schemas initialized at application startup.
|
||||
pub(crate) async fn connect_postgres_store(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
) -> std::result::Result<kb_store_pg::PostgresStore, std::string::String> {
|
||||
if !profile.database.enabled {
|
||||
return std::result::Result::Err(std::string::String::from(
|
||||
"database configuration is disabled in the active profile",
|
||||
));
|
||||
}
|
||||
let options_result = kb_store_pg::PostgresStoreOptions::from_profile_config(profile);
|
||||
let mut options = match options_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
options.auto_initialize_schema = false;
|
||||
let store_result = kb_store_pg::PostgresStore::connect(options).await;
|
||||
return match store_result {
|
||||
std::result::Result::Ok(store) => std::result::Result::Ok(store),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts store table diagnostics to the UI payload shape.
|
||||
pub(crate) fn table_snapshot_from_pg(
|
||||
value: &kb_store_pg::PostgresTableDiagnostics,
|
||||
) -> crate::DemoSqlTableSnapshot {
|
||||
let row_count = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let min_slot = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.min_slot,
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let max_slot = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.max_slot,
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let latest_created_at = match &value.statistics {
|
||||
std::option::Option::Some(statistics) => statistics.latest_created_at.clone(),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
return crate::DemoSqlTableSnapshot {
|
||||
table_name: value.table_name.clone(),
|
||||
domain: value.domain.clone(),
|
||||
role: value.role.clone(),
|
||||
exists: value.exists,
|
||||
row_count,
|
||||
min_slot,
|
||||
max_slot,
|
||||
latest_created_at,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts many table diagnostics to UI payloads.
|
||||
pub(crate) fn table_snapshots_from_pg(
|
||||
values: &[kb_store_pg::PostgresTableDiagnostics],
|
||||
) -> std::vec::Vec<crate::DemoSqlTableSnapshot> {
|
||||
let mut output = std::vec::Vec::new();
|
||||
for value in values {
|
||||
output.push(table_snapshot_from_pg(value));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup.
|
||||
pub(crate) async fn initialize_postgres_schema_for_startup(
|
||||
state: &crate::AppState,
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
) {
|
||||
let profile = state.active_profile();
|
||||
if !profile.database.enabled {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"SQL store disabled; schema initialization skipped.",
|
||||
"info",
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization");
|
||||
return;
|
||||
}
|
||||
if profile.database.backend != "postgres" {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"SQL backend is not PostgreSQL; schema initialization skipped.",
|
||||
"info",
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization");
|
||||
return;
|
||||
}
|
||||
let options_result = kb_store_pg::PostgresStoreOptions::from_profile_config(profile);
|
||||
let mut options = match options_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let error_message = error.to_string();
|
||||
emit_sql_startup_error(splash_window, error_message.as_str());
|
||||
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot build PostgreSQL store options");
|
||||
return;
|
||||
},
|
||||
};
|
||||
if !options.auto_initialize_schema {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"PostgreSQL schema auto-initialization disabled.",
|
||||
"info",
|
||||
false,
|
||||
);
|
||||
let masked_dsn = options.masked_dsn();
|
||||
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "PostgreSQL schema auto-initialization disabled");
|
||||
return;
|
||||
}
|
||||
let masked_dsn = options.masked_dsn();
|
||||
options.auto_initialize_schema = false;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
"Checking PostgreSQL raw/core/decode tables...",
|
||||
"info",
|
||||
true,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization");
|
||||
let store_result = kb_store_pg::PostgresStore::connect(options).await;
|
||||
let store = match store_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let error_message = error.to_string();
|
||||
emit_sql_startup_error(splash_window, error_message.as_str());
|
||||
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL connection failed during startup schema initialization");
|
||||
return;
|
||||
},
|
||||
};
|
||||
let before_result = store.known_table_diagnostics().await;
|
||||
let before_tables = match before_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let error_message = error.to_string();
|
||||
emit_sql_startup_error(splash_window, error_message.as_str());
|
||||
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics before schema initialization");
|
||||
return;
|
||||
},
|
||||
};
|
||||
let initialize_result = store.initialize_store_schema().await;
|
||||
if let std::result::Result::Err(error) = initialize_result {
|
||||
let error_message = error.to_string();
|
||||
emit_sql_startup_error(splash_window, error_message.as_str());
|
||||
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed");
|
||||
return;
|
||||
}
|
||||
let after_result = store.known_table_diagnostics().await;
|
||||
let after_tables = match after_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
let error_message = error.to_string();
|
||||
emit_sql_startup_error(splash_window, error_message.as_str());
|
||||
tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics after schema initialization");
|
||||
return;
|
||||
},
|
||||
};
|
||||
emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice());
|
||||
}
|
||||
|
||||
fn emit_sql_startup_table_report(
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
before_tables: &[kb_store_pg::PostgresTableDiagnostics],
|
||||
after_tables: &[kb_store_pg::PostgresTableDiagnostics],
|
||||
) {
|
||||
let mut created_count = 0_u32;
|
||||
let mut existing_count = 0_u32;
|
||||
let mut missing_count = 0_u32;
|
||||
for table in after_tables {
|
||||
let before_exists = table_exists_in(before_tables, table.table_name.as_str());
|
||||
if table.exists && before_exists == std::option::Option::Some(false) {
|
||||
created_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Created table {}", table.table_name).as_str(),
|
||||
"success",
|
||||
true,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created");
|
||||
} else if table.exists {
|
||||
existing_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Table {} already exists", table.table_name).as_str(),
|
||||
"info",
|
||||
true,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists");
|
||||
} else {
|
||||
missing_count += 1;
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("Table {} is missing after initialization", table.table_name).as_str(),
|
||||
"warning",
|
||||
true,
|
||||
);
|
||||
tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization");
|
||||
}
|
||||
}
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!(
|
||||
"PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing."
|
||||
)
|
||||
.as_str(),
|
||||
schema_report_status(missing_count),
|
||||
false,
|
||||
);
|
||||
tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed");
|
||||
}
|
||||
|
||||
fn table_exists_in(
|
||||
tables: &[kb_store_pg::PostgresTableDiagnostics],
|
||||
table_name: &str,
|
||||
) -> std::option::Option<bool> {
|
||||
for table in tables {
|
||||
if table.table_name.as_str() == table_name {
|
||||
return std::option::Option::Some(table.exists);
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn schema_report_status(missing_count: u32) -> &'static str {
|
||||
if missing_count == 0 {
|
||||
return "success";
|
||||
}
|
||||
return "warning";
|
||||
}
|
||||
|
||||
fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) {
|
||||
emit_sql_startup_splash(
|
||||
splash_window,
|
||||
format!("PostgreSQL schema initialization error: {message}").as_str(),
|
||||
"danger",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_sql_startup_splash(
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
message: &str,
|
||||
status: &str,
|
||||
log_only: bool,
|
||||
) {
|
||||
let order = if log_only { "add_log" } else { "add_msg" };
|
||||
crate::emit_splash_order(
|
||||
splash_window,
|
||||
order,
|
||||
std::option::Option::Some(message),
|
||||
std::option::Option::Some(status),
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats debug enums for UI display.
|
||||
pub(crate) fn debug_status<T: std::fmt::Debug>(value: T) -> std::string::String {
|
||||
return format!("{value:?}");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// file: kb_app_demo/src/demo_sql_diag.rs
|
||||
// version: 2
|
||||
|
||||
//! SQL diagnostic demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Complete SQL diagnostic payload shown by `demo_sql_diag`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_diag/DemoSqlDiagPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlDiagPayload {
|
||||
/// Configuration file path.
|
||||
pub(crate) config_path: std::string::String,
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Configured database backend.
|
||||
pub(crate) backend: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// PostgreSQL current schema.
|
||||
pub(crate) current_schema: std::option::Option<std::string::String>,
|
||||
/// PostgreSQL health status.
|
||||
pub(crate) health_status: std::string::String,
|
||||
/// PostgreSQL health message.
|
||||
pub(crate) health_message: std::option::Option<std::string::String>,
|
||||
/// Migration status.
|
||||
pub(crate) migration_status: std::string::String,
|
||||
/// Migration diagnostic message.
|
||||
pub(crate) migration_message: std::option::Option<std::string::String>,
|
||||
/// PostgreSQL server version when available.
|
||||
pub(crate) server_version: std::option::Option<std::string::String>,
|
||||
/// Known raw/core table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// file: kb_app_demo/src/demo_sql_pg_core.rs
|
||||
// version: 3
|
||||
|
||||
//! PostgreSQL core store demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// PostgreSQL core store payload shown by `demo_sql_pg_core`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_pg_core/DemoSqlPgCorePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlPgCorePayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Core table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// file: kb_app_demo/src/demo_sql_pg_raw.rs
|
||||
// version: 4
|
||||
|
||||
//! PostgreSQL canonical acquisition store demo commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_pg_raw/DemoSqlPgRawPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlPgRawPayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Raw table diagnostics.
|
||||
pub(crate) tables: std::vec::Vec<crate::DemoSqlTableSnapshot>,
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// file: kb_app_demo/src/demo_sql_replay_candidates.rs
|
||||
// version: 6
|
||||
|
||||
//! Read-only SQL replay candidate browser commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One program identifier exposed by the runtime `kb_program_ids` registry.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayKnownProgramOption {
|
||||
/// Stable lower snake case program code.
|
||||
pub(crate) code: std::string::String,
|
||||
/// Base58 Solana program identifier.
|
||||
pub(crate) program_id: std::string::String,
|
||||
}
|
||||
|
||||
/// Static options for the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayOptionsPayload {
|
||||
/// Active profile name.
|
||||
pub(crate) active_profile_name: std::string::String,
|
||||
/// Masked PostgreSQL DSN.
|
||||
pub(crate) masked_dsn: std::string::String,
|
||||
/// Maximum rows accepted by one query.
|
||||
pub(crate) maximum_limit: u32,
|
||||
/// Program identifiers enumerable from `kb_program_ids`.
|
||||
pub(crate) known_programs: std::vec::Vec<crate::DemoSqlReplayKnownProgramOption>,
|
||||
}
|
||||
|
||||
/// UI request for bounded transaction replay candidates.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayTransactionRequest {
|
||||
/// Optional partial signature search.
|
||||
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
||||
/// Optional inclusive minimum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) min_slot: std::option::Option<u64>,
|
||||
/// Optional inclusive maximum slot.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) max_slot: std::option::Option<u64>,
|
||||
/// Optional raw processing state.
|
||||
pub(crate) raw_processing_state: std::option::Option<std::string::String>,
|
||||
/// Optional latest ledger status.
|
||||
pub(crate) ledger_status: std::option::Option<std::string::String>,
|
||||
/// Optional exact program id.
|
||||
pub(crate) program_id: std::option::Option<std::string::String>,
|
||||
/// Program scope code: any, outer, inner or logs.
|
||||
pub(crate) program_scope: std::string::String,
|
||||
/// Optional entity kind: mint, owner or account_key.
|
||||
pub(crate) entity_kind: std::option::Option<std::string::String>,
|
||||
/// Optional exact entity value.
|
||||
pub(crate) entity_value: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
/// Orders newest slots first when true.
|
||||
pub(crate) newest_first: bool,
|
||||
}
|
||||
|
||||
/// One transaction row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayTransactionRow {
|
||||
/// Canonical transaction signature.
|
||||
pub(crate) signature: std::string::String,
|
||||
/// Transaction slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) slot: i64,
|
||||
/// Current raw processing state.
|
||||
pub(crate) raw_processing_state: std::string::String,
|
||||
/// Current raw retention state.
|
||||
pub(crate) retention_state: std::string::String,
|
||||
/// Whether a core transaction exists.
|
||||
pub(crate) has_core_transaction: bool,
|
||||
/// Core transaction failure flag when available.
|
||||
pub(crate) transaction_failed: std::option::Option<bool>,
|
||||
/// Latest core extraction ledger status.
|
||||
pub(crate) ledger_status: std::string::String,
|
||||
/// Latest processor version when available.
|
||||
pub(crate) processor_version: std::option::Option<std::string::String>,
|
||||
/// Latest attempt count.
|
||||
pub(crate) attempt_count: i32,
|
||||
/// Number of top-level instructions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_instruction_count: i64,
|
||||
/// Number of inner instructions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_instruction_count: i64,
|
||||
/// Number of distinct top-level programs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_program_count: i64,
|
||||
/// Number of distinct inner programs.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_program_count: i64,
|
||||
/// Raw row update timestamp.
|
||||
pub(crate) updated_at: std::string::String,
|
||||
}
|
||||
|
||||
/// UI request for bounded program summaries.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayProgramRequest {
|
||||
/// Optional partial program id search.
|
||||
pub(crate) program_id_contains: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// Aggregated program row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayProgramRow {
|
||||
/// Optional stable code from `kb_program_ids`.
|
||||
pub(crate) program_code: std::option::Option<std::string::String>,
|
||||
/// Program id.
|
||||
pub(crate) program_id: std::string::String,
|
||||
/// Number of distinct transactions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) transaction_count: i64,
|
||||
/// Number of top-level instruction occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) outer_instruction_count: i64,
|
||||
/// Number of inner instruction occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) inner_instruction_count: i64,
|
||||
/// Number of reliably linked log occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) log_count: i64,
|
||||
/// Lowest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) min_slot: i64,
|
||||
/// Highest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_slot: i64,
|
||||
}
|
||||
|
||||
/// UI request for bounded core entity summaries.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayEntityRequest {
|
||||
/// Entity kind: mint, owner or account_key.
|
||||
pub(crate) entity_kind: std::string::String,
|
||||
/// Optional partial entity value search.
|
||||
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
|
||||
/// Maximum returned rows.
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
/// Aggregated core entity row shown by the replay candidate browser.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts"
|
||||
)]
|
||||
pub(crate) struct DemoSqlReplayEntityRow {
|
||||
/// Stable entity kind code.
|
||||
pub(crate) entity_kind: std::string::String,
|
||||
/// Mint, owner or account-key address.
|
||||
pub(crate) entity_value: std::string::String,
|
||||
/// Number of distinct transactions.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) transaction_count: i64,
|
||||
/// Total number of core-table occurrences.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) occurrence_count: i64,
|
||||
/// Lowest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) min_slot: i64,
|
||||
/// Highest observed slot.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) max_slot: i64,
|
||||
}
|
||||
|
||||
pub(crate) fn transaction_row_from_pg(
|
||||
row: kb_store_pg::PostgresReplayTransactionCandidate,
|
||||
) -> crate::DemoSqlReplayTransactionRow {
|
||||
return crate::DemoSqlReplayTransactionRow {
|
||||
signature: row.signature,
|
||||
slot: row.slot,
|
||||
raw_processing_state: row.raw_processing_state,
|
||||
retention_state: row.retention_state,
|
||||
has_core_transaction: row.has_core_transaction,
|
||||
transaction_failed: row.transaction_failed,
|
||||
ledger_status: row.ledger_status,
|
||||
processor_version: row.processor_version,
|
||||
attempt_count: row.attempt_count,
|
||||
outer_instruction_count: row.outer_instruction_count,
|
||||
inner_instruction_count: row.inner_instruction_count,
|
||||
outer_program_count: row.outer_program_count,
|
||||
inner_program_count: row.inner_program_count,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn program_row_from_pg(
|
||||
row: kb_store_pg::PostgresReplayProgramSummary,
|
||||
) -> crate::DemoSqlReplayProgramRow {
|
||||
let program_code = kb_program_ids::find_registered_program_id(&row.program_id)
|
||||
.map(|entry| return entry.code().to_owned());
|
||||
return crate::DemoSqlReplayProgramRow {
|
||||
program_code,
|
||||
program_id: row.program_id,
|
||||
transaction_count: row.transaction_count,
|
||||
outer_instruction_count: row.outer_instruction_count,
|
||||
inner_instruction_count: row.inner_instruction_count,
|
||||
log_count: row.log_count,
|
||||
min_slot: row.min_slot,
|
||||
max_slot: row.max_slot,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn entity_row_from_pg(
|
||||
row: kb_store_pg::PostgresReplayEntitySummary,
|
||||
) -> crate::DemoSqlReplayEntityRow {
|
||||
return crate::DemoSqlReplayEntityRow {
|
||||
entity_kind: row.entity_kind,
|
||||
entity_value: row.entity_value,
|
||||
transaction_count: row.transaction_count,
|
||||
occurrence_count: row.occurrence_count,
|
||||
min_slot: row.min_slot,
|
||||
max_slot: row.max_slot,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn validated_csv_file_name(
|
||||
file_name: &str,
|
||||
) -> std::result::Result<&'static str, std::string::String> {
|
||||
return match file_name {
|
||||
"replay_transactions.csv" => std::result::Result::Ok("replay_transactions.csv"),
|
||||
"replay_programs.csv" => std::result::Result::Ok("replay_programs.csv"),
|
||||
"replay_mints.csv" => std::result::Result::Ok("replay_mints.csv"),
|
||||
"replay_owners.csv" => std::result::Result::Ok("replay_owners.csv"),
|
||||
"replay_account_keys.csv" => std::result::Result::Ok("replay_account_keys.csv"),
|
||||
_ => std::result::Result::Err(format!("unsupported replay CSV file name: {file_name}")),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn csv_export_directory_from_current_dir(
|
||||
current_dir: &std::path::Path,
|
||||
) -> std::path::PathBuf {
|
||||
let current_name = current_dir.file_name().and_then(std::ffi::OsStr::to_str);
|
||||
if let (std::option::Option::Some("kb_app_demo"), std::option::Option::Some(parent)) =
|
||||
(current_name, current_dir.parent())
|
||||
{
|
||||
return parent.join("data").join("exports_csv");
|
||||
}
|
||||
return current_dir.join("data").join("exports_csv");
|
||||
}
|
||||
|
||||
pub(crate) async fn available_csv_export_path(
|
||||
export_dir: &std::path::Path,
|
||||
file_name: &str,
|
||||
) -> std::result::Result<std::path::PathBuf, std::string::String> {
|
||||
let direct_path = export_dir.join(file_name);
|
||||
let direct_exists_result = tokio::fs::try_exists(&direct_path).await;
|
||||
let direct_exists = match direct_exists_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!("cannot inspect CSV export path: {error}"));
|
||||
},
|
||||
};
|
||||
if !direct_exists {
|
||||
return std::result::Result::Ok(direct_path);
|
||||
}
|
||||
let stem = file_name.trim_end_matches(".csv");
|
||||
for suffix in 1_u16..=999_u16 {
|
||||
let candidate = export_dir.join(format!("{stem}_{suffix}.csv"));
|
||||
let exists_result = tokio::fs::try_exists(&candidate).await;
|
||||
let exists = match exists_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"cannot inspect CSV export path: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
if !exists {
|
||||
return std::result::Result::Ok(candidate);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Err("cannot allocate a unique CSV export path".to_owned());
|
||||
}
|
||||
|
||||
pub(crate) fn program_scope_from_code(
|
||||
code: &str,
|
||||
) -> std::result::Result<kb_store_pg::PostgresReplayProgramScope, std::string::String> {
|
||||
return match code {
|
||||
"any" => std::result::Result::Ok(kb_store_pg::PostgresReplayProgramScope::Any),
|
||||
"outer" => std::result::Result::Ok(kb_store_pg::PostgresReplayProgramScope::Outer),
|
||||
"inner" => std::result::Result::Ok(kb_store_pg::PostgresReplayProgramScope::Inner),
|
||||
"logs" => std::result::Result::Ok(kb_store_pg::PostgresReplayProgramScope::Logs),
|
||||
_ => std::result::Result::Err(format!("unsupported replay program scope: {code}")),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn optional_entity_kind_from_code(
|
||||
code: std::option::Option<&str>,
|
||||
) -> std::result::Result<
|
||||
std::option::Option<kb_store_pg::PostgresReplayEntityKind>,
|
||||
std::string::String,
|
||||
> {
|
||||
return match code {
|
||||
std::option::Option::Some(value) => {
|
||||
let result = entity_kind_from_code(value);
|
||||
match result {
|
||||
std::result::Result::Ok(kind) => {
|
||||
std::result::Result::Ok(std::option::Option::Some(kind))
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn entity_kind_from_code(
|
||||
code: &str,
|
||||
) -> std::result::Result<kb_store_pg::PostgresReplayEntityKind, std::string::String> {
|
||||
return match code {
|
||||
"mint" => std::result::Result::Ok(kb_store_pg::PostgresReplayEntityKind::Mint),
|
||||
"owner" => std::result::Result::Ok(kb_store_pg::PostgresReplayEntityKind::Owner),
|
||||
"account_key" => std::result::Result::Ok(kb_store_pg::PostgresReplayEntityKind::AccountKey),
|
||||
_ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn program_scope_parser_accepts_logs() {
|
||||
let result = crate::program_scope_from_code("logs");
|
||||
assert_eq!(result, std::result::Result::Ok(kb_store_pg::PostgresReplayProgramScope::Logs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_kind_parser_rejects_unknown_code() {
|
||||
let result = crate::entity_kind_from_code("unknown");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_file_name_rejects_unknown_name() {
|
||||
let result = crate::validated_csv_file_name("arbitrary.csv");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_export_directory_uses_workspace_root_from_demo_crate() {
|
||||
let current_dir = std::path::Path::new("/tmp/khadhroony-bot2/kb_app_demo");
|
||||
let result = crate::csv_export_directory_from_current_dir(current_dir);
|
||||
assert_eq!(result, std::path::Path::new("/tmp/khadhroony-bot2/data/exports_csv"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_file_name_accepts_split_entity_exports() {
|
||||
let mint_result = crate::validated_csv_file_name("replay_mints.csv");
|
||||
let owner_result = crate::validated_csv_file_name("replay_owners.csv");
|
||||
let account_result = crate::validated_csv_file_name("replay_account_keys.csv");
|
||||
assert!(mint_result.is_ok());
|
||||
assert!(owner_result.is_ok());
|
||||
assert!(account_result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_row_exposes_registered_code() {
|
||||
let row = kb_store_pg::PostgresReplayProgramSummary {
|
||||
program_id: kb_program_ids::SYSTEM_PROGRAM_ID.to_owned(),
|
||||
transaction_count: 1,
|
||||
outer_instruction_count: 1,
|
||||
inner_instruction_count: 0,
|
||||
log_count: 1,
|
||||
min_slot: 1,
|
||||
max_slot: 1,
|
||||
};
|
||||
let converted = crate::program_row_from_pg(row);
|
||||
assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system"));
|
||||
}
|
||||
}
|
||||
869
migration/khadhroony-bot2-reference/kb_app_demo/src/demo_ws.rs
Normal file
869
migration/khadhroony-bot2-reference/kb_app_demo/src/demo_ws.rs
Normal file
@@ -0,0 +1,869 @@
|
||||
// file: kb_app_demo/src/demo_ws.rs
|
||||
// version: 13
|
||||
|
||||
//! Standard Solana WebSocket demo commands backed by `kb_rpc::WsSession`.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// One selectable role shown by the WebSocket demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsRoleOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsRoleOption {
|
||||
/// Endpoint role code.
|
||||
pub(crate) role: std::string::String,
|
||||
/// Request kinds accepted by this role.
|
||||
pub(crate) request_kinds: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// One selectable WebSocket JSON-RPC method shown by the WebSocket demo.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsMethodOption.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsMethodOption {
|
||||
/// JSON-RPC subscribe method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Matching unsubscribe method name.
|
||||
pub(crate) unsubscribe_method: std::string::String,
|
||||
/// Derived request kind used for endpoint routing.
|
||||
pub(crate) request_kind: std::string::String,
|
||||
/// Human-readable method label.
|
||||
pub(crate) label: std::string::String,
|
||||
/// Whether this method needs the target field.
|
||||
pub(crate) requires_target: bool,
|
||||
/// Whether this method needs the filter JSON field.
|
||||
pub(crate) requires_filter_json: bool,
|
||||
/// Whether this method supports an optional configuration object.
|
||||
pub(crate) supports_config_json: bool,
|
||||
}
|
||||
|
||||
/// WebSocket demo options derived from configuration and the standard registry.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsOptionsPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsOptionsPayload {
|
||||
/// Selectable roles.
|
||||
pub(crate) roles: std::vec::Vec<crate::DemoWsRoleOption>,
|
||||
/// Selectable methods.
|
||||
pub(crate) methods: std::vec::Vec<crate::DemoWsMethodOption>,
|
||||
}
|
||||
|
||||
/// Request payload for one WebSocket subscription demo command.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsRequest {
|
||||
/// Required endpoint role used by the WebSocket pool.
|
||||
pub(crate) role: std::string::String,
|
||||
/// JSON-RPC WebSocket subscribe method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Optional target string used by account/program/signature subscriptions.
|
||||
pub(crate) target: std::option::Option<std::string::String>,
|
||||
/// Optional JSON filter string used by logsSubscribe or blockSubscribe.
|
||||
pub(crate) filter_json: std::option::Option<std::string::String>,
|
||||
/// Optional JSON configuration string.
|
||||
pub(crate) config_json: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Response payload for one WebSocket subscription response.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsExecutionPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsExecutionPayload {
|
||||
/// Selected endpoint name.
|
||||
pub(crate) endpoint_name: std::string::String,
|
||||
/// Selected provider name.
|
||||
pub(crate) provider: std::string::String,
|
||||
/// Selected endpoint URL.
|
||||
pub(crate) endpoint_url: std::string::String,
|
||||
/// Required role used by the selection.
|
||||
pub(crate) role: std::string::String,
|
||||
/// JSON-RPC method name.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Derived request kind.
|
||||
pub(crate) request_kind: std::string::String,
|
||||
/// Parsed response kind.
|
||||
pub(crate) response_kind: std::string::String,
|
||||
/// Remote subscription id returned by the node.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) subscription_id: std::option::Option<u64>,
|
||||
/// Pretty JSON response text.
|
||||
pub(crate) response_json: std::string::String,
|
||||
}
|
||||
|
||||
/// Current WebSocket demo session status.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsStatusPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsStatusPayload {
|
||||
/// Whether a session is currently connected.
|
||||
pub(crate) connected: bool,
|
||||
/// Selected endpoint name.
|
||||
pub(crate) endpoint_name: std::option::Option<std::string::String>,
|
||||
/// Selected endpoint URL.
|
||||
pub(crate) endpoint_url: std::option::Option<std::string::String>,
|
||||
/// Last subscription method.
|
||||
pub(crate) method: std::option::Option<std::string::String>,
|
||||
/// Last unsubscribe method.
|
||||
pub(crate) unsubscribe_method: std::option::Option<std::string::String>,
|
||||
/// Last remote subscription id.
|
||||
#[ts(type = "number | null")]
|
||||
pub(crate) subscription_id: std::option::Option<u64>,
|
||||
/// Number of active subscriptions on the current connection.
|
||||
pub(crate) subscription_count: u32,
|
||||
/// Active subscriptions kept by the session.
|
||||
pub(crate) subscriptions: std::vec::Vec<crate::DemoWsSubscriptionStatusPayload>,
|
||||
}
|
||||
|
||||
/// One active WebSocket subscription shown by the demo UI.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsSubscriptionStatusPayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsSubscriptionStatusPayload {
|
||||
/// Remote subscription id.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) subscription_id: u64,
|
||||
/// Subscribe method used to create this subscription.
|
||||
pub(crate) method: std::string::String,
|
||||
/// Matching unsubscribe method.
|
||||
pub(crate) unsubscribe_method: std::string::String,
|
||||
}
|
||||
|
||||
/// Request payload for one explicit WebSocket unsubscribe command.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsUnsubscribeRequest.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsUnsubscribeRequest {
|
||||
/// Remote subscription id to unsubscribe.
|
||||
#[ts(type = "number")]
|
||||
pub(crate) subscription_id: u64,
|
||||
}
|
||||
|
||||
/// WebSocket event payload emitted to the demo window.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/demo_ws/DemoWsMessagePayload.ts"
|
||||
)]
|
||||
pub(crate) struct DemoWsMessagePayload {
|
||||
/// Message kind.
|
||||
pub(crate) kind: std::string::String,
|
||||
/// Pretty JSON or text payload.
|
||||
pub(crate) payload_json: std::string::String,
|
||||
}
|
||||
|
||||
const DEMO_WS_UI_RATE_WINDOW_MS: u64 = 1000;
|
||||
const DEMO_WS_UI_MAX_MESSAGES_PER_WINDOW: u32 = 12;
|
||||
const DEMO_WS_UI_MAX_PAYLOAD_CHARS: usize = 8_000;
|
||||
const DEMO_WS_RECONNECT_ATTEMPTS: u32 = 3;
|
||||
const DEMO_WS_RECONNECT_INITIAL_DELAY_MS: u64 = 250;
|
||||
const DEMO_WS_RECONNECT_MAX_DELAY_MS: u64 = 2_000;
|
||||
|
||||
struct DemoWsUiRateLimiter {
|
||||
window_started_at: std::time::Instant,
|
||||
emitted_in_window: u32,
|
||||
dropped_in_window: u32,
|
||||
}
|
||||
|
||||
impl DemoWsUiRateLimiter {
|
||||
fn new() -> Self {
|
||||
return Self {
|
||||
window_started_at: std::time::Instant::now(),
|
||||
emitted_in_window: 0,
|
||||
dropped_in_window: 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn should_emit(&mut self, app_handle: &tauri::AppHandle) -> bool {
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(self.window_started_at)
|
||||
>= std::time::Duration::from_millis(DEMO_WS_UI_RATE_WINDOW_MS)
|
||||
{
|
||||
if self.dropped_in_window > 0 {
|
||||
emit_demo_ws_message(
|
||||
app_handle,
|
||||
"throttled",
|
||||
format!(
|
||||
"{} WebSocket notification(s) masquée(s) pendant la dernière fenêtre UI.",
|
||||
self.dropped_in_window
|
||||
),
|
||||
);
|
||||
}
|
||||
self.window_started_at = now;
|
||||
self.emitted_in_window = 0;
|
||||
self.dropped_in_window = 0;
|
||||
}
|
||||
if self.emitted_in_window >= DEMO_WS_UI_MAX_MESSAGES_PER_WINDOW {
|
||||
self.dropped_in_window = self.dropped_in_window.saturating_add(1);
|
||||
return false;
|
||||
}
|
||||
self.emitted_in_window = self.emitted_in_window.saturating_add(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn demo_ws_status_inner(
|
||||
state: &crate::AppState,
|
||||
) -> std::result::Result<crate::DemoWsStatusPayload, std::string::String> {
|
||||
let session = {
|
||||
let guard = state.demo_ws_session().lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
return match session {
|
||||
std::option::Option::Some(session) => {
|
||||
std::result::Result::Ok(status_from_snapshot(session.snapshot().await))
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(disconnected_status()),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn demo_ws_connect_inner(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: &crate::AppState,
|
||||
request: crate::DemoWsRequest,
|
||||
) -> std::result::Result<crate::DemoWsExecutionPayload, std::string::String> {
|
||||
let role = request.role.trim().to_string();
|
||||
if role.is_empty() {
|
||||
return std::result::Result::Err("demo WebSocket role must not be empty".to_string());
|
||||
}
|
||||
let method = request.method.trim().to_string();
|
||||
if method.is_empty() {
|
||||
return std::result::Result::Err("demo WebSocket method must not be empty".to_string());
|
||||
}
|
||||
let standard_request = match build_standard_ws_request(
|
||||
&method,
|
||||
request.target,
|
||||
request.filter_json,
|
||||
request.config_json,
|
||||
) {
|
||||
std::result::Result::Ok(request) => request,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let selected_client = match state.ws_pool().select_client_for_role_and_method(&role, &method) {
|
||||
std::result::Result::Ok(client) => client,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let session = match ensure_demo_ws_session(&app_handle, state, selected_client.clone()).await {
|
||||
std::result::Result::Ok(session) => session,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let acknowledgement = match session.subscribe(standard_request).await {
|
||||
std::result::Result::Ok(acknowledgement) => acknowledgement,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response_value = match acknowledgement.response.to_value() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
let response_json = match serde_json::to_string_pretty(&response_value) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
emit_demo_ws_status(&app_handle, session.snapshot().await);
|
||||
return std::result::Result::Ok(crate::DemoWsExecutionPayload {
|
||||
endpoint_name: selected_client.endpoint_name().to_string(),
|
||||
provider: selected_client.provider().to_string(),
|
||||
endpoint_url: selected_client.endpoint_url().to_string(),
|
||||
role,
|
||||
method: method.clone(),
|
||||
request_kind: kb_rpc::request_kind_from_method(&method),
|
||||
response_kind: acknowledgement.response.kind_name().to_string(),
|
||||
subscription_id: acknowledgement.subscription.remote_subscription_id,
|
||||
response_json,
|
||||
});
|
||||
}
|
||||
|
||||
async fn ensure_demo_ws_session(
|
||||
app_handle: &tauri::AppHandle,
|
||||
state: &crate::AppState,
|
||||
selected_client: kb_rpc::WsClient,
|
||||
) -> std::result::Result<std::sync::Arc<kb_rpc::WsSession>, std::string::String> {
|
||||
let existing = {
|
||||
let guard = state.demo_ws_session().lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
if let std::option::Option::Some(session) = existing {
|
||||
let snapshot = session.snapshot().await;
|
||||
if snapshot.state != kb_rpc::WsSessionState::Disconnected {
|
||||
if snapshot.endpoint_url != selected_client.endpoint_url() {
|
||||
return std::result::Result::Err(format!(
|
||||
"demo WebSocket session already uses endpoint '{}'; disconnect before selecting '{}'",
|
||||
snapshot.endpoint_name,
|
||||
selected_client.endpoint_name()
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(session);
|
||||
}
|
||||
}
|
||||
let reconnect_policy = if selected_client.endpoint_config().auto_reconnect {
|
||||
match kb_rpc::WsReconnectPolicy::bounded(
|
||||
DEMO_WS_RECONNECT_ATTEMPTS,
|
||||
DEMO_WS_RECONNECT_INITIAL_DELAY_MS,
|
||||
DEMO_WS_RECONNECT_MAX_DELAY_MS,
|
||||
) {
|
||||
std::result::Result::Ok(policy) => policy,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
}
|
||||
} else {
|
||||
kb_rpc::WsReconnectPolicy::disabled()
|
||||
};
|
||||
let capabilities =
|
||||
kb_rpc::StandardWsCapabilities::from_endpoint(selected_client.endpoint_config());
|
||||
let session =
|
||||
match kb_rpc::WsSession::connect(selected_client, capabilities, reconnect_policy).await {
|
||||
std::result::Result::Ok(session) => std::sync::Arc::new(session),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||||
};
|
||||
spawn_demo_ws_event_bridge(app_handle.clone(), session.clone());
|
||||
{
|
||||
let mut guard = state.demo_ws_session().lock().await;
|
||||
*guard = std::option::Option::Some(session.clone());
|
||||
}
|
||||
return std::result::Result::Ok(session);
|
||||
}
|
||||
|
||||
pub(crate) async fn demo_ws_unsubscribe_inner(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: &crate::AppState,
|
||||
remote_subscription_id: u64,
|
||||
) -> std::result::Result<crate::DemoWsStatusPayload, std::string::String> {
|
||||
let session = {
|
||||
let guard = state.demo_ws_session().lock().await;
|
||||
guard.clone()
|
||||
};
|
||||
let session = match session {
|
||||
std::option::Option::Some(session) => session,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err("demo WebSocket session is not connected".to_string());
|
||||
},
|
||||
};
|
||||
let unsubscribe_result = session.unsubscribe(remote_subscription_id).await;
|
||||
if let std::result::Result::Err(error) = unsubscribe_result {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
let snapshot = session.snapshot().await;
|
||||
emit_demo_ws_status(&app_handle, snapshot.clone());
|
||||
return std::result::Result::Ok(status_from_snapshot(snapshot));
|
||||
}
|
||||
|
||||
pub(crate) async fn demo_ws_disconnect_inner(
|
||||
state: &crate::AppState,
|
||||
) -> std::result::Result<crate::DemoWsStatusPayload, std::string::String> {
|
||||
let session = {
|
||||
let mut guard = state.demo_ws_session().lock().await;
|
||||
guard.take()
|
||||
};
|
||||
if let std::option::Option::Some(session) = session {
|
||||
if let std::result::Result::Err(error) = session.disconnect().await {
|
||||
return std::result::Result::Err(error.to_string());
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(disconnected_status());
|
||||
}
|
||||
|
||||
/// Disconnects the demo WebSocket session from application lifecycle hooks.
|
||||
pub(crate) async fn disconnect_demo_ws_app_state(state: &crate::AppState, _wait_for_relay: bool) {
|
||||
let disconnect_result = demo_ws_disconnect_inner(state).await;
|
||||
if let std::result::Result::Err(error) = disconnect_result {
|
||||
tracing::debug!(target: crate::TRACING_TARGET, "demo ws lifecycle disconnect failed: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_demo_ws_event_bridge(
|
||||
app_handle: tauri::AppHandle,
|
||||
session: std::sync::Arc<kb_rpc::WsSession>,
|
||||
) {
|
||||
let mut receiver = session.subscribe_events();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut rate_limiter = DemoWsUiRateLimiter::new();
|
||||
loop {
|
||||
let event = receiver.recv().await;
|
||||
let event = match event {
|
||||
std::result::Result::Ok(event) => event,
|
||||
std::result::Result::Err(tokio::sync::broadcast::error::RecvError::Lagged(
|
||||
count,
|
||||
)) => {
|
||||
emit_demo_ws_message(
|
||||
&app_handle,
|
||||
"lagged",
|
||||
format!("{count} événement(s) WebSocket interne(s) perdu(s)"),
|
||||
);
|
||||
continue;
|
||||
},
|
||||
std::result::Result::Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
};
|
||||
match event {
|
||||
kb_rpc::WsSessionEvent::Notification { notification, .. } => {
|
||||
if rate_limiter.should_emit(&app_handle) {
|
||||
let payload = match serde_json::to_string_pretty(¬ification) {
|
||||
std::result::Result::Ok(payload) => payload,
|
||||
std::result::Result::Err(error) => {
|
||||
format!("cannot serialize typed WebSocket notification: {error}")
|
||||
},
|
||||
};
|
||||
emit_demo_ws_message(
|
||||
&app_handle,
|
||||
"notification",
|
||||
truncate_payload(payload),
|
||||
);
|
||||
}
|
||||
},
|
||||
kb_rpc::WsSessionEvent::Diagnostic { code, message } => {
|
||||
emit_demo_ws_message(&app_handle, &code, truncate_payload(message));
|
||||
},
|
||||
kb_rpc::WsSessionEvent::Reconnecting { attempt, maximum_attempts } => {
|
||||
emit_demo_ws_message(
|
||||
&app_handle,
|
||||
"reconnecting",
|
||||
format!("tentative {attempt}/{maximum_attempts}"),
|
||||
);
|
||||
emit_demo_ws_status(&app_handle, session.snapshot().await);
|
||||
},
|
||||
kb_rpc::WsSessionEvent::Reconnected { reconnect_count } => {
|
||||
emit_demo_ws_message(
|
||||
&app_handle,
|
||||
"reconnected",
|
||||
format!("reconnexion numéro {reconnect_count}"),
|
||||
);
|
||||
emit_demo_ws_status(&app_handle, session.snapshot().await);
|
||||
},
|
||||
kb_rpc::WsSessionEvent::Connected
|
||||
| kb_rpc::WsSessionEvent::SubscriptionAdded(_)
|
||||
| kb_rpc::WsSessionEvent::SubscriptionRemapped { .. }
|
||||
| kb_rpc::WsSessionEvent::SubscriptionRemoved(_) => {
|
||||
emit_demo_ws_status(&app_handle, session.snapshot().await);
|
||||
},
|
||||
kb_rpc::WsSessionEvent::Disconnected => {
|
||||
emit_demo_ws_status(&app_handle, session.snapshot().await);
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn emit_demo_ws_status(app_handle: &tauri::AppHandle, snapshot: kb_rpc::WsSessionSnapshot) {
|
||||
let window = match app_handle.get_webview_window("demo_ws") {
|
||||
std::option::Option::Some(window) => window,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let status = status_from_snapshot(snapshot);
|
||||
if let std::result::Result::Err(error) = window.emit("demo-ws-status", status) {
|
||||
tracing::debug!(target: crate::TRACING_TARGET, "cannot emit demo ws status: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_demo_ws_message(
|
||||
app_handle: &tauri::AppHandle,
|
||||
kind: &str,
|
||||
payload_json: std::string::String,
|
||||
) {
|
||||
let window = match app_handle.get_webview_window("demo_ws") {
|
||||
std::option::Option::Some(window) => window,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
if let std::result::Result::Err(error) = window.emit(
|
||||
"demo-ws-message",
|
||||
crate::DemoWsMessagePayload { kind: kind.to_string(), payload_json },
|
||||
) {
|
||||
tracing::debug!(target: crate::TRACING_TARGET, "cannot emit demo ws message: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn status_from_snapshot(snapshot: kb_rpc::WsSessionSnapshot) -> crate::DemoWsStatusPayload {
|
||||
let mut subscriptions = std::vec::Vec::new();
|
||||
for subscription in &snapshot.subscriptions {
|
||||
if let std::option::Option::Some(remote_subscription_id) =
|
||||
subscription.remote_subscription_id
|
||||
{
|
||||
subscriptions.push(crate::DemoWsSubscriptionStatusPayload {
|
||||
subscription_id: remote_subscription_id,
|
||||
method: subscription.subscribe_method.clone(),
|
||||
unsubscribe_method: subscription.unsubscribe_method.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let (method, unsubscribe_method, subscription_id) = match subscriptions.last() {
|
||||
std::option::Option::Some(subscription) => (
|
||||
std::option::Option::Some(subscription.method.clone()),
|
||||
std::option::Option::Some(subscription.unsubscribe_method.clone()),
|
||||
std::option::Option::Some(subscription.subscription_id),
|
||||
),
|
||||
std::option::Option::None => {
|
||||
(std::option::Option::None, std::option::Option::None, std::option::Option::None)
|
||||
},
|
||||
};
|
||||
return crate::DemoWsStatusPayload {
|
||||
connected: snapshot.state != kb_rpc::WsSessionState::Disconnected,
|
||||
endpoint_name: std::option::Option::Some(snapshot.endpoint_name),
|
||||
endpoint_url: std::option::Option::Some(snapshot.endpoint_url),
|
||||
method,
|
||||
unsubscribe_method,
|
||||
subscription_id,
|
||||
subscription_count: match u32::try_from(subscriptions.len()) {
|
||||
std::result::Result::Ok(count) => count,
|
||||
std::result::Result::Err(_) => u32::MAX,
|
||||
},
|
||||
subscriptions,
|
||||
};
|
||||
}
|
||||
|
||||
fn disconnected_status() -> crate::DemoWsStatusPayload {
|
||||
return crate::DemoWsStatusPayload {
|
||||
connected: false,
|
||||
endpoint_name: std::option::Option::None,
|
||||
endpoint_url: std::option::Option::None,
|
||||
method: std::option::Option::None,
|
||||
unsubscribe_method: std::option::Option::None,
|
||||
subscription_id: std::option::Option::None,
|
||||
subscription_count: 0,
|
||||
subscriptions: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn build_ws_role_options(
|
||||
snapshots: std::vec::Vec<kb_rpc::WsPoolClientSnapshot>,
|
||||
) -> std::vec::Vec<crate::DemoWsRoleOption> {
|
||||
let mut roles = std::collections::BTreeMap::<
|
||||
std::string::String,
|
||||
std::collections::BTreeSet<std::string::String>,
|
||||
>::new();
|
||||
for snapshot in snapshots {
|
||||
for role in snapshot.roles {
|
||||
if !role.enabled {
|
||||
continue;
|
||||
}
|
||||
let role_entry = roles.entry(role.role).or_default();
|
||||
for request_kind in role.request_kinds {
|
||||
role_entry.insert(request_kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut options = std::vec::Vec::new();
|
||||
for (role, request_kinds) in roles {
|
||||
options.push(crate::DemoWsRoleOption {
|
||||
role,
|
||||
request_kinds: request_kinds.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
pub(crate) fn build_ws_method_options() -> std::vec::Vec<crate::DemoWsMethodOption> {
|
||||
let labels = std::collections::BTreeMap::from([
|
||||
("accountSubscribe", ("Compte", true, false, true)),
|
||||
("blockSubscribe", ("Bloc", false, true, true)),
|
||||
("logsSubscribe", ("Logs", false, true, true)),
|
||||
("programSubscribe", ("Programme", true, false, true)),
|
||||
("rootSubscribe", ("Roots", false, false, false)),
|
||||
("signatureSubscribe", ("Signature", true, false, true)),
|
||||
("slotSubscribe", ("Slots", false, false, false)),
|
||||
("slotsUpdatesSubscribe", ("Mises à jour slots", false, false, false)),
|
||||
("voteSubscribe", ("Votes", false, false, false)),
|
||||
]);
|
||||
let mut options = std::vec::Vec::new();
|
||||
for specification in &kb_rpc::STANDARD_WS_SUBSCRIPTIONS {
|
||||
let metadata = labels.get(specification.subscribe_method);
|
||||
let (label, requires_target, requires_filter_json, supports_config_json) = match metadata {
|
||||
std::option::Option::Some(metadata) => *metadata,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
options.push(crate::DemoWsMethodOption {
|
||||
method: specification.subscribe_method.to_string(),
|
||||
unsubscribe_method: specification.unsubscribe_method.to_string(),
|
||||
request_kind: kb_rpc::request_kind_from_method(specification.subscribe_method),
|
||||
label: label.to_string(),
|
||||
requires_target,
|
||||
requires_filter_json,
|
||||
supports_config_json,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
fn build_standard_ws_request(
|
||||
method: &str,
|
||||
target: std::option::Option<std::string::String>,
|
||||
filter_json: std::option::Option<std::string::String>,
|
||||
config_json: std::option::Option<std::string::String>,
|
||||
) -> std::result::Result<kb_rpc::StandardWsRequest, std::string::String> {
|
||||
return match method {
|
||||
"accountSubscribe" => {
|
||||
let target = match required_target(target, method) {
|
||||
std::result::Result::Ok(target) => target,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let config = match parse_optional_json_as::<kb_rpc::WsAccountSubscribeConfig>(
|
||||
config_json,
|
||||
"configJson",
|
||||
) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_present(filter_json, "filterJson", method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Account(
|
||||
kb_rpc::AccountSubscribeRequest { pubkey: target, config },
|
||||
))
|
||||
},
|
||||
"blockSubscribe" => {
|
||||
if let std::result::Result::Err(error) = reject_present(target, "target", method) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let filter =
|
||||
match parse_required_json_as::<kb_rpc::WsBlockFilter>(filter_json, "filterJson") {
|
||||
std::result::Result::Ok(filter) => filter,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let config = match parse_optional_json_as::<kb_rpc::WsBlockSubscribeConfig>(
|
||||
config_json,
|
||||
"configJson",
|
||||
) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Block(
|
||||
kb_rpc::BlockSubscribeRequest { filter, config },
|
||||
))
|
||||
},
|
||||
"logsSubscribe" => {
|
||||
if let std::result::Result::Err(error) = reject_present(target, "target", method) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let filter =
|
||||
match parse_required_json_as::<kb_rpc::WsLogsFilter>(filter_json, "filterJson") {
|
||||
std::result::Result::Ok(filter) => filter,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let config = match parse_optional_json_as::<kb_rpc::WsLogsSubscribeConfig>(
|
||||
config_json,
|
||||
"configJson",
|
||||
) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Logs(kb_rpc::LogsSubscribeRequest {
|
||||
filter,
|
||||
config,
|
||||
}))
|
||||
},
|
||||
"programSubscribe" => {
|
||||
let target = match required_target(target, method) {
|
||||
std::result::Result::Ok(target) => target,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_present(filter_json, "filterJson", method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let config = match parse_optional_json_as::<kb_rpc::WsProgramSubscribeConfig>(
|
||||
config_json,
|
||||
"configJson",
|
||||
) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Program(
|
||||
kb_rpc::ProgramSubscribeRequest { program_id: target, config },
|
||||
))
|
||||
},
|
||||
"signatureSubscribe" => {
|
||||
let target = match required_target(target, method) {
|
||||
std::result::Result::Ok(target) => target,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_present(filter_json, "filterJson", method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let config = match parse_optional_json_as::<kb_rpc::WsSignatureSubscribeConfig>(
|
||||
config_json,
|
||||
"configJson",
|
||||
) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Signature(
|
||||
kb_rpc::SignatureSubscribeRequest { signature: target, config },
|
||||
))
|
||||
},
|
||||
"rootSubscribe" => {
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_parameterless_inputs(target, filter_json, config_json, method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Root(kb_rpc::RootSubscribeRequest))
|
||||
},
|
||||
"slotSubscribe" => {
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_parameterless_inputs(target, filter_json, config_json, method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Slot(kb_rpc::SlotSubscribeRequest))
|
||||
},
|
||||
"slotsUpdatesSubscribe" => {
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_parameterless_inputs(target, filter_json, config_json, method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::SlotsUpdates(
|
||||
kb_rpc::SlotsUpdatesSubscribeRequest,
|
||||
))
|
||||
},
|
||||
"voteSubscribe" => {
|
||||
if let std::result::Result::Err(error) =
|
||||
reject_parameterless_inputs(target, filter_json, config_json, method)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
std::result::Result::Ok(kb_rpc::StandardWsRequest::Vote(kb_rpc::VoteSubscribeRequest))
|
||||
},
|
||||
_ => std::result::Result::Err(format!("unsupported standard WebSocket method '{method}'")),
|
||||
};
|
||||
}
|
||||
|
||||
fn required_target(
|
||||
target: std::option::Option<std::string::String>,
|
||||
method: &str,
|
||||
) -> std::result::Result<std::string::String, std::string::String> {
|
||||
let target = match target {
|
||||
std::option::Option::Some(target) => target.trim().to_string(),
|
||||
std::option::Option::None => std::string::String::new(),
|
||||
};
|
||||
if target.is_empty() {
|
||||
return std::result::Result::Err(format!("method '{method}' requires target"));
|
||||
}
|
||||
return std::result::Result::Ok(target);
|
||||
}
|
||||
|
||||
fn reject_parameterless_inputs(
|
||||
target: std::option::Option<std::string::String>,
|
||||
filter_json: std::option::Option<std::string::String>,
|
||||
config_json: std::option::Option<std::string::String>,
|
||||
method: &str,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
if let std::result::Result::Err(error) = reject_present(target, "target", method) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = reject_present(filter_json, "filterJson", method) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = reject_present(config_json, "configJson", method) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn reject_present(
|
||||
value: std::option::Option<std::string::String>,
|
||||
field: &str,
|
||||
method: &str,
|
||||
) -> std::result::Result<(), std::string::String> {
|
||||
if let std::option::Option::Some(value) = value {
|
||||
if !value.trim().is_empty() {
|
||||
return std::result::Result::Err(format!("method '{method}' does not accept {field}"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_required_json_as<ValueType: serde::de::DeserializeOwned>(
|
||||
text: std::option::Option<std::string::String>,
|
||||
field: &str,
|
||||
) -> std::result::Result<ValueType, std::string::String> {
|
||||
let text = match text {
|
||||
std::option::Option::Some(text) if !text.trim().is_empty() => text,
|
||||
_ => return std::result::Result::Err(format!("{field} is required")),
|
||||
};
|
||||
return match serde_json::from_str::<ValueType>(&text) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(format!("invalid {field}: {error}"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_json_as<ValueType: serde::de::DeserializeOwned>(
|
||||
text: std::option::Option<std::string::String>,
|
||||
field: &str,
|
||||
) -> std::result::Result<std::option::Option<ValueType>, std::string::String> {
|
||||
let text = match text {
|
||||
std::option::Option::Some(text) if !text.trim().is_empty() => text,
|
||||
_ => return std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
return match serde_json::from_str::<ValueType>(&text) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(std::option::Option::Some(value)),
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(format!("invalid {field}: {error}"))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn truncate_payload(payload: std::string::String) -> std::string::String {
|
||||
if payload.chars().count() <= DEMO_WS_UI_MAX_PAYLOAD_CHARS {
|
||||
return payload;
|
||||
}
|
||||
let mut truncated = payload
|
||||
.chars()
|
||||
.take(DEMO_WS_UI_MAX_PAYLOAD_CHARS)
|
||||
.collect::<std::string::String>();
|
||||
truncated.push_str("\n… payload tronqué côté Rust …");
|
||||
return truncated;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
#[test]
|
||||
fn tauri_numeric_bindings_use_json_compatible_numbers() {
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
<crate::DemoWsExecutionPayload as TS>::decl(&config),
|
||||
<crate::DemoWsStatusPayload as TS>::decl(&config),
|
||||
<crate::DemoWsSubscriptionStatusPayload as TS>::decl(&config),
|
||||
<crate::DemoWsUnsubscribeRequest as TS>::decl(&config),
|
||||
<crate::DemoBackfillProgressPayload as TS>::decl(&config),
|
||||
<crate::DemoBackfillSummaryPayload as TS>::decl(&config),
|
||||
<crate::DemoSqlTableSnapshot as TS>::decl(&config),
|
||||
];
|
||||
for declaration in declarations {
|
||||
assert!(!declaration.contains("bigint"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// file: kb_app_demo/src/frontend_log.rs
|
||||
// version: 10
|
||||
|
||||
//! Frontend logging bridge commands for Tauri WebView scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Log payload sent by frontend scripts when a typed tracing target is required.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_app_demo/frontend_log/FrontendLogPayload.ts"
|
||||
)]
|
||||
pub(crate) struct FrontendLogPayload {
|
||||
/// Lowercase tracing level: `trace`, `debug`, `info`, `warn`, or `error`.
|
||||
pub(crate) level: std::string::String,
|
||||
/// Explicit tracing target used for route-based filtering.
|
||||
pub(crate) target: std::string::String,
|
||||
/// Message rendered by the frontend before forwarding it to Rust.
|
||||
pub(crate) message: std::string::String,
|
||||
}
|
||||
|
||||
pub(crate) fn emit_trace(target: &str, message: &str) {
|
||||
tracing::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "frontend_log",
|
||||
frontend_target = target,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn emit_debug(target: &str, message: &str) {
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "frontend_log",
|
||||
frontend_target = target,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn emit_info(target: &str, message: &str) {
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "frontend_log",
|
||||
frontend_target = target,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn emit_warn(target: &str, message: &str) {
|
||||
tracing::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "frontend_log",
|
||||
frontend_target = target,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn emit_error(target: &str, message: &str) {
|
||||
tracing::error!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "frontend_log",
|
||||
frontend_target = target,
|
||||
"{}",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_frontend_target(target: &str) -> std::string::String {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.is_empty() {
|
||||
return "kb_app_demo.frontend".to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.main" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_config" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_http" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_ws" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_backfill" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_core_extraction" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_decode_replay" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_execution_solana_core" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_execution_spl" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.demo_sql_replay_candidates" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
if trimmed == "kb_app_demo.frontend.splash" {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
return "kb_app_demo.frontend".to_string();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn normalize_frontend_target_uses_default_for_empty_target() {
|
||||
let normalized = crate::normalize_frontend_target(" ");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_main_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.main");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.main");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_config_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.demo_config");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_http_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.demo_http");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_http");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_ws_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.demo_ws");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_ws");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_backfill_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.demo_backfill");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_backfill");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_core_extraction_target() {
|
||||
let normalized =
|
||||
crate::normalize_frontend_target("kb_app_demo.frontend.demo_core_extraction");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_core_extraction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_decode_replay_target() {
|
||||
let normalized =
|
||||
crate::normalize_frontend_target("kb_app_demo.frontend.demo_decode_replay");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_decode_replay");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_execution_solana_core_target() {
|
||||
let normalized =
|
||||
crate::normalize_frontend_target("kb_app_demo.frontend.demo_execution_solana_core");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_execution_solana_core");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_demo_sql_replay_candidates_target() {
|
||||
let normalized =
|
||||
crate::normalize_frontend_target("kb_app_demo.frontend.demo_sql_replay_candidates");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.demo_sql_replay_candidates");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_keeps_splash_target() {
|
||||
let normalized = crate::normalize_frontend_target("kb_app_demo.frontend.splash");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend.splash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_frontend_target_rejects_unknown_dynamic_targets() {
|
||||
let normalized = crate::normalize_frontend_target("other.frontend.target");
|
||||
assert_eq!(normalized, "kb_app_demo.frontend");
|
||||
}
|
||||
}
|
||||
338
migration/khadhroony-bot2-reference/kb_app_demo/src/lib.rs
Normal file
338
migration/khadhroony-bot2-reference/kb_app_demo/src/lib.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
// file: kb_app_demo/src/lib.rs
|
||||
// version: 30
|
||||
|
||||
//! Tauri application demo library for `khadhroony-bot2`.
|
||||
//!
|
||||
//! This crate loads the shared configuration, initializes workspace logging and
|
||||
//! exposes focused demo windows for early runtime validation.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod app_state;
|
||||
mod constants;
|
||||
mod demo_backfill;
|
||||
mod demo_config;
|
||||
mod demo_core_extraction;
|
||||
mod demo_decode_replay;
|
||||
mod demo_execution_solana_core;
|
||||
mod demo_execution_spl;
|
||||
mod demo_http;
|
||||
mod demo_spl_ata;
|
||||
mod demo_spl_token;
|
||||
mod demo_spl_token_2022;
|
||||
mod demo_sql_common;
|
||||
mod demo_sql_diag;
|
||||
mod demo_sql_pg_core;
|
||||
mod demo_sql_pg_raw;
|
||||
mod demo_sql_replay_candidates;
|
||||
mod demo_ws;
|
||||
mod frontend_log;
|
||||
mod main_window;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
|
||||
/// Shared state managed by Tauri for the demo application.
|
||||
pub(crate) use crate::app_state::AppState;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillObserver;
|
||||
/// Initial options shown by the backfill demo.
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillOptionsPayload;
|
||||
/// One progress event emitted to the backfill window.
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillProgressPayload;
|
||||
/// UI request for one bounded backfill campaign.
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillRequest;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillRunGuard;
|
||||
/// Final UI-safe summary for one backfill campaign.
|
||||
pub(crate) use crate::demo_backfill::DemoBackfillSummaryPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_backfill::build_demo_backfill_pipeline_request;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_backfill::build_role_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_backfill::demo_backfill_summary_payload;
|
||||
/// Serializable payload shown by the configuration demo page.
|
||||
pub(crate) use crate::demo_config::DemoConfigPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionObserver;
|
||||
/// Initial options shown by the core extraction demo.
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionOptionsPayload;
|
||||
/// One progress event emitted to the core extraction window.
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionProgressPayload;
|
||||
/// UI request for one bounded core extraction campaign.
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionRequest;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionRunGuard;
|
||||
/// Final UI-safe summary for one extraction campaign.
|
||||
pub(crate) use crate::demo_core_extraction::DemoCoreExtractionSummaryPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_core_extraction::build_demo_core_extraction_pipeline_request;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_core_extraction::demo_core_extraction_summary_payload;
|
||||
/// One aggregated coverage row returned to the UI.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeCoverageSummaryPayload;
|
||||
/// Read-only decode, ledger and coverage diagnostics.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeDiagnosticsPayload;
|
||||
/// One processor counter row returned to the UI.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeProcessorSummaryPayload;
|
||||
/// One decoder selectable by the decode replay demo.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplayDecoderOption;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplayObserver;
|
||||
/// Initial options shown by the contextual decode replay demo.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplayOptionsPayload;
|
||||
/// UI request for one bounded contextual decode replay campaign.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplayRequest;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplayRunGuard;
|
||||
/// Final UI-safe summary for one contextual decode replay campaign.
|
||||
pub(crate) use crate::demo_decode_replay::DemoDecodeReplaySummaryPayload;
|
||||
/// Bounded read request for committed transaction annotations.
|
||||
pub(crate) use crate::demo_decode_replay::DemoTransactionAnnotationRequest;
|
||||
/// UI-safe committed transaction annotation row.
|
||||
pub(crate) use crate::demo_decode_replay::DemoTransactionAnnotationRow;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::annotation_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::available_decoders;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::available_materializers;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::build_demo_decode_replay_pipeline_request;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::coverage_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::demo_decode_replay_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::optional_line_count;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::register_active_campaign;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_decode_replay::text_sample;
|
||||
/// Request sent by the Memo v4 Devnet execution panel.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionMemoRequest;
|
||||
/// UI-safe result of one Memo v4 Devnet execution orchestration.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionMemoSummaryPayload;
|
||||
/// Public key generated for a disposable Devnet recipient.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreGeneratedRecipientPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreObserver;
|
||||
/// Initial options shown by the Solana Core execution demo.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreOptionsPayload;
|
||||
/// One Devnet execution profile exposed to the demo window.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreProfileOption;
|
||||
/// Progress event emitted during execution and post-validation.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreProgressPayload;
|
||||
/// Request sent by the execution demo.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreRequest;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreRunGuard;
|
||||
/// UI-safe result of one execution orchestration.
|
||||
pub(crate) use crate::demo_execution_solana_core::DemoExecutionSolanaCoreSummaryPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::confirmation_status_code;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::demo_execution_solana_core_memo_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::demo_execution_solana_core_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::devnet_profile_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::execution_cluster_code;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::pretty_json;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_execution_solana_core::select_devnet_profile;
|
||||
/// Frontend payload for one independent Devnet SPL validation scenario.
|
||||
pub(crate) use crate::demo_execution_spl::DevnetSplValidationScenarioPayload;
|
||||
/// Response payload for one HTTP JSON-RPC demo execution.
|
||||
pub(crate) use crate::demo_http::DemoHttpExecutionPayload;
|
||||
/// One selectable HTTP JSON-RPC method shown by the HTTP demo.
|
||||
pub(crate) use crate::demo_http::DemoHttpMethodOption;
|
||||
/// HTTP demo options derived from configuration and local method presets.
|
||||
pub(crate) use crate::demo_http::DemoHttpOptionsPayload;
|
||||
/// Request payload for one HTTP JSON-RPC demo execution.
|
||||
pub(crate) use crate::demo_http::DemoHttpRequest;
|
||||
/// One selectable role shown by the HTTP demo.
|
||||
pub(crate) use crate::demo_http::DemoHttpRoleOption;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_http::build_http_method_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_http::build_http_role_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_http::demo_http_execute_request_inner;
|
||||
/// Request for one representative ATA creation on Devnet.
|
||||
pub(crate) use crate::demo_spl_ata::DemoExecutionSplAtaRequest;
|
||||
/// UI-safe ATA execution result.
|
||||
pub(crate) use crate::demo_spl_ata::DemoExecutionSplAtaSummaryPayload;
|
||||
/// Readonly payer, wallet, Token Program and derived ATA values.
|
||||
pub(crate) use crate::demo_spl_ata::DemoSplAtaDerivationPayload;
|
||||
/// Request used to derive the representative wallet ATA before execution.
|
||||
pub(crate) use crate::demo_spl_ata::DemoSplAtaDerivationRequest;
|
||||
/// Bounded ATA lifecycle journal request.
|
||||
pub(crate) use crate::demo_spl_ata::DemoSplAtaJournalRequest;
|
||||
/// One UI-safe ATA lifecycle journal row.
|
||||
pub(crate) use crate::demo_spl_ata::DemoSplAtaJournalRow;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::demo_spl_ata_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::derive_ata;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::journal_matches;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::journal_row;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::load_profile_wallet;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_ata::parse_token_program;
|
||||
/// Request for one checked classic SPL Token transfer on Devnet.
|
||||
pub(crate) use crate::demo_spl_token::DemoExecutionSplTokenRequest;
|
||||
/// UI-safe result of one checked SPL Token transfer orchestration.
|
||||
pub(crate) use crate::demo_spl_token::DemoExecutionSplTokenSummaryPayload;
|
||||
/// Bounded exact filters for the classic SPL Token materialized journal.
|
||||
pub(crate) use crate::demo_spl_token::DemoSplTokenJournalRequest;
|
||||
/// UI-safe materialized classic SPL Token journal row.
|
||||
pub(crate) use crate::demo_spl_token::DemoSplTokenJournalRow;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_token::demo_spl_token_journal_row;
|
||||
/// Executes one checked SPL Token simulation or explicitly authorized Devnet submission.
|
||||
pub(crate) use crate::demo_spl_token::demo_spl_token_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_token::payload_matches;
|
||||
/// Loads a bounded, typed journal of committed classic SPL Token projections.
|
||||
pub(crate) use crate::demo_spl_token::validate_journal_request;
|
||||
/// Request for one independent public Token-2022 Devnet scenario.
|
||||
pub(crate) use crate::demo_spl_token_2022::DemoExecutionSplToken2022Request;
|
||||
/// UI-safe result of one public Token-2022 Devnet orchestration.
|
||||
pub(crate) use crate::demo_spl_token_2022::DemoExecutionSplToken2022SummaryPayload;
|
||||
/// Public values loaded from one persisted Token-2022 validation fixture.
|
||||
pub(crate) use crate::demo_spl_token_2022::DemoSplToken2022FixturePayload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_token_2022::demo_spl_token_2022_summary_payload;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_token_2022::operation_from_request;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_spl_token_2022::parse_fixture;
|
||||
/// UI-safe table diagnostics shown by SQL demo windows.
|
||||
pub(crate) use crate::demo_sql_common::DemoSqlTableSnapshot;
|
||||
/// Connects to PostgreSQL without reapplying schemas initialized at application startup.
|
||||
pub(crate) use crate::demo_sql_common::connect_postgres_store;
|
||||
/// Formats debug enums for UI display.
|
||||
pub(crate) use crate::demo_sql_common::debug_status;
|
||||
/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup.
|
||||
pub(crate) use crate::demo_sql_common::initialize_postgres_schema_for_startup;
|
||||
/// Opens or focuses a SQL demo window.
|
||||
pub(crate) use crate::demo_sql_common::open_sql_demo_window;
|
||||
/// Converts store table diagnostics to the UI payload shape.
|
||||
pub(crate) use crate::demo_sql_common::table_snapshot_from_pg;
|
||||
/// Converts many table diagnostics to UI payloads.
|
||||
pub(crate) use crate::demo_sql_common::table_snapshots_from_pg;
|
||||
/// Complete SQL diagnostic payload shown by `demo_sql_diag`.
|
||||
pub(crate) use crate::demo_sql_diag::DemoSqlDiagPayload;
|
||||
/// PostgreSQL core store payload shown by `demo_sql_pg_core`.
|
||||
pub(crate) use crate::demo_sql_pg_core::DemoSqlPgCorePayload;
|
||||
/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`.
|
||||
pub(crate) use crate::demo_sql_pg_raw::DemoSqlPgRawPayload;
|
||||
/// UI request for bounded core entity summaries.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayEntityRequest;
|
||||
/// Aggregated core entity row shown by the replay candidate browser.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayEntityRow;
|
||||
/// One program identifier exposed by the runtime `kb_program_ids` registry.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption;
|
||||
/// Static options for the replay candidate browser.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload;
|
||||
/// UI request for bounded program summaries.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayProgramRequest;
|
||||
/// Aggregated program row shown by the replay candidate browser.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayProgramRow;
|
||||
/// UI request for bounded transaction replay candidates.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest;
|
||||
/// One transaction row shown by the replay candidate browser.
|
||||
pub(crate) use crate::demo_sql_replay_candidates::DemoSqlReplayTransactionRow;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::available_csv_export_path;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::csv_export_directory_from_current_dir;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::entity_kind_from_code;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::entity_row_from_pg;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::optional_entity_kind_from_code;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::program_row_from_pg;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::program_scope_from_code;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::transaction_row_from_pg;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_sql_replay_candidates::validated_csv_file_name;
|
||||
/// Response payload for one WebSocket subscription response.
|
||||
pub(crate) use crate::demo_ws::DemoWsExecutionPayload;
|
||||
/// WebSocket event payload emitted to the demo window.
|
||||
pub(crate) use crate::demo_ws::DemoWsMessagePayload;
|
||||
/// One selectable WebSocket JSON-RPC method shown by the WebSocket demo.
|
||||
pub(crate) use crate::demo_ws::DemoWsMethodOption;
|
||||
/// WebSocket demo options derived from configuration and the standard registry.
|
||||
pub(crate) use crate::demo_ws::DemoWsOptionsPayload;
|
||||
/// Request payload for one WebSocket subscription demo command.
|
||||
pub(crate) use crate::demo_ws::DemoWsRequest;
|
||||
/// One selectable role shown by the WebSocket demo.
|
||||
pub(crate) use crate::demo_ws::DemoWsRoleOption;
|
||||
/// Current WebSocket demo session status.
|
||||
pub(crate) use crate::demo_ws::DemoWsStatusPayload;
|
||||
/// One active WebSocket subscription shown by the demo UI.
|
||||
pub(crate) use crate::demo_ws::DemoWsSubscriptionStatusPayload;
|
||||
/// Request payload for one explicit WebSocket unsubscribe command.
|
||||
pub(crate) use crate::demo_ws::DemoWsUnsubscribeRequest;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::build_ws_method_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::build_ws_role_options;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::demo_ws_connect_inner;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::demo_ws_disconnect_inner;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::demo_ws_status_inner;
|
||||
/// ????
|
||||
pub(crate) use crate::demo_ws::demo_ws_unsubscribe_inner;
|
||||
/// Disconnects the demo WebSocket session from application lifecycle hooks.
|
||||
pub(crate) use crate::demo_ws::disconnect_demo_ws_app_state;
|
||||
/// Log payload sent by frontend scripts when a typed tracing target is required.
|
||||
pub(crate) use crate::frontend_log::FrontendLogPayload;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::emit_debug;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::emit_error;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::emit_info;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::emit_trace;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::emit_warn;
|
||||
/// ????
|
||||
pub(crate) use crate::frontend_log::normalize_frontend_target;
|
||||
/// ????
|
||||
pub(crate) use crate::main_window::strip_readme_metadata_comments;
|
||||
/// Resolves the workspace root directory used by the demo application.
|
||||
pub(crate) use crate::main_window::workspace_root_dir;
|
||||
/// Delay after the fade-out command before destroying the splash window.
|
||||
pub(crate) use crate::splash::SPLASH_CLOSE_WAIT_MS;
|
||||
/// Splash fade-in and fade-out animation duration.
|
||||
pub(crate) use crate::splash::SPLASH_FADE_MS;
|
||||
/// Minimum startup splash duration before fade-out starts.
|
||||
pub(crate) use crate::splash::SPLASH_MINIMUM_MS;
|
||||
/// Command payload sent from Rust to the splash frontend.
|
||||
pub(crate) use crate::splash::SplashOrder;
|
||||
/// Emits a splash-screen order to the splash WebView.
|
||||
pub(crate) use crate::splash::emit_splash_order;
|
||||
/// Waits until the minimum splash duration has elapsed.
|
||||
pub(crate) use crate::splash::wait_until_minimum;
|
||||
|
||||
/// Runs the Tauri application.
|
||||
pub use crate::tauri::run;
|
||||
43
migration/khadhroony-bot2-reference/kb_app_demo/src/main.rs
Normal file
43
migration/khadhroony-bot2-reference/kb_app_demo/src/main.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
// file: kb_app_demo/src/main.rs
|
||||
// version: 6
|
||||
|
||||
//! Binary entry point for the `kb_app_demo` crate.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use fs2::FileExt; // rust-rules: trait-import
|
||||
|
||||
/// Entrypoint of the kb app binary.
|
||||
#[tokio::main]
|
||||
async fn main() -> std::process::ExitCode {
|
||||
let mut lock_path = std::env::temp_dir();
|
||||
lock_path.push("com_khadhroony_bot2_kb_app_demo.lock");
|
||||
let lock_file = match std::fs::File::create(&lock_path) {
|
||||
std::result::Result::Ok(lock) => lock,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock {
|
||||
eprintln!("another kb_app_demo instance is already running");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
let _lock_file = lock_file;
|
||||
let run_result = kb_app_demo_lib::run().await;
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("application error: {error}");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// file: kb_app_demo/src/main_window.rs
|
||||
// version: 3
|
||||
|
||||
//! Main window commands and workspace README loading helpers.
|
||||
|
||||
pub(crate) fn workspace_root_dir() -> std::path::PathBuf {
|
||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
return match manifest_dir.parent() {
|
||||
std::option::Option::Some(parent) => parent.to_path_buf(),
|
||||
std::option::Option::None => manifest_dir,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn strip_readme_metadata_comments(content: &str) -> std::string::String {
|
||||
let mut output = std::string::String::new();
|
||||
let mut leading_metadata = true;
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if leading_metadata && is_readme_metadata_comment(trimmed) {
|
||||
continue;
|
||||
}
|
||||
leading_metadata = false;
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn is_readme_metadata_comment(line: &str) -> bool {
|
||||
if line.starts_with("<!-- file:") || line.starts_with("<!--- file:") {
|
||||
return true;
|
||||
}
|
||||
if line.starts_with("<!-- version:") || line.starts_with("<!--- version:") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn strip_readme_metadata_comments_removes_workspace_header_lines() {
|
||||
let content = "<!-- file: README.md -->\n<!-- version: 7 -->\n# Title\nBody\n";
|
||||
let cleaned = crate::strip_readme_metadata_comments(content);
|
||||
assert_eq!(cleaned, "# Title\nBody\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_readme_metadata_comments_keeps_later_html_comments() {
|
||||
let content = "<!-- file: README.md -->\n# Title\n<!-- normal comment -->\nBody\n";
|
||||
let cleaned = crate::strip_readme_metadata_comments(content);
|
||||
assert_eq!(cleaned, "# Title\n<!-- normal comment -->\nBody\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// file: kb_app_demo/src/splash.rs
|
||||
// version: 5
|
||||
|
||||
//! Shared splash-screen payload types and helpers.
|
||||
//!
|
||||
//! The startup sequence is intentionally wired in `lib.rs` so it stays close to
|
||||
//! the Tauri builder setup pattern used by the previous demo application.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Minimum startup splash duration before fade-out starts.
|
||||
pub(crate) const SPLASH_MINIMUM_MS: u64 = 3100;
|
||||
/// Splash fade-in and fade-out animation duration.
|
||||
pub(crate) const SPLASH_FADE_MS: u32 = 3000;
|
||||
/// Delay after the fade-out command before destroying the splash window.
|
||||
pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 3100;
|
||||
|
||||
/// Command payload sent from Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/kb_app_demo/splash/SplashOrder.ts")]
|
||||
pub(crate) struct SplashOrder {
|
||||
/// Splash command name such as `fadein`, `fadeout`, `add_msg`, or `add_log`.
|
||||
pub(crate) order: std::string::String,
|
||||
/// Optional message payload attached to the command.
|
||||
pub(crate) msg: std::option::Option<std::string::String>,
|
||||
/// Optional status payload attached to the command.
|
||||
pub(crate) status: std::option::Option<std::string::String>,
|
||||
/// Optional animation duration in milliseconds.
|
||||
pub(crate) duration_ms: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
/// Emits a splash-screen order to the splash WebView.
|
||||
pub(crate) fn emit_splash_order(
|
||||
splash_window: &tauri::WebviewWindow,
|
||||
order: &str,
|
||||
msg: std::option::Option<&str>,
|
||||
status: std::option::Option<&str>,
|
||||
duration_ms: std::option::Option<u32>,
|
||||
) {
|
||||
let payload = crate::SplashOrder {
|
||||
order: order.to_string(),
|
||||
msg: msg.map(std::string::ToString::to_string),
|
||||
status: status.map(std::string::ToString::to_string),
|
||||
duration_ms,
|
||||
};
|
||||
let emit_result = splash_window.emit("splash", payload);
|
||||
if let std::result::Result::Err(error) = emit_result {
|
||||
tracing::error!(target: crate::TRACING_TARGET, "error emitting splash event '{order}': {error:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until the minimum splash duration has elapsed.
|
||||
pub(crate) async fn wait_until_minimum(start: tokio::time::Instant, minimum_ms: u64) {
|
||||
let minimum = std::time::Duration::from_millis(minimum_ms);
|
||||
let elapsed = start.elapsed();
|
||||
if elapsed >= minimum {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(minimum - elapsed).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn splash_order_serializes_duration_ms() {
|
||||
let order = crate::SplashOrder {
|
||||
order: "fadein".to_string(),
|
||||
msg: std::option::Option::None,
|
||||
status: std::option::Option::None,
|
||||
duration_ms: std::option::Option::Some(3000),
|
||||
};
|
||||
let value_result = serde_json::to_value(order);
|
||||
let value = match value_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("cannot serialize splash order: {error}"),
|
||||
};
|
||||
assert_eq!(value["duration_ms"], serde_json::json!(3000));
|
||||
}
|
||||
}
|
||||
2258
migration/khadhroony-bot2-reference/kb_app_demo/src/tauri.rs
Normal file
2258
migration/khadhroony-bot2-reference/kb_app_demo/src/tauri.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user