v0.3.8-pre.008
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -127,14 +127,57 @@
|
||||
</section>
|
||||
|
||||
<section data-view-panel="accounts" hidden>
|
||||
<div class="mb-4">
|
||||
<h1 class="h3 mb-1">RAW Accounts</h1>
|
||||
<p class="text-body-secondary mb-0">Structure DataTables conservée hors connexion métier jusqu'à pre.008. Aucun byte Account ne traverse IPC dans cette tranche.</p>
|
||||
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">RAW Accounts</h1>
|
||||
<p class="text-body-secondary mb-0">Inspection Store random-access ; DataTables reste l'unique propriétaire de la pagination visible.</p>
|
||||
</div>
|
||||
<button id="refreshAccounts" class="btn btn-outline-primary" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<label class="form-label" for="accountPubkey">Pubkey exacte</label>
|
||||
<input id="accountPubkey" class="form-control font-monospace" type="text" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label class="form-label" for="accountSlotMin">Slot minimum</label>
|
||||
<input id="accountSlotMin" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-2">
|
||||
<label class="form-label" for="accountSlotMax">Slot maximum</label>
|
||||
<input id="accountSlotMax" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" placeholder="optionnel">
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<label class="form-label" for="accountDirection">Direction canonique</label>
|
||||
<select id="accountDirection" class="form-select">
|
||||
<option value="descending" selected>Descending</option>
|
||||
<option value="ascending">Ascending</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accountsQueryError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<div class="table-responsive app-store-table-container">
|
||||
<table id="rawAccountsTable" class="table table-hover align-middle mb-0 app-store-table">
|
||||
<caption class="visually-hidden">Structure future de la table RAW Accounts.</caption>
|
||||
<thead><tr><th>Pubkey</th><th>Slot</th><th>State hash</th><th>Owner</th><th>Lamports</th><th>Data length</th></tr></thead>
|
||||
<caption class="visually-hidden">Inspection server-side des états RAW Account.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pubkey</th>
|
||||
<th>Slot</th>
|
||||
<th>State hash</th>
|
||||
<th>Owner</th>
|
||||
<th>Lamports</th>
|
||||
<th>Executable</th>
|
||||
<th>Rent epoch</th>
|
||||
<th>Data length</th>
|
||||
<th>Détail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -211,6 +254,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="accountDetailModal" class="modal fade" tabindex="-1" aria-labelledby="accountDetailTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="accountDetailTitle" class="modal-title h5">RAW Account detail</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="accountDetailError" class="alert alert-danger" role="status" aria-live="polite" hidden></div>
|
||||
<dl class="row app-runtime-list mb-4">
|
||||
<dt class="col-sm-3">Pubkey</dt><dd id="accountDetailPubkey" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Slot</dt><dd id="accountDetailSlot" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">State hash</dt><dd id="accountDetailStateHash" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Owner</dt><dd id="accountDetailOwner" class="col-sm-9 text-break">—</dd>
|
||||
<dt class="col-sm-3">Lamports</dt><dd id="accountDetailLamports" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Executable</dt><dd id="accountDetailExecutable" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Rent epoch</dt><dd id="accountDetailRentEpoch" class="col-sm-9">—</dd>
|
||||
<dt class="col-sm-3">Data length</dt><dd id="accountDetailDataLength" class="col-sm-9">—</dd>
|
||||
</dl>
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-2">
|
||||
<h3 class="h6 mb-0">Account data preview hex</h3>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button id="accountDetailDataPreviewCopy" class="btn btn-sm btn-outline-secondary" type="button" title="Copier le preview hex affiché" aria-label="Copier le preview hex affiché" hidden>
|
||||
<i class="fa-regular fa-copy" aria-hidden="true"></i>
|
||||
<span>Copier</span>
|
||||
</button>
|
||||
<span id="accountDetailPreviewTruncated" class="badge text-bg-warning" hidden>Preview tronquée à 512 bytes</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre id="accountDetailDataPreview" class="small bg-body-tertiary border rounded p-3 overflow-auto text-break mb-0">—</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<small>© 2026 SASEDEV</small>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
import DataTable from "datatables.net-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
import { Modal } from "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import type { StoreAccountDetailDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountDetailDto.ts";
|
||||
import type { StoreAccountDetailRequestDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountDetailRequestDto.ts";
|
||||
import type { StoreAccountQueryRequestDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountQueryRequestDto.ts";
|
||||
import type { StoreAccountQueryResponseDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountQueryResponseDto.ts";
|
||||
import type { StoreAccountRowDto } from "./bindings/ksp_app_store_desk/dto_account/StoreAccountRowDto.ts";
|
||||
import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts";
|
||||
import type { StoreRuntimeStatusDto } from "./bindings/ksp_app_store_desk/dto_common/StoreRuntimeStatusDto.ts";
|
||||
import type { StoreTransactionDetailDto } from "./bindings/ksp_app_store_desk/dto_transaction/StoreTransactionDetailDto.ts";
|
||||
@@ -332,23 +337,128 @@ function initializeTransactionTable(): void {
|
||||
frontendTrace("main", "Store Desk transaction DataTable initialized", { pagingOwner: "datatables", serverSide: true });
|
||||
}
|
||||
|
||||
function initializeAccountSkeleton(): void {
|
||||
function currentAccountQuery(page: DataTablesPageRequest): StoreAccountQueryRequestDto | null {
|
||||
if (!Number.isSafeInteger(page.start) || page.start < 0 || !Number.isSafeInteger(page.length) || ![25, 50, 100].includes(page.length)) {
|
||||
return null;
|
||||
}
|
||||
const pubkeyElement = document.querySelector<HTMLInputElement>("#accountPubkey");
|
||||
const slotMinElement = document.querySelector<HTMLInputElement>("#accountSlotMin");
|
||||
const slotMaxElement = document.querySelector<HTMLInputElement>("#accountSlotMax");
|
||||
const directionElement = document.querySelector<HTMLSelectElement>("#accountDirection");
|
||||
const pubkey = pubkeyElement?.value.trim() ?? "";
|
||||
const slotMin = slotMinElement?.value.trim() ?? "";
|
||||
const slotMax = slotMaxElement?.value.trim() ?? "";
|
||||
return {
|
||||
direction: directionElement?.value ?? "descending",
|
||||
limit: page.length,
|
||||
offset: page.start.toString(),
|
||||
pubkey: pubkey === "" ? null : pubkey,
|
||||
slotMax: slotMax === "" ? null : slotMax,
|
||||
slotMin: slotMin === "" ? null : slotMin,
|
||||
};
|
||||
}
|
||||
|
||||
function emptyAccountPage(draw: number): DataTablesPageResponse<StoreAccountRowDto> {
|
||||
return { data: [], draw, recordsFiltered: 0, recordsTotal: 0 };
|
||||
}
|
||||
|
||||
function setAccountQueryError(message: string | null): void {
|
||||
const element = document.querySelector<HTMLElement>("#accountsQueryError");
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.hidden = message === null;
|
||||
element.textContent = message ?? "";
|
||||
}
|
||||
|
||||
async function queryAccounts(page: DataTablesPageRequest, callback: (response: DataTablesPageResponse<StoreAccountRowDto>) => void): Promise<void> {
|
||||
const draw = Number.isSafeInteger(page.draw) && page.draw >= 0 ? page.draw : 0;
|
||||
const request = currentAccountQuery(page);
|
||||
if (!request) {
|
||||
setAccountQueryError("La pagination demandée n'est pas admise par Store Desk.");
|
||||
callback(emptyAccountPage(draw));
|
||||
frontendError("main", "Store Desk account DataTables query rejected before IPC");
|
||||
return;
|
||||
}
|
||||
setAccountQueryError(null);
|
||||
frontendDebug("main", "Store Desk account DataTables server-side query started", {
|
||||
pageLength: request.limit,
|
||||
pubkeyFilterActive: request.pubkey !== null,
|
||||
slotFilterActive: request.slotMin !== null || request.slotMax !== null,
|
||||
});
|
||||
try {
|
||||
const response = await invokeKsp<StoreAccountQueryResponseDto>("main", "store_query_accounts", { request });
|
||||
const recordsTotal = decimalCountToSafeNumber(response.recordsTotalDecimal);
|
||||
const recordsFiltered = decimalCountToSafeNumber(response.recordsFilteredDecimal);
|
||||
if (recordsTotal === null || recordsFiltered === null) {
|
||||
setAccountQueryError("Le volume Store dépasse le domaine entier sûr de l'interface.");
|
||||
callback(emptyAccountPage(draw));
|
||||
frontendError("main", "Store Desk account DataTables exact count exceeds frontend safe integer domain");
|
||||
return;
|
||||
}
|
||||
callback({ data: response.rows, draw, recordsFiltered, recordsTotal });
|
||||
frontendDebug("main", "Store Desk account DataTables server-side query completed", { rowCount: response.rows.length });
|
||||
} catch {
|
||||
setAccountQueryError("La requête RAW Accounts a échoué.");
|
||||
callback(emptyAccountPage(draw));
|
||||
frontendError("main", "Store Desk account DataTables server-side query failed");
|
||||
}
|
||||
}
|
||||
|
||||
function initializeAccountTable(): void {
|
||||
accountTable = new DataTable("#rawAccountsTable", {
|
||||
ajax: (data, callback) => {
|
||||
const request = data as unknown as DataTablesPageRequest;
|
||||
void queryAccounts(request, response => callback(response));
|
||||
},
|
||||
autoWidth: false,
|
||||
data: [],
|
||||
columns: [
|
||||
{ data: "pubkey", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-pubkey") : String(data)) },
|
||||
{ className: "font-monospace", data: "slotDecimal" },
|
||||
{ data: "stateHash", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-state-hash") : String(data)) },
|
||||
{ data: "owner", render: (data, type) => (type === "display" ? renderCopyableLongText(String(data), "account-owner") : String(data)) },
|
||||
{ className: "font-monospace", data: "lamportsDecimal" },
|
||||
{ data: "executable", render: data => (Boolean(data) ? '<span class="badge text-bg-info">oui</span>' : '<span class="badge text-bg-secondary">non</span>') },
|
||||
{ className: "font-monospace", data: "rentEpochDecimal" },
|
||||
{ className: "font-monospace", data: "dataLengthDecimal" },
|
||||
{
|
||||
data: null,
|
||||
defaultContent: "",
|
||||
render: (_data, _type, row) => {
|
||||
const account = row as StoreAccountRowDto;
|
||||
return `<button class="btn btn-sm btn-outline-primary" type="button" data-account-detail data-account-pubkey="${escapeHtml(account.pubkey)}" data-account-slot="${escapeHtml(account.slotDecimal)}" data-account-state-hash="${escapeHtml(account.stateHash)}"><i class="fa-solid fa-magnifying-glass me-1" aria-hidden="true"></i>Détail</button>`;
|
||||
},
|
||||
},
|
||||
],
|
||||
info: true,
|
||||
lengthMenu: [25, 50, 100],
|
||||
ordering: false,
|
||||
pageLength: 25,
|
||||
paging: true,
|
||||
processing: true,
|
||||
searching: false,
|
||||
serverSide: false,
|
||||
serverSide: true,
|
||||
scrollX: true,
|
||||
language: {
|
||||
emptyTable: "Aucune donnée RAW Account n'est chargée avant la tranche pre.008.",
|
||||
emptyTable: "Aucun état RAW Account ne correspond à la requête Store.",
|
||||
info: "_START_ à _END_ sur _TOTAL_ état(s) Account",
|
||||
infoEmpty: "0 état Account",
|
||||
lengthMenu: "Afficher _MENU_",
|
||||
loadingRecords: "Chargement Store…",
|
||||
processing: "Inspection Store…",
|
||||
zeroRecords: "Aucun état RAW Account correspondant.",
|
||||
},
|
||||
}) as unknown as StoreDataTable;
|
||||
frontendTrace("main", "Store Desk account DataTable skeleton initialized", { pagingOwner: "datatables", serverSide: false });
|
||||
accountTable.on("page", () => {
|
||||
frontendDebug("main", "Store Desk account DataTables page changed");
|
||||
});
|
||||
accountTable.on("length", () => {
|
||||
frontendDebug("main", "Store Desk account DataTables page length changed");
|
||||
});
|
||||
accountTable.on("draw", () => {
|
||||
frontendTrace("main", "Store Desk account DataTables redraw completed");
|
||||
});
|
||||
frontendTrace("main", "Store Desk account DataTable initialized", { pagingOwner: "datatables", serverSide: true });
|
||||
}
|
||||
|
||||
function renderShellStatus(status: ShellStatusDto): void {
|
||||
@@ -414,6 +524,83 @@ async function refreshDiagnostics(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function clearAccountDetail(): void {
|
||||
for (const elementId of [
|
||||
"accountDetailPubkey",
|
||||
"accountDetailSlot",
|
||||
"accountDetailStateHash",
|
||||
"accountDetailOwner",
|
||||
"accountDetailLamports",
|
||||
"accountDetailExecutable",
|
||||
"accountDetailRentEpoch",
|
||||
"accountDetailDataLength",
|
||||
"accountDetailDataPreview",
|
||||
]) {
|
||||
setText(elementId, "—");
|
||||
}
|
||||
setVisible("accountDetailError", false);
|
||||
setVisible("accountDetailPreviewTruncated", false);
|
||||
const dataPreviewCopy = document.querySelector<HTMLButtonElement>("#accountDetailDataPreviewCopy");
|
||||
if (dataPreviewCopy) {
|
||||
delete dataPreviewCopy.dataset.copyLongText;
|
||||
delete dataPreviewCopy.dataset.copyField;
|
||||
dataPreviewCopy.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAccountDetail(detail: StoreAccountDetailDto): void {
|
||||
setCopyableLongText("accountDetailPubkey", detail.pubkey, "account-detail-pubkey");
|
||||
setText("accountDetailSlot", detail.slotDecimal);
|
||||
setCopyableLongText("accountDetailStateHash", detail.stateHash, "account-detail-state-hash");
|
||||
setCopyableLongText("accountDetailOwner", detail.owner, "account-detail-owner");
|
||||
setText("accountDetailLamports", detail.lamportsDecimal);
|
||||
setText("accountDetailExecutable", detail.executable ? "oui" : "non");
|
||||
setText("accountDetailRentEpoch", detail.rentEpochDecimal);
|
||||
setText("accountDetailDataLength", detail.dataLengthDecimal);
|
||||
const dataPreview = detail.dataPreviewHex;
|
||||
setText("accountDetailDataPreview", dataPreview === "" ? "Données Account vides." : dataPreview);
|
||||
const dataPreviewCopy = document.querySelector<HTMLButtonElement>("#accountDetailDataPreviewCopy");
|
||||
if (dataPreviewCopy && dataPreview !== "") {
|
||||
dataPreviewCopy.dataset.copyLongText = dataPreview;
|
||||
dataPreviewCopy.dataset.copyField = "account-data-preview";
|
||||
dataPreviewCopy.hidden = false;
|
||||
} else if (dataPreviewCopy) {
|
||||
delete dataPreviewCopy.dataset.copyLongText;
|
||||
delete dataPreviewCopy.dataset.copyField;
|
||||
dataPreviewCopy.hidden = true;
|
||||
}
|
||||
setVisible("accountDetailPreviewTruncated", detail.dataPreviewTruncated);
|
||||
}
|
||||
|
||||
async function openAccountDetail(pubkey: string, slot: string, stateHash: string): Promise<void> {
|
||||
const modalElement = document.querySelector<HTMLElement>("#accountDetailModal");
|
||||
if (!modalElement) {
|
||||
return;
|
||||
}
|
||||
clearAccountDetail();
|
||||
setCopyableLongText("accountDetailPubkey", pubkey, "account-detail-pubkey");
|
||||
setText("accountDetailSlot", slot);
|
||||
setCopyableLongText("accountDetailStateHash", stateHash, "account-detail-state-hash");
|
||||
frontendDebug("main", "Store Desk account detail load requested");
|
||||
const modal = Modal.getOrCreateInstance(modalElement);
|
||||
modal.show();
|
||||
const request: StoreAccountDetailRequestDto = { pubkey, slot, stateHash };
|
||||
try {
|
||||
const detail = await invokeKsp<StoreAccountDetailDto>("main", "store_get_account_detail", { request });
|
||||
renderAccountDetail(detail);
|
||||
frontendDebug("main", "Store Desk account detail opened", { dataPreviewTruncated: detail.dataPreviewTruncated });
|
||||
} catch {
|
||||
setText("accountDetailError", "Le détail de cet état Account n'a pas pu être chargé.");
|
||||
setVisible("accountDetailError", true);
|
||||
frontendError("main", "Store Desk account detail load failed");
|
||||
}
|
||||
}
|
||||
|
||||
function redrawAccounts(resetPaging: boolean, reason: string): void {
|
||||
frontendDebug("main", "Store Desk account DataTables redraw requested", { reason });
|
||||
accountTable?.draw(resetPaging);
|
||||
}
|
||||
|
||||
function clearTransactionDetail(): void {
|
||||
for (const elementId of [
|
||||
"transactionDetailSignature",
|
||||
@@ -507,6 +694,11 @@ function installInteractions(): void {
|
||||
frontendDebug("main", "Store Desk RAW Transactions refresh button clicked");
|
||||
redrawTransactions(false, "refresh");
|
||||
});
|
||||
const refreshAccounts = document.querySelector<HTMLButtonElement>("#refreshAccounts");
|
||||
refreshAccounts?.addEventListener("click", () => {
|
||||
frontendDebug("main", "Store Desk RAW Accounts refresh button clicked");
|
||||
redrawAccounts(false, "refresh");
|
||||
});
|
||||
for (const elementId of ["transactionSlotMin", "transactionSlotMax", "transactionDirection"]) {
|
||||
const control = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||
control?.addEventListener("change", () => {
|
||||
@@ -514,6 +706,13 @@ function installInteractions(): void {
|
||||
redrawTransactions(true, "store-filter");
|
||||
});
|
||||
}
|
||||
for (const elementId of ["accountPubkey", "accountSlotMin", "accountSlotMax", "accountDirection"]) {
|
||||
const control = document.querySelector<HTMLElement>(`#${elementId}`);
|
||||
control?.addEventListener("change", () => {
|
||||
frontendDebug("main", "Store Desk account Store filter changed", { controlId: elementId });
|
||||
redrawAccounts(true, "store-filter");
|
||||
});
|
||||
}
|
||||
const refreshButton = document.querySelector<HTMLButtonElement>("#refreshDiagnostics");
|
||||
refreshButton?.addEventListener("click", () => {
|
||||
frontendDebug("main", "Store Desk diagnostics refresh button clicked");
|
||||
@@ -533,6 +732,16 @@ function installInteractions(): void {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const accountDetailButton = eventTarget.closest<HTMLElement>("[data-account-detail]");
|
||||
if (accountDetailButton) {
|
||||
const pubkey = accountDetailButton.dataset.accountPubkey;
|
||||
const slot = accountDetailButton.dataset.accountSlot;
|
||||
const stateHash = accountDetailButton.dataset.accountStateHash;
|
||||
if (pubkey && slot && stateHash) {
|
||||
void openAccountDetail(pubkey, slot, stateHash);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const detailButton = eventTarget.closest<HTMLElement>("[data-transaction-detail]");
|
||||
if (detailButton) {
|
||||
const signature = detailButton.dataset.transactionDetail;
|
||||
@@ -545,7 +754,7 @@ function installInteractions(): void {
|
||||
if (!control) {
|
||||
return;
|
||||
}
|
||||
if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions")) {
|
||||
if (control instanceof HTMLButtonElement && (control.dataset.view || control.id === "refreshDiagnostics" || control.id === "refreshOverview" || control.id === "refreshTransactions" || control.id === "refreshAccounts")) {
|
||||
return;
|
||||
}
|
||||
const controlId = control.id || control.getAttribute("data-bs-target") || control.getAttribute("aria-controls") || "anonymous";
|
||||
@@ -570,6 +779,11 @@ function installInteractions(): void {
|
||||
controlType: target instanceof HTMLSelectElement ? "select" : target.type || "text",
|
||||
});
|
||||
});
|
||||
const accountDetailModal = document.querySelector<HTMLElement>("#accountDetailModal");
|
||||
accountDetailModal?.addEventListener("hidden.bs.modal", () => {
|
||||
clearAccountDetail();
|
||||
frontendDebug("main", "Store Desk account detail closed");
|
||||
});
|
||||
const detailModal = document.querySelector<HTMLElement>("#transactionDetailModal");
|
||||
detailModal?.addEventListener("hidden.bs.modal", () => {
|
||||
clearTransactionDetail();
|
||||
@@ -581,7 +795,7 @@ function installInteractions(): void {
|
||||
async function initializeMain(): Promise<void> {
|
||||
frontendInfo("main", "Store Desk main frontend loaded");
|
||||
initializeTransactionTable();
|
||||
initializeAccountSkeleton();
|
||||
initializeAccountTable();
|
||||
installInteractions();
|
||||
activateView("overview");
|
||||
await refreshDiagnostics();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/app_state.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Shared backend state owned by the Store Desk Tauri application.
|
||||
|
||||
@@ -105,7 +105,7 @@ impl crate::AppState {
|
||||
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
config_document_count: document_count,
|
||||
fallback_logging_active: runtime.fallback_active,
|
||||
shell_phase: "pre.007-raw-transactions".to_owned(),
|
||||
shell_phase: "pre.008-raw-accounts".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
});
|
||||
}
|
||||
@@ -115,6 +115,16 @@ impl crate::AppState {
|
||||
return self.store_startup.status().await;
|
||||
}
|
||||
|
||||
/// Executes one bounded RAW account-state inspection query through the retained Store facade.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
return self.store_startup.query_accounts(request).await;
|
||||
}
|
||||
|
||||
/// Loads one explicit RAW account-state detail through the retained Store facade.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
return self.store_startup.account_detail(request).await;
|
||||
}
|
||||
|
||||
/// Executes one bounded RAW transaction inspection query through the retained Store facade.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
|
||||
105
crates/ksp-app-store-desk/src/dto_account.rs
Normal file
105
crates/ksp-app-store-desk/src/dto_account.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-app-store-desk/src/dto_account.rs
|
||||
// version: 1
|
||||
|
||||
//! Application-owned DTOs for RAW account-state inspection and detail commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Bounded Store-owned query request projected from one DataTables page request.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountQueryRequestDto.ts")]
|
||||
pub(crate) struct StoreAccountQueryRequestDto {
|
||||
/// Stable Store sort direction code (`ascending` or `descending`).
|
||||
pub(crate) direction: String,
|
||||
/// Whitelisted page size (`25`, `50`, or `100`).
|
||||
pub(crate) limit: u32,
|
||||
/// Absolute zero-based inspection offset encoded as exact decimal text.
|
||||
pub(crate) offset: String,
|
||||
/// Optional exact account public key in canonical base58 text.
|
||||
pub(crate) pubkey: std::option::Option<String>,
|
||||
/// Optional inclusive maximum slot encoded as decimal text.
|
||||
pub(crate) slot_max: std::option::Option<String>,
|
||||
/// Optional inclusive minimum slot encoded as decimal text.
|
||||
pub(crate) slot_min: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Data-free RAW account-state row returned to the server-side DataTables adapter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountRowDto.ts")]
|
||||
pub(crate) struct StoreAccountRowDto {
|
||||
/// Exact complete account-data length encoded as decimal text.
|
||||
pub(crate) data_length_decimal: String,
|
||||
/// Whether the account is executable.
|
||||
pub(crate) executable: bool,
|
||||
/// Exact lamport balance encoded as decimal text.
|
||||
pub(crate) lamports_decimal: String,
|
||||
/// Account owner program public key in canonical base58 text.
|
||||
pub(crate) owner: String,
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Exact rent epoch encoded as decimal text.
|
||||
pub(crate) rent_epoch_decimal: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
/// Server-side account-state table result using exact decimal counts.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountQueryResponseDto.ts")]
|
||||
pub(crate) struct StoreAccountQueryResponseDto {
|
||||
/// Exact count after optional Store filters.
|
||||
pub(crate) records_filtered_decimal: String,
|
||||
/// Exact count in the mandatory Store network scope before optional filters.
|
||||
pub(crate) records_total_decimal: String,
|
||||
/// Data-free inspection rows for the requested random-access page.
|
||||
pub(crate) rows: std::vec::Vec<crate::StoreAccountRowDto>,
|
||||
}
|
||||
|
||||
/// App-owned identity request for one explicit RAW account-state detail load.
|
||||
#[derive(Debug, serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountDetailRequestDto.ts")]
|
||||
pub(crate) struct StoreAccountDetailRequestDto {
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
/// Bounded detail projection for one complete RAW account state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_account/StoreAccountDetailDto.ts")]
|
||||
pub(crate) struct StoreAccountDetailDto {
|
||||
/// Exact complete account-data length encoded as decimal text.
|
||||
pub(crate) data_length_decimal: String,
|
||||
/// Bounded lower-case hexadecimal preview of complete canonical account data.
|
||||
pub(crate) data_preview_hex: String,
|
||||
/// Whether complete account data is larger than the returned preview.
|
||||
pub(crate) data_preview_truncated: bool,
|
||||
/// Whether the account is executable.
|
||||
pub(crate) executable: bool,
|
||||
/// Exact lamport balance encoded as decimal text.
|
||||
pub(crate) lamports_decimal: String,
|
||||
/// Account owner program public key in canonical base58 text.
|
||||
pub(crate) owner: String,
|
||||
/// Account public key in canonical base58 text.
|
||||
pub(crate) pubkey: String,
|
||||
/// Exact rent epoch encoded as decimal text.
|
||||
pub(crate) rent_epoch_decimal: String,
|
||||
/// Solana slot encoded as exact decimal text.
|
||||
pub(crate) slot_decimal: String,
|
||||
/// Canonical account-state hash encoded as lower-case hexadecimal text.
|
||||
pub(crate) state_hash: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/dto_account.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/errors.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Application-local error codes for Store Desk desktop runtime surfaces.
|
||||
|
||||
@@ -19,7 +19,9 @@ pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode =
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_setting_invalid");
|
||||
/// Store Desk received an invalid transaction inspection/detail request.
|
||||
/// Store Desk could not resolve the requested RAW account-state identity to a coherent Store entity.
|
||||
pub(crate) const ERROR_CODE_STORE_ACCOUNT_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_account_not_found");
|
||||
/// Store Desk received an invalid RAW inspection/detail request.
|
||||
pub(crate) const ERROR_CODE_STORE_QUERY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_query_invalid");
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
pub(crate) const ERROR_CODE_STORE_RUNTIME_UNAVAILABLE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "store_runtime_unavailable");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Tauri desktop scaffold for backend-neutral KSP Store inspection.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
mod app_state;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_account;
|
||||
mod dto_common;
|
||||
mod dto_transaction;
|
||||
mod errors;
|
||||
@@ -60,6 +61,16 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Bounded detail projection for one complete RAW account state.
|
||||
pub(crate) use self::dto_account::StoreAccountDetailDto;
|
||||
/// App-owned identity request for one explicit RAW account-state detail load.
|
||||
pub(crate) use self::dto_account::StoreAccountDetailRequestDto;
|
||||
/// Bounded Store-owned account query request projected from one DataTables page request.
|
||||
pub(crate) use self::dto_account::StoreAccountQueryRequestDto;
|
||||
/// Server-side account-state table result using exact decimal counts.
|
||||
pub(crate) use self::dto_account::StoreAccountQueryResponseDto;
|
||||
/// Data-free RAW account-state row returned to the server-side DataTables adapter.
|
||||
pub(crate) use self::dto_account::StoreAccountRowDto;
|
||||
/// Safe command error projection exposed to Tauri commands.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe shell/Config/Logging snapshot exposed to the shell.
|
||||
@@ -92,6 +103,8 @@ pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Store Desk could not resolve the requested RAW account-state identity to a coherent Store entity.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_ACCOUNT_NOT_FOUND;
|
||||
/// Store Desk received an invalid transaction inspection/detail request.
|
||||
pub(crate) use self::errors::ERROR_CODE_STORE_QUERY_INVALID;
|
||||
/// Store Desk cannot serve a Store command because the runtime is unavailable or already closed.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/store_runtime.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Composite-selected Store readiness, status and bounded shutdown lifecycle owned by Store Desk.
|
||||
|
||||
@@ -35,6 +35,24 @@ impl crate::StoreStartup {
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded server-side RAW account-state inspection query.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.query_accounts(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads one explicit bounded RAW account-state detail projection.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
let runtime = self.runtime.as_ref();
|
||||
return match runtime {
|
||||
std::option::Option::Some(value) => value.account_detail(request).await,
|
||||
std::option::Option::None => std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one bounded server-side RAW transaction inspection query.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
@@ -116,6 +134,56 @@ impl crate::StoreRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one validated random-access account-state inspection query while retaining the Store read guard.
|
||||
pub(crate) async fn query_accounts(&self, request: crate::StoreAccountQueryRequestDto) -> ksp_core_lib::Result<crate::StoreAccountQueryResponseDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let query = account_query_from_request(store, &request);
|
||||
let query = match query {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let page = ksp_store_lib::RawAccountStateInspectionRead::inspect_raw_account_states(store, &query).await;
|
||||
let page = match page {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let records_total_decimal = page.total_items().to_string();
|
||||
let records_filtered_decimal = page.filtered_items().to_string();
|
||||
let items = page.into_items();
|
||||
let mut rows = std::vec::Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
rows.push(account_row_dto(&item));
|
||||
}
|
||||
return std::result::Result::Ok(crate::StoreAccountQueryResponseDto { records_filtered_decimal, records_total_decimal, rows });
|
||||
}
|
||||
|
||||
/// Loads one account-state detail by application-owned row identity without exposing Store network selection to the frontend.
|
||||
pub(crate) async fn account_detail(&self, request: crate::StoreAccountDetailRequestDto) -> ksp_core_lib::Result<crate::StoreAccountDetailDto> {
|
||||
let locked = self.store.read().await;
|
||||
let store = locked.as_ref();
|
||||
let store = match store {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(store_runtime_unavailable()),
|
||||
};
|
||||
let reference = account_reference_from_request(store, &request);
|
||||
let reference = match reference {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state = ksp_store_lib::RawAccountStateRead::get_raw_account_state(store, &reference).await;
|
||||
let state = match state {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => return std::result::Result::Err(store_account_not_found()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(account_detail_dto(&state));
|
||||
}
|
||||
|
||||
/// Executes one validated random-access transaction inspection query while retaining the Store read guard.
|
||||
pub(crate) async fn query_transactions(
|
||||
&self,
|
||||
@@ -210,8 +278,111 @@ impl crate::StoreRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;
|
||||
const TRANSACTION_DETAIL_PREVIEW_BYTES: usize = 512;
|
||||
|
||||
fn account_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreAccountQueryRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateInspectionQuery> {
|
||||
let limit = match request.limit {
|
||||
25 | 50 | 100 => ksp_store_lib::RawPageLimit::new(u64::from(request.limit)),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let limit = match limit {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let offset = request.offset.parse::<u64>();
|
||||
let offset = match offset {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let pubkey = parse_optional_pubkey(request.pubkey.as_deref());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_min = parse_optional_decimal_u64(request.slot_min.as_deref());
|
||||
let slot_min = match slot_min {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot_max = parse_optional_decimal_u64(request.slot_max.as_deref());
|
||||
let slot_max = match slot_max {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slots = ksp_store_lib::RawSlotRange::new(slot_min, slot_max);
|
||||
let slots = match slots {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let direction = match request.direction.as_str() {
|
||||
"ascending" => ksp_store_lib::RawSortDirection::Ascending,
|
||||
"descending" => ksp_store_lib::RawSortDirection::Descending,
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
let page = ksp_store_lib::RawInspectionPageRequest::new(offset, limit);
|
||||
return std::result::Result::Ok(ksp_store_lib::RawAccountStateInspectionQuery::new(network, pubkey, slots, direction, page));
|
||||
}
|
||||
|
||||
fn account_reference_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreAccountDetailRequestDto,
|
||||
) -> ksp_core_lib::Result<ksp_store_lib::RawAccountStateReference> {
|
||||
let pubkey = request.pubkey.parse::<ksp_store_lib::Pubkey>();
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let slot = request.slot.parse::<u64>();
|
||||
let slot = match slot {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
let state_hash = decode_hex_32(request.state_hash.as_str());
|
||||
let state_hash = match state_hash {
|
||||
std::result::Result::Ok(value) => ksp_store_lib::RawContentHash::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let network = store.runtime_snapshot().network().clone();
|
||||
return std::result::Result::Ok(ksp_store_lib::RawAccountStateReference::new(network, pubkey, slot, state_hash));
|
||||
}
|
||||
|
||||
fn account_detail_dto(state: &ksp_store_lib::RawAccountState) -> crate::StoreAccountDetailDto {
|
||||
let data = state.data();
|
||||
let preview_len = std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES);
|
||||
let state_hash = state.reference().state_hash();
|
||||
return crate::StoreAccountDetailDto {
|
||||
data_length_decimal: state.data_len().to_string(),
|
||||
data_preview_hex: encode_hex(&data[..preview_len]),
|
||||
data_preview_truncated: data.len() > preview_len,
|
||||
executable: state.executable(),
|
||||
lamports_decimal: state.lamports().to_string(),
|
||||
owner: state.owner().to_string(),
|
||||
pubkey: state.reference().pubkey().to_string(),
|
||||
rent_epoch_decimal: state.rent_epoch().to_string(),
|
||||
slot_decimal: state.reference().slot().to_string(),
|
||||
state_hash: encode_hex(state_hash.as_bytes()),
|
||||
};
|
||||
}
|
||||
|
||||
fn account_row_dto(summary: &ksp_store_lib::RawAccountStateSummary) -> crate::StoreAccountRowDto {
|
||||
let state_hash = summary.reference().state_hash();
|
||||
return crate::StoreAccountRowDto {
|
||||
data_length_decimal: summary.data_length_bytes().to_string(),
|
||||
executable: summary.executable(),
|
||||
lamports_decimal: summary.lamports().to_string(),
|
||||
owner: summary.owner().to_string(),
|
||||
pubkey: summary.reference().pubkey().to_string(),
|
||||
rent_epoch_decimal: summary.rent_epoch().to_string(),
|
||||
slot_decimal: summary.reference().slot().to_string(),
|
||||
state_hash: encode_hex(state_hash.as_bytes()),
|
||||
};
|
||||
}
|
||||
|
||||
fn transaction_query_from_request(
|
||||
store: &ksp_store_lib::Store,
|
||||
request: &crate::StoreTransactionQueryRequestDto,
|
||||
@@ -350,6 +521,16 @@ fn retention_state_code(state: ksp_store_lib::RawRetentionState) -> &'static str
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_pubkey(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<ksp_store_lib::Pubkey>> {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => match text.parse::<ksp_store_lib::Pubkey>() {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(std::option::Option::Some(parsed)),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(store_query_invalid()),
|
||||
},
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_optional_decimal_u64(value: std::option::Option<&str>) -> ksp_core_lib::Result<std::option::Option<u64>> {
|
||||
return match value {
|
||||
std::option::Option::Some(text) => match text.parse::<u64>() {
|
||||
@@ -360,6 +541,24 @@ fn parse_optional_decimal_u64(value: std::option::Option<&str>) -> ksp_core_lib:
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_hex_32(value: &str) -> ksp_core_lib::Result<[u8; 32]> {
|
||||
if value.len() != 64 || !value.is_ascii() {
|
||||
return std::result::Result::Err(store_query_invalid());
|
||||
}
|
||||
let source = value.as_bytes();
|
||||
let mut decoded = [0_u8; 32];
|
||||
for index in 0..32 {
|
||||
let high = decode_hex_nibble(source[index * 2]);
|
||||
let low = decode_hex_nibble(source[index * 2 + 1]);
|
||||
let (high, low) = match (high, low) {
|
||||
(std::option::Option::Some(high), std::option::Option::Some(low)) => (high, low),
|
||||
_ => return std::result::Result::Err(store_query_invalid()),
|
||||
};
|
||||
decoded[index] = (high << 4) | low;
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn decode_hex_64(value: &str) -> ksp_core_lib::Result<[u8; 64]> {
|
||||
if value.len() != 128 || !value.is_ascii() {
|
||||
return std::result::Result::Err(store_query_invalid());
|
||||
@@ -398,8 +597,12 @@ fn encode_hex(bytes: &[u8]) -> String {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
fn store_account_not_found() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_ACCOUNT_NOT_FOUND, "Store Desk account state was not found");
|
||||
}
|
||||
|
||||
fn store_query_invalid() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_QUERY_INVALID, "Store Desk transaction query is invalid");
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STORE_QUERY_INVALID, "Store Desk RAW inspection/detail request is invalid");
|
||||
}
|
||||
|
||||
fn store_runtime_unavailable() -> ksp_core_lib::Error {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/src/tauri.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Tauri runtime assembly for the KSP Store desktop application.
|
||||
|
||||
@@ -79,7 +79,9 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
emit_frontend_log,
|
||||
get_shell_status,
|
||||
splash_frontend_ready,
|
||||
store_get_account_detail,
|
||||
store_get_transaction_detail,
|
||||
store_query_accounts,
|
||||
store_query_transactions,
|
||||
store_runtime_status,
|
||||
]);
|
||||
@@ -174,6 +176,30 @@ fn get_shell_status(state: tauri::State<'_, crate::AppState>) -> std::result::Re
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_accounts(
|
||||
request: crate::StoreAccountQueryRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreAccountQueryResponseDto, crate::CommandErrorDto> {
|
||||
let result = state.query_accounts(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("store_query_accounts", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_get_account_detail(
|
||||
request: crate::StoreAccountDetailRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::StoreAccountDetailDto, crate::CommandErrorDto> {
|
||||
let result = state.account_detail(request).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("store_get_account_detail", crate::TRACING_DOMAIN_STORE, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn store_query_transactions(
|
||||
request: crate::StoreTransactionQueryRequestDto,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Dependency-boundary tests for the Store Desk application.
|
||||
|
||||
@@ -104,3 +104,30 @@ fn pre_007_transaction_inspection_uses_store_facade_and_new_visible_items_use_cr
|
||||
assert!(!sources.contains(forbidden), "Store Desk transaction inspection crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_inspection_uses_store_facade_and_new_visible_items_use_crate_root_exports() {
|
||||
let lib = include_str!("../src/lib.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let app_state = include_str!("../src/app_state.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
for expected in [
|
||||
"pub(crate) use self::dto_account::StoreAccountDetailRequestDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountDetailDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountQueryRequestDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountQueryResponseDto;",
|
||||
"pub(crate) use self::dto_account::StoreAccountRowDto;",
|
||||
] {
|
||||
assert!(lib.contains(expected), "missing pre.008 crate-root DTO re-export: {expected}");
|
||||
}
|
||||
assert!(runtime.contains("ksp_store_lib::RawAccountStateInspectionRead::inspect_raw_account_states"));
|
||||
assert!(runtime.contains("ksp_store_lib::RawAccountStateRead::get_raw_account_state"));
|
||||
assert!(app_state.contains("request: crate::StoreAccountQueryRequestDto"));
|
||||
assert!(app_state.contains("request: crate::StoreAccountDetailRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreAccountQueryRequestDto"));
|
||||
assert!(tauri.contains("request: crate::StoreAccountDetailRequestDto"));
|
||||
let sources = [runtime, app_state, tauri].join("\n");
|
||||
for forbidden in ["ksp_store_api", "ksp_store_postgres", "tokio_postgres", "deadpool_postgres", "SELECT ", "OFFSET "] {
|
||||
assert!(!sources.contains(forbidden), "Store Desk account inspection crossed physical/backend-neutral boundary: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Desktop template and frontend contract tests for the Store Desk scaffold.
|
||||
|
||||
@@ -80,7 +80,7 @@ fn pre_002_binary_assets_reuse_the_ksp_desktop_template_byte_exact() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_shell_exposes_transaction_filters_detail_and_keeps_account_skeleton_unconnected() {
|
||||
fn pre_007_shell_exposes_transaction_filters_detail_and_planned_account_view() {
|
||||
let html = include_str!("../frontend/main.html");
|
||||
for view in ["overview", "transactions", "accounts", "diagnostics"] {
|
||||
assert!(html.contains(&format!("data-view=\"{view}\"")));
|
||||
@@ -95,11 +95,10 @@ fn pre_007_shell_exposes_transaction_filters_detail_and_keeps_account_skeleton_u
|
||||
for transaction_id in ["refreshTransactions", "transactionSlotMin", "transactionSlotMax", "transactionDirection", "transactionDetailModal"] {
|
||||
assert!(html.contains(&format!("id=\"{transaction_id}\"")), "missing pre.007 transaction control: {transaction_id}");
|
||||
}
|
||||
assert!(html.contains("jusqu'à pre.008"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_transaction_datatable_is_server_side_while_account_remains_unconnected_and_no_second_pager_exists() {
|
||||
fn pre_007_transaction_datatable_is_server_side_and_no_second_pager_exists() {
|
||||
let source = include_str!("../frontend/ts/main.ts");
|
||||
assert!(source.contains("datatables.net-bs5"));
|
||||
assert!(source.contains("paging: true"));
|
||||
@@ -108,8 +107,6 @@ fn pre_007_transaction_datatable_is_server_side_while_account_remains_unconnecte
|
||||
assert!(source.contains("store_query_transactions"));
|
||||
assert!(source.contains("serverSide: true"));
|
||||
assert!(source.contains("Store Desk transaction DataTable initialized"));
|
||||
assert!(source.contains("Store Desk account DataTable skeleton initialized"));
|
||||
assert!(source.contains("serverSide: false"));
|
||||
assert!(source.contains("searching: false"));
|
||||
assert!(source.contains("ordering: false"));
|
||||
let html = include_str!("../frontend/main.html");
|
||||
@@ -231,3 +228,47 @@ fn pre_007_fix_002_payload_preview_remains_bounded_and_is_explicitly_copyable()
|
||||
assert!(frontend.contains("const previousTitle = button.title"));
|
||||
assert!(frontend.contains("button.title = previousTitle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_ipc_maps_datatables_filters_counts_and_bounded_detail_without_cursor_contract() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let tauri = include_str!("../src/tauri.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
assert!(dto.contains("records_total_decimal"));
|
||||
assert!(dto.contains("records_filtered_decimal"));
|
||||
assert!(runtime.contains("RawAccountStateInspectionQuery::new"));
|
||||
assert!(runtime.contains("RawInspectionPageRequest::new(offset, limit)"));
|
||||
assert!(runtime.contains("RawAccountStateRead::get_raw_account_state"));
|
||||
assert!(runtime.contains("const ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512;"));
|
||||
assert!(tauri.contains("store_query_accounts"));
|
||||
assert!(tauri.contains("store_get_account_detail"));
|
||||
assert!(frontend.contains("store_query_accounts"));
|
||||
assert!(frontend.contains("store_get_account_detail"));
|
||||
assert!(frontend.contains("pubkey: pubkey === "" ? null : pubkey"));
|
||||
assert!(frontend.contains("recordsTotalDecimal"));
|
||||
assert!(frontend.contains("recordsFilteredDecimal"));
|
||||
assert!(frontend.contains("data-account-detail"));
|
||||
assert!(!dto.contains("RawPageCursor"));
|
||||
assert!(!frontend.contains("storePager"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_datatable_is_server_side_copyable_and_uses_datatables_as_only_pager() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let html = include_str!("../frontend/main.html");
|
||||
assert!(frontend.contains("Store Desk account DataTable initialized"));
|
||||
assert!(frontend.contains("Store Desk account DataTables server-side query started"));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-pubkey")"#));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-state-hash")"#));
|
||||
assert!(frontend.contains(r#"renderCopyableLongText(String(data), "account-owner")"#));
|
||||
assert!(frontend.contains(r#"dataPreviewCopy.dataset.copyField = "account-data-preview""#));
|
||||
for control in ["refreshAccounts", "accountPubkey", "accountSlotMin", "accountSlotMax", "accountDirection", "accountDetailModal"] {
|
||||
assert!(html.contains(&format!("id=\"{control}\"")), "missing pre.008 account control: {control}");
|
||||
}
|
||||
assert!(html.contains("Account data preview hex"));
|
||||
assert!(html.contains("Preview tronquée à 512 bytes"));
|
||||
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
|
||||
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
//! Security boundary tests for the Store Desk scaffold.
|
||||
|
||||
@@ -162,3 +162,71 @@ fn pre_007_fix_002_payload_preview_copy_never_logs_preview_material() {
|
||||
assert!(!frontend.contains(r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#));
|
||||
assert!(!frontend.contains(r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_rows_are_data_free_detail_is_bounded_and_filter_values_are_not_logged() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
let row_start = dto.find("pub(crate) struct StoreAccountRowDto");
|
||||
let response_start = dto.find("pub(crate) struct StoreAccountQueryResponseDto");
|
||||
assert!(row_start.is_some());
|
||||
assert!(response_start.is_some());
|
||||
if let (std::option::Option::Some(row_start), std::option::Option::Some(response_start)) = (row_start, response_start) {
|
||||
let row_source = &dto[row_start..response_start];
|
||||
assert!(!row_source.contains("data_preview_hex"));
|
||||
assert!(!row_source.contains("pub(crate) data:"));
|
||||
}
|
||||
assert!(dto.contains("data_preview_hex"));
|
||||
assert!(dto.contains("data_preview_truncated"));
|
||||
assert!(runtime.contains("ACCOUNT_DETAIL_PREVIEW_BYTES: usize = 512"));
|
||||
assert!(runtime.contains("std::cmp::min(data.len(), ACCOUNT_DETAIL_PREVIEW_BYTES)"));
|
||||
for required in [
|
||||
"account DataTables page changed",
|
||||
"account DataTables page length changed",
|
||||
"account DataTables redraw completed",
|
||||
"account detail opened",
|
||||
"account detail closed",
|
||||
] {
|
||||
assert!(frontend.contains(required), "missing account tracing event: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { pubkey"#,
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMin"#,
|
||||
r#"frontendDebug("main", "Store Desk account Store filter changed", { slotMax"#,
|
||||
r#"frontendDebug("main", "Store Desk account detail opened", { pubkey"#,
|
||||
"dataPreviewHex:",
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "account business/filter value leaked into frontend tracing: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_request_rejects_datatables_search_order_draw_network_and_physical_material() {
|
||||
let dto = include_str!("../src/dto_account.rs");
|
||||
let runtime = include_str!("../src/store_runtime.rs");
|
||||
for forbidden in ["draw:", "columns:", "search:", "order:", "cursor:", "network:", "connection_uri", "postgresql://", "endpoint"] {
|
||||
assert!(!dto.contains(forbidden), "forbidden DataTables/physical account request material present: {forbidden}");
|
||||
}
|
||||
assert!(runtime.contains("25 | 50 | 100"));
|
||||
assert!(runtime.contains("request.offset.parse::<u64>()"));
|
||||
assert!(runtime.contains("parse_optional_pubkey(request.pubkey.as_deref())"));
|
||||
assert!(runtime.contains("RawSlotRange::new"));
|
||||
assert!(runtime.contains("request.pubkey.parse::<ksp_store_lib::Pubkey>()"));
|
||||
assert!(runtime.contains("decode_hex_32(request.state_hash.as_str())"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_copy_tracing_never_logs_identity_or_data_material() {
|
||||
let frontend = include_str!("../frontend/ts/main.ts");
|
||||
for required in ["account-pubkey", "account-state-hash", "account-owner", "account-data-preview"] {
|
||||
assert!(frontend.contains(required), "missing account copy field id: {required}");
|
||||
}
|
||||
for forbidden in [
|
||||
r#"frontendDebug("main", "Store Desk long text copy requested", { fieldId, value })"#,
|
||||
r#"frontendDebug("main", "Store Desk long text copy completed", { fieldId, value })"#,
|
||||
r#"frontendError("main", "Store Desk long text copy failed", { fieldId, value })"#,
|
||||
] {
|
||||
assert!(!frontend.contains(forbidden), "account copy tracing leaked copied value material: {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
56
crates/ksp-app-store-desk/unit_tests/dto_account.rs
Normal file
56
crates/ksp-app-store-desk/unit_tests/dto_account.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-app-store-desk/unit_tests/dto_account.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_rows_are_data_free_and_detail_preview_is_explicitly_bounded() {
|
||||
let row = crate::StoreAccountRowDto {
|
||||
data_length_decimal: "2048".to_owned(),
|
||||
executable: false,
|
||||
lamports_decimal: "12345678901234567890".to_owned(),
|
||||
owner: "11111111111111111111111111111111".to_owned(),
|
||||
pubkey: "SysvarRent111111111111111111111111111111111".to_owned(),
|
||||
rent_epoch_decimal: "18446744073709551615".to_owned(),
|
||||
slot_decimal: "987654321".to_owned(),
|
||||
state_hash: "11".repeat(32),
|
||||
};
|
||||
let serialized = serde_json::to_value(&row);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(serialized.get("dataPreviewHex").is_none());
|
||||
assert!(serialized.get("data").is_none());
|
||||
assert_eq!(serialized["dataLengthDecimal"], "2048");
|
||||
let detail = crate::StoreAccountDetailDto {
|
||||
data_length_decimal: "8192".to_owned(),
|
||||
data_preview_hex: "abcd".to_owned(),
|
||||
data_preview_truncated: true,
|
||||
executable: true,
|
||||
lamports_decimal: "42".to_owned(),
|
||||
owner: "11111111111111111111111111111111".to_owned(),
|
||||
pubkey: "SysvarRent111111111111111111111111111111111".to_owned(),
|
||||
rent_epoch_decimal: "7".to_owned(),
|
||||
slot_decimal: "99".to_owned(),
|
||||
state_hash: "22".repeat(32),
|
||||
};
|
||||
let serialized = serde_json::to_value(&detail);
|
||||
assert!(serialized.is_ok());
|
||||
let serialized = match serialized {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(serialized["dataPreviewHex"], "abcd");
|
||||
assert_eq!(serialized["dataPreviewTruncated"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_account_query_request_contains_only_random_access_and_explicit_store_filters() {
|
||||
let source = include_str!("../src/dto_account.rs");
|
||||
for required in ["direction", "limit", "offset", "pubkey", "slot_max", "slot_min"] {
|
||||
assert!(source.contains(required), "missing Store account query field: {required}");
|
||||
}
|
||||
for forbidden in ["draw", "columns", "search", "order", "cursor", "network", "connection_uri", "endpoint"] {
|
||||
assert!(!source.contains(&format!("pub(crate) {forbidden}")), "DataTables/physical field leaked into Store account query DTO: {forbidden}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user