From a595fd920d44e029bfe21b7f84e037b51db24d51 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 25 Jul 2026 20:19:28 +0200 Subject: [PATCH] v0.1.0-pre.048 --- kb-app-demo-desktop/frontend/demo_http.html | 96 +++++ kb-app-demo-desktop/frontend/main.html | 3 +- kb-app-demo-desktop/frontend/ts/demo_http.ts | 181 +++++++++ kb-app-demo-desktop/frontend/ts/main.ts | 10 +- kb-app-demo-desktop/src/demo_backfill.rs | 8 +- kb-app-demo-desktop/src/demo_http.rs | 400 +++++++++++++++++++ kb-app-demo-desktop/src/frontend_log.rs | 19 +- kb-app-demo-desktop/src/lib.rs | 19 +- kb-app-demo-desktop/src/splash.rs | 6 +- kb-app-demo-desktop/src/tauri.rs | 105 ++++- 10 files changed, 834 insertions(+), 13 deletions(-) create mode 100644 kb-app-demo-desktop/frontend/demo_http.html create mode 100644 kb-app-demo-desktop/frontend/ts/demo_http.ts create mode 100644 kb-app-demo-desktop/src/demo_http.rs diff --git a/kb-app-demo-desktop/frontend/demo_http.html b/kb-app-demo-desktop/frontend/demo_http.html new file mode 100644 index 0000000..1fccd1e --- /dev/null +++ b/kb-app-demo-desktop/frontend/demo_http.html @@ -0,0 +1,96 @@ + + + + + + + + + Khadhroony Bot3 - HTTP JSON-RPC + + + + +
+ +
+ +
+
+
+
+
+
+
+

Requête HTTP Solana standard

+

Cette démo route une requête JSON-RPC HTTP par rôle d'endpoint.

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Si renseigné, ce tableau JSON remplace le premier argument et la config JSON.
+
+ + +
+
+
+
+
+
+
+

Endpoints HTTP

+ +
+ +
+
+
+
+
+

Résultat

