v0.1.0-pre.058

This commit is contained in:
2026-07-27 02:02:11 +02:00
parent e56f2561c5
commit e828677201
22 changed files with 378 additions and 192 deletions

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 9
// version: 10
//! Shared Tauri application state and startup initialization.
@@ -23,6 +23,7 @@ pub(crate) struct AppState {
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,
startup_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
@@ -68,6 +69,7 @@ impl crate::AppState {
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),
startup_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
@@ -172,6 +174,19 @@ impl crate::AppState {
return &self.demo_execution_solana_core_cancel_requested;
}
/// Starts the splash startup sequence at most once.
pub(crate) fn begin_startup_sequence(&self) -> bool {
return self
.startup_sequence_started
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::AcqRel,
std::sync::atomic::Ordering::Acquire,
)
.is_ok();
}
/// 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();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_backfill.rs
// version: 11
// version: 12
//! Tauri commands and UI payloads for bounded HTTP transaction backfills.
@@ -20,6 +20,20 @@ pub(crate) struct DemoBackfillRoleOption {
pub(crate) providers: std::vec::Vec<std::string::String>,
}
/// One known program suggested by the free-form Program ID field.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillProgramOption.ts"
)]
pub(crate) struct DemoBackfillProgramOption {
/// Stable program code.
pub(crate) code: std::string::String,
/// Canonical program identifier.
pub(crate) program_id: std::string::String,
}
/// Initial options shown by the backfill demo.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -30,6 +44,8 @@ pub(crate) struct DemoBackfillRoleOption {
pub(crate) struct DemoBackfillOptionsPayload {
/// Selectable endpoint roles.
pub(crate) roles: std::vec::Vec<crate::DemoBackfillRoleOption>,
/// Known programs offered as non-blocking autocomplete suggestions.
pub(crate) programs: std::vec::Vec<crate::DemoBackfillProgramOption>,
/// Preferred role when configured.
pub(crate) default_role: std::option::Option<std::string::String>,
/// Default transaction commitment.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
// version: 28
// version: 29
//! Tauri commands and UI payloads for contextual instruction decode replay.
@@ -720,22 +720,22 @@ mod tests {
fn native_memo_token2022_elgamal_classic_token_and_ata_decoders_are_registered() {
let decoders = crate::available_decoders();
assert_eq!(decoders.len(), 7);
let names = decoders
let mut 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_token2022".to_string(),
"metadata_metaplex_token_metadata".to_string(),
]
);
names.sort();
let mut expected_names = std::vec![
"metadata_metaplex_token_metadata".to_string(),
"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_token2022".to_string(),
];
expected_names.sort();
assert_eq!(names, expected_names);
}
#[test]

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_sql_common.rs
// version: 8
// version: 9
//! Shared SQL demo helpers and serializable payloads.
@@ -201,25 +201,23 @@ pub(crate) async fn initialize_postgres_schema_for_startup(
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 initialize_schema = options.auto_initialize_schema;
let masked_dsn = options.masked_dsn();
options.auto_initialize_schema = false;
emit_sql_startup_splash(
splash_window,
"Checking PostgreSQL raw/core/decode tables...",
"Connexion PostgreSQL et vérification des tables raw/core/decode...",
"info",
true,
false,
);
if !initialize_schema {
emit_sql_startup_splash(
splash_window,
"Auto-initialisation désactivée : vérification sans création de table.",
"info",
true,
);
}
tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization");
let store_result = kb_store::PostgresStore::connect(options).await;
let store = match store_result {
@@ -241,12 +239,14 @@ pub(crate) async fn initialize_postgres_schema_for_startup(
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;
if initialize_schema {
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 {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_ws.rs
// version: 14
// version: 16
//! Standard Solana WebSocket demo commands backed by `kb_onchain_transport::WsSession`.
@@ -431,17 +431,26 @@ fn spawn_demo_ws_event_bridge(
};
match event {
kb_onchain_transport::WsSessionEvent::Notification { notification, .. } => {
let compact_payload = match serde_json::to_string(&notification) {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(error) => {
format!("cannot serialize typed WebSocket notification: {error}")
},
};
tracing::debug!(
target: crate::TRACING_TARGET,
payload = %compact_payload,
"demo WebSocket notification received"
);
if rate_limiter.should_emit(&app_handle) {
let payload = match serde_json::to_string_pretty(&notification) {
let ui_payload = match serde_json::to_string_pretty(&notification) {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(error) => {
format!("cannot serialize typed WebSocket notification: {error}")
},
std::result::Result::Err(_) => compact_payload,
};
emit_demo_ws_message(
&app_handle,
"notification",
truncate_payload(payload),
truncate_payload(ui_payload),
);
}
},
@@ -640,20 +649,27 @@ fn build_standard_ws_request(
std::result::Result::Ok(target) => target,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let config = match parse_optional_json_as::<
let mut config = match parse_optional_json_as::<
kb_onchain_transport::WsAccountSubscribeConfig,
>(config_json, "configJson")
{
std::result::Result::Ok(config) => config,
std::result::Result::Ok(config) => config.unwrap_or_default(),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if config.encoding.is_none() {
config.encoding =
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64);
}
if let std::result::Result::Err(error) =
reject_present(filter_json, "filterJson", method)
{
return std::result::Result::Err(error);
}
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Account(
kb_onchain_transport::AccountSubscribeRequest { pubkey: target, config },
kb_onchain_transport::AccountSubscribeRequest {
pubkey: target,
config: std::option::Option::Some(config),
},
))
},
"blockSubscribe" => {
@@ -710,15 +726,22 @@ fn build_standard_ws_request(
{
return std::result::Result::Err(error);
}
let config = match parse_optional_json_as::<
let mut config = match parse_optional_json_as::<
kb_onchain_transport::WsProgramSubscribeConfig,
>(config_json, "configJson")
{
std::result::Result::Ok(config) => config,
std::result::Result::Ok(config) => config.unwrap_or_default(),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if config.encoding.is_none() {
config.encoding =
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64);
}
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Program(
kb_onchain_transport::ProgramSubscribeRequest { program_id: target, config },
kb_onchain_transport::ProgramSubscribeRequest {
program_id: target,
config: std::option::Option::Some(config),
},
))
},
"signatureSubscribe" => {
@@ -879,6 +902,42 @@ fn truncate_payload(payload: std::string::String) -> std::string::String {
mod tests {
use ts_rs::TS; // rust-rules: derive-import
#[test]
fn account_and_program_subscriptions_default_to_base64_without_overriding_explicit_encoding() {
let account = super::build_standard_ws_request(
"accountSubscribe",
std::option::Option::Some("11111111111111111111111111111111".to_string()),
std::option::Option::None,
std::option::Option::None,
);
let account = match account {
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Account(value)) => {
value
},
_ => panic!("accountSubscribe request was not built"),
};
assert_eq!(
account.config.and_then(|value| return value.encoding),
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::Base64)
);
let program = super::build_standard_ws_request(
"programSubscribe",
std::option::Option::Some(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string()),
std::option::Option::None,
std::option::Option::Some(r#"{"encoding":"jsonParsed"}"#.to_string()),
);
let program = match program {
std::result::Result::Ok(kb_onchain_transport::StandardWsRequest::Program(value)) => {
value
},
_ => panic!("programSubscribe request was not built"),
};
assert_eq!(
program.config.and_then(|value| return value.encoding),
std::option::Option::Some(kb_onchain_transport::RpcAccountEncoding::JsonParsed)
);
}
#[test]
fn tauri_numeric_bindings_use_json_compatible_numbers() {
let config = ts_rs::Config::default();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 9
// version: 10
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -39,6 +39,8 @@ pub(crate) use self::app_state::AppState;
pub(crate) use self::demo_backfill::DemoBackfillObserver;
/// Initial options shown by the backfill window.
pub(crate) use self::demo_backfill::DemoBackfillOptionsPayload;
/// One known program suggested by the backfill Program ID autocomplete.
pub(crate) use self::demo_backfill::DemoBackfillProgramOption;
/// One progress event emitted to the backfill window.
pub(crate) use self::demo_backfill::DemoBackfillProgressPayload;
/// UI request for one bounded backfill campaign.
@@ -327,6 +329,8 @@ pub(crate) use self::splash::SPLASH_MINIMUM_MS;
pub(crate) use self::splash::SplashOrder;
/// Emits one order to the splash frontend.
pub(crate) use self::splash::emit_splash_order;
/// Runs the startup sequence after the splash frontend is ready.
pub(crate) use self::splash::splash_frontend_ready_inner;
/// Waits for the minimum splash duration.
pub(crate) use self::splash::wait_until_minimum;

View File

@@ -1,17 +1,18 @@
// file: kb-app-demo-desktop/src/splash.rs
// version: 3
// version: 4
//! Splash-window payloads and startup sequencing helpers.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
use ts_rs::TS; // rust-rules: derive-import
/// Minimum splash duration before the main window is shown.
pub(crate) const SPLASH_MINIMUM_MS: u64 = 3100;
pub(crate) const SPLASH_MINIMUM_MS: u64 = 12100;
/// Splash fade duration.
pub(crate) const SPLASH_FADE_MS: u32 = 3000;
pub(crate) const SPLASH_FADE_MS: u32 = 12000;
/// Delay after fade-out before destroying the splash window.
pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 3100;
pub(crate) const SPLASH_CLOSE_WAIT_MS: u64 = 12100;
/// Command sent from Rust to the splash frontend.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -50,6 +51,96 @@ pub(crate) fn emit_splash_order(
}
}
/// Runs the startup sequence after the splash frontend installed its listener.
pub(crate) async fn splash_frontend_ready_inner(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
state: &crate::AppState,
) -> std::result::Result<(), std::string::String> {
if window.label() != "splash" {
return std::result::Result::Err(
"splash readiness may only originate from splash".to_string(),
);
}
if !state.begin_startup_sequence() {
return std::result::Result::Ok(());
}
let main_window = match app.get_webview_window("main") {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err("main window is missing".to_string());
},
};
let started_at = tokio::time::Instant::now();
crate::emit_splash_order(
&window,
"fadein",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Chargement de la configuration terminé."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_log",
std::option::Option::Some(
format!("Profil actif : {}", state.active_profile().name).as_str(),
),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some(
format!("Logging initialisé : {} routes actives.", state.logging_route_count())
.as_str(),
),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Pool HTTP initialisé."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::initialize_postgres_schema_for_startup(state, &window).await;
crate::emit_splash_order(
&window,
"add_msg",
std::option::Option::Some("Initialisation terminée."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
crate::emit_splash_order(
&window,
"fadeout",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
if let std::result::Result::Err(error) = main_window.show() {
return std::result::Result::Err(format!("cannot show main window: {error:?}"));
}
if let std::result::Result::Err(error) = main_window.set_focus() {
return std::result::Result::Err(format!("cannot focus main window: {error:?}"));
}
if let std::result::Result::Err(error) = window.destroy() {
return std::result::Result::Err(format!("cannot destroy splash window: {error:?}"));
}
return std::result::Result::Ok(());
}
/// Waits until the configured 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);

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 10
// version: 12
//! Tauri runtime assembly and private command wrappers.
@@ -36,6 +36,7 @@ pub fn run() -> kb_core::Result<()> {
builder = builder.manage(app_state);
builder = builder.invoke_handler(tauri::generate_handler![
emit_frontend_log,
splash_frontend_ready,
load_project_readme,
open_demo_backfill_window,
demo_backfill_options,
@@ -104,99 +105,18 @@ pub fn run() -> kb_core::Result<()> {
});
builder = builder.plugin(tracing_builder.build::<tauri::Wry>());
builder = builder.setup(|app| {
let splash_window = match app.get_webview_window("splash") {
std::option::Option::Some(window) => window,
std::option::Option::None => {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"splash window is missing",
)));
},
};
let main_window = match app.get_webview_window("main") {
std::option::Option::Some(window) => window,
std::option::Option::None => {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"main window is missing",
)));
},
};
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let started_at = tokio::time::Instant::now();
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Configuration chargée"),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Sous-système de logs initialisé"),
std::option::Option::None,
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Pool HTTP initialisé"),
std::option::Option::None,
std::option::Option::None,
);
let state = app_handle.state::<crate::AppState>();
crate::initialize_postgres_schema_for_startup(&state, &splash_window).await;
crate::emit_splash_order(
&splash_window,
"fadein",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
crate::emit_splash_order(
&splash_window,
"add_msg",
std::option::Option::Some("Initialisation..."),
std::option::Option::Some("info"),
std::option::Option::None,
);
crate::emit_splash_order(
&splash_window,
"add_msg",
std::option::Option::Some("Loading complete..."),
std::option::Option::Some("success"),
std::option::Option::None,
);
crate::wait_until_minimum(started_at, crate::SPLASH_MINIMUM_MS).await;
if cfg!(debug_assertions) {
crate::emit_splash_order(
&splash_window,
"add_log",
std::option::Option::Some("Start Fade-out"),
std::option::Option::None,
std::option::Option::None,
);
}
crate::emit_splash_order(
&splash_window,
"fadeout",
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(crate::SPLASH_FADE_MS),
);
tokio::time::sleep(std::time::Duration::from_millis(crate::SPLASH_CLOSE_WAIT_MS)).await;
if let std::result::Result::Err(error) = splash_window.destroy() {
tracing::error!(target: crate::TRACING_TARGET, "cannot destroy splash window: {error:?}");
}
if let std::result::Result::Err(error) = main_window.show() {
tracing::error!(target: crate::TRACING_TARGET, "cannot show main window: {error:?}");
}
if let std::result::Result::Err(error) = main_window.set_focus() {
tracing::error!(target: crate::TRACING_TARGET, "cannot focus main window: {error:?}");
}
});
if app.get_webview_window("splash").is_none() {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"splash window is missing",
)));
}
if app.get_webview_window("main").is_none() {
return std::result::Result::Err(std::boxed::Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"main window is missing",
)));
}
return std::result::Result::Ok(());
});
let run_result = builder.run(tauri::generate_context!());
@@ -208,6 +128,15 @@ pub fn run() -> kb_core::Result<()> {
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,
window: tauri::WebviewWindow,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<(), std::string::String> {
return crate::splash_frontend_ready_inner(app, window, &state).await;
}
fn install_default_rustls_provider() -> kb_core::Result<()> {
if rustls::crypto::CryptoProvider::get_default().is_some() {
return std::result::Result::Ok(());
@@ -759,8 +688,18 @@ fn demo_backfill_options(
} else {
roles.first().map(|item| return item.role.clone())
};
let programs = kb_program_ids::registered_program_ids()
.iter()
.map(|entry| {
return crate::DemoBackfillProgramOption {
code: entry.code().to_string(),
program_id: entry.program_id().to_string(),
};
})
.collect();
return crate::DemoBackfillOptionsPayload {
roles,
programs,
default_role,
default_commitment: "confirmed".to_string(),
default_page_size: 100,