v0.5.3-pre.005-fix010

This commit is contained in:
2026-08-14 14:40:36 +02:00
parent b6583bd9fb
commit 734a27f2ca
59 changed files with 4337 additions and 747 deletions

View File

@@ -1,8 +1,24 @@
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
<!-- version: 66 -->
<!-- version: 71 -->
# CHANGELOG — kb-app-demo-desktop
## `0.5.3-pre.005`
- centralise la résolution et la préparation du Store des profils Devnet dans `demo_devnet_common`, utilisé par Solana Core, SPL et Metadata au lieu de rattacher ce contrat partagé à `demo_execution_solana_core` ;
- ajoute des traces `frontendDebug` explicites pour les clics et invocations de chargement des journaux SPL Token/ATA ainsi que pour la préparation des profils SPL/Metadata, afin de distinguer la réutilisation normale du Store partagé dune absence de requête ;
- corrige la préparation automatique des profils Execution Devnet afin quelle utilise elle aussi le `ks_store::Store` partagé par profil : louverture dune seconde fenêtre Devnet et le premier chargement de journal ne rouvrent plus un pool PostgreSQL ni ne relancent `schema_initialize` après que le profil a déjà été préparé ;
- réutilise désormais un `ks_store::Store` partagé par profil pour toutes les surfaces Execution Devnet : les lectures de journaux et exécutions successives ne recréent plus un pool PostgreSQL ni ne relancent `schema_initialize` à chaque commande, tout en conservant le Store du profil actif séparé des profils Devnet ;
- corrige la restauration des boutons de navigation Store après le chargement initial parallèle de Replay Candidates : chaque contrôleur déjà chargé retrouve son état `Premier/Précédent/Suivant` une fois toutes les opérations concurrentes terminées, sans exiger un second clic sur `Charger` ;
- supprime les champs éditables de limite des listings de consultation Store : cinq listes Replay Candidates, annotations de transactions, journal SPL Token et journal ATA ; la taille backend est fixée à 500 lignes par bloc ;
- ajoute une navigation Store supérieure `Premier bloc / Bloc précédent / Bloc suivant` indépendante de la pagination locale DataTables ;
- recalcule `totalRows` et `totalBlocks` à chaque chargement ou navigation ; tout bouton `Charger` repart du bloc 1 avec les filtres courants, tandis que `Premier/Précédent/Suivant` rafraîchissent également le nombre de blocs ;
- invalide les curseurs mémorisés dès quun filtre serveur change et exige alors un nouveau chargement du premier bloc ;
- étend DataTables jusquà 500 lignes affichables localement dans le bloc courant ;
- applique le même contrôleur de blocs aux annotations de Decode Replay et aux journaux SPL Token/ATA ; leurs comptages backend utilisent exactement les filtres serveur visibles, y compris les contraintes de payload, sans `OFFSET` ;
- termine le nettoyage des identifiants frontend `refreshSql*` / `copySql*` au profit de `refreshStore*` / `copyStore*` ;
- conserve `demo_config` et le splash sur les résumés Store backend-agnostiques déjà introduits, sans noms physiques dobjets PostgreSQL.
## `0.5.3-pre.004`
- aligne la limite maximale de la fenêtre technique de candidats replay Store sur la nouvelle borne interactive backend-agnostique de 500 lignes ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/README.md -->
<!-- version: 23 -->
<!-- version: 24 -->
# kb-app-demo-desktop
@@ -20,7 +20,7 @@
Les commandes Tauri sont des adaptateurs minces. La logique réutilisable appartient à `ks-pipeline`, `ks-pipeline-demo-scenarios`, `ks-lib`, `ks-store` et `ks-onchain-transport`.
Depuis `0.5.3-pre.002`, l'application conserve une façade `ks_store::Store` unique dans `AppState`. Les commandes ne construisent plus de `PostgresStore` et les payloads de configuration/diagnostic ne transportent plus de champs PostgreSQL/SQLite concrets. Les anciennes surfaces `demo_sql_*` sont renommées en `demo_store_*` dès `pre.002`; la refonte fonctionnelle finale de leurs tableaux et compteurs reste planifiée en `pre.005`.
Depuis `0.5.3-pre.002`, l'application conserve une façade `ks_store::Store` unique dans `AppState`. Les commandes ne construisent plus de backend concret et les payloads de configuration/diagnostic ne transportent plus de champs PostgreSQL/SQLite concrets. Depuis `0.5.3-pre.005`, les listings de consultation Store utilisent des blocs fixes de 500 parcourus par curseurs : Replay Candidates, annotations de transactions et journaux SPL Token/ATA partagent le même principe. Chaque chargement recalcule le nombre total de blocs pour les filtres actifs. Lorsquune vue utilise DataTables, sa pagination/son tri/son filtre restent strictement locaux au bloc déjà chargé.
Les campagnes Devnet/Testnet réutilisables appartiennent à `ks-pipeline-demo-scenarios`. Le desktop conserve leurs adaptateurs UI/Tauri, la sélection opérateur, la progression, les payloads TS-RS et la présentation ; `0.5.4` doit retirer les orchestrations réutilisables qui subsistent encore localement.

View File