+ +
+ +
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/kb-app-demo-desktop/frontend/main.html b/kb-app-demo-desktop/frontend/main.html index 815ba02..0734ca5 100644 --- a/kb-app-demo-desktop/frontend/main.html +++ b/kb-app-demo-desktop/frontend/main.html @@ -1,5 +1,5 @@ - + @@ -22,6 +22,7 @@ Démos diff --git a/kb-app-demo-desktop/frontend/ts/demo_http.ts b/kb-app-demo-desktop/frontend/ts/demo_http.ts new file mode 100644 index 0000000..c87feab --- /dev/null +++ b/kb-app-demo-desktop/frontend/ts/demo_http.ts @@ -0,0 +1,181 @@ +// file: kb-app-demo-desktop/frontend/ts/demo_http.ts +// version: 4 + +import * as bootstrap from "bootstrap"; +import "simplebar"; +import ResizeObserver from "resize-observer-polyfill"; +import { invoke } from "@tauri-apps/api/core"; +import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts"; +import type { DemoHttpExecutionPayload } from "./bindings/kb_app_demo_desktop/demo_http/DemoHttpExecutionPayload.ts"; +import type { DemoHttpMethodOption } from "./bindings/kb_app_demo_desktop/demo_http/DemoHttpMethodOption.ts"; +import type { DemoHttpOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_http/DemoHttpOptionsPayload.ts"; +import type { DemoHttpRequest } from "./bindings/kb_app_demo_desktop/demo_http/DemoHttpRequest.ts"; +import type { DemoHttpRoleOption } from "./bindings/kb_app_demo_desktop/demo_http/DemoHttpRoleOption.ts"; + +(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; +(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; + +let roleOptions: DemoHttpRoleOption[] = []; +let methodOptions: DemoHttpMethodOption[] = []; + +function textInputValue(selector: string): string { + const element = document.querySelector(selector); + return element ? element.value.trim() : ""; +} + +function writeTextarea(selector: string, value: string): void { + const element = document.querySelector(selector); + if (element) { + element.value = value; + } +} + +function jsonText(value: unknown): string { + if (typeof value === "string") { + return value; + } + return JSON.stringify(value, null, 2); +} + +function roleSupportsMethod(role: DemoHttpRoleOption, method: DemoHttpMethodOption): boolean { + return role.requestKinds.includes("*") || role.requestKinds.includes(method.requestKind); +} + +function selectedRole(): DemoHttpRoleOption | null { + const role = textInputValue("#httpRoleSelect"); + return roleOptions.find(option => option.role === role) ?? null; +} + +function populateSelect(selector: string, values: Array<{ value: string; label: string }>): void { + const select = document.querySelector(selector); + if (!select) { + return; + } + select.textContent = ""; + for (const item of values) { + const option = document.createElement("option"); + option.value = item.value; + option.textContent = item.label; + select.appendChild(option); + } +} + +function refreshMethodList(): void { + const role = selectedRole(); + const filtered = role ? methodOptions.filter(method => roleSupportsMethod(role, method)) : methodOptions; + populateSelect("#httpMethodSelect", filtered.map(method => ({ + value: method.method, + label: `${method.method} — ${method.label}`, + }))); + refreshMethodFields(); +} + +function refreshMethodFields(): void { + const methodName = textInputValue("#httpMethodSelect"); + const method = methodOptions.find(option => option.method === methodName) ?? null; + const firstArg = document.querySelector("#httpFirstArgInput"); + const config = document.querySelector("#httpConfigInput"); + const params = document.querySelector("#httpParamsInput"); + if (firstArg) { + firstArg.disabled = method ? !method.requiresFirstArg : false; + firstArg.placeholder = method && method.requiresFirstArg ? "Argument requis" : "Non requis pour cette méthode"; + } + if (config) { + config.disabled = method ? !method.supportsConfigJson : false; + } + if (params) { + params.disabled = false; + } +} + +function formatHttpExecutionPayload(payload: DemoHttpExecutionPayload): string { + const header = { + endpointName: payload.endpointName, + provider: payload.provider, + endpointUrl: payload.endpointUrl, + role: payload.role, + method: payload.method, + requestKind: payload.requestKind, + methodClass: payload.methodClass, + }; + return `${JSON.stringify(header, null, 2)}\n\n--- response ---\n${payload.responseJson}`; +} + +async function copyTextarea(selector: string): Promise { + const element = document.querySelector(selector); + if (!element) { + return; + } + await navigator.clipboard.writeText(element.value); +} + +async function refreshHttpOptions(): Promise { + try { + const options = await invoke("demo_http_options"); + roleOptions = options.roles; + methodOptions = options.methods; + populateSelect("#httpRoleSelect", roleOptions.map(role => ({ + value: role.role, + label: role.role, + }))); + refreshMethodList(); + } catch (caughtError) { + const message = caughtError instanceof Error ? caughtError.message : String(caughtError); + writeTextarea("#httpResultOutput", `Erreur options HTTP : ${message}`); + frontendError("kb-app-demo-desktop.frontend.demo_http", `HTTP options loading failed: ${message}`); + } +} + +async function refreshHttpPool(): Promise { + try { + const snapshots = await invoke("demo_http_list_pool_clients"); + writeTextarea("#httpPoolOutput", jsonText(snapshots)); + } catch (caughtError) { + const message = caughtError instanceof Error ? caughtError.message : String(caughtError); + writeTextarea("#httpPoolOutput", `Erreur : ${message}`); + frontendError("kb-app-demo-desktop.frontend.demo_http", `HTTP pool refresh failed: ${message}`); + } +} + +async function executeHttpRequest(): Promise { + const request: DemoHttpRequest = { + role: textInputValue("#httpRoleSelect"), + method: textInputValue("#httpMethodSelect"), + firstArg: textInputValue("#httpFirstArgInput") || null, + configJson: textInputValue("#httpConfigInput") || null, + paramsJson: textInputValue("#httpParamsInput") || null, + }; + writeTextarea("#httpResultOutput", "Exécution en cours..."); + try { + const response = await invoke("demo_http_execute_request", { request }); + writeTextarea("#httpResultOutput", formatHttpExecutionPayload(response)); + frontendDebug("kb-app-demo-desktop.frontend.demo_http", `HTTP request completed: ${response.method}`); + } catch (caughtError) { + const message = caughtError instanceof Error ? caughtError.message : String(caughtError); + writeTextarea("#httpResultOutput", `Erreur : ${message}`); + frontendError("kb-app-demo-desktop.frontend.demo_http", `HTTP request failed: ${message}`); + } +} + +document.addEventListener("DOMContentLoaded", () => { + installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_http"); + frontendDebug("kb-app-demo-desktop.frontend.demo_http", "HTTP demo window loaded"); + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); + document.querySelector("#httpRoleSelect")?.addEventListener("change", refreshMethodList); + document.querySelector("#httpMethodSelect")?.addEventListener("change", refreshMethodFields); + document.querySelector("#executeHttpButton")?.addEventListener("click", () => { + void executeHttpRequest(); + }); + document.querySelector("#refreshHttpPoolButton")?.addEventListener("click", () => { + void refreshHttpPool(); + }); + document.querySelector("#copyHttpPoolButton")?.addEventListener("click", () => { + void copyTextarea("#httpPoolOutput"); + }); + document.querySelector("#copyHttpResultButton")?.addEventListener("click", () => { + void copyTextarea("#httpResultOutput"); + }); + void refreshHttpOptions(); + void refreshHttpPool(); +}); diff --git a/kb-app-demo-desktop/frontend/ts/main.ts b/kb-app-demo-desktop/frontend/ts/main.ts index 19b7dae..a31caea 100644 --- a/kb-app-demo-desktop/frontend/ts/main.ts +++ b/kb-app-demo-desktop/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/main.ts -// version: 1 +// version: 2 import * as bootstrap from "bootstrap"; import MarkdownIt from "markdown-it"; @@ -37,6 +37,14 @@ document.addEventListener("DOMContentLoaded", async () => { } }); +const openDemoHttpLink = document.querySelector("#openDemoHttpLink"); +if (openDemoHttpLink) { + openDemoHttpLink.addEventListener("click", event => { + event.preventDefault(); + void invoke("open_demo_http_window"); + }); +} + const openDemoBackfillLink = document.querySelector("#openDemoBackfillLink"); if (openDemoBackfillLink) { openDemoBackfillLink.addEventListener("click", event => { diff --git a/kb-app-demo-desktop/src/demo_backfill.rs b/kb-app-demo-desktop/src/demo_backfill.rs index a6f61ce..4851872 100644 --- a/kb-app-demo-desktop/src/demo_backfill.rs +++ b/kb-app-demo-desktop/src/demo_backfill.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_backfill.rs -// version: 10 +// version: 11 //! Tauri commands and UI payloads for bounded HTTP transaction backfills. @@ -29,7 +29,7 @@ pub(crate) struct DemoBackfillRoleOption { )] pub(crate) struct DemoBackfillOptionsPayload { /// Selectable endpoint roles. - pub(crate) roles: std::vec::Vec, + pub(crate) roles: std::vec::Vec, /// Preferred role when configured. pub(crate) default_role: std::option::Option, /// Default transaction commitment. @@ -213,7 +213,7 @@ impl std::ops::Drop for crate::DemoBackfillRunGuard<'_> { pub(crate) fn build_role_options( snapshots: std::vec::Vec, -) -> std::vec::Vec { +) -> std::vec::Vec { let mut role_methods = std::collections::BTreeMap::< std::string::String, ( @@ -242,7 +242,7 @@ pub(crate) fn build_role_options( { continue; } - output.push(DemoBackfillRoleOption { + output.push(crate::DemoBackfillRoleOption { role, providers: providers.into_iter().collect(), }); diff --git a/kb-app-demo-desktop/src/demo_http.rs b/kb-app-demo-desktop/src/demo_http.rs new file mode 100644 index 0000000..1b7df11 --- /dev/null +++ b/kb-app-demo-desktop/src/demo_http.rs @@ -0,0 +1,400 @@ +// file: kb-app-demo-desktop/src/demo_http.rs +// version: 9 + +//! 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_desktop/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, +} + +/// 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_desktop/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_desktop/demo_http/DemoHttpOptionsPayload.ts" +)] +pub(crate) struct DemoHttpOptionsPayload { + /// Selectable roles. + pub(crate) roles: std::vec::Vec, + /// Selectable methods. + pub(crate) methods: std::vec::Vec, +} + +/// 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_desktop/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, + /// Optional JSON configuration string appended after the first argument. + pub(crate) config_json: std::option::Option, + /// Optional raw JSON-RPC params array. When present, it overrides firstArg and configJson. + pub(crate) params_json: std::option::Option, +} + +/// 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_desktop/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 { + 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_onchain_transport::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_onchain_transport::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, +) -> std::vec::Vec { + let mut roles = std::collections::BTreeMap::< + std::string::String, + std::collections::BTreeSet, + >::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 { + 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_onchain_transport::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::result::Result, 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::(&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::result::Result>, 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::(¶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, + params_json: std::option::Option>, +) -> std::result::Result, 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_onchain_transport::HttpMethodClass) -> &'static str { + return match method_class { + kb_onchain_transport::HttpMethodClass::GeneralRpc => "GeneralRpc", + kb_onchain_transport::HttpMethodClass::SendTransaction => "SendTransaction", + kb_onchain_transport::HttpMethodClass::HeavyRead => "HeavyRead", + }; +} + +#[cfg(test)] +mod tests { + #[test] + fn raw_params_override_first_argument_and_config() { + let params = super::build_demo_http_params( + "getBalance", + std::option::Option::Some("ignored"), + std::option::Option::Some(serde_json::json!({"commitment":"confirmed"})), + std::option::Option::Some(vec![serde_json::json!("exact")]), + ); + assert_eq!(params, std::result::Result::Ok(vec![serde_json::json!("exact")])); + } + + #[test] + fn required_first_argument_fails_closed() { + let params = super::build_demo_http_params( + "getBalance", + std::option::Option::None, + std::option::Option::None, + std::option::Option::None, + ); + assert!(params.is_err()); + } + + #[test] + fn method_inventory_is_unique_and_classified() { + let options = crate::build_http_method_options(); + let mut names = std::collections::BTreeSet::new(); + for option in &options { + assert!(names.insert(option.method.as_str())); + assert_eq!( + option.request_kind, + kb_onchain_transport::request_kind_from_method(option.method.as_str()) + ); + } + assert!(!options.is_empty()); + } +} diff --git a/kb-app-demo-desktop/src/frontend_log.rs b/kb-app-demo-desktop/src/frontend_log.rs index 5f68bcd..2a81881 100644 --- a/kb-app-demo-desktop/src/frontend_log.rs +++ b/kb-app-demo-desktop/src/frontend_log.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/frontend_log.rs -// version: 1 +// version: 2 //! Frontend logging bridge used by Tauri WebView scripts. @@ -49,6 +49,12 @@ fn normalize_frontend_target(target: &str) -> std::string::String { if trimmed == "kb-app-demo-desktop.frontend.splash" { return trimmed.to_string(); } + if trimmed == "kb-app-demo-desktop.frontend.backfill" { + return trimmed.to_string(); + } + if trimmed == "kb-app-demo-desktop.frontend.demo_http" { + return trimmed.to_string(); + } return "kb-app-demo-desktop.frontend".to_string(); } @@ -69,4 +75,15 @@ mod tests { "kb-app-demo-desktop.frontend.main" ); } + #[test] + fn normalize_frontend_target_keeps_demo_targets() { + assert_eq!( + super::normalize_frontend_target("kb-app-demo-desktop.frontend.backfill"), + "kb-app-demo-desktop.frontend.backfill" + ); + assert_eq!( + super::normalize_frontend_target("kb-app-demo-desktop.frontend.demo_http"), + "kb-app-demo-desktop.frontend.demo_http" + ); + } } diff --git a/kb-app-demo-desktop/src/lib.rs b/kb-app-demo-desktop/src/lib.rs index 270fcd9..d0ec96e 100644 --- a/kb-app-demo-desktop/src/lib.rs +++ b/kb-app-demo-desktop/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/lib.rs -// version: 4 +// version: 5 //! Tauri desktop demo application for `khadhroony-bot3`. @@ -10,6 +10,7 @@ mod app_state; mod constants; mod demo_backfill; +mod demo_http; mod frontend_log; mod main_window; mod splash; @@ -40,6 +41,22 @@ pub(crate) use self::demo_backfill::build_demo_backfill_pipeline_request; pub(crate) use self::demo_backfill::build_role_options; /// Converts a pipeline summary to the UI-safe summary. pub(crate) use self::demo_backfill::demo_backfill_summary_payload; +/// HTTP demo response payload. +pub(crate) use self::demo_http::DemoHttpExecutionPayload; +/// One selectable HTTP JSON-RPC method. +pub(crate) use self::demo_http::DemoHttpMethodOption; +/// HTTP demo options payload. +pub(crate) use self::demo_http::DemoHttpOptionsPayload; +/// HTTP demo request payload. +pub(crate) use self::demo_http::DemoHttpRequest; +/// One selectable HTTP endpoint role. +pub(crate) use self::demo_http::DemoHttpRoleOption; +/// Builds the HTTP method inventory. +pub(crate) use self::demo_http::build_http_method_options; +/// Builds the HTTP role inventory from pool snapshots. +pub(crate) use self::demo_http::build_http_role_options; +/// Executes one raw HTTP JSON-RPC request. +pub(crate) use self::demo_http::demo_http_execute_request_inner; /// Frontend logging payload. pub(crate) use self::frontend_log::FrontendLogPayload; /// Emits one normalized frontend log event. diff --git a/kb-app-demo-desktop/src/splash.rs b/kb-app-demo-desktop/src/splash.rs index 2c424b0..8460f3f 100644 --- a/kb-app-demo-desktop/src/splash.rs +++ b/kb-app-demo-desktop/src/splash.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/splash.rs -// version: 2 +// version: 3 //! Splash-window payloads and startup sequencing helpers. @@ -80,8 +80,8 @@ mod tests { #[test] fn splash_timing_is_ordered() { - assert!(crate::SPLASH_MINIMUM_MS > 0); - assert!(crate::SPLASH_FADE_MS > 0); + assert_eq!(crate::SPLASH_MINIMUM_MS, 3100); + assert_eq!(crate::SPLASH_FADE_MS, 3000); assert!(crate::SPLASH_CLOSE_WAIT_MS >= u64::from(crate::SPLASH_FADE_MS)); } } diff --git a/kb-app-demo-desktop/src/tauri.rs b/kb-app-demo-desktop/src/tauri.rs index bc976cb..6e094fb 100644 --- a/kb-app-demo-desktop/src/tauri.rs +++ b/kb-app-demo-desktop/src/tauri.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/tauri.rs -// version: 4 +// version: 5 //! Tauri runtime assembly and private command wrappers. @@ -28,6 +28,7 @@ pub fn run() -> kb_core::Result<()> { config_path = app_state.config_path(), active_profile = app_state.active_profile().name.as_str(), logging_routes = app_state.logging_route_count(), + configured_profiles = app_state.app_config().profiles.len(), "starting desktop demo application" ); let tracing_builder = tauri_plugin_tracing::Builder::new(); @@ -40,6 +41,10 @@ pub fn run() -> kb_core::Result<()> { demo_backfill_options, demo_backfill_execute, demo_backfill_cancel, + open_demo_http_window, + demo_http_list_pool_clients, + demo_http_options, + demo_http_execute_request, ]); builder = builder.plugin(tracing_builder.build::()); builder = builder.setup(|app| { @@ -63,6 +68,27 @@ pub fn run() -> kb_core::Result<()> { }; 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, + ); crate::emit_splash_order( &splash_window, "fadein", @@ -73,11 +99,27 @@ pub fn run() -> kb_core::Result<()> { crate::emit_splash_order( &splash_window, "add_msg", - std::option::Option::Some("Initialisation de Khadhroony Bot3..."), + 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", @@ -202,6 +244,7 @@ fn demo_backfill_options( }; } +#[allow(clippy::question_mark_used)] #[tauri::command] async fn demo_backfill_execute( app_handle: tauri::AppHandle, @@ -259,3 +302,61 @@ async fn demo_backfill_execute( }; return std::result::Result::Ok(crate::demo_backfill_summary_payload(summary)); } + +#[tauri::command] +fn open_demo_http_window( + app_handle: tauri::AppHandle, +) -> std::result::Result<(), std::string::String> { + let existing_window = app_handle.get_webview_window("demo_http"); + if let std::option::Option::Some(window) = existing_window { + if let std::result::Result::Err(error) = window.show() { + return std::result::Result::Err(error.to_string()); + } + if let std::result::Result::Err(error) = window.set_focus() { + return std::result::Result::Err(error.to_string()); + } + return std::result::Result::Ok(()); + } + let build_result = tauri::WebviewWindowBuilder::new( + &app_handle, + "demo_http", + tauri::WebviewUrl::App("demo_http.html".into()), + ) + .title("Khadhroony Bot3 - HTTP JSON-RPC") + .inner_size(1280.0, 860.0) + .min_inner_size(960.0, 620.0) + .resizable(true) + .visible(true) + .build(); + return match build_result { + std::result::Result::Ok(window) => match window.set_focus() { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }, + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }; +} + +#[tauri::command] +fn demo_http_list_pool_clients( + state: tauri::State<'_, crate::AppState>, +) -> std::vec::Vec { + return state.http_pool().snapshot(); +} + +#[tauri::command] +fn demo_http_options(state: tauri::State<'_, crate::AppState>) -> crate::DemoHttpOptionsPayload { + return crate::DemoHttpOptionsPayload { + roles: crate::build_http_role_options(state.http_pool().snapshot()), + methods: crate::build_http_method_options(), + }; +} + +#[allow(clippy::question_mark_used)] +#[tauri::command] +async fn demo_http_execute_request( + state: tauri::State<'_, crate::AppState>, + request: crate::DemoHttpRequest, +) -> std::result::Result { + return crate::demo_http_execute_request_inner(state, request).await; +}