import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import { invoke } from "@tauri-apps/api/core"; import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; interface DemoHttpPoolClientSnapshot { endpointName: string; provider: string; endpointUrl: string; roles: string[]; status: string; pausedRemainingMs: number | null; availableConcurrencySlots: number; } interface DemoHttpRequest { role: string; method: string; firstArg: string | null; configJson: string | null; } interface DemoHttpExecutionPayload { endpointName: string; provider: string; endpointUrl: string; role: string; method: string; methodClass: string; responseJson: string; } let demoHttpLastResponseRawText = ""; function appendLogLine(textarea: HTMLTextAreaElement, line: string): void { const now = new Date(); const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false }); const lines = textarea.value === "" ? [] : textarea.value.split("\n"); lines.push(`[${timestamp}] ${line}`); const maxLines = 400; textarea.value = lines.slice(-maxLines).join("\n"); textarea.scrollTop = textarea.scrollHeight; } function methodNeedsFirstArg(method: string): boolean { return [ "getBalance", "getAccountInfo", "getProgramAccounts", "getSignaturesForAddress", "getTransaction", "sendTransaction", ].includes(method); } function firstArgLabelForMethod(method: string): string { if (method === "getBalance") return "Address"; if (method === "getAccountInfo") return "Address"; if (method === "getProgramAccounts") return "Program id"; if (method === "getSignaturesForAddress") return "Address"; if (method === "getTransaction") return "Signature"; if (method === "sendTransaction") return "Encoded transaction (base64)"; return "First arg"; } function firstArgHelpForMethod(method: string): string { if (method === "getBalance") return "Adresse Solana dont on veut lire le solde."; if (method === "getAccountInfo") return "Adresse Solana du compte à lire."; if (method === "getProgramAccounts") return "Program id dont on veut lister les comptes."; if (method === "getSignaturesForAddress") return "Adresse Solana dont on veut lire les signatures récentes."; if (method === "getTransaction") return "Signature exacte d’une transaction."; if (method === "sendTransaction") { return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion d’erreur."; } return "Adresse, program id, signature ou transaction encodée selon la méthode."; } function configHelpForMethod(method: string): string { if (method === "getProgramAccounts") { return "Preset volontairement filtré pour éviter une réponse trop large."; } if (method === "sendTransaction") { return "Configuration d’envoi. Avec le preset de transaction invalide, la requête doit échouer côté RPC."; } return "Configuration JSON optionnelle."; } function defaultRoleForMethod(method: string): string { if (method === "sendTransaction") return "http_transactions"; if (method === "getProgramAccounts") return "http_heavy"; return "http_queries"; } function defaultFirstArgForMethod(method: string): string { if (method === "getBalance") return "11111111111111111111111111111111"; if (method === "getAccountInfo") return "11111111111111111111111111111111"; if (method === "getProgramAccounts") return "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; if (method === "getSignaturesForAddress") return "11111111111111111111111111111111"; if (method === "getTransaction") { return "5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7"; } if (method === "sendTransaction") return "AQ=="; return ""; } function defaultConfigForMethod(method: string): string { if (method === "getSlot" || method === "getBlockHeight" || method === "getLatestBlockhash") { return `{"commitment":"confirmed"}`; } if (method === "getBalance") { return `{"commitment":"confirmed"}`; } if (method === "getAccountInfo") { return `{"encoding":"base64","commitment":"confirmed"}`; } if (method === "getProgramAccounts") { return `{ "encoding":"base64", "commitment":"confirmed", "dataSlice":{"offset":0,"length":0}, "filters":[ {"dataSize":165}, {"memcmp":{"offset":32,"bytes":"11111111111111111111111111111111"}} ] }`; } if (method === "getSignaturesForAddress") { return `{"limit":10,"commitment":"confirmed"}`; } if (method === "getTransaction") { return `{"encoding":"json","commitment":"confirmed","maxSupportedTransactionVersion":0}`; } if (method === "sendTransaction") { return `{"encoding":"base64","skipPreflight":true,"maxRetries":0}`; } return ""; } function renderPoolSnapshots( snapshots: DemoHttpPoolClientSnapshot[], tableBody: HTMLTableSectionElement, ): void { tableBody.innerHTML = ""; for (const snapshot of snapshots) { const row = document.createElement("tr"); const endpointCell = document.createElement("td"); endpointCell.innerHTML = `
${snapshot.endpointName}
${snapshot.endpointUrl}
`; const providerCell = document.createElement("td"); providerCell.textContent = snapshot.provider; const statusCell = document.createElement("td"); statusCell.textContent = snapshot.status; const pausedCell = document.createElement("td"); pausedCell.textContent = snapshot.pausedRemainingMs === null ? "-" : String(snapshot.pausedRemainingMs); const concurrencyCell = document.createElement("td"); concurrencyCell.textContent = String(snapshot.availableConcurrencySlots); const rolesCell = document.createElement("td"); rolesCell.textContent = snapshot.roles.join(", "); row.appendChild(endpointCell); row.appendChild(providerCell); row.appendChild(statusCell); row.appendChild(pausedCell); row.appendChild(concurrencyCell); row.appendChild(rolesCell); tableBody.appendChild(row); } } function renderResponse( responseTextarea: HTMLTextAreaElement, prettyToggle: HTMLInputElement, ): void { if (demoHttpLastResponseRawText.trim() === "") { responseTextarea.value = ""; return; } if (!prettyToggle.checked) { responseTextarea.value = demoHttpLastResponseRawText; return; } try { const parsed = JSON.parse(demoHttpLastResponseRawText); responseTextarea.value = JSON.stringify(parsed, null, 2); } catch { responseTextarea.value = demoHttpLastResponseRawText; } } async function copyTextToClipboard(text: string): Promise { if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return; } const tempTextarea = document.createElement("textarea"); tempTextarea.value = text; tempTextarea.style.position = "fixed"; tempTextarea.style.left = "-9999px"; document.body.appendChild(tempTextarea); tempTextarea.focus(); tempTextarea.select(); document.execCommand("copy"); document.body.removeChild(tempTextarea); } function updateMethodForm( roleSelect: HTMLSelectElement, methodSelect: HTMLSelectElement, firstArgGroup: HTMLDivElement, firstArgLabel: HTMLLabelElement, firstArgHelp: HTMLDivElement, firstArgInput: HTMLInputElement, configTextarea: HTMLTextAreaElement, configHelp: HTMLDivElement, ): void { const method = methodSelect.value; firstArgGroup.style.display = methodNeedsFirstArg(method) ? "" : "none"; firstArgLabel.textContent = firstArgLabelForMethod(method); firstArgHelp.textContent = firstArgHelpForMethod(method); firstArgInput.value = defaultFirstArgForMethod(method); configTextarea.value = defaultConfigForMethod(method); configHelp.textContent = configHelpForMethod(method); roleSelect.value = defaultRoleForMethod(method); } async function refreshPoolSnapshot( tableBody: HTMLTableSectionElement, logTextarea: HTMLTextAreaElement, shouldLog: boolean, ): Promise { try { const snapshots = await invoke("demo_http_list_pool_clients"); renderPoolSnapshots(snapshots, tableBody); if (shouldLog) { appendLogLine(logTextarea, `[ui] refreshed http pool snapshot (${snapshots.length} endpoint(s))`); } } catch (error) { appendLogLine(logTextarea, `[ui] refresh pool error: ${String(error)}`); } } document.addEventListener("DOMContentLoaded", async () => { void takeoverConsole(); const sidebarToggle = document.querySelector('#sidebarToggle'); if (sidebarToggle) { // restaurer l’état depuis localStorage if (localStorage.getItem('sidebar-toggle') === 'true') { document.body.classList.add('sidenav-toggled'); } sidebarToggle.addEventListener('click', (event) => { event.preventDefault(); document.body.classList.toggle('sidenav-toggled'); localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false'); }); } const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); const toastElList = document.querySelectorAll('.toast'); Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl)); const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]'); Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)); const gobackto = location.pathname + location.search; document.querySelectorAll('a[data-setlang]').forEach((a) => { const href = a.getAttribute("href"); if (!href) return; // pas de href => on ignore const url = new URL(href, location.origin); url.searchParams.set("gobackto", gobackto); // conserve une URL relative (path + query) a.setAttribute("href", url.pathname + "?" + url.searchParams.toString()); }); const roleSelect = document.querySelector("#demoHttpRoleSelect"); const methodSelect = document.querySelector("#demoHttpMethodSelect"); const firstArgGroup = document.querySelector("#demoHttpFirstArgGroup"); const firstArgLabel = document.querySelector("#demoHttpFirstArgLabel"); const firstArgHelp = document.querySelector("#demoHttpFirstArgHelp"); const firstArgInput = document.querySelector("#demoHttpFirstArgInput"); const configTextarea = document.querySelector("#demoHttpConfigTextarea"); const configHelp = document.querySelector("#demoHttpConfigHelp"); const executeButton = document.querySelector("#demoHttpExecuteButton"); const refreshPoolButton = document.querySelector("#demoHttpRefreshPoolButton"); const clearLogButton = document.querySelector("#demoHttpClearLogButton"); const copyResponseButton = document.querySelector("#demoHttpCopyResponseButton"); const prettyToggle = document.querySelector("#demoHttpPrettyToggle"); const responseTextarea = document.querySelector("#demoHttpResponseTextarea"); const logTextarea = document.querySelector("#demoHttpLogTextarea"); const poolTableBody = document.querySelector("#demoHttpPoolTableBody"); const lastEndpointText = document.querySelector("#demoHttpLastEndpointText"); const lastProviderText = document.querySelector("#demoHttpLastProviderText"); const lastMethodClassText = document.querySelector("#demoHttpLastMethodClassText"); if ( !roleSelect || !methodSelect || !firstArgGroup || !firstArgLabel || !firstArgHelp || !firstArgInput || !configTextarea || !configHelp || !executeButton || !refreshPoolButton || !clearLogButton || !copyResponseButton || !prettyToggle || !responseTextarea || !logTextarea || !poolTableBody || !lastEndpointText || !lastProviderText || !lastMethodClassText ) { debug("demo_http UI controls not found"); return; } updateMethodForm( roleSelect, methodSelect, firstArgGroup, firstArgLabel, firstArgHelp, firstArgInput, configTextarea, configHelp, ); methodSelect.addEventListener("change", () => { updateMethodForm( roleSelect, methodSelect, firstArgGroup, firstArgLabel, firstArgHelp, firstArgInput, configTextarea, configHelp, ); }); prettyToggle.addEventListener("change", () => { renderResponse(responseTextarea, prettyToggle); }); refreshPoolButton.addEventListener("click", async () => { await refreshPoolSnapshot(poolTableBody, logTextarea, true); }); clearLogButton.addEventListener("click", () => { logTextarea.value = ""; }); copyResponseButton.addEventListener("click", async () => { if (responseTextarea.value.trim() === "") { appendLogLine(logTextarea, "[ui] copy skipped: no response available"); return; } try { await copyTextToClipboard(responseTextarea.value); appendLogLine(logTextarea, "[ui] response copied to clipboard"); } catch (error) { appendLogLine(logTextarea, `[ui] copy response error: ${String(error)}`); } }); executeButton.addEventListener("click", async () => { const request: DemoHttpRequest = { role: roleSelect.value, method: methodSelect.value, firstArg: firstArgInput.value.trim() === "" ? null : firstArgInput.value.trim(), configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(), }; try { const response = await invoke("demo_http_execute_request", { request }); lastEndpointText.textContent = `${response.endpointName} (${response.endpointUrl})`; lastProviderText.textContent = response.provider; lastMethodClassText.textContent = response.methodClass; demoHttpLastResponseRawText = response.responseJson; renderResponse(responseTextarea, prettyToggle); appendLogLine( logTextarea, `[ui] ${response.method} via ${response.endpointName} / role=${response.role} / class=${response.methodClass}`, ); await refreshPoolSnapshot(poolTableBody, logTextarea, true); } catch (error) { demoHttpLastResponseRawText = String(error); renderResponse(responseTextarea, prettyToggle); appendLogLine(logTextarea, `[ui] execute error: ${String(error)}`); await refreshPoolSnapshot(poolTableBody, logTextarea, true); } }); appendLogLine(logTextarea, "[ui] demo_http window loaded"); await refreshPoolSnapshot(poolTableBody, logTextarea, true); debug("demo_http window loaded"); });