v0.3.8-pre.008
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 461
|
||||
# version: 462
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.8-pre.7.fix.3"
|
||||
version = "0.3.8-pre.8"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
126
deltas/0.3.8/pre.008.md
Normal file
126
deltas/0.3.8/pre.008.md
Normal file
@@ -0,0 +1,126 @@
|
||||
<!-- file: deltas/0.3.8/pre.008.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.8-pre.008` — RAW Accounts DataTables server-side + détail
|
||||
|
||||
## Base requise
|
||||
|
||||
Base directe attendue : `0.3.8-pre.007-fix.003` (`workspace.package.version = 0.3.8-pre.7.fix.3`).
|
||||
|
||||
## Objet
|
||||
|
||||
Brancher la vue `RAW Accounts` de `ksp-app-store-desk` sur les capabilities backend-neutrales `RawAccountStateInspectionRead` et `RawAccountStateRead`, sans modifier Store API, Store façade, PostgreSQL ou les queries keyset machine.
|
||||
|
||||
## Backend applicatif
|
||||
|
||||
Store Desk ajoute cinq DTO app-owned et deux commands Tauri :
|
||||
|
||||
```text
|
||||
store_query_accounts
|
||||
store_get_account_detail
|
||||
```
|
||||
|
||||
`store_query_accounts` reçoit uniquement :
|
||||
|
||||
```text
|
||||
offset decimal
|
||||
limit = 25 | 50 | 100
|
||||
pubkey exacte optionnelle
|
||||
slot_min optionnel
|
||||
slot_max optionnel
|
||||
direction = ascending | descending
|
||||
```
|
||||
|
||||
Le network n'est jamais fourni par le frontend : il est dérivé du `ksp-store-lib::Store` déjà ouvert. Le runtime construit `RawInspectionPageRequest` + `RawAccountStateInspectionQuery` puis appelle `RawAccountStateInspectionRead`.
|
||||
|
||||
La réponse app-owned contient :
|
||||
|
||||
```text
|
||||
rows data-free
|
||||
records_total_decimal
|
||||
records_filtered_decimal
|
||||
```
|
||||
|
||||
Tous les `u64` susceptibles de dépasser le domaine JS-safe restent encodés en texte décimal.
|
||||
|
||||
## Table Accounts
|
||||
|
||||
La table passe à :
|
||||
|
||||
```text
|
||||
serverSide: true
|
||||
searching: false
|
||||
ordering: false
|
||||
pageLength: 25
|
||||
lengthMenu: [25, 50, 100]
|
||||
ajax: fonction custom -> invoke Tauri
|
||||
```
|
||||
|
||||
Le mapping visible est :
|
||||
|
||||
```text
|
||||
DataTables start -> offset
|
||||
DataTables length -> limit
|
||||
Store total -> recordsTotal
|
||||
Store filtered -> recordsFiltered
|
||||
Store summaries -> data
|
||||
DataTables draw -> frontend uniquement
|
||||
```
|
||||
|
||||
Les filtres Store explicites sont `pubkey` exacte, `slot min`, `slot max` et `direction`. Tout changement de filtre redessine depuis la première page. Aucun pager Store ou cursor n'est rendu.
|
||||
|
||||
## Rows et détail
|
||||
|
||||
Les rows exposent uniquement pubkey, slot, state hash, owner, lamports, executable, rent epoch et data length. Aucun byte Account n'est présent dans la table.
|
||||
|
||||
Le bouton `Détail` recharge l'identité exacte `(pubkey, slot, state_hash)` via `RawAccountStateRead`. Le modal expose :
|
||||
|
||||
- pubkey, slot, state hash, owner, lamports, executable et rent epoch ;
|
||||
- data length exacte ;
|
||||
- preview hex des 512 premiers bytes maximum ;
|
||||
- flag explicite de troncature ;
|
||||
- bouton de copie du preview affiché uniquement, sans charger le reste des bytes.
|
||||
|
||||
Pubkey, owner et state hash réutilisent le helper générique de troncature centrale + tooltip valeur complète + copie stabilisé par les fixes `pre.007`.
|
||||
|
||||
## Sécurité / instrumentation
|
||||
|
||||
- rejet Rust de `length=-1` et de toute taille autre que 25/50/100 ;
|
||||
- offset/slots/pubkey/state hash parsés sans écho de valeur dans les erreurs ;
|
||||
- counts décimaux convertis côté TypeScript seulement après contrôle `Number.MAX_SAFE_INTEGER` ;
|
||||
- aucune search/order DataTables arbitraire dans le contrat IPC ;
|
||||
- aucune URI, credential, SQL, handle PostgreSQL ou byte Account dans les logs ;
|
||||
- tracing query/page/page-length/redraw/refresh/détail/copie sans valeurs de filtres ni identité ;
|
||||
- nouveaux items `pub(crate)` réexportés par `lib.rs` et consommés via `crate::Item` conformément aux règles KSP.
|
||||
|
||||
## Hors scope
|
||||
|
||||
Aucune observation Store n'est ajoutée à l'UI ; cette extension reste réservée à `pre.009`/`pre.010`. Aucun changement de Store API, Store façade, PostgreSQL, migration, index, capability ou Config composite.
|
||||
|
||||
## Version
|
||||
|
||||
```text
|
||||
delivery = 0.3.8-pre.008
|
||||
workspace.package.version = 0.3.8-pre.8
|
||||
commit = v0.3.8-pre.008
|
||||
tag = aucun
|
||||
```
|
||||
|
||||
## Gate requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-store-desk
|
||||
cargo test -p ksp-store-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
```
|
||||
|
||||
Smoke runtime recommandé :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-store-desk && cargo tauri dev)
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Plan v0.3.8 — Store Desk V1 RAW
|
||||
|
||||
@@ -592,9 +592,19 @@ Implémentation retenue : `store_query_transactions` reçoit uniquement `offset`
|
||||
|
||||
Les signatures et content hashes déjà visibles dans la table Transactions et son modal sont affichés sous une forme tronquée au centre, avec la valeur complète disponible au survol via tooltip natif et un bouton de copie explicite. Le mécanisme reste générique et doit être réutilisé par `pre.008` pour les pubkeys, owners, state hashes et autres identifiants longs. Les actions de copie sont tracées uniquement par identifiant logique de champ ; la valeur copiée n'est jamais journalisée. Aucun plugin clipboard Tauri ni capability supplémentaire n'est ajouté : le frontend utilise l'API Clipboard du WebView avec fallback local `document.execCommand("copy")`.
|
||||
|
||||
#### pre.007-fix.002 — copie explicite du preview payload
|
||||
|
||||
Le preview transaction reste borné à 512 bytes côté Rust mais devient copiable explicitement depuis le modal. Le bouton copie uniquement le preview hex déjà présent côté frontend ; il ne charge ni n'expose le payload complet. Les canaris Rust ajoutés en `fix.001` sont corrigés avec des raw string literals valides.
|
||||
|
||||
#### pre.007-fix.003 — rescope du canari de tracing copie
|
||||
|
||||
Le canari de sécurité ne bannit plus globalement des variables métier légitimes comme `signature` : il vérifie uniquement que les événements de copie transportent `{ fieldId }` et jamais la valeur copiée. Aucun runtime/frontend n'est modifié.
|
||||
|
||||
### pre.008 — RawAccountState DataTables serverSide + détail
|
||||
|
||||
Même intégration pour Account, pubkey/range/direction, summaries JS-safe, détail data borné, responsive/scrollX/copy UX.
|
||||
Brancher `RAW Accounts` sur `RawAccountStateInspectionRead` avec le même modèle server-side que Transactions : `start/length` -> offset/limit, counts exacts, filtres Store explicites `pubkey` exacte + slot min/max + direction, `searching:false`, `ordering:false`, whitelist 25/50/100 et DataTables comme unique pager. Les rows restent data-free et exposent pubkey, slot, state hash, owner, lamports, executable, rent epoch et data length.
|
||||
|
||||
Le détail reçoit uniquement l'identité de row `(pubkey, slot, state_hash)`, dérive le network du Store ouvert et appelle `RawAccountStateRead`. Les bytes Account ne traversent jamais la table ; le modal reçoit au maximum 512 bytes sous forme de preview hex avec taille exacte et flag de troncature. Pubkey, owner, state hash et preview data réutilisent le helper générique troncature/tooltip/copie stabilisé en `pre.007-fix.001/002/003`. Les traces ne contiennent aucune valeur de pubkey, slot, state hash, owner ou data.
|
||||
|
||||
### pre.009 — extension Store observations
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Validation v0.3.8 — Store Desk V1 RAW
|
||||
|
||||
@@ -638,3 +638,39 @@ Le gate opérateur de `pre.007-fix.002` confirme le comportement runtime attendu
|
||||
- [X] aucun frontend, backend, IPC, Store, SQL ou capability Tauri n'est modifié.
|
||||
|
||||
Le gate Cargo de ce fix reste à rejouer après application du delta.
|
||||
|
||||
## 32. Clôture `pre.007-fix.003` et assemblage `pre.008` — RAW Accounts server-side
|
||||
|
||||
Le gate opérateur de `0.3.8-pre.7.fix.3` est entièrement vert :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (314 table(s), 707 file(s))
|
||||
cargo check --workspace: PASS
|
||||
cargo clippy --workspace --all-targets: PASS
|
||||
cargo test -p ksp-app-store-desk: PASS (21 unitaires + 4 dependency + 11 desktop contract + 9 security + 1 public API)
|
||||
cargo test -p ksp-store-lib: PASS
|
||||
cargo check -p ksp-store-lib --no-default-features: PASS
|
||||
```
|
||||
|
||||
`pre.008` branche uniquement la famille RAW AccountState dans Store Desk :
|
||||
|
||||
- [X] `workspace.package.version = 0.3.8-pre.8` ;
|
||||
- [X] nouveaux DTO app-owned `StoreAccount*Dto`, tous `pub(crate)` réexportés via `lib.rs` et consommés sous `crate::Item` ;
|
||||
- [X] la requête Account IPC ne transporte que offset décimal, limit 25/50/100, pubkey exacte optionnelle, slot min/max optionnels et direction canonique ;
|
||||
- [X] aucun draw/search/order/cursor/network/URI/backend physique ne traverse la requête ;
|
||||
- [X] le network est dérivé du Store déjà ouvert ;
|
||||
- [X] `RawAccountStateInspectionRead::inspect_raw_account_states` fournit counts exacts et summaries data-free ;
|
||||
- [X] les rows exposent pubkey, slot, state hash, owner, lamports, executable, rent epoch et data length sans byte Account ;
|
||||
- [X] `RAW Accounts` passe à `serverSide:true`, `searching:false`, `ordering:false`, 25/50/100 et conserve DataTables comme unique pager ;
|
||||
- [X] les counts restent décimaux jusqu'au contrôle `Number.MAX_SAFE_INTEGER` côté frontend ;
|
||||
- [X] `store_get_account_detail` reçoit uniquement pubkey + slot + state hash de la row et dérive le network côté Rust ;
|
||||
- [X] le détail appelle `RawAccountStateRead::get_raw_account_state` et projette au maximum 512 bytes de data en preview hex ;
|
||||
- [X] pubkey, owner, state hash et preview data sont copiables via le helper générique sans journaliser leur valeur ;
|
||||
- [X] les filtres Account, pages, page lengths, redraws, refresh et détail sont tracés sans valeurs métier ;
|
||||
- [X] aucune modification Store API, Store façade, PostgreSQL, SQL, migration, Config ou capability Tauri n'est nécessaire.
|
||||
|
||||
Le gate Cargo de `pre.008` reste à rejouer après application du delta.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user