@@ -1,11 +1,11 @@
<!-- file: kb-app-demo-desktop/TODO.md -->
<!-- version: 22 -->
<!-- version: 23 -->
# TODO — kb-app-demo-desktop
## Évolutions générales
- [ ] `0.5.3` — finaliser en `pre.005` la présentation des surfaces `demo_store_*`, puis aligner `demo_config` et le splash sur les modèles/compteurs du nouveau baseline sans exposer de noms physiques ; le renommage technique est terminé en `pre.002`.
- [x] `0.5.3-pre.005` — finaliser les listings de consultation Store avec blocs fixes de 500, curseurs stables, total de blocs recalculé à chaque chargement/navigation et pagination DataTables locale lorsquelle existe ; Replay Candidates, annotations et journaux SPL Token/ATA suivent le même contrat, tandis que `demo_config` et le splash consomment les résumés génériques/sanitisés sans noms physiques.
- [ ] `0.5.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`.
- [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active.
- [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/USAGE.md -->
<!-- version: 24 -->
<!-- version: 25 -->
# Utilisation de kb-app-demo-desktop
@@ -125,6 +125,14 @@ Les autres fenêtres sont ouvertes par les commandes Tauri dédiées :
- exécution Solana Core ;
- exécution SPL.
## Pagination des listings Store
Les listings de consultation Store nexposent plus de champ `limit` éditable lorsquils utilisent la borne interactive de 500. Le backend charge un bloc fixe de 500 lignes maximum et recalcule `totalRows`/`totalBlocks` à chaque action `Charger`, `Premier bloc`, `Bloc précédent` ou `Bloc suivant`.
Le curseur reste opaque et nest jamais saisi par lopérateur. Modifier un filtre serveur invalide les curseurs mémorisés et impose de recharger le premier bloc. Lorsquune vue utilise DataTables, sa pagination, son tri et son filtre restent locaux aux lignes du bloc courant et ne modifient jamais la pagination Store.
Cette règle sapplique aux Replay Candidates, aux annotations de transaction et aux journaux SPL Token/ATA. Les limites métier de backfill, de campagne decode/Core ou de requêtes RPC restent indépendantes et peuvent demeurer configurables.
## Interface Tauri publique
Les commandes Tauri constituent linterface applicative frontend/backend. Elles ne sont pas des APIs Rust publiques pour les autres crates.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_decode_replay.html -->
<!-- version: 9 -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -131,11 +131,13 @@
<label class="form-label" for="annotationSignatureInput">Journal des annotations — signature contient</label>
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
</div>
<div>
<label class="form-label" for="annotationLimitInput">Limite</label>
<input id="annotationLimitInput" class="form-control" type="number" min="1" max="500" value="100" />
</div>
<button id="loadAnnotationsButton" class="btn btn-outline-primary" type="button">Charger les annotations</button>
<div class="btn-group" role="group" aria-label="Navigation des blocs Store dannotations">
<button id="annotationFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="annotationPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="annotationNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="annotationBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
<div class="table-responsive">

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_execution_spl.html -->
<!-- version: 15 -->
<!-- version: 16 -->
<!DOCTYPE html>
<html lang="fr">
@@ -306,7 +306,12 @@
<h2 class="h5 mb-0">Journal métier SPL Token</h2>
<div class="form-text">Événements commités uniquement, sans solde final reconstruit ni OHLC.</div>
</div>
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Actualiser</button>
<div class="d-flex flex-wrap align-items-center gap-2">
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Charger</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalFirstBlockButton" type="button">Premier bloc</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalPreviousBlockButton" type="button">Bloc précédent</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalNextBlockButton" type="button">Bloc suivant</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-lg-6">
@@ -330,15 +335,15 @@
<option value="risk">risk</option>
</select>
</div>
<div class="col-6 col-lg-3">
<label class="form-label" for="tokenJournalLimitInput">Limite</label>
<input class="form-control" id="tokenJournalLimitInput" max="500" min="1" step="1" type="number" value="100" />
</div>
<div class="col-12">
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
</div>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<span class="badge text-bg-light border" id="tokenJournalBlockSummary">Bloc 0 / 0 · 0 résultat · 500/bloc</span>
<span class="form-text">Le total de blocs est recalculé à chaque chargement ou navigation.</span>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
@@ -375,7 +380,12 @@
<h2 class="h5 mb-0">Journal lifecycle ATA</h2>
<div class="form-text">Création, réutilisation idempotente et récupération nested, sans duplication des CPI Token.</div>
</div>
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Actualiser</button>
<div class="d-flex flex-wrap align-items-center gap-2">
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Charger</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalFirstBlockButton" type="button">Premier bloc</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalPreviousBlockButton" type="button">Bloc précédent</button>
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalNextBlockButton" type="button">Bloc suivant</button>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-lg-6">
@@ -390,14 +400,14 @@
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
</div>
<div class="col-8 col-lg-4">
<div class="col-12 col-lg-6">
<label class="form-label" for="ataJournalOperationInput">Opération</label>
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
</div>
<div class="col-4 col-lg-2">
<label class="form-label" for="ataJournalLimitInput">Limite</label>
<input class="form-control" id="ataJournalLimitInput" max="500" min="1" type="number" value="100" />
</div>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<span class="badge text-bg-light border" id="ataJournalBlockSummary">Bloc 0 / 0 · 0 résultat · 500/bloc</span>
<span class="form-text">Le total de blocs est recalculé à chaque chargement ou navigation.</span>
</div>
<textarea class="form-control font-monospace" data-json-viewer="true" id="ataJournalOutput" readonly="" rows="14">{"title":"Journal lifecycle ATA","status":"idle","message":"Aucune lecture."}</textarea>
</div>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_store_core.html -->
<!-- version: 8 -->
<!-- version: 9 -->
<!DOCTYPE html>
<html lang="fr">
@@ -32,7 +32,7 @@
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<h1 class="h3 card-title mb-0">Core store Solana</h1>
<button id="refreshSqlCoreButton" class="btn btn-primary" type="button">Rafraîchir</button>
<button id="refreshStoreCoreButton" class="btn btn-primary" type="button">Rafraîchir</button>
</div>
<dl class="row mb-0">
<dt class="col-sm-3">Statut</dt>
@@ -94,4 +94,4 @@
<script type="module" src="ts/demo_store_core.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_store_diag.html -->
<!-- version: 7 -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
@@ -33,8 +33,8 @@
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<h1 class="h3 card-title mb-0">Diagnostic du store</h1>
<div class="d-flex gap-2">
<button id="refreshSqlDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
<button id="copySqlDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
<button id="refreshStoreDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
<button id="copyStoreDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
</div>
</div>
<dl class="row mb-0">
@@ -107,4 +107,4 @@
<script type="module" src="ts/demo_store_diag.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_store_raw.html -->
<!-- version: 9 -->
<!-- version: 10 -->
<!DOCTYPE html>
<html lang="fr">
@@ -32,7 +32,7 @@
<div class="card-body">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
<h1 class="h3 card-title mb-0">Acquisition transactionnelle Solana</h1>
<button id="refreshSqlRawButton" class="btn btn-primary" type="button">Rafraîchir</button>
<button id="refreshStoreRawButton" class="btn btn-primary" type="button">Rafraîchir</button>
</div>
<dl class="row mb-0">
<dt class="col-sm-3">Statut</dt>
@@ -94,4 +94,4 @@
<script type="module" src="ts/demo_store_raw.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/demo_store_replay_candidates.html -->
<!-- version: 17 -->
<!-- version: 18 -->
<!DOCTYPE html>
<html lang="fr">
@@ -36,12 +36,12 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h1 class="h3 card-title">Exploration read-only des candidats</h1>
<p class="text-body-secondary mb-2">Les requêtes sont paramétrées, bornées et indépendantes du backend concret. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et lexport CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue dimport, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
<p class="text-body-secondary mb-2">Les requêtes sont paramétrées, bornées et indépendantes du backend concret. La navigation <strong>Premier bloc / Bloc précédent / Bloc suivant</strong> recharge le Store par blocs fixes de 500 et recalcule leur nombre total à chaque chargement ; la pagination, le tri et le filtre DataTables restent locaux au bloc déjà chargé. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et lexport CSV utilisent toutes les lignes filtrées du bloc courant. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue dimport, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
<div class="small text-body-secondary">
<span class="fw-semibold">Connexion :</span>
<code id="replayCandidateDsn"></code>
<span class="ms-3 fw-semibold">Limite serveur maximale :</span>
<code id="replayCandidateMaximumLimit"></code>
<span class="ms-3 fw-semibold">Taille fixe dun bloc Store :</span>
<code id="replayCandidateBlockSize"></code>
</div>
</div>
</div>
@@ -129,10 +129,6 @@
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
</div>
<div class="col-6 col-md-3 col-xl-1">
<label for="transactionLimitInput" class="form-label">Limite</label>
<input id="transactionLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
</div>
<div class="col-6 col-md-3 col-xl-2">
<div class="form-check form-switch mb-2">
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
@@ -155,6 +151,14 @@
<div>
<h2 class="h5 card-title mb-1">Transactions candidates</h2>
<div id="transactionResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store transaction">
<div class="btn-group btn-group-sm" role="group">
<button id="transactionFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="transactionPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="transactionNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="transactionBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
</div>
<div>
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
@@ -199,15 +203,11 @@
<option value="">Tous les programmes connus</option>
</select>
</div>
<div class="col-12 col-lg-4">
<div class="col-12 col-lg-6">
<label for="programContainsInput" class="form-label">Program ID contient</label>
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
<datalist id="knownProgramDatalist"></datalist>
</div>
<div class="col-6 col-lg-1">
<label for="programLimitInput" class="form-label">Limite</label>
<input id="programLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
</div>
<div class="col-12 col-lg-2">
<div class="d-grid gap-2">
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
@@ -223,6 +223,14 @@
<div>
<h2 class="h5 card-title mb-1">Programmes indexés</h2>
<div id="programResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store program">
<div class="btn-group btn-group-sm" role="group">
<button id="programFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="programPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="programNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="programBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
</div>
<div>
<div class="btn-group flex-wrap" role="group" aria-label="Actions programmes">
@@ -258,14 +266,10 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-7">
<div class="col-12 col-lg-8">
<label for="mintContainsInput" class="form-label">Mint contient</label>
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
</div>
<div class="col-6 col-lg-2">
<label for="mintLimitInput" class="form-label">Limite</label>
<input id="mintLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
</div>
<div class="col-12 col-lg-3">
<div class="d-grid d-sm-flex gap-2">
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
@@ -281,6 +285,14 @@
<div>
<h2 class="h5 card-title mb-1">Mints</h2>
<div id="mintResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store mint">
<div class="btn-group btn-group-sm" role="group">
<button id="mintFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="mintPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="mintNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="mintBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
<div class="small text-body-secondary">Source : faits Core de variation de balance (mint).</div>
</div>
<div>
@@ -316,14 +328,10 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-7">
<div class="col-12 col-lg-8">
<label for="ownerContainsInput" class="form-label">Owner contient</label>
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
</div>
<div class="col-6 col-lg-2">
<label for="ownerLimitInput" class="form-label">Limite</label>
<input id="ownerLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
</div>
<div class="col-12 col-lg-3">
<div class="d-grid d-sm-flex gap-2">
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
@@ -339,6 +347,14 @@
<div>
<h2 class="h5 card-title mb-1">Owners</h2>
<div id="ownerResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store owner">
<div class="btn-group btn-group-sm" role="group">
<button id="ownerFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="ownerPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="ownerNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="ownerBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
<div class="small text-body-secondary">Source : faits Core de variation de balance (owner).</div>
</div>
<div>
@@ -374,14 +390,10 @@
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-7">
<div class="col-12 col-lg-8">
<label for="accountContainsInput" class="form-label">Account key contient</label>
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
</div>
<div class="col-6 col-lg-2">
<label for="accountLimitInput" class="form-label">Limite</label>
<input id="accountLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
</div>
<div class="col-12 col-lg-3">
<div class="d-grid d-sm-flex gap-2">
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
@@ -397,6 +409,14 @@
<div>
<h2 class="h5 card-title mb-1">Comptes</h2>
<div id="accountResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store account">
<div class="btn-group btn-group-sm" role="group">
<button id="accountFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
<button id="accountPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
<button id="accountNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
</div>
<span id="accountBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
</div>
<div class="small text-body-secondary">Source : clés de comptes Core résolues.</div>
</div>
<div>
@@ -444,4 +464,4 @@
<script type="module" src="ts/demo_store_replay_candidates.ts" defer></script>
</body>
</html>
</html>

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
// version: 8
// version: 9
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -9,11 +9,21 @@ import DataTable, { type Api } from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
import {
applyStoreBlockMetadata,
createStoreBlockState,
cursorForBlock,
markStoreBlockFiltersDirty,
resetStoreBlockState,
renderStoreBlockControls,
type StoreBlockControls,
} from "./demo_store_block_pagination.ts";
import type { DemoDecodeDiagnosticsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts";
import type { DemoDecodeReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayOptionsPayload.ts";
import type { DemoDecodeReplayProgressPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayProgressPayload.ts";
import type { DemoDecodeReplayRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayRequest.ts";
import type { DemoDecodeReplaySummaryPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplaySummaryPayload.ts";
import type { DemoTransactionAnnotationPagePayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationPagePayload.ts";
import type { DemoTransactionAnnotationRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRequest.ts";
import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRow.ts";
@@ -23,6 +33,15 @@ import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_deskto
const logLines: string[] = [];
let running = false;
let materializationAvailable = false;
const annotationBlockControls: StoreBlockControls = {
firstSelector: "#annotationFirstBlockButton",
previousSelector: "#annotationPreviousBlockButton",
nextSelector: "#annotationNextBlockButton",
summarySelector: "#annotationBlockSummary",
};
const annotationBlockState = createStoreBlockState();
type AnnotationBaseRequest = Omit<DemoTransactionAnnotationRequest, "cursor" | "blockIndex">;
let annotationBaseRequest: AnnotationBaseRequest | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
@@ -250,7 +269,7 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
annotationJournalTable = new DataTable<DemoTransactionAnnotationRow>("#annotationJournalTable", {
autoWidth: false,
pageLength: 25,
lengthMenu: [10, 25, 50, 100],
lengthMenu: [10, 25, 50, 100, 250, 500],
order: [[0, "desc"]],
scrollX: true,
language: {
@@ -265,23 +284,53 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
}
async function loadAnnotations(): Promise<void> {
const request: DemoTransactionAnnotationRequest = {
async function startAnnotationLoad(): Promise<void> {
annotationBaseRequest = {
signatureContains: inputValue("#annotationSignatureInput") || null,
limit: integerValue("#annotationLimitInput"),
};
resetStoreBlockState(annotationBlockState);
await loadAnnotationBlock(1, null);
}
async function loadAnnotationBlock(blockIndex: number, cursor: string | null): Promise<void> {
if (annotationBaseRequest === null) {
throw new Error("Rechargez les annotations pour initialiser les filtres Store.");
}
const request: DemoTransactionAnnotationRequest = {
...annotationBaseRequest,
cursor,
blockIndex,
};
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations limit=${request.limit}`);
const rows = await invoke<DemoTransactionAnnotationRow[]>("demo_decode_replay_annotations", { request });
renderAnnotations(rows);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations block=${blockIndex}`);
const payload = await invoke<DemoTransactionAnnotationPagePayload>("demo_decode_replay_annotations", { request });
renderAnnotations(payload.rows);
applyStoreBlockMetadata(annotationBlockState, payload.page, annotationBlockControls);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
renderStoreBlockControls(annotationBlockState, annotationBlockControls);
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
}
}
async function loadPreviousAnnotationBlock(): Promise<void> {
const targetBlock = annotationBlockState.currentBlock - 1;
const cursor = cursorForBlock(annotationBlockState, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadAnnotationBlock(targetBlock, cursor);
}
async function loadNextAnnotationBlock(): Promise<void> {
if (annotationBlockState.nextCursor === null) {
return;
}
await loadAnnotationBlock(annotationBlockState.currentBlock + 1, annotationBlockState.nextCursor);
}
async function openWindow(command: "open_demo_store_core_window" | "open_demo_store_replay_candidates_window"): Promise<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`);
@@ -357,7 +406,14 @@ document.addEventListener("DOMContentLoaded", () => {
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void startAnnotationLoad());
element<HTMLButtonElement>(annotationBlockControls.firstSelector).addEventListener("click", () => void loadAnnotationBlock(1, null));
element<HTMLButtonElement>(annotationBlockControls.previousSelector).addEventListener("click", () => void loadPreviousAnnotationBlock());
element<HTMLButtonElement>(annotationBlockControls.nextSelector).addEventListener("click", () => void loadNextAnnotationBlock());
element<HTMLInputElement>("#annotationSignatureInput").addEventListener("input", () => {
annotationBaseRequest = null;
markStoreBlockFiltersDirty(annotationBlockState, annotationBlockControls);
});
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_replay_candidates_window"));
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_core_window"));
void listen<DemoDecodeReplayProgressPayload>("demo-decode-replay-progress", event => appendLog(event.payload));

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
// version: 14
// version: 15
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -479,7 +479,9 @@ async function prepareProfile(): Promise<void> {
setExecutionRunning(executionRunning);
updateProfileStatus("Préparation", "badge text-bg-warning");
try {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile profile=${profileName}`);
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile completed profile=${profileName} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
synchronizeProfileSelections(profileName);
window.localStorage.setItem(profileStorageKey, profileName);
for (const selector of profileOutputSelectors) {
@@ -580,9 +582,18 @@ document.addEventListener("DOMContentLoaded", () => {
void prepareProfile();
});
}
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metaplex profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Token-2022 Metadata profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => {
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Solana Program Metadata profile load button clicked");
void prepareProfile();
});
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
// version: 11
// version: 13
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -19,13 +19,23 @@ import type { DemoExecutionMemoSummaryPayload } from "./bindings/kb_app_demo_des
import type { DemoExecutionSplTokenRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenRequest.ts";
import type { DemoExecutionSplTokenSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts";
import type { DemoSplTokenJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts";
import type { DemoSplTokenJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalPagePayload.ts";
import type { DemoSplTokenJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRow.ts";
import type { DemoExecutionSplAtaRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts";
import type { DemoExecutionSplAtaSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts";
import type { DemoSplAtaDerivationPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts";
import type { DemoSplAtaDerivationRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts";
import type { DemoSplAtaJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts";
import type { DemoSplAtaJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts";
import type { DemoSplAtaJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalPagePayload.ts";
import {
applyStoreBlockMetadata,
createStoreBlockState,
cursorForBlock,
markStoreBlockFiltersDirty,
renderStoreBlockControls,
resetStoreBlockState,
type StoreBlockControls,
} from "./demo_store_block_pagination";
import type { DemoExecutionSplToken2022Request } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022Request.ts";
import type { DemoExecutionSplToken2022SummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022SummaryPayload.ts";
import type { DemoSplToken2022FixturePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoSplToken2022FixturePayload.ts";
@@ -40,6 +50,24 @@ let running = false;
let profileStoreReady = false;
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
let token_2022Fixture: DemoSplToken2022FixturePayload | null = null;
const tokenJournalBlockState = createStoreBlockState();
const ataJournalBlockState = createStoreBlockState();
const tokenJournalBlockControls: StoreBlockControls = {
firstSelector: "#tokenJournalFirstBlockButton",
previousSelector: "#tokenJournalPreviousBlockButton",
nextSelector: "#tokenJournalNextBlockButton",
summarySelector: "#tokenJournalBlockSummary",
};
const ataJournalBlockControls: StoreBlockControls = {
firstSelector: "#ataJournalFirstBlockButton",
previousSelector: "#ataJournalPreviousBlockButton",
nextSelector: "#ataJournalNextBlockButton",
summarySelector: "#ataJournalBlockSummary",
};
type TokenJournalBaseRequest = Omit<DemoSplTokenJournalRequest, "cursor" | "blockIndex">;
type AtaJournalBaseRequest = Omit<DemoSplAtaJournalRequest, "cursor" | "blockIndex">;
let tokenJournalBaseRequest: TokenJournalBaseRequest | null = null;
let ataJournalBaseRequest: AtaJournalBaseRequest | null = null;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
@@ -400,7 +428,7 @@ async function executeAta(submit: boolean): Promise<void> {
element<HTMLInputElement>("#ataJournalSignatureInput").value = summary.transactionSignature;
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
await loadAtaJournal();
await startAtaJournalLoad();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -412,27 +440,52 @@ async function executeAta(submit: boolean): Promise<void> {
}
}
function ataJournalRequest(): DemoSplAtaJournalRequest {
function ataJournalBaseFilters(): AtaJournalBaseRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#ataJournalSignatureInput"),
mint: optionalText("#ataJournalMintInput"),
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
operation: optionalText("#ataJournalOperationInput"),
limit: integerValue("#ataJournalLimitInput"),
};
}
async function loadAtaJournal(): Promise<void> {
async function startAtaJournalLoad(): Promise<void> {
ataJournalBaseRequest = ataJournalBaseFilters();
resetStoreBlockState(ataJournalBlockState);
await loadAtaJournalBlock(1);
}
async function loadAtaJournalBlock(blockIndex: number): Promise<void> {
if (ataJournalBaseRequest === null) {
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
return;
}
const cursor = cursorForBlock(ataJournalBlockState, blockIndex);
if (cursor === undefined) {
return;
}
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
button.disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.firstSelector).disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.previousSelector).disabled = true;
element<HTMLButtonElement>(ataJournalBlockControls.nextSelector).disabled = true;
const request: DemoSplAtaJournalRequest = {
...ataJournalBaseRequest,
cursor,
blockIndex,
};
try {
const rows = await invoke<DemoSplAtaJournalRow[]>("demo_spl_ata_journal", { request: ataJournalRequest() });
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal block=${blockIndex}`);
const payload = await invoke<DemoSplAtaJournalPagePayload>("demo_spl_ata_journal", { request });
applyStoreBlockMetadata(ataJournalBlockState, payload.page, ataJournalBlockControls);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({
title: "Journal lifecycle ATA",
status: "success",
count: rows.length,
rows: rows.map(row => ({
block: payload.page,
count: payload.rows.length,
rows: payload.rows.map(row => ({
...row,
payloadJson: parsedJson(row.payloadJson),
})),
@@ -445,11 +498,17 @@ async function loadAtaJournal(): Promise<void> {
message,
error: message,
}, null, 2);
renderStoreBlockControls(ataJournalBlockState, ataJournalBlockControls);
} finally {
button.disabled = false;
}
}
function markAtaJournalFiltersDirty(): void {
ataJournalBaseRequest = null;
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
}
async function execute(submit: boolean): Promise<void> {
if (running) {
return;
@@ -567,7 +626,7 @@ async function executeToken(submit: boolean): Promise<void> {
});
if (summary.transactionSignature) {
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
await loadTokenJournal();
await startTokenJournalLoad();
}
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -579,7 +638,7 @@ async function executeToken(submit: boolean): Promise<void> {
}
}
function tokenJournalRequest(): DemoSplTokenJournalRequest {
function tokenJournalBaseFilters(): TokenJournalBaseRequest {
return {
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
signatureContains: optionalText("#tokenJournalSignatureInput"),
@@ -587,7 +646,6 @@ function tokenJournalRequest(): DemoSplTokenJournalRequest {
account: optionalText("#tokenJournalAccountInput"),
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
operation: optionalText("#tokenJournalOperationInput"),
limit: integerValue("#tokenJournalLimitInput"),
};
}
@@ -831,17 +889,42 @@ async function executeToken2022(submit: boolean): Promise<void> {
}
}
async function loadTokenJournal(): Promise<void> {
async function startTokenJournalLoad(): Promise<void> {
tokenJournalBaseRequest = tokenJournalBaseFilters();
resetStoreBlockState(tokenJournalBlockState);
await loadTokenJournalBlock(1);
}
async function loadTokenJournalBlock(blockIndex: number): Promise<void> {
if (tokenJournalBaseRequest === null) {
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
return;
}
const cursor = cursorForBlock(tokenJournalBlockState, blockIndex);
if (cursor === undefined) {
return;
}
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
button.disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.firstSelector).disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.previousSelector).disabled = true;
element<HTMLButtonElement>(tokenJournalBlockControls.nextSelector).disabled = true;
const request: DemoSplTokenJournalRequest = {
...tokenJournalBaseRequest,
cursor,
blockIndex,
};
try {
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
renderTokenJournal(rows);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal block=${blockIndex}`);
const payload = await invoke<DemoSplTokenJournalPagePayload>("demo_spl_token_journal", { request });
applyStoreBlockMetadata(tokenJournalBlockState, payload.page, tokenJournalBlockControls);
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
renderTokenJournal(payload.rows);
appendLog({
timestamp: new Date().toISOString(),
level: "info",
stage: "spl_token_journal",
message: `${rows.length} événement(s) matérialisé(s) chargé(s).`,
message: `${payload.rows.length} événement(s) matérialisé(s) chargé(s) dans le bloc ${payload.page.blockIndex}/${payload.page.totalBlocks}.`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
@@ -851,12 +934,18 @@ async function loadTokenJournal(): Promise<void> {
message,
error: message,
}, null, 2);
renderStoreBlockControls(tokenJournalBlockState, tokenJournalBlockControls);
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
} finally {
button.disabled = false;
}
}
function markTokenJournalFiltersDirty(): void {
tokenJournalBaseRequest = null;
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
}
async function generateRecipient(): Promise<void> {
try {
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
@@ -898,7 +987,9 @@ async function prepareSelectedProfile(): Promise<void> {
}
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` });
try {
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile profile=${profile.name}`);
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName: profile.name });
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile completed profile=${profile.name} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
profileStoreReady = true;
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `Store ${readiness.backend} prêt: ${readiness.availableResourcesAfter}/${readiness.expectedResources} ressources, créées=${readiness.createdResources}, autoInit=${readiness.autoInitializeSchema}.` });
} catch (caughtError) {
@@ -948,6 +1039,8 @@ document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => {
updateProfileHelp();
markTokenJournalFiltersDirty();
markAtaJournalFiltersDirty();
void prepareSelectedProfile();
});
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
@@ -982,8 +1075,27 @@ document.addEventListener("DOMContentLoaded", () => {
void executeToken2022(true);
});
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
void loadTokenJournal();
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "Token journal load button clicked");
void startTokenJournalLoad();
});
element<HTMLButtonElement>("#tokenJournalFirstBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(1);
});
element<HTMLButtonElement>("#tokenJournalPreviousBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock - 1);
});
element<HTMLButtonElement>("#tokenJournalNextBlockButton").addEventListener("click", () => {
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock + 1);
});
for (const selector of [
"#tokenJournalSignatureInput",
"#tokenJournalMintInput",
"#tokenJournalAccountInput",
"#tokenJournalOperationInput",
]) {
element<HTMLInputElement>(selector).addEventListener("input", markTokenJournalFiltersDirty);
}
element<HTMLSelectElement>("#tokenJournalFamilySelect").addEventListener("change", markTokenJournalFiltersDirty);
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
void deriveAta();
});
@@ -994,8 +1106,26 @@ document.addEventListener("DOMContentLoaded", () => {
void executeAta(true);
});
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
void loadAtaJournal();
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "ATA journal load button clicked");
void startAtaJournalLoad();
});
element<HTMLButtonElement>("#ataJournalFirstBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(1);
});
element<HTMLButtonElement>("#ataJournalPreviousBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(ataJournalBlockState.currentBlock - 1);
});
element<HTMLButtonElement>("#ataJournalNextBlockButton").addEventListener("click", () => {
void loadAtaJournalBlock(ataJournalBlockState.currentBlock + 1);
});
for (const selector of [
"#ataJournalSignatureInput",
"#ataJournalMintInput",
"#ataJournalAddressInput",
"#ataJournalOperationInput",
]) {
element<HTMLInputElement>(selector).addEventListener("input", markAtaJournalFiltersDirty);
}
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
void cancelExecution();
});

View File

@@ -0,0 +1,114 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
// version: 1
export type StoreBlockMetadata = {
blockSize: number;
blockIndex: number;
totalRows: string;
totalBlocks: number;
nextCursor: string | null;
};
export type StoreBlockState = {
blockSize: number;
currentBlock: number;
totalBlocks: number;
totalRows: string;
nextCursor: string | null;
cursors: Map<number, string | null>;
};
export type StoreBlockControls = {
firstSelector: string;
previousSelector: string;
nextSelector: string;
summarySelector: string;
};
export function createStoreBlockState(): StoreBlockState {
return {
blockSize: 500,
currentBlock: 0,
totalBlocks: 0,
totalRows: "0",
nextCursor: null,
cursors: new Map<number, string | null>([[1, null]]),
};
}
export function resetStoreBlockState(state: StoreBlockState): void {
state.blockSize = 500;
state.currentBlock = 0;
state.totalBlocks = 0;
state.totalRows = "0";
state.nextCursor = null;
state.cursors = new Map<number, string | null>([[1, null]]);
}
export function applyStoreBlockMetadata(
state: StoreBlockState,
metadata: StoreBlockMetadata,
controls: StoreBlockControls,
): void {
state.blockSize = metadata.blockSize;
state.currentBlock = metadata.blockIndex;
state.totalBlocks = metadata.totalBlocks;
state.totalRows = metadata.totalRows;
state.nextCursor = metadata.nextCursor;
if (metadata.blockIndex > 0 && metadata.nextCursor !== null) {
state.cursors.set(metadata.blockIndex + 1, metadata.nextCursor);
}
renderStoreBlockControls(state, controls);
}
export function renderStoreBlockControls(
state: StoreBlockState,
controls: StoreBlockControls,
): void {
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
const hasRows = state.totalBlocks > 0;
if (firstButton !== null) {
firstButton.disabled = !hasRows || state.currentBlock <= 1;
}
if (previousButton !== null) {
previousButton.disabled = !hasRows || state.currentBlock <= 1;
}
if (nextButton !== null) {
nextButton.disabled = state.nextCursor === null;
}
if (summary !== null) {
summary.textContent = hasRows
? `Bloc ${state.currentBlock} / ${state.totalBlocks} · ${state.totalRows} résultat(s) · ${state.blockSize}/bloc`
: `Bloc 0 / 0 · 0 résultat · ${state.blockSize}/bloc`;
}
}
export function markStoreBlockFiltersDirty(
state: StoreBlockState,
controls: StoreBlockControls,
): void {
resetStoreBlockState(state);
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
if (firstButton !== null) {
firstButton.disabled = true;
}
if (previousButton !== null) {
previousButton.disabled = true;
}
if (nextButton !== null) {
nextButton.disabled = true;
}
if (summary !== null) {
summary.textContent = "Filtres modifiés — rechargez le premier bloc.";
}
}
export function cursorForBlock(state: StoreBlockState, blockIndex: number): string | null | undefined {
return state.cursors.get(blockIndex);
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_core.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlCoreButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreCoreButton")?.addEventListener("click", () => {
void refreshCore();
});
void refreshCore();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
// version: 5
// version: 6
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -50,10 +50,10 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store diagnostics demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlDiagButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreDiagButton")?.addEventListener("click", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#copyStoreDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
// version: 6
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlRawButton")?.addEventListener("click", () => {
document.querySelector<HTMLButtonElement>("#refreshStoreRawButton")?.addEventListener("click", () => {
void refreshRaw();
});
void refreshRaw();

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts
// version: 12
// version: 14
import * as bootstrap from "bootstrap";
import "simplebar";
@@ -10,12 +10,25 @@ import "datatables.net-select-bs5";
import "datatables.net-select-bs5/css/select.bootstrap5.css";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import {
applyStoreBlockMetadata,
createStoreBlockState,
cursorForBlock,
markStoreBlockFiltersDirty,
resetStoreBlockState,
renderStoreBlockControls,
type StoreBlockControls,
type StoreBlockState,
} from "./demo_store_block_pagination";
import type { DemoStoreReplayEntityPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityPagePayload";
import type { DemoStoreReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest";
import type { DemoStoreReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow";
import type { DemoStoreReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption";
import type { DemoStoreReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload";
import type { DemoStoreReplayProgramPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramPagePayload";
import type { DemoStoreReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest";
import type { DemoStoreReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow";
import type { DemoStoreReplayTransactionPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionPagePayload";
import type { DemoStoreReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest";
import type { DemoStoreReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow";
@@ -30,7 +43,7 @@ type EntityViewConfig = {
pluralLabel: string;
tableSelector: string;
containsSelector: string;
limitSelector: string;
blockControls: StoreBlockControls;
summarySelector: string;
loadButtonSelector: string;
resetButtonSelector: string;
@@ -48,7 +61,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "mints",
tableSelector: "#mintCandidateTable",
containsSelector: "#mintContainsInput",
limitSelector: "#mintLimitInput",
blockControls: {
firstSelector: "#mintFirstBlockButton",
previousSelector: "#mintPreviousBlockButton",
nextSelector: "#mintNextBlockButton",
summarySelector: "#mintBlockSummary",
},
summarySelector: "#mintResultSummary",
loadButtonSelector: "#loadMintsButton",
resetButtonSelector: "#resetMintsButton",
@@ -63,7 +81,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "owners",
tableSelector: "#ownerCandidateTable",
containsSelector: "#ownerContainsInput",
limitSelector: "#ownerLimitInput",
blockControls: {
firstSelector: "#ownerFirstBlockButton",
previousSelector: "#ownerPreviousBlockButton",
nextSelector: "#ownerNextBlockButton",
summarySelector: "#ownerBlockSummary",
},
summarySelector: "#ownerResultSummary",
loadButtonSelector: "#loadOwnersButton",
resetButtonSelector: "#resetOwnersButton",
@@ -78,7 +101,12 @@ const entityViewConfigs: EntityViewConfig[] = [
pluralLabel: "comptes",
tableSelector: "#accountCandidateTable",
containsSelector: "#accountContainsInput",
limitSelector: "#accountLimitInput",
blockControls: {
firstSelector: "#accountFirstBlockButton",
previousSelector: "#accountPreviousBlockButton",
nextSelector: "#accountNextBlockButton",
summarySelector: "#accountBlockSummary",
},
summarySelector: "#accountResultSummary",
loadButtonSelector: "#loadAccountsButton",
resetButtonSelector: "#resetAccountsButton",
@@ -94,7 +122,31 @@ let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
let accountTable: Api<DemoStoreReplayEntityRow> | null = null;
let busyOperationCount = 0;
let maximumReplayCandidateLimit = 500;
const transactionBlockControls: StoreBlockControls = {
firstSelector: "#transactionFirstBlockButton",
previousSelector: "#transactionPreviousBlockButton",
nextSelector: "#transactionNextBlockButton",
summarySelector: "#transactionBlockSummary",
};
const programBlockControls: StoreBlockControls = {
firstSelector: "#programFirstBlockButton",
previousSelector: "#programPreviousBlockButton",
nextSelector: "#programNextBlockButton",
summarySelector: "#programBlockSummary",
};
const transactionBlockState = createStoreBlockState();
const programBlockState = createStoreBlockState();
const entityBlockStates = new Map<EntityKindCode, StoreBlockState>([
["mint", createStoreBlockState()],
["owner", createStoreBlockState()],
["account_key", createStoreBlockState()],
]);
type TransactionBaseRequest = Omit<DemoStoreReplayTransactionRequest, "cursor" | "blockIndex">;
type ProgramBaseRequest = Omit<DemoStoreReplayProgramRequest, "cursor" | "blockIndex">;
type EntityBaseRequest = Omit<DemoStoreReplayEntityRequest, "cursor" | "blockIndex">;
let transactionBaseRequest: TransactionBaseRequest | null = null;
let programBaseRequest: ProgramBaseRequest | null = null;
const entityBaseRequests = new Map<EntityKindCode, EntityBaseRequest>();
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
@@ -117,24 +169,6 @@ function requiredInputValue(selector: string): string {
return value;
}
function positiveIntegerValue(selector: string): number {
const value = Number.parseInt(requiredInputValue(selector), 10);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`Valeur entière positive invalide pour ${selector}`);
}
return value;
}
function boundedPositiveIntegerValue(selector: string): number {
const value = positiveIntegerValue(selector);
if (value > maximumReplayCandidateLimit) {
throw new Error(
`La limite doit être comprise entre 1 et ${maximumReplayCandidateLimit} pour ${selector}`,
);
}
return value;
}
function optionalNonNegativeIntegerValue(selector: string): number | null {
const raw = optionalInputValue(selector);
if (raw === null) {
@@ -285,7 +319,7 @@ function createTransactionTable(): Api<DemoStoreReplayTransactionRow> {
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucune transaction chargée",
@@ -319,7 +353,7 @@ function createProgramTable(): Api<DemoStoreReplayProgramRow> {
],
order: [[3, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucun programme chargé",
@@ -352,7 +386,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoStoreReplayEntityR
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
lengthMenu: [10, 25, 50, 100, 250, 500],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
`Aucun ${config.label} chargé`,
@@ -412,6 +446,21 @@ function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`);
}
function restoreLoadedStoreBlockControls(): void {
if (transactionBlockState.currentBlock > 0) {
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
}
if (programBlockState.currentBlock > 0) {
renderStoreBlockControls(programBlockState, programBlockControls);
}
for (const config of entityViewConfigs) {
const state = entityBlockState(config.kind);
if (state.currentBlock > 0) {
renderStoreBlockControls(state, config.blockControls);
}
}
}
function setBusy(busy: boolean, message: string): void {
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
const operationInProgress = busyOperationCount > 0;
@@ -419,13 +468,20 @@ function setBusy(busy: boolean, message: string): void {
badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
if (button.id !== "openCoreExtractionButton") {
button.disabled = operationInProgress;
if (button.id === "openCoreExtractionButton") {
return;
}
if (!operationInProgress && button.dataset.storeBlockNavigation === "true") {
return;
}
button.disabled = operationInProgress;
});
if (!operationInProgress) {
restoreLoadedStoreBlockControls();
}
}
function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
function buildTransactionBaseRequest(): TransactionBaseRequest {
const entityKind = optionalInputValue("#transactionEntityKindSelect");
const entityValue = optionalInputValue("#transactionEntityValueInput");
if ((entityKind === null) !== (entityValue === null)) {
@@ -441,11 +497,31 @@ function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
programScope: requiredInputValue("#transactionProgramScopeSelect"),
entityKind,
entityValue,
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
};
}
function buildProgramBaseRequest(): ProgramBaseRequest {
return {
programIdContains: optionalInputValue("#programContainsInput"),
};
}
function buildEntityBaseRequest(config: EntityViewConfig): EntityBaseRequest {
return {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
};
}
function entityBlockState(kind: EntityKindCode): StoreBlockState {
const state = entityBlockStates.get(kind);
if (state === undefined) {
throw new Error(`État de pagination Store manquant pour ${kind}.`);
}
return state;
}
async function resetTransactionFilters(): Promise<void> {
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
@@ -456,95 +532,190 @@ async function resetTransactionFilters(): Promise<void> {
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
element<HTMLInputElement>("#transactionLimitInput").value = "500";
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
resetTableState(transactionTable);
await loadTransactions();
await startTransactionLoad();
}
async function resetProgramFilters(): Promise<void> {
element<HTMLSelectElement>("#knownProgramSelect").value = "";
element<HTMLInputElement>("#programContainsInput").value = "";
element<HTMLInputElement>("#programLimitInput").value = "500";
resetTableState(programTable);
await loadPrograms();
await startProgramLoad();
}
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
element<HTMLInputElement>(config.containsSelector).value = "";
element<HTMLInputElement>(config.limitSelector).value = "500";
resetTableState(entityTable(config.kind));
await loadEntity(config);
await startEntityLoad(config);
}
async function loadTransactions(): Promise<void> {
async function startTransactionLoad(): Promise<void> {
transactionBaseRequest = buildTransactionBaseRequest();
resetStoreBlockState(transactionBlockState);
await loadTransactionBlock(1, null);
}
async function loadTransactionBlock(blockIndex: number, cursor: string | null): Promise<void> {
if (transactionTable === null) {
return;
}
if (transactionBaseRequest === null) {
throw new Error("Rechargez les transactions pour initialiser les filtres Store.");
}
try {
setBusy(true, "Chargement transactions");
const request = buildTransactionRequest();
const rows = await invoke<DemoStoreReplayTransactionRow[]>("load_demo_store_replay_transactions", { request });
replaceRows(transactionTable, rows);
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis le store actif.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
const request: DemoStoreReplayTransactionRequest = {
...transactionBaseRequest,
cursor,
blockIndex,
};
const payload = await invoke<DemoStoreReplayTransactionPagePayload>(
"load_demo_store_replay_transactions",
{ request },
);
replaceRows(transactionTable, payload.rows);
applyStoreBlockMetadata(transactionBlockState, payload.page, transactionBlockControls);
element<HTMLElement>("#transactionResultSummary").textContent = `${payload.rows.length} transaction(s) dans le bloc Store courant.`;
frontendDebug(tracingTarget, `loaded replay transaction block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadPrograms(): Promise<void> {
async function startProgramLoad(): Promise<void> {
programBaseRequest = buildProgramBaseRequest();
resetStoreBlockState(programBlockState);
await loadProgramBlock(1, null);
}
async function loadProgramBlock(blockIndex: number, cursor: string | null): Promise<void> {
if (programTable === null) {
return;
}
if (programBaseRequest === null) {
throw new Error("Rechargez les programmes pour initialiser les filtres Store.");
}
try {
setBusy(true, "Chargement programmes");
const request: DemoStoreReplayProgramRequest = {
programIdContains: optionalInputValue("#programContainsInput"),
limit: boundedPositiveIntegerValue("#programLimitInput"),
...programBaseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoStoreReplayProgramRow[]>("load_demo_store_replay_programs", { request });
replaceRows(programTable, rows);
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences top-level, inner et logs liés.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
const payload = await invoke<DemoStoreReplayProgramPagePayload>(
"load_demo_store_replay_programs",
{ request },
);
replaceRows(programTable, payload.rows);
applyStoreBlockMetadata(programBlockState, payload.page, programBlockControls);
element<HTMLElement>("#programResultSummary").textContent = `${payload.rows.length} programme(s) dans le bloc Store courant, avec occurrences top-level, inner et logs liés.`;
frontendDebug(tracingTarget, `loaded replay program block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
renderStoreBlockControls(programBlockState, programBlockControls);
frontendError(tracingTarget, `program summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadEntity(config: EntityViewConfig): Promise<void> {
async function startEntityLoad(config: EntityViewConfig): Promise<void> {
entityBaseRequests.set(config.kind, buildEntityBaseRequest(config));
resetStoreBlockState(entityBlockState(config.kind));
await loadEntityBlock(config, 1, null);
}
async function loadEntityBlock(config: EntityViewConfig, blockIndex: number, cursor: string | null): Promise<void> {
const table = entityTable(config.kind);
if (table === null) {
return;
}
const baseRequest = entityBaseRequests.get(config.kind);
if (baseRequest === undefined) {
throw new Error(`Rechargez les ${config.pluralLabel} pour initialiser les filtres Store.`);
}
try {
setBusy(true, `Chargement ${config.pluralLabel}`);
const request: DemoStoreReplayEntityRequest = {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
limit: boundedPositiveIntegerValue(config.limitSelector),
...baseRequest,
cursor,
blockIndex,
};
const rows = await invoke<DemoStoreReplayEntityRow[]>("load_demo_store_replay_entities", { request });
replaceRows(table, rows);
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis le store actif.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
const payload = await invoke<DemoStoreReplayEntityPagePayload>(
"load_demo_store_replay_entities",
{ request },
);
replaceRows(table, payload.rows);
applyStoreBlockMetadata(entityBlockState(config.kind), payload.page, config.blockControls);
element<HTMLElement>(config.summarySelector).textContent = `${payload.rows.length} ${config.pluralLabel} dans le bloc Store courant.`;
frontendDebug(tracingTarget, `loaded replay ${config.kind} block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
renderStoreBlockControls(entityBlockState(config.kind), config.blockControls);
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadPreviousTransactionBlock(): Promise<void> {
const targetBlock = transactionBlockState.currentBlock - 1;
const cursor = cursorForBlock(transactionBlockState, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadTransactionBlock(targetBlock, cursor);
}
async function loadNextTransactionBlock(): Promise<void> {
if (transactionBlockState.nextCursor === null) {
return;
}
await loadTransactionBlock(transactionBlockState.currentBlock + 1, transactionBlockState.nextCursor);
}
async function loadPreviousProgramBlock(): Promise<void> {
const targetBlock = programBlockState.currentBlock - 1;
const cursor = cursorForBlock(programBlockState, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadProgramBlock(targetBlock, cursor);
}
async function loadNextProgramBlock(): Promise<void> {
if (programBlockState.nextCursor === null) {
return;
}
await loadProgramBlock(programBlockState.currentBlock + 1, programBlockState.nextCursor);
}
async function loadPreviousEntityBlock(config: EntityViewConfig): Promise<void> {
const state = entityBlockState(config.kind);
const targetBlock = state.currentBlock - 1;
const cursor = cursorForBlock(state, targetBlock);
if (targetBlock < 1 || cursor === undefined) {
return;
}
await loadEntityBlock(config, targetBlock, cursor);
}
async function loadNextEntityBlock(config: EntityViewConfig): Promise<void> {
const state = entityBlockState(config.kind);
if (state.nextCursor === null) {
return;
}
await loadEntityBlock(config, state.currentBlock + 1, state.nextCursor);
}
async function copyLines(lines: string[], label: string): Promise<void> {
if (lines.length === 0) {
throw new Error(`Aucune ${label} à copier.`);
@@ -635,7 +806,7 @@ async function useSelectedProgram(): Promise<void> {
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
showTransactionsTab();
await loadTransactions();
await startTransactionLoad();
}
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
@@ -647,7 +818,7 @@ async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
showTransactionsTab();
await loadTransactions();
await startTransactionLoad();
}
async function openCoreExtraction(): Promise<void> {
@@ -661,16 +832,12 @@ async function openCoreExtraction(): Promise<void> {
async function loadOptions(): Promise<void> {
const options = await invoke<DemoStoreReplayOptionsPayload>("demo_store_replay_options");
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
if (!Number.isSafeInteger(options.blockSize) || options.blockSize !== 500) {
throw new Error("La taille de bloc Store retournée par le backend est invalide.");
}
maximumReplayCandidateLimit = options.maximumLimit;
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
input.max = String(options.maximumLimit);
});
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
populateKnownPrograms(options.knownPrograms);
}
@@ -729,14 +896,20 @@ function reportUiError(action: string, caughtError: unknown): void {
}
function installHandlers(): void {
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad());
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
});
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
element<HTMLButtonElement>(transactionBlockControls.firstSelector).addEventListener("click", () => void loadTransactionBlock(1, null));
element<HTMLButtonElement>(transactionBlockControls.previousSelector).addEventListener("click", () => void loadPreviousTransactionBlock());
element<HTMLButtonElement>(transactionBlockControls.nextSelector).addEventListener("click", () => void loadNextTransactionBlock());
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void startProgramLoad());
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
});
element<HTMLButtonElement>(programBlockControls.firstSelector).addEventListener("click", () => void loadProgramBlock(1, null));
element<HTMLButtonElement>(programBlockControls.previousSelector).addEventListener("click", () => void loadPreviousProgramBlock());
element<HTMLButtonElement>(programBlockControls.nextSelector).addEventListener("click", () => void loadNextProgramBlock());
element<HTMLButtonElement>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
@@ -754,10 +927,13 @@ function installHandlers(): void {
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
});
for (const config of entityViewConfigs) {
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void loadEntity(config));
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void startEntityLoad(config));
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
});
element<HTMLButtonElement>(config.blockControls.firstSelector).addEventListener("click", () => void loadEntityBlock(config, 1, null));
element<HTMLButtonElement>(config.blockControls.previousSelector).addEventListener("click", () => void loadPreviousEntityBlock(config));
element<HTMLButtonElement>(config.blockControls.nextSelector).addEventListener("click", () => void loadNextEntityBlock(config));
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
});
@@ -767,16 +943,49 @@ function installHandlers(): void {
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
});
element<HTMLInputElement>(config.containsSelector).addEventListener("input", () => {
entityBaseRequests.delete(config.kind);
markStoreBlockFiltersDirty(entityBlockState(config.kind), config.blockControls);
});
}
const transactionFilterSelectors = [
"#transactionSignatureContainsInput",
"#transactionMinSlotInput",
"#transactionMaxSlotInput",
"#transactionRawStateSelect",
"#transactionLedgerStatusSelect",
"#transactionProgramIdInput",
"#transactionProgramScopeSelect",
"#transactionEntityKindSelect",
"#transactionEntityValueInput",
"#transactionNewestFirstInput",
];
for (const selector of transactionFilterSelectors) {
element<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("change", () => {
transactionBaseRequest = null;
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
});
element<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("input", () => {
transactionBaseRequest = null;
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
});
}
element<HTMLInputElement>("#programContainsInput").addEventListener("input", () => {
programBaseRequest = null;
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
});
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
element<HTMLInputElement>("#programContainsInput").value = target.value;
programBaseRequest = null;
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
});
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
button.addEventListener("shown.bs.tab", adjustVisibleTables);
});
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge(tracingTarget);
frontendDebug(tracingTarget, "Store replay candidate browser loaded");
@@ -790,11 +999,11 @@ document.addEventListener("DOMContentLoaded", () => {
installInlineCopyHandler();
void loadOptions()
.then(() => Promise.all([
loadTransactions(),
loadPrograms(),
loadEntity(entityViewConfigs[0]),
loadEntity(entityViewConfigs[1]),
loadEntity(entityViewConfigs[2]),
startTransactionLoad(),
startProgramLoad(),
startEntityLoad(entityViewConfigs[0]),
startEntityLoad(entityViewConfigs[1]),
startEntityLoad(entityViewConfigs[2]),
]))
.catch(caughtError => reportUiError("initial loading", caughtError));
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/app_state.rs
// version: 20
// version: 21
//! Shared Tauri application state and startup initialization.
@@ -18,6 +18,12 @@ pub(crate) struct AppState {
logging_guard: std::sync::Mutex<ks_logging::LoggingGuard>,
http_pool: ks_onchain_transport::HttpEndpointPool,
store: tokio::sync::OnceCell<ks_store::Store>,
profile_stores: tokio::sync::Mutex<
std::collections::BTreeMap<
std::string::String,
std::sync::Arc<tokio::sync::OnceCell<ks_store::Store>>,
>,
>,
ws_pool:
std::sync::Mutex<std::option::Option<std::sync::Arc<ks_onchain_transport::WsEndpointPool>>>,
demo_ws_session:
@@ -108,6 +114,7 @@ impl crate::AppState {
logging_guard: std::sync::Mutex::new(logging_guard),
http_pool,
store: tokio::sync::OnceCell::new(),
profile_stores: tokio::sync::Mutex::new(std::collections::BTreeMap::new()),
ws_pool: std::sync::Mutex::new(std::option::Option::None),
demo_ws_session: tokio::sync::Mutex::new(std::option::Option::None),
demo_backfill_running: std::sync::atomic::AtomicBool::new(false),
@@ -195,6 +202,63 @@ impl crate::AppState {
.await;
}
/// Returns a shared persistent store for one immutable configured profile.
pub(crate) async fn store_for_profile(
&self,
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<ks_store::Store> {
let configured_profile = self
.app_config
.profiles
.iter()
.find(|candidate| return candidate.name == profile.name);
let configured_database = match configured_profile {
std::option::Option::Some(value) => value.database.clone(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::invalid_state(
"store profile is not part of the resolved application configuration",
));
},
};
if configured_database != profile.database {
return std::result::Result::Err(ks_core::Error::invalid_state(
"store profile database configuration mismatch",
));
}
if profile.name == self.active_profile.name {
let store_result = self.store().await;
return match store_result {
std::result::Result::Ok(store) => std::result::Result::Ok(store.clone()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
let cell = {
let mut stores = self.profile_stores.lock().await;
if let std::option::Option::Some(existing) = stores.get(profile.name.as_str()) {
std::sync::Arc::clone(existing)
} else {
let created = std::sync::Arc::new(tokio::sync::OnceCell::new());
stores.insert(profile.name.clone(), std::sync::Arc::clone(&created));
created
}
};
let options = match ks_store::StoreOpenOptions::new(
configured_database.enabled,
configured_database.backend,
configured_database.backend_options,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_result = cell
.get_or_try_init(|| return async move { return ks_store::Store::open(options).await })
.await;
return match store_result {
std::result::Result::Ok(store) => std::result::Result::Ok(store.clone()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Returns the session-only execution-wallet alias override for one profile.
pub(crate) fn demo_execution_wallet_alias_override(
&self,

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_decode_replay.rs
// version: 45
// version: 46
//! Tauri commands and UI payloads for contextual instruction decode replay.
@@ -271,8 +271,10 @@ pub(crate) struct DemoDecodeDiagnosticsPayload {
pub(crate) struct DemoTransactionAnnotationRequest {
/// Optional partial transaction signature.
pub(crate) signature_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
}
/// UI-safe committed transaction annotation row.
@@ -313,6 +315,20 @@ pub(crate) struct DemoTransactionAnnotationRow {
pub(crate) updated_at: std::string::String,
}
/// One counted Store block of committed transaction annotations.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationPagePayload.ts"
)]
pub(crate) struct DemoTransactionAnnotationPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Annotations in the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoTransactionAnnotationRow>,
}
pub(crate) struct DemoDecodeReplayObserver<'a> {
pub(crate) app_handle: tauri::AppHandle,
pub(crate) campaign_id: std::string::String,
@@ -604,39 +620,49 @@ pub(crate) async fn demo_decode_replay_diagnostics(
pub(crate) async fn demo_decode_replay_annotations(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoTransactionAnnotationRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoTransactionAnnotationRow>, std::string::String> {
let filter_result = ks_store::MaterializedOutputFilter::new(
) -> std::result::Result<crate::DemoTransactionAnnotationPagePayload, std::string::String> {
let filter = ks_store::MaterializedOutputPageFilter::new(
std::option::Option::Some("materializer.transaction.annotations".to_string()),
std::option::Option::Some("transaction_annotation".to_string()),
std::option::Option::Some("spl.memo".to_string()),
request.signature_contains,
request.limit,
std::vec::Vec::new(),
);
let filter = match filter_result {
let page_request_result = crate::demo_store_block_request(request.cursor);
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", signature_contains = ?filter.signature_contains, limit = filter.limit, "load bounded committed transaction annotation journal");
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", block_index = request.block_index, "load committed transaction annotation Store block");
let store_result = crate::shared_store(state.inner()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result =
ks_store::DecodePipelineStore::list_materialized_outputs(&store, &filter).await;
let rows = match rows_result {
let page_result = store.materialized_output_page(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut rows = std::vec::Vec::with_capacity(page.rows.len());
for row in page.rows {
let mapped = annotation_payload(row);
match mapped {
std::result::Result::Ok(value) => output.push(value),
std::result::Result::Ok(value) => rows.push(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", row_count = output.len(), "bounded committed transaction annotation journal loaded");
return std::result::Result::Ok(output);
tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", row_count = rows.len(), total_rows = %metadata.total_rows, block_index = metadata.block_index, total_blocks = metadata.total_blocks, "committed transaction annotation Store block loaded");
return std::result::Result::Ok(crate::DemoTransactionAnnotationPagePayload {
page: metadata,
rows,
});
}
fn register_active_campaign(

View File

@@ -1,7 +1,7 @@
// file: kb-app-demo-desktop/src/demo_devnet_common.rs
// version: 7
// version: 9
//! Shared Devnet demo UI contracts.
//! Shared Devnet demo UI contracts and profile/store helpers.
use ts_rs::TS; // rust-rules: derive-import
@@ -31,8 +31,57 @@ pub(crate) struct DemoExecutionDevnetStoreReadinessPayload {
pub(crate) expected_resources: u32,
}
/// Resolves one explicitly selected compatible Devnet profile.
pub(crate) fn select_devnet_profile(
config: &ks_config::AppConfig,
profile_name: &str,
) -> std::result::Result<ks_config::ProfileConfig, std::string::String> {
return ks_pipeline_demo_scenarios::resolve_demo_devnet_profile(
config,
std::option::Option::Some(profile_name),
)
.map_err(|error| return error.to_string());
}
/// Verifies or initializes the configured store selected by one Devnet profile.
pub(crate) async fn demo_execution_devnet_prepare_profile(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
) -> std::result::Result<DemoExecutionDevnetStoreReadinessPayload, std::string::String> {
let profile = match select_devnet_profile(state.app_config(), profile_name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let configuration = store.configuration_summary();
let initialization = store.initialization_summary();
if initialization.status != ks_store::StoreInitializationStatus::Ready {
return std::result::Result::Err(format!(
"Devnet profile '{}' store model is incomplete and automatic initialization is {}",
profile.name,
if configuration.auto_initialize_schema { "enabled" } else { "disabled" }
));
}
let readiness = ks_pipeline_demo_scenarios::DevnetProfileStoreReadiness {
profile_name: profile.name.clone(),
backend: configuration.backend_code.clone(),
auto_initialize_schema: configuration.auto_initialize_schema,
available_resources_before: initialization
.available_resource_count
.saturating_sub(initialization.created_resource_count)
as usize,
created_resources: initialization.created_resource_count as usize,
available_resources_after: initialization.available_resource_count as usize,
expected_resources: initialization.expected_resource_count as usize,
};
return std::result::Result::Ok(devnet_store_readiness_payload(readiness));
}
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
pub(crate) fn devnet_store_readiness_payload(
fn devnet_store_readiness_payload(
readiness: ks_pipeline_demo_scenarios::DevnetProfileStoreReadiness,
) -> DemoExecutionDevnetStoreReadinessPayload {
return DemoExecutionDevnetStoreReadinessPayload {

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs
// version: 13
// version: 14
//! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows.
@@ -190,7 +190,7 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
@@ -1041,7 +1041,7 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs
// version: 7
// version: 8
//! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign.
@@ -88,7 +88,7 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
// version: 23
// version: 26
//! Tauri adapter for bounded Solana Core execution on Devnet.
@@ -359,17 +359,6 @@ impl ks_pipeline_demo_scenarios::SolanaExecutionObserver
}
}
pub(crate) fn select_devnet_profile(
config: &ks_config::AppConfig,
profile_name: &str,
) -> std::result::Result<ks_config::ProfileConfig, std::string::String> {
return ks_pipeline_demo_scenarios::resolve_demo_devnet_profile(
config,
std::option::Option::Some(profile_name),
)
.map_err(|error| return error.to_string());
}
pub(crate) fn execution_cluster_code(
cluster: ks_lib::ExApiExecutionCluster,
) -> std::string::String {
@@ -394,25 +383,6 @@ pub(crate) fn confirmation_status_code(
};
}
/// Verifies or initializes the configured store selected by one Devnet profile.
pub(crate) async fn demo_execution_devnet_prepare_profile(
state: tauri::State<'_, crate::AppState>,
profile_name: std::string::String,
) -> std::result::Result<crate::DemoExecutionDevnetStoreReadinessPayload, std::string::String> {
let profile = match crate::select_devnet_profile(state.app_config(), profile_name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let readiness =
match ks_pipeline_demo_scenarios::prepare_demo_devnet_profile_store(&profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(error.to_string());
},
};
return std::result::Result::Ok(crate::devnet_store_readiness_payload(readiness));
}
/// Returns compatible Devnet profiles and conservative defaults.
pub(crate) fn demo_execution_solana_core_options(
state: tauri::State<'_, crate::AppState>,
@@ -473,7 +443,7 @@ pub(crate) async fn demo_execution_solana_core_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
@@ -557,7 +527,7 @@ pub(crate) async fn demo_execution_spl_memo_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
// version: 19
// version: 21
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
@@ -129,8 +129,24 @@ pub(crate) struct DemoSplAtaJournalRequest {
pub(crate) associated_token_account: std::option::Option<std::string::String>,
/// Optional exact materialized operation.
pub(crate) operation: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
}
/// One counted Store block of ATA lifecycle journal rows.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalPagePayload.ts"
)]
pub(crate) struct DemoSplAtaJournalPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Typed rows retained from the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoSplAtaJournalRow>,
}
/// One UI-safe ATA lifecycle journal row.
@@ -260,7 +276,7 @@ pub(crate) async fn demo_execution_spl_ata_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
@@ -303,46 +319,54 @@ pub(crate) async fn demo_execution_spl_ata_execute(
pub(crate) async fn demo_spl_ata_journal(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSplAtaJournalRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSplAtaJournalRow>, std::string::String> {
if request.limit == 0 || request.limit > 500 {
return std::result::Result::Err("ATA journal limit must be between 1 and 500".to_string());
}
) -> std::result::Result<crate::DemoSplAtaJournalPagePayload, std::string::String> {
let profile =
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let filter = match ks_store::MaterializedOutputFilter::new(
let mut payload_match_groups = std::vec::Vec::new();
if let std::option::Option::Some(operation) = request.operation.as_ref() {
payload_match_groups.push(vec![serde_json::json!({"operation": operation})]);
}
if let std::option::Option::Some(mint) = request.mint.as_ref() {
payload_match_groups.push(vec![serde_json::json!({"mint": mint})]);
}
if let std::option::Option::Some(associated_token_account) =
request.associated_token_account.as_ref()
{
payload_match_groups
.push(vec![serde_json::json!({"associatedTokenAccount": associated_token_account})]);
}
let filter = ks_store::MaterializedOutputPageFilter::new(
std::option::Option::Some("materializer.token.accounts".to_string()),
std::option::Option::None,
std::option::Option::Some("spl.associated_token_account".to_string()),
request.signature_contains.clone(),
500,
) {
payload_match_groups,
);
let page_request_result = crate::demo_store_block_request(request.cursor.clone());
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let page_result = store.materialized_output_page(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let rows = match ks_store::DecodePipelineStore::list_materialized_outputs(&store, &filter).await
{
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut output = std::vec::Vec::new();
for row in rows {
if row.source_decoder_name != "spl.associated_token_account"
|| !journal_matches(&row.payload_json, &request)
{
continue;
}
output.push(journal_row(row));
if output.len() >= request.limit as usize {
break;
}
}
return std::result::Result::Ok(output);
let rows = page.rows.into_iter().map(journal_row).collect();
return std::result::Result::Ok(crate::DemoSplAtaJournalPagePayload { page: metadata, rows });
}
async fn load_profile_wallet(
@@ -544,6 +568,7 @@ fn summary_payload(
};
}
#[cfg(test)]
fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJournalRequest) -> bool {
for (expected, key) in [
(&request.operation, "operation"),
@@ -605,7 +630,8 @@ mod tests {
mint: std::option::Option::Some("mint111".to_string()),
associated_token_account: std::option::Option::Some("ata111".to_string()),
operation: std::option::Option::Some("create_idempotent".to_string()),
limit: 100,
cursor: std::option::Option::None,
block_index: 1,
};
assert!(super::journal_matches(
&serde_json::json!({

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token.rs
// version: 17
// version: 19
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
@@ -111,8 +111,24 @@ pub(crate) struct DemoSplTokenJournalRequest {
pub(crate) family: std::option::Option<std::string::String>,
/// Optional exact operation code without the `spl.token.` prefix.
pub(crate) operation: std::option::Option<std::string::String>,
/// Maximum number of rows returned after typed filtering.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
}
/// One counted Store block of classic SPL Token journal rows.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalPagePayload.ts"
)]
pub(crate) struct DemoSplTokenJournalPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Typed rows retained from the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoSplTokenJournalRow>,
}
/// UI-safe materialized classic SPL Token journal row.
@@ -183,7 +199,7 @@ pub(crate) async fn demo_execution_spl_token_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
@@ -246,7 +262,7 @@ pub(crate) async fn demo_execution_spl_token_execute(
pub(crate) async fn demo_spl_token_journal(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSplTokenJournalRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSplTokenJournalRow>, std::string::String> {
) -> std::result::Result<crate::DemoSplTokenJournalPagePayload, std::string::String> {
let validated = match validate_journal_request(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -256,43 +272,48 @@ pub(crate) async fn demo_spl_token_journal(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let needs_payload_filter =
validated.mint.is_some() || validated.account.is_some() || validated.operation.is_some();
let query_limit = if needs_payload_filter {
ks_store::MAX_MATERIALIZED_OUTPUT_QUERY_ROWS
} else {
validated.limit
};
let filter = match ks_store::MaterializedOutputFilter::new(
let mut payload_match_groups = std::vec::Vec::new();
if let std::option::Option::Some(operation) = validated.operation.as_ref() {
payload_match_groups.push(vec![serde_json::json!({"operation": operation})]);
}
if let std::option::Option::Some(mint) = validated.mint.as_ref() {
payload_match_groups.push(vec![
serde_json::json!({"mint": mint}),
serde_json::json!({"accounts": [{"role": "mint", "accountKey": mint}]}),
]);
}
if let std::option::Option::Some(account) = validated.account.as_ref() {
payload_match_groups.push(vec![serde_json::json!({"accounts": [{"accountKey": account}]})]);
}
let filter = ks_store::MaterializedOutputPageFilter::new(
std::option::Option::None,
validated.family.clone(),
std::option::Option::Some("spl.token".to_string()),
validated.signature_contains.clone(),
query_limit,
) {
payload_match_groups,
);
let page_request_result = crate::demo_store_block_request(validated.cursor.clone());
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let page_result = store.materialized_output_page(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let rows = match ks_store::DecodePipelineStore::list_materialized_outputs(&store, &filter).await
{
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, validated.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut output = std::vec::Vec::new();
for row in rows {
if row.source_decoder_name != "spl.token" || !payload_matches(&row.payload_json, &validated)
{
continue;
}
output.push(journal_row(row));
if output.len() >= validated.limit as usize {
break;
}
}
return std::result::Result::Ok(output);
let rows = page.rows.into_iter().map(journal_row).collect();
return std::result::Result::Ok(crate::DemoSplTokenJournalPagePayload { page: metadata, rows });
}
fn summary_payload(
@@ -412,12 +433,6 @@ fn replay_diagnostics(summary: &ks_pipeline::DecodeReplaySummary) -> serde_json:
fn validate_journal_request(
request: crate::DemoSplTokenJournalRequest,
) -> std::result::Result<crate::DemoSplTokenJournalRequest, std::string::String> {
if request.limit == 0 || request.limit > ks_store::MAX_MATERIALIZED_OUTPUT_QUERY_ROWS {
return std::result::Result::Err(format!(
"journal limit must be between 1 and {}",
ks_store::MAX_MATERIALIZED_OUTPUT_QUERY_ROWS
));
}
if request.profile_name.trim().is_empty() {
return std::result::Result::Err("journal profile name must not be empty".to_string());
}
@@ -456,7 +471,8 @@ fn validate_journal_request(
account,
family,
operation,
limit: request.limit,
cursor: request.cursor,
block_index: request.block_index,
});
}
@@ -473,6 +489,7 @@ fn bounded_optional(
return std::result::Result::Ok(trimmed);
}
#[cfg(test)]
fn payload_matches(
payload: &serde_json::Value,
request: &crate::DemoSplTokenJournalRequest,
@@ -586,7 +603,8 @@ mod tests {
account: std::option::Option::Some("source111".to_string()),
family: std::option::Option::Some("token_account".to_string()),
operation: std::option::Option::Some("transfer_checked".to_string()),
limit: 25,
cursor: std::option::Option::None,
block_index: 1,
};
let payload = serde_json::json!({
"operation": "transfer_checked",
@@ -622,7 +640,7 @@ mod tests {
}
#[test]
fn journal_rejects_unbounded_or_unknown_family_requests() {
fn journal_rejects_unknown_family_requests() {
let request = crate::DemoSplTokenJournalRequest {
profile_name: "local_devnet".to_string(),
signature_contains: std::option::Option::None,
@@ -630,7 +648,8 @@ mod tests {
account: std::option::Option::None,
family: std::option::Option::Some("fee".to_string()),
operation: std::option::Option::None,
limit: 501,
cursor: std::option::Option::None,
block_index: 1,
};
assert!(super::validate_journal_request(request).is_err());
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_spl_token_2022.rs
// version: 20
// version: 21
//! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios.
@@ -159,7 +159,7 @@ pub(crate) async fn demo_execution_spl_token_2022_execute(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let store = match crate::open_store(&profile).await {
let store = match crate::shared_store_for_profile(state.inner(), &profile).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};

View File

@@ -1,10 +1,78 @@
// file: kb-app-demo-desktop/src/demo_store_common.rs
// version: 19
// version: 21
//! Shared backend-agnostic store demo helpers and serializable payloads.
use ts_rs::TS; // rust-rules: derive-import
/// Pagination metadata shared by desktop Store block listings.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store/DemoStoreBlockMetadata.ts"
)]
pub(crate) struct DemoStoreBlockMetadata {
/// Fixed number of backend rows requested per Store block.
pub(crate) block_size: u16,
/// One-based block index, or zero when the filtered result set is empty.
pub(crate) block_index: u32,
/// Total matching rows rendered losslessly for the WebView.
pub(crate) total_rows: std::string::String,
/// Total number of fixed Store blocks.
pub(crate) total_blocks: u32,
/// Opaque cursor for the next Store block when one exists.
pub(crate) next_cursor: std::option::Option<std::string::String>,
}
/// Fixed backend block size used by desktop Store listings.
pub(crate) const DEMO_STORE_BLOCK_SIZE: u16 = ks_store::MAX_PAGE_SIZE;
/// Builds one fixed-size Store block request.
pub(crate) fn demo_store_block_request(
cursor: std::option::Option<std::string::String>,
) -> std::result::Result<ks_store::PageRequest, std::string::String> {
let request_result = ks_store::PageRequest::new(crate::DEMO_STORE_BLOCK_SIZE, cursor);
return match request_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Calculates refreshed desktop block metadata from one counted Store page.
pub(crate) fn demo_store_block_metadata(
total_rows: u64,
next_cursor: std::option::Option<std::string::String>,
requested_block_index: u32,
) -> std::result::Result<crate::DemoStoreBlockMetadata, std::string::String> {
if requested_block_index == 0 {
return std::result::Result::Err("Store block index must be greater than zero".to_string());
}
let block_size = u64::from(crate::DEMO_STORE_BLOCK_SIZE);
let total_blocks_u64 = if total_rows == 0 { 0 } else { ((total_rows - 1) / block_size) + 1 };
let total_blocks_result = u32::try_from(total_blocks_u64);
let total_blocks = match total_blocks_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_error) => {
return std::result::Result::Err(
"Store block count exceeds desktop bounds".to_string(),
);
},
};
if total_blocks > 0 && requested_block_index > total_blocks {
return std::result::Result::Err(format!(
"Store block {requested_block_index} no longer exists; reload the first block"
));
}
return std::result::Result::Ok(crate::DemoStoreBlockMetadata {
block_size: crate::DEMO_STORE_BLOCK_SIZE,
block_index: if total_rows == 0 { 0 } else { requested_block_index },
total_rows: total_rows.to_string(),
total_blocks,
next_cursor,
});
}
/// UI-safe diagnostics for one logical store resource.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -34,33 +102,6 @@ pub(crate) struct DemoStoreResourceSnapshot {
pub(crate) latest_created_at: std::option::Option<std::string::String>,
}
/// Builds backend-agnostic store-open options from one resolved profile.
fn store_open_options_from_profile(
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<ks_store::StoreOpenOptions> {
return ks_store::StoreOpenOptions::new(
profile.database.enabled,
profile.database.backend.clone(),
profile.database.backend_options.clone(),
);
}
/// Opens a backend-agnostic store from one resolved profile.
///
/// Prefer [`crate::AppState::store`] for desktop commands that already own application state.
pub(crate) async fn open_store(
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_store::Store, std::string::String> {
let options = match store_open_options_from_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
return match ks_store::Store::open(options).await {
std::result::Result::Ok(store) => std::result::Result::Ok(store),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Clones the persistent store held by application state.
pub(crate) async fn shared_store(
state: &crate::AppState,
@@ -72,6 +113,18 @@ pub(crate) async fn shared_store(
};
}
/// Clones the shared persistent store held for one configured profile.
pub(crate) async fn shared_store_for_profile(
state: &crate::AppState,
profile: &ks_config::ProfileConfig,
) -> std::result::Result<ks_store::Store, std::string::String> {
let store_result = state.store_for_profile(profile).await;
return match store_result {
std::result::Result::Ok(store) => std::result::Result::Ok(store),
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
};
}
/// Converts one logical store resource diagnostic to the UI payload shape.
pub(crate) fn store_resource_snapshot(
value: &ks_store::StoreResourceDiagnostics,
@@ -262,3 +315,22 @@ fn emit_store_startup_splash(
std::option::Option::None,
);
}
#[cfg(test)]
mod tests {
#[test]
fn fixed_store_block_metadata_rounds_up_and_keeps_empty_zero() {
let first_result = crate::demo_store_block_metadata(
1749,
std::option::Option::Some("next".to_string()),
1,
);
let first = match first_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("unexpected block metadata error: {error}"),
};
assert_eq!(first.total_blocks, 4);
assert_eq!(first.total_rows, "1749");
assert_eq!(first.block_size, 500);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/demo_store_replay_candidates.rs
// version: 16
// version: 17
//! Backend-agnostic read-only replay candidate browser commands.
@@ -31,8 +31,8 @@ pub(crate) struct DemoStoreReplayOptionsPayload {
pub(crate) active_profile_name: std::string::String,
/// Safe backend connection descriptor.
pub(crate) connection_descriptor: std::string::String,
/// Maximum rows accepted by one query.
pub(crate) maximum_limit: u32,
/// Fixed Store block size used by every replay-candidate listing.
pub(crate) block_size: u16,
/// Program identifiers enumerable from `ks_program_ids`.
pub(crate) known_programs: std::vec::Vec<crate::DemoStoreReplayKnownProgramOption>,
}
@@ -65,8 +65,10 @@ pub(crate) struct DemoStoreReplayTransactionRequest {
pub(crate) entity_kind: std::option::Option<std::string::String>,
/// Optional exact entity value.
pub(crate) entity_value: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
/// Orders newest slots first when true.
pub(crate) newest_first: bool,
}
@@ -114,6 +116,20 @@ pub(crate) struct DemoStoreReplayTransactionRow {
pub(crate) updated_at: std::string::String,
}
/// One counted Store block of transaction replay candidates.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionPagePayload.ts"
)]
pub(crate) struct DemoStoreReplayTransactionPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Rows in the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoStoreReplayTransactionRow>,
}
/// UI request for bounded program summaries.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -124,8 +140,10 @@ pub(crate) struct DemoStoreReplayTransactionRow {
pub(crate) struct DemoStoreReplayProgramRequest {
/// Optional partial program id search.
pub(crate) program_id_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
}
/// Aggregated program row shown by the replay candidate browser.
@@ -160,6 +178,20 @@ pub(crate) struct DemoStoreReplayProgramRow {
pub(crate) max_slot: i64,
}
/// One counted Store block of program summaries.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramPagePayload.ts"
)]
pub(crate) struct DemoStoreReplayProgramPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Rows in the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoStoreReplayProgramRow>,
}
/// UI request for bounded core entity summaries.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -172,8 +204,10 @@ pub(crate) struct DemoStoreReplayEntityRequest {
pub(crate) entity_kind: std::string::String,
/// Optional partial entity value search.
pub(crate) entity_value_contains: std::option::Option<std::string::String>,
/// Maximum returned rows.
pub(crate) limit: u32,
/// Opaque cursor identifying the requested Store block.
pub(crate) cursor: std::option::Option<std::string::String>,
/// One-based Store block index maintained by the desktop navigation.
pub(crate) block_index: u32,
}
/// Aggregated core entity row shown by the replay candidate browser.
@@ -202,6 +236,20 @@ pub(crate) struct DemoStoreReplayEntityRow {
pub(crate) max_slot: i64,
}
/// One counted Store block of mint, owner or account-key summaries.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityPagePayload.ts"
)]
pub(crate) struct DemoStoreReplayEntityPagePayload {
/// Shared block metadata refreshed with this load.
pub(crate) page: crate::DemoStoreBlockMetadata,
/// Rows in the current Store block.
pub(crate) rows: std::vec::Vec<crate::DemoStoreReplayEntityRow>,
}
/// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`.
pub(crate) async fn export_demo_store_replay_csv(
file_name: std::string::String,
@@ -264,7 +312,7 @@ pub(crate) async fn demo_store_replay_options(
return std::result::Result::Ok(crate::DemoStoreReplayOptionsPayload {
active_profile_name: profile.name,
connection_descriptor,
maximum_limit: ks_store::MAX_REPLAY_CANDIDATE_ROWS,
block_size: crate::DEMO_STORE_BLOCK_SIZE,
known_programs,
});
}
@@ -273,7 +321,7 @@ pub(crate) async fn demo_store_replay_options(
pub(crate) async fn load_demo_store_replay_transactions(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayTransactionRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayTransactionRow>, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayTransactionPagePayload, std::string::String> {
let program_scope_result = program_scope_from_code(request.program_scope.as_str());
let program_scope = match program_scope_result {
std::result::Result::Ok(value) => value,
@@ -294,95 +342,132 @@ pub(crate) async fn load_demo_store_replay_transactions(
program_scope,
entity_kind,
request.entity_value,
request.limit,
request.newest_first,
);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let page_request_result = crate::demo_store_block_request(request.cursor);
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_result = crate::shared_store(state.inner()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_transaction_candidates(&filter).await;
let rows = match rows_result {
let page_result = store.replay_transaction_candidates(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay transaction candidates");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(transaction_row_from_store(row));
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut rows = std::vec::Vec::with_capacity(page.rows.len());
for row in page.rows {
rows.push(transaction_row_from_store(row));
}
return std::result::Result::Ok(output);
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), total_rows = %metadata.total_rows, block_index = metadata.block_index, total_blocks = metadata.total_blocks, "loaded replay transaction candidate block");
return std::result::Result::Ok(crate::DemoStoreReplayTransactionPagePayload {
page: metadata,
rows,
});
}
/// Loads bounded program summaries from the active store.
/// Loads one counted Store block of program summaries.
pub(crate) async fn load_demo_store_replay_programs(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayProgramRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayProgramRow>, std::string::String> {
let filter_result =
ks_store::ReplayProgramFilter::new(request.program_id_contains, request.limit);
) -> std::result::Result<crate::DemoStoreReplayProgramPagePayload, std::string::String> {
let filter_result = ks_store::ReplayProgramFilter::new(request.program_id_contains);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let page_request_result = crate::demo_store_block_request(request.cursor);
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_result = crate::shared_store(state.inner()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_program_summaries(&filter).await;
let rows = match rows_result {
let page_result = store.replay_program_summaries(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay program summaries");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(program_row_from_store(row));
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut rows = std::vec::Vec::with_capacity(page.rows.len());
for row in page.rows {
rows.push(program_row_from_store(row));
}
return std::result::Result::Ok(output);
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), total_rows = %metadata.total_rows, block_index = metadata.block_index, total_blocks = metadata.total_blocks, "loaded replay program summary block");
return std::result::Result::Ok(crate::DemoStoreReplayProgramPagePayload {
page: metadata,
rows,
});
}
/// Loads bounded mint, owner or account-key summaries from Core facts.
/// Loads one counted Store block of mint, owner or account-key summaries.
pub(crate) async fn load_demo_store_replay_entities(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayEntityRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayEntityRow>, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayEntityPagePayload, std::string::String> {
let entity_kind_result = entity_kind_from_code(request.entity_kind.as_str());
let entity_kind = match entity_kind_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let filter_result = ks_store::ReplayEntityFilter::new(
entity_kind,
request.entity_value_contains,
request.limit,
);
let filter_result =
ks_store::ReplayEntityFilter::new(entity_kind, request.entity_value_contains);
let filter = match filter_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
let page_request_result = crate::demo_store_block_request(request.cursor);
let page_request = match page_request_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_result = crate::shared_store(state.inner()).await;
let store = match store_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows_result = store.replay_entity_summaries(&filter).await;
let rows = match rows_result {
let page_result = store.replay_entity_summaries(&filter, &page_request).await;
let page = match page_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
};
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay entity summaries");
let mut output = std::vec::Vec::with_capacity(rows.len());
for row in rows {
output.push(entity_row_from_store(row));
let metadata_result =
crate::demo_store_block_metadata(page.total_rows, page.next_cursor, request.block_index);
let metadata = match metadata_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut rows = std::vec::Vec::with_capacity(page.rows.len());
for row in page.rows {
rows.push(entity_row_from_store(row));
}
return std::result::Result::Ok(output);
tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), total_rows = %metadata.total_rows, block_index = metadata.block_index, total_blocks = metadata.total_blocks, entity_kind = request.entity_kind.as_str(), "loaded replay entity summary block");
return std::result::Result::Ok(crate::DemoStoreReplayEntityPagePayload {
page: metadata,
rows,
});
}
fn transaction_row_from_store(

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/lib.rs
// version: 47
// version: 50
//! Tauri desktop demo application for `khadhroony-bot3`.
@@ -133,6 +133,8 @@ pub(crate) use self::demo_decode_replay::DemoDecodeReplayRequest;
pub(crate) use self::demo_decode_replay::DemoDecodeReplayRunGuard;
/// Internal demo decode replay item.
pub(crate) use self::demo_decode_replay::DemoDecodeReplaySummaryPayload;
/// Counted Store block of transaction annotations.
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationPagePayload;
/// Internal demo decode replay item.
pub(crate) use self::demo_decode_replay::DemoTransactionAnnotationRequest;
/// Internal demo decode replay item.
@@ -151,16 +153,18 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_options;
pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload;
/// Restores the shared single-execution state flag when a Devnet run finishes.
pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard;
/// Prepares the Store selected by one compatible Devnet profile.
pub(crate) use self::demo_devnet_common::demo_execution_devnet_prepare_profile;
/// Loads the backend-only demo wallet password.
pub(crate) use self::demo_devnet_common::demo_wallet_password;
/// Returns whether the backend-only demo wallet password is configured.
pub(crate) use self::demo_devnet_common::demo_wallet_password_configured;
/// Converts the reusable scenario readiness report to the desktop TS-RS payload.
pub(crate) use self::demo_devnet_common::devnet_store_readiness_payload;
/// Serializes a diagnostic value to indented JSON without panicking.
pub(crate) use self::demo_devnet_common::pretty_json;
/// Resolves the temporary or persistent signer selected by one Devnet profile.
pub(crate) use self::demo_devnet_common::resolve_devnet_execution_wallet;
/// Selects and validates one Devnet profile by name.
pub(crate) use self::demo_devnet_common::select_devnet_profile;
/// Bridges Metadata execution progress to the Metadata desktop window.
pub(crate) use self::demo_execution_metadata::DemoExecutionMetadataObserver;
/// Progress event emitted by Metadata execution workflows.
@@ -237,8 +241,6 @@ pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreRequest;
pub(crate) use self::demo_execution_solana_core::DemoExecutionSolanaCoreSummaryPayload;
/// Maps a confirmation status to its stable UI code.
pub(crate) use self::demo_execution_solana_core::confirmation_status_code;
/// Returns compatible Devnet profiles and conservative defaults.
pub(crate) use self::demo_execution_solana_core::demo_execution_devnet_prepare_profile;
/// Requests cooperative cancellation before the next execution stage.
pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_cancel;
/// Executes one bounded System transfer simulation or Devnet submission.
@@ -251,8 +253,6 @@ pub(crate) use self::demo_execution_solana_core::demo_execution_solana_core_opti
pub(crate) use self::demo_execution_solana_core::demo_execution_spl_memo_execute;
/// Maps an execution cluster to its stable UI code.
pub(crate) use self::demo_execution_solana_core::execution_cluster_code;
/// Selects and validates one Devnet profile by name.
pub(crate) use self::demo_execution_solana_core::select_devnet_profile;
/// Frontend payload for one independent Devnet SPL validation scenario.
pub(crate) use self::demo_execution_spl::DevnetSplValidationScenarioPayload;
/// Returns the complete ordered Devnet SPL validation inventory for milestone 0.4.6.
@@ -283,6 +283,8 @@ pub(crate) use self::demo_spl_ata::DemoExecutionSplAtaSummaryPayload;
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationPayload;
/// Request used to derive the representative wallet ATA before execution.
pub(crate) use self::demo_spl_ata::DemoSplAtaDerivationRequest;
/// One counted Store block of ATA lifecycle journal rows.
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalPagePayload;
/// Bounded ATA lifecycle journal request.
pub(crate) use self::demo_spl_ata::DemoSplAtaJournalRequest;
/// One UI-safe ATA lifecycle journal row.
@@ -297,6 +299,8 @@ pub(crate) use self::demo_spl_ata::demo_spl_ata_journal;
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenRequest;
/// UI-safe result of one checked SPL Token transfer orchestration.
pub(crate) use self::demo_spl_token::DemoExecutionSplTokenSummaryPayload;
/// One counted Store block of classic SPL Token journal rows.
pub(crate) use self::demo_spl_token::DemoSplTokenJournalPagePayload;
/// Bounded exact filters for the classic SPL Token materialized journal.
pub(crate) use self::demo_spl_token::DemoSplTokenJournalRequest;
/// UI-safe materialized classic SPL Token journal row.
@@ -315,16 +319,24 @@ pub(crate) use self::demo_spl_token_2022::DemoSplToken2022FixturePayload;
pub(crate) use self::demo_spl_token_2022::demo_execution_spl_token_2022_execute;
/// Loads public Token-2022 fixture values without reading private key bytes.
pub(crate) use self::demo_spl_token_2022::demo_spl_token_2022_fixture;
/// Fixed Store block size used by desktop listings.
pub(crate) use self::demo_store_common::DEMO_STORE_BLOCK_SIZE;
/// UI-safe Store block pagination metadata.
pub(crate) use self::demo_store_common::DemoStoreBlockMetadata;
/// UI-safe logical store resource diagnostic snapshot.
pub(crate) use self::demo_store_common::DemoStoreResourceSnapshot;
/// Formats diagnostic statuses.
pub(crate) use self::demo_store_common::debug_status;
/// Calculates refreshed Store block metadata for desktop listings.
pub(crate) use self::demo_store_common::demo_store_block_metadata;
/// Builds one fixed-size Store block request for desktop listings.
pub(crate) use self::demo_store_common::demo_store_block_request;
/// Opens and verifies the configured store during startup.
pub(crate) use self::demo_store_common::initialize_store_for_startup;
/// Opens the configured backend-agnostic store.
pub(crate) use self::demo_store_common::open_store;
/// Clones the persistent application store.
pub(crate) use self::demo_store_common::shared_store;
/// Clones the shared persistent store held for one configured profile.
pub(crate) use self::demo_store_common::shared_store_for_profile;
/// Returns the safe connection descriptor exposed by the active store backend.
pub(crate) use self::demo_store_common::store_connection_descriptor;
/// Converts one logical store resource diagnostic.
@@ -343,6 +355,8 @@ pub(crate) use self::demo_store_diag::load_demo_store_diag;
pub(crate) use self::demo_store_raw::DemoStoreRawPayload;
/// Loads read-only raw resource diagnostics from the active store.
pub(crate) use self::demo_store_raw::load_demo_store_raw;
/// Store replay entity page payload.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityPagePayload;
/// Store replay entity request.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityRequest;
/// Store replay entity row.
@@ -351,10 +365,14 @@ pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityRow;
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayKnownProgramOption;
/// Store replay options payload.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayOptionsPayload;
/// Store replay program page payload.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramPagePayload;
/// Store replay program request.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRequest;
/// Store replay program row.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRow;
/// Store replay transaction page payload.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionPagePayload;
/// Store replay transaction request.
pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionRequest;
/// Store replay transaction row.

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/src/tauri.rs
// version: 46
// version: 47
//! Tauri runtime assembly and private command wrappers.
@@ -513,7 +513,7 @@ async fn demo_decode_replay_diagnostics(
async fn demo_decode_replay_annotations(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoTransactionAnnotationRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoTransactionAnnotationRow>, std::string::String> {
) -> std::result::Result<crate::DemoTransactionAnnotationPagePayload, std::string::String> {
return crate::demo_decode_replay_annotations(state, request).await;
}
@@ -605,7 +605,7 @@ async fn demo_store_replay_options(
async fn load_demo_store_replay_transactions(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayTransactionRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayTransactionRow>, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayTransactionPagePayload, std::string::String> {
return crate::load_demo_store_replay_transactions(state, request).await;
}
@@ -613,7 +613,7 @@ async fn load_demo_store_replay_transactions(
async fn load_demo_store_replay_programs(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayProgramRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayProgramRow>, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayProgramPagePayload, std::string::String> {
return crate::load_demo_store_replay_programs(state, request).await;
}
@@ -621,7 +621,7 @@ async fn load_demo_store_replay_programs(
async fn load_demo_store_replay_entities(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoStoreReplayEntityRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoStoreReplayEntityRow>, std::string::String> {
) -> std::result::Result<crate::DemoStoreReplayEntityPagePayload, std::string::String> {
return crate::load_demo_store_replay_entities(state, request).await;
}
@@ -857,7 +857,7 @@ async fn demo_execution_spl_token_execute(
async fn demo_spl_token_journal(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSplTokenJournalRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSplTokenJournalRow>, std::string::String> {
) -> std::result::Result<crate::DemoSplTokenJournalPagePayload, std::string::String> {
return crate::demo_spl_token_journal(state, request).await;
}
@@ -882,7 +882,7 @@ async fn demo_execution_spl_ata_execute(
async fn demo_spl_ata_journal(
state: tauri::State<'_, crate::AppState>,
request: crate::DemoSplAtaJournalRequest,
) -> std::result::Result<std::vec::Vec<crate::DemoSplAtaJournalRow>, std::string::String> {
) -> std::result::Result<crate::DemoSplAtaJournalPagePayload, std::string::String> {
return crate::demo_spl_ata_journal(state, request).await;
}