v0.1.0-pre.048

This commit is contained in:
2026-07-25 20:19:28 +02:00
parent 33a0762658
commit a595fd920d
10 changed files with 834 additions and 13 deletions

View File

@@ -0,0 +1,96 @@
<!-- file: kb-app-demo-desktop/frontend/demo_http.html -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Khadhroony Bot3 - HTTP JSON-RPC</title>
<link rel="stylesheet" href="sass/main.scss" />
</head>
<body class="bg-body-tertiary">
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<div class="navbar-brand d-flex align-items-center">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">HTTP JSON-RPC</span>
</div>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container py-4">
<div class="row g-4">
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0">
<div class="card-body">
<h1 class="h4 card-title">Requête HTTP Solana standard</h1>
<p class="text-body-secondary">Cette démo route une requête JSON-RPC HTTP par rôle d'endpoint.</p>
<div class="mb-3">
<label class="form-label" for="httpRoleSelect">Rôle</label>
<select id="httpRoleSelect" class="form-select"></select>
</div>
<div class="mb-3">
<label class="form-label" for="httpMethodSelect">Méthode</label>
<select id="httpMethodSelect" class="form-select"></select>
</div>
<div class="mb-3">
<label class="form-label" for="httpFirstArgInput">Premier argument</label>
<input id="httpFirstArgInput" class="form-control" placeholder="Signature, pubkey ou transaction encodée" />
</div>
<div class="mb-3">
<label class="form-label" for="httpConfigInput">Config JSON</label>
<textarea id="httpConfigInput" class="form-control font-monospace" rows="4" placeholder='{"commitment":"confirmed"}'></textarea>
</div>
<div class="mb-3">
<label class="form-label" for="httpParamsInput">Params JSON complet</label>
<textarea id="httpParamsInput" class="form-control font-monospace" rows="4" placeholder='["pubkey", {"encoding":"jsonParsed"}]'></textarea>
<div class="form-text">Si renseigné, ce tableau JSON remplace le premier argument et la config JSON.</div>
</div>
<button id="executeHttpButton" class="btn btn-primary">Exécuter</button>
<button id="refreshHttpPoolButton" class="btn btn-outline-secondary ms-2">Rafraîchir endpoints</button>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h5 card-title mb-0">Endpoints HTTP</h2>
<button id="copyHttpPoolButton" class="btn btn-sm btn-outline-secondary">Copier</button>
</div>
<textarea id="httpPoolOutput" class="form-control font-monospace" rows="10" readonly>Chargement...</textarea>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h2 class="h5 card-title mb-0">Résultat</h2>
<button id="copyHttpResultButton" class="btn btn-sm btn-outline-secondary">Copier</button>
</div>
<textarea id="httpResultOutput" class="form-control font-monospace" rows="18" readonly>Aucune requête exécutée.</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_http.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 2 -->
<!-- version: 3 -->
<!DOCTYPE html>
<html lang="fr">
<head>
@@ -22,6 +22,7 @@
Démos
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a id="openDemoHttpLink" class="dropdown-item" href="#">HTTP JSON-RPC</a></li>
<li><a id="openDemoBackfillLink" class="dropdown-item" href="#">Backfill HTTP</a></li>
</ul>
</div>

View File

@@ -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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selector);
return element ? element.value.trim() : "";
}
function writeTextarea(selector: string, value: string): void {
const element = document.querySelector<HTMLTextAreaElement>(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<HTMLSelectElement>(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<HTMLInputElement>("#httpFirstArgInput");
const config = document.querySelector<HTMLTextAreaElement>("#httpConfigInput");
const params = document.querySelector<HTMLTextAreaElement>("#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<void> {
const element = document.querySelector<HTMLTextAreaElement>(selector);
if (!element) {
return;
}
await navigator.clipboard.writeText(element.value);
}
async function refreshHttpOptions(): Promise<void> {
try {
const options = await invoke<DemoHttpOptionsPayload>("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<void> {
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<void> {
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<DemoHttpExecutionPayload>("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<HTMLSelectElement>("#httpRoleSelect")?.addEventListener("change", refreshMethodList);
document.querySelector<HTMLSelectElement>("#httpMethodSelect")?.addEventListener("change", refreshMethodFields);
document.querySelector<HTMLButtonElement>("#executeHttpButton")?.addEventListener("click", () => {
void executeHttpRequest();
});
document.querySelector<HTMLButtonElement>("#refreshHttpPoolButton")?.addEventListener("click", () => {
void refreshHttpPool();
});
document.querySelector<HTMLButtonElement>("#copyHttpPoolButton")?.addEventListener("click", () => {
void copyTextarea("#httpPoolOutput");
});
document.querySelector<HTMLButtonElement>("#copyHttpResultButton")?.addEventListener("click", () => {
void copyTextarea("#httpResultOutput");
});
void refreshHttpOptions();
void refreshHttpPool();
});

View File

@@ -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<HTMLAnchorElement>("#openDemoHttpLink");
if (openDemoHttpLink) {
openDemoHttpLink.addEventListener("click", event => {
event.preventDefault();
void invoke("open_demo_http_window");
});
}
const openDemoBackfillLink = document.querySelector<HTMLAnchorElement>("#openDemoBackfillLink");
if (openDemoBackfillLink) {
openDemoBackfillLink.addEventListener("click", event => {

View File

@@ -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<DemoBackfillRoleOption>,
pub(crate) roles: std::vec::Vec<crate::DemoBackfillRoleOption>,
/// Preferred role when configured.
pub(crate) default_role: std::option::Option<std::string::String>,
/// Default transaction commitment.
@@ -213,7 +213,7 @@ impl std::ops::Drop for crate::DemoBackfillRunGuard<'_> {
pub(crate) fn build_role_options(
snapshots: std::vec::Vec<kb_onchain_transport::HttpPoolClientSnapshot>,
) -> std::vec::Vec<DemoBackfillRoleOption> {
) -> std::vec::Vec<crate::DemoBackfillRoleOption> {
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(),
});

View File

@@ -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<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_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<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_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<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_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<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_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<kb_onchain_transport::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 dun 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 dinflation", 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::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>(&params_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_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());
}
}

View File

@@ -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"
);
}
}

View File

@@ -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.

View File

@@ -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));
}
}

View File

@@ -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::<tauri::Wry>());
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<kb_onchain_transport::HttpPoolClientSnapshot> {
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<crate::DemoHttpExecutionPayload, std::string::String> {
return crate::demo_http_execute_request_inner(state, request).await;
